From f4e4e5c5c63ef7a1578b0e7c859df4321667088c Mon Sep 17 00:00:00 2001 From: Leo Date: Sat, 24 Feb 2024 18:48:46 +0000 Subject: [PATCH] Make DHW Only mode check the slots properly each time instead of fixating --- src/brain/modes/dhw_only.rs | 92 ++++++++++--------- src/brain/modes/heating_mode/mod.rs | 6 +- src/brain/modes/heating_mode/test.rs | 11 +-- src/brain/modes/on.rs | 27 +----- src/brain/python_like/config/mod.rs | 4 + .../python_like/config/overrun_config.rs | 8 +- src/brain/python_like/test.rs | 18 ++-- 7 files changed, 81 insertions(+), 85 deletions(-) diff --git a/src/brain/modes/dhw_only.rs b/src/brain/modes/dhw_only.rs index 76c983f..0036502 100644 --- a/src/brain/modes/dhw_only.rs +++ b/src/brain/modes/dhw_only.rs @@ -13,12 +13,11 @@ use crate::time_util::timeslot::ZonedSlot; use chrono::{DateTime, SecondsFormat, Utc}; use log::{debug, error, info, warn}; use std::fmt::{Display, Formatter}; +use std::time::Duration; use tokio::runtime::Runtime; #[derive(Debug, PartialEq)] pub struct DhwOnlyMode { - pub temps: DhwTemps, - pub expire: HeatUpEnd, } impl Mode for DhwOnlyMode { @@ -40,9 +39,6 @@ impl Mode for DhwOnlyMode { io_bundle: &mut IOBundle, time: &impl TimeProvider, ) -> Result { - if self.expire.has_expired(time.get_utc_time()) { - return Ok(Intention::finish()); - } let temps = rt.block_on(info_cache.get_temps(io_bundle.temperature_manager())); if temps.is_err() { error!( @@ -53,14 +49,29 @@ impl Mode for DhwOnlyMode { } let temps = temps.unwrap(); - let temp = match temps.get(&self.temps.sensor) { + let now = time.get_utc_time(); + + let heating_control = expect_available!(io_bundle.heating_control())?; + let (_hp_on, hp_duration) = heating_control.get_heat_pump_on_with_time()?; + let short_duration = hp_duration < Duration::from_secs(60 * 10); + + let slot = config.get_overrun_during().find_matching_slot(&now, &temps, + |temps, temp| temp < temps.max || (short_duration && temp < temps.extra.unwrap_or(temps.max)) + ); + + let Some(slot) = slot else { + info!("No longer matches a DHW slot"); + return Ok(Intention::finish()); + }; + + let temp = match temps.get(&slot.temps.sensor) { Some(temp) => temp, None => { - error!("Sensor {} targeted by overrun didn't have a temperature associated.", self.temps.sensor); + error!("Sensor {} targeted by overrun didn't have a temperature associated.", slot.temps.sensor); return Ok(Intention::off_now()); } }; - info!("Target: {} ({}), currently {:.2}", self.temps.max, self.expire, temp); + info!("Target: {}-{}/{:?} until {}, currently {:.2}", slot.temps.min, slot.temps.max, short_duration.then_some(slot.temps.extra), slot.slot, temp); if info_cache.heating_on() { match find_working_temp_action( @@ -74,8 +85,8 @@ impl Mode for DhwOnlyMode { debug!("Continuing to heat hot water as we would be circulating."); } Ok(WorkingTempAction::Heat { .. }) => { - info!("Call for heat during HeatUpTo, checking min {:.2?}", self.temps.min); - if *temp < self.temps.min { + info!("Call for heat during HeatUpTo, checking min {:.2?}", slot.temps.min); + if *temp < slot.temps.min { info!("Below minimum - Ignoring call for heat"); } else { return Ok(Intention::finish()); @@ -86,10 +97,6 @@ impl Mode for DhwOnlyMode { } }; } - if *temp > self.temps.max { - info!("Reached target overrun temp."); - return Ok(Intention::finish()); - } Ok(Intention::KeepState) } @@ -128,18 +135,8 @@ impl Display for HeatUpEnd { } impl DhwOnlyMode { - pub fn from_overrun(dhw: &DhwBap) -> Self { - Self { - temps: dhw.temps.clone(), - expire: HeatUpEnd::Slot(dhw.slot.clone()), - } - } - - pub fn from_time(temps: DhwTemps, expire: DateTime) -> Self { - Self { - temps, - expire: HeatUpEnd::Utc(expire), - } + pub fn new() -> Self { + Self {} } } @@ -155,7 +152,7 @@ mod test { use crate::io::temperatures::Sensor; use crate::time_util::mytime::DummyTimeProvider; use crate::time_util::test_utils::{date, time, utc_datetime, utc_time_slot}; - use chrono::{Duration, TimeZone, Utc}; + use chrono::{TimeZone, Utc}; #[test] fn test_results() { @@ -166,11 +163,14 @@ mod test { WorkingRange::from_temp_only(WorkingTemperatureRange::from_delta(45.0, 10.0)), ); - let mut heat_up_to = DhwOnlyMode::from_overrun(&DhwBap::new( - utc_time_slot(10, 00, 00, 12, 00, 00), - 40.0, - Sensor::TKBT, - )); + let mut config = PythonBrainConfig::default(); + config._add_dhw_slot(DhwBap { + slot: utc_time_slot(10, 00, 00, 12, 00, 00), + disable_below: None, + temps: DhwTemps { sensor: Sensor::TKBT, min: 10.0, max: 40.0, extra: None } + }); + + let mut heat_up_to = DhwOnlyMode::new(); let (mut io_bundle, mut io_handle) = new_dummy_io(); @@ -187,7 +187,7 @@ mod test { let result = heat_up_to.update( &rt, - &PythonBrainConfig::default(), + &config, &mut info_cache, &mut io_bundle, &time_provider, @@ -211,7 +211,7 @@ mod test { let result = heat_up_to.update( &rt, - &PythonBrainConfig::default(), + &config, &mut info_cache, &mut io_bundle, &time_provider, @@ -235,7 +235,7 @@ mod test { let result = heat_up_to.update( &rt, - &PythonBrainConfig::default(), + &config, &mut info_cache, &mut io_bundle, &time_provider, @@ -260,10 +260,15 @@ mod test { ); let utc_time = utc_datetime(2023, 06, 12, 10, 00, 00); - let mut mode = DhwOnlyMode::from_time( - DhwTemps { sensor: Sensor::TKBT, min: 0.0, max: 39.0, extra: None }, - utc_time + Duration::hours(1), - ); + + let mut config = PythonBrainConfig::default(); + config._add_dhw_slot(DhwBap { + slot: utc_time_slot(09, 00, 00, 11, 00, 00), + disable_below: None, + temps: DhwTemps { sensor: Sensor::TKBT, min: 0.0, max: 39.0, extra: None } + }); + + let mut mode = DhwOnlyMode::new(); let rt = Runtime::new().unwrap(); @@ -277,7 +282,7 @@ mod test { let next = mode.update( &rt, - &PythonBrainConfig::default(), + &config, &mut info_cache, &mut io_bundle, &time, @@ -297,13 +302,17 @@ mod test { let working_range = WorkingTemperatureRange::from_min_max(40.0, 50.0); let utc_slot = utc_time_slot(12, 0, 0, 13, 0, 0); - let mut mode = DhwOnlyMode::from_overrun(&DhwBap::new_with_min( + + let mut config = PythonBrainConfig::default(); + config._add_dhw_slot(DhwBap::new_with_min( utc_slot.clone(), 50.0, Sensor::TKBT, 30.0, )); + + let mut mode = DhwOnlyMode::new(); let rt = Runtime::new().unwrap(); let (mut io_bundle, mut handle) = new_dummy_io(); let time = DummyTimeProvider::in_slot(&utc_slot); @@ -317,7 +326,6 @@ mod test { HeatingState::ON, WorkingRange::from_temp_only(working_range), ); - let config = PythonBrainConfig::default(); mode.enter(&config, &rt, &mut io_bundle)?; diff --git a/src/brain/modes/heating_mode/mod.rs b/src/brain/modes/heating_mode/mod.rs index d8dec37..ac2e64e 100644 --- a/src/brain/modes/heating_mode/mod.rs +++ b/src/brain/modes/heating_mode/mod.rs @@ -370,7 +370,7 @@ fn get_heatup_while_off( } else { error!("Failed to retrieve sensor {} from temperatures when we really should have been able to.", bap.temps.sensor) } - return Some(HeatingMode::DhwOnly(DhwOnlyMode::from_overrun(bap))); + return Some(HeatingMode::DhwOnly(DhwOnlyMode::new())); } None } @@ -475,7 +475,7 @@ pub fn handle_finish_mode( |temps, temp| temp < temps.max); if let Some(slot) = slot { debug!("Overrun: {slot:?} would apply, going into overrun instead of circulating."); - return Ok(Some(HeatingMode::DhwOnly(DhwOnlyMode::from_overrun(slot)))); + return Ok(Some(HeatingMode::DhwOnly(DhwOnlyMode::new()))); } if !circulate { @@ -525,7 +525,7 @@ pub fn handle_finish_mode( |temps, temp| temp < temps.max || (hp_duration < Duration::from_secs(60 * 10) && temp < temps.extra.unwrap_or(temps.max)) ); if let Some(slot) = slot { - return Ok(Some(HeatingMode::DhwOnly(DhwOnlyMode::from_overrun(slot)))); + return Ok(Some(HeatingMode::DhwOnly(DhwOnlyMode::new()))); } Ok(Some(HeatingMode::off())) } diff --git a/src/brain/modes/heating_mode/test.rs b/src/brain/modes/heating_mode/test.rs index 9fc44c3..d2446a8 100644 --- a/src/brain/modes/heating_mode/test.rs +++ b/src/brain/modes/heating_mode/test.rs @@ -245,11 +245,11 @@ pub fn test_transitions() -> Result<(), BrainFailure> { HeatingMode::Circulate(CirculateMode::default()), HeatingMode::On(OnMode::default()), )?; + + test_transition_between( - HeatingMode::DhwOnly(DhwOnlyMode::from_time( - DhwTemps { sensor: Sensor::TKBT, min: 0.0, max: 47.0, extra: None }, - Utc::now(), - )), + HeatingMode::DhwOnly(DhwOnlyMode::new()), + //DhwTemps { sensor: Sensor::TKBT, min: 0.0, max: 47.0, extra: None }, NOW HeatingMode::off(), )?; @@ -338,8 +338,7 @@ fn test_overrun_scenarios() { println!("Mode: {:?}", mode); assert!(mode.is_some()); if let HeatingMode::DhwOnly(heat_up_to) = mode.unwrap() { - assert_eq!(heat_up_to.temps.sensor, Sensor::TKBT); - assert_eq!(heat_up_to.temps.max, 46.0) // Fine to have this lower of the two as it will increase anyway if needed. + // Nothing else to check } else { panic!("Should have been heat up to mode.") } diff --git a/src/brain/modes/on.rs b/src/brain/modes/on.rs index 20c56cd..7cb4f16 100644 --- a/src/brain/modes/on.rs +++ b/src/brain/modes/on.rs @@ -93,29 +93,10 @@ impl Mode for OnMode { let temps = temps.unwrap(); if !info_cache.heating_on() { - // TODO: 6 minute / overrun should move to Intention / tracking out of state. - let running_for = self.started.elapsed(); - let min_runtime = config.get_min_hp_runtime(); - if running_for < *min_runtime.get_min_runtime() { - warn!( - "Warning: Carrying on until the {} second mark or safety cut off: {}", - min_runtime.get_min_runtime().as_secs(), - min_runtime.get_safety_cut_off() - ); - let remaining = *min_runtime.get_min_runtime() - running_for; - let end = time.get_utc_time() + chrono::Duration::from_std(remaining).unwrap(); - return Ok(Intention::SwitchForce(HeatingMode::DhwOnly( - DhwOnlyMode::from_time( - DhwTemps { - sensor: min_runtime.get_safety_cut_off().get_target_sensor().clone(), - min: 0.0, - max: min_runtime.get_safety_cut_off().get_target_temp(), - extra: None, - }, - end - ) - ))); - } + // Finish mode should pick up any overrun whether considering + // minimum run time or not. + // TODO: config.get_min_hp_runtime(); + // min_runtime.get_safety_cut_off().get_target_sensor().clone(), return Ok(Intention::finish()); } diff --git a/src/brain/python_like/config/mod.rs b/src/brain/python_like/config/mod.rs index 06598fd..31f4cd6 100644 --- a/src/brain/python_like/config/mod.rs +++ b/src/brain/python_like/config/mod.rs @@ -111,6 +111,10 @@ impl PythonBrainConfig { pub fn get_min_hp_runtime(&self) -> &MinHeatPumpRuntime { &self.min_hp_runtime } + + pub fn _add_dhw_slot(&mut self, slot: overrun_config::DhwBap) { + self.additive_config.overrun_during.slots.push(slot); + } } impl Default for PythonBrainConfig { diff --git a/src/brain/python_like/config/overrun_config.rs b/src/brain/python_like/config/overrun_config.rs index ca95e78..dec876e 100644 --- a/src/brain/python_like/config/overrun_config.rs +++ b/src/brain/python_like/config/overrun_config.rs @@ -10,7 +10,7 @@ use std::fmt::{Display, Formatter}; #[derive(Deserialize, Clone, Debug, PartialEq, Default)] pub struct OverrunConfig { - slots: Vec, + pub slots: Vec, } impl OverrunConfig { @@ -148,13 +148,13 @@ impl DhwBap { } #[cfg(test)] - pub fn new_with_min(slot: ZonedSlot, temp: f32, sensor: Sensor, min_temp: f32) -> Self { - assert!(min_temp < temp, "min_temp should be less than temp"); + pub fn new_with_min(slot: ZonedSlot, max_temp: f32, sensor: Sensor, min_temp: f32) -> Self { + assert!(min_temp < max_temp, "min_temp should be less than max_temp"); Self { slot, disable_below: None, temps: DhwTemps { - sensor, min: min_temp, max: temp, extra: None + sensor, min: min_temp, max: max_temp, extra: None } } } diff --git a/src/brain/python_like/test.rs b/src/brain/python_like/test.rs index d6ab7c4..8bd37b4 100644 --- a/src/brain/python_like/test.rs +++ b/src/brain/python_like/test.rs @@ -141,8 +141,17 @@ temps = { sensor = "TKBT", min = 30.0, max = 55.0 } #[test_log::test] fn test_ignore_wiser_into_overrun() -> Result<(), BrainFailure> { let rt = Runtime::new().expect("Failed to create runtime."); - let config = + let mut config: PythonBrainConfig = toml::from_str(IGNORE_WISER_OVERRUN_CONFIG_STR).expect("Failed to deserialize config"); + + // TODO: Include in config above + config._add_dhw_slot(DhwBap::new_with_min( + utc_time_slot(13, 00, 00, 15, 00, 00), + 55.0, + Sensor::TKBT, + 30.0) + ); + let mut brain = PythonBrain::new(config); let (mut io_bundle, mut handle) = new_dummy_io(); @@ -182,12 +191,7 @@ fn test_ignore_wiser_into_overrun() -> Result<(), BrainFailure> { ); brain.run(&rt, &mut io_bundle, &time_provider)?; - let expected_mode = HeatingMode::DhwOnly(DhwOnlyMode::from_overrun(&DhwBap::new_with_min( - utc_time_slot(13, 00, 00, 15, 00, 00), - 55.0, - Sensor::TKBT, - 30.0, - ))); + let expected_mode = HeatingMode::DhwOnly(DhwOnlyMode::new()); assert_eq!(brain.heating_mode, Some(expected_mode)); Ok(())