//! # Devices entities definition use crate::constants::StaticConstraints; use std::collections::{HashMap, HashSet}; /// Device information provided directly by the device during syncrhonisation. /// /// It should not be editable fro the Web UI #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DeviceInfo { /// Device reference reference: String, /// Device firmware / software version version: semver::Version, /// Maximum number of relay that the device can support pub max_relays: usize, } impl DeviceInfo { /// Identify errors in device information definition pub fn error(&self) -> Option<&str> { if self.reference.trim().is_empty() { return Some("Given device reference is empty or blank!"); } if self.max_relays == 0 { return Some("Given device cannot handle any relay!"); } None } } /// Device identifier #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct DeviceId(pub String); /// Single device information #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Device { /// The device ID pub id: DeviceId, /// Information about the device /// /// These information shall not be editable from the webui. They are automatically updated during /// device synchronization pub info: DeviceInfo, /// Time at which device was initially enrolled pub time_create: u64, /// Time at which device was last updated pub time_update: u64, /// Name given to the device on the Web UI pub name: String, /// Description given to the device on the Web UI pub description: String, /// Specify whether the device has been validated or not. Validated devices are given a /// certificate pub validated: bool, /// Specify whether the device is enabled or not pub enabled: bool, /// Information about the relays handled by the device /// /// There cannot be more than [info.max_relays] relays pub relays: Vec, } /// Structure that contains information about the minimal expected execution /// time of a device #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DailyMinRuntime { /// Minimum time, in seconds, that this relay should run each day pub min_runtime: usize, /// The seconds in the days (from 00:00) where the counter is reset pub reset_time: usize, /// The hours during which the relay should be turned on to reach expected runtime pub catch_up_hours: Vec, } #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct DeviceRelayID(uuid::Uuid); impl Default for DeviceRelayID { fn default() -> Self { Self(uuid::Uuid::new_v4()) } } /// Single device relay information #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)] pub struct DeviceRelay { /// Device relay id. Should be unique across the whole application #[serde(default)] pub id: DeviceRelayID, /// Human-readable name for the relay pub name: String, /// Whether this relay can be turned on or not pub enabled: bool, /// Relay priority when selecting relays to turn on. 0 = lowest priority pub priority: usize, /// Estimated consumption of the electrical equipment triggered by the relay pub consumption: usize, /// Minimal time this relay shall be left on before it can be turned off (in seconds) pub minimal_uptime: usize, /// Minimal time this relay shall be left off before it can be turned on again (in seconds) pub minimal_downtime: usize, /// Optional minimal runtime requirements for this relay pub daily_runtime: Option, /// Specify relay that must be turned on before this relay can be started pub depends_on: Vec, /// Specify relays that must be turned off before this relay can be started pub conflicts_with: Vec, } impl DeviceRelay { /// Check device relay for errors pub fn error(&self, list: &[DeviceRelay]) -> Option<&'static str> { let constraints = StaticConstraints::default(); if !constraints.relay_name_len.validate(&self.name) { return Some("Invalid relay name length!"); } if !constraints.relay_priority.validate_usize(self.priority) { return Some("Invalid relay priority!"); } if !constraints .relay_consumption .validate_usize(self.consumption) { return Some("Invalid consumption!"); } if !constraints .relay_minimal_uptime .validate_usize(self.minimal_uptime) { return Some("Invalid minimal uptime!"); } if !constraints .relay_minimal_downtime .validate_usize(self.minimal_downtime) { return Some("Invalid minimal uptime!"); } if let Some(daily) = &self.daily_runtime { if !constraints .relay_daily_minimal_runtime .validate_usize(daily.min_runtime) { return Some("Invalid minimal daily runtime!"); } if daily.reset_time > 3600 * 24 { return Some("Invalid daily reset time!"); } if daily.catch_up_hours.is_empty() { return Some("No catchup hours defined!"); } if daily.catch_up_hours.iter().any(|h| h > &23) { return Some("At least one catch up hour is invalid!"); } } let mut relays_map = list.iter().map(|r| (r.id, r)).collect::>(); relays_map.insert(self.id, self); if self.depends_on.iter().any(|d| !relays_map.contains_key(d)) { return Some("A specified dependent relay does not exists!"); } if self .conflicts_with .iter() .any(|d| !relays_map.contains_key(d)) { return Some("A specified conflicting relay does not exists!"); } // Check for loops in dependencies if self.check_for_loop_in_dependencies(&HashSet::new(), &relays_map) { return Some("A loop was detected in relay dependencies!"); } // Check if relay is in conflicts with one of its dependencies let mut all_dependencies = HashSet::new(); let mut all_conflicts = HashSet::new(); self.get_list_of_dependencies_and_conflicts( &mut all_dependencies, &mut all_conflicts, &relays_map, ); for conf_id in all_conflicts { if all_dependencies.contains(&conf_id) { return Some( "The relay or one of its dependencies is in conflict with a dependency!", ); } } None } fn check_for_loop_in_dependencies( &self, visited: &HashSet, list: &HashMap, ) -> bool { let mut clone = visited.clone(); clone.insert(self.id); for d in &self.depends_on { if visited.contains(d) { return true; } if list .get(d) .expect("Missing a relay!") .check_for_loop_in_dependencies(&clone, list) { return true; } } false } fn get_list_of_dependencies_and_conflicts( &self, deps_out: &mut HashSet, conflicts_out: &mut HashSet, list: &HashMap, ) { for d in &self.depends_on { let dependency = list.get(d).expect("Missing a relay!"); deps_out.insert(dependency.id); dependency.get_list_of_dependencies_and_conflicts(deps_out, conflicts_out, list); } for d in &self.conflicts_with { conflicts_out.insert(*d); } } } /// Device general information /// /// This structure is used to update device information #[derive(serde::Deserialize, Debug, Clone)] pub struct DeviceGeneralInfo { pub name: String, pub description: String, pub enabled: bool, } impl DeviceGeneralInfo { /// Check for errors in the structure pub fn error(&self) -> Option<&'static str> { let constraints = StaticConstraints::default(); if !constraints.dev_name_len.validate(&self.name) { return Some("Invalid device name length!"); } if !constraints.dev_description_len.validate(&self.description) { return Some("Invalid device description length!"); } None } } #[cfg(test)] mod tests { use crate::devices::device::{DeviceRelay, DeviceRelayID}; #[test] fn check_device_relay_error() { let unitary = DeviceRelay { name: "unitary".to_string(), ..Default::default() }; let bad_name = DeviceRelay { name: "".to_string(), ..Default::default() }; let dep_on_unitary = DeviceRelay { name: "dep_on_unitary".to_string(), depends_on: vec![unitary.id], ..Default::default() }; assert_eq!(unitary.error(&[]), None); assert_eq!(unitary.error(&[unitary.clone(), bad_name.clone()]), None); assert!(bad_name.error(&[]).is_some()); assert_eq!(dep_on_unitary.error(&[unitary.clone()]), None); assert!(dep_on_unitary.error(&[]).is_some()); // Dependency loop let mut dep_cycle_1 = DeviceRelay { id: DeviceRelayID::default(), name: "dep_cycle_1".to_string(), ..Default::default() }; let dep_cycle_2 = DeviceRelay { id: DeviceRelayID::default(), name: "dep_cycle_2".to_string(), depends_on: vec![dep_cycle_1.id], ..Default::default() }; let dep_cycle_3 = DeviceRelay { id: DeviceRelayID::default(), name: "dep_cycle_3".to_string(), depends_on: vec![dep_cycle_2.id], ..Default::default() }; dep_cycle_1.depends_on = vec![dep_cycle_3.id]; assert!(dep_cycle_1 .error(&[dep_cycle_2.clone(), dep_cycle_3.clone()]) .is_some()); dep_cycle_1.depends_on = vec![]; assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).is_none()); // Impossible conflict let other_dep = DeviceRelay { id: DeviceRelayID::default(), name: "other_dep".to_string(), ..Default::default() }; let mut second_dep = DeviceRelay { id: DeviceRelayID::default(), name: "second_dep".to_string(), conflicts_with: vec![other_dep.id], ..Default::default() }; let target_relay = DeviceRelay { id: DeviceRelayID::default(), name: "target_relay".to_string(), depends_on: vec![other_dep.id, second_dep.id], ..Default::default() }; assert!(target_relay .error(&[other_dep.clone(), second_dep.clone()]) .is_some()); assert!(target_relay .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) .is_some()); second_dep.conflicts_with = vec![]; assert!(target_relay .error(&[other_dep.clone(), second_dep.clone()]) .is_none()); assert!(target_relay .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) .is_none()); // self loop let mut self_loop = DeviceRelay { id: DeviceRelayID::default(), name: "self_loop".to_string(), ..Default::default() }; let self_loop_good = self_loop.clone(); self_loop.depends_on = vec![self_loop.id]; assert!(self_loop.error(&[]).is_some()); assert!(self_loop.error(&[self_loop.clone()]).is_some()); assert!(self_loop.error(&[self_loop_good.clone()]).is_some()); self_loop.depends_on = vec![]; assert!(self_loop_good.error(&[]).is_none()); assert!(self_loop_good.error(&[self_loop_good.clone()]).is_none()); assert!(self_loop_good.error(&[self_loop.clone()]).is_none()); } }