v0.7.4
This commit is contained in:
+86
-29
@@ -436,48 +436,66 @@ impl GreeClient {
|
||||
.ok_or_else(|| anyhow!("status response has no cols"))?;
|
||||
let data = response.get("dat").and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("status response has no dat"))?;
|
||||
if data.len() < response_cols.len() {
|
||||
bail!("status response contains fewer values than columns")
|
||||
}
|
||||
|
||||
// Parse into a clone and commit only when every climate-relevant value is valid.
|
||||
// This prevents null/text/malformed frames from being silently converted into OFF,
|
||||
// AUTO or a zero setpoint while leaving the rest of the packet partially applied.
|
||||
let mut next = device.clone();
|
||||
let mut set_temp = None;
|
||||
for (name, value) in response_cols.iter().zip(data.iter()) {
|
||||
let Some(name) = name.as_str() else { continue; };
|
||||
match name {
|
||||
"Pow" => device.power = value_as_i64(value) != 0,
|
||||
"Mod" => device.mode = mode_name(value_as_i64(value)).into(),
|
||||
"SetTem" => set_temp = Some(value_as_f64(value)),
|
||||
"WdSpd" => device.fan_speed = value_as_i64(value).clamp(0, 5) as u8,
|
||||
"SwUpDn" => device.swing_vertical = value_as_i64(value) != 0,
|
||||
"SwingLfRig" => device.swing_horizontal = value_as_i64(value) != 0,
|
||||
"Quiet" => { device.quiet = value_as_i64(value) != 0; device.supports_quiet = Some(true); },
|
||||
"Tur" => { device.turbo = value_as_i64(value) != 0; device.supports_turbo = Some(true); },
|
||||
"Lig" => { device.light = value_as_i64(value) != 0; device.supports_light = Some(true); },
|
||||
"Air" => { device.air = value_as_i64(value) != 0; device.supports_air = Some(true); },
|
||||
"Blo" => { device.xfan = value_as_i64(value) != 0; device.supports_xfan = Some(true); },
|
||||
"Health" => { device.health = value_as_i64(value) != 0; device.supports_health = Some(true); },
|
||||
"SwhSlp" => { device.sleep = value_as_i64(value) != 0; device.supports_sleep = Some(true); },
|
||||
"Pow" => next.power = status_flag(name, value)?,
|
||||
"Mod" => {
|
||||
let raw = status_i64(name, value)?;
|
||||
next.mode = mode_name_checked(raw).ok_or_else(|| anyhow!("invalid GREE mode value for {name}: {raw}"))?.into();
|
||||
}
|
||||
"SetTem" => {
|
||||
let raw = status_f64(name, value)?;
|
||||
if !(8.0..=30.0).contains(&raw) { bail!("invalid GREE setpoint for {name}: {raw}") }
|
||||
set_temp = Some(raw.round());
|
||||
}
|
||||
"WdSpd" => {
|
||||
let raw = status_i64(name, value)?;
|
||||
if !(0..=5).contains(&raw) { bail!("invalid GREE fan value for {name}: {raw}") }
|
||||
next.fan_speed = raw as u8;
|
||||
}
|
||||
"SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0,
|
||||
"SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0,
|
||||
"Quiet" => { next.quiet = status_flag(name, value)?; next.supports_quiet = Some(true); },
|
||||
"Tur" => { next.turbo = status_flag(name, value)?; next.supports_turbo = Some(true); },
|
||||
"Lig" => { next.light = status_flag(name, value)?; next.supports_light = Some(true); },
|
||||
"Air" => { next.air = status_flag(name, value)?; next.supports_air = Some(true); },
|
||||
"Blo" => { next.xfan = status_flag(name, value)?; next.supports_xfan = Some(true); },
|
||||
"Health" => { next.health = status_flag(name, value)?; next.supports_health = Some(true); },
|
||||
"SwhSlp" => { next.sleep = status_flag(name, value)?; next.supports_sleep = Some(true); },
|
||||
"TemSen" => {
|
||||
let raw = value_as_f64(value);
|
||||
// The room sensor is a useful discriminator because normal indoor
|
||||
// temperatures cannot exceed 40 C in controller operation. Persist
|
||||
// the detected wire format and reuse it for OutEnvTem, including
|
||||
// sub-zero outdoor values encoded as (temperature + 40).
|
||||
let raw = status_f64(name, value)?;
|
||||
if raw != 0.0 {
|
||||
let offset = raw > 40.0;
|
||||
device.temperature_sensor_offset = Some(offset);
|
||||
device.current_temperature = Some(if offset { raw - 40.0 } else { raw });
|
||||
let temperature = if offset { raw - 40.0 } else { raw };
|
||||
if !(-40.0..=80.0).contains(&temperature) { bail!("invalid GREE indoor temperature: {temperature}") }
|
||||
next.temperature_sensor_offset = Some(offset);
|
||||
next.current_temperature = Some(temperature);
|
||||
}
|
||||
}
|
||||
"OutEnvTem" => {
|
||||
let raw = value_as_f64(value);
|
||||
let raw = status_f64(name, value)?;
|
||||
if raw != 0.0 {
|
||||
let offset = device.temperature_sensor_offset.unwrap_or(raw > 50.0);
|
||||
device.outdoor_temperature = Some(if offset { raw - 40.0 } else { raw });
|
||||
let offset = next.temperature_sensor_offset.unwrap_or(raw > 50.0);
|
||||
let temperature = if offset { raw - 40.0 } else { raw };
|
||||
if !(-60.0..=80.0).contains(&temperature) { bail!("invalid GREE outdoor temperature: {temperature}") }
|
||||
next.outdoor_temperature = Some(temperature);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(base) = set_temp {
|
||||
device.target_temperature = base.clamp(8.0, 30.0);
|
||||
}
|
||||
if let Some(base) = set_temp { next.target_temperature = base; }
|
||||
*device = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -792,9 +810,24 @@ fn interface_ipv4_config(interface: &str) -> Result<(Ipv4Addr, Ipv4Addr)> {
|
||||
}
|
||||
|
||||
fn interface_ipv4(interface: &str) -> Result<Ipv4Addr> { interface_ipv4_config(interface).map(|(ip, _)| ip) }
|
||||
fn value_as_i64(value: &Value) -> i64 { value.as_i64().or_else(|| value.as_str()?.parse().ok()).unwrap_or_default() }
|
||||
fn value_as_f64(value: &Value) -> f64 { value.as_f64().or_else(|| value.as_str()?.parse().ok()).unwrap_or_default() }
|
||||
fn mode_name(value: i64) -> &'static str { match value { 0 => "auto", 1 => "cool", 2 => "dry", 3 => "fan", 4 => "heat", _ => "auto" } }
|
||||
fn value_as_i64(value: &Value) -> Option<i64> { value.as_i64().or_else(|| value.as_str()?.trim().parse().ok()) }
|
||||
fn value_as_f64(value: &Value) -> Option<f64> {
|
||||
value.as_f64().or_else(|| value.as_str()?.trim().parse().ok()).filter(|value| value.is_finite())
|
||||
}
|
||||
fn status_i64(name: &str, value: &Value) -> Result<i64> {
|
||||
value_as_i64(value).ok_or_else(|| anyhow!("invalid GREE integer value for {name}: {value}"))
|
||||
}
|
||||
fn status_f64(name: &str, value: &Value) -> Result<f64> {
|
||||
value_as_f64(value).ok_or_else(|| anyhow!("invalid GREE numeric value for {name}: {value}"))
|
||||
}
|
||||
fn status_flag(name: &str, value: &Value) -> Result<bool> {
|
||||
match status_i64(name, value)? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
other => bail!("invalid GREE flag value for {name}: {other}"),
|
||||
}
|
||||
}
|
||||
fn mode_name_checked(value: i64) -> Option<&'static str> { match value { 0 => Some("auto"), 1 => Some("cool"), 2 => Some("dry"), 3 => Some("fan"), 4 => Some("heat"), _ => None } }
|
||||
fn mode_value(value: &str) -> Result<i64> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"auto" => Ok(0), "cool" => Ok(1), "dry" => Ok(2), "fan" => Ok(3), "heat" => Ok(4),
|
||||
@@ -830,6 +863,30 @@ pub fn merge_discovered(existing: Option<Device>, discovered: Device) -> Device
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn invalid_status_frame_does_not_partially_mutate_device() {
|
||||
let client = GreeClient::new(
|
||||
"test-controller".into(),
|
||||
None,
|
||||
None,
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
);
|
||||
let mut device = Device::simulated_default();
|
||||
device.power = true;
|
||||
device.mode = "heat".into();
|
||||
device.target_temperature = 24.0;
|
||||
let before = device.clone();
|
||||
|
||||
let response = json!({
|
||||
"cols": ["Pow", "Mod", "SetTem"],
|
||||
"dat": [0, null, "not-a-number"]
|
||||
});
|
||||
assert!(client.apply_status(&mut device, &response).is_err());
|
||||
assert_eq!(device.power, before.power);
|
||||
assert_eq!(device.mode, before.mode);
|
||||
assert_eq!(device.target_temperature, before.target_temperature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermostat_standby_setpoint_low_fan_quiet_and_sleep_share_one_frame() {
|
||||
let payload = GreeClient::command_payload(&DeviceCommand {
|
||||
|
||||
Reference in New Issue
Block a user