Files
gree-controller/src/protocol/gree/discovery.rs
T
2026-09-13 10:21:55 +02:00

229 lines
8.0 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,
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,
}))
}
}