diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs index 513c7e8..97f1475 100644 --- a/central_backend/src/energy/consumption_history_file.rs +++ b/central_backend/src/energy/consumption_history_file.rs @@ -132,12 +132,14 @@ impl ConsumptionHistoryFile { #[cfg(test)] mod tests { + use crate::app_config::ConsumptionHistoryType; use crate::energy::consumption::EnergyConsumption; use crate::energy::consumption_history_file::{ConsumptionHistoryFile, TIME_INTERVAL}; #[test] fn test_consumption_history() { - let mut history = ConsumptionHistoryFile::new_memory(0); + let mut history = + ConsumptionHistoryFile::new_memory(0, ConsumptionHistoryType::GridConsumption); for i in 0..50 { assert_eq!( diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index e1f64c2..0f58c71 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -399,8 +399,8 @@ impl Handler for EnergyActor { #[derive(serde::Serialize)] pub struct ResRelayState { pub id: DeviceRelayID, - on: bool, - r#for: usize, + pub on: bool, + pub r#for: usize, } /// Get the state of all relays diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index 1f8c236..6df1c69 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -8,7 +8,7 @@ use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID}; use crate::energy::consumption::EnergyConsumption; use crate::energy::relay_state_history; use crate::energy::relay_state_history::RelayStateHistory; -use crate::utils::time_utils::{curr_hour, time_secs, time_start_of_day}; +use crate::utils::time_utils::{curr_hour, time_secs}; #[derive(Default)] pub struct DeviceState { @@ -283,12 +283,7 @@ impl EnergyEngine { continue; } - let time_start_day = time_start_of_day().unwrap_or(1726696800); - let start_time = time_start_day + constraints.reset_time as u64; - let end_time = time_start_day + 3600 * 24 + constraints.reset_time as u64; - let total_runtime = - relay_state_history::relay_total_runtime(r.id, start_time, end_time) - .unwrap_or(3600 * 24); + let total_runtime = relay_state_history::relay_total_runtime_adjusted(r); if total_runtime > constraints.min_runtime { continue; @@ -454,7 +449,7 @@ mod test { fn run_test(name: &str, conf: &str) { let (devices, mut energy_engine, consumption, states) = parse_test_config(conf); - energy_engine.refresh(consumption, &devices); + energy_engine.refresh(consumption, &devices.iter().collect::>()); for (device_s, device) in states.iter().zip(&devices) { for (relay_s, relay) in device_s.relays.iter().zip(&device.relays) { diff --git a/central_backend/src/energy/relay_state_history.rs b/central_backend/src/energy/relay_state_history.rs index c46a179..b8e6e6b 100644 --- a/central_backend/src/energy/relay_state_history.rs +++ b/central_backend/src/energy/relay_state_history.rs @@ -1,7 +1,7 @@ use crate::app_config::AppConfig; -use crate::devices::device::DeviceRelayID; +use crate::devices::device::{DeviceRelay, DeviceRelayID}; use crate::utils::files_utils; -use crate::utils::time_utils::day_number; +use crate::utils::time_utils::{day_number, time_start_of_day}; const TIME_INTERVAL: usize = 30; @@ -119,6 +119,20 @@ pub fn relay_total_runtime(device_id: DeviceRelayID, from: u64, to: u64) -> anyh Ok(total) } +/// Get the total runtime of a relay taking account of daily reset time +pub fn relay_total_runtime_adjusted(relay: &DeviceRelay) -> usize { + let reset_time = relay + .daily_runtime + .as_ref() + .map(|r| r.reset_time) + .unwrap_or(0); + + let time_start_day = time_start_of_day().unwrap_or(1726696800); + let start_time = time_start_day + reset_time as u64; + let end_time = time_start_day + 3600 * 24 + reset_time as u64; + relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24) +} + #[cfg(test)] mod tests { use crate::devices::device::DeviceRelayID; diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index 76de370..e847194 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -44,8 +44,8 @@ async fn main() -> std::io::Result<()> { .expect("Failed to initialize energy actor!") .start(); - let s1 = servers::secure_server(actor); - let s2 = servers::unsecure_server(); + let s1 = servers::secure_server(actor.clone()); + let s2 = servers::unsecure_server(actor); future::try_join(s1, s2) .await .expect("Failed to start servers!"); diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 8f01cfe..76d7b82 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -22,14 +22,15 @@ use openssl::ssl::{SslAcceptor, SslMethod}; use std::time::Duration; /// Start unsecure (HTTP) server -pub async fn unsecure_server() -> anyhow::Result<()> { +pub async fn unsecure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> { log::info!( "Unsecure server starting to listen on {} for {}", AppConfig::get().unsecure_listen_address, AppConfig::get().unsecure_origin() ); - HttpServer::new(|| { + HttpServer::new(move || { App::new() + .app_data(web::Data::new(energy_actor.clone())) .wrap(Logger::default()) .route( "/", @@ -43,6 +44,10 @@ pub async fn unsecure_server() -> anyhow::Result<()> { "/pki/{file}", web::get().to(unsecure_pki_controller::serve_pki_file), ) + .route( + "/relay/{id}/legacy_state", + web::get().to(unsecure_relay_controller::legacy_state), + ) }) .bind(&AppConfig::get().unsecure_listen_address)? .run() diff --git a/central_backend/src/server/unsecure_server/mod.rs b/central_backend/src/server/unsecure_server/mod.rs index 95a8ad0..eca8ea0 100644 --- a/central_backend/src/server/unsecure_server/mod.rs +++ b/central_backend/src/server/unsecure_server/mod.rs @@ -1,2 +1,3 @@ pub mod unsecure_pki_controller; +pub mod unsecure_relay_controller; pub mod unsecure_server_controller; diff --git a/central_backend/src/server/unsecure_server/unsecure_relay_controller.rs b/central_backend/src/server/unsecure_server/unsecure_relay_controller.rs new file mode 100644 index 0000000..7455fff --- /dev/null +++ b/central_backend/src/server/unsecure_server/unsecure_relay_controller.rs @@ -0,0 +1,60 @@ +use crate::devices::device::DeviceRelayID; +use crate::energy::{energy_actor, relay_state_history}; +use crate::server::custom_error::HttpResult; +use crate::server::WebEnergyActor; +use actix_web::{web, HttpResponse}; + +#[derive(serde::Deserialize)] +pub struct LegacyStateRelay { + id: DeviceRelayID, +} + +/// Legacy relay state +#[derive(serde::Serialize)] +pub struct LegacyState { + /// Indicates if relay is on or off + is_on: bool, + /// Relay name + name: String, + /// Duration since last change of state + r#for: usize, + /// Current grid consumption + prod: i32, + /// Total uptime since last reset + total_uptime: usize, + /// Required uptime during a day + /// + /// Will be 0 if there is no daily requirements + required_uptime: usize, +} + +/// Get the state of a relay, adapted for old system components +pub async fn legacy_state( + energy_actor: WebEnergyActor, + path: web::Path, +) -> HttpResult { + let Some(relay) = energy_actor + .send(energy_actor::GetSingleRelay(path.id)) + .await? + else { + return Ok(HttpResponse::NotFound().body("Relay not found!")); + }; + + let all_states = energy_actor.send(energy_actor::GetAllRelaysState).await?; + let Some(state) = all_states.into_iter().find(|r| r.id == path.id) else { + return Ok(HttpResponse::InternalServerError().body("Relay status unavailable!")); + }; + + let production = energy_actor.send(energy_actor::GetCurrConsumption).await?; + + let total_uptime = relay_state_history::relay_total_runtime_adjusted(&relay); + + Ok(HttpResponse::Ok().json(LegacyState { + name: relay.name, + is_on: state.on, + r#for: state.r#for.min(3600 * 24 * 7), + prod: production, + total_uptime, + required_uptime: relay.daily_runtime.map(|r| r.min_runtime).unwrap_or(0), + })) +} diff --git a/central_backend/src/server/web_api/server_controller.rs b/central_backend/src/server/web_api/server_controller.rs index 9b0626c..95b067c 100644 --- a/central_backend/src/server/web_api/server_controller.rs +++ b/central_backend/src/server/web_api/server_controller.rs @@ -12,6 +12,7 @@ pub async fn secure_home() -> HttpResponse { struct ServerConfig { auth_disabled: bool, constraints: StaticConstraints, + unsecure_origin: String, } impl Default for ServerConfig { @@ -19,6 +20,7 @@ impl Default for ServerConfig { Self { auth_disabled: AppConfig::get().unsecure_disable_login, constraints: Default::default(), + unsecure_origin: AppConfig::get().unsecure_origin(), } } } diff --git a/central_frontend/src/api/ServerApi.ts b/central_frontend/src/api/ServerApi.ts index 798df78..c77105f 100644 --- a/central_frontend/src/api/ServerApi.ts +++ b/central_frontend/src/api/ServerApi.ts @@ -3,6 +3,7 @@ import { APIClient } from "./ApiClient"; export interface ServerConfig { auth_disabled: boolean; constraints: ServerConstraint; + unsecure_origin: string; } export interface ServerConstraint { diff --git a/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx b/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx index 8ea8ff0..6018173 100644 --- a/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx +++ b/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx @@ -129,7 +129,9 @@ export function DeviceRelays(p: { ); } -function RelayEntryStatus(p: { relay: DeviceRelay }): React.ReactElement { +function RelayEntryStatus( + p: Readonly<{ relay: DeviceRelay }> +): React.ReactElement { const [state, setState] = React.useState(); const load = async () => { diff --git a/central_frontend/src/routes/RelaysListRoute.tsx b/central_frontend/src/routes/RelaysListRoute.tsx index 6696bc6..6437e8d 100644 --- a/central_frontend/src/routes/RelaysListRoute.tsx +++ b/central_frontend/src/routes/RelaysListRoute.tsx @@ -1,3 +1,4 @@ +import LinkIcon from "@mui/icons-material/Link"; import RefreshIcon from "@mui/icons-material/Refresh"; import { IconButton, @@ -19,6 +20,8 @@ import { AsyncWidget } from "../widgets/AsyncWidget"; import { BoolText } from "../widgets/BoolText"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; +import { CopyToClipboard } from "../widgets/CopyToClipboard"; +import { ServerApi } from "../api/ServerApi"; export function RelaysListRoute(p: { homeWidget?: boolean; @@ -101,6 +104,7 @@ function RelaysList(p: { Priority Consumption Status + @@ -125,6 +129,20 @@ function RelaysList(p: { />{" "} for + + + + + + + + + ))} diff --git a/central_frontend/src/widgets/CopyToClipboard.tsx b/central_frontend/src/widgets/CopyToClipboard.tsx new file mode 100644 index 0000000..72ea019 --- /dev/null +++ b/central_frontend/src/widgets/CopyToClipboard.tsx @@ -0,0 +1,30 @@ +import { ButtonBase } from "@mui/material"; +import { PropsWithChildren } from "react"; +import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; + +export function CopyToClipboard( + p: PropsWithChildren<{ content: string }> +): React.ReactElement { + const snackbar = useSnackbar(); + + const copy = () => { + navigator.clipboard.writeText(p.content); + snackbar(`${p.content} copied to clipboard.`); + }; + + return ( + + {p.children} + + ); +}