From d98be00a40aa26c0862a288009330e0f718df2c5 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 23 Sep 2024 18:55:10 +0200 Subject: [PATCH 01/14] Update custom consumption dependencies --- custom_consumption/Cargo.lock | 167 ++++++++++++++------------------- custom_consumption/Cargo.toml | 10 +- custom_consumption/src/main.rs | 2 +- 3 files changed, 74 insertions(+), 105 deletions(-) diff --git a/custom_consumption/Cargo.lock b/custom_consumption/Cargo.lock index 266dcc9..cff5c7f 100644 --- a/custom_consumption/Cargo.lock +++ b/custom_consumption/Cargo.lock @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -619,6 +619,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -691,9 +697,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -701,9 +707,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -713,9 +719,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -738,36 +744,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -778,12 +754,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.1" @@ -985,21 +955,22 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ecolor" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" +checksum = "2e6b451ff1143f6de0f33fc7f1b68fecfd2c7de06e104de96c4514de3f5396f8" dependencies = [ "bytemuck", + "emath", ] [[package]] name = "eframe" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020e2ccef6bbcec71dbc542f7eed64a5846fc3076727f5746da8fd307c91bab2" +checksum = "6490ef800b2e41ee129b1f32f9ac15f713233fe3bc18e241a1afe1e4fb6811e0" dependencies = [ + "ahash", "bytemuck", - "cocoa", "document-features", "egui", "egui-wgpu", @@ -1011,13 +982,14 @@ dependencies = [ "image", "js-sys", "log", - "objc", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", "percent-encoding", "raw-window-handle 0.5.2", "raw-window-handle 0.6.2", "static_assertions", - "thiserror", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1028,12 +1000,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a" +checksum = "20c97e70a2768de630f161bb5392cbd3874fcf72868f14df0e002e82e06cb798" dependencies = [ "accesskit", "ahash", + "emath", "epaint", "log", "nohash-hasher", @@ -1041,10 +1014,11 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469ff65843f88a702b731a1532b7d03b0e8e96d283e70f3a22b0e06c46cb9b37" +checksum = "47c7a7c707877c3362a321ebb4f32be811c0b91f7aebf345fb162405c0218b4c" dependencies = [ + "ahash", "bytemuck", "document-features", "egui", @@ -1059,11 +1033,12 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609" +checksum = "fac4e066af341bf92559f60dbdf2020b2a03c963415349af5f3f8d79ff7a4926" dependencies = [ "accesskit_winit", + "ahash", "arboard", "egui", "log", @@ -1076,10 +1051,11 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e5d975f3c86edc3d35b1db88bb27c15dde7c55d3b5af164968ab5ede3f44ca" +checksum = "4e2bdc8b38cfa17cc712c4ae079e30c71c00cd4c2763c9e16dc7860a02769103" dependencies = [ + "ahash", "bytemuck", "egui", "glow", @@ -1092,9 +1068,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" +checksum = "0a6a21708405ea88f63d8309650b4d77431f4bc28fb9d8e6f77d3963b51249e6" dependencies = [ "bytemuck", ] @@ -1132,9 +1108,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -1145,9 +1121,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176" +checksum = "3f0dcc0a0771e7500e94cd1cb797bd13c9f23b9409bdc3c824e2cbc562b7fa01" dependencies = [ "ab_glyph", "ahash", @@ -1510,9 +1486,9 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", @@ -1521,9 +1497,9 @@ dependencies = [ [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ "bitflags 2.6.0", ] @@ -1621,13 +1597,12 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", - "color_quant", + "byteorder-lite", "num-traits", "png", ] @@ -1846,9 +1821,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ "bitflags 2.6.0", "block", @@ -1871,10 +1846,11 @@ dependencies = [ [[package]] name = "naga" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ + "arrayvec", "bit-set", "bitflags 2.6.0", "codespan-reporting", @@ -1975,7 +1951,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -2119,15 +2094,6 @@ dependencies = [ "objc2-metal", ] -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -3149,30 +3115,32 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.15" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" dependencies = [ + "block2 0.5.1", "core-foundation", "home", "jni", "log", "ndk-context", - "objc", - "raw-window-handle 0.5.2", + "objc2 0.5.2", + "objc2-foundation", "url", "web-sys", ] [[package]] name = "wgpu" -version = "0.19.4" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" dependencies = [ "arrayvec", "cfg-if", "cfg_aliases", + "document-features", "js-sys", "log", "parking_lot", @@ -3190,15 +3158,16 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.4" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ "arrayvec", "bit-vec", "bitflags 2.6.0", "cfg_aliases", "codespan-reporting", + "document-features", "indexmap", "log", "naga", @@ -3216,9 +3185,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.4" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" dependencies = [ "android_system_properties", "arrayvec", @@ -3257,9 +3226,9 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ "bitflags 2.6.0", "js-sys", diff --git a/custom_consumption/Cargo.toml b/custom_consumption/Cargo.toml index 3eaf2e8..b38451e 100644 --- a/custom_consumption/Cargo.toml +++ b/custom_consumption/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -env_logger = "0.11.3" +env_logger = "0.11.5" log = "0.4.22" -clap = { version = "4.5.8", features = ["derive", "env"] } -egui = "0.27.2" -eframe = "0.27.2" -lazy_static = "1.5.0" \ No newline at end of file +clap = { version = "4.5.18", features = ["derive", "env"] } +egui = "0.28.1" +eframe = "0.28.1" +lazy_static = "1.5.0" diff --git a/custom_consumption/src/main.rs b/custom_consumption/src/main.rs index 33d6471..972fd23 100644 --- a/custom_consumption/src/main.rs +++ b/custom_consumption/src/main.rs @@ -11,7 +11,7 @@ fn main() { eframe::run_native( "Custom consumption", options, - Box::new(|_cc| Box::::default()), + Box::new(|_cc| Ok(Box::::default())), ) .unwrap() } From 0e0da14fde6e53a83de34a36375bd717569149a5 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 23 Sep 2024 20:30:34 +0200 Subject: [PATCH 02/14] Use cached consumption value --- central_backend/src/app_config.rs | 6 +- .../src/energy/consumption_cache.rs | 85 +++++++++++++++++++ central_backend/src/energy/energy_actor.rs | 44 +++++++--- central_backend/src/energy/mod.rs | 1 + 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 central_backend/src/energy/consumption_cache.rs diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index 8f159c8..73fced1 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -81,9 +81,13 @@ pub struct AppConfig { pub production_margin: i32, /// Energy refresh operations interval, in seconds - #[arg(short('i'), long, env, default_value_t = 20)] + #[arg(short('i'), long, env, default_value_t = 25)] pub refresh_interval: u64, + /// Energy refresh operations interval, in seconds + #[arg(short('f'), long, env, default_value_t = 5)] + pub energy_fetch_interval: u64, + /// Consumption backend provider #[clap(subcommand)] pub consumption_backend: Option, diff --git a/central_backend/src/energy/consumption_cache.rs b/central_backend/src/energy/consumption_cache.rs new file mode 100644 index 0000000..fb4aa8e --- /dev/null +++ b/central_backend/src/energy/consumption_cache.rs @@ -0,0 +1,85 @@ +use crate::constants; +use crate::energy::consumption::EnergyConsumption; +use log::log; + +pub struct ConsumptionCache { + nb_vals: usize, + values: Vec, +} + +impl ConsumptionCache { + pub fn new(nb_vals: usize) -> Self { + Self { + nb_vals, + values: vec![], + } + } + + pub fn add_value(&mut self, value: EnergyConsumption) { + if self.values.len() >= self.nb_vals { + self.values.remove(0); + } + + self.values.push(value); + } + + pub fn median_value(&self) -> EnergyConsumption { + if self.values.is_empty() { + return constants::FALLBACK_PRODUCTION_VALUE; + } + + let mut clone = self.values.clone(); + clone.sort(); + let median = clone[clone.len() / 2]; + + log::info!("Cached consumption: {:?} / Median: {}", self.values, median); + + median + } +} + +#[cfg(test)] +pub mod test { + use crate::constants; + use crate::energy::consumption_cache::ConsumptionCache; + + #[test] + fn empty_vec() { + let cache = ConsumptionCache::new(10); + assert_eq!(cache.median_value(), constants::FALLBACK_PRODUCTION_VALUE); + } + + #[test] + fn single_value() { + let mut cache = ConsumptionCache::new(10); + cache.add_value(-10); + assert_eq!(cache.median_value(), -10); + } + + #[test] + fn four_values() { + let mut cache = ConsumptionCache::new(10); + cache.add_value(50); + cache.add_value(-10); + cache.add_value(-10); + cache.add_value(-10000); + assert_eq!(cache.median_value(), -10); + } + + #[test] + fn many_values() { + let mut cache = ConsumptionCache::new(6); + + for i in 0..1000 { + cache.add_value(-i); + } + + cache.add_value(10); + cache.add_value(50); + cache.add_value(-10); + cache.add_value(-10); + cache.add_value(-30); + cache.add_value(-10000); + assert_eq!(cache.median_value(), -10); + } +} diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 766eca7..57f9594 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -6,6 +6,7 @@ use crate::devices::device::{ use crate::devices::devices_list::DevicesList; use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; +use crate::energy::consumption_cache::ConsumptionCache; use crate::energy::engine::EnergyEngine; use crate::utils::time_utils::time_secs; use actix::prelude::*; @@ -13,34 +14,55 @@ use openssl::x509::X509Req; use std::time::Duration; pub struct EnergyActor { - curr_consumption: EnergyConsumption, + consumption_cache: ConsumptionCache, devices: DevicesList, engine: EnergyEngine, + last_engine_refresh: u64, } impl EnergyActor { pub async fn new() -> anyhow::Result { + let consumption_cache_size = + AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval; + let curr_consumption = consumption::get_curr_consumption().await?; + let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize); + consumption_cache.add_value(curr_consumption); + + if consumption_cache_size < 1 { + panic!("Energy fetch interval must be equal or smaller than refresh interval!"); + } + Ok(Self { - curr_consumption: consumption::get_curr_consumption().await?, + consumption_cache, devices: DevicesList::load()?, engine: EnergyEngine::default(), + last_engine_refresh: 0, }) } async fn refresh(&mut self) -> anyhow::Result<()> { // Refresh energy - self.curr_consumption = consumption::get_curr_consumption() - .await - .unwrap_or_else(|e| { - log::error!( + self.consumption_cache + .add_value( + consumption::get_curr_consumption() + .await + .unwrap_or_else(|e| { + log::error!( "Failed to fetch latest consumption value, will use fallback value! {e}" ); - constants::FALLBACK_PRODUCTION_VALUE - }); + constants::FALLBACK_PRODUCTION_VALUE + }), + ); + + if self.last_engine_refresh + AppConfig::get().refresh_interval > time_secs() { + return Ok(()); + } + self.last_engine_refresh = time_secs(); let devices_list = self.devices.full_list(); - self.engine.refresh(self.curr_consumption, &devices_list); + self.engine + .refresh(self.consumption_cache.median_value(), &devices_list); self.engine.persist_relays_state(&devices_list)?; @@ -55,7 +77,7 @@ impl Actor for EnergyActor { log::info!("Energy actor successfully started!"); ctx.run_interval( - Duration::from_secs(AppConfig::get().refresh_interval), + Duration::from_secs(AppConfig::get().energy_fetch_interval), |act, _ctx| { log::info!("Performing energy refresh operation"); if let Err(e) = futures::executor::block_on(act.refresh()) { @@ -81,7 +103,7 @@ impl Handler for EnergyActor { type Result = EnergyConsumption; fn handle(&mut self, _msg: GetCurrConsumption, _ctx: &mut Context) -> Self::Result { - self.curr_consumption + self.consumption_cache.median_value() } } diff --git a/central_backend/src/energy/mod.rs b/central_backend/src/energy/mod.rs index 2922113..5ef5270 100644 --- a/central_backend/src/energy/mod.rs +++ b/central_backend/src/energy/mod.rs @@ -1,4 +1,5 @@ pub mod consumption; +pub mod consumption_cache; pub mod energy_actor; pub mod engine; pub mod relay_state_history; From 228e1a7293d565bc7f3caf3454bf39ffa2b8d45a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 23 Sep 2024 21:47:25 +0200 Subject: [PATCH 03/14] Record received consumption from inverter --- central_backend/Cargo.lock | 26 ++++ central_backend/Cargo.toml | 3 +- central_backend/src/app_config.rs | 12 ++ .../src/energy/consumption_cache.rs | 1 - .../src/energy/consumption_history_file.rs | 128 ++++++++++++++++++ central_backend/src/energy/energy_actor.rs | 21 +-- central_backend/src/energy/mod.rs | 1 + .../src/energy/relay_state_history.rs | 2 +- central_backend/src/main.rs | 1 + 9 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 central_backend/src/energy/consumption_history_file.rs diff --git a/central_backend/Cargo.lock b/central_backend/Cargo.lock index 9c1271b..f1408bd 100644 --- a/central_backend/Cargo.lock +++ b/central_backend/Cargo.lock @@ -523,6 +523,25 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -608,6 +627,7 @@ dependencies = [ "actix-web", "anyhow", "asn1", + "bincode", "chrono", "clap", "env_logger", @@ -2697,6 +2717,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/central_backend/Cargo.toml b/central_backend/Cargo.toml index a275bc5..44cd8da 100644 --- a/central_backend/Cargo.toml +++ b/central_backend/Cargo.toml @@ -37,4 +37,5 @@ rust-embed = "8.5.0" jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } prettytable-rs = "0.10.0" chrono = "0.4.38" -serde_yml = "0.0.12" \ No newline at end of file +serde_yml = "0.0.12" +bincode = "=2.0.0-rc.3" \ No newline at end of file diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index 73fced1..c03cd63 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -255,6 +255,18 @@ impl AppConfig { pub fn relay_runtime_day_file_path(&self, relay_id: DeviceRelayID, day: u64) -> PathBuf { self.relay_runtime_stats_dir(relay_id).join(day.to_string()) } + + /// Get energy consumption history path + pub fn energy_consumption_history(&self) -> PathBuf { + self.storage_path().join("consumption_history") + } + + /// Get energy consumption history file path for a given day + pub fn energy_consumption_history_day(&self, number: u64) -> PathBuf { + self.storage_path() + .join("consumption_history") + .join(number.to_string()) + } } #[cfg(test)] diff --git a/central_backend/src/energy/consumption_cache.rs b/central_backend/src/energy/consumption_cache.rs index fb4aa8e..f9beefe 100644 --- a/central_backend/src/energy/consumption_cache.rs +++ b/central_backend/src/energy/consumption_cache.rs @@ -1,6 +1,5 @@ use crate::constants; use crate::energy::consumption::EnergyConsumption; -use log::log; pub struct ConsumptionCache { nb_vals: usize, diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs new file mode 100644 index 0000000..5b9a43d --- /dev/null +++ b/central_backend/src/energy/consumption_history_file.rs @@ -0,0 +1,128 @@ +use crate::app_config::AppConfig; +use crate::energy::consumption::EnergyConsumption; +use crate::utils::time_utils::day_number; + +const TIME_INTERVAL: usize = 10; + +#[derive(thiserror::Error, Debug)] +pub enum ConsumptionHistoryError { + #[error("Given time is out of file bounds!")] + TimeOutOfFileBound, +} + +/// # ConsumptionHistoryFile +/// +/// Stores the history of house consumption +pub struct ConsumptionHistoryFile { + day: u64, + buff: Vec, +} + +impl ConsumptionHistoryFile { + /// Open consumption history file, if it exists, or create an empty one + pub fn open(time: u64) -> anyhow::Result { + let day = day_number(time); + let path = AppConfig::get().energy_consumption_history_day(day); + + if path.exists() { + Ok(Self { + day, + buff: bincode::decode_from_slice( + &std::fs::read(path)?, + bincode::config::standard(), + )? + .0, + }) + } else { + log::debug!( + "Energy consumption stats for day {day} does not exists yet, creating memory buffer" + ); + Ok(Self::new_memory(day)) + } + } + + /// Create a new in memory consumption history + fn new_memory(day: u64) -> Self { + Self { + day, + buff: vec![0; 3600 * 24 / TIME_INTERVAL], + } + } + + /// Resolve time offset of a given time in buffer + fn resolve_offset(&self, time: u64) -> anyhow::Result { + let start_of_day = self.day * 3600 * 24; + + if time < start_of_day || time >= start_of_day + 3600 * 24 { + return Err(ConsumptionHistoryError::TimeOutOfFileBound.into()); + } + + let relative_time = (time - start_of_day) / TIME_INTERVAL as u64; + + Ok(relative_time as usize) + } + + /// Check if a time is contained in this history + pub fn contains_time(&self, time: u64) -> bool { + self.resolve_offset(time).is_ok() + } + + /// Set new state of relay + pub fn set_consumption( + &mut self, + time: u64, + consumption: EnergyConsumption, + ) -> anyhow::Result<()> { + let idx = self.resolve_offset(time)?; + self.buff[idx] = consumption; + Ok(()) + } + + /// Get the consumption recorded at a given time + pub fn get_consumption(&self, time: u64) -> anyhow::Result { + let idx = self.resolve_offset(time)?; + + Ok(self.buff[idx]) + } + + /// Persist device relay state history + pub fn save(&self) -> anyhow::Result<()> { + let path = AppConfig::get().energy_consumption_history_day(self.day); + std::fs::write( + path, + bincode::encode_to_vec(&self.buff, bincode::config::standard())?, + )?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + 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); + + for i in 0..50 { + assert_eq!( + history.get_consumption(i * TIME_INTERVAL as u64).unwrap(), + 0 + ); + } + + for i in 0..50 { + history + .set_consumption(i * TIME_INTERVAL as u64, i as EnergyConsumption * 2) + .unwrap(); + } + + for i in 0..50 { + assert_eq!( + history.get_consumption(i * TIME_INTERVAL as u64).unwrap(), + i as EnergyConsumption * 2 + ); + } + } +} diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 57f9594..b580f8f 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -7,6 +7,7 @@ use crate::devices::devices_list::DevicesList; 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::utils::time_utils::time_secs; use actix::prelude::*; @@ -42,17 +43,19 @@ impl EnergyActor { async fn refresh(&mut self) -> anyhow::Result<()> { // Refresh energy - self.consumption_cache - .add_value( - consumption::get_curr_consumption() - .await - .unwrap_or_else(|e| { - log::error!( + let latest_consumption = consumption::get_curr_consumption() + .await + .unwrap_or_else(|e| { + log::error!( "Failed to fetch latest consumption value, will use fallback value! {e}" ); - constants::FALLBACK_PRODUCTION_VALUE - }), - ); + constants::FALLBACK_PRODUCTION_VALUE + }); + self.consumption_cache.add_value(latest_consumption); + + let mut history = ConsumptionHistoryFile::open(time_secs())?; + history.set_consumption(time_secs(), latest_consumption)?; + history.save()?; if self.last_engine_refresh + AppConfig::get().refresh_interval > time_secs() { return Ok(()); diff --git a/central_backend/src/energy/mod.rs b/central_backend/src/energy/mod.rs index 5ef5270..4c09ee7 100644 --- a/central_backend/src/energy/mod.rs +++ b/central_backend/src/energy/mod.rs @@ -1,5 +1,6 @@ pub mod consumption; pub mod consumption_cache; +pub mod consumption_history_file; pub mod energy_actor; pub mod engine; pub mod relay_state_history; diff --git a/central_backend/src/energy/relay_state_history.rs b/central_backend/src/energy/relay_state_history.rs index 0eafaf4..c46a179 100644 --- a/central_backend/src/energy/relay_state_history.rs +++ b/central_backend/src/energy/relay_state_history.rs @@ -47,7 +47,7 @@ impl RelayStateHistory { Self { id, day, - buff: vec![0; 3600 * 24 / TIME_INTERVAL], + buff: vec![0; (3600 * 24 / (TIME_INTERVAL * 8)) + 1], } } diff --git a/central_backend/src/main.rs b/central_backend/src/main.rs index f997c65..f910784 100644 --- a/central_backend/src/main.rs +++ b/central_backend/src/main.rs @@ -18,6 +18,7 @@ async fn main() -> std::io::Result<()> { create_directory_if_missing(AppConfig::get().pki_path()).unwrap(); create_directory_if_missing(AppConfig::get().devices_config_path()).unwrap(); create_directory_if_missing(AppConfig::get().relays_runtime_stats_storage_path()).unwrap(); + create_directory_if_missing(AppConfig::get().energy_consumption_history()).unwrap(); // Initialize PKI pki::initialize_root_ca().expect("Failed to initialize Root CA!"); From d0a80c79607d6953f5c1b4816fba468f2b960de7 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 23 Sep 2024 21:49:45 +0200 Subject: [PATCH 04/14] Prevent potential value overflow --- central_backend/src/energy/consumption_history_file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs index 5b9a43d..0ffa290 100644 --- a/central_backend/src/energy/consumption_history_file.rs +++ b/central_backend/src/energy/consumption_history_file.rs @@ -45,7 +45,7 @@ impl ConsumptionHistoryFile { fn new_memory(day: u64) -> Self { Self { day, - buff: vec![0; 3600 * 24 / TIME_INTERVAL], + buff: vec![0; (3600 * 24 / TIME_INTERVAL) + 1], } } From 78ace02d1514c10e54bd4355728cec6eac323563 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 25 Sep 2024 19:05:54 +0200 Subject: [PATCH 05/14] Store relay consumption values --- central_backend/src/app_config.rs | 20 +++++++++++++++++-- .../src/energy/consumption_history_file.rs | 15 ++++++++------ central_backend/src/energy/energy_actor.rs | 17 ++++++++++++---- central_backend/src/energy/engine.rs | 6 +++++- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/central_backend/src/app_config.rs b/central_backend/src/app_config.rs index c03cd63..09a7130 100644 --- a/central_backend/src/app_config.rs +++ b/central_backend/src/app_config.rs @@ -2,6 +2,12 @@ use crate::devices::device::{DeviceId, DeviceRelayID}; use clap::{Parser, Subcommand}; use std::path::{Path, PathBuf}; +#[derive(Copy, Clone, Debug)] +pub enum ConsumptionHistoryType { + GridConsumption, + RelayConsumption, +} + /// Electrical consumption fetcher backend #[derive(Subcommand, Debug, Clone)] pub enum ConsumptionBackend { @@ -262,10 +268,20 @@ impl AppConfig { } /// Get energy consumption history file path for a given day - pub fn energy_consumption_history_day(&self, number: u64) -> PathBuf { + pub fn energy_consumption_history_day( + &self, + number: u64, + r#type: ConsumptionHistoryType, + ) -> PathBuf { self.storage_path() .join("consumption_history") - .join(number.to_string()) + .join(format!( + "{number}-{}", + match r#type { + ConsumptionHistoryType::GridConsumption => "grid", + ConsumptionHistoryType::RelayConsumption => "relay-consumption", + } + )) } } diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs index 0ffa290..d4dc96b 100644 --- a/central_backend/src/energy/consumption_history_file.rs +++ b/central_backend/src/energy/consumption_history_file.rs @@ -1,4 +1,4 @@ -use crate::app_config::AppConfig; +use crate::app_config::{AppConfig, ConsumptionHistoryType}; use crate::energy::consumption::EnergyConsumption; use crate::utils::time_utils::day_number; @@ -16,13 +16,14 @@ pub enum ConsumptionHistoryError { pub struct ConsumptionHistoryFile { day: u64, buff: Vec, + r#type: ConsumptionHistoryType, } impl ConsumptionHistoryFile { /// Open consumption history file, if it exists, or create an empty one - pub fn open(time: u64) -> anyhow::Result { + pub fn open(time: u64, r#type: ConsumptionHistoryType) -> anyhow::Result { let day = day_number(time); - let path = AppConfig::get().energy_consumption_history_day(day); + let path = AppConfig::get().energy_consumption_history_day(day, r#type); if path.exists() { Ok(Self { @@ -32,20 +33,22 @@ impl ConsumptionHistoryFile { bincode::config::standard(), )? .0, + r#type, }) } else { log::debug!( "Energy consumption stats for day {day} does not exists yet, creating memory buffer" ); - Ok(Self::new_memory(day)) + Ok(Self::new_memory(day, r#type)) } } /// Create a new in memory consumption history - fn new_memory(day: u64) -> Self { + fn new_memory(day: u64, r#type: ConsumptionHistoryType) -> Self { Self { day, buff: vec![0; (3600 * 24 / TIME_INTERVAL) + 1], + r#type, } } @@ -87,7 +90,7 @@ impl ConsumptionHistoryFile { /// Persist device relay state history pub fn save(&self) -> anyhow::Result<()> { - let path = AppConfig::get().energy_consumption_history_day(self.day); + let path = AppConfig::get().energy_consumption_history_day(self.day, self.r#type); std::fs::write( path, bincode::encode_to_vec(&self.buff, bincode::config::standard())?, diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index b580f8f..06a7b3b 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -1,4 +1,4 @@ -use crate::app_config::AppConfig; +use crate::app_config::{AppConfig, ConsumptionHistoryType}; use crate::constants; use crate::devices::device::{ Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID, @@ -53,17 +53,26 @@ impl EnergyActor { }); self.consumption_cache.add_value(latest_consumption); - let mut history = ConsumptionHistoryFile::open(time_secs())?; + let devices_list = self.devices.full_list(); + + let mut history = + ConsumptionHistoryFile::open(time_secs(), ConsumptionHistoryType::GridConsumption)?; history.set_consumption(time_secs(), latest_consumption)?; history.save()?; + let mut relays_consumption = + ConsumptionHistoryFile::open(time_secs(), ConsumptionHistoryType::RelayConsumption)?; + relays_consumption.set_consumption( + time_secs(), + self.engine.sum_relays_consumption(&devices_list) as EnergyConsumption, + )?; + relays_consumption.save()?; + if self.last_engine_refresh + AppConfig::get().refresh_interval > time_secs() { return Ok(()); } self.last_engine_refresh = time_secs(); - let devices_list = self.devices.full_list(); - self.engine .refresh(self.consumption_cache.median_value(), &devices_list); diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index 54d3190..0d3f2dd 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -115,6 +115,10 @@ impl EnergyEngine { self.relays_state.get_mut(&relay_id).unwrap() } + pub fn sum_relays_consumption(&self, devices: &[Device]) -> usize { + sum_relays_consumption(&self.relays_state, devices) + } + fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) { log::info!("Current consumption: {curr_consumption}"); @@ -166,7 +170,7 @@ impl EnergyEngine { curr_consumption: EnergyConsumption, devices: &[Device], ) -> EnergyConsumption { - curr_consumption - sum_relays_consumption(&self.relays_state, devices) as i32 + curr_consumption - self.sum_relays_consumption(devices) as i32 } /// Refresh energy engine; this method shall never fail ! From 3c2fa18d9af482310c76cb8ae70c36085e30a3b1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 25 Sep 2024 19:35:39 +0200 Subject: [PATCH 06/14] Display relays status --- central_backend/src/energy/energy_actor.rs | 31 ++++++++++++++++ central_backend/src/energy/engine.rs | 4 ++ central_backend/src/server/servers.rs | 4 ++ .../src/server/web_api/relays_controller.rs | 7 ++++ central_frontend/src/api/RelayApi.ts | 26 +++++++++++++ .../src/routes/RelaysListRoute.tsx | 37 +++++++++++++++---- 6 files changed, 101 insertions(+), 8 deletions(-) diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 06a7b3b..9d3de54 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -360,3 +360,34 @@ impl Handler for EnergyActor { .collect() } } + +#[derive(serde::Serialize)] +pub struct ResRelayState { + pub id: DeviceRelayID, + on: bool, + r#for: usize, +} + +/// Get the state of all relays +#[derive(Message)] +#[rtype(result = "Vec")] +pub struct GetAllRelaysState; + +impl Handler for EnergyActor { + type Result = Vec; + + fn handle(&mut self, _msg: GetAllRelaysState, _ctx: &mut Context) -> Self::Result { + let mut list = vec![]; + + for d in &self.devices.relays_list() { + let state = self.engine.relay_state(d.id); + list.push(ResRelayState { + id: d.id, + on: state.is_on(), + r#for: state.state_for(), + }) + } + + list + } +} diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index 0d3f2dd..46bcd94 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -39,6 +39,10 @@ impl RelayState { fn is_off(&self) -> bool { !self.on } + + pub fn state_for(&self) -> usize { + (time_secs() - self.since as u64) as usize + } } type RelaysState = HashMap; diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 2e331f8..4bb4f51 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -185,6 +185,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/relay/{id}", web::delete().to(relays_controller::delete), ) + .route( + "/web_api/relays/status", + web::get().to(relays_controller::get_status_all), + ) // 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 3ff5dfc..909d570 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -93,3 +93,10 @@ pub async fn delete(actor: WebEnergyActor, path: web::Path) -> Ht Ok(HttpResponse::Accepted().finish()) } + +/// Get the status of all relays +pub async fn get_status_all(actor: WebEnergyActor) -> HttpResult { + let list = actor.send(energy_actor::GetAllRelaysState).await?; + + Ok(HttpResponse::Ok().json(list)) +} diff --git a/central_frontend/src/api/RelayApi.ts b/central_frontend/src/api/RelayApi.ts index 7d6a8db..6e92d2a 100644 --- a/central_frontend/src/api/RelayApi.ts +++ b/central_frontend/src/api/RelayApi.ts @@ -1,6 +1,14 @@ import { APIClient } from "./ApiClient"; import { Device, DeviceRelay } from "./DeviceApi"; +export interface RelayStatus { + id: string; + on: boolean; + for: number; +} + +export type RelaysStatus = Map; + export class RelayApi { /** * Get the full list of relays @@ -49,4 +57,22 @@ export class RelayApi { uri: `/relay/${relay.id}`, }); } + + /** + * Get the status of all relays + */ + static async GetRelaysStatus(): Promise { + const data: any[] = ( + await APIClient.exec({ + method: "GET", + uri: `/relays/status`, + }) + ).data; + + const map = new Map(); + for (let r of data) { + map.set(r.id, r); + } + return map; + } } diff --git a/central_frontend/src/routes/RelaysListRoute.tsx b/central_frontend/src/routes/RelaysListRoute.tsx index 1c297bf..8a43e90 100644 --- a/central_frontend/src/routes/RelaysListRoute.tsx +++ b/central_frontend/src/routes/RelaysListRoute.tsx @@ -12,17 +12,20 @@ import { } from "@mui/material"; import React from "react"; import { DeviceRelay } from "../api/DeviceApi"; -import { RelayApi } from "../api/RelayApi"; +import { RelayApi, RelaysStatus } from "../api/RelayApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; +import { TimeWidget } from "../widgets/TimeWidget"; export function RelaysListRoute(): React.ReactElement { const loadKey = React.useRef(1); const [list, setList] = React.useState(); + const [status, setStatus] = React.useState(); const load = async () => { setList(await RelayApi.GetList()); + setStatus(await RelayApi.GetRelaysStatus()); list?.sort((a, b) => b.priority - a.priority); }; @@ -48,7 +51,9 @@ export function RelaysListRoute(): React.ReactElement { ready={!!list} errMsg="Failed to load the list of relays!" load={load} - build={() => } + build={() => ( + + )} /> ); @@ -56,6 +61,7 @@ export function RelaysListRoute(): React.ReactElement { function RelaysList(p: { list: DeviceRelay[]; + status: RelaysStatus; onReload: () => void; }): React.ReactElement { return ( @@ -78,15 +84,18 @@ function RelaysList(p: { > {row.name} - {row.enabled ? ( - YES - ) : ( - NO - )} + {row.priority} {row.consumption} - TODO + + {" "} + for + ))} @@ -94,3 +103,15 @@ function RelaysList(p: { ); } + +function BoolText(p: { + val: boolean; + positive: string; + negative: string; +}): React.ReactElement { + return p.val ? ( + {p.positive} + ) : ( + {p.negative} + ); +} From e0f0067e8924b20ebf250792c24c2775b2a3d503 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 25 Sep 2024 21:56:54 +0200 Subject: [PATCH 07/14] Get state of relay on device page --- central_backend/src/server/servers.rs | 6 +++- .../src/server/web_api/relays_controller.rs | 12 ++++++- central_frontend/src/api/RelayApi.ts | 12 +++++++ .../src/routes/DeviceRoute/DeviceRelays.tsx | 32 +++++++++++++++++-- .../src/routes/RelaysListRoute.tsx | 13 +------- central_frontend/src/widgets/BoolText.tsx | 11 +++++++ 6 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 central_frontend/src/widgets/BoolText.tsx diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 4bb4f51..f6d67be 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -187,7 +187,11 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> ) .route( "/web_api/relays/status", - web::get().to(relays_controller::get_status_all), + web::get().to(relays_controller::status_all), + ) + .route( + "/web_api/relay/{id}/status", + web::get().to(relays_controller::status_single), ) // Devices API .route( diff --git a/central_backend/src/server/web_api/relays_controller.rs b/central_backend/src/server/web_api/relays_controller.rs index 909d570..6405aeb 100644 --- a/central_backend/src/server/web_api/relays_controller.rs +++ b/central_backend/src/server/web_api/relays_controller.rs @@ -95,8 +95,18 @@ pub async fn delete(actor: WebEnergyActor, path: web::Path) -> Ht } /// Get the status of all relays -pub async fn get_status_all(actor: WebEnergyActor) -> HttpResult { +pub async fn status_all(actor: WebEnergyActor) -> HttpResult { let list = actor.send(energy_actor::GetAllRelaysState).await?; Ok(HttpResponse::Ok().json(list)) } + +/// Get the state of a single relay +pub async fn status_single(actor: WebEnergyActor, path: web::Path) -> HttpResult { + let list = actor.send(energy_actor::GetAllRelaysState).await?; + let Some(state) = list.into_iter().find(|r| r.id == path.id) else { + return Ok(HttpResponse::NotFound().json("Relay not found!")); + }; + + Ok(HttpResponse::Ok().json(state)) +} diff --git a/central_frontend/src/api/RelayApi.ts b/central_frontend/src/api/RelayApi.ts index 6e92d2a..53d68a4 100644 --- a/central_frontend/src/api/RelayApi.ts +++ b/central_frontend/src/api/RelayApi.ts @@ -75,4 +75,16 @@ export class RelayApi { } return map; } + + /** + * Get the status of a single relay + */ + static async SingleStatus(relay: DeviceRelay): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/relay/${relay.id}/state`, + }) + ).data; + } } diff --git a/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx b/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx index a0810ad..8ea8ff0 100644 --- a/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx +++ b/central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx @@ -14,9 +14,12 @@ import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog"; import { DeviceRouteCard } from "./DeviceRouteCard"; import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider"; -import { RelayApi } from "../../api/RelayApi"; +import { RelayApi, RelayStatus } from "../../api/RelayApi"; import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; +import { AsyncWidget } from "../../widgets/AsyncWidget"; +import { TimeWidget } from "../../widgets/TimeWidget"; +import { BoolText } from "../../widgets/BoolText"; export function DeviceRelays(p: { device: Device; @@ -115,10 +118,35 @@ export function DeviceRelays(p: { } > - + } + /> ))} ); } + +function RelayEntryStatus(p: { relay: DeviceRelay }): React.ReactElement { + const [state, setState] = React.useState(); + + const load = async () => { + setState(await RelayApi.SingleStatus(p.relay)); + }; + + return ( + ( + <> + for{" "} + + + )} + /> + ); +} diff --git a/central_frontend/src/routes/RelaysListRoute.tsx b/central_frontend/src/routes/RelaysListRoute.tsx index 8a43e90..70317bd 100644 --- a/central_frontend/src/routes/RelaysListRoute.tsx +++ b/central_frontend/src/routes/RelaysListRoute.tsx @@ -14,6 +14,7 @@ import React from "react"; import { DeviceRelay } from "../api/DeviceApi"; import { RelayApi, RelaysStatus } from "../api/RelayApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; +import { BoolText } from "../widgets/BoolText"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; @@ -103,15 +104,3 @@ function RelaysList(p: { ); } - -function BoolText(p: { - val: boolean; - positive: string; - negative: string; -}): React.ReactElement { - return p.val ? ( - {p.positive} - ) : ( - {p.negative} - ); -} diff --git a/central_frontend/src/widgets/BoolText.tsx b/central_frontend/src/widgets/BoolText.tsx new file mode 100644 index 0000000..52eabbd --- /dev/null +++ b/central_frontend/src/widgets/BoolText.tsx @@ -0,0 +1,11 @@ +export function BoolText(p: { + val: boolean; + positive: string; + negative: string; +}): React.ReactElement { + return p.val ? ( + {p.positive} + ) : ( + {p.negative} + ); +} From 821b4644a2a1c4d4a0118009d552e69f534ad999 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 25 Sep 2024 22:00:36 +0200 Subject: [PATCH 08/14] Minor refacto --- .../src/routes/DeviceRoute/DeviceInfoProperty.tsx | 2 +- .../src/routes/DeviceRoute/DeviceStateBlock.tsx | 15 +++++++++++---- .../src/routes/DeviceRoute/GeneralDeviceInfo.tsx | 6 ++++-- central_frontend/src/routes/DevicesRoute.tsx | 11 ++++++----- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/central_frontend/src/routes/DeviceRoute/DeviceInfoProperty.tsx b/central_frontend/src/routes/DeviceRoute/DeviceInfoProperty.tsx index cffd8a7..0714f1f 100644 --- a/central_frontend/src/routes/DeviceRoute/DeviceInfoProperty.tsx +++ b/central_frontend/src/routes/DeviceRoute/DeviceInfoProperty.tsx @@ -3,7 +3,7 @@ import { TableCell, TableRow } from "@mui/material"; export function DeviceInfoProperty(p: { icon?: React.ReactElement; label: string; - value: string; + value: string | React.ReactElement; color?: string; }): React.ReactElement { return ( diff --git a/central_frontend/src/routes/DeviceRoute/DeviceStateBlock.tsx b/central_frontend/src/routes/DeviceRoute/DeviceStateBlock.tsx index 726c0e3..2180558 100644 --- a/central_frontend/src/routes/DeviceRoute/DeviceStateBlock.tsx +++ b/central_frontend/src/routes/DeviceRoute/DeviceStateBlock.tsx @@ -1,10 +1,11 @@ +import { Table, TableBody } from "@mui/material"; import React from "react"; import { Device, DeviceApi, DeviceState } from "../../api/DeviceApi"; import { AsyncWidget } from "../../widgets/AsyncWidget"; -import { DeviceRouteCard } from "./DeviceRouteCard"; -import { Table, TableBody } from "@mui/material"; -import { DeviceInfoProperty } from "./DeviceInfoProperty"; +import { BoolText } from "../../widgets/BoolText"; import { timeDiff } from "../../widgets/TimeWidget"; +import { DeviceInfoProperty } from "./DeviceInfoProperty"; +import { DeviceRouteCard } from "./DeviceRouteCard"; export function DeviceStateBlock(p: { device: Device }): React.ReactElement { const [state, setState] = React.useState(); @@ -32,7 +33,13 @@ function DeviceStateInner(p: { state: DeviceState }): React.ReactElement { + } /> + } /> - {p.states.get(dev.id)!.online ? ( - Online - ) : ( - Offline - )} +
From 2e72634abff3a39d00189b99111ed11d010ae289 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 25 Sep 2024 22:31:00 +0200 Subject: [PATCH 09/14] Can request consumption history --- .../src/energy/consumption_history_file.rs | 24 +++++++++++++++++++ central_backend/src/server/servers.rs | 8 +++++++ .../src/server/web_api/energy_controller.rs | 24 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs index d4dc96b..f4f8757 100644 --- a/central_backend/src/energy/consumption_history_file.rs +++ b/central_backend/src/energy/consumption_history_file.rs @@ -97,6 +97,30 @@ impl ConsumptionHistoryFile { )?; Ok(()) } + + /// Get the total runtime of a relay during a given time window + pub fn get_history( + r#type: ConsumptionHistoryType, + from: u64, + to: u64, + interval: u64, + ) -> anyhow::Result> { + let mut res = Vec::with_capacity(((to - from) / interval) as usize); + let mut file = Self::open(from, r#type)?; + let mut curr_time = from; + + while curr_time < to { + if !file.contains_time(curr_time) { + file = Self::open(curr_time, r#type)?; + } + + res.push(file.get_consumption(curr_time)?); + + curr_time += interval; + } + + Ok(res) + } } #[cfg(test)] diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index f6d67be..1dbced0 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -131,10 +131,18 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/energy/curr_consumption", web::get().to(energy_controller::curr_consumption), ) + .route( + "/web_api/energy/curr_consumption/history", + web::get().to(energy_controller::curr_consumption_history), + ) .route( "/web_api/energy/cached_consumption", web::get().to(energy_controller::cached_consumption), ) + .route( + "/web_api/energy/relays_consumption/history", + web::get().to(energy_controller::relays_consumption_history), + ) // Devices controller .route( "/web_api/devices/list_pending", diff --git a/central_backend/src/server/web_api/energy_controller.rs b/central_backend/src/server/web_api/energy_controller.rs index 2ab729e..4147321 100644 --- a/central_backend/src/server/web_api/energy_controller.rs +++ b/central_backend/src/server/web_api/energy_controller.rs @@ -1,6 +1,9 @@ +use crate::app_config::ConsumptionHistoryType; +use crate::energy::consumption_history_file::ConsumptionHistoryFile; use crate::energy::{consumption, energy_actor}; use crate::server::custom_error::HttpResult; use crate::server::WebEnergyActor; +use crate::utils::time_utils::time_secs; use actix_web::HttpResponse; #[derive(serde::Serialize)] @@ -15,9 +18,30 @@ pub async fn curr_consumption() -> HttpResult { Ok(HttpResponse::Ok().json(Consumption { consumption })) } +/// Get curr consumption history +pub async fn curr_consumption_history() -> HttpResult { + let history = ConsumptionHistoryFile::get_history( + ConsumptionHistoryType::GridConsumption, + time_secs() - 3600 * 24, + time_secs(), + 60 * 10, + )?; + Ok(HttpResponse::Ok().json(history)) +} + /// Get cached energy consumption pub async fn cached_consumption(energy_actor: WebEnergyActor) -> HttpResult { let consumption = energy_actor.send(energy_actor::GetCurrConsumption).await?; Ok(HttpResponse::Ok().json(Consumption { consumption })) } + +pub async fn relays_consumption_history() -> HttpResult { + let history = ConsumptionHistoryFile::get_history( + ConsumptionHistoryType::RelayConsumption, + time_secs() - 3600 * 24, + time_secs(), + 60 * 10, + )?; + Ok(HttpResponse::Ok().json(history)) +} From 903f1fa8ce37de5d07921dddaaf09648b02019a6 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 26 Sep 2024 22:37:43 +0200 Subject: [PATCH 10/14] Show current consumption chart --- central_frontend/src/api/EnergyApi.ts | 28 ++++++- .../HomeRoute/CachedConsumptionWidget.tsx | 8 +- .../HomeRoute/CurrConsumptionWidget.tsx | 11 +-- central_frontend/src/widgets/StatCard.tsx | 81 ++++++++++--------- 4 files changed, 77 insertions(+), 51 deletions(-) diff --git a/central_frontend/src/api/EnergyApi.ts b/central_frontend/src/api/EnergyApi.ts index a6d484d..f202348 100644 --- a/central_frontend/src/api/EnergyApi.ts +++ b/central_frontend/src/api/EnergyApi.ts @@ -2,9 +2,9 @@ import { APIClient } from "./ApiClient"; export class EnergyApi { /** - * Get current house consumption + * Get current grid consumption */ - static async CurrConsumption(): Promise { + static async GridConsumption(): Promise { const data = await APIClient.exec({ method: "GET", uri: "/energy/curr_consumption", @@ -12,6 +12,18 @@ export class EnergyApi { return data.data.consumption; } + /** + * Get grid consumption history + */ + static async GridConsumptionHistory(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/energy/curr_consumption/history", + }) + ).data; + } + /** * Get current cached consumption */ @@ -22,4 +34,16 @@ export class EnergyApi { }); return data.data.consumption; } + + /** + * Get relays consumption history + */ + static async RelaysConsumptionHistory(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/energy/relays_consumption/history", + }) + ).data; + } } diff --git a/central_frontend/src/routes/HomeRoute/CachedConsumptionWidget.tsx b/central_frontend/src/routes/HomeRoute/CachedConsumptionWidget.tsx index 53c399b..bb5d97d 100644 --- a/central_frontend/src/routes/HomeRoute/CachedConsumptionWidget.tsx +++ b/central_frontend/src/routes/HomeRoute/CachedConsumptionWidget.tsx @@ -26,12 +26,6 @@ export function CachedConsumptionWidget(): React.ReactElement { }); return ( - + ); } diff --git a/central_frontend/src/routes/HomeRoute/CurrConsumptionWidget.tsx b/central_frontend/src/routes/HomeRoute/CurrConsumptionWidget.tsx index f521bca..474e269 100644 --- a/central_frontend/src/routes/HomeRoute/CurrConsumptionWidget.tsx +++ b/central_frontend/src/routes/HomeRoute/CurrConsumptionWidget.tsx @@ -7,11 +7,14 @@ export function CurrConsumptionWidget(): React.ReactElement { const snackbar = useSnackbar(); const [val, setVal] = React.useState(); + const [history, setHistory] = React.useState(); const refresh = async () => { try { - const s = await EnergyApi.CurrConsumption(); + const s = await EnergyApi.GridConsumption(); + const history = await EnergyApi.GridConsumptionHistory(); setVal(s); + setHistory(history); } catch (e) { console.error(e); snackbar("Failed to refresh current consumption!"); @@ -19,7 +22,6 @@ export function CurrConsumptionWidget(): React.ReactElement { }; React.useEffect(() => { - refresh(); const i = setInterval(() => refresh(), 3000); return () => clearInterval(i); @@ -28,9 +30,8 @@ export function CurrConsumptionWidget(): React.ReactElement { return ( ); diff --git a/central_frontend/src/widgets/StatCard.tsx b/central_frontend/src/widgets/StatCard.tsx index 54f6ed7..64872ab 100644 --- a/central_frontend/src/widgets/StatCard.tsx +++ b/central_frontend/src/widgets/StatCard.tsx @@ -11,24 +11,25 @@ import { areaElementClasses } from "@mui/x-charts/LineChart"; export type StatCardProps = { title: string; value: string; - interval: string; - trend: "up" | "down" | "neutral"; - data: number[]; + interval?: string; + trend?: "up" | "down" | "neutral"; + data?: number[]; }; -function getDaysInMonth(month: number, year: number) { - const date = new Date(year, month, 0); - const monthName = date.toLocaleDateString("en-US", { - month: "short", - }); - const daysInMonth = date.getDate(); - const days = []; - let i = 1; - while (days.length < daysInMonth) { - days.push(`${monthName} ${i}`); - i += 1; +function last24Hours(): string[] { + let res: Array = []; + + for (let index = 0; index < 3600 * 24; index += 60 * 10) { + const date = new Date(); + date.setTime(date.getTime() - index * 1000); + res.push(date.getHours() + "h" + date.getMinutes()); } - return days; + + res.reverse(); + + console.log(res); + + return res; } function AreaGradient({ color, id }: { color: string; id: string }) { @@ -50,7 +51,6 @@ export default function StatCard({ data, }: StatCardProps) { const theme = useTheme(); - const daysInWeek = getDaysInMonth(4, 2024); const trendColors = { up: @@ -73,8 +73,8 @@ export default function StatCard({ neutral: "default" as const, }; - const color = labelColors[trend]; - const chartColor = trendColors[trend]; + const color = labelColors[trend ?? "neutral"]; + const chartColor = trendColors[trend ?? "neutral"]; const trendValues = { up: "+25%", down: "-25%", neutral: "+5%" }; return ( @@ -95,31 +95,38 @@ export default function StatCard({ {value} - + {trend && ( + + )} {interval} - - - + {data && interval && ( + + + + )} From 7895b9eca8dcc523f1df6cfcb12e679b9bb79164 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 26 Sep 2024 22:51:43 +0200 Subject: [PATCH 11/14] Display relay consumption history --- central_backend/src/devices/devices_list.rs | 5 +++ central_backend/src/energy/energy_actor.rs | 18 ++++++++- central_backend/src/energy/engine.rs | 16 ++++---- central_backend/src/server/servers.rs | 4 ++ .../src/server/web_api/energy_controller.rs | 9 +++++ central_frontend/src/api/EnergyApi.ts | 12 ++++++ central_frontend/src/api/RelayApi.ts | 2 +- central_frontend/src/routes/HomeRoute.tsx | 4 ++ .../HomeRoute/RelayConsumptionWidget.tsx | 38 +++++++++++++++++++ central_frontend/src/widgets/TimeWidget.tsx | 5 ++- 10 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 central_frontend/src/routes/HomeRoute/RelayConsumptionWidget.tsx diff --git a/central_backend/src/devices/devices_list.rs b/central_backend/src/devices/devices_list.rs index 0f5763e..9f196de 100644 --- a/central_backend/src/devices/devices_list.rs +++ b/central_backend/src/devices/devices_list.rs @@ -115,6 +115,11 @@ impl DevicesList { self.0.clone().into_values().collect() } + /// Get a reference on the full list of devices + pub fn full_list_ref(&self) -> Vec<&Device> { + self.0.values().collect() + } + /// Get the information about a single device pub fn get_single(&self, id: &DeviceId) -> Option { self.0.get(id).cloned() diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index 9d3de54..35426be 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -53,7 +53,7 @@ impl EnergyActor { }); self.consumption_cache.add_value(latest_consumption); - let devices_list = self.devices.full_list(); + let devices_list = self.devices.full_list_ref(); let mut history = ConsumptionHistoryFile::open(time_secs(), ConsumptionHistoryType::GridConsumption)?; @@ -119,7 +119,21 @@ impl Handler for EnergyActor { } } -/// Get current consumption +/// Get relays consumption +#[derive(Message)] +#[rtype(result = "usize")] +pub struct RelaysConsumption; + +impl Handler for EnergyActor { + type Result = usize; + + fn handle(&mut self, _msg: RelaysConsumption, _ctx: &mut Context) -> Self::Result { + self.engine + .sum_relays_consumption(&self.devices.full_list_ref()) + } +} + +/// Check if device exists #[derive(Message)] #[rtype(result = "bool")] pub struct CheckDeviceExists(pub DeviceId); diff --git a/central_backend/src/energy/engine.rs b/central_backend/src/energy/engine.rs index 46bcd94..1f8c236 100644 --- a/central_backend/src/energy/engine.rs +++ b/central_backend/src/energy/engine.rs @@ -55,7 +55,7 @@ pub struct EnergyEngine { impl DeviceRelay { // Note : this function is not recursive - fn has_running_dependencies(&self, s: &RelaysState, devices: &[Device]) -> bool { + fn has_running_dependencies(&self, s: &RelaysState, devices: &[&Device]) -> bool { for d in devices { for r in &d.relays { if r.depends_on.contains(&self.id) && s.get(&r.id).unwrap().is_on() { @@ -72,7 +72,7 @@ impl DeviceRelay { self.depends_on.iter().any(|id| s.get(id).unwrap().is_off()) } - fn is_having_conflict(&self, s: &RelaysState, devices: &[Device]) -> bool { + fn is_having_conflict(&self, s: &RelaysState, devices: &[&Device]) -> bool { if self .conflicts_with .iter() @@ -94,7 +94,7 @@ impl DeviceRelay { } } -fn sum_relays_consumption(state: &RelaysState, devices: &[Device]) -> usize { +fn sum_relays_consumption(state: &RelaysState, devices: &[&Device]) -> usize { let mut consumption = 0; for d in devices { @@ -119,11 +119,11 @@ impl EnergyEngine { self.relays_state.get_mut(&relay_id).unwrap() } - pub fn sum_relays_consumption(&self, devices: &[Device]) -> usize { + pub fn sum_relays_consumption(&self, devices: &[&Device]) -> usize { sum_relays_consumption(&self.relays_state, devices) } - fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) { + fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) { log::info!("Current consumption: {curr_consumption}"); let mut table = Table::new(); @@ -172,13 +172,13 @@ impl EnergyEngine { pub fn estimated_consumption_without_relays( &self, curr_consumption: EnergyConsumption, - devices: &[Device], + devices: &[&Device], ) -> EnergyConsumption { curr_consumption - self.sum_relays_consumption(devices) as i32 } /// Refresh energy engine; this method shall never fail ! - pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) { + pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) { let base_production = self.estimated_consumption_without_relays(curr_consumption, devices); log::info!("Estimated base production: {base_production}"); @@ -366,7 +366,7 @@ impl EnergyEngine { } /// Save relays state to disk - pub fn persist_relays_state(&mut self, devices: &[Device]) -> anyhow::Result<()> { + pub fn persist_relays_state(&mut self, devices: &[&Device]) -> anyhow::Result<()> { // Save all relays state for d in devices { for r in &d.relays { diff --git a/central_backend/src/server/servers.rs b/central_backend/src/server/servers.rs index 1dbced0..c0325f3 100644 --- a/central_backend/src/server/servers.rs +++ b/central_backend/src/server/servers.rs @@ -139,6 +139,10 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> "/web_api/energy/cached_consumption", web::get().to(energy_controller::cached_consumption), ) + .route( + "/web_api/energy/relays_consumption", + web::get().to(energy_controller::relays_consumption), + ) .route( "/web_api/energy/relays_consumption/history", web::get().to(energy_controller::relays_consumption_history), diff --git a/central_backend/src/server/web_api/energy_controller.rs b/central_backend/src/server/web_api/energy_controller.rs index 4147321..86c19ed 100644 --- a/central_backend/src/server/web_api/energy_controller.rs +++ b/central_backend/src/server/web_api/energy_controller.rs @@ -1,4 +1,5 @@ use crate::app_config::ConsumptionHistoryType; +use crate::energy::consumption::EnergyConsumption; use crate::energy::consumption_history_file::ConsumptionHistoryFile; use crate::energy::{consumption, energy_actor}; use crate::server::custom_error::HttpResult; @@ -36,6 +37,14 @@ pub async fn cached_consumption(energy_actor: WebEnergyActor) -> HttpResult { Ok(HttpResponse::Ok().json(Consumption { consumption })) } +/// Get current relays consumption +pub async fn relays_consumption(energy_actor: WebEnergyActor) -> HttpResult { + let consumption = + energy_actor.send(energy_actor::RelaysConsumption).await? as EnergyConsumption; + + Ok(HttpResponse::Ok().json(Consumption { consumption })) +} + pub async fn relays_consumption_history() -> HttpResult { let history = ConsumptionHistoryFile::get_history( ConsumptionHistoryType::RelayConsumption, diff --git a/central_frontend/src/api/EnergyApi.ts b/central_frontend/src/api/EnergyApi.ts index f202348..bd5640c 100644 --- a/central_frontend/src/api/EnergyApi.ts +++ b/central_frontend/src/api/EnergyApi.ts @@ -35,6 +35,18 @@ export class EnergyApi { return data.data.consumption; } + /** + * Get relays consumption + */ + static async RelaysConsumption(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/energy/relays_consumption", + }) + ).data.consumption; + } + /** * Get relays consumption history */ diff --git a/central_frontend/src/api/RelayApi.ts b/central_frontend/src/api/RelayApi.ts index 53d68a4..0620541 100644 --- a/central_frontend/src/api/RelayApi.ts +++ b/central_frontend/src/api/RelayApi.ts @@ -83,7 +83,7 @@ export class RelayApi { return ( await APIClient.exec({ method: "GET", - uri: `/relay/${relay.id}/state`, + uri: `/relay/${relay.id}/status`, }) ).data; } diff --git a/central_frontend/src/routes/HomeRoute.tsx b/central_frontend/src/routes/HomeRoute.tsx index a01d284..40b97f0 100644 --- a/central_frontend/src/routes/HomeRoute.tsx +++ b/central_frontend/src/routes/HomeRoute.tsx @@ -2,6 +2,7 @@ import { Typography } from "@mui/material"; import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget"; import Grid from "@mui/material/Grid2"; import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget"; +import { RelayConsumptionWidget } from "./HomeRoute/RelayConsumptionWidget"; export function HomeRoute(): React.ReactElement { return ( @@ -18,6 +19,9 @@ export function HomeRoute(): React.ReactElement { + + + diff --git a/central_frontend/src/routes/HomeRoute/RelayConsumptionWidget.tsx b/central_frontend/src/routes/HomeRoute/RelayConsumptionWidget.tsx new file mode 100644 index 0000000..8004fb1 --- /dev/null +++ b/central_frontend/src/routes/HomeRoute/RelayConsumptionWidget.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { EnergyApi } from "../../api/EnergyApi"; +import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; +import StatCard from "../../widgets/StatCard"; + +export function RelayConsumptionWidget(): React.ReactElement { + const snackbar = useSnackbar(); + + const [val, setVal] = React.useState(); + const [history, setHistory] = React.useState(); + + const refresh = async () => { + try { + const s = await EnergyApi.RelaysConsumption(); + const history = await EnergyApi.RelaysConsumptionHistory(); + setVal(s); + setHistory(history); + } catch (e) { + console.error(e); + snackbar("Failed to refresh current relays consumption!"); + } + }; + + React.useEffect(() => { + const i = setInterval(() => refresh(), 3000); + + return () => clearInterval(i); + }); + + return ( + + ); +} diff --git a/central_frontend/src/widgets/TimeWidget.tsx b/central_frontend/src/widgets/TimeWidget.tsx index 9e0548c..f590a48 100644 --- a/central_frontend/src/widgets/TimeWidget.tsx +++ b/central_frontend/src/widgets/TimeWidget.tsx @@ -61,7 +61,10 @@ export function TimeWidget(p: { }): React.ReactElement { if (!p.time) return <>; return ( - + {p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time)} ); From 3f41269c0ba31ac16fc9e66f588b3bf5283c204b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 26 Sep 2024 23:05:10 +0200 Subject: [PATCH 12/14] Use medians --- central_backend/src/energy/consumption_cache.rs | 9 ++------- .../src/energy/consumption_history_file.rs | 11 +++++++++-- central_backend/src/utils/math_utils.rs | 8 ++++++++ central_backend/src/utils/mod.rs | 1 + 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 central_backend/src/utils/math_utils.rs diff --git a/central_backend/src/energy/consumption_cache.rs b/central_backend/src/energy/consumption_cache.rs index f9beefe..c702dc7 100644 --- a/central_backend/src/energy/consumption_cache.rs +++ b/central_backend/src/energy/consumption_cache.rs @@ -1,5 +1,6 @@ use crate::constants; use crate::energy::consumption::EnergyConsumption; +use crate::utils::math_utils::median; pub struct ConsumptionCache { nb_vals: usize, @@ -27,13 +28,7 @@ impl ConsumptionCache { return constants::FALLBACK_PRODUCTION_VALUE; } - let mut clone = self.values.clone(); - clone.sort(); - let median = clone[clone.len() / 2]; - - log::info!("Cached consumption: {:?} / Median: {}", self.values, median); - - median + median(&self.values) } } diff --git a/central_backend/src/energy/consumption_history_file.rs b/central_backend/src/energy/consumption_history_file.rs index f4f8757..513c7e8 100644 --- a/central_backend/src/energy/consumption_history_file.rs +++ b/central_backend/src/energy/consumption_history_file.rs @@ -1,5 +1,6 @@ use crate::app_config::{AppConfig, ConsumptionHistoryType}; use crate::energy::consumption::EnergyConsumption; +use crate::utils::math_utils::median; use crate::utils::time_utils::day_number; const TIME_INTERVAL: usize = 10; @@ -109,14 +110,20 @@ impl ConsumptionHistoryFile { let mut file = Self::open(from, r#type)?; let mut curr_time = from; + let mut intermediate_values = Vec::new(); while curr_time < to { if !file.contains_time(curr_time) { file = Self::open(curr_time, r#type)?; } - res.push(file.get_consumption(curr_time)?); + intermediate_values.push(file.get_consumption(curr_time)?); - curr_time += interval; + if curr_time % interval == from % interval { + res.push(median(&intermediate_values)); + intermediate_values = Vec::new(); + } + + curr_time += TIME_INTERVAL as u64; } Ok(res) diff --git a/central_backend/src/utils/math_utils.rs b/central_backend/src/utils/math_utils.rs new file mode 100644 index 0000000..4421ff8 --- /dev/null +++ b/central_backend/src/utils/math_utils.rs @@ -0,0 +1,8 @@ +use std::ops::Div; + +pub fn median(numbers: &[E]) -> E { + let mut numbers = numbers.to_vec(); + numbers.sort(); + let mid = numbers.len() / 2; + numbers[mid] +} diff --git a/central_backend/src/utils/mod.rs b/central_backend/src/utils/mod.rs index 25676ba..5ccc580 100644 --- a/central_backend/src/utils/mod.rs +++ b/central_backend/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod files_utils; + pub mod math_utils; pub mod time_utils; From cb798dfd14af48a5db71fe2153095907613ed5fd Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 26 Sep 2024 23:14:18 +0200 Subject: [PATCH 13/14] Enrich home page --- central_frontend/src/routes/DevicesRoute.tsx | 25 ++++++++++--------- central_frontend/src/routes/HomeRoute.tsx | 10 ++++++++ .../src/routes/RelaysListRoute.tsx | 5 +++- .../src/widgets/SolarEnergyRouteContainer.tsx | 5 ++-- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/central_frontend/src/routes/DevicesRoute.tsx b/central_frontend/src/routes/DevicesRoute.tsx index e0ab75f..9545bf9 100644 --- a/central_frontend/src/routes/DevicesRoute.tsx +++ b/central_frontend/src/routes/DevicesRoute.tsx @@ -19,7 +19,7 @@ import { BoolText } from "../widgets/BoolText"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; -export function DevicesRoute(): React.ReactElement { +export function DevicesRoute(p: { homeWidget?: boolean }): React.ReactElement { const loadKey = React.useRef(1); const [list, setList] = React.useState(); @@ -38,6 +38,7 @@ export function DevicesRoute(): React.ReactElement { return ( @@ -81,12 +82,12 @@ function ValidatedDevicesList(p: { # - Model - Version - Max number of relays - Created - Updated - Status + Model + Version + Max relays + Created + Updated + Status @@ -100,13 +101,13 @@ function ValidatedDevicesList(p: { {dev.id} - {dev.info.reference} - {dev.info.version} - {dev.info.max_relays} - + {dev.info.reference} + {dev.info.version} + {dev.info.max_relays} + - + diff --git a/central_frontend/src/routes/HomeRoute.tsx b/central_frontend/src/routes/HomeRoute.tsx index 40b97f0..6ec346e 100644 --- a/central_frontend/src/routes/HomeRoute.tsx +++ b/central_frontend/src/routes/HomeRoute.tsx @@ -3,6 +3,8 @@ import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget"; import Grid from "@mui/material/Grid2"; import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget"; import { RelayConsumptionWidget } from "./HomeRoute/RelayConsumptionWidget"; +import { RelaysListRoute } from "./RelaysListRoute"; +import { DevicesRoute } from "./DevicesRoute"; export function HomeRoute(): React.ReactElement { return ( @@ -25,6 +27,14 @@ export function HomeRoute(): React.ReactElement { + + + + + + + + ); diff --git a/central_frontend/src/routes/RelaysListRoute.tsx b/central_frontend/src/routes/RelaysListRoute.tsx index 70317bd..bb69731 100644 --- a/central_frontend/src/routes/RelaysListRoute.tsx +++ b/central_frontend/src/routes/RelaysListRoute.tsx @@ -18,7 +18,9 @@ import { BoolText } from "../widgets/BoolText"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; -export function RelaysListRoute(): React.ReactElement { +export function RelaysListRoute(p: { + homeWidget?: boolean; +}): React.ReactElement { const loadKey = React.useRef(1); const [list, setList] = React.useState(); @@ -39,6 +41,7 @@ export function RelaysListRoute(): React.ReactElement { return ( diff --git a/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx b/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx index 9c7a706..5b87fee 100644 --- a/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx +++ b/central_frontend/src/widgets/SolarEnergyRouteContainer.tsx @@ -4,11 +4,12 @@ import React, { PropsWithChildren } from "react"; export function SolarEnergyRouteContainer( p: { label: string; + homeWidget?: boolean; actions?: React.ReactElement; } & PropsWithChildren ): React.ReactElement { return ( -
+
- {p.label} + {p.label} {p.actions ?? <>}
From 7f9db9f2cc101a5a745a7a04e305add0c5f63570 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 26 Sep 2024 23:21:56 +0200 Subject: [PATCH 14/14] Improve home page --- .../src/routes/RelaysListRoute.tsx | 67 ++++++++++++------- central_frontend/src/widgets/StatCard.tsx | 2 +- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/central_frontend/src/routes/RelaysListRoute.tsx b/central_frontend/src/routes/RelaysListRoute.tsx index bb69731..8a04555 100644 --- a/central_frontend/src/routes/RelaysListRoute.tsx +++ b/central_frontend/src/routes/RelaysListRoute.tsx @@ -11,12 +11,14 @@ import { Tooltip, } from "@mui/material"; import React from "react"; -import { DeviceRelay } from "../api/DeviceApi"; +import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi"; import { RelayApi, RelaysStatus } from "../api/RelayApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { BoolText } from "../widgets/BoolText"; import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; +import { EditDeviceRelaysDialog } from "../dialogs/EditDeviceRelaysDialog"; +import { useNavigate } from "react-router-dom"; export function RelaysListRoute(p: { homeWidget?: boolean; @@ -24,10 +26,12 @@ export function RelaysListRoute(p: { const loadKey = React.useRef(1); const [list, setList] = React.useState(); + const [devices, setDevices] = React.useState(); const [status, setStatus] = React.useState(); const load = async () => { setList(await RelayApi.GetList()); + setDevices(await DeviceApi.ValidatedList()); setStatus(await RelayApi.GetRelaysStatus()); list?.sort((a, b) => b.priority - a.priority); @@ -39,38 +43,53 @@ export function RelaysListRoute(p: { }; return ( - - - - - - } - > - ( - - )} - /> - + <> + + + + + + } + > + ( + + )} + /> + + ); } function RelaysList(p: { list: DeviceRelay[]; + devices: Device[]; status: RelaysStatus; onReload: () => void; }): React.ReactElement { + const navigate = useNavigate(); + + const openDevicePage = (relay: DeviceRelay) => { + const dev = p.devices.find((d) => d.relays.find((r) => r.id === relay.id)); + navigate(DeviceURL(dev!)); + }; + return ( - +
Name @@ -85,6 +104,8 @@ function RelaysList(p: { openDevicePage(row)} > {row.name} diff --git a/central_frontend/src/widgets/StatCard.tsx b/central_frontend/src/widgets/StatCard.tsx index 64872ab..7a8d91b 100644 --- a/central_frontend/src/widgets/StatCard.tsx +++ b/central_frontend/src/widgets/StatCard.tsx @@ -103,7 +103,7 @@ export default function StatCard({ {interval} - + {data && interval && (