This commit is contained in:
Pierre HUBERT 2024-08-31 18:06:30 +02:00
commit 8c2dcd3855
6 changed files with 237 additions and 9 deletions

View File

@ -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.
///
@ -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
@ -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<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
@ -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,66 @@ 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.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());
}
}

View File

@ -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<DeviceRelay> {
pub fn relays_list(&self) -> Vec<DeviceRelay> {
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<DeviceRelay> {
self.relays_list().into_iter().find(|i| i.id == relay_id)
}
}

View File

@ -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<GetRelaysList> 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<CreateDeviceRelay> for EnergyActor {
type Result = anyhow::Result<()>;
fn handle(&mut self, msg: CreateDeviceRelay, _ctx: &mut Context<Self>) -> Self::Result {
self.devices.relay_create(&msg.0, msg.1)
}
}
/// Get the information about a single relay
#[derive(Message)]
#[rtype(result = "Option<DeviceRelay>")]
pub struct GetSingleRelay(pub DeviceRelayID);
impl Handler<GetSingleRelay> for EnergyActor {
type Result = Option<DeviceRelay>;
fn handle(&mut self, msg: GetSingleRelay, _ctx: &mut Context<Self>) -> Self::Result {
self.devices.relay_get_single(msg.0)
}
}

View File

@ -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/relay/create",
web::post().to(relays_controller::create),
)
// Devices API
.route(
"/devices_api/utils/time",

View File

@ -1,10 +1,60 @@
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<CreateDeviceRelayRequest>) -> HttpResult {
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!"));
}
if actor
.send(energy_actor::GetSingleRelay(req.relay.id))
.await?
.is_some()
{
log::error!("Invalid relay create query: A relay with the same ID already exists!");
return Ok(HttpResponse::BadRequest().json("A relay with the same ID already exists!"));
}
// Create the device
actor
.send(energy_actor::CreateDeviceRelay(
req.device_id.clone(),
req.relay.clone(),
))
.await??;
Ok(HttpResponse::Accepted().finish())
}

View File

@ -23,6 +23,7 @@ export class RelayApi {
uri: "/relay/create",
jsonData: {
...relay,
id: undefined,
device_id: device.id,
},
});