impl GreeClient { pub fn new( controller_id: String, interface: Option, debug_events: Option>, debug_gree_frames: Arc, ) -> Self { Self { controller_id, interface, debug_events, debug_gree_frames, received_frames_total: Arc::new(AtomicU64::new(0)), received_frames_by_device: Arc::new(Mutex::new(HashMap::new())), buzzer_unsupported: Arc::new(Mutex::new(HashSet::new())), quiet_unsupported: Arc::new(Mutex::new(HashSet::new())), sleep_unsupported: Arc::new(Mutex::new(HashSet::new())), } } pub fn received_frame_stats(&self) -> (u64, HashMap) { let total = self.received_frames_total.load(Ordering::Relaxed); let by_device = self.received_frames_by_device.lock() .map(|counts| counts.clone()) .unwrap_or_default(); (total, by_device) } fn record_received_frame(&self, device: &Device) { let total = self.received_frames_total.fetch_add(1, Ordering::Relaxed).saturating_add(1); let device_count = self.received_frames_by_device.lock().ok().map(|mut counts| { let count = counts.entry(device.id.clone()).or_insert(0); *count = (*count).saturating_add(1); *count }).unwrap_or(0); if let Some(events) = &self.debug_events { let _ = events.send(ApiEvent { event: "gree.frame_received".into(), timestamp: Utc::now(), data: json!({ "device_id": device.id, "device_name": device.name, "total": total, "device_count": device_count, }), }); } } 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) -> Result { let socket = if let Some(interface) = self.interface.as_deref() { let ip = interface_ipv4(interface)?; UdpSocket::bind(SocketAddrV4::new(ip, 0)).await .with_context(|| format!("cannot bind GREE UDP socket to {ip} from interface {interface}"))? } else if let Some(target) = target_hint { if let Some(config) = local_ipv4_config_for_target(target)? { tracing::debug!( target = %target, interface = %config.interface, local_ip = %config.ip, "Automatically selected local interface for GREE UDP" ); UdpSocket::bind(SocketAddrV4::new(config.ip, 0)).await .with_context(|| format!("cannot bind GREE UDP socket to {} on {}", config.ip, config.interface))? } else { UdpSocket::bind("0.0.0.0:0").await? } } else { UdpSocket::bind("0.0.0.0:0").await? }; socket.set_broadcast(broadcast)?; Ok(socket) } fn bind_scan_target(&self, target: SocketAddr) -> Result { let SocketAddr::V4(target_v4) = target else { return Ok(target); }; let broadcast = if let Some(interface) = self.interface.as_deref() { let (_, broadcast) = interface_ipv4_config(interface)?; Some(broadcast) } else { local_ipv4_config_for_target(*target_v4.ip())?.map(|config| config.broadcast) }; Ok(broadcast .map(|ip| SocketAddr::V4(SocketAddrV4::new(ip, target_v4.port()))) .unwrap_or(target)) } fn discovery_target(&self, configured: &str) -> Result { let value = configured.trim(); if value.eq_ignore_ascii_case("auto") || value.to_ascii_lowercase().starts_with("auto:") { let port = value.split_once(':') .map(|(_, port)| port.parse::().context("invalid automatic discovery port")) .transpose()? .unwrap_or(7000); let interface = self.interface.as_deref() .ok_or_else(|| anyhow!("automatic discovery broadcast requires GREE_CONTROLLER_GREE_INTERFACE"))?; let (_, broadcast) = interface_ipv4_config(interface)?; return Ok(SocketAddr::V4(SocketAddrV4::new(broadcast, port))); } value.parse().context("invalid discovery broadcast address") } }