This commit is contained in:
Mateusz Gruszczyński
2026-08-28 22:11:38 +02:00
parent 9a5e4892f4
commit 930cb7b1e1
10 changed files with 110 additions and 55 deletions
+68 -1
View File
@@ -563,6 +563,49 @@ fn command_field_matches_device(command: &DeviceCommand, field: &str, device: &D
}
}
fn device_control_snapshot(device: &Device) -> Value {
json!({
"power": device.power,
"mode": device.mode.clone(),
"target_temperature": device.target_temperature,
"fan_speed": device.fan_speed,
"quiet": device.quiet,
"sleep": device.sleep,
"turbo": device.turbo,
"swing_vertical": device.swing_vertical,
"swing_horizontal": device.swing_horizontal,
"online": device.online,
"communication_failures": device.communication_failures,
"last_seen": device.last_seen.clone(),
"updated_at": device.updated_at.clone(),
})
}
async fn controller_settling_diagnostics(state: &AppState, device_id: &str) -> Value {
let pending = state.pending_controller_commands.lock().await;
let Some(expected) = pending.get(device_id).cloned() else {
return json!({ "active": false, "reason": "none" });
};
let now = Instant::now();
if now > expected.expires_at {
let expired_by_ms = now.saturating_duration_since(expected.expires_at).as_millis().min(u64::MAX as u128) as u64;
return json!({
"active": false,
"reason": "expired",
"expired_by_ms": expired_by_ms,
"commands": expected.commands,
"baselines": expected.baselines,
});
}
let remaining_ms = expected.expires_at.saturating_duration_since(now).as_millis().min(u64::MAX as u128) as u64;
json!({
"active": true,
"remaining_ms": remaining_ms,
"commands": expected.commands,
"baselines": expected.baselines,
})
}
async fn suppress_expected_controller_changes(
state: &AppState,
device: &Device,
@@ -1279,10 +1322,16 @@ fn set_device_manual_override(state: &AppState, zone: &mut Zone, fields: Vec<Str
async fn detect_external_device_control(state: &AppState, before: &Device, after: &Device) -> Result<(), AppError> {
if before.id != after.id { return Ok(()); }
for mut zone in state.db.list_zones()?.into_iter().filter(|zone| zone.device_id == after.id) {
let raw_fields = externally_changed_control_fields(before, after, &zone);
let controller_settling = if raw_fields.is_empty() {
json!({ "active": false, "reason": "no_changed_control_fields" })
} else {
controller_settling_diagnostics(state, &after.id).await
};
let fields = suppress_expected_controller_changes(
state,
after,
externally_changed_control_fields(before, after, &zone),
raw_fields.clone(),
).await;
if zone.device_manual_override && manual_override_matches_baseline(&zone, after) {
persist_manual_override_clear(state, &mut zone, "gree_poll", true)?;
@@ -1295,6 +1344,24 @@ async fn detect_external_device_control(state: &AppState, before: &Device, after
persist_manual_override_clear(state, &mut zone, "gree_poll", false)?;
continue;
}
state.log("info", "device.remote_control_detected", &format!("External/pilot control detected for {}", zone.name), json!({
"zone_id": zone.id.clone(),
"device_id": zone.device_id.clone(),
"raw_fields": raw_fields,
"detected_fields": fields.clone(),
"before": device_control_snapshot(before),
"after": device_control_snapshot(after),
"controller_settling": controller_settling,
"source": "gree_poll",
"zone_state": {
"enabled": zone.enabled,
"control_owner": zone.control_owner.clone(),
"control_source": zone.control_source.clone(),
"demand": zone.demand,
"local_thermostat_power": zone.local_thermostat_power,
"temporary_quick_thermostat": zone.temporary_quick_thermostat.clone(),
},
}));
set_device_manual_override(state, &mut zone, fields, "gree_poll", before)?;
}
Ok(())