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

162 lines
7.1 KiB
Rust

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<Vec<Device>> {
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::<Value>(&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<Option<Device>> {
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::<Value>(&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::<String>().chars().rev().collect::<String>().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,
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,
}))
}
}