v0.13.10
This commit is contained in:
@@ -112,6 +112,7 @@ async fn add_device(
|
||||
swing_vertical: false,
|
||||
swing_horizontal: false,
|
||||
quiet: false,
|
||||
quiet_wire_value: None,
|
||||
turbo: false,
|
||||
light: true,
|
||||
air: false,
|
||||
|
||||
@@ -34,6 +34,14 @@ pub struct Device {
|
||||
pub swing_horizontal: bool,
|
||||
#[serde(default)]
|
||||
pub quiet: bool,
|
||||
/// Raw non-zero value last reported by the unit for the GREE `Quiet` property.
|
||||
///
|
||||
/// Most of the application intentionally exposes Quiet as a boolean, but the wire
|
||||
/// protocol is not consistently boolean across GREE firmware. Known units report
|
||||
/// values 1, 2 or 3 for active Quiet modes. Remembering the observed value lets us
|
||||
/// preserve the device-specific encoding when a later command enables Quiet again.
|
||||
#[serde(default)]
|
||||
pub quiet_wire_value: Option<u8>,
|
||||
#[serde(default)]
|
||||
pub turbo: bool,
|
||||
#[serde(default)]
|
||||
@@ -106,6 +114,7 @@ impl Device {
|
||||
swing_vertical: false,
|
||||
swing_horizontal: false,
|
||||
quiet: false,
|
||||
quiet_wire_value: None,
|
||||
turbo: false,
|
||||
light: true,
|
||||
air: false,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -199,6 +199,7 @@ impl GreeClient {
|
||||
swing_vertical: false,
|
||||
swing_horizontal: false,
|
||||
quiet: false,
|
||||
quiet_wire_value: None,
|
||||
turbo: false,
|
||||
light: true,
|
||||
air: false,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user