WIP refactor plant_state

This commit is contained in:
2025-04-18 01:05:12 +02:00
parent cf31ce8d43
commit 2b5c1da484
7 changed files with 263 additions and 409 deletions

View File

@@ -1,68 +1,110 @@
use chrono::{DateTime, Utc};
use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use serde::Serialize;
use measurements::humidity;
use serde::{Deserialize, Serialize};
use crate::{config, plant_hal};
use crate::{
config::{self, PlantConfig},
plant_hal::{self, PLANT_COUNT},
};
const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5500; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: u32 = 150; // this is really really dry, think like cactus levels
pub enum HumiditySensorError{
ShortCircuit{hz: f32, max: f32},
OpenLoop{hz: f32, min: f32}
pub enum HumiditySensorError {
ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 },
}
#[derive(Debug, PartialEq)]
pub enum HumiditySensorState {
Disabled,
HumidityValue{raw_hz: u32, moisture_percent: f32},
HumidityValue { raw_hz: u32, moisture_percent: f32 },
SensorError(HumiditySensorError),
BoardError(String)
BoardError(String),
}
impl HumiditySensorState {
pub fn is_err(&self) -> bool {
matches!(self, Self::SensorError(_)) || matches!(self, Self::BoardError(_))
}
pub fn moisture_percent(&self) -> Option<f32> {
if let HumiditySensorState::HumidityValue {
raw_hz,
moisture_percent,
} = self
{
Some(moisture_percent)
} else {
None
}
}
}
pub enum PumpError {}
impl HumiditySensorState {}
#[derive(Debug, PartialEq)]
pub enum PumpError {
PumpNotWorking {
failed_attempts: usize,
max_allowed_failures: usize,
},
}
pub struct PumpState {
consecutive_pump_count: u32,
previous_pump: Option<DateTime<Utc>>
previous_pump: Option<DateTime<Utc>>,
}
pub enum PlantError{}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PlantWateringMode {
OFF,
TargetMoisture,
TimerOnly,
}
pub enum PlantError {}
pub struct PlantState {
sensor_a: HumiditySensorState,
sensor_b: HumiditySensorState,
pump: PumpState,
pub sensor_a: HumiditySensorState,
pub sensor_b: HumiditySensorState,
pub pump: PumpState,
}
fn map_range_moisture(s: f32) -> Result<f32, HumiditySensorError> {
if s < MOIST_SENSOR_MIN_FREQUENCY {
return Err(HumiditySensorError::OpenCircuit { hz: s, min: FROM.0 });
return Err(HumiditySensorError::OpenCircuit {
hz: s,
min: MOIST_SENSOR_MIN_FREQUENCY,
});
}
if s > MOIST_SENSOR_MAX_FREQUENCY {
return Err(HumiditySensorError::ShortCircuit { hz: s, max: FROM.1 });
return Err(HumiditySensorError::ShortCircuit {
hz: s,
max: MOIST_SENSOR_MAX_FREQUENCY,
});
}
let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100 / (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY);
let moisture_percent = (s - MOIST_SENSOR_MIN_FREQUENCY) * 100
/ (MOIST_SENSOR_MAX_FREQUENCY - MOIST_SENSOR_MIN_FREQUENCY);
return Ok(moisture_percent);
}
impl PlantState {
pub fn read_hardware_state(
plant_id: usize,
board: &mut plant_hal::PlantCtrlBoard,
config: &config::PlantConfig
config: &config::PlantConfig,
) -> Self {
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 { raw_hz: raw, moisture_percent },
Err(err) => HumiditySensorState::SensorError(err),
}
Ok(raw) => match map_range_moisture(raw) {
Ok(moisture_percent) => HumiditySensorState::HumidityValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => HumiditySensorState::SensorError(err),
},
Err(err) => HumiditySensorState::BoardError(err.to_string()),
}
@@ -71,11 +113,12 @@ impl PlantState {
};
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 { raw_hz: raw, moisture_percent },
Err(err) => HumiditySensorState::SensorError(err),
}
Ok(raw) => match map_range_moisture(raw) {
Ok(moisture_percent) => HumiditySensorState::HumidityValue {
raw_hz: raw,
moisture_percent,
},
Err(err) => HumiditySensorState::SensorError(err),
},
Err(err) => HumiditySensorState::BoardError(err.to_string()),
}
@@ -84,45 +127,145 @@ impl PlantState {
};
let previous_pump = board.last_pump_time(plant_id);
let consecutive_pump_count = board.consecutive_pump_count(plant_id);
Self {
let state = Self {
sensor_a,
sensor_b,
pump: PumpState { consecutive_pump_count , previous_pump}
pump: PumpState {
consecutive_pump_count,
previous_pump,
},
};
if state.is_err() {
board.fault(plant_id, true);
}
state
}
pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
self.pump.previous_pump.is_some_and(|last_pump| {
last_pump
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
.is_some_and(|earliest_next_allowed_pump| {
earliest_next_allowed_pump > *current_time
})
})
}
pub fn is_err(&self) -> bool {
self.sensor_a.is_err() || self.sensor_b.is_err()
}
pub fn needs_to_be_watered(
&self,
plant_conf: &PlantConfig,
current_time: &DateTime<Tz>,
) -> bool {
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 {
false
}
}
}
PlantWateringMode::TimerOnly => {
if self.pump_in_timeout(plant_conf, current_time) {
false
} else {
true
}
}
}
}
}
#[derive(Debug, PartialEq, Default, Serialize)]
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);
}
};
}
}
#[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked
pub struct PlantInfo {
/// state of humidity sensor on bank a
a: HumiditySensorState,
/// raw measured frequency value for sensor on bank a in hertz
a_raw: Option<u32>,
sensor_a: HumiditySensorState,
/// state of humidity sensor on bank b
b: HumiditySensorState,
/// raw measured frequency value for sensor on bank b in hertz
b_raw: Option<u32>,
sensor_b: HumiditySensorState,
/// configured plant watering mode
mode: config::Mode,
/// how often has the logic determined that plant should have been irrigated but wasn't
consecutive_pump_count: u32,
mode: config::PlantWateringMode,
/// plant needs to be watered
do_water: bool,
/// is plant considerd to be dry according to settings
dry: bool,
/// is pump currently running
active: bool,
/// TODO: convert this to an Option<PumpErorr> enum for every case that can happen
pump_error: bool,
/// if pump count has increased higher than configured limit
not_effective: bool,
/// plant irrigation cooldown is active
cooldown: bool,
/// we want to irrigate but tank is empty
no_water: bool,
/// pump should not be watered at this time of day
/// 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
consecutive_pump_count: u32,
pump_error: Option<PumpError>,
/// last time when pump was active
last_pump: Option<DateTime<Tz>>,
/// next time when pump should activate