All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			201 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			201 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use crate::app_config::AppConfig;
 | |
| use crate::devices::device::{DeviceRelay, DeviceRelayID};
 | |
| use crate::utils::files_utils;
 | |
| use crate::utils::time_utils::{day_number, time_secs, time_start_of_day};
 | |
| 
 | |
| 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<u8>,
 | |
| }
 | |
| 
 | |
| impl RelayStateHistory {
 | |
|     /// Open relay state history file, if it exists, or create an empty one
 | |
|     pub fn open(id: DeviceRelayID, time: u64) -> anyhow::Result<Self> {
 | |
|         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 * 8)) + 1],
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// 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<bool> {
 | |
|         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<usize> {
 | |
|     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)
 | |
| }
 | |
| 
 | |
| /// Get the total runtime of a relay taking account of daily reset time
 | |
| pub fn relay_total_runtime_adjusted(relay: &DeviceRelay) -> usize {
 | |
|     let reset_time = relay
 | |
|         .daily_runtime
 | |
|         .as_ref()
 | |
|         .map(|r| r.reset_time)
 | |
|         .unwrap_or(0);
 | |
| 
 | |
|     let time_start_day = time_start_of_day().unwrap_or(1726696800);
 | |
| 
 | |
|     // Check if we have reached reset_time today yet or not
 | |
|     if time_start_day + reset_time as u64 <= time_secs() {
 | |
|         let start_time = time_start_day + reset_time as u64;
 | |
|         let end_time = time_start_day + 3600 * 24 + reset_time as u64;
 | |
|         relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24)
 | |
|     }
 | |
|     // If we have not reached reset time yet, we need to focus on previous day
 | |
|     else {
 | |
|         let time_start_yesterday = time_start_day - 3600 * 24;
 | |
|         let start_time = time_start_yesterday + reset_time as u64;
 | |
|         let end_time = time_start_day + reset_time as u64;
 | |
|         relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24)
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod tests {
 | |
|     use crate::devices::device::DeviceRelayID;
 | |
|     use crate::energy::relay_state_history::{RelayStateHistory, relay_total_runtime};
 | |
| 
 | |
|     #[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
 | |
|         );
 | |
|     }
 | |
| }
 |