From 50e61707cc53d7a649ccef2bf08b4e8921d0036a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 27 Aug 2024 18:38:49 +0200 Subject: [PATCH 1/4] Check for loops in relays --- central_backend/src/devices/device.rs | 119 +++++++++++++++++- central_backend/src/server/servers.rs | 4 + .../src/server/web_api/relays_controller.rs | 15 ++- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index e92870d..150a3e4 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -1,7 +1,7 @@ //! # Devices entities definition use crate::constants::StaticConstraints; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; /// Device information provided directly by the device during syncrhonisation. /// @@ -165,7 +165,7 @@ impl DeviceRelay { } } - let relays_map = list.iter().map(|r| (r.id, r)).collect::>(); + let mut relays_map = list.iter().map(|r| (r.id, r)).collect::>(); if self.depends_on.iter().any(|d| !relays_map.contains_key(d)) { return Some("A specified dependent relay does not exists!"); @@ -179,10 +179,73 @@ impl DeviceRelay { return Some("A specified conflicting relay does not exists!"); } - // TODO : check for loops + // 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 @@ -213,7 +276,7 @@ impl DeviceGeneralInfo { #[cfg(test)] mod tests { - use crate::devices::device::DeviceRelay; + use crate::devices::device::{DeviceRelay, DeviceRelayID}; #[test] fn check_device_relay_error() { @@ -238,5 +301,53 @@ mod tests { 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, dep_cycle_3]).is_some()); + + // Impossible conflict + let other_dep = DeviceRelay { + id: DeviceRelayID::default(), + name: "other_dep".to_string(), + ..Default::default() + }; + let 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, second_dep, target_relay.clone()]) + .is_some()); } } diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 985d412..7e717ec 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -165,6 +165,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/relays/list", web::get().to(relays_controller::get_list), ) + .route( + "/web_api/relays/create", + web::post().to(relays_controller::create), + ) // Devices API .route( "/devices_api/utils/time", diff --git a/central_backend/src/server/web_api/relays_controller.rs b/central_backend/src/server/web_api/relays_controller.rs index 45256f3..8f62978 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -1,10 +1,23 @@ +use crate::devices::device::{DeviceId, DeviceRelay}; use crate::energy::energy_actor; use crate::server::custom_error::HttpResult; use crate::server::WebEnergyActor; -use actix_web::HttpResponse; +use actix_web::{web, HttpResponse}; /// Get the full list of relays pub async fn get_list(actor: WebEnergyActor) -> HttpResult { let list = actor.send(energy_actor::GetRelaysList).await?; Ok(HttpResponse::Ok().json(list)) } + +#[derive(serde::Deserialize)] +pub struct CreateDeviceRelayRequest { + device_id: DeviceId, + #[serde(flatten)] + relay: DeviceRelay, +} + +/// Create a new relay +pub async fn create(actor: WebEnergyActor, req: web::Json) -> HttpResult { + todo!() +} From f46a7dbc9477547c7e8764a1b28d227576e748b1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 27 Aug 2024 18:41:01 +0200 Subject: [PATCH 2/4] Update tests --- central_backend/src/devices/device.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index 150a3e4..e93f01c 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -321,8 +321,12 @@ mod tests { ..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()); - assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).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 { @@ -330,7 +334,7 @@ mod tests { name: "other_dep".to_string(), ..Default::default() }; - let second_dep = DeviceRelay { + let mut second_dep = DeviceRelay { id: DeviceRelayID::default(), name: "second_dep".to_string(), conflicts_with: vec![other_dep.id], @@ -347,7 +351,16 @@ mod tests { .error(&[other_dep.clone(), second_dep.clone()]) .is_some()); assert!(target_relay - .error(&[other_dep, second_dep, target_relay.clone()]) + .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()); } } From 87fb3360fbd29d5660018ea6135ccc751167009c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 27 Aug 2024 22:32:22 +0200 Subject: [PATCH 3/4] Can create a relay --- central_backend/src/devices/device.rs | 12 ++++---- central_backend/src/devices/devices_list.rs | 25 ++++++++++++++-- central_backend/src/energy/energy_actor.rs | 30 ++++++++++++++++++- central_backend/src/server/servers.rs | 2 +- .../src/server/web_api/relays_controller.rs | 30 ++++++++++++++++++- central_frontend/src/api/RelayApi.ts | 1 + 6 files changed, 89 insertions(+), 11 deletions(-) diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index e93f01c..4b2bf08 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -13,7 +13,7 @@ pub struct DeviceInfo { /// Device firmware / software version version: semver::Version, /// Maximum number of relay that the device can support - max_relays: usize, + pub max_relays: usize, } impl DeviceInfo { @@ -90,7 +90,7 @@ impl Default for DeviceRelayID { pub struct DeviceRelay { /// Device relay id. Should be unique across the whole application #[serde(default)] - id: DeviceRelayID, + pub id: DeviceRelayID, /// Human-readable name for the relay name: String, /// Whether this relay can be turned on or not @@ -165,7 +165,7 @@ impl DeviceRelay { } } - let mut relays_map = list.iter().map(|r| (r.id, r)).collect::>(); + let relays_map = list.iter().map(|r| (r.id, r)).collect::>(); if self.depends_on.iter().any(|d| !relays_map.contains_key(d)) { return Some("A specified dependent relay does not exists!"); @@ -212,12 +212,12 @@ impl DeviceRelay { clone.insert(self.id); for d in &self.depends_on { - if visited.contains(&d) { + if visited.contains(d) { return true; } if list - .get(&d) + .get(d) .expect("Missing a relay!") .check_for_loop_in_dependencies(&clone, list) { @@ -235,7 +235,7 @@ impl DeviceRelay { list: &HashMap, ) { for d in &self.depends_on { - let dependency = list.get(&d).expect("Missing a relay!"); + let dependency = list.get(d).expect("Missing a relay!"); deps_out.insert(dependency.id); diff --git a/central_backend/src/devices/devices_list.rs b/central_backend/src/devices/devices_list.rs index 095f95f..6115412 100644 --- a/central_backend/src/devices/devices_list.rs +++ b/central_backend/src/devices/devices_list.rs @@ -1,6 +1,8 @@ use crate::app_config::AppConfig; use crate::crypto::pki; -use crate::devices::device::{Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay}; +use crate::devices::device::{ + Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID, +}; use crate::utils::time_utils::time_secs; use openssl::x509::{X509Req, X509}; use std::collections::HashMap; @@ -194,10 +196,29 @@ impl DevicesList { } /// Get the full list of relays - pub fn relays_list(&mut self) -> Vec { + pub fn relays_list(&self) -> Vec { self.0 .iter() .flat_map(|(_id, d)| d.relays.clone()) .collect() } + + /// Create a new relay + pub fn relay_create(&mut self, dev_id: &DeviceId, relay: DeviceRelay) -> anyhow::Result<()> { + let dev = self + .0 + .get_mut(dev_id) + .ok_or(DevicesListError::DeviceNotFound)?; + + dev.relays.push(relay); + + self.persist_dev_config(dev_id)?; + + Ok(()) + } + + /// Get a single relay + pub fn relay_get_single(&self, relay_id: DeviceRelayID) -> Option { + self.relays_list().into_iter().find(|i| i.id == relay_id) + } } diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 91067d2..ec328fc 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -1,5 +1,7 @@ use crate::constants; -use crate::devices::device::{Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay}; +use crate::devices::device::{ + Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID, +}; use crate::devices::devices_list::DevicesList; use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; @@ -184,3 +186,29 @@ impl Handler for EnergyActor { self.devices.relays_list() } } + +/// Create a new device relay +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct CreateDeviceRelay(pub DeviceId, pub DeviceRelay); + +impl Handler for EnergyActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: CreateDeviceRelay, _ctx: &mut Context) -> Self::Result { + self.devices.relay_create(&msg.0, msg.1) + } +} + +/// Get the information about a single relay +#[derive(Message)] +#[rtype(result = "Option")] +pub struct GetSingleRelay(pub DeviceRelayID); + +impl Handler for EnergyActor { + type Result = Option; + + fn handle(&mut self, msg: GetSingleRelay, _ctx: &mut Context) -> Self::Result { + self.devices.relay_get_single(msg.0) + } +} diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 7e717ec..baf1189 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -166,7 +166,7 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> web::get().to(relays_controller::get_list), ) .route( - "/web_api/relays/create", + "/web_api/relay/create", web::post().to(relays_controller::create), ) // Devices API diff --git a/central_backend/src/server/web_api/relays_controller.rs b/central_backend/src/server/web_api/relays_controller.rs index 8f62978..63484ee 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -19,5 +19,33 @@ pub struct CreateDeviceRelayRequest { /// Create a new relay pub async fn create(actor: WebEnergyActor, req: web::Json) -> HttpResult { - todo!() + let list = actor.send(energy_actor::GetRelaysList).await?; + + if let Some(e) = req.relay.error(&list) { + log::error!("Invalid relay create query: {e}"); + return Ok(HttpResponse::BadRequest().json(e)); + } + + let Some(device) = actor + .send(energy_actor::GetSingleDevice(req.device_id.clone())) + .await? + else { + log::error!("Invalid relay create query: specified device does not exists!"); + return Ok(HttpResponse::NotFound().json("Linked device not found!")); + }; + + if device.relays.len() >= device.info.max_relays { + log::error!("Invalid relay create query: too many relay for the target device!"); + return Ok(HttpResponse::BadRequest().json("Too many relays for the target device!")); + } + + // Create the device + actor + .send(energy_actor::CreateDeviceRelay( + req.device_id.clone(), + req.relay.clone(), + )) + .await??; + + Ok(HttpResponse::Accepted().finish()) } diff --git a/central_frontend/src/api/RelayApi.ts b/central_frontend/src/api/RelayApi.ts index 2951884..7296bef 100644 --- a/central_frontend/src/api/RelayApi.ts +++ b/central_frontend/src/api/RelayApi.ts @@ -23,6 +23,7 @@ export class RelayApi { uri: "/relay/create", jsonData: { ...relay, + id: undefined, device_id: device.id, }, }); From 6028be92efc66fb9f3a334ea49e6603a22350f89 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 27 Aug 2024 22:35:07 +0200 Subject: [PATCH 4/4] Anticipate relay ID collision --- central_backend/src/server/web_api/relays_controller.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/central_backend/src/server/web_api/relays_controller.rs b/central_backend/src/server/web_api/relays_controller.rs index 63484ee..639eb3f 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -39,6 +39,15 @@ pub async fn create(actor: WebEnergyActor, req: web::Json