This commit is contained in:
Mateusz Gruszczyński
2026-09-01 12:07:04 +02:00
parent 6207bc4de6
commit e496f911da
25 changed files with 227 additions and 309 deletions
+5 -27
View File
@@ -23,24 +23,6 @@ async fn clear_group_control_sources(state: &AppState, reason: &str) -> Result<(
Ok(())
}
async fn set_all_groups_power(state: &AppState, power: bool) -> Result<(), AppError> {
let mut group_ids: Vec<String> = state.db.list_groups()?.into_iter().map(|group| group.id).collect();
group_ids.sort();
group_ids.dedup();
let mut _group_guards = Vec::with_capacity(group_ids.len());
for group_id in &group_ids {
_group_guards.push(state.lock_group_operation(group_id).await);
}
for mut group in state.db.list_groups()? {
if group.power_enabled == power { continue; }
group.power_enabled = power;
group.updated_at = Utc::now();
state.db.save_group(&group)?;
state.broadcast("group.updated", serde_json::to_value(&group)?);
}
Ok(())
}
fn clear_all_local_thermostat_overrides(state: &AppState) -> Result<usize, AppError> {
let mut cleared = 0;
for mut zone in state.db.list_zones()? {
@@ -123,9 +105,6 @@ async fn update_house_control(State(state): State<AppState>, Json(input): Json<H
};
state.broadcast("settings.updated", payload.clone());
clear_group_control_sources(&state, "Whole-house mode control took ownership").await?;
if activate_all {
set_all_groups_power(&state, true).await?;
}
// run_zone_control_now takes the same cycle lock, so release the mutation window first.
drop(cycle_guard);
if activate_all {
@@ -157,10 +136,10 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
}
}
// Global power is a true cascade across group gates. OFF clears the current takeover
// markers once, then each enabled device is re-cleared atomically with its OFF command.
// A later pilot action is therefore not erased by subsequent controller cycles.
set_all_groups_power(&state, input.power).await?;
// Whole-house power is independent from group-control enablement. OFF clears current
// ownership markers and powers devices down, but preserves which groups the user has
// enabled for future group actions. A later house ON therefore does not silently turn
// disabled group control back on.
clear_group_control_sources(&state, if input.power { "Whole-house power control resumed automation" } else { "Whole-house power disabled" }).await?;
if !input.power {
let zone_snapshot = state.db.list_zones()?;
@@ -205,7 +184,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
let settings = state.settings.read().await;
let settings_payload = public_settings(&settings);
drop(settings);
state.log("info", "house.power_all", if input.power { "Whole-house automation enabled; thermostat arbiter resumed" } else { "Whole-house power disabled; all groups and enabled devices powered off" }, json!({
state.log("info", "house.power_all", if input.power { "Whole-house automation enabled; thermostat arbiter resumed" } else { "Whole-house power disabled; enabled devices powered off while group-control preferences were preserved" }, json!({
"power": input.power,
"failed": failed.len(),
}));
@@ -237,7 +216,6 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
public_settings(&settings)
};
state.broadcast("settings.updated", settings_payload.clone());
set_all_groups_power(&state, true).await?;
clear_group_control_sources(&state, "Whole-house preset control took ownership").await?;
let schedules = state.db.list_schedules()?;
+1 -6
View File
@@ -380,24 +380,19 @@ async fn import_settings(State(state): State<AppState>, Json(mut export): Json<C
}
// Configuration import never restores ephemeral owners/timers or cached physical state.
// Imported devices are reconciled from a fresh poll and current house/group/zone gates.
// Imported devices are reconciled from a fresh poll and current house/zone gates.
sanitize_configuration_runtime(&mut export);
state.initial_device_sync_complete.store(false, Ordering::Release);
state.db.replace_configuration(&export)?;
state.debug_gree_frames.store(export.settings.debug.gree_frames, Ordering::Relaxed);
*state.settings.write().await = export.settings.clone();
let disabled_group_zones: std::collections::HashSet<String> = export.groups.iter()
.filter(|group| !group.power_enabled)
.flat_map(|group| group.zone_ids.iter().cloned())
.collect();
let controllable_devices: std::collections::HashSet<String> = export.zones.iter()
.filter(|zone| {
let effective_mode = if zone.inherit_house_mode { export.settings.house_mode.as_str() } else { zone.mode.as_str() };
export.settings.house_power_enabled
&& zone.enabled
&& effective_mode != "off"
&& !disabled_group_zones.contains(&zone.id)
})
.map(|zone| zone.device_id.clone())
.collect();
+1 -3
View File
@@ -618,9 +618,7 @@ async fn apply_zone_control_patch(state: &AppState, id: &str, patch: ZoneControl
let device_override_cleared = if resume_device_automation { engine::reset_device_manual_override(&mut zone) } else { false };
let runtime = state.settings.read().await.clone();
let house_mode = runtime.house_mode.clone();
let blocked_by_group = zone.local_thermostat_power != Some(true)
&& state.db.list_groups()?.iter().any(|group| !group.power_enabled && group.zone_ids.iter().any(|zone_id| zone_id == &zone.id));
engine::refresh_control_ownership(&mut zone, runtime.house_power_enabled, blocked_by_group);
engine::refresh_control_ownership(&mut zone, runtime.house_power_enabled);
engine::refresh_zone_runtime_target(&mut zone, &schedules, &house_mode);
zone.revision = zone.revision.saturating_add(1);
zone.updated_at = Utc::now();