This commit is contained in:
Mateusz Gruszczyński
2026-09-01 10:20:19 +02:00
parent 1a5c1305dc
commit 3479ed750d
34 changed files with 575 additions and 113 deletions
+37 -3
View File
@@ -42,6 +42,9 @@ async fn get_group(State(state): State<AppState>, Path(id): Path<String>) -> Res
}
async fn create_group(State(state): State<AppState>, Json(input): Json<GroupInput>) -> Result<(StatusCode, Json<ClimateGroup>), AppError> {
let _configuration_guard = state.lock_configuration_operation().await;
let _reference_guard = state.lock_automation_operation().await;
let _house_guard = state.lock_house_operation().await;
let zone_ids = validate_group_input(&state, &input)?;
let now = Utc::now();
let group = ClimateGroup {
@@ -59,6 +62,12 @@ async fn create_group(State(state): State<AppState>, Json(input): Json<GroupInpu
}
async fn update_group(State(state): State<AppState>, Path(id): Path<String>, Json(input): Json<GroupInput>) -> Result<Json<ClimateGroup>, AppError> {
let _configuration_guard = state.lock_configuration_operation().await;
// Membership changes alter the target set of group automations, so serialize them with
// automation execution/reference validation before taking the group lock.
let _automation_guard = state.lock_automation_operation().await;
let _house_guard = state.lock_house_operation().await;
let _group_guard = state.lock_group_operation(&id).await;
let existing = state.db.get_group(&id)?.ok_or_else(|| AppError::NotFound(format!("group {id}")))?;
let zone_ids = validate_group_input(&state, &input)?;
let group = ClimateGroup {
@@ -76,6 +85,10 @@ async fn update_group(State(state): State<AppState>, Path(id): Path<String>, Jso
}
async fn delete_group(State(state): State<AppState>, Path(id): Path<String>) -> Result<StatusCode, AppError> {
let _configuration_guard = state.lock_configuration_operation().await;
let _automation_guard = state.lock_automation_operation().await;
let _house_guard = state.lock_house_operation().await;
let _group_guard = state.lock_group_operation(&id).await;
if state.db.list_automations()?.iter().any(|item| item.action_group_id.as_deref() == Some(id.as_str())) {
return Err(AppError::BadRequest("group is used by an automation; remove or retarget that automation first".into()));
}
@@ -99,9 +112,14 @@ fn ensure_zone_removal_safe(state: &AppState, zone_ids: &std::collections::HashS
Ok(())
}
fn remove_zone_ids_from_groups(state: &AppState, zone_ids: &std::collections::HashSet<String>) -> Result<(), AppError> {
async fn remove_zone_ids_from_groups_locked(state: &AppState, zone_ids: &std::collections::HashSet<String>) -> Result<(), AppError> {
if zone_ids.is_empty() { return Ok(()); }
for mut group in state.db.list_groups()? {
let mut group_ids: Vec<String> = state.db.list_groups()?.into_iter().map(|group| group.id).collect();
group_ids.sort();
group_ids.dedup();
for group_id in group_ids {
let _group_guard = state.lock_group_operation(&group_id).await;
let Some(mut group) = state.db.get_group(&group_id)? else { continue; };
let before = group.zone_ids.len();
group.zone_ids.retain(|zone_id| !zone_ids.contains(zone_id));
if group.zone_ids.len() == before { continue; }
@@ -141,7 +159,7 @@ fn home_assistant_group_preset(zones: &[&Zone]) -> String {
let mut value: Option<&str> = None;
for zone in zones {
let current = zone.manual_preset.as_deref().unwrap_or("auto");
if !matches!(current, "auto" | "comfort" | "sleep" | "away") {
if !matches!(current, "auto" | "comfort" | "sleep" | "away" | "custom") {
return "mixed".into();
}
if let Some(previous) = value {
@@ -153,6 +171,21 @@ fn home_assistant_group_preset(zones: &[&Zone]) -> String {
value.unwrap_or("mixed").to_string()
}
fn home_assistant_group_custom_setpoint(zones: &[&Zone]) -> Option<f64> {
let mut value: Option<f64> = None;
for zone in zones {
if zone.manual_preset.as_deref() != Some("custom") { return None; }
let current = zone.manual_setpoint.or(zone.effective_setpoint)?;
if let Some(previous) = value {
if (previous - current).abs() > 0.05 { return None; }
} else {
value = Some(current);
}
}
value
}
async fn list_home_assistant_groups(State(state): State<AppState>) -> Result<Json<Vec<Value>>, AppError> {
let groups = state.db.list_groups()?;
let zones = state.db.list_zones()?;
@@ -223,6 +256,7 @@ async fn list_home_assistant_groups(State(state): State<AppState>) -> Result<Jso
"effective_power": settings.house_power_enabled && group.power_enabled,
"mode": home_assistant_group_mode(&members),
"preset": home_assistant_group_preset(&members),
"custom_setpoint": home_assistant_group_custom_setpoint(&members),
"house_mode": settings.house_mode,
"zone_count": members.len(),
"enabled_zones": members.iter().filter(|zone| zone.enabled).count(),