v0.8.20
This commit is contained in:
+34
-23
@@ -25,9 +25,11 @@ pub fn refresh_control_ownership(zone: &mut Zone, house_power_enabled: bool, blo
|
||||
};
|
||||
("local_thermostat", source, resume_at, reason)
|
||||
} else if blocked_by_group {
|
||||
("automation", "group".to_string(), None, "Zone is blocked by a disabled group".to_string())
|
||||
let source = if zone.control_source.starts_with("group:") { zone.control_source.clone() } else { "group".into() };
|
||||
("automation", source, None, "Zone is blocked by a disabled group".to_string())
|
||||
} else {
|
||||
("automation", "automation".to_string(), zone.manual_override_until, "Automatic thermostat/schedule control".to_string())
|
||||
let source = if zone.control_source.starts_with("group:") { zone.control_source.clone() } else { "automation".into() };
|
||||
("automation", source, zone.manual_override_until, "Automatic thermostat/schedule control".to_string())
|
||||
};
|
||||
if zone.control_owner != owner || zone.control_source != source {
|
||||
zone.control_since = Some(now);
|
||||
@@ -156,12 +158,11 @@ fn set_device_manual_override(state: &AppState, zone: &mut Zone, fields: Vec<Str
|
||||
zone.control_owner = "direct_manual".into();
|
||||
zone.control_source = normalized_direct_source(source).into();
|
||||
zone.control_reason = "Direct/manual device control has priority".into();
|
||||
zone.device_manual_override_until = if zone.enabled {
|
||||
next_schedule_boundary_utc(&zone.id, &state.db.list_schedules()?, Local::now())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
zone.control_resume_at = zone.device_manual_override_until;
|
||||
// Direct/pilot control is an explicit ownership takeover, not a temporary preset.
|
||||
// Keep it manual until the user chooses Resume automation (or a global safety OFF).
|
||||
// A schedule boundary must never silently take the unit back from the person controlling it.
|
||||
zone.device_manual_override_until = None;
|
||||
zone.control_resume_at = None;
|
||||
for field in fields {
|
||||
if !zone.device_manual_override_fields.iter().any(|existing| existing == &field) {
|
||||
zone.device_manual_override_fields.push(field);
|
||||
@@ -196,10 +197,9 @@ async fn detect_external_device_control(state: &AppState, before: &Device, after
|
||||
after,
|
||||
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)?;
|
||||
continue;
|
||||
}
|
||||
// Once direct/pilot ownership has been detected, merely returning the device to a
|
||||
// previous physical state must not silently hand control back to schedules. Only the
|
||||
// explicit Resume automation action (or global OFF safety reset) ends manual ownership.
|
||||
if fields.is_empty() { continue; }
|
||||
// A disabled zone is outside controller ownership. When its manually operated unit is
|
||||
// switched off there is no takeover left to display or remember.
|
||||
@@ -243,15 +243,14 @@ pub async fn send_manual_command(state: &AppState, device_id: &str, command: Dev
|
||||
let _device_guard = state.lock_device_operation(device_id).await;
|
||||
let before = state.db.get_device(device_id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {device_id}")))?;
|
||||
let effective_command = if before.online && before.communication_failures == 0 { command.changed_from(&before) } else { command.clone() };
|
||||
let fields = command_manual_control_fields(&effective_command);
|
||||
// An explicit direct-control request is an ownership action even when the requested
|
||||
// value already matches the cached device state. Derive takeover fields from the user's
|
||||
// request, not only from the physical delta, so clicking ON / entering the current target
|
||||
// still switches the thermostat zone to persistent manual control.
|
||||
let fields = command_manual_control_fields(&command);
|
||||
let updated = send_command_locked_inner(state, device_id, command, true, false).await?;
|
||||
if !fields.is_empty() {
|
||||
for mut zone in state.db.list_zones()?.into_iter().filter(|zone| zone.device_id == device_id) {
|
||||
if zone.device_manual_override && manual_override_matches_baseline(&zone, &updated) {
|
||||
persist_manual_override_clear(state, &mut zone, source, true)?;
|
||||
continue;
|
||||
}
|
||||
if !zone.enabled && !updated.power {
|
||||
persist_manual_override_clear(state, &mut zone, source, false)?;
|
||||
continue;
|
||||
@@ -263,11 +262,22 @@ pub async fn send_manual_command(state: &AppState, device_id: &str, command: Dev
|
||||
}
|
||||
|
||||
pub async fn force_house_power_off_device(state: &AppState, device_id: &str, source: &str) -> Result<Device, AppError> {
|
||||
// Global OFF is a one-shot authority transition. Clear takeover and send OFF while
|
||||
// polling for this unit is excluded; a later remote change happens after the lock and
|
||||
// is therefore preserved as a new manual takeover.
|
||||
// Preserve the global zone -> device lock order even for the whole-house safety path.
|
||||
// Otherwise a zone edit could hold its zone lock while waiting for this device lock as
|
||||
// this function saved a stale zone snapshot without owning the corresponding zone lock.
|
||||
let mut zone_ids: Vec<String> = state.db.list_zones()?.into_iter()
|
||||
.filter(|zone| zone.device_id == device_id)
|
||||
.map(|zone| zone.id)
|
||||
.collect();
|
||||
zone_ids.sort();
|
||||
zone_ids.dedup();
|
||||
let mut _zone_guards = Vec::with_capacity(zone_ids.len());
|
||||
for zone_id in &zone_ids {
|
||||
_zone_guards.push(state.lock_zone_operation(zone_id).await);
|
||||
}
|
||||
let _device_guard = state.lock_device_operation(device_id).await;
|
||||
for mut zone in state.db.list_zones()?.into_iter().filter(|zone| zone.device_id == device_id) {
|
||||
for zone_id in &zone_ids {
|
||||
let Some(mut zone) = state.db.get_zone(zone_id)? else { continue; };
|
||||
if !reset_device_manual_override(&mut zone) { continue; }
|
||||
zone.updated_at = Utc::now();
|
||||
state.db.save_zone(&zone)?;
|
||||
@@ -276,7 +286,8 @@ pub async fn force_house_power_off_device(state: &AppState, device_id: &str, sou
|
||||
"zone_id": zone.id, "device_id": zone.device_id, "source": source
|
||||
}));
|
||||
}
|
||||
send_command_locked_forced(state, device_id, DeviceCommand { power: Some(false), ..Default::default() }).await
|
||||
// The device lock is already held, so use the locked forced-command variant directly.
|
||||
force_power_off_device_locked(state, device_id).await
|
||||
}
|
||||
|
||||
pub async fn force_power_off_device(state: &AppState, device_id: &str) -> Result<Device, AppError> {
|
||||
|
||||
Reference in New Issue
Block a user