use pamoja_kit::{Depletion, Surge, Thermostat};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Alert {
OutOfRange {
reading: f32,
},
RunningOut {
samples: u32,
},
ChangingFast {
rate: f32,
},
}
impl Alert {
pub fn kind(self) -> &'static str {
match self {
Alert::OutOfRange { .. } => "OutOfRange",
Alert::RunningOut { .. } => "RunningOut",
Alert::ChangingFast { .. } => "ChangingFast",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Reaction {
pub actuator: Option<bool>,
pub alert: Option<Alert>,
}
#[derive(Clone, Copy, Debug)]
enum Policy {
Setpoint {
thermostat: Thermostat,
setpoint: f32,
safe_band: f32,
},
Level {
depletion: Depletion,
warn_within: u32,
},
Surge {
surge: Surge,
},
Monitor,
}
#[derive(Clone, Copy, Debug)]
pub struct Controller {
policy: Policy,
}
impl Controller {
pub fn setpoint(setpoint: f32, hysteresis: f32, cooling: bool, safe_band: f32) -> Self {
let thermostat = if cooling {
Thermostat::cooling(setpoint, hysteresis)
} else {
Thermostat::heating(setpoint, hysteresis)
};
Self {
policy: Policy::Setpoint {
thermostat,
setpoint,
safe_band,
},
}
}
pub fn level(empty: f32, warn_within: u32) -> Self {
Self {
policy: Policy::Level {
depletion: Depletion::new(empty),
warn_within,
},
}
}
pub fn surge(rising: bool, limit: f32) -> Self {
let surge = if rising {
Surge::rising(limit)
} else {
Surge::falling(limit)
};
Self {
policy: Policy::Surge { surge },
}
}
pub fn monitor() -> Self {
Self {
policy: Policy::Monitor,
}
}
pub fn evaluate(&mut self, reading: f32) -> Reaction {
match &mut self.policy {
Policy::Setpoint {
thermostat,
setpoint,
safe_band,
} => {
let on = thermostat.update(reading);
let alert = if (reading - *setpoint).abs() > *safe_band {
Some(Alert::OutOfRange { reading })
} else {
None
};
Reaction {
actuator: Some(on),
alert,
}
}
Policy::Level {
depletion,
warn_within,
} => {
let warn_within = *warn_within;
let alert = depletion
.update(reading)
.filter(|samples| *samples <= warn_within)
.map(|samples| Alert::RunningOut { samples });
Reaction {
actuator: None,
alert,
}
}
Policy::Surge { surge } => {
let alert = surge
.update(reading)
.map(|rate| Alert::ChangingFast { rate });
Reaction {
actuator: None,
alert,
}
}
Policy::Monitor => Reaction::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn setpoint_switches_the_output_and_flags_excursions() {
let mut control = Controller::setpoint(5.0, 0.5, true, 3.0);
let hot = control.evaluate(9.0);
assert_eq!(hot.actuator, Some(true));
assert_eq!(hot.alert, Some(Alert::OutOfRange { reading: 9.0 }));
let warm = control.evaluate(6.0);
assert_eq!(warm.actuator, Some(true));
assert_eq!(warm.alert, None);
let cold = control.evaluate(4.0);
assert_eq!(cold.actuator, Some(false));
assert_eq!(cold.alert, None);
}
#[test]
fn heating_setpoint_switches_on_below_the_band() {
let mut control = Controller::setpoint(35.0, 5.0, false, 25.0);
assert_eq!(control.evaluate(28.0).actuator, Some(true)); assert_eq!(control.evaluate(42.0).actuator, Some(false)); }
#[test]
fn level_warns_only_inside_the_window() {
let mut control = Controller::level(0.0, 3);
assert_eq!(control.evaluate(10.0).alert, None); assert_eq!(control.evaluate(8.0).alert, None); assert_eq!(
control.evaluate(6.0).alert,
Some(Alert::RunningOut { samples: 3 })
); assert_eq!(control.evaluate(6.0).actuator, None); }
#[test]
fn surge_warns_on_a_rapid_rise_without_an_output() {
let mut control = Controller::surge(true, 0.5);
assert_eq!(control.evaluate(1.0).alert, None); assert_eq!(control.evaluate(1.25).alert, None); let flood = control.evaluate(2.0); assert_eq!(flood.alert, Some(Alert::ChangingFast { rate: 0.75 }));
assert_eq!(flood.actuator, None); }
#[test]
fn monitor_is_inert() {
let mut control = Controller::monitor();
let reaction = control.evaluate(42.0);
assert_eq!(reaction, Reaction::default());
}
}