v0.15.15
This commit is contained in:
@@ -106,6 +106,38 @@ async fn command_all_enabled_devices_power(
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
async fn clear_all_automation_compressor_queues(state: &AppState) -> Result<usize, AppError> {
|
||||
let mut zone_ids: Vec<String> = state
|
||||
.db
|
||||
.list_zones()?
|
||||
.into_iter()
|
||||
.map(|zone| zone.id)
|
||||
.collect();
|
||||
zone_ids.sort();
|
||||
zone_ids.dedup();
|
||||
let mut changed = 0usize;
|
||||
for zone_id in zone_ids {
|
||||
let _zone_guard = state.lock_zone_operation(&zone_id).await;
|
||||
let Some(mut zone) = state.db.get_zone(&zone_id)? else {
|
||||
continue;
|
||||
};
|
||||
if zone.compressor_pending_action.is_none()
|
||||
&& zone.compressor_cancelled_action.is_none()
|
||||
&& zone.lockout_until.is_none()
|
||||
&& zone.lockout_reason.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
engine::rearm_compressor_queue(&mut zone);
|
||||
zone.revision = zone.revision.saturating_add(1);
|
||||
zone.updated_at = Utc::now();
|
||||
state.db.save_zone(&zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||
changed += 1;
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
async fn update_house_control(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<HouseControlPatch>,
|
||||
@@ -155,6 +187,96 @@ struct HousePowerPatch {
|
||||
power: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HouseEmergencyStopPatch {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
async fn update_house_emergency_stop(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<HouseEmergencyStopPatch>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
// Automation execution already uses this lock before committing an action. Taking it first
|
||||
// gives the emergency stop a clean barrier: an in-flight action finishes, then no newer
|
||||
// automatic action can cross the persisted safety flag.
|
||||
let _automation_guard = state.lock_automation_operation().await;
|
||||
let _house_guard = state.lock_house_operation().await;
|
||||
// Persist the safety gate while automatic control is serialized. Once the flag is stored,
|
||||
// neither the background regulator nor an immediate thermostat run can emit automation
|
||||
// commands until the user explicitly resumes normal operation.
|
||||
let cycle_guard = state.lock_zone_control_cycle().await;
|
||||
let (changed, emergency_stop_since) = {
|
||||
let mut settings = state.settings.write().await;
|
||||
let changed = settings.emergency_stop_enabled != input.active;
|
||||
if changed {
|
||||
settings.emergency_stop_enabled = input.active;
|
||||
settings.emergency_stop_since = if input.active { Some(Utc::now()) } else { None };
|
||||
state.db.save_runtime_settings(&settings)?;
|
||||
}
|
||||
(changed, settings.emergency_stop_since.clone())
|
||||
};
|
||||
|
||||
// OFF is intentionally a one-shot side effect of activating the emergency stop. The
|
||||
// persistent flag survives restarts, but startup never replays physical commands from it.
|
||||
let mut failed = Vec::new();
|
||||
let mut cleared_queues = 0usize;
|
||||
if input.active && changed {
|
||||
cleared_queues = clear_all_automation_compressor_queues(&state).await?;
|
||||
failed = command_all_enabled_devices_power(&state, false, "house_emergency_stop").await?;
|
||||
}
|
||||
|
||||
let payload = json!({
|
||||
"active": input.active,
|
||||
"since": emergency_stop_since,
|
||||
"changed": changed,
|
||||
"failed": failed,
|
||||
"cleared_queues": cleared_queues,
|
||||
});
|
||||
state.broadcast("house.emergency_stop_changed", payload.clone());
|
||||
drop(cycle_guard);
|
||||
state.wake_zone_control();
|
||||
|
||||
// Resuming does not force any unit ON. It only releases the persistent safety gate and
|
||||
// immediately re-evaluates current schedules, temperatures and ownership from fresh state.
|
||||
if !input.active
|
||||
&& changed
|
||||
&& state.initial_device_sync_complete.load(Ordering::Acquire)
|
||||
{
|
||||
if let Err(err) = engine::run_zone_control_now(&state).await {
|
||||
state.log(
|
||||
"error",
|
||||
"house.emergency_resume_control_error",
|
||||
&err.to_string(),
|
||||
json!({"active": false}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
state.log(
|
||||
"info",
|
||||
if input.active {
|
||||
"house.emergency_stop_activated"
|
||||
} else {
|
||||
"house.emergency_stop_released"
|
||||
},
|
||||
if input.active {
|
||||
"Emergency stop activated; automatic climate control paused"
|
||||
} else {
|
||||
"Emergency stop released; automatic climate control resumed"
|
||||
},
|
||||
json!({
|
||||
"active": input.active,
|
||||
"changed": changed,
|
||||
"failed_devices": payload["failed"].as_array().map(Vec::len).unwrap_or(0),
|
||||
"cleared_queues": cleared_queues,
|
||||
"persistent_across_restart": true,
|
||||
"off_replayed_on_restart": false,
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(Json(payload))
|
||||
}
|
||||
|
||||
async fn update_house_power(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<HousePowerPatch>,
|
||||
|
||||
Reference in New Issue
Block a user