diff --git a/rust/src/main.rs b/rust/src/main.rs index 171d61b..d628a11 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,8 +1,13 @@ -use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::{ + fmt::Display, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; -use chrono::{DateTime, Datelike, TimeDelta, Timelike}; +use anyhow::Result; +use chrono::{DateTime, Datelike, TimeDelta, Timelike, Utc}; use chrono_tz::{Europe::Berlin, Tz}; +use config::Mode; use esp_idf_hal::delay::Delay; use esp_idf_sys::{ esp_deep_sleep, esp_ota_get_app_partition_count, esp_ota_get_running_partition, @@ -73,7 +78,6 @@ struct PlantState { consecutive_pump_count: u32, after_p: Option, do_water: bool, - frozen: bool, dry: bool, active: bool, pump_error: bool, @@ -102,6 +106,33 @@ struct TankState { sensor_error: bool, raw: u16, } +#[derive(Serialize)] +struct PlantStateMQTT<'a> { + a: &'a str, + b: &'a str, + p_start: &'a str, + p_end: &'a str, + mode: &'a str, + consecutive_pump_count: u32, + dry: bool, + active: bool, + pump_error: bool, + not_effective: bool, + cooldown: bool, + out_of_work_hour: bool, + last_pump: &'a str, + next_pump: &'a str, +} +#[derive(Serialize)] +struct BatteryState<'a> { + voltage_milli_volt: &'a str, + current_milli_ampere: &'a str, + cycle_count: &'a str, + design_milli_ampere: &'a str, + remaining_milli_ampere: &'a str, + state_of_charge: &'a str, + state_of_health: &'a str, +} fn safe_main() -> anyhow::Result<()> { // It is necessary to call this function once. Otherwise some patches to the runtime @@ -325,27 +356,14 @@ fn safe_main() -> anyhow::Result<()> { let tank_state = determine_tank_state(&mut board, &config); if online_mode == OnlineMode::Online { - if tank_state.sensor_error { - let _ = board.mqtt_publish(&config, "/water/ml", "error".to_string().as_bytes()); - } else { - let _ = board.mqtt_publish( - &config, - "/water/ml", - tank_state.left_ml.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - "/water/enough_water", - tank_state.enough_water.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - &config, - "/water/low_warning", - tank_state.warn_level.to_string().as_bytes(), - ); - let _ = - board.mqtt_publish(&config, "/water/raw", tank_state.raw.to_string().as_bytes()); - } + match serde_json::to_string(&tank_state) { + Ok(state) => { + let _ = board.mqtt_publish(&config, "/water", state.as_bytes()); + } + Err(err) => { + println!("Error publishing lightstate {}", err); + } + }; } let mut water_frozen = false; @@ -371,6 +389,11 @@ fn safe_main() -> anyhow::Result<()> { if online_mode == OnlineMode::Online { let _ = board.mqtt_publish(&config, "/water/temperature", res.to_string().as_bytes()); + let _ = board.mqtt_publish( + &config, + "/water/frozen", + water_frozen.to_string().as_bytes(), + ); } } None => { @@ -437,6 +460,7 @@ fn safe_main() -> anyhow::Result<()> { state.active = true; for _ in 0..plant_config.pump_time_s { unsafe { vTaskDelay(CONFIG_FREERTOS_HZ) }; + let p_live_topic = format!("/plant{}/p live", plant + 1); if plant_config.sensor_p { let moist = map_range_moisture( board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32, @@ -444,17 +468,13 @@ fn safe_main() -> anyhow::Result<()> { if online_mode == OnlineMode::Online { let _ = board.mqtt_publish( &config, - format!("/plant{}/Sensor P after", plant + 1).as_str(), + &p_live_topic, option_to_string(moist.ok()).as_bytes(), ); } } else { if online_mode == OnlineMode::Online { - let _ = board.mqtt_publish( - &config, - format!("/plant{}/Sensor P after", plant + 1).as_str(), - "disabled".as_bytes(), - ); + let _ = board.mqtt_publish(&config, &p_live_topic, "disabled".as_bytes()); } } } @@ -534,7 +554,7 @@ fn safe_main() -> anyhow::Result<()> { if online_mode == OnlineMode::Online { match serde_json::to_string(&light_state) { Ok(state) => { - let _ = board.mqtt_publish(&config, "/light/active", state.as_bytes()); + let _ = board.mqtt_publish(&config, "/light", state.as_bytes()); } Err(err) => { println!("Error publishing lightstate {}", err); @@ -578,120 +598,34 @@ fn safe_main() -> anyhow::Result<()> { unsafe { esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) }; } +fn to_string(value: Result) -> String { + return match value { + Ok(v) => v.to_string(), + Err(err) => { + format!("{:?}", err) + } + }; +} + fn publish_battery_state( board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>, config: &Config, ) { - match board.voltage_milli_volt() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/voltage_milli_volt", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish(&config, "/battery/voltage_milli_volt", "-1".as_bytes()); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } + let bat = BatteryState { + voltage_milli_volt: &to_string(board.voltage_milli_volt()), + current_milli_ampere: &to_string(board.average_current_milli_ampere()), + cycle_count: &to_string(board.cycle_count()), + design_milli_ampere: &to_string(board.design_milli_ampere_hour()), + remaining_milli_ampere: &to_string(board.remaining_milli_ampere_hour()), + state_of_charge: &to_string(board.state_charge_percent()), + state_of_health: &to_string(board.state_health_percent()), }; - match board.average_current_milli_ampere() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/average_current_milli_ampere", - v.to_string().as_bytes(), - ); + match serde_json::to_string(&bat) { + Ok(state) => { + let _ = board.mqtt_publish(&config, "/plant/battery", state.as_bytes()); } Err(err) => { - let _ = board.mqtt_publish( - &config, - "/battery/average_current_milli_ampere", - "-1".as_bytes(), - ); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.cycle_count() { - Ok(v) => { - let _ = board.mqtt_publish(&config, "/battery/cycle_count", v.to_string().as_bytes()); - } - Err(err) => { - let _ = board.mqtt_publish(&config, "/battery/cycle_count", "-1".as_bytes()); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.design_milli_ampere_hour() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/design_milli_ampere_hour", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish( - &config, - "/battery/design_milli_ampere_hour", - "-1".as_bytes(), - ); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.max_milli_ampere_hour() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/max_milli_ampere_hour", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish(&config, "/battery/max_milli_ampere_hour", "-1".as_bytes()); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.remaining_milli_ampere_hour() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/remaining_milli_ampere_hour", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish( - &config, - "/battery/remaining_milli_ampere_hour", - "-1".as_bytes(), - ); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.state_charge_percent() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/state_charge_percent", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish(&config, "/battery/state_charge_percent", "-1".as_bytes()); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); - } - }; - match board.state_health_percent() { - Ok(v) => { - let _ = board.mqtt_publish( - &config, - "/battery/state_health_percent", - v.to_string().as_bytes(), - ); - } - Err(err) => { - let _ = board.mqtt_publish(&config, "/battery/state_health_percent", "-1".as_bytes()); - let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes()); + println!("Error publishing battery_state {}", err); } }; } @@ -750,6 +684,7 @@ fn determine_tank_state( return rv; } return TankState { + warn_level: false, enough_water: true, left_ml: 1337, sensor_error: false, @@ -811,24 +746,13 @@ fn determine_state_target_moisture_for_plant( state: &mut PlantState, config: &Config, tank_state: &TankState, - water_frozen: bool, cur: DateTime, ) { let plant_config = &config.plants[plant]; - match board.measure_moisture_hz(plant, plant_hal::Sensor::A) { - Ok(a) => { - let mapped = map_range_moisture(a as f32); - match mapped { - Ok(result) => state.a = Some(result), - Err(err) => { - state.sensor_error_a = Some(err); - } - } - } - Err(_) => { - state.sensor_error_a = Some(SensorError::Unknown); - } + if plant_config.mode == Mode::OFF { + return; } + if plant_config.sensor_b { match board.measure_moisture_hz(plant, plant_hal::Sensor::B) { Ok(b) => { @@ -886,15 +810,8 @@ fn determine_state_target_moisture_for_plant( ) { state.out_of_work_hour = true; } - if water_frozen { - state.frozen = true; - } if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour { - if water_frozen { - state.frozen = true; - } else { - state.do_water = true; - } + state.do_water = true; } } @@ -904,7 +821,6 @@ fn determine_state_timer_only_for_plant( state: &mut PlantState, config: &Config, tank_state: &TankState, - water_frozen: bool, cur: DateTime, ) { let plant_config = &config.plants[plant]; @@ -919,14 +835,10 @@ fn determine_state_timer_only_for_plant( state.next_pump = Some(europe_time); state.cooldown = true; } else { - if water_frozen { - state.frozen = true; - } else { - if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { - state.do_water = true; - } else if !tank_state.enough_water { - state.no_water = true; - } + if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { + state.do_water = true; + } else if !tank_state.enough_water { + state.no_water = true; } } } @@ -947,7 +859,6 @@ fn determine_state_timer_and_deadzone_for_plant( state: &mut PlantState, config: &Config, tank_state: &TankState, - water_frozen: bool, cur: DateTime, ) { let plant_config = &config.plants[plant]; @@ -970,14 +881,10 @@ fn determine_state_timer_and_deadzone_for_plant( state.out_of_work_hour = true; } if !state.cooldown && !state.out_of_work_hour { - if water_frozen { - state.frozen = true; - } else { - if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { - state.do_water = true; - } else if !tank_state.enough_water { - state.no_water = true; - } + if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error { + state.do_water = true; + } else if !tank_state.enough_water { + state.no_water = true; } } } @@ -1007,35 +914,15 @@ fn determine_next_plant( config::Mode::OFF => {} config::Mode::TargetMoisture => { determine_state_target_moisture_for_plant( - board, - plant, - state, - config, - tank_state, - water_frozen, - cur, + board, plant, state, config, tank_state, cur, ); } config::Mode::TimerOnly => { - determine_state_timer_only_for_plant( - board, - plant, - state, - config, - tank_state, - water_frozen, - cur, - ); + determine_state_timer_only_for_plant(board, plant, state, config, tank_state, cur); } config::Mode::TimerAndDeadzone => { determine_state_timer_and_deadzone_for_plant( - board, - plant, - state, - config, - tank_state, - water_frozen, - cur, + board, plant, state, config, tank_state, cur, ); } } @@ -1058,8 +945,10 @@ fn determine_next_plant( "Checking for water plant {} with state {}", plant, state.do_water ); - if state.do_water { - return Some(plant); + if !water_frozen { + if state.do_water { + return Some(plant); + } } } println!("No plant needs water"); @@ -1075,158 +964,70 @@ fn update_plant_state( let state = &plantstate[plant]; let plant_config = config.plants[plant]; - let _ = board.mqtt_publish( - &config, - format!("/plant{}/mode", plant + 1).as_str(), - match plant_config.mode { - config::Mode::OFF => "OFF".as_bytes(), - config::Mode::TargetMoisture => "TargetMoisture".as_bytes(), - config::Mode::TimerOnly => "TimerOnly".as_bytes(), - config::Mode::TimerAndDeadzone => "TimerAndDeadzone".as_bytes(), - }, - ); + let mode = format!("{:?}", plant_config.mode); - let last_time = board.last_pump_time(plant); - match last_time { - Some(last_time) => { - let europe_time = last_time.with_timezone(&Berlin); - if europe_time.year() > 2023 { - let time = europe_time.to_rfc3339(); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/last pump", plant + 1).as_str(), - time.as_bytes(), - ); - } else { - let _ = board.mqtt_publish( - &config, - format!("/plant{}/last pump", plant + 1).as_str(), - "N/A".as_bytes(), - ); - } - } - None => { - let _ = board.mqtt_publish( - &config, - format!("/plant{}/last pump", plant + 1).as_str(), - "N/A".as_bytes(), - ); - } - } + let plant_dto = PlantStateMQTT { + p_start: &sensor_to_string(state.p, state.sensor_error_p, plant_config.sensor_p), + p_end: &sensor_to_string(state.after_p, state.sensor_error_p, plant_config.sensor_p), + a: &sensor_to_string(state.a, state.sensor_error_a, true), + b: &sensor_to_string(state.b, state.sensor_error_b, plant_config.sensor_b), + 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 state.next_pump { - Some(next) => { - let time = next.to_rfc3339(); - let _ = board.mqtt_publish( - &config, - format!("/plant{}/next pump", plant + 1).as_str(), - time.as_bytes(), - ); + 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()); } - None => { - let _ = board.mqtt_publish( - &config, - format!("/plant{}/next pump", plant + 1).as_str(), - "N/A".as_bytes(), - ); + Err(err) => { + println!("Error publishing lightstate {}", err); } - } - let _ = board.mqtt_publish( - config, - format!("/plant{}/active", plant + 1).as_str(), - state.active.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor A", plant + 1).as_str(), - option_to_string(state.a).as_bytes(), - ); - if plant_config.sensor_b { - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor B", plant + 1).as_str(), - option_to_string(state.b).as_bytes(), - ); - } else { - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor B", plant + 1).as_str(), - "disabled".as_bytes(), - ); - } - - if plant_config.sensor_p { - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor P before", plant + 1).as_str(), - option_to_string(state.p).as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor P after", plant + 1).as_str(), - option_to_string(state.after_p).as_bytes(), - ); - } else { - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor P before", plant + 1).as_str(), - "disabled".as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Sensor P after", plant + 1).as_str(), - "disabled".as_bytes(), - ); - } - - let _ = board.mqtt_publish( - config, - format!("/plant{}/Should water", plant + 1).as_str(), - state.do_water.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Is frozen", plant + 1).as_str(), - state.frozen.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Is dry", plant + 1).as_str(), - state.dry.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Pump Error", plant + 1).as_str(), - state.pump_error.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Pump Ineffective", plant + 1).as_str(), - state.not_effective.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Is in Cooldown", plant + 1).as_str(), - state.cooldown.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/No Water", plant + 1).as_str(), - state.no_water.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/Out of Work Hour", plant + 1).as_str(), - state.out_of_work_hour.to_string().as_bytes(), - ); - let _ = board.mqtt_publish( - config, - format!("/plant{}/consecutive pump count", plant + 1).as_str(), - state.consecutive_pump_count.to_string().as_bytes(), - ); + }; } } +fn time_to_string_utc(value_option: Option>) -> String { + let converted = value_option.and_then(|utc| Some(utc.with_timezone(&Berlin))); + return time_to_string(converted); +} + +fn time_to_string(value_option: Option>) -> String { + match value_option { + Some(value) => { + let europe_time = value.with_timezone(&Berlin); + if europe_time.year() > 2023 { + return europe_time.to_rfc3339(); + } else { + return "smtp error".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 wait_infinity(wait_type: WaitType, reboot_now: Arc) -> ! { let delay = match wait_type { WaitType::InitialConfig => 250_u32, diff --git a/rust/src/plant_hal.rs b/rust/src/plant_hal.rs index 8a318f8..4ef49b9 100644 --- a/rust/src/plant_hal.rs +++ b/rust/src/plant_hal.rs @@ -369,7 +369,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { fn general_fault(&mut self, enable: bool) { unsafe { gpio_hold_dis(self.general_fault.pin()) }; self.general_fault.set_state(enable.into()).unwrap(); - unsafe { gpio_hold_en(self.general_fault.pin()) }; + unsafe { gpio_hold_en(self.general_fault.pin()) }; } fn wifi_ap(&mut self) -> Result<()> { @@ -803,7 +803,7 @@ impl PlantCtrlBoardInteraction for PlantCtrlBoard<'_> { }; } None => { - bail!("No mqtt client, aborting publish"); + bail!("No mqtt client"); } } }