From 565db05fb0a68afd7028c26f895324b7f8b4bb0e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 17 Sep 2024 22:31:51 +0200 Subject: [PATCH] Record relays state --- central_backend/Cargo.toml | 2 +- central_backend/src/app_config.rs | 16 ++- central_backend/src/devices/device.rs | 2 +- central_backend/src/energy/energy_actor.rs | 7 +- central_backend/src/energy/engine.rs | 15 +++ .../src/energy/relay_state_history.rs | 123 ++++++++++++++++-- central_backend/src/main.rs | 1 + central_backend/src/utils/time_utils.rs | 18 +++ 8 files changed, 165 insertions(+), 19 deletions(-) diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index 6a3ccf9..e0b8ef1 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -35,4 +35,4 @@ tokio_schedule = "0.3.2" mime_guess = "2.0.5" rust-embed = "8.5.0" jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } -prettytable-rs = "0.10.0" \ No newline at end of file +prettytable-rs = "0.10.0" diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index 8a76687..9a89241 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -1,4 +1,4 @@ -use crate::devices::device::DeviceId; +use crate::devices::device::{DeviceId, DeviceRelayID}; use clap::{Parser, Subcommand}; use std::path::{Path, PathBuf}; @@ -81,7 +81,7 @@ pub struct AppConfig { pub production_margin: i32, /// Energy refresh operations interval, in seconds - #[arg(short('i'), long, env, default_value_t = 30)] + #[arg(short('i'), long, env, default_value_t = 20)] pub refresh_interval: u64, /// Consumption backend provider @@ -235,6 +235,18 @@ impl AppConfig { pub fn device_csr_path(&self, id: &DeviceId) -> PathBuf { self.devices_config_path().join(format!("{}.csr", id.0)) } + + /// Get relays runtime storage path + pub fn relays_runtime_stats_storage_path(&self) -> PathBuf { + self.storage_path().join("relays_runtime") + } + + /// Get relay runtime stats path for a given day + pub fn relay_runtime_file_path(&self, relay_id: DeviceRelayID, day: u64) -> PathBuf { + self.relays_runtime_stats_storage_path() + .join(relay_id.0.to_string()) + .join(day.to_string()) + } } #[cfg(test)] diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index 4cd4be5..a334891 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -77,7 +77,7 @@ pub struct DailyMinRuntime { } #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] -pub struct DeviceRelayID(uuid::Uuid); +pub struct DeviceRelayID(pub uuid::Uuid); impl Default for DeviceRelayID { fn default() -> Self { diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index b169b98..1371708 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -38,8 +38,11 @@ impl EnergyActor { constants::FALLBACK_PRODUCTION_VALUE }); - self.engine - .refresh(self.curr_consumption, &self.devices.full_list()); + let devices_list = self.devices.full_list(); + + self.engine.refresh(self.curr_consumption, &devices_list); + + self.engine.persist_relays_state(&devices_list)?; Ok(()) } diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index ad6675d..65feb13 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -6,6 +6,7 @@ use prettytable::{row, Table}; use crate::constants; use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID}; use crate::energy::consumption::EnergyConsumption; +use crate::energy::relay_state_history::RelayStateHistory; use crate::utils::time_utils::time_secs; #[derive(Default)] @@ -320,4 +321,18 @@ impl EnergyEngine { self.print_summary(curr_consumption, devices); } + + /// Save relays state to disk + pub fn persist_relays_state(&mut self, devices: &[Device]) -> anyhow::Result<()> { + // Save all relays state + for d in devices { + for r in &d.relays { + let mut file = RelayStateHistory::open(r.id, time_secs())?; + file.set_state(time_secs(), self.relay_state(r.id).is_on())?; + file.save()?; + } + } + + Ok(()) + } } diff --git a/central_backend/src/energy/relay_state_history.rs b/central_backend/src/energy/relay_state_history.rs index 117e094..a84e63e 100644 --- a/central_backend/src/energy/relay_state_history.rs +++ b/central_backend/src/energy/relay_state_history.rs @@ -1,4 +1,15 @@ +use crate::app_config::AppConfig; use crate::devices::device::DeviceRelayID; +use crate::utils::files_utils; +use crate::utils::time_utils::day_number; + +const TIME_INTERVAL: usize = 30; + +#[derive(thiserror::Error, Debug)] +pub enum RelayStateHistoryError { + #[error("Given time is out of file bounds!")] + TimeOutOfFileBound, +} /// # RelayStateHistory /// @@ -7,38 +18,124 @@ use crate::devices::device::DeviceRelayID; /// These file are binary file optimizing used space. pub struct RelayStateHistory { id: DeviceRelayID, - day: usize, + day: u64, buff: Vec, } impl RelayStateHistory { - /// Open relay state history file, if it exist, or create an empty one - pub fn open(id: DeviceRelayID, day: usize) -> anyhow::Result { - todo!() + /// Open relay state history file, if it exists, or create an empty one + pub fn open(id: DeviceRelayID, time: u64) -> anyhow::Result { + let day = day_number(time); + let path = AppConfig::get().relay_runtime_file_path(id, day); + + if path.exists() { + Ok(Self { + id, + day, + buff: std::fs::read(path)?, + }) + } else { + log::debug!( + "Stats for relay {id:?} for day {day} does not exists yet, creating memory buffer" + ); + Ok(Self::new_memory(id, day)) + } } /// Create a new in memory dev relay state history - fn new_memory(id: DeviceRelayID, day: usize) -> Self { - todo!() + fn new_memory(id: DeviceRelayID, day: u64) -> Self { + Self { + id, + day, + buff: vec![0; 3600 * 24 / TIME_INTERVAL], + } } /// Resolve time offset of a given time in buffer - fn resolve_offset(&self, time: i64) -> anyhow::Result<(usize, u8)> { - todo!() + fn resolve_offset(&self, time: u64) -> anyhow::Result<(usize, u8)> { + let start_of_day = self.day * 3600 * 24; + + if time < start_of_day || time >= start_of_day + 3600 * 24 { + return Err(RelayStateHistoryError::TimeOutOfFileBound.into()); + } + + let relative_time = (time - start_of_day) / TIME_INTERVAL as u64; + + Ok(((relative_time / 8) as usize, (relative_time % 8) as u8)) } /// Set new state of relay - fn set_state(&mut self, time: i64, on: bool) -> anyhow::Result<()> { - todo!() + pub fn set_state(&mut self, time: u64, on: bool) -> anyhow::Result<()> { + let (idx, offset) = self.resolve_offset(time)?; + + self.buff[idx] = if on { + self.buff[idx] | (0x1 << offset) + } else { + self.buff[idx] & !(0x1 << offset) + }; + + Ok(()) } /// Get the state of relay at a given time - fn get_state(&self, time: i64) -> anyhow::Result { - todo!() + pub fn get_state(&self, time: u64) -> anyhow::Result { + let (idx, offset) = self.resolve_offset(time)?; + + Ok(self.buff[idx] & (0x1 << offset) != 0) } /// Persist device relay state history pub fn save(&self) -> anyhow::Result<()> { - todo!() + let path = AppConfig::get().relay_runtime_file_path(self.id, self.day); + files_utils::create_directory_if_missing(path.parent().unwrap())?; + std::fs::write(path, &self.buff)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::devices::device::DeviceRelayID; + use crate::energy::relay_state_history::RelayStateHistory; + + #[test] + fn test_relay_state_history() { + let mut history = RelayStateHistory::new_memory(DeviceRelayID::default(), 0); + + let val_1 = 5 * 30; + let val_2 = 7 * 30; + + for i in 0..500 { + assert!(!history.get_state(i).unwrap()) + } + + history.set_state(val_1, true).unwrap(); + + for i in 0..500 { + assert_eq!(history.get_state(i).unwrap(), (i / 30) * 30 == val_1); + } + + history.set_state(val_2, true).unwrap(); + + for i in 0..500 { + assert_eq!( + history.get_state(i).unwrap(), + (i / 30) * 30 == val_1 || (i / 30) * 30 == val_2 + ); + } + + history.set_state(val_2, false).unwrap(); + + for i in 0..500 { + assert_eq!(history.get_state(i).unwrap(), (i / 30) * 30 == val_1); + } + + history.set_state(val_1, false).unwrap(); + + for i in 0..500 { + assert!(!history.get_state(i).unwrap()) + } + + assert!(history.get_state(8989898).is_err()); } } diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index 3f40d90..f997c65 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -17,6 +17,7 @@ async fn main() -> std::io::Result<()> { // Initialize storage 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(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!"); diff --git a/central_backend/src/utils/time_utils.rs b/central_backend/src/utils/time_utils.rs index 4f1a368..af8bfd1 100644 --- a/central_backend/src/utils/time_utils.rs +++ b/central_backend/src/utils/time_utils.rs @@ -15,3 +15,21 @@ pub fn time_millis() -> u128 { .unwrap() .as_millis() } + +/// Get the number of the day since 01-01-1970 of a given UNIX timestamp +pub fn day_number(time: u64) -> u64 { + time / (3600 * 24) +} + +#[cfg(test)] +mod test { + use crate::utils::time_utils::day_number; + + #[test] + fn test_time_of_day() { + assert_eq!(day_number(500), 0); + assert_eq!(day_number(1726592301), 19983); + assert_eq!(day_number(1726592401), 19983); + assert_eq!(day_number(1726498701), 19982); + } +}