WIP refactor plant_state
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user