diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index b586ecd..bec05f0 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -8,7 +8,7 @@ use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; use crate::energy::consumption_cache::ConsumptionCache; use crate::energy::consumption_history_file::ConsumptionHistoryFile; -use crate::energy::engine::EnergyEngine; +use crate::energy::engine::{EnergyEngine, RelayForcedState}; use crate::utils::time_utils::time_secs; use actix::prelude::*; use openssl::x509::X509Req; @@ -328,6 +328,19 @@ impl Handler for EnergyActor { } } +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct SetRelayForcedState(pub DeviceRelayID, pub RelayForcedState); + +impl Handler for EnergyActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: SetRelayForcedState, _ctx: &mut Context) -> Self::Result { + self.engine.relay_state(msg.0).set_forced(msg.1); + Ok(()) + } +} + /// Delete a device relay #[derive(Message)] #[rtype(result = "anyhow::Result<()>")] @@ -408,6 +421,7 @@ pub struct ResRelayState { pub id: DeviceRelayID, pub on: bool, pub r#for: usize, + pub forced_state: RelayForcedState, } /// Get the state of all relays @@ -427,6 +441,7 @@ impl Handler for EnergyActor { id: d.id, on: state.is_on(), r#for: state.state_for(), + forced_state: state.actual_forced_state(), }) } diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index 4070270..a7f8eb4 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -25,19 +25,56 @@ impl DeviceState { } } +#[derive(Default, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum RelayForcedState { + #[default] + None, + Off { + until: u64, + }, + On { + until: u64, + }, +} + #[derive(Default, Clone)] pub struct RelayState { on: bool, since: usize, + forced_state: RelayForcedState, } impl RelayState { + /// Get actual forced state (returns None if state is expired) + pub fn actual_forced_state(&self) -> RelayForcedState { + match self.forced_state { + RelayForcedState::Off { until } if until > time_secs() => { + RelayForcedState::Off { until } + } + RelayForcedState::On { until } if until > time_secs() => RelayForcedState::On { until }, + _ => RelayForcedState::None, + } + } + pub fn is_on(&self) -> bool { - self.on + let forced_state = self.actual_forced_state(); + (self.on || matches!(forced_state, RelayForcedState::On { .. })) + && !matches!(forced_state, RelayForcedState::Off { .. }) } fn is_off(&self) -> bool { - !self.on + !self.is_on() + } + + /// Check if relay state is enforced + pub fn is_forced(&self) -> bool { + self.actual_forced_state() != RelayForcedState::None + } + + pub fn set_forced(&mut self, s: RelayForcedState) { + self.since = time_secs() as usize; + self.forced_state = s; } pub fn state_for(&self) -> usize { @@ -146,7 +183,11 @@ impl EnergyEngine { r.name, r.consumption, format!("{} / {}", r.minimal_downtime, r.minimal_uptime), - status.is_on().to_string(), + status.is_on().to_string() + + match status.is_forced() { + true => " (Forced)", + false => "", + }, status.since, match dev_online { true => "Online", @@ -192,19 +233,28 @@ impl EnergyEngine { let mut new_relays_state = self.relays_state.clone(); - // Forcefully turn off relays that belongs to offline devices + // Forcefully turn off disabled relays for d in devices { - if !self.device_state(&d.id).is_online() { - for r in &d.relays { + for r in &d.relays { + if !r.enabled || !d.enabled { new_relays_state.get_mut(&r.id).unwrap().on = false; } } } - // Forcefully turn off disabled relays + // Apply forced relays state for d in devices { for r in &d.relays { - if !r.enabled || !d.enabled { + if self.relay_state(r.id).is_forced() { + new_relays_state.get_mut(&r.id).unwrap().on = self.relay_state(r.id).is_on(); + } + } + } + + // Forcefully turn off relays that belongs to offline devices + for d in devices { + if !self.device_state(&d.id).is_online() { + for r in &d.relays { new_relays_state.get_mut(&r.id).unwrap().on = false; } } @@ -216,7 +266,9 @@ impl EnergyEngine { for d in devices { for r in &d.relays { - if new_relays_state.get(&r.id).unwrap().is_off() { + if new_relays_state.get(&r.id).unwrap().is_off() + || new_relays_state.get(&r.id).unwrap().is_forced() + { continue; } @@ -240,7 +292,7 @@ impl EnergyEngine { for d in devices { for r in &d.relays { let state = new_relays_state.get(&r.id).unwrap(); - if state.is_off() { + if state.is_off() || state.is_forced() { continue; } @@ -271,7 +323,9 @@ impl EnergyEngine { continue; } - if new_relays_state.get(&r.id).unwrap().is_on() { + if new_relays_state.get(&r.id).unwrap().is_on() + || new_relays_state.get(&r.id).unwrap().is_forced() + { continue; } @@ -298,7 +352,7 @@ impl EnergyEngine { } } - // Order relays + // Order relays to select the ones with the most elevated priorities let mut ordered_relays = devices .iter() .filter(|d| self.device_state(&d.id).is_online() && d.enabled) @@ -308,10 +362,13 @@ impl EnergyEngine { ordered_relays.sort_by_key(|r| r.priority); ordered_relays.reverse(); + // Select relays to start, starting with those with highest priorities loop { let mut changed = false; for relay in &ordered_relays { - if new_relays_state.get(&relay.id).unwrap().is_on() { + if new_relays_state.get(&relay.id).unwrap().is_on() + || new_relays_state.get(&relay.id).unwrap().is_forced() + { continue; } diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 07faa3b..ca990f6 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -231,6 +231,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/relay/{id}", web::put().to(relays_controller::update), ) + .route( + "/web_api/relay/{id}/forced_state", + web::put().to(relays_controller::set_forced_state), + ) .route( "/web_api/relay/{id}", web::delete().to(relays_controller::delete), diff --git a/central_backend/src/server/web_api/relays_controller.rs b/central_backend/src/server/web_api/relays_controller.rs index 625b5e1..7749218 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -1,7 +1,9 @@ use crate::devices::device::{DeviceId, DeviceRelay, DeviceRelayID}; use crate::energy::energy_actor; +use crate::energy::engine::RelayForcedState; use crate::server::WebEnergyActor; use crate::server::custom_error::HttpResult; +use crate::utils::time_utils::time_secs; use actix_web::{HttpResponse, web}; /// Get the full list of relays @@ -85,6 +87,50 @@ pub async fn update( Ok(HttpResponse::Accepted().finish()) } +#[derive(Default, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] +pub enum SetRelayForcedStateReq { + #[default] + None, + Off { + for_secs: u64, + }, + On { + for_secs: u64, + }, +} + +/// Set relay forced status +pub async fn set_forced_state( + actor: WebEnergyActor, + req: web::Json, + path: web::Path, +) -> HttpResult { + // Check if relay exists first + let list = actor.send(energy_actor::GetAllRelaysState).await?; + if !list.into_iter().any(|r| r.id == path.id) { + return Ok(HttpResponse::NotFound().json("Relay not found!")); + }; + + // Update relay forced state + actor + .send(energy_actor::SetRelayForcedState( + path.id, + match &req.0 { + SetRelayForcedStateReq::None => RelayForcedState::None, + SetRelayForcedStateReq::Off { for_secs } => RelayForcedState::Off { + until: time_secs() + for_secs, + }, + SetRelayForcedStateReq::On { for_secs } => RelayForcedState::On { + until: time_secs() + for_secs, + }, + }, + )) + .await??; + + Ok(HttpResponse::Accepted().finish()) +} + /// Delete an existing relay pub async fn delete(actor: WebEnergyActor, path: web::Path) -> HttpResult { actor