Files
gree-controller/src/protocol/gree/core.rs
T
2026-08-30 13:39:29 +02:00

125 lines
5.2 KiB
Rust

impl GreeClient {
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,
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<String, u64>) {
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<Ipv4Addr>) -> Result<UdpSocket> {
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<SocketAddr> {
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<SocketAddr> {
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::<u16>().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")
}
}