v0.5.4
This commit is contained in:
+120
-20
@@ -24,6 +24,7 @@ pub struct GreeClient {
|
||||
debug_gree_frames: Arc<AtomicBool>,
|
||||
buzzer_unsupported: Arc<Mutex<HashSet<String>>>,
|
||||
quiet_unsupported: Arc<Mutex<HashSet<String>>>,
|
||||
sleep_unsupported: Arc<Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl GreeClient {
|
||||
@@ -40,6 +41,7 @@ impl GreeClient {
|
||||
debug_gree_frames,
|
||||
buzzer_unsupported: Arc::new(Mutex::new(HashSet::new())),
|
||||
quiet_unsupported: Arc::new(Mutex::new(HashSet::new())),
|
||||
sleep_unsupported: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +254,17 @@ impl GreeClient {
|
||||
quiet: false,
|
||||
turbo: false,
|
||||
light: true,
|
||||
air: false,
|
||||
xfan: false,
|
||||
health: false,
|
||||
sleep: false,
|
||||
supports_light: None,
|
||||
supports_quiet: None,
|
||||
supports_turbo: None,
|
||||
supports_air: None,
|
||||
supports_xfan: None,
|
||||
supports_health: None,
|
||||
supports_sleep: None,
|
||||
current_temperature: None,
|
||||
outdoor_temperature: None,
|
||||
temperature_sensor_offset: None,
|
||||
@@ -357,6 +370,10 @@ impl GreeClient {
|
||||
Err(err) => tracing::trace!(device=%device.id, error=?err, "GREE outdoor temperature is not available"),
|
||||
}
|
||||
}
|
||||
// Capability discovery is deliberately lazy. Existing installations start with
|
||||
// unknown support flags and each optional property is probed at most until a
|
||||
// definitive success/failure has been persisted with the device state.
|
||||
self.probe_optional_features(device, &key).await;
|
||||
device.online = true;
|
||||
device.communication_failures = 0;
|
||||
device.last_seen = Some(Utc::now());
|
||||
@@ -370,6 +387,49 @@ impl GreeClient {
|
||||
self.request(device, &inner, key, false, device.protocol_version).await
|
||||
}
|
||||
|
||||
async fn probe_optional_features(&self, device: &mut Device, key: &str) {
|
||||
let probes = [
|
||||
("Lig", device.supports_light.is_none()),
|
||||
("Quiet", device.supports_quiet.is_none()),
|
||||
("Tur", device.supports_turbo.is_none()),
|
||||
("Air", device.supports_air.is_none()),
|
||||
("Blo", device.supports_xfan.is_none()),
|
||||
("Health", device.supports_health.is_none()),
|
||||
("SwhSlp", device.supports_sleep.is_none()),
|
||||
];
|
||||
for (property, needed) in probes {
|
||||
if !needed { continue; }
|
||||
match self.status_request(device, key, &[property]).await {
|
||||
Ok(value) => {
|
||||
let returned = value.get("cols").and_then(Value::as_array)
|
||||
.map(|cols| cols.iter().any(|name| name.as_str() == Some(property)))
|
||||
.unwrap_or(false);
|
||||
if !returned || self.apply_status(device, &value).is_err() {
|
||||
Self::set_feature_support(device, property, false);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
Self::set_feature_support(device, property, false);
|
||||
tracing::trace!(device=%device.id, property, error=?err, "optional GREE feature is not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_feature_support(device: &mut Device, property: &str, supported: bool) {
|
||||
let value = Some(supported);
|
||||
match property {
|
||||
"Lig" => device.supports_light = value,
|
||||
"Quiet" => device.supports_quiet = value,
|
||||
"Tur" => device.supports_turbo = value,
|
||||
"Air" => device.supports_air = value,
|
||||
"Blo" => device.supports_xfan = value,
|
||||
"Health" => device.supports_health = value,
|
||||
"SwhSlp" => device.supports_sleep = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_status(&self, device: &mut Device, response: &Value) -> Result<()> {
|
||||
let response_cols = response.get("cols").and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("status response has no cols"))?;
|
||||
@@ -385,9 +445,13 @@ impl GreeClient {
|
||||
"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,
|
||||
"Tur" => device.turbo = value_as_i64(value) != 0,
|
||||
"Lig" => device.light = 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); },
|
||||
"TemSen" => {
|
||||
let raw = value_as_f64(value);
|
||||
// The room sensor is a useful discriminator because normal indoor
|
||||
@@ -420,6 +484,10 @@ impl GreeClient {
|
||||
self.quiet_unsupported.lock().map(|items| !items.contains(device_id)).unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn sleep_command_supported(&self, device_id: &str) -> bool {
|
||||
self.sleep_unsupported.lock().map(|items| !items.contains(device_id)).unwrap_or(true)
|
||||
}
|
||||
|
||||
async fn request_command_with_buzzer_fallback(
|
||||
&self,
|
||||
device: &Device,
|
||||
@@ -455,29 +523,56 @@ impl GreeClient {
|
||||
if effective.quiet.is_some() && !self.quiet_command_supported(&device.id) {
|
||||
effective.quiet = None;
|
||||
}
|
||||
if effective.sleep.is_some() && !self.sleep_command_supported(&device.id) {
|
||||
effective.sleep = None;
|
||||
}
|
||||
if effective.is_empty() {
|
||||
return Ok(effective);
|
||||
}
|
||||
|
||||
match self.request_command_with_buzzer_fallback(device, key, &effective, suppress_beep).await {
|
||||
Ok(_) => Ok(effective),
|
||||
Err(first_err) if effective.quiet.is_some() => {
|
||||
// Quiet is not implemented by every GREE indoor unit. If a combined thermostat
|
||||
// command is rejected, retry the same setpoint/fan change without Quiet. Once
|
||||
// that succeeds, remember the unit so future thermostat frames omit Quiet.
|
||||
let mut fallback = effective.clone();
|
||||
fallback.quiet = None;
|
||||
if fallback.is_empty() { return Err(first_err); }
|
||||
match self.request_command_with_buzzer_fallback(device, key, &fallback, suppress_beep).await {
|
||||
Ok(_) => {
|
||||
if let Ok(mut items) = self.quiet_unsupported.lock() { items.insert(device.id.clone()); }
|
||||
tracing::warn!(device=%device.id, "GREE Quiet command is unsupported; thermostat will use Low fan without Quiet for this device");
|
||||
Ok(fallback)
|
||||
Err(first_err) => {
|
||||
// Quiet and native Sleep are optional GREE features. A unit may report a
|
||||
// broader status schema than it accepts in command frames, so preserve
|
||||
// the actual thermostat change and retry without the optional property.
|
||||
if effective.sleep.is_some() {
|
||||
let mut fallback = effective.clone();
|
||||
fallback.sleep = None;
|
||||
if !fallback.is_empty() {
|
||||
if self.request_command_with_buzzer_fallback(device, key, &fallback, suppress_beep).await.is_ok() {
|
||||
if let Ok(mut items) = self.sleep_unsupported.lock() { items.insert(device.id.clone()); }
|
||||
tracing::warn!(device=%device.id, "GREE native Sleep command is unsupported; night mode will continue without Sleep for this device");
|
||||
return Ok(fallback);
|
||||
}
|
||||
}
|
||||
Err(_) => Err(first_err),
|
||||
}
|
||||
if effective.quiet.is_some() {
|
||||
let mut fallback = effective.clone();
|
||||
fallback.quiet = None;
|
||||
if !fallback.is_empty() {
|
||||
if self.request_command_with_buzzer_fallback(device, key, &fallback, suppress_beep).await.is_ok() {
|
||||
if let Ok(mut items) = self.quiet_unsupported.lock() { items.insert(device.id.clone()); }
|
||||
tracing::warn!(device=%device.id, "GREE Quiet command is unsupported; thermostat will continue without Quiet for this device");
|
||||
return Ok(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
if effective.sleep.is_some() && effective.quiet.is_some() {
|
||||
let mut fallback = effective.clone();
|
||||
fallback.sleep = None;
|
||||
fallback.quiet = None;
|
||||
if !fallback.is_empty() {
|
||||
if self.request_command_with_buzzer_fallback(device, key, &fallback, suppress_beep).await.is_ok() {
|
||||
if let Ok(mut items) = self.sleep_unsupported.lock() { items.insert(device.id.clone()); }
|
||||
if let Ok(mut items) = self.quiet_unsupported.lock() { items.insert(device.id.clone()); }
|
||||
tracing::warn!(device=%device.id, "GREE Quiet/Sleep optional command fields are unsupported; using the core thermostat command");
|
||||
return Ok(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(first_err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +593,10 @@ impl GreeClient {
|
||||
if let Some(v) = command.quiet { opt.push("Quiet"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.turbo { opt.push("Tur"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.light { opt.push("Lig"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.air { opt.push("Air"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.xfan { opt.push("Blo"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.health { opt.push("Health"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if let Some(v) = command.sleep { opt.push("SwhSlp"); values.push(json!(if v { 1 } else { 0 })); }
|
||||
if opt.is_empty() { bail!("empty device command") }
|
||||
if suppress_beep {
|
||||
opt.push("Buzzer_ON_OFF"); values.push(json!(1));
|
||||
@@ -731,15 +830,16 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn thermostat_standby_setpoint_low_fan_and_quiet_share_one_frame() {
|
||||
fn thermostat_standby_setpoint_low_fan_quiet_and_sleep_share_one_frame() {
|
||||
let payload = GreeClient::command_payload(&DeviceCommand {
|
||||
target_temperature: Some(19.0),
|
||||
fan_speed: Some(1),
|
||||
quiet: Some(true),
|
||||
sleep: Some(true),
|
||||
..DeviceCommand::default()
|
||||
}, false).expect("thermostat command payload");
|
||||
|
||||
assert_eq!(payload.get("opt").cloned(), Some(serde_json::json!(["SetTem", "WdSpd", "Quiet"])));
|
||||
assert_eq!(payload.get("p").cloned(), Some(serde_json::json!([19, 1, 1])));
|
||||
assert_eq!(payload.get("opt").cloned(), Some(serde_json::json!(["SetTem", "WdSpd", "Quiet", "SwhSlp"])));
|
||||
assert_eq!(payload.get("p").cloned(), Some(serde_json::json!([19, 1, 1, 1])));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user