impl GreeClient { /// protocol_filter: 0=auto/both, 1=ECB only, 2=GCM only. pub async fn discover( &self, broadcast: &str, duration: Duration, protocol_filter: u8, passes: u8, ) -> Result> { let target = self.discovery_target(broadcast)?; let target_hint = match target { SocketAddr::V4(addr) => Some(*addr.ip()), SocketAddr::V6(_) => None, }; let socket = self.udp_socket(true, target_hint).await?; let local = socket.local_addr()?; let passes = passes.clamp(1, 10); tracing::info!( target = %target, local = %local, interface = %self.interface.as_deref().unwrap_or("auto"), protocol = protocol_filter, passes, controller_id = %self.controller_id, "Starting GREE discovery" ); let deadline = Instant::now() + duration; let interval = if passes > 1 { duration / passes as u32 } else { duration }; let mut next_scan = Instant::now(); let mut sent = 0_u8; let mut result = Vec::new(); let mut seen = HashSet::new(); let mut buffer = vec![0_u8; 16 * 1024]; while Instant::now() < deadline { if sent < passes && Instant::now() >= next_scan { socket.send_to(br#"{"t":"scan"}"#, target).await?; sent += 1; next_scan = Instant::now() + interval.max(Duration::from_millis(250)); tracing::debug!(pass = sent, passes, target = %target, "Sent GREE discovery packet"); } let remaining = deadline.saturating_duration_since(Instant::now()); let wait = remaining.min(Duration::from_millis(250)); match timeout(wait, socket.recv_from(&mut buffer)).await { Ok(Ok((size, source))) => { let Ok(value) = serde_json::from_slice::(&buffer[..size]) else { continue; }; match self.parse_discovery(value, source) { Ok(Some(mut device)) => { if protocol_filter != 0 && device.protocol_version != protocol_filter { continue; } let key = device.mac.to_ascii_lowercase(); if seen.insert(key) { device.last_seen = Some(Utc::now()); tracing::info!(ip=%device.ip, mac=%device.mac, protocol=device.protocol_version, model=%device.model, firmware=%device.firmware, "Discovered GREE device"); result.push(device); } } Ok(None) => {} Err(err) => { tracing::debug!(source=%source, error=?err, "Ignoring undecodable discovery response") } } } Ok(Err(err)) => return Err(err.into()), Err(_) => continue, } } Ok(result) } fn parse_discovery(&self, mut value: Value, source: SocketAddr) -> Result> { let mut detected_protocol = 1_u8; if value.get("t").and_then(Value::as_str) == Some("pack") { if let Some(pack_value) = value.get("pack") { if let Some(pack) = pack_value.as_str() { let clear = if let Some(tag) = value.get("tag").and_then(Value::as_str) { detected_protocol = 2; decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)? } else { decrypt_v1(GENERIC_GREE_V1_KEY, pack)? }; value = serde_json::from_slice::(&clear) .context("invalid decrypted discovery JSON")?; } else if pack_value.is_object() { value = pack_value.clone(); } } } let kind = value .get("t") .and_then(Value::as_str) .unwrap_or_default() .to_ascii_lowercase(); if kind != "dev" && kind != "scan" && value.get("mac").is_none() && value.get("cid").is_none() { return Ok(None); } let mac = value .get("mac") .or_else(|| value.get("cid")) .and_then(Value::as_str) .unwrap_or_default() .replace([':', '-'], "") .to_ascii_uppercase(); if mac.is_empty() { return Ok(None); } let raw_model = value .get("model") .or_else(|| value.get("series")) .and_then(Value::as_str) .unwrap_or_default() .trim() .to_string(); let model_type = value .get("ModelType") .and_then(|v| { v.as_str() .map(str::to_string) .or_else(|| v.as_i64().map(|n| n.to_string())) }) .unwrap_or_default(); let model = if !model_type.is_empty() && (raw_model.is_empty() || raw_model.eq_ignore_ascii_case("gree")) { format!("GREE {model_type}") } else if raw_model.is_empty() { "GREE".to_string() } else { raw_model }; let ver = value .get("ver") .and_then(Value::as_str) .unwrap_or_default() .trim(); let hid = value .get("hid") .and_then(Value::as_str) .unwrap_or_default() .trim(); let firmware = match (ver.is_empty(), hid.is_empty()) { (false, false) => format!("{ver} ยท {hid}"), (false, true) => ver.to_string(), (true, false) => hid.to_string(), (true, true) => String::new(), }; let suffix = mac .chars() .rev() .take(4) .collect::() .chars() .rev() .collect::() .to_ascii_uppercase(); let name = value .get("name") .and_then(Value::as_str) .map(str::trim) .filter(|v| !v.is_empty()) .map(str::to_string) .unwrap_or_else(|| format!("{model} {suffix}")); let now = Utc::now(); Ok(Some(Device { id: format!("gree-{}", mac.to_ascii_lowercase()), mac, name, ip: source.ip().to_string(), port: if source.port() == 0 { 7000 } else { source.port() }, protocol_version: detected_protocol, model, firmware, key: None, cid: Some("app".into()), enabled: true, simulated: false, power: false, mode: "cool".into(), target_temperature: 24.0, fan_speed: 0, swing_vertical: false, swing_horizontal: false, quiet: false, quiet_wire_value: None, turbo: false, light: true, air: false, xfan: false, health: false, sleep: false, supports_light: None, supports_quiet: None, supports_turbo: None, supports_air: None, supports_xfan: None, supports_health: None, supports_sleep: None, current_temperature: None, outdoor_temperature: None, temperature_sensor_offset: None, online: true, response_time_ms: None, last_seen: Some(now), last_error: None, communication_failures: 0, created_at: now, updated_at: now, })) } }