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());
|
|
}
|
|
}
|