This commit is contained in:
Mateusz Gruszczyński
2026-08-25 09:43:28 +02:00
parent fd26d6877f
commit 440c7c3a8d
15 changed files with 118 additions and 55 deletions
+71 -24
View File
@@ -594,16 +594,55 @@ async fn update_zone_control(State(state): State<AppState>, Path(id): Path<Strin
#[derive(Debug, Deserialize)]
struct HouseControlPatch { mode: String }
async fn command_all_enabled_devices_power(state: &AppState, power: bool, source: &str) -> Result<Vec<Value>, AppError> {
let mut failed = Vec::new();
for device in state.db.list_devices()? {
if !device.enabled || device.power == power { continue; }
let command = DeviceCommand { power: Some(power), ..Default::default() };
if let Err(err) = engine::send_command(state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": power,
"source": source,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
Ok(failed)
}
async fn update_house_control(State(state): State<AppState>, Json(input): Json<HouseControlPatch>) -> Result<Json<Value>, AppError> {
if !matches!(input.mode.as_str(), "cool" | "heat" | "off") {
return Err(AppError::BadRequest("house mode must be cool, heat or off".into()));
}
let mut settings = state.settings.write().await;
settings.house_mode = input.mode;
state.db.save_runtime_settings(&settings)?;
let payload = public_settings(&settings);
let mode = input.mode;
let activate_all = mode != "off";
let payload = {
let mut settings = state.settings.write().await;
settings.house_mode = mode.clone();
// Choosing a real whole-house operating mode is an explicit request to run the
// house climate. It therefore clears a previous global power-off. "off" keeps
// its separate meaning: do not control, without changing master power.
if activate_all { settings.house_power_enabled = true; }
state.db.save_runtime_settings(&settings)?;
public_settings(&settings)
};
state.broadcast("settings.updated", payload.clone());
state.log("info", "house.mode", &format!("House mode set to {}", settings.house_mode), json!({"mode": settings.house_mode}));
if activate_all {
let failed = command_all_enabled_devices_power(&state, true, "house_mode").await?;
if !failed.is_empty() {
state.log("warn", "house.mode_power_partial", "House mode enabled master power, but some devices could not be powered on", json!({
"mode": mode,
"failed": failed.len(),
}));
}
}
state.log("info", "house.mode", &format!("House mode set to {}", mode), json!({"mode": mode, "master_power_enabled": activate_all}));
Ok(Json(payload))
}
@@ -623,23 +662,7 @@ async fn update_house_power(State(state): State<AppState>, Json(input): Json<Hou
}
}
let mut failed = Vec::new();
for device in state.db.list_devices()? {
if !device.enabled || device.power == input.power { continue; }
let command = DeviceCommand { power: Some(input.power), ..Default::default() };
if let Err(err) = engine::send_command(&state, &device.id, command).await {
state.log("error", "house.power_all_error", &err.to_string(), json!({
"device_id": device.id,
"device_name": device.name,
"power": input.power,
}));
failed.push(json!({
"device_id": device.id,
"device_name": device.name,
"error": err.to_string(),
}));
}
}
let failed = command_all_enabled_devices_power(&state, input.power, "house_power").await?;
let devices = state.db.list_devices()?;
let settings = state.settings.read().await;
@@ -664,6 +687,17 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
if !matches!(input.preset.as_str(), "auto" | "comfort" | "sleep" | "away") {
return Err(AppError::BadRequest("house preset must be auto, comfort, sleep or away".into()));
}
// A whole-house profile is also an explicit whole-house activation. This mirrors
// selecting cooling/heating and makes the separate master-power control intuitive.
let settings_payload = {
let mut settings = state.settings.write().await;
settings.house_power_enabled = true;
state.db.save_runtime_settings(&settings)?;
public_settings(&settings)
};
state.broadcast("settings.updated", settings_payload.clone());
let schedules = state.db.list_schedules()?;
let mut zones = state.db.list_zones()?;
for zone in &mut zones {
@@ -680,8 +714,21 @@ async fn update_house_preset(State(state): State<AppState>, Json(input): Json<Ho
state.db.save_zone(zone)?;
state.broadcast("zone.updated", serde_json::to_value(&*zone)?);
}
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({"preset": input.preset}));
Ok(Json(json!({"preset": input.preset, "zones": zones})))
let failed = command_all_enabled_devices_power(&state, true, "house_preset").await?;
let devices = state.db.list_devices()?;
state.log("info", "house.preset", &format!("House preset set to {}", input.preset), json!({
"preset": input.preset,
"master_power_enabled": true,
"failed": failed.len(),
}));
Ok(Json(json!({
"preset": input.preset,
"zones": zones,
"devices": devices,
"settings": settings_payload,
"failed": failed,
})))
}
#[derive(Debug, Deserialize)]