diff --git a/Cargo.lock b/Cargo.lock index b82e952..9855b01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,27 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -333,6 +354,7 @@ dependencies = [ "ads1x1x", "axum", "chrono", + "csv", "libc", "nb 1.1.0", "rppal", diff --git a/Cargo.toml b/Cargo.toml index 9bf774a..cf425c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ axum = { "version" = "0.7", features = ["macros"] } tokio = { "version" = "1.37" } chrono = "0.4" tower-http = { "version" = "0.5", features = ["cors"] } +csv = "1.3.0" diff --git a/growpi.history.csv b/growpi.history.csv new file mode 100644 index 0000000..bcd801d --- /dev/null +++ b/growpi.history.csv @@ -0,0 +1,2 @@ +time,amount +1714687536,456 diff --git a/growpi.toml b/growpi.toml index e16cc05..2181a75 100644 --- a/growpi.toml +++ b/growpi.toml @@ -33,4 +33,5 @@ grams_per_millisecond = 0.05280999839305878 temperature_set_point_upper = 35.0 temperature_set_point_lower = 28.0 temperature_loop_mins = 60 +sunlight_hours = 24 soil_loop_hours = 12 diff --git a/src/actuators.rs b/src/actuators.rs index 87b79c0..ef862ec 100644 --- a/src/actuators.rs +++ b/src/actuators.rs @@ -1,6 +1,8 @@ use std::{thread, time::Duration}; -use crate::{error::GenericResult, io::RelaySwitchState, state::ProgramState}; +use crate::{ + error::GenericResult, history::WateringRecord, io::RelaySwitchState, state::ProgramState, +}; pub fn switch_lights( state: RelaySwitchState, @@ -50,5 +52,11 @@ pub fn pump_water(water_mass_g: u16, program_state: &mut ProgramState) -> Generi switch_water_pump(RelaySwitchState::On, program_state)?; thread::sleep(duration); switch_water_pump(RelaySwitchState::Off, program_state)?; + + program_state + .history + .watering_records + .push(WateringRecord::new(water_mass_g.into())); + Ok(()) } diff --git a/src/config.rs b/src/config.rs index 9cb0ef1..0eed285 100644 --- a/src/config.rs +++ b/src/config.rs @@ -49,6 +49,7 @@ pub struct ControllerSettings { pub temperature_set_point_upper: f32, pub temperature_set_point_lower: f32, pub temperature_loop_mins: u64, + pub sunlight_hours: u64, pub soil_loop_hours: u64, } @@ -107,6 +108,7 @@ impl Default for Configuration { temperature_set_point_lower: 28., temperature_loop_mins: 60, soil_loop_hours: 12, + sunlight_hours: 24, }, } } diff --git a/src/control/light.rs b/src/control/light.rs index 5fb2c52..be5f2c0 100644 --- a/src/control/light.rs +++ b/src/control/light.rs @@ -13,8 +13,7 @@ fn light_control(program_state: ProgramStateShared) -> GenericResult<()> { let mut program_state = program_state.lock().map_err(lock_err)?; let local = Local::now(); let hour = local.time().hour(); - const HOURS_ON: u32 = 24; - if hour <= HOURS_ON { + if hour as u64 <= program_state.config.controller_settings.sunlight_hours { actuators::switch_lights(crate::io::RelaySwitchState::On, &mut program_state)?; } else { actuators::switch_lights(crate::io::RelaySwitchState::Off, &mut program_state)?; diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..6a095f9 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,70 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +use crate::error::GenericResult; + +#[derive(Serialize, Deserialize)] +pub struct WateringRecord { + pub time: i64, + pub amount: u64, +} + +impl WateringRecord { + pub fn new(amount: u64) -> WateringRecord { + WateringRecord { + time: Utc::now().timestamp(), + amount, + } + } +} + +#[derive(Default)] +pub struct History { + pub watering_records: Vec, +} + +const FILE_PATH: &str = "./growpi.history.csv"; + +impl History { + pub fn save(&self) -> GenericResult<()> { + let mut writer = csv::WriterBuilder::new() + .has_headers(true) + .from_path(FILE_PATH)?; + for record in &self.watering_records { + writer.serialize(record)?; + } + writer.flush()?; + Ok(()) + } + + pub fn load() -> GenericResult { + let mut history = csv::ReaderBuilder::new() + .has_headers(true) + .from_path(FILE_PATH)?; + let mut result = Vec::new(); + for record in history.deserialize() { + result.push(record?); + } + Ok(History { + watering_records: result, + }) + } +} + +#[cfg(test)] +mod tests { + + use chrono::Local; + + use super::*; + + #[test] + fn test_write_default() { + let mut history = History::default(); + history.watering_records.push(WateringRecord { + time: Local::now().timestamp(), + amount: 456, + }); + history.save().unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index d53d72d..9b1e8dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ 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")); diff --git a/src/server.rs b/src/server.rs index 40c0ff6..143f9a8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -58,7 +58,6 @@ async fn switch_handler( let mut program_state = program_state.lock().map_err(lock_err)?; match device.as_str() { "lights" => actuators::switch_lights(state, &mut program_state)?, - "pump" => actuators::switch_water_pump(state, &mut program_state)?, "fan" => actuators::switch_fan(state, &mut program_state)?, _ => (), } diff --git a/src/state.rs b/src/state.rs index 0c324ac..9458dd4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,14 +1,20 @@ use std::sync::{Arc, Mutex}; -use crate::{config::Configuration, error::GenericResult, io}; +use crate::{config::Configuration, error::GenericResult, history::History, io}; pub type ProgramStateShared = Arc>; pub struct ProgramState { pub config: Configuration, pub relay: io::Relay, + pub history: History, } pub fn init_state(config: Configuration) -> GenericResult { let relay = io::Relay::new(&config)?; - Ok(Arc::new(Mutex::new(ProgramState { config, relay }))) + let history = History::load()?; + Ok(Arc::new(Mutex::new(ProgramState { + config, + relay, + history, + }))) }