v0.12.0-preety_code
This commit is contained in:
+158
-40
@@ -4,10 +4,15 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
let devices = state.db.list_devices()?;
|
||||
let zones = state.db.list_zones()?;
|
||||
let groups = state.db.list_groups()?;
|
||||
let zone_names: HashMap<String, String> = zones.iter().map(|zone| (zone.id.clone(), zone.name.clone())).collect();
|
||||
let zone_names: HashMap<String, String> = zones
|
||||
.iter()
|
||||
.map(|zone| (zone.id.clone(), zone.name.clone()))
|
||||
.collect();
|
||||
let house_preset = zones.first().and_then(|first| {
|
||||
let first_preset = first.manual_preset.as_deref().unwrap_or("auto");
|
||||
zones.iter().all(|zone| zone.manual_preset.as_deref().unwrap_or("auto") == first_preset)
|
||||
zones
|
||||
.iter()
|
||||
.all(|zone| zone.manual_preset.as_deref().unwrap_or("auto") == first_preset)
|
||||
.then(|| first_preset.to_string())
|
||||
});
|
||||
let house_power = true;
|
||||
@@ -21,9 +26,14 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
let configured_effective_mode_owned = effective_zone_mode(&zone, &settings.house_mode);
|
||||
refresh_control_ownership(&mut zone);
|
||||
let configured_effective_mode = configured_effective_mode_owned.as_str();
|
||||
let target_mode = if configured_effective_mode == "off" { zone.mode.as_str() } else { configured_effective_mode };
|
||||
let target_mode = if configured_effective_mode == "off" {
|
||||
zone.mode.as_str()
|
||||
} else {
|
||||
configured_effective_mode
|
||||
};
|
||||
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
|
||||
let has_intent_source = zone_has_thermostat_intent_source(&zone, active_for_target, Utc::now());
|
||||
let has_intent_source =
|
||||
zone_has_thermostat_intent_source(&zone, active_for_target, Utc::now());
|
||||
let automation_idle = zone.enabled
|
||||
&& !zone.device_manual_override
|
||||
&& zone.local_thermostat_power != Some(false)
|
||||
@@ -34,7 +44,13 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
&& zone.local_thermostat_power != Some(false)
|
||||
&& configured_effective_mode != "off"
|
||||
&& has_intent_source;
|
||||
let manual_device_mode = device.map(|item| if item.power { item.mode.as_str() } else { "off" });
|
||||
let manual_device_mode = device.map(|item| {
|
||||
if item.power {
|
||||
item.mode.as_str()
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
});
|
||||
let effective_mode = if zone.device_manual_override {
|
||||
manual_device_mode.unwrap_or(configured_effective_mode)
|
||||
} else if automatic_active {
|
||||
@@ -43,8 +59,13 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
"off"
|
||||
};
|
||||
|
||||
let (resolved_preset, resolved_target) = resolve_zone_target(&zone, active_for_target, target_mode);
|
||||
let active = if automatic_active { active_for_target } else { None };
|
||||
let (resolved_preset, resolved_target) =
|
||||
resolve_zone_target(&zone, active_for_target, target_mode);
|
||||
let active = if automatic_active {
|
||||
active_for_target
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Future schedule events remain visible while the zone is currently idle between
|
||||
// windows; idle must not be confused with a disabled schedule system.
|
||||
let next_events = if configured_effective_mode == "off" {
|
||||
@@ -57,19 +78,24 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
event.label = format!("{}: {}", zone.name, event.label);
|
||||
house_events.push(event);
|
||||
}
|
||||
let effective_enabled = zone.enabled
|
||||
&& zone.local_thermostat_power != Some(false);
|
||||
let effective_enabled = zone.enabled && zone.local_thermostat_power != Some(false);
|
||||
zones_out.push(ZoneControlPlan {
|
||||
zone_id: zone.id.clone(),
|
||||
zone_name: zone.name.clone(),
|
||||
device_id: zone.device_id.clone(),
|
||||
device_name: device.map(|item| item.name.clone()).unwrap_or_else(|| zone.device_id.clone()),
|
||||
device_name: device
|
||||
.map(|item| item.name.clone())
|
||||
.unwrap_or_else(|| zone.device_id.clone()),
|
||||
enabled: zone.enabled,
|
||||
effective_enabled,
|
||||
mode: effective_mode.to_string(),
|
||||
configured_mode: zone.mode.clone(),
|
||||
inherit_house_mode: zone.inherit_house_mode,
|
||||
preset: if has_intent_source { resolved_preset } else { "auto".into() },
|
||||
preset: if has_intent_source {
|
||||
resolved_preset
|
||||
} else {
|
||||
"auto".into()
|
||||
},
|
||||
preset_override: zone.manual_preset.clone(),
|
||||
current_temperature: zone.current_temperature,
|
||||
target_temperature: if !has_intent_source {
|
||||
@@ -81,12 +107,22 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
} else {
|
||||
zone.effective_setpoint.or(Some(resolved_target))
|
||||
},
|
||||
device_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
||||
device_setpoint: device
|
||||
.filter(|item| item.power)
|
||||
.map(|item| item.target_temperature),
|
||||
desired_power: automatic_active,
|
||||
desired_mode: effective_mode.to_string(),
|
||||
actual_power: device.map(|item| item.power),
|
||||
actual_mode: device.map(|item| if item.power { item.mode.clone() } else { "off".into() }),
|
||||
actual_setpoint: device.filter(|item| item.power).map(|item| item.target_temperature),
|
||||
actual_mode: device.map(|item| {
|
||||
if item.power {
|
||||
item.mode.clone()
|
||||
} else {
|
||||
"off".into()
|
||||
}
|
||||
}),
|
||||
actual_setpoint: device
|
||||
.filter(|item| item.power)
|
||||
.map(|item| item.target_temperature),
|
||||
demand: automatic_active && zone.demand,
|
||||
control_source: zone.control_temperature_source.clone(),
|
||||
manual_override_until: zone.manual_override_until,
|
||||
@@ -103,7 +139,30 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
} else {
|
||||
zone.control_reason.clone()
|
||||
},
|
||||
blocked_reason: if zone.device_manual_override { Some("manual_override".into()) } else if zone.lockout_until.map(|until| until > Utc::now()).unwrap_or(false) { Some(zone.lockout_reason.clone().unwrap_or_else(|| "lockout".into())) } else if !zone.enabled { Some("zone_disabled".into()) } else if device.map(|d| !d.online || d.communication_failures > 0).unwrap_or(true) { Some("offline".into()) } else if automation_idle { Some("no_active_thermostat_intent".into()) } else { None },
|
||||
blocked_reason: if zone.device_manual_override {
|
||||
Some("manual_override".into())
|
||||
} else if zone
|
||||
.lockout_until
|
||||
.map(|until| until > Utc::now())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(
|
||||
zone.lockout_reason
|
||||
.clone()
|
||||
.unwrap_or_else(|| "lockout".into()),
|
||||
)
|
||||
} else if !zone.enabled {
|
||||
Some("zone_disabled".into())
|
||||
} else if device
|
||||
.map(|d| !d.online || d.communication_failures > 0)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
Some("offline".into())
|
||||
} else if automation_idle {
|
||||
Some("no_active_thermostat_intent".into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
lockout_until: zone.lockout_until,
|
||||
current_schedule_id: active.map(|item| item.id.clone()),
|
||||
current_schedule_name: active.map(|item| item.name.clone()),
|
||||
@@ -112,19 +171,33 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
}
|
||||
let mut rules = Vec::new();
|
||||
for item in state.db.list_automations()? {
|
||||
let action_zone_name = item.action_zone_id.as_deref()
|
||||
let action_zone_name = item
|
||||
.action_zone_id
|
||||
.as_deref()
|
||||
.and_then(|id| zone_names.get(id))
|
||||
.cloned();
|
||||
let action_group_name = item.action_group_id.as_deref()
|
||||
let action_group_name = item
|
||||
.action_group_id
|
||||
.as_deref()
|
||||
.and_then(|id| groups.iter().find(|group| group.id == id))
|
||||
.map(|group| group.name.clone());
|
||||
let action_name = action_zone_name.or_else(|| action_group_name.clone()).unwrap_or_else(|| {
|
||||
devices.iter().find(|device| device.id == item.action_device_id)
|
||||
.map(|device| device.name.clone())
|
||||
.unwrap_or_else(|| item.action_device_id.clone())
|
||||
});
|
||||
let trigger_name = item.trigger_device_id.as_deref().and_then(|id| devices.iter().find(|device| device.id == id)).map(|device| device.name.clone());
|
||||
let next_ready_at = item.last_fired_at.map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64));
|
||||
let action_name = action_zone_name
|
||||
.or_else(|| action_group_name.clone())
|
||||
.unwrap_or_else(|| {
|
||||
devices
|
||||
.iter()
|
||||
.find(|device| device.id == item.action_device_id)
|
||||
.map(|device| device.name.clone())
|
||||
.unwrap_or_else(|| item.action_device_id.clone())
|
||||
});
|
||||
let trigger_name = item
|
||||
.trigger_device_id
|
||||
.as_deref()
|
||||
.and_then(|id| devices.iter().find(|device| device.id == id))
|
||||
.map(|device| device.name.clone());
|
||||
let next_ready_at = item
|
||||
.last_fired_at
|
||||
.map(|last| last + chrono::Duration::seconds(item.cooldown_seconds as i64));
|
||||
if item.enabled && item.trigger_kind == "time" {
|
||||
if let Some(event) = next_time_automation_event(&item, &action_name, now) {
|
||||
house_events.push(event);
|
||||
@@ -170,11 +243,20 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn next_night_mode_events(settings: &NightModeSettings, now: DateTime<Local>, limit: usize) -> Vec<ControlPlanEvent> {
|
||||
if !settings.enabled || limit == 0 { return Vec::new(); }
|
||||
let Ok(start) = NaiveTime::parse_from_str(&settings.start_time, "%H:%M") else { return Vec::new(); };
|
||||
let Ok(end) = NaiveTime::parse_from_str(&settings.end_time, "%H:%M") else { return Vec::new(); };
|
||||
fn next_night_mode_events(
|
||||
settings: &NightModeSettings,
|
||||
now: DateTime<Local>,
|
||||
limit: usize,
|
||||
) -> Vec<ControlPlanEvent> {
|
||||
if !settings.enabled || limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let Ok(start) = NaiveTime::parse_from_str(&settings.start_time, "%H:%M") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(end) = NaiveTime::parse_from_str(&settings.end_time, "%H:%M") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut events = Vec::new();
|
||||
let base = minute_floor(now);
|
||||
for minute in 1..=(48 * 60) {
|
||||
@@ -182,7 +264,14 @@ fn next_night_mode_events(settings: &NightModeSettings, now: DateTime<Local>, li
|
||||
let time = candidate.time();
|
||||
let (kind, label) = if time.hour() == start.hour() && time.minute() == start.minute() {
|
||||
let quiet = if settings.force_quiet { " + Quiet" } else { "" };
|
||||
("night_mode_start", format!("Night mode -> fan max {}{}", settings.max_fan_speed.clamp(1, 5), quiet))
|
||||
(
|
||||
"night_mode_start",
|
||||
format!(
|
||||
"Night mode -> fan max {}{}",
|
||||
settings.max_fan_speed.clamp(1, 5),
|
||||
quiet
|
||||
),
|
||||
)
|
||||
} else if time.hour() == end.hour() && time.minute() == end.minute() {
|
||||
("night_mode_end", "Night mode ends".to_string())
|
||||
} else {
|
||||
@@ -195,12 +284,18 @@ fn next_night_mode_events(settings: &NightModeSettings, now: DateTime<Local>, li
|
||||
preset: None,
|
||||
target_temperature: None,
|
||||
});
|
||||
if events.len() >= limit { break; }
|
||||
if events.len() >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn next_time_automation_event(item: &Automation, action_name: &str, now: DateTime<Local>) -> Option<ControlPlanEvent> {
|
||||
fn next_time_automation_event(
|
||||
item: &Automation,
|
||||
action_name: &str,
|
||||
now: DateTime<Local>,
|
||||
) -> Option<ControlPlanEvent> {
|
||||
let expected = NaiveTime::parse_from_str(item.at_time.as_deref()?, "%H:%M").ok()?;
|
||||
let base = minute_floor(now.clone());
|
||||
if time_automation_due(item, now) {
|
||||
@@ -228,8 +323,16 @@ fn next_time_automation_event(item: &Automation, action_name: &str, now: DateTim
|
||||
None
|
||||
}
|
||||
|
||||
fn next_schedule_events(zone: &Zone, schedules: &[Schedule], mode: &str, now: DateTime<Local>, limit: usize) -> Vec<ControlPlanEvent> {
|
||||
if mode == "off" { return Vec::new(); }
|
||||
fn next_schedule_events(
|
||||
zone: &Zone,
|
||||
schedules: &[Schedule],
|
||||
mode: &str,
|
||||
now: DateTime<Local>,
|
||||
limit: usize,
|
||||
) -> Vec<ControlPlanEvent> {
|
||||
if mode == "off" {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
let mut current = active_schedule_for_zone(zone, schedules, now).map(|item| item.id.as_str());
|
||||
let base = minute_floor(now);
|
||||
@@ -237,14 +340,28 @@ fn next_schedule_events(zone: &Zone, schedules: &[Schedule], mode: &str, now: Da
|
||||
let candidate = base + chrono::Duration::minutes(minute);
|
||||
let next = active_schedule_for_zone(zone, schedules, candidate);
|
||||
let next_id = next.map(|item| item.id.as_str());
|
||||
if next_id == current { continue; }
|
||||
if next_id == current {
|
||||
continue;
|
||||
}
|
||||
current = next_id;
|
||||
let (preset, target, label) = if let Some(item) = next {
|
||||
let target = if item.preset == "custom" { item.setpoint } else { profile_setpoint(zone, &item.preset, mode) };
|
||||
(Some(item.preset.clone()), Some(target), format!("{} -> {} {:.1} C", item.name, item.preset, target))
|
||||
let target = if item.preset == "custom" {
|
||||
item.setpoint
|
||||
} else {
|
||||
profile_setpoint(zone, &item.preset, mode)
|
||||
};
|
||||
(
|
||||
Some(item.preset.clone()),
|
||||
Some(target),
|
||||
format!("{} -> {} {:.1} C", item.name, item.preset, target),
|
||||
)
|
||||
} else {
|
||||
let target = profile_setpoint(zone, "comfort", mode);
|
||||
(Some("comfort".into()), Some(target), format!("comfort {:.1} C", target))
|
||||
(
|
||||
Some("comfort".into()),
|
||||
Some(target),
|
||||
format!("comfort {:.1} C", target),
|
||||
)
|
||||
};
|
||||
events.push(ControlPlanEvent {
|
||||
at: candidate.with_timezone(&Utc),
|
||||
@@ -253,8 +370,9 @@ fn next_schedule_events(zone: &Zone, schedules: &[Schedule], mode: &str, now: Da
|
||||
preset,
|
||||
target_temperature: target,
|
||||
});
|
||||
if events.len() >= limit { break; }
|
||||
if events.len() >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user