diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 9c1271b..f1408bd 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -523,6 +523,25 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -608,6 +627,7 @@ dependencies = [ "actix-web", "anyhow", "asn1", + "bincode", "chrono", "clap", "env_logger", @@ -2697,6 +2717,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index a275bc5..44cd8da 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -37,4 +37,5 @@ rust-embed = "8.5.0" jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } prettytable-rs = "0.10.0" chrono = "0.4.38" -serde_yml = "0.0.12" \ No newline at end of file +serde_yml = "0.0.12" +bincode = "=2.0.0-rc.3" \ No newline at end of file diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index 73fced1..c03cd63 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -255,6 +255,18 @@ impl AppConfig { pub fn relay_runtime_day_file_path(&self, relay_id: DeviceRelayID, day: u64) -> PathBuf { self.relay_runtime_stats_dir(relay_id).join(day.to_string()) } + + /// Get energy consumption history path + pub fn energy_consumption_history(&self) -> PathBuf { + self.storage_path().join("consumption_history") + } + + /// Get energy consumption history file path for a given day + pub fn energy_consumption_history_day(&self, number: u64) -> PathBuf { + self.storage_path() + .join("consumption_history") + .join(number.to_string()) + } } #[cfg(test)] diff --git a/central_backend/src/energy/consumption_cache.rs b/central_backend/src/energy/consumption_cache.rs index fb4aa8e..f9beefe 100644 --- a/central_backend/src/energy/consumption_cache.rs +++ b/central_backend/src/energy/consumption_cache.rs @@ -1,6 +1,5 @@ use crate::constants; use crate::energy::consumption::EnergyConsumption; -use log::log; pub struct ConsumptionCache { nb_vals: usize, diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs new file mode 100644 index 0000000..5b9a43d --- /dev/null +++ b/central_backend/src/energy/consumption_history_file.rs @@ -0,0 +1,128 @@ +use crate::app_config::AppConfig; +use crate::energy::consumption::EnergyConsumption; +use crate::utils::time_utils::day_number; + +const TIME_INTERVAL: usize = 10; + +#[derive(thiserror::Error, Debug)] +pub enum ConsumptionHistoryError { + #[error("Given time is out of file bounds!")] + TimeOutOfFileBound, +} + +/// # ConsumptionHistoryFile +/// +/// Stores the history of house consumption +pub struct ConsumptionHistoryFile { + day: u64, + buff: Vec, +} + +impl ConsumptionHistoryFile { + /// Open consumption history file, if it exists, or create an empty one + pub fn open(time: u64) -> anyhow::Result { + let day = day_number(time); + let path = AppConfig::get().energy_consumption_history_day(day); + + if path.exists() { + Ok(Self { + day, + buff: bincode::decode_from_slice( + &std::fs::read(path)?, + bincode::config::standard(), + )? + .0, + }) + } else { + log::debug!( + "Energy consumption stats for day {day} does not exists yet, creating memory buffer" + ); + Ok(Self::new_memory(day)) + } + } + + /// Create a new in memory consumption history + fn new_memory(day: u64) -> Self { + Self { + day, + buff: vec![0; 3600 * 24 / TIME_INTERVAL], + } + } + + /// Resolve time offset of a given time in buffer + fn resolve_offset(&self, time: u64) -> anyhow::Result { + let start_of_day = self.day * 3600 * 24; + + if time < start_of_day || time >= start_of_day + 3600 * 24 { + return Err(ConsumptionHistoryError::TimeOutOfFileBound.into()); + } + + let relative_time = (time - start_of_day) / TIME_INTERVAL as u64; + + Ok(relative_time as usize) + } + + /// Check if a time is contained in this history + pub fn contains_time(&self, time: u64) -> bool { + self.resolve_offset(time).is_ok() + } + + /// Set new state of relay + pub fn set_consumption( + &mut self, + time: u64, + consumption: EnergyConsumption, + ) -> anyhow::Result<()> { + let idx = self.resolve_offset(time)?; + self.buff[idx] = consumption; + Ok(()) + } + + /// Get the consumption recorded at a given time + pub fn get_consumption(&self, time: u64) -> anyhow::Result { + let idx = self.resolve_offset(time)?; + + Ok(self.buff[idx]) + } + + /// Persist device relay state history + pub fn save(&self) -> anyhow::Result<()> { + let path = AppConfig::get().energy_consumption_history_day(self.day); + std::fs::write( + path, + bincode::encode_to_vec(&self.buff, bincode::config::standard())?, + )?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::energy::consumption::EnergyConsumption; + use crate::energy::consumption_history_file::{ConsumptionHistoryFile, TIME_INTERVAL}; + + #[test] + fn test_consumption_history() { + let mut history = ConsumptionHistoryFile::new_memory(0); + + for i in 0..50 { + assert_eq!( + history.get_consumption(i * TIME_INTERVAL as u64).unwrap(), + 0 + ); + } + + for i in 0..50 { + history + .set_consumption(i * TIME_INTERVAL as u64, i as EnergyConsumption * 2) + .unwrap(); + } + + for i in 0..50 { + assert_eq!( + history.get_consumption(i * TIME_INTERVAL as u64).unwrap(), + i as EnergyConsumption * 2 + ); + } + } +} diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 57f9594..b580f8f 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -7,6 +7,7 @@ use crate::devices::devices_list::DevicesList; use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; use crate::energy::consumption_cache::ConsumptionCache; +use crate::energy::consumption_history_file::ConsumptionHistoryFile; use crate::energy::engine::EnergyEngine; use crate::utils::time_utils::time_secs; use actix::prelude::*; @@ -42,17 +43,19 @@ impl EnergyActor { async fn refresh(&mut self) -> anyhow::Result<()> { // Refresh energy - self.consumption_cache - .add_value( - consumption::get_curr_consumption() - .await - .unwrap_or_else(|e| { - log::error!( + let latest_consumption = consumption::get_curr_consumption() + .await + .unwrap_or_else(|e| { + log::error!( "Failed to fetch latest consumption value, will use fallback value! {e}" ); - constants::FALLBACK_PRODUCTION_VALUE - }), - ); + constants::FALLBACK_PRODUCTION_VALUE + }); + self.consumption_cache.add_value(latest_consumption); + + let mut history = ConsumptionHistoryFile::open(time_secs())?; + history.set_consumption(time_secs(), latest_consumption)?; + history.save()?; if self.last_engine_refresh + AppConfig::get().refresh_interval > time_secs() { return Ok(()); diff --git a/central_backend/src/energy/mod.rs b/central_backend/src/energy/mod.rs index 5ef5270..4c09ee7 100644 --- a/central_backend/src/energy/mod.rs +++ b/central_backend/src/energy/mod.rs @@ -1,5 +1,6 @@ pub mod consumption; pub mod consumption_cache; +pub mod consumption_history_file; pub mod energy_actor; pub mod engine; pub mod relay_state_history; diff --git a/central_backend/src/energy/relay_state_history.rs b/central_backend/src/energy/relay_state_history.rs index 0eafaf4..c46a179 100644 --- a/central_backend/src/energy/relay_state_history.rs +++ b/central_backend/src/energy/relay_state_history.rs @@ -47,7 +47,7 @@ impl RelayStateHistory { Self { id, day, - buff: vec![0; 3600 * 24 / TIME_INTERVAL], + buff: vec![0; (3600 * 24 / (TIME_INTERVAL * 8)) + 1], } } diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index f997c65..f910784 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -18,6 +18,7 @@ async fn main() -> std::io::Result<()> { create_directory_if_missing(AppConfig::get().pki_path()).unwrap(); create_directory_if_missing(AppConfig::get().devices_config_path()).unwrap(); create_directory_if_missing(AppConfig::get().relays_runtime_stats_storage_path()).unwrap(); + create_directory_if_missing(AppConfig::get().energy_consumption_history()).unwrap(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!");