Compare commits

..

4 Commits

Author SHA1 Message Date
b9bbc4b6e2
save compiling state during refactor 2025-04-18 14:54:57 +02:00
6fa332e708
WIP refactor plant_state 2025-04-18 14:54:47 +02:00
79113530b8
WIP introduce plant_state module 2025-03-27 22:28:41 +01:00
76835b23b1
add config field to enable moisture sensor a 2025-03-27 21:48:42 +01:00
5 changed files with 147 additions and 145 deletions

View File

@ -4,7 +4,7 @@ use std::{
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use chrono::{DateTime, Datelike, Timelike}; use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc};
use chrono_tz::{Europe::Berlin, Tz}; use chrono_tz::{Europe::Berlin, Tz};
use esp_idf_hal::delay::Delay; use esp_idf_hal::delay::Delay;
@ -29,7 +29,7 @@ mod plant_state;
mod tank; mod tank;
pub mod util; pub mod util;
use plant_state::PlantState; use plant_state::{PlantInfo, PlantState};
use tank::*; use tank::*;
const TIME_ZONE: Tz = Berlin; const TIME_ZONE: Tz = Berlin;
@ -406,21 +406,8 @@ fn safe_main() -> anyhow::Result<()> {
} }
}; };
let plantstate: [PlantState; PLANT_COUNT] = let mut plantstate: [PlantState; PLANT_COUNT] =
core::array::from_fn(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])); 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 let pump_required = plantstate
.iter() .iter()
@ -470,6 +457,7 @@ fn safe_main() -> anyhow::Result<()> {
} }
} }
} }
//update_plant_state(&mut plantstate, &mut board, &config);
let is_day = board.is_day(); let is_day = board.is_day();
let state_of_charge = board.state_charge_percent().unwrap_or(0); let state_of_charge = board.state_charge_percent().unwrap_or(0);
@ -634,6 +622,40 @@ fn main() {
} }
} }
fn time_to_string_utc(value_option: Option<DateTime<Utc>>) -> 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<DateTime<Tz>>) -> 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<u8>, error: &Option<SensorError>, 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<T: Display>(value: Result<T>) -> String { fn to_string<T: Display>(value: Result<T>) -> String {
return match value { return match value {
Ok(v) => v.to_string(), Ok(v) => v.to_string(),
@ -643,7 +665,7 @@ fn to_string<T: Display>(value: Result<T>) -> String {
}; };
} }
pub fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool { fn in_time_range(cur: &DateTime<Tz>, start: u8, end: u8) -> bool {
let curhour = cur.hour() as u8; let curhour = cur.hour() as u8;
//eg 10-14 //eg 10-14
if start < end { if start < end {

View File

@ -1,40 +1,38 @@
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use measurements::humidity;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
config::{self, PlantConfig}, config::{self, PlantConfig},
in_time_range, plant_hal, plant_hal::{self, PLANT_COUNT},
}; };
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 5500.; // 60kHz (500Hz margin) 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 const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really really dry, think like cactus levels
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub enum MoistureSensorError { pub enum HumiditySensorError {
ShortCircuit { hz: f32, max: f32 }, ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 }, OpenLoop { hz: f32, min: f32 },
BoardError(String),
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub enum MoistureSensorState { pub enum HumiditySensorState {
Disabled, Disabled,
MoistureValue { raw_hz: f32, moisture_percent: f32 }, HumidityValue { raw_hz: f32, moisture_percent: f32 },
SensorError(MoistureSensorError), SensorError(HumiditySensorError),
BoardError(String),
} }
impl MoistureSensorState { impl HumiditySensorState {
pub fn is_err(&self) -> Option<&MoistureSensorError> { pub fn is_err(&self) -> bool {
match self { matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_))
MoistureSensorState::SensorError(moisture_sensor_error) => Some(moisture_sensor_error),
_ => None,
}
} }
pub fn moisture_percent(&self) -> Option<f32> { pub fn moisture_percent(&self) -> Option<f32> {
if let MoistureSensorState::MoistureValue { if let HumiditySensorState::HumidityValue {
raw_hz: _, raw_hz,
moisture_percent, moisture_percent,
} = self } = self
{ {
@ -45,7 +43,7 @@ impl MoistureSensorState {
} }
} }
impl MoistureSensorState {} impl HumiditySensorState {}
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub enum PumpError { pub enum PumpError {
@ -61,41 +59,30 @@ pub struct PumpState {
previous_pump: Option<DateTime<Utc>>, previous_pump: Option<DateTime<Utc>>,
} }
impl PumpState { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
fn is_err(&self, plant_config: &PlantConfig) -> Option<PumpError> {
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 { pub enum PlantWateringMode {
OFF, OFF,
TargetMoisture, TargetMoisture,
TimerOnly, TimerOnly,
} }
pub enum PlantError {}
pub struct PlantState { pub struct PlantState {
pub sensor_a: MoistureSensorState, pub sensor_a: HumiditySensorState,
pub sensor_b: MoistureSensorState, pub sensor_b: HumiditySensorState,
pub pump: PumpState, pub pump: PumpState,
} }
fn map_range_moisture(s: f32) -> Result<f32, MoistureSensorError> { fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> {
if s < MOIST_SENSOR_MIN_FREQUENCY { if s < MOIST_SENSOR_MIN_FREQUENCY {
return Err(MoistureSensorError::OpenLoop { return Err(HumiditySensorError::OpenLoop {
hz: s, hz: s,
min: MOIST_SENSOR_MIN_FREQUENCY, min: MOIST_SENSOR_MIN_FREQUENCY,
}); });
} }
if s > MOIST_SENSOR_MAX_FREQUENCY { if s > MOIST_SENSOR_MAX_FREQUENCY {
return Err(MoistureSensorError::ShortCircuit { return Err(HumiditySensorError::ShortCircuit {
hz: s, hz: s,
max: MOIST_SENSOR_MAX_FREQUENCY, max: MOIST_SENSOR_MAX_FREQUENCY,
}); });
@ -115,34 +102,30 @@ impl PlantState {
let sensor_a = if config.sensor_a { let sensor_a = if config.sensor_a {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) {
Ok(raw) => match map_range_moisture(raw) { Ok(raw) => match map_range_moisture(raw) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue { Ok(moisture_percent) => HumiditySensorState::HumidityValue {
raw_hz: raw, raw_hz: raw,
moisture_percent, moisture_percent,
}, },
Err(err) => MoistureSensorState::SensorError(err), Err(err) => HumiditySensorState::SensorError(err),
}, },
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( Err(err) => HumiditySensorState::BoardError(err.to_string()),
err.to_string(),
)),
} }
} else { } else {
MoistureSensorState::Disabled HumiditySensorState::Disabled
}; };
let sensor_b = if config.sensor_b { let sensor_b = if config.sensor_b {
match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) { match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) {
Ok(raw) => match map_range_moisture(raw) { Ok(raw) => match map_range_moisture(raw) {
Ok(moisture_percent) => MoistureSensorState::MoistureValue { Ok(moisture_percent) => HumiditySensorState::HumidityValue {
raw_hz: raw, raw_hz: raw,
moisture_percent, moisture_percent,
}, },
Err(err) => MoistureSensorState::SensorError(err), Err(err) => HumiditySensorState::SensorError(err),
}, },
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( Err(err) => HumiditySensorState::BoardError(err.to_string()),
err.to_string(),
)),
} }
} else { } else {
MoistureSensorState::Disabled HumiditySensorState::Disabled
}; };
let previous_pump = board.last_pump_time(plant_id); let previous_pump = board.last_pump_time(plant_id);
let consecutive_pump_count = board.consecutive_pump_count(plant_id); let consecutive_pump_count = board.consecutive_pump_count(plant_id);
@ -161,9 +144,6 @@ impl PlantState {
} }
pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool { pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
if matches!(plant_conf.mode, PlantWateringMode::OFF) {
return false;
}
self.pump.previous_pump.is_some_and(|last_pump| { self.pump.previous_pump.is_some_and(|last_pump| {
last_pump last_pump
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
@ -174,26 +154,7 @@ impl PlantState {
} }
pub fn is_err(&self) -> bool { pub fn is_err(&self) -> bool {
self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some() self.sensor_a.is_err() || self.sensor_b.is_err()
}
pub fn plant_moisture(
&self,
) -> (
Option<f32>,
(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( pub fn needs_to_be_watered(
@ -204,8 +165,18 @@ impl PlantState {
match plant_conf.mode { match plant_conf.mode {
PlantWateringMode::OFF => false, PlantWateringMode::OFF => false,
PlantWateringMode::TargetMoisture => { PlantWateringMode::TargetMoisture => {
let (moisture_percent, _) = self.plant_moisture(); let moisture_percent = match (
if let Some(moisture_percent) = moisture_percent { 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) { if self.pump_in_timeout(plant_conf, current_time) {
false false
} else { } else {
@ -215,10 +186,6 @@ impl PlantState {
false false
} }
} }
} else {
// in case no moisture can be determined do not water plant
return false;
}
} }
PlantWateringMode::TimerOnly => { PlantWateringMode::TimerOnly => {
if self.pump_in_timeout(plant_conf, current_time) { if self.pump_in_timeout(plant_conf, current_time) {
@ -229,53 +196,61 @@ impl PlantState {
} }
} }
} }
}
pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo { //fn update_plant_state(
PlantInfo { // plantstate: &mut [PlantInfo; PLANT_COUNT],
sensor_a: &self.sensor_a, // board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
sensor_b: &self.sensor_b, // config: &PlantControllerConfig,
mode: plant_conf.mode, //) {
do_water: self.needs_to_be_watered(plant_conf, current_time), // for plant in 0..PLANT_COUNT {
dry: if let Some(moisture_percent) = self.plant_moisture().0 { // let state = &plantstate[plant];
moisture_percent < plant_conf.target_moisture // let plant_config = &config.plants[plant];
} else { //
false // let mode = format!("{:?}", plant_config.mode);
}, //
cooldown: self.pump_in_timeout(plant_conf, current_time), // let plant_dto = PlantStateMQTT {
out_of_work_hour: in_time_range( // a: &sensor_to_string(
current_time, // &state.a,
plant_conf.pump_hour_start, // &state.sensor_error_a,
plant_conf.pump_hour_end, // plant_config.mode != PlantWateringMode::OFF,
), // ),
consecutive_pump_count: self.pump.consecutive_pump_count, // a_raw: &state.a_raw.unwrap_or(0).to_string(),
pump_error: self.pump.is_err(plant_conf), // b: &sensor_to_string(&state.b, &state.sensor_error_b, plant_config.sensor_b),
last_pump: self // b_raw: &state.b_raw.unwrap_or(0).to_string(),
.pump // active: state.active,
.previous_pump // mode: &mode,
.map(|t| t.with_timezone(&current_time.timezone())), // last_pump: &time_to_string_utc(board.last_pump_time(plant)),
next_pump: if matches!( // next_pump: &time_to_string(state.next_pump),
plant_conf.mode, // consecutive_pump_count: state.consecutive_pump_count,
PlantWateringMode::TimerOnly | PlantWateringMode::TargetMoisture // cooldown: state.cooldown,
) { // dry: state.dry,
self.pump.previous_pump.and_then(|last_pump| { // not_effective: state.not_effective,
last_pump // out_of_work_hour: state.out_of_work_hour,
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into())) // pump_error: state.pump_error,
.map(|t| t.with_timezone(&current_time.timezone())) // };
}) //
} else { // match serde_json::to_string(&plant_dto) {
None // 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);
// }
// };
// }
//}
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked /// State of a single plant to be tracked
pub struct PlantInfo<'a> { pub struct PlantInfo {
/// state of humidity sensor on bank a /// state of humidity sensor on bank a
sensor_a: &'a MoistureSensorState, sensor_a: HumiditySensorState,
/// state of humidity sensor on bank b /// state of humidity sensor on bank b
sensor_b: &'a MoistureSensorState, sensor_b: HumiditySensorState,
/// configured plant watering mode /// configured plant watering mode
mode: PlantWateringMode, mode: PlantWateringMode,
/// plant needs to be watered /// plant needs to be watered
@ -284,9 +259,13 @@ pub struct PlantInfo<'a> {
dry: bool, dry: bool,
/// plant irrigation cooldown is active /// plant irrigation cooldown is active
cooldown: bool, 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? /// 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, out_of_work_hour: bool,
/// how often has the pump been watered without reaching target moisture /// is pump currently running
active: bool,
/// how often has the logic determined that plant should have been irrigated but wasn't
consecutive_pump_count: u32, consecutive_pump_count: u32,
pump_error: Option<PumpError>, pump_error: Option<PumpError>,
/// last time when pump was active /// last time when pump was active

View File

@ -1,3 +1,4 @@
pub trait LimitPrecision { pub trait LimitPrecision {
fn to_precision(self, presision: i32) -> Self; fn to_precision(self, presision: i32) -> Self;
} }

View File

@ -139,13 +139,13 @@ export class PlantView {
} }
update(a: number, b: number) { update(a: number, b: number) {
if (a == 200){ if (a = 200){
this.moistureA.innerText = "error" this.moistureA.innerText = "error"
} else { } else {
this.moistureA.innerText = String(a) this.moistureA.innerText = String(a)
} }
if (b == 200){ if (b = 200){
this.moistureB.innerText = "error" this.moistureB.innerText = "error"
} else { } else {
this.moistureB.innerText = String(b) this.moistureB.innerText = String(b)