diff --git a/growpi.history.csv b/growpi.history.csv deleted file mode 100644 index bcd801d..0000000 --- a/growpi.history.csv +++ /dev/null @@ -1,2 +0,0 @@ -time,amount -1714687536,456 diff --git a/growpi.service b/growpi.service new file mode 100644 index 0000000..afe8604 --- /dev/null +++ b/growpi.service @@ -0,0 +1,12 @@ +[Unit] +Description=GrowPi Server and Control +After=network.target + +[Service] +Type=simple +Restart=always +RestartSec=10 +ExecStart=/opt/growpi/growpi + +[Install] +WantedBy=mutli-user.target diff --git a/src/actuators.rs b/src/actuators.rs index 966eb58..2a1a92a 100644 --- a/src/actuators.rs +++ b/src/actuators.rs @@ -1,7 +1,8 @@ use std::{thread, time::Duration}; use crate::{ - error::GenericResult, history::WateringRecord, io::RelaySwitchState, state::ProgramState, + error::GenericResult, history::WateringRecord, io::RelaySwitchState, sensors, + state::ProgramState, }; pub fn switch_lights( @@ -49,6 +50,7 @@ pub fn pump_water(water_mass_g: u16, program_state: &mut ProgramState) -> Generi .grams_per_millisecond; let duration_ms = duration_ms.round() as u64; let duration = Duration::from_millis(duration_ms); + let moisture_before_watering = sensors::get_soil_moisture(&program_state.config)?; switch_water_pump(RelaySwitchState::On, program_state)?; thread::sleep(duration); switch_water_pump(RelaySwitchState::Off, program_state)?; @@ -56,7 +58,10 @@ pub fn pump_water(water_mass_g: u16, program_state: &mut ProgramState) -> Generi program_state .history .watering_records - .push(WateringRecord::new(water_mass_g.into())); + .push(WateringRecord::new( + water_mass_g.into(), + moisture_before_watering, + )); program_state.history.save()?; Ok(()) diff --git a/src/config.rs b/src/config.rs index 0eed285..1d49339 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,6 +51,9 @@ pub struct ControllerSettings { pub temperature_loop_mins: u64, pub sunlight_hours: u64, pub soil_loop_hours: u64, + pub max_water_over_window_grams: u64, + pub max_water_window_hours: u64, + pub ideal_soil_moisture_pct: u64, } #[derive(Serialize, Deserialize)] @@ -109,6 +112,9 @@ impl Default for Configuration { temperature_loop_mins: 60, soil_loop_hours: 12, sunlight_hours: 24, + max_water_over_window_grams: 1000, + max_water_window_hours: 24, + ideal_soil_moisture_pct: 70, }, } } diff --git a/src/control/soil.rs b/src/control/soil.rs index c4b7a87..04a527a 100644 --- a/src/control/soil.rs +++ b/src/control/soil.rs @@ -1,6 +1,11 @@ use std::time::Duration; -use crate::state::ProgramStateShared; +use chrono::{DateTime, Utc}; + +use crate::{ + error::{lock_err, GenericResult}, + state::ProgramStateShared, +}; pub async fn soil_moisture_control_loop(program_state: ProgramStateShared) { let loop_duration = program_state @@ -9,6 +14,29 @@ pub async fn soil_moisture_control_loop(program_state: ProgramStateShared) { .unwrap_or(1); loop { + let _ = soil_moisture_control(program_state.clone()); tokio::time::sleep(Duration::from_hours(loop_duration)).await; } } + +fn soil_moisture_control(program_state: ProgramStateShared) -> GenericResult<()> { + let program_state = program_state.lock().map_err(lock_err)?; + let config = &program_state.config.controller_settings; + let history = &program_state.history.watering_records; + + let water_amount_over_window: u64 = history + .iter() + .filter(|record| { + if let Some(time) = DateTime::from_timestamp(record.time, 0) { + let delta = Utc::now() - time; + let delta = delta.num_hours(); + delta <= config.max_water_window_hours.try_into().unwrap_or(24) + } else { + false + } + }) + .map(|record| record.amount) + .sum(); + + Ok(()) +} diff --git a/src/data_logging.rs b/src/data_logging.rs new file mode 100644 index 0000000..634ee02 --- /dev/null +++ b/src/data_logging.rs @@ -0,0 +1,49 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::{lock_err, GenericResult}, + sensors, + state::ProgramStateShared, +}; + +#[derive(Serialize, Deserialize)] +pub struct DataRecord { + pub timestamp: i64, + pub temperature: f32, + pub soil_mositure: f32, +} + +#[derive(Serialize, Deserialize)] +pub struct DataRecords { + pub records: Vec, +} + +const FILE_PATH: &str = "./growpi.datalog.csv"; +impl DataRecords { + fn load() -> GenericResult { + let mut data = csv::ReaderBuilder::new() + .has_headers(true) + .from_path(FILE_PATH)?; + let mut result = Vec::new(); + for record in data.deserialize() { + result.push(record?); + } + Ok(DataRecords { records: result }) + } + pub fn push(program_state: ProgramStateShared) -> GenericResult<()> { + let program_state = program_state.lock().map_err(lock_err)?; + let config = &program_state.config; + let record = DataRecord { + timestamp: Utc::now().timestamp(), + temperature: sensors::get_temperature(config)?, + soil_mositure: sensors::get_soil_moisture(config)?, + }; + let mut writer = csv::WriterBuilder::new() + .has_headers(true) + .from_path(FILE_PATH)?; + writer.serialize(record)?; + writer.flush()?; + Ok(()) + } +} diff --git a/src/history.rs b/src/history.rs index 6a095f9..5d6408c 100644 --- a/src/history.rs +++ b/src/history.rs @@ -7,13 +7,15 @@ use crate::error::GenericResult; pub struct WateringRecord { pub time: i64, pub amount: u64, + pub moisture_before_watering: f32, } impl WateringRecord { - pub fn new(amount: u64) -> WateringRecord { + pub fn new(amount: u64, moisture_before_watering: f32) -> WateringRecord { WateringRecord { time: Utc::now().timestamp(), amount, + moisture_before_watering, } } } @@ -64,6 +66,7 @@ mod tests { history.watering_records.push(WateringRecord { time: Local::now().timestamp(), amount: 456, + moisture_before_watering: 71.1, }); history.save().unwrap(); } diff --git a/src/main.rs b/src/main.rs index 9b1e8dd..71ac8de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,12 +10,13 @@ mod actuators; mod cli_mode; mod config; mod control; +mod data_logging; mod error; +mod history; mod io; mod sensors; mod server; mod state; -mod history; fn load_config() -> config::Configuration { let config = Configuration::from_file(std::path::Path::new("./growpi.toml"));