diff --git a/rust/src/main.rs b/rust/src/main.rs index f2b03da..20d7161 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{bail, Result}; -use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc}; +use chrono::{DateTime, Datelike, Timelike}; use chrono_tz::{Europe::Berlin, Tz}; use esp_idf_hal::delay::Delay; @@ -29,7 +29,7 @@ mod plant_state; mod tank; pub mod util; -use plant_state::{PlantInfo, PlantState}; +use plant_state::PlantState; use tank::*; const TIME_ZONE: Tz = Berlin; @@ -406,8 +406,21 @@ fn safe_main() -> anyhow::Result<()> { } }; - let mut plantstate: [PlantState; PLANT_COUNT] = + let plantstate: [PlantState; PLANT_COUNT] = core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); + for (plant_id, (plant_state, plant_conf)) in plantstate.iter().zip(&config.plants).enumerate() { + match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, &timezone_time)) { + Ok(state) => { + let plant_topic = format!("/plant{}", plant_id + 1); + let _ = board.mqtt_publish(&config, &plant_topic, state.as_bytes()); + //reduce speed as else messages will be dropped + Delay::new_default().delay_ms(200); + } + Err(err) => { + println!("Error publishing plant state {}", err); + } + }; + } let pump_required = plantstate .iter() @@ -447,17 +460,16 @@ fn safe_main() -> anyhow::Result<()> { //state.active = true; if !dry_run { board.pump(plant_id, true)?; - Delay::new_default().delay_ms(1000*plant_config.pump_time_s as u32); + Delay::new_default().delay_ms(1000 * plant_config.pump_time_s as u32); board.pump(plant_id, false)?; } - } else if !state.pump_in_timeout(&plant_config, &timezone_time){ + } else if !state.pump_in_timeout(&plant_config, &timezone_time) { // plant does not need to be watered and is not in timeout // -> reset consecutive pump count board.store_consecutive_pump_count(plant_id, 0); } } } - //update_plant_state(&mut plantstate, &mut board, &config); let is_day = board.is_day(); let state_of_charge = board.state_charge_percent().unwrap_or(0); @@ -622,40 +634,6 @@ fn main() { } } -fn time_to_string_utc(value_option: Option>) -> String { - let converted = value_option.and_then(|utc| Some(utc.with_timezone(&TIME_ZONE))); - return time_to_string(converted); -} - -fn time_to_string(value_option: Option>) -> String { - match value_option { - Some(value) => { - let europe_time = value.with_timezone(&TIME_ZONE); - if europe_time.year() > 2023 { - return europe_time.to_rfc3339(); - } else { - //initial value of 0 in rtc memory - return "N/A".to_owned(); - } - } - None => return "N/A".to_owned(), - }; -} - -fn sensor_to_string(value: &Option, error: &Option, enabled: bool) -> String { - if enabled { - match error { - Some(error) => return format!("{:?}", error), - None => match value { - Some(v) => return v.to_string(), - None => return "Error".to_owned(), - }, - } - } else { - return "disabled".to_owned(); - }; -} - fn to_string(value: Result) -> String { return match value { Ok(v) => v.to_string(), @@ -665,7 +643,7 @@ fn to_string(value: Result) -> String { }; } -fn in_time_range(cur: &DateTime, start: u8, end: u8) -> bool { +pub fn in_time_range(cur: &DateTime, start: u8, end: u8) -> bool { let curhour = cur.hour() as u8; //eg 10-14 if start < end { diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 412391b..7496caf 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -668,7 +668,7 @@ impl PlantCtrlBoard<'_> { ); results[repeat] = hz; } - results.sort_by(|a,b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord + results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord let mid = results.len() / 2; diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index aa2078a..98b3fb1 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -1,38 +1,40 @@ use chrono::{DateTime, TimeDelta, Utc}; use chrono_tz::Tz; -use measurements::humidity; use serde::{Deserialize, Serialize}; use crate::{ config::{self, PlantConfig}, - plant_hal::{self, PLANT_COUNT}, + in_time_range, plant_hal, }; const MOIST_SENSOR_MAX_FREQUENCY: f32 = 5500.; // 60kHz (500Hz margin) const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really really dry, think like cactus levels #[derive(Debug, PartialEq, Serialize)] -pub enum HumiditySensorError { +pub enum MoistureSensorError { ShortCircuit { hz: f32, max: f32 }, OpenLoop { hz: f32, min: f32 }, -} - -#[derive(Debug, PartialEq, Serialize)] -pub enum HumiditySensorState { - Disabled, - HumidityValue { raw_hz: f32, moisture_percent: f32 }, - SensorError(HumiditySensorError), BoardError(String), } -impl HumiditySensorState { - pub fn is_err(&self) -> bool { - matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_)) +#[derive(Debug, PartialEq, Serialize)] +pub enum MoistureSensorState { + Disabled, + MoistureValue { raw_hz: f32, moisture_percent: f32 }, + SensorError(MoistureSensorError), +} + +impl MoistureSensorState { + pub fn is_err(&self) -> Option<&MoistureSensorError> { + match self { + MoistureSensorState::SensorError(moisture_sensor_error) => Some(moisture_sensor_error), + _ => None, + } } pub fn moisture_percent(&self) -> Option { - if let HumiditySensorState::HumidityValue { - raw_hz, + if let MoistureSensorState::MoistureValue { + raw_hz: _, moisture_percent, } = self { @@ -43,7 +45,7 @@ impl HumiditySensorState { } } -impl HumiditySensorState {} +impl MoistureSensorState {} #[derive(Debug, PartialEq, Serialize)] pub enum PumpError { @@ -59,30 +61,41 @@ pub struct PumpState { previous_pump: Option>, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +impl PumpState { + fn is_err(&self, plant_config: &PlantConfig) -> Option { + if self.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 { + Some(PumpError::PumpNotWorking { + failed_attempts: self.consecutive_pump_count as usize, + max_allowed_failures: plant_config.max_consecutive_pump_count as usize, + }) + } else { + None + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] pub enum PlantWateringMode { OFF, TargetMoisture, TimerOnly, } -pub enum PlantError {} - pub struct PlantState { - pub sensor_a: HumiditySensorState, - pub sensor_b: HumiditySensorState, + pub sensor_a: MoistureSensorState, + pub sensor_b: MoistureSensorState, pub pump: PumpState, } -fn map_range_moisture(s: f32) -> Result { +fn map_range_moisture(s: f32) -> Result { if s < MOIST_SENSOR_MIN_FREQUENCY { - return Err(HumiditySensorError::OpenLoop { + return Err(MoistureSensorError::OpenLoop { hz: s, min: MOIST_SENSOR_MIN_FREQUENCY, }); } if s > MOIST_SENSOR_MAX_FREQUENCY { - return Err(HumiditySensorError::ShortCircuit { + return Err(MoistureSensorError::ShortCircuit { hz: s, max: MOIST_SENSOR_MAX_FREQUENCY, }); @@ -102,30 +115,34 @@ impl PlantState { let sensor_a = if config.sensor_a { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { Ok(raw) => match map_range_moisture(raw) { - Ok(moisture_percent) => HumiditySensorState::HumidityValue { + Ok(moisture_percent) => MoistureSensorState::MoistureValue { raw_hz: raw, moisture_percent, }, - Err(err) => HumiditySensorState::SensorError(err), + Err(err) => MoistureSensorState::SensorError(err), }, - Err(err) => HumiditySensorState::BoardError(err.to_string()), + Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( + err.to_string(), + )), } } else { - HumiditySensorState::Disabled + MoistureSensorState::Disabled }; let sensor_b = if config.sensor_b { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { Ok(raw) => match map_range_moisture(raw) { - Ok(moisture_percent) => HumiditySensorState::HumidityValue { + Ok(moisture_percent) => MoistureSensorState::MoistureValue { raw_hz: raw, moisture_percent, }, - Err(err) => HumiditySensorState::SensorError(err), + Err(err) => MoistureSensorState::SensorError(err), }, - Err(err) => HumiditySensorState::BoardError(err.to_string()), + Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( + err.to_string(), + )), } } else { - HumiditySensorState::Disabled + MoistureSensorState::Disabled }; let previous_pump = board.last_pump_time(plant_id); let consecutive_pump_count = board.consecutive_pump_count(plant_id); @@ -144,6 +161,9 @@ impl PlantState { } pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime) -> bool { + if matches!(plant_conf.mode, PlantWateringMode::OFF) { + return false; + } self.pump.previous_pump.is_some_and(|last_pump| { last_pump .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) @@ -154,7 +174,26 @@ impl PlantState { } pub fn is_err(&self) -> bool { - self.sensor_a.is_err() || self.sensor_b.is_err() + self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some() + } + + pub fn plant_moisture( + &self, + ) -> ( + Option, + (Option<&MoistureSensorError>, Option<&MoistureSensorError>), + ) { + match ( + self.sensor_a.moisture_percent(), + self.sensor_b.moisture_percent(), + ) { + (Some(moisture_a), Some(moisture_b)) => { + (Some((moisture_a + moisture_b) / 2.), (None, None)) + } + (Some(moisture_percent), _) => (Some(moisture_percent), (None, self.sensor_b.is_err())), + (_, Some(moisture_percent)) => (Some(moisture_percent), (self.sensor_a.is_err(), None)), + _ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())), + } } pub fn needs_to_be_watered( @@ -165,26 +204,20 @@ impl PlantState { match plant_conf.mode { PlantWateringMode::OFF => false, PlantWateringMode::TargetMoisture => { - let moisture_percent = match ( - self.sensor_a.moisture_percent(), - self.sensor_b.moisture_percent(), - ) { - (Some(moisture_a), Some(moisture_b)) => (moisture_a + moisture_b) / 2., - (Some(moisture_percent), _) => moisture_percent, - (_, Some(moisture_percent)) => moisture_percent, - _ => { - // Case for both sensors hitting an error do not water plant in this case - return false; - } - }; - if self.pump_in_timeout(plant_conf, current_time) { - false - } else { - if moisture_percent < plant_conf.target_moisture { - true - } else { + let (moisture_percent, _) = self.plant_moisture(); + if let Some(moisture_percent) = moisture_percent { + if self.pump_in_timeout(plant_conf, current_time) { false + } else { + if moisture_percent < plant_conf.target_moisture { + true + } else { + false + } } + } else { + // in case no moisture can be determined do not water plant + return false; } } PlantWateringMode::TimerOnly => { @@ -196,61 +229,53 @@ impl PlantState { } } } -} -//fn update_plant_state( -// plantstate: &mut [PlantInfo; PLANT_COUNT], -// board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, -// config: &PlantControllerConfig, -//) { -// for plant in 0..PLANT_COUNT { -// let state = &plantstate[plant]; -// let plant_config = &config.plants[plant]; -// -// let mode = format!("{:?}", plant_config.mode); -// -// let plant_dto = PlantStateMQTT { -// a: &sensor_to_string( -// &state.a, -// &state.sensor_error_a, -// plant_config.mode != PlantWateringMode::OFF, -// ), -// a_raw: &state.a_raw.unwrap_or(0).to_string(), -// b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b), -// b_raw: &state.b_raw.unwrap_or(0).to_string(), -// active: state.active, -// mode: &mode, -// last_pump: &time_to_string_utc(board.last_pump_time(plant)), -// next_pump: &time_to_string(state.next_pump), -// consecutive_pump_count: state.consecutive_pump_count, -// cooldown: state.cooldown, -// dry: state.dry, -// not_effective: state.not_effective, -// out_of_work_hour: state.out_of_work_hour, -// pump_error: state.pump_error, -// }; -// -// match serde_json::to_string(&plant_dto) { -// Ok(state) => { -// let plant_topic = format!("/plant{}", plant + 1); -// let _ = board.mqtt_publish(&config, &plant_topic, state.as_bytes()); -// //reduce speed as else messages will be dropped -// Delay::new_default().delay_ms(200); -// } -// Err(err) => { -// println!("Error publishing lightstate {}", err); -// } -// }; -// } -//} + pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime) -> PlantInfo { + PlantInfo { + sensor_a: &self.sensor_a, + sensor_b: &self.sensor_b, + mode: plant_conf.mode, + do_water: self.needs_to_be_watered(plant_conf, current_time), + dry: if let Some(moisture_percent) = self.plant_moisture().0 { + moisture_percent < plant_conf.target_moisture + } else { + false + }, + cooldown: self.pump_in_timeout(plant_conf, current_time), + out_of_work_hour: in_time_range( + current_time, + plant_conf.pump_hour_start, + plant_conf.pump_hour_end, + ), + consecutive_pump_count: self.pump.consecutive_pump_count, + pump_error: self.pump.is_err(plant_conf), + last_pump: self + .pump + .previous_pump + .map(|t| t.with_timezone(¤t_time.timezone())), + next_pump: if matches!( + plant_conf.mode, + PlantWateringMode::TimerOnly | PlantWateringMode::TargetMoisture + ) { + self.pump.previous_pump.and_then(|last_pump| { + last_pump + .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) + .map(|t| t.with_timezone(¤t_time.timezone())) + }) + } else { + None + }, + } + } +} #[derive(Debug, PartialEq, Serialize)] /// State of a single plant to be tracked -pub struct PlantInfo { +pub struct PlantInfo<'a> { /// state of humidity sensor on bank a - sensor_a: HumiditySensorState, + sensor_a: &'a MoistureSensorState, /// state of humidity sensor on bank b - sensor_b: HumiditySensorState, + sensor_b: &'a MoistureSensorState, /// configured plant watering mode mode: PlantWateringMode, /// plant needs to be watered @@ -259,13 +284,9 @@ pub struct PlantInfo { dry: bool, /// plant irrigation cooldown is active cooldown: bool, - /// we want to irrigate but tank is empty - no_water: bool, /// plant should not be watered at this time of day TODO: does this really belong here? Isn't this a global setting? out_of_work_hour: bool, - /// is pump currently running - active: bool, - /// how often has the logic determined that plant should have been irrigated but wasn't + /// how often has the pump been watered without reaching target moisture consecutive_pump_count: u32, pump_error: Option, /// last time when pump was active diff --git a/rust/src/util.rs b/rust/src/util.rs index b14fa95..3d46fb4 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -1,4 +1,3 @@ - pub trait LimitPrecision { fn to_precision(self, presision: i32) -> Self; }