This commit is contained in:
Mateusz Gruszczyński
2026-09-13 10:21:55 +02:00
parent a7fc1f2dc4
commit c3fbc5ccc6
14 changed files with 152 additions and 42 deletions
+16 -4
View File
@@ -20,13 +20,17 @@ impl GreeClient {
command: &DeviceCommand,
suppress_beep: bool,
) -> Result<Value> {
// Keep legacy behaviour (Quiet=1) until a device proves that it uses another
// active encoding. Once polling observes Quiet=2/3, persist and reuse that value.
// This makes newer/multi-state units work without changing working older units.
let quiet_on_value = device.quiet_wire_value.unwrap_or(1).clamp(1, 3);
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)?;
let inner = Self::command_payload(command, try_buzzer_suppression, quiet_on_value)?;
match self
.request(device, &inner, key, false, device.protocol_version)
.await
@@ -35,7 +39,7 @@ impl GreeClient {
Err(first_err) if try_buzzer_suppression => {
// 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)?;
let fallback = Self::command_payload(command, false, quiet_on_value)?;
match self
.request(device, &fallback, key, false, device.protocol_version)
.await
@@ -159,7 +163,11 @@ impl GreeClient {
}
}
fn command_payload(command: &DeviceCommand, suppress_beep: bool) -> Result<Value> {
fn command_payload(
command: &DeviceCommand,
suppress_beep: bool,
quiet_on_value: u8,
) -> Result<Value> {
let mut opt = Vec::<&str>::new();
let mut values = Vec::<Value>::new();
if let Some(v) = command.power {
@@ -191,7 +199,11 @@ impl GreeClient {
}
if let Some(v) = command.quiet {
opt.push("Quiet");
values.push(json!(if v { 1 } else { 0 }));
values.push(json!(if v {
quiet_on_value.clamp(1, 3)
} else {
0
}));
}
if let Some(v) = command.turbo {
opt.push("Tur");
+1
View File
@@ -199,6 +199,7 @@ impl GreeClient {
swing_vertical: false,
swing_horizontal: false,
quiet: false,
quiet_wire_value: None,
turbo: false,
light: true,
air: false,
+13
View File
@@ -108,6 +108,19 @@ fn status_flag(name: &str, value: &Value) -> Result<bool> {
other => bail!("invalid GREE flag value for {name}: {other}"),
}
}
/// Optional GREE capabilities are not uniformly encoded as strict 0/1 flags.
/// For example Quiet is known to use 1, 2 and 3 on different units/firmware,
/// and other optional properties can also expose multi-state non-zero values.
/// The public application model is boolean, so keep 0 = off and treat any
/// positive value as active without allowing one optional field to invalidate
/// the complete climate status frame.
fn status_feature_flag(name: &str, value: &Value) -> Result<bool> {
let raw = status_i64(name, value)?;
if raw < 0 {
bail!("invalid GREE feature value for {name}: {raw}")
}
Ok(raw != 0)
}
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() {
+17 -7
View File
@@ -196,31 +196,41 @@ impl GreeClient {
"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)?;
let raw = status_i64(name, value)?;
if raw < 0 {
bail!("invalid GREE feature value for {name}: {raw}")
}
next.quiet = raw != 0;
// Quiet is a multi-state field on some GREE families. Preserve known
// active encodings so a later boolean "enable Quiet" command does not
// silently change mode (e.g. a unit that reports/uses Quiet=2).
if (1..=3).contains(&raw) {
next.quiet_wire_value = Some(raw as u8);
}
next.supports_quiet = Some(true);
}
"Tur" => {
next.turbo = status_flag(name, value)?;
next.turbo = status_feature_flag(name, value)?;
next.supports_turbo = Some(true);
}
"Lig" => {
next.light = status_flag(name, value)?;
next.light = status_feature_flag(name, value)?;
next.supports_light = Some(true);
}
"Air" => {
next.air = status_flag(name, value)?;
next.air = status_feature_flag(name, value)?;
next.supports_air = Some(true);
}
"Blo" => {
next.xfan = status_flag(name, value)?;
next.xfan = status_feature_flag(name, value)?;
next.supports_xfan = Some(true);
}
"Health" => {
next.health = status_flag(name, value)?;
next.health = status_feature_flag(name, value)?;
next.supports_health = Some(true);
}
"SwhSlp" => {
next.sleep = status_flag(name, value)?;
next.sleep = status_feature_flag(name, value)?;
next.supports_sleep = Some(true);
}
"TemSen" => {
+62
View File
@@ -37,6 +37,7 @@ mod tests {
..DeviceCommand::default()
},
false,
1,
)
.expect("thermostat command payload");
@@ -49,4 +50,65 @@ mod tests {
Some(serde_json::json!([19, 1, 1, 1]))
);
}
#[test]
fn quiet_status_accepts_multistate_value_and_remembers_wire_mode() {
let client = GreeClient::new(
"test-controller".into(),
None,
None,
Arc::new(AtomicBool::new(false)),
);
let mut device = Device::simulated_default();
device.quiet = false;
device.quiet_wire_value = None;
let response = json!({
"cols": ["Pow", "Quiet"],
"dat": [1, 2]
});
client.apply_status(&mut device, &response).expect("Quiet=2 status");
assert!(device.power);
assert!(device.quiet);
assert_eq!(device.quiet_wire_value, Some(2));
assert_eq!(device.supports_quiet, Some(true));
}
#[test]
fn quiet_command_can_preserve_learned_wire_mode() {
let payload = GreeClient::command_payload(
&DeviceCommand {
quiet: Some(true),
..DeviceCommand::default()
},
false,
2,
)
.expect("quiet command payload");
assert_eq!(payload.get("opt").cloned(), Some(serde_json::json!(["Quiet"])));
assert_eq!(payload.get("p").cloned(), Some(serde_json::json!([2])));
}
#[test]
fn optional_multistate_feature_does_not_reject_whole_status() {
let client = GreeClient::new(
"test-controller".into(),
None,
None,
Arc::new(AtomicBool::new(false)),
);
let mut device = Device::simulated_default();
let response = json!({
"cols": ["Pow", "Air"],
"dat": [1, 3]
});
client.apply_status(&mut device, &response).expect("Air=3 status");
assert!(device.power);
assert!(device.air);
assert_eq!(device.supports_air, Some(true));
}
}