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 /// /// This structures handles the manipulation of relay state history files /// /// These file are binary file optimizing used space. pub struct RelayStateHistory { id: DeviceRelayID, day: u64, buff: Vec, } impl RelayStateHistory { /// 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_day_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: 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: 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)) } /// 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_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 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<()> { let path = AppConfig::get().relay_runtime_day_file_path(self.id, self.day); files_utils::create_directory_if_missing(path.parent().unwrap())?; std::fs::write(path, &self.buff)?; Ok(()) } } /// Get the total runtime of a relay during a given time window pub fn relay_total_runtime(device_id: DeviceRelayID, from: u64, to: u64) -> anyhow::Result { let mut total = 0; let mut file = RelayStateHistory::open(device_id, from)?; let mut curr_time = from; while curr_time < to { if !file.contains_time(curr_time) { file = RelayStateHistory::open(device_id, curr_time)?; } if file.get_state(curr_time)? { total += TIME_INTERVAL; } curr_time += TIME_INTERVAL as u64; } Ok(total) } #[cfg(test)] mod tests { use crate::devices::device::DeviceRelayID; use crate::energy::relay_state_history::{relay_total_runtime, 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()); } #[test] fn test_relay_total_runtime() { assert_eq!( relay_total_runtime(DeviceRelayID::default(), 50, 3600 * 24 * 60 + 500).unwrap(), 0 ); } }