Compare commits

...

3 Commits

Author SHA1 Message Date
bbbe6d846f
streamline mqtt state information handling 2025-03-15 14:50:50 +01:00
184c2b52cb
use proper error handling 2025-03-15 01:13:48 +01:00
e0dc3d21c7
fix tank.rs errors after intial implementation 2025-03-14 23:40:23 +01:00
3 changed files with 124 additions and 85 deletions

View File

@ -401,41 +401,32 @@ fn safe_main() -> anyhow::Result<()> {
let dry_run = false; let dry_run = false;
let tank_state = determine_tank_state(&mut board, &config); let tank_state = determine_tank_state(&mut board, &config);
let mut tank_state_mqtt = TankStateMQTT {
enough_water: tank_state.enough_water,
left_ml: tank_state.left_ml,
warn_level: tank_state.warn_level,
sensor_error: tank_state.sensor_error,
raw: tank_state.raw,
water_frozen: "".to_owned(),
};
let mut water_frozen = false; let mut water_frozen = false;
let mut temp: Option<f32> = None; let mut attempt = 1;
for _attempt in 0..5 { let water_temp: Result<f32, anyhow::Error> = loop {
let water_temperature = board.water_temperature_c(); let temp = board.water_temperature_c();
match water_temperature { match &temp {
Ok(res) => { Ok(res) => {
temp = Some(res); println!("Water temp is {}", res);
break; break temp
} }
Err(err) => { Err(err) => {
println!("Could not get water temp {} attempt {}", err, _attempt) println!("Could not get water temp {} attempt {}", err, attempt)
} }
} }
if attempt == 5 {
break temp
} }
match temp { attempt += 1;
Some(res) => { };
println!("Water temp is {}", res); if let Ok(res) = water_temp {
if res < 4_f32 { if res < WATER_FROZEN_THRESH {
water_frozen = true; water_frozen = true;
} }
tank_state_mqtt.water_frozen = water_frozen.to_string();
}
None => tank_state_mqtt.water_frozen = "tank sensor error".to_owned(),
} }
match serde_json::to_string(&tank_state_mqtt) { match serde_json::to_string(&tank_state.as_mqtt_info(&config.tank, water_temp)) {
Ok(state) => { Ok(state) => {
let _ = board.mqtt_publish(&config, "/water", state.as_bytes()); let _ = board.mqtt_publish(&config, "/water", state.as_bytes());
} }
@ -611,7 +602,7 @@ fn determine_state_target_moisture_for_plant(
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &PlantControllerConfig, config: &PlantControllerConfig,
tank_state: &TankInfo, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
let plant_config = &config.plants[plant]; let plant_config = &config.plants[plant];
@ -657,10 +648,17 @@ fn determine_state_target_moisture_for_plant(
if a_low || b_low { if a_low || b_low {
state.dry = true; state.dry = true;
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error { match tank_state.enough_water(&config.tank) {
//ignore is ok Err(_tank_err) => {
} else if !tank_state.enough_water { if !config.tank.tank_allow_pumping_if_sensor_error {
state.no_water = true; // ignore is ok
// wtf does this meen, shouldn't something happen if the configuration specifies
// that no water should flow if there was an error?
}
},
Ok(enough_water) => {
state.no_water = !enough_water
},
} }
} }
let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap(); let duration = TimeDelta::try_minutes(plant_config.pump_cooldown_min as i64).unwrap();
@ -701,7 +699,7 @@ fn determine_state_timer_only_for_plant(
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &PlantControllerConfig, config: &PlantControllerConfig,
tank_state: &TankInfo, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
let plant_config = &config.plants[plant]; let plant_config = &config.plants[plant];
@ -716,10 +714,15 @@ fn determine_state_timer_only_for_plant(
state.next_pump = Some(europe_time); state.next_pump = Some(europe_time);
state.cooldown = true; state.cooldown = true;
} else { } else {
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error { match tank_state.enough_water(&config.tank) {
Err(_tank_err) => {
if !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true; state.do_water = true;
} else if !tank_state.enough_water { }
state.no_water = true; },
Ok(enough_water) => {
state.no_water = !enough_water;
},
} }
} }
} }
@ -739,7 +742,7 @@ fn determine_state_timer_and_deadzone_for_plant(
plant: usize, plant: usize,
state: &mut PlantState, state: &mut PlantState,
config: &PlantControllerConfig, config: &PlantControllerConfig,
tank_state: &TankInfo, tank_state: &TankState,
cur: DateTime<Tz>, cur: DateTime<Tz>,
) { ) {
let plant_config = &config.plants[plant]; let plant_config = &config.plants[plant];
@ -762,10 +765,15 @@ fn determine_state_timer_and_deadzone_for_plant(
state.out_of_work_hour = true; state.out_of_work_hour = true;
} }
if !state.cooldown && !state.out_of_work_hour { if !state.cooldown && !state.out_of_work_hour {
if tank_state.sensor_error && !config.tank.tank_allow_pumping_if_sensor_error { match tank_state.enough_water(&config.tank) {
Err(_tank_err) => {
if !config.tank.tank_allow_pumping_if_sensor_error {
state.do_water = true; state.do_water = true;
} else if !tank_state.enough_water { }
state.no_water = true; },
Ok(enough_water) => {
state.no_water = !enough_water;
},
} }
} }
} }
@ -783,7 +791,7 @@ fn determine_state_timer_and_deadzone_for_plant(
fn determine_plant_state( fn determine_plant_state(
plantstate: &mut [PlantState; PLANT_COUNT], plantstate: &mut [PlantState; PLANT_COUNT],
cur: DateTime<Tz>, cur: DateTime<Tz>,
tank_state: &TankInfo, tank_state: &TankState,
config: &PlantControllerConfig, config: &PlantControllerConfig,
board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
) { ) {

View File

@ -472,7 +472,7 @@ impl PlantCtrlBoard<'_> {
} }
/// return median tank sensor value in milli volt /// return median tank sensor value in milli volt
pub fn tank_sensor_voltage(&mut self) -> Result<u16> { pub fn tank_sensor_voltage(&mut self) -> Result<f32> {
let delay = Delay::new_default(); let delay = Delay::new_default();
self.tank_power.set_high()?; self.tank_power.set_high()?;
//let stabilize //let stabilize

View File

@ -1,37 +1,25 @@
use crate::config::{self, PlantControllerConfig, TankConfig}; use serde::Serialize;
use crate::{config::{PlantControllerConfig, TankConfig}, plant_hal::PlantCtrlBoard};
const OPEN_TANK_VOLTAGE: f32 = 3.0; const OPEN_TANK_VOLTAGE: f32 = 3.0;
pub const WATER_FROZEN_THRESH: f32 = 4.0;
#[derive(Debug, PartialEq, Default)] #[derive(Debug, Clone, Serialize)]
/// State data for water tank
///
/// TODO unify with TankStateMQTT
pub struct TankInfo {
/// is there enough water in the tank
enough_water: bool,
/// warning that water needs to be refilled soon
warn_level: bool,
/// estimation how many ml are still in tank
left_ml: u32,
/// if there is was an issue with the water level sensor
/// TODO merge with left_ml as Result<u32, error_type>
sensor_error: bool,
/// raw water sensor value
raw: u16,
}
pub enum TankError { pub enum TankError {
SensorDisabled, SensorDisabled,
SensorMissing(f32), SensorMissing(f32),
SensorValueError { value: f32, min: f32, max: f32 }, SensorValueError { value: f32, min: f32, max: f32 },
BoardError(String)
} }
pub enum TankState { pub enum TankState {
TankSensorPresent(u16), TankSensorPresent(f32),
TankSensorError(TankError),
TankSensorDisabled, TankSensorDisabled,
} }
fn raw_volatge_to_divider_percent(raw_value_mv: u16) -> Result<f32, TankError> { fn raw_volatge_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
if raw_value_mv > OPEN_TANK_VOLTAGE { if raw_value_mv > OPEN_TANK_VOLTAGE {
return Err(TankError::SensorMissing(raw_value_mv)); return Err(TankError::SensorMissing(raw_value_mv));
} }
@ -51,19 +39,19 @@ fn raw_volatge_to_divider_percent(raw_value_mv: u16) -> Result<f32, TankError> {
} }
fn raw_voltage_to_tank_fill_percent( fn raw_voltage_to_tank_fill_percent(
raw_value_mv: u16, raw_value_mv: f32,
config: &TankConfig, config: &TankConfig,
) -> Result<f32, TankError> { ) -> Result<f32, TankError> {
let divider_percent = raw_volatge_to_divider_percent(raw_value_mv)?; let divider_percent = raw_volatge_to_divider_percent(raw_value_mv)?;
if s < config.tank_empty_percent || s > config.tank_full_percent { if divider_percent < config.tank_empty_percent.into() || divider_percent > config.tank_full_percent.into() {
return Err(TankError::SensorValueError { return Err(TankError::SensorValueError {
value: divider_percent, value: divider_percent,
min: config.tank_empty_percent, min: config.tank_empty_percent.into(),
max: config.tank_full_percent, max: config.tank_full_percent.into(),
}); });
} }
Ok((divider_percent - config.tank_empty_percent) * 100 Ok((divider_percent - f32::from(config.tank_empty_percent)) * 100.
/ (config.tank_full_percent - config.tank_empty_percent)) / f32::from(config.tank_full_percent - config.tank_empty_percent))
} }
@ -71,8 +59,9 @@ impl TankState {
pub fn left_ml(&self, config: &TankConfig) -> Result<f32, TankError> { pub fn left_ml(&self, config: &TankConfig) -> Result<f32, TankError> {
match self { match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled), TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => { TankState::TankSensorPresent(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(raw_value_mv, config)?; let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
//TODO(judge) move logging to more sensible place //TODO(judge) move logging to more sensible place
//println!( //println!(
//"Tank sensor returned mv {} as {}% leaving {} ml useable", //"Tank sensor returned mv {} as {}% leaving {} ml useable",
@ -85,9 +74,10 @@ impl TankState {
pub fn enough_water(&self, config: &TankConfig) -> Result<bool, TankError> { pub fn enough_water(&self, config: &TankConfig) -> Result<bool, TankError> {
match self { match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled), TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => { TankState::TankSensorPresent(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(raw_value_mv, config)?; let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
if tank_fill_percent > config.tank_empty_percent { if tank_fill_percent > config.tank_empty_percent.into() {
//TODO(judge) move logging to more sensible place //TODO(judge) move logging to more sensible place
//println!( //println!(
//"Enough water, current percent is {}, minimum empty level is {}", //"Enough water, current percent is {}, minimum empty level is {}",
@ -104,9 +94,10 @@ impl TankState {
pub fn warn_level(&self, config: &TankConfig) -> Result<bool, TankError> { pub fn warn_level(&self, config: &TankConfig) -> Result<bool, TankError> {
match self { match self {
TankState::TankSensorDisabled => Err(TankError::SensorDisabled), TankState::TankSensorDisabled => Err(TankError::SensorDisabled),
TankState::TankSensorError(err) => Err(err.clone()),
TankState::TankSensorPresent(raw_value_mv) => { TankState::TankSensorPresent(raw_value_mv) => {
let tank_fill_percent = raw_voltage_to_tank_fill_percent(raw_value_mv, config)?; let tank_fill_percent = raw_voltage_to_tank_fill_percent(*raw_value_mv, config)?;
if tank_fill_percent < config.tank_warn_percent { if tank_fill_percent < config.tank_warn_percent.into() {
//TODO(judge) move logging to more sensible place //TODO(judge) move logging to more sensible place
//println!( //println!(
//"Low water, current percent is {}, minimum warn level is {}", //"Low water, current percent is {}, minimum warn level is {}",
@ -121,16 +112,32 @@ impl TankState {
}, },
} }
} }
}
#[derive(Serialize)] pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: Result<f32, anyhow::Error>) -> TankInfo {
pub struct TankStateMQTT { let mut tank_err: Option<TankError> = None;
enough_water: bool, let left_ml = match self.left_ml(config) {
warn_level: bool, Err(err) => { tank_err = Some(err); None },
left_ml: u32, Ok(left_ml) => Some(left_ml),
sensor_error: bool, };
raw: u16, let enough_water = self.enough_water(config).unwrap_or(false); //NOTE: is this correct if there is an error assume not enough water?
water_frozen: String, let warn_level = self.warn_level(config).unwrap_or(false); //NOTE: should no warn level be triggered if there is an error?
let raw = match self {
TankState::TankSensorDisabled
| TankState::TankSensorError(_) => None,
TankState::TankSensorPresent(raw_value_mv) => Some(*raw_value_mv),
};
TankInfo {
enough_water,
warn_level,
left_ml,
sensor_error: tank_err,
raw,
water_frozen: water_temp.as_ref().is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
water_temp: water_temp.as_ref().copied().ok(),
temp_sensor_error: water_temp.err().map(|err| err.to_string())
}
}
} }
pub fn determine_tank_state( pub fn determine_tank_state(
@ -138,9 +145,33 @@ pub fn determine_tank_state(
config: &PlantControllerConfig, config: &PlantControllerConfig,
) -> TankState { ) -> TankState {
if config.tank.tank_sensor_enabled { if config.tank.tank_sensor_enabled {
let raw_sensor_value_mv = board.tank_sensor_voltage(); match board.tank_sensor_voltage() {
TankState::TankSensorPresent(raw_sensor_value_mv) Ok(raw_sensor_value_mv) => TankState::TankSensorPresent(raw_sensor_value_mv),
Err(err) => TankState::TankSensorError(TankError::BoardError(err.to_string()))
}
} else { } else {
TankState::TankSensorDisabled TankState::TankSensorDisabled
} }
} }
#[derive(Debug, Serialize)]
/// Information structure send to mqtt for monitoring purposes
pub struct TankInfo {
/// is there enough water in the tank
enough_water: bool,
/// warning that water needs to be refilled soon
warn_level: bool,
/// estimation how many ml are still in tank
left_ml: Option<f32>,
/// if there is was an issue with the water level sensor
/// TODO merge with left_ml as Result<u32, error_type>
sensor_error: Option<TankError>,
/// raw water sensor value
raw: Option<f32>,
/// water in tank might be frozen
water_frozen: bool,
/// water temperature
water_temp: Option<f32>,
temp_sensor_error: Option<String>,
}