This commit is contained in:
Mateusz Gruszczyński
2026-08-24 14:05:43 +02:00
parent eb02f66056
commit 04fc91b9f4
30 changed files with 2257 additions and 175 deletions
+73 -8
View File
@@ -1,10 +1,10 @@
use std::{collections::HashSet, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, time::Duration};
use std::{collections::HashSet, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}, time::Duration};
use anyhow::{anyhow, bail, Context, Result};
use chrono::Utc;
use serde_json::{json, Value};
use tokio::{net::UdpSocket, time::{timeout, Instant}};
use tokio::{net::UdpSocket, sync::broadcast, time::{timeout, Instant}};
use uuid::Uuid;
use crate::models::{Device, DeviceCommand};
use crate::models::{ApiEvent, Device, DeviceCommand};
use super::crypto::{
decrypt_v1, decrypt_v2, encrypt_v1, encrypt_v2,
GENERIC_GREE_V1_KEY, GENERIC_GREE_V2_KEY,
@@ -20,11 +20,46 @@ pub struct BindResult {
pub struct GreeClient {
controller_id: String,
interface: Option<String>,
debug_events: Option<broadcast::Sender<ApiEvent>>,
debug_gree_frames: Arc<AtomicBool>,
buzzer_unsupported: Arc<Mutex<HashSet<String>>>,
}
impl GreeClient {
pub fn new(controller_id: String, interface: Option<String>) -> Self {
Self { controller_id, interface }
pub fn new(
controller_id: String,
interface: Option<String>,
debug_events: Option<broadcast::Sender<ApiEvent>>,
debug_gree_frames: Arc<AtomicBool>,
) -> Self {
Self {
controller_id,
interface,
debug_events,
debug_gree_frames,
buzzer_unsupported: Arc::new(Mutex::new(HashSet::new())),
}
}
fn debug_frame(&self, direction: &str, device: &Device, target: SocketAddr, protocol: u8, payload: &Value) {
if !self.debug_gree_frames.load(Ordering::Relaxed) { return; }
let Some(events) = &self.debug_events else { return; };
let mut safe = payload.clone();
if let Some(object) = safe.as_object_mut() {
if object.contains_key("key") { object.insert("key".into(), json!("***")); }
}
let _ = events.send(ApiEvent {
event: "gree.frame".into(),
timestamp: Utc::now(),
data: json!({
"direction": direction,
"device_id": device.id,
"device_name": device.name,
"target": target.to_string(),
"protocol_version": protocol,
"payload": safe,
}),
});
}
async fn udp_socket(&self, broadcast: bool, target_hint: Option<Ipv4Addr>) -> Result<UdpSocket> {
@@ -379,8 +414,32 @@ impl GreeClient {
Ok(())
}
pub async fn command(&self, device: &Device, command: &DeviceCommand) -> Result<Value> {
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"))?;
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.
let fallback = Self::command_payload(command, false)?;
match self.request(device, &fallback, key, false, device.protocol_version).await {
Ok(value) => {
if let Ok(mut items) = self.buzzer_unsupported.lock() { items.insert(device.id.clone()); }
tracing::warn!(device=%device.id, "GREE buzzer suppression is unsupported; using normal command frames for this device");
Ok(value)
}
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();
if let Some(v) = command.power { opt.push("Pow"); values.push(json!(if v { 1 } else { 0 })); }
@@ -398,8 +457,11 @@ impl GreeClient {
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 opt.is_empty() { bail!("empty device command") }
let inner = json!({"opt": opt, "p": values, "t": "cmd"});
self.request(device, &inner, key, false, device.protocol_version).await
if suppress_beep {
opt.push("Buzzer_ON_OFF"); values.push(json!(1));
opt.push("BuzzerCtrl"); values.push(json!(0));
}
Ok(json!({"opt": opt, "p": values, "t": "cmd"}))
}
async fn request(&self, device: &Device, inner: &Value, key: &str, binding: bool, protocol_version: u8) -> Result<Value> {
@@ -430,6 +492,7 @@ impl GreeClient {
}
let payload = serde_json::to_vec(&outer)?;
tracing::debug!(target=%target, local=%socket.local_addr()?, protocol=version, wire_mac=%wire_mac, interface=%self.interface.as_deref().unwrap_or("auto"), binding, "Sending GREE request");
self.debug_frame("tx", device, target, version, inner);
socket.send_to(&payload, target).await?;
let deadline = Instant::now() + Duration::from_secs(4);
@@ -458,6 +521,7 @@ impl GreeClient {
}
}
if let Some(err) = decoded.get("err").filter(|v| !v.is_null()) { bail!("GREE device error: {err}") }
self.debug_frame("rx", device, target, version, &decoded);
return Ok(decoded);
}
let Some(pack) = response.get("pack").and_then(Value::as_str) else { continue; };
@@ -485,6 +549,7 @@ impl GreeClient {
if !response_type.eq_ignore_ascii_case("bindok") { continue; }
}
if let Some(err) = decoded.get("err").filter(|v| !v.is_null()) { bail!("GREE device error: {err}") }
self.debug_frame("rx", device, target, version, &decoded);
return Ok(decoded);
}
if let Some(err) = last_decode_error { return Err(err); }