367 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			367 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! # 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
 | 
						|
    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<DeviceRelay>,
 | 
						|
}
 | 
						|
 | 
						|
/// 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<usize>,
 | 
						|
}
 | 
						|
 | 
						|
#[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)]
 | 
						|
    id: DeviceRelayID,
 | 
						|
    /// Human-readable name for the relay
 | 
						|
    name: String,
 | 
						|
    /// Whether this relay can be turned on or not
 | 
						|
    enabled: bool,
 | 
						|
    /// Relay priority when selecting relays to turn on. 0 = lowest priority
 | 
						|
    priority: usize,
 | 
						|
    /// Estimated consumption of the electrical equipment triggered by the relay
 | 
						|
    consumption: usize,
 | 
						|
    /// Minimal time this relay shall be left on before it can be turned off (in seconds)
 | 
						|
    minimal_uptime: usize,
 | 
						|
    /// Minimal time this relay shall be left off before it can be turned on again (in seconds)
 | 
						|
    minimal_downtime: usize,
 | 
						|
    /// Optional minimal runtime requirements for this relay
 | 
						|
    daily_runtime: Option<DailyMinRuntime>,
 | 
						|
    /// Specify relay that must be turned on before this relay can be started
 | 
						|
    depends_on: Vec<DeviceRelayID>,
 | 
						|
    /// Specify relays that must be turned off before this relay can be started
 | 
						|
    conflicts_with: Vec<DeviceRelayID>,
 | 
						|
}
 | 
						|
 | 
						|
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::<HashMap<_, _>>();
 | 
						|
 | 
						|
        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<DeviceRelayID>,
 | 
						|
        list: &HashMap<DeviceRelayID, &Self>,
 | 
						|
    ) -> 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<DeviceRelayID>,
 | 
						|
        conflicts_out: &mut HashSet<DeviceRelayID>,
 | 
						|
        list: &HashMap<DeviceRelayID, &Self>,
 | 
						|
    ) {
 | 
						|
        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());
 | 
						|
    }
 | 
						|
}
 |