This commit is contained in:
Mateusz Gruszczyński
2026-08-24 14:46:57 +02:00
parent 085431efb8
commit 4f70e36e89
14 changed files with 488 additions and 51 deletions
+65 -5
View File
@@ -23,6 +23,7 @@ pub struct GreeClient {
debug_events: Option<broadcast::Sender<ApiEvent>>,
debug_gree_frames: Arc<AtomicBool>,
buzzer_unsupported: Arc<Mutex<HashSet<String>>>,
quiet_unsupported: Arc<Mutex<HashSet<String>>>,
}
impl GreeClient {
@@ -38,6 +39,7 @@ impl GreeClient {
debug_events,
debug_gree_frames,
buzzer_unsupported: Arc::new(Mutex::new(HashSet::new())),
quiet_unsupported: Arc::new(Mutex::new(HashSet::new())),
}
}
@@ -414,17 +416,25 @@ impl GreeClient {
Ok(())
}
pub async fn command(&self, device: &Device, command: &DeviceCommand, suppress_beep: bool) -> Result<Value> {
let key = device.key.as_deref().ok_or_else(|| anyhow!("device is not bound"))?;
pub fn quiet_command_supported(&self, device_id: &str) -> bool {
self.quiet_unsupported.lock().map(|items| !items.contains(device_id)).unwrap_or(true)
}
async fn request_command_with_buzzer_fallback(
&self,
device: &Device,
key: &str,
command: &DeviceCommand,
suppress_beep: bool,
) -> Result<Value> {
let try_buzzer_suppression = suppress_beep
&& self.buzzer_unsupported.lock().map(|items| !items.contains(&device.id)).unwrap_or(true);
let inner = Self::command_payload(command, try_buzzer_suppression)?;
match self.request(device, &inner, key, false, device.protocol_version).await {
Ok(value) => Ok(value),
Err(first_err) if try_buzzer_suppression => {
// Some firmwares reject unknown command properties instead of ignoring them.
// Retry the exact state change without buzzer fields; only remember the device
// as incompatible after that fallback succeeds.
// Some firmwares reject unknown buzzer properties instead of ignoring them.
// Retry the exact state change without buzzer fields and remember the fallback.
let fallback = Self::command_payload(command, false)?;
match self.request(device, &fallback, key, false, device.protocol_version).await {
Ok(value) => {
@@ -439,6 +449,38 @@ impl GreeClient {
}
}
pub async fn command(&self, device: &Device, command: &DeviceCommand, suppress_beep: bool) -> Result<DeviceCommand> {
let key = device.key.as_deref().ok_or_else(|| anyhow!("device is not bound"))?;
let mut effective = command.clone();
if effective.quiet.is_some() && !self.quiet_command_supported(&device.id) {
effective.quiet = 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(_) => Err(first_err),
}
}
Err(err) => Err(err),
}
}
fn command_payload(command: &DeviceCommand, suppress_beep: bool) -> Result<Value> {
let mut opt = Vec::<&str>::new();
let mut values = Vec::<Value>::new();
@@ -683,3 +725,21 @@ pub fn merge_discovered(existing: Option<Device>, discovered: Device) -> Device
new
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thermostat_standby_setpoint_low_fan_and_quiet_share_one_frame() {
let payload = GreeClient::command_payload(&DeviceCommand {
target_temperature: Some(19.0),
fan_speed: Some(1),
quiet: 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])));
}
}