v0.5.0
This commit is contained in:
+73
-8
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user