Compare commits
	
		
			199 Commits
		
	
	
		
			1.0.1
			...
			074137c2f7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 074137c2f7 | |||
| f01df2818c | |||
| da60a57f53 | |||
| 0629bd60c3 | |||
| 995977fd37 | |||
| 9bf15f28b8 | |||
| 8941ec2aef | |||
| b19961ed6a | |||
| 082efa367c | |||
| 3ffcdad666 | |||
| 65db36d097 | |||
| 57bb552950 | |||
| 1d9c539cd1 | |||
| 11d718cfe8 | |||
| 0125b16177 | |||
| d97dcddb96 | |||
| 9eafbd8aeb | |||
| 6aa7fc3a75 | |||
| 345b3566ae | |||
| 22cd346330 | |||
| aee9303f91 | |||
| 6462645d26 | |||
| 4bb76777db | |||
| d79b55b86d | |||
| 665a04c8a0 | |||
| 658b10f5f8 | |||
| c0374e35b1 | |||
| 15f701668f | |||
| 8fdfa19806 | |||
| 22d84e9464 | |||
| a5c5663390 | |||
| 7878fb9686 | |||
| b24642b10d | |||
| ce45d841b2 | |||
| 1b4e5eda9d | |||
| bb1917d1b4 | |||
| b285323bd7 | |||
| ecb161ee82 | |||
| 00c6ae338b | |||
| 814046146c | |||
| f52e992d84 | |||
| dc73882347 | |||
| 5ed8c42b99 | |||
| 0fcb902e9e | |||
| cfafbda77b | |||
| dace42aef2 | |||
| 67401e8faf | |||
| 77a278bd53 | |||
| 6df43fcc0e | |||
| 8add37fc42 | |||
| bfde6531c2 | |||
| 5f6ac7bcfd | |||
| c01f1ca484 | |||
| c6975c2097 | |||
| 2d079403c5 | |||
| 2d408871ad | |||
| 22fd077380 | |||
| 0fba1caf62 | |||
| 7e99cfc086 | |||
| 511011bb4b | |||
| dfca6a04bc | |||
| 4f639522b9 | |||
| 7d9af6af64 | |||
| e1136926a1 | |||
| 4206d9529b | |||
| b606aed10e | |||
| 9a2ceb9804 | |||
| f6bd7b1061 | |||
| 34460500a0 | |||
| 72afa3df62 | |||
| e74f7d6f6d | |||
| 5b09aec93a | |||
| 9f93f76d8e | |||
| 211369a1b2 | |||
| d7c4cd6635 | |||
| 4f78e99f65 | |||
| 4309a19f24 | |||
| 67a0436d02 | |||
| 5ff169d8c2 | |||
| 541f7cbe95 | |||
| 166ac5c8c2 | |||
| 0b53037140 | |||
| 901f6b0e6f | |||
| fc5f9735bf | |||
| 094ff457ac | |||
| ffbbd14ac3 | |||
| e4447e9dcb | |||
| 973190f5b9 | |||
| ededf48977 | |||
| 9a5211812e | |||
| 6f589f3ee4 | |||
| 499c5cb81e | |||
| d72cdbf3cd | |||
| 7ab60c6fe6 | |||
| bf28d1c926 | |||
| 07ca3aa80e | |||
| 4cc18d407d | |||
| 7bcafd782f | |||
| 404fa716f5 | |||
| 43bbb444db | |||
| 71a139af59 | |||
| c40b4acf7a | |||
| 41a228484f | |||
| 9965de686d | |||
| 3cf808df1c | |||
| 40b41688e0 | |||
| 124b0b825c | |||
| 8e4bed012d | |||
| ff79fd968e | |||
| ac26065f10 | |||
| b8b172f17d | |||
| 12fe1abb0f | |||
| acfacf574b | |||
| d0e426bbbc | |||
| 92a9a5741c | |||
| 9a480dfa98 | |||
| f8eafe31bd | |||
| 4330e64489 | |||
| fd5730cfce | |||
| 76acf07b17 | |||
| 315e11a2bb | |||
| 5722ccc2c4 | |||
| 71718151d0 | |||
| 9efd8db8cf | |||
| fc0c86bf8b | |||
| 1a8c3ff9ff | |||
| 4cb05b375e | |||
| f67ccc7cda | |||
| 8a08ff53df | |||
| 2607ac7355 | |||
| e644aa1390 | |||
| e015f01539 | |||
| 37aed38174 | |||
| aa3677a787 | |||
| 549193632c | |||
| 407aeaaf6e | |||
| 36d269dde7 | |||
| 0c4f352815 | |||
| e1abc68292 | |||
| bb0226577d | |||
| 9fcd16784a | |||
| d6e0eccb00 | |||
| dc621984fb | |||
| b2878510d6 | |||
| a059076323 | |||
| f594802523 | |||
| 747d2d819b | |||
| a52868a3fb | |||
| fce38386eb | |||
| cb88a19352 | |||
| c6c34efebd | |||
| eed9637f1e | |||
| 3a64b2b09c | |||
| 8d2f0cb38a | |||
| 5b1cf61832 | |||
| 6cd5d5f93a | |||
| fc409b2584 | |||
| 5a1942cb15 | |||
| 4af8904294 | |||
| 0d1605169d | |||
| 346ea8db11 | |||
| f534a9c61b | |||
| 2ef056da30 | |||
| af16091fab | |||
| 28f81248bf | |||
| cc43f6c78b | |||
| 060ff08c1e | |||
| 57ce643163 | |||
| 7a536ac850 | |||
| 685eef5c5b | |||
| 180126d22a | |||
| e29f01bc62 | |||
| f2abdfe302 | |||
| 598286d1cb | |||
| 30f196aa7a | |||
| 08f1ec6d4d | |||
| d31a568c00 | |||
| 1b25c07e50 | |||
| 96f1640378 | |||
| ccb4ae22f8 | |||
| d66e2b9bf7 | |||
| 0660066941 | |||
| 7476924e0e | |||
| 65c3c534f4 | |||
| 995e1fa07e | |||
| 1735077db3 | |||
| 31bb956a29 | |||
| ff0e548422 | |||
| 837835da7e | |||
| 2d262bb4c9 | |||
| f594ebfbaa | |||
| 57a9c03308 | |||
| 7b9db9c7c3 | |||
| b7720df305 | |||
| 445c1b014e | |||
| aa732af571 | |||
| c365f959e7 | |||
| 9a4c6d2de2 | |||
| 3c20cca915 | 
| @@ -5,7 +5,7 @@ name: default | |||||||
|  |  | ||||||
| steps: | steps: | ||||||
|   - name: web_build |   - name: web_build | ||||||
|     image: node:21 |     image: node:23 | ||||||
|     volumes: |     volumes: | ||||||
|       - name: web_app |       - name: web_app | ||||||
|         path: /tmp/web_build |         path: /tmp/web_build | ||||||
| @@ -56,7 +56,7 @@ steps: | |||||||
|       - ls -lah target/release/central_backend |       - ls -lah target/release/central_backend | ||||||
|  |  | ||||||
|   - name: esp32_compile |   - name: esp32_compile | ||||||
|     image: espressif/idf:v5.3.1 |     image: espressif/idf:v5.4.1 | ||||||
|     commands: |     commands: | ||||||
|       - cd esp32_device |       - cd esp32_device | ||||||
|       - /opt/esp/entrypoint.sh idf.py build |       - /opt/esp/entrypoint.sh idf.py build | ||||||
|   | |||||||
							
								
								
									
										1484
									
								
								central_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1484
									
								
								central_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,44 +1,46 @@ | |||||||
| [package] | [package] | ||||||
| name = "central_backend" | name = "central_backend" | ||||||
| version = "0.1.0" | version = "1.0.2" | ||||||
| edition = "2021" | edition = "2024" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| log = "0.4.22" | log = "0.4.27" | ||||||
| env_logger = "0.11.5" | env_logger = "0.11.7" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.5.0" | ||||||
| dotenvy = "0.15.7" | dotenvy = "0.15.7" | ||||||
| clap = { version = "4.5.20", features = ["derive", "env"] } | clap = { version = "4.5.34", features = ["derive", "env"] } | ||||||
| anyhow = "1.0.89" | anyhow = "1.0.97" | ||||||
| thiserror = "1.0.64" | thiserror = "2.0.3" | ||||||
| openssl = { version = "0.10.66" } | openssl = { version = "0.10.71" } | ||||||
| openssl-sys = "0.9.102" | openssl-sys = "0.9.106" | ||||||
| libc = "0.2.159" | libc = "0.2.171" | ||||||
| foreign-types-shared = "0.1.1" | foreign-types-shared = "0.1.1" | ||||||
| asn1 = "0.17" | asn1 = "0.21" | ||||||
| actix-web = { version = "4", features = ["openssl"] } | actix-web = { version = "4.10.2", features = ["openssl"] } | ||||||
| futures = "0.3.31" | futures = "0.3.31" | ||||||
| serde = { version = "1.0.210", features = ["derive"] } | serde = { version = "1.0.215", features = ["derive"] } | ||||||
| reqwest = { version = "0.12.7", features = ["json"] } | reqwest = { version = "0.12.15", features = ["json"] } | ||||||
| serde_json = "1.0.128" | serde_json = "1.0.131" | ||||||
| rand = "0.8.5" | rand = "0.9.0" | ||||||
| actix = "0.13.5" | actix = "0.13.5" | ||||||
| actix-identity = "0.8.0" | actix-identity = "0.8.0" | ||||||
| actix-session = { version = "0.10.1", features = ["cookie-session"] } | actix-session = { version = "0.10.1", features = ["cookie-session"] } | ||||||
| actix-cors = "0.7.0" | actix-cors = "0.7.1" | ||||||
| actix-multipart = { version ="0.7.2", features = ["derive"] } | actix-multipart = { version = "0.7.2", features = ["derive"] } | ||||||
| actix-remote-ip = "0.1.0" | actix-remote-ip = "0.1.0" | ||||||
| futures-util = "0.3.31" | futures-util = "0.3.31" | ||||||
| uuid = { version = "1.10.0", features = ["v4", "serde"] } | uuid = { version = "1.11.0", features = ["v4", "serde"] } | ||||||
| semver = { version = "1.0.23", features = ["serde"] } | semver = { version = "1.0.23", features = ["serde"] } | ||||||
| lazy-regex = "3.3.0" | lazy-regex = "3.4.1" | ||||||
| tokio = { version = "1.40.0", features = ["full"] } | tokio = { version = "1.40.0", features = ["full"] } | ||||||
| tokio_schedule = "0.3.2" | tokio_schedule = "0.3.2" | ||||||
| mime_guess = "2.0.5" | mime_guess = "2.0.5" | ||||||
| rust-embed = "8.5.0" | rust-embed = "8.6.0" | ||||||
| jsonwebtoken = { version = "9.3.0", features = ["use_pem"] } | jsonwebtoken = { version = "9.3.1", features = ["use_pem"] } | ||||||
| prettytable-rs = "0.10.0" | prettytable-rs = "0.10.0" | ||||||
| chrono = "0.4.38" | chrono = "0.4.40" | ||||||
| serde_yml = "0.0.12" | serde_yml = "0.0.12" | ||||||
| bincode = "=2.0.0-rc.3" | bincode = "2.0.1" | ||||||
| fs4 = { version = "0.10.0", features = ["sync"] } | fs4 = { version = "0.13.1", features = ["sync"] } | ||||||
|  | zip = { version = "2.2.0", features = ["bzip2"] } | ||||||
|  | walkdir = "2.5.0" | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ pub enum ConsumptionHistoryType { | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Electrical consumption fetcher backend | /// Electrical consumption fetcher backend | ||||||
| #[derive(Subcommand, Debug, Clone)] | #[derive(Subcommand, Debug, Clone, serde::Serialize)] | ||||||
| pub enum ConsumptionBackend { | pub enum ConsumptionBackend { | ||||||
|     /// Constant consumption value |     /// Constant consumption value | ||||||
|     Constant { |     Constant { | ||||||
| @@ -49,7 +49,7 @@ pub enum ConsumptionBackend { | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Solar system central backend | /// Solar system central backend | ||||||
| #[derive(Parser, Debug)] | #[derive(Parser, Debug, serde::Serialize)] | ||||||
| #[command(version, about, long_about = None)] | #[command(version, about, long_about = None)] | ||||||
| pub struct AppConfig { | pub struct AppConfig { | ||||||
|     /// Read arguments from env file |     /// Read arguments from env file | ||||||
| @@ -110,6 +110,18 @@ pub struct AppConfig { | |||||||
|     #[arg(short('f'), long, env, default_value_t = 5)] |     #[arg(short('f'), long, env, default_value_t = 5)] | ||||||
|     pub energy_fetch_interval: u64, |     pub energy_fetch_interval: u64, | ||||||
|  |  | ||||||
|  |     /// Custom current consumption title in dashboard | ||||||
|  |     #[arg(long, env)] | ||||||
|  |     pub dashboard_custom_current_consumption_title: Option<String>, | ||||||
|  |  | ||||||
|  |     /// Custom relays consumption title in dashboard | ||||||
|  |     #[arg(long, env)] | ||||||
|  |     pub dashboard_custom_relays_consumption_title: Option<String>, | ||||||
|  |  | ||||||
|  |     /// Custom cached consumption title in dashboard | ||||||
|  |     #[arg(long, env)] | ||||||
|  |     pub dashboard_custom_cached_consumption_title: Option<String>, | ||||||
|  |  | ||||||
|     /// Consumption backend provider |     /// Consumption backend provider | ||||||
|     #[clap(subcommand)] |     #[clap(subcommand)] | ||||||
|     pub consumption_backend: Option<ConsumptionBackend>, |     pub consumption_backend: Option<ConsumptionBackend>, | ||||||
|   | |||||||
| @@ -13,10 +13,10 @@ use openssl::pkey::{PKey, Private}; | |||||||
| use openssl::x509::extension::{ | use openssl::x509::extension::{ | ||||||
|     BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, |     BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, | ||||||
| }; | }; | ||||||
| use openssl::x509::{CrlStatus, X509Crl, X509Name, X509NameBuilder, X509Req, X509}; | use openssl::x509::{CrlStatus, X509, X509Crl, X509Name, X509NameBuilder, X509Req}; | ||||||
| use openssl_sys::{ | use openssl_sys::{ | ||||||
|     X509_CRL_add0_revoked, X509_CRL_new, X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate, |     X509_CRL_add0_revoked, X509_CRL_new, X509_CRL_set_issuer_name, X509_CRL_set_version, | ||||||
|     X509_CRL_set_issuer_name, X509_CRL_set_version, X509_CRL_sign, X509_REVOKED_dup, |     X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate, X509_CRL_sign, X509_REVOKED_dup, | ||||||
|     X509_REVOKED_new, X509_REVOKED_set_revocationDate, X509_REVOKED_set_serialNumber, |     X509_REVOKED_new, X509_REVOKED_set_revocationDate, X509_REVOKED_set_serialNumber, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -120,7 +120,7 @@ enum GenCertificatSubjectReq<'a> { | |||||||
|     CSR { csr: &'a X509Req }, |     CSR { csr: &'a X509Req }, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> Default for GenCertificatSubjectReq<'a> { | impl Default for GenCertificatSubjectReq<'_> { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self::Subject { cn: "" } |         Self::Subject { cn: "" } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -325,9 +325,11 @@ mod tests { | |||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         dep_cycle_1.depends_on = vec![dep_cycle_3.id]; |         dep_cycle_1.depends_on = vec![dep_cycle_3.id]; | ||||||
|         assert!(dep_cycle_1 |         assert!( | ||||||
|             .error(&[dep_cycle_2.clone(), dep_cycle_3.clone()]) |             dep_cycle_1 | ||||||
|             .is_some()); |                 .error(&[dep_cycle_2.clone(), dep_cycle_3.clone()]) | ||||||
|  |                 .is_some() | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         dep_cycle_1.depends_on = vec![]; |         dep_cycle_1.depends_on = vec![]; | ||||||
|         assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).is_none()); |         assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).is_none()); | ||||||
| @@ -351,21 +353,29 @@ mod tests { | |||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         assert!(target_relay |         assert!( | ||||||
|             .error(&[other_dep.clone(), second_dep.clone()]) |             target_relay | ||||||
|             .is_some()); |                 .error(&[other_dep.clone(), second_dep.clone()]) | ||||||
|         assert!(target_relay |                 .is_some() | ||||||
|             .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) |         ); | ||||||
|             .is_some()); |         assert!( | ||||||
|  |             target_relay | ||||||
|  |                 .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) | ||||||
|  |                 .is_some() | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         second_dep.conflicts_with = vec![]; |         second_dep.conflicts_with = vec![]; | ||||||
|  |  | ||||||
|         assert!(target_relay |         assert!( | ||||||
|             .error(&[other_dep.clone(), second_dep.clone()]) |             target_relay | ||||||
|             .is_none()); |                 .error(&[other_dep.clone(), second_dep.clone()]) | ||||||
|         assert!(target_relay |                 .is_none() | ||||||
|             .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) |         ); | ||||||
|             .is_none()); |         assert!( | ||||||
|  |             target_relay | ||||||
|  |                 .error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()]) | ||||||
|  |                 .is_none() | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         // self loop |         // self loop | ||||||
|         let mut self_loop = DeviceRelay { |         let mut self_loop = DeviceRelay { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ use crate::devices::device::{ | |||||||
|     Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID, |     Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID, | ||||||
| }; | }; | ||||||
| use crate::utils::time_utils::time_secs; | use crate::utils::time_utils::time_secs; | ||||||
| use openssl::x509::{X509Req, X509}; | use openssl::x509::{X509, X509Req}; | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
| #[derive(thiserror::Error, Debug)] | #[derive(thiserror::Error, Debug)] | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| use crate::app_config::{AppConfig, ConsumptionBackend}; | use crate::app_config::{AppConfig, ConsumptionBackend}; | ||||||
| use rand::{thread_rng, Rng}; | use rand::{Rng, rng}; | ||||||
| use std::num::ParseIntError; | use std::num::ParseIntError; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
|  |  | ||||||
| @@ -49,7 +49,7 @@ pub async fn get_curr_consumption() -> anyhow::Result<EnergyConsumption> { | |||||||
|     match backend { |     match backend { | ||||||
|         ConsumptionBackend::Constant { value } => Ok(*value), |         ConsumptionBackend::Constant { value } => Ok(*value), | ||||||
|  |  | ||||||
|         ConsumptionBackend::Random { min, max } => Ok(thread_rng().gen_range(*min..*max)), |         ConsumptionBackend::Random { min, max } => Ok(rng().random_range(*min..*max)), | ||||||
|  |  | ||||||
|         ConsumptionBackend::File { path } => { |         ConsumptionBackend::File { path } => { | ||||||
|             let path = Path::new(path); |             let path = Path::new(path); | ||||||
| @@ -71,7 +71,11 @@ pub async fn get_curr_consumption() -> anyhow::Result<EnergyConsumption> { | |||||||
|             let response = match curl { |             let response = match curl { | ||||||
|                 false => reqwest::get(url).await?.json::<FroniusResponse>().await?, |                 false => reqwest::get(url).await?.json::<FroniusResponse>().await?, | ||||||
|                 true => { |                 true => { | ||||||
|                     let res = std::process::Command::new("curl").arg(url).output()?; |                     let res = std::process::Command::new("curl") | ||||||
|  |                         .arg("--connect-timeout") | ||||||
|  |                         .arg("1.5") | ||||||
|  |                         .arg(url) | ||||||
|  |                         .output()?; | ||||||
|  |  | ||||||
|                     if !res.status.success() { |                     if !res.status.success() { | ||||||
|                         return Err(ConsumptionError::CurlReqFailed.into()); |                         return Err(ConsumptionError::CurlReqFailed.into()); | ||||||
|   | |||||||
| @@ -25,7 +25,14 @@ impl EnergyActor { | |||||||
|     pub async fn new() -> anyhow::Result<Self> { |     pub async fn new() -> anyhow::Result<Self> { | ||||||
|         let consumption_cache_size = |         let consumption_cache_size = | ||||||
|             AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval; |             AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval; | ||||||
|         let curr_consumption = consumption::get_curr_consumption().await?; |         let curr_consumption = match consumption::get_curr_consumption().await { | ||||||
|  |             Ok(v) => v, | ||||||
|  |             Err(e) => { | ||||||
|  |                 log::warn!("Failed to fetch consumption, using default value! {e}"); | ||||||
|  |                 constants::FALLBACK_PRODUCTION_VALUE | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         log::info!("Initial consumption value: {curr_consumption}"); | ||||||
|         let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize); |         let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize); | ||||||
|         consumption_cache.add_value(curr_consumption); |         consumption_cache.add_value(curr_consumption); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| use prettytable::{row, Table}; | use prettytable::{Table, row}; | ||||||
|  |  | ||||||
| use crate::constants; | use crate::constants; | ||||||
| use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID}; | use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID}; | ||||||
| @@ -289,7 +289,11 @@ impl EnergyEngine { | |||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 log::info!("Forcefully turn on relay {} to catch up running constraints (only {}s this day)", r.name, total_runtime); |                 log::info!( | ||||||
|  |                     "Forcefully turn on relay {} to catch up running constraints (only {}s this day)", | ||||||
|  |                     r.name, | ||||||
|  |                     total_runtime | ||||||
|  |                 ); | ||||||
|                 new_relays_state.get_mut(&r.id).unwrap().on = true; |                 new_relays_state.get_mut(&r.id).unwrap().on = true; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| use crate::devices::device::{DeviceRelay, DeviceRelayID}; | use crate::devices::device::{DeviceRelay, DeviceRelayID}; | ||||||
| use crate::utils::files_utils; | use crate::utils::files_utils; | ||||||
| use crate::utils::time_utils::{day_number, time_start_of_day}; | use crate::utils::time_utils::{day_number, time_secs, time_start_of_day}; | ||||||
|  |  | ||||||
| const TIME_INTERVAL: usize = 30; | const TIME_INTERVAL: usize = 30; | ||||||
|  |  | ||||||
| @@ -128,15 +128,26 @@ pub fn relay_total_runtime_adjusted(relay: &DeviceRelay) -> usize { | |||||||
|         .unwrap_or(0); |         .unwrap_or(0); | ||||||
|  |  | ||||||
|     let time_start_day = time_start_of_day().unwrap_or(1726696800); |     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; |     // Check if we have reached reset_time today yet or not | ||||||
|     relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24) |     if time_start_day + reset_time as u64 <= time_secs() { | ||||||
|  |         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) | ||||||
|  |     } | ||||||
|  |     // If we have not reached reset time yet, we need to focus on previous day | ||||||
|  |     else { | ||||||
|  |         let time_start_yesterday = time_start_day - 3600 * 24; | ||||||
|  |         let start_time = time_start_yesterday + reset_time as u64; | ||||||
|  |         let end_time = time_start_day + reset_time as u64; | ||||||
|  |         relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use crate::devices::device::DeviceRelayID; |     use crate::devices::device::DeviceRelayID; | ||||||
|     use crate::energy::relay_state_history::{relay_total_runtime, RelayStateHistory}; |     use crate::energy::relay_state_history::{RelayStateHistory, relay_total_runtime}; | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_relay_state_history() { |     fn test_relay_state_history() { | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ pub fn save_log( | |||||||
|         .as_bytes(), |         .as_bytes(), | ||||||
|     )?; |     )?; | ||||||
|     file.flush()?; |     file.flush()?; | ||||||
|     file.unlock()?; |     fs4::fs_std::FileExt::unlock(&file)?; | ||||||
|  |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ use central_backend::energy::energy_actor::EnergyActor; | |||||||
| use central_backend::server::servers; | use central_backend::server::servers; | ||||||
| use central_backend::utils::files_utils::create_directory_if_missing; | use central_backend::utils::files_utils::create_directory_if_missing; | ||||||
| use futures::future; | use futures::future; | ||||||
| use tokio_schedule::{every, Job}; | use tokio_schedule::{Job, every}; | ||||||
|  |  | ||||||
| #[actix_web::main] | #[actix_web::main] | ||||||
| async fn main() -> std::io::Result<()> { | async fn main() -> std::io::Result<()> { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| use actix_identity::Identity; | use actix_identity::Identity; | ||||||
| use std::future::{ready, Ready}; | use std::future::{Ready, ready}; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
|  |  | ||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| @@ -7,8 +7,8 @@ use crate::constants; | |||||||
| use actix_web::body::EitherBody; | use actix_web::body::EitherBody; | ||||||
| use actix_web::dev::Payload; | use actix_web::dev::Payload; | ||||||
| use actix_web::{ | use actix_web::{ | ||||||
|     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, |  | ||||||
|     Error, FromRequest, HttpResponse, |     Error, FromRequest, HttpResponse, | ||||||
|  |     dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, | ||||||
| }; | }; | ||||||
| use futures_util::future::LocalBoxFuture; | use futures_util::future::LocalBoxFuture; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
|  | use actix_web::HttpResponse; | ||||||
| use actix_web::body::BoxBody; | use actix_web::body::BoxBody; | ||||||
| use actix_web::http::StatusCode; | use actix_web::http::StatusCode; | ||||||
| use actix_web::HttpResponse; |  | ||||||
| use std::error::Error; | use std::error::Error; | ||||||
| use std::fmt::{Display, Formatter}; | use std::fmt::{Display, Formatter}; | ||||||
| use std::io::ErrorKind; | use std::io::ErrorKind; | ||||||
|  | use zip::result::ZipError; | ||||||
|  |  | ||||||
| /// Custom error to ease controller writing | /// Custom error to ease controller writing | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| @@ -109,6 +110,18 @@ impl From<openssl::error::ErrorStack> for HttpErr { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl From<ZipError> for HttpErr { | ||||||
|  |     fn from(value: ZipError) -> Self { | ||||||
|  |         HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<walkdir::Error> for HttpErr { | ||||||
|  |     fn from(value: walkdir::Error) -> Self { | ||||||
|  |         HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl From<HttpResponse> for HttpErr { | impl From<HttpResponse> for HttpErr { | ||||||
|     fn from(value: HttpResponse) -> Self { |     fn from(value: HttpResponse) -> Self { | ||||||
|         HttpErr::HTTPResponse(value) |         HttpErr::HTTPResponse(value) | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| use crate::logs::logs_manager; | use crate::logs::logs_manager; | ||||||
| use crate::logs::severity::LogSeverity; | use crate::logs::severity::LogSeverity; | ||||||
|  | use crate::server::WebEnergyActor; | ||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use crate::server::devices_api::jwt_parser::JWTRequest; | use crate::server::devices_api::jwt_parser::JWTRequest; | ||||||
| use crate::server::WebEnergyActor; | use actix_web::{HttpResponse, web}; | ||||||
| use actix_web::{web, HttpResponse}; |  | ||||||
|  |  | ||||||
| #[derive(Debug, serde::Deserialize)] | #[derive(Debug, serde::Deserialize)] | ||||||
| pub struct LogRequest { | pub struct LogRequest { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| use crate::ota::ota_manager; | use crate::ota::ota_manager; | ||||||
| use crate::ota::ota_update::OTAPlatform; | use crate::ota::ota_update::OTAPlatform; | ||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use actix_web::{web, HttpResponse}; | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct FirmwarePath { | pub struct FirmwarePath { | ||||||
|   | |||||||
| @@ -4,10 +4,10 @@ use crate::energy::energy_actor; | |||||||
| use crate::energy::energy_actor::RelaySyncStatus; | use crate::energy::energy_actor::RelaySyncStatus; | ||||||
| use crate::ota::ota_manager; | use crate::ota::ota_manager; | ||||||
| use crate::ota::ota_update::OTAPlatform; | use crate::ota::ota_update::OTAPlatform; | ||||||
|  | use crate::server::WebEnergyActor; | ||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use crate::server::devices_api::jwt_parser::JWTRequest; | use crate::server::devices_api::jwt_parser::JWTRequest; | ||||||
| use crate::server::WebEnergyActor; | use actix_web::{HttpResponse, web}; | ||||||
| use actix_web::{web, HttpResponse}; |  | ||||||
| use openssl::nid::Nid; | use openssl::nid::Nid; | ||||||
| use openssl::x509::X509Req; | use openssl::x509::X509Req; | ||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
|   | |||||||
| @@ -10,14 +10,14 @@ use crate::server::unsecure_server::*; | |||||||
| use crate::server::web_api::*; | use crate::server::web_api::*; | ||||||
| use crate::server::web_app_controller; | use crate::server::web_app_controller; | ||||||
| use actix_cors::Cors; | use actix_cors::Cors; | ||||||
| use actix_identity::config::LogoutBehaviour; |  | ||||||
| use actix_identity::IdentityMiddleware; | use actix_identity::IdentityMiddleware; | ||||||
|  | use actix_identity::config::LogoutBehaviour; | ||||||
| use actix_remote_ip::RemoteIPConfig; | use actix_remote_ip::RemoteIPConfig; | ||||||
| use actix_session::storage::CookieSessionStore; |  | ||||||
| use actix_session::SessionMiddleware; | use actix_session::SessionMiddleware; | ||||||
|  | use actix_session::storage::CookieSessionStore; | ||||||
| use actix_web::cookie::{Key, SameSite}; | use actix_web::cookie::{Key, SameSite}; | ||||||
| use actix_web::middleware::Logger; | use actix_web::middleware::Logger; | ||||||
| use actix_web::{web, App, HttpServer}; | use actix_web::{App, HttpServer, web}; | ||||||
| use openssl::ssl::{SslAcceptor, SslMethod}; | use openssl::ssl::{SslAcceptor, SslMethod}; | ||||||
| use std::time::Duration; | use std::time::Duration; | ||||||
|  |  | ||||||
| @@ -243,6 +243,11 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()> | |||||||
|                 "/web_api/relay/{id}/status", |                 "/web_api/relay/{id}/status", | ||||||
|                 web::get().to(relays_controller::status_single), |                 web::get().to(relays_controller::status_single), | ||||||
|             ) |             ) | ||||||
|  |             // Management API | ||||||
|  |             .route( | ||||||
|  |                 "/web_api/management/download_storage", | ||||||
|  |                 web::get().to(management_controller::download_storage), | ||||||
|  |             ) | ||||||
|             // Devices API |             // Devices API | ||||||
|             .route( |             .route( | ||||||
|                 "/devices_api/utils/time", |                 "/devices_api/utils/time", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| use crate::app_config::AppConfig; | use crate::app_config::AppConfig; | ||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use actix_web::{web, HttpResponse}; | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct ServeCRLPath { | pub struct ServeCRLPath { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| use crate::devices::device::DeviceRelayID; | use crate::devices::device::DeviceRelayID; | ||||||
| use crate::energy::{energy_actor, relay_state_history}; | use crate::energy::{energy_actor, relay_state_history}; | ||||||
| use crate::server::custom_error::HttpResult; |  | ||||||
| use crate::server::WebEnergyActor; | use crate::server::WebEnergyActor; | ||||||
| use actix_web::{web, HttpResponse}; | use crate::server::custom_error::HttpResult; | ||||||
|  | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct LegacyStateRelay { | pub struct LegacyStateRelay { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; | |||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use actix_identity::Identity; | use actix_identity::Identity; | ||||||
| use actix_remote_ip::RemoteIP; | use actix_remote_ip::RemoteIP; | ||||||
| use actix_web::{web, HttpMessage, HttpRequest, HttpResponse}; | use actix_web::{HttpMessage, HttpRequest, HttpResponse, web}; | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct AuthRequest { | pub struct AuthRequest { | ||||||
| @@ -17,11 +17,11 @@ pub async fn password_auth( | |||||||
|     remote_ip: RemoteIP, |     remote_ip: RemoteIP, | ||||||
| ) -> HttpResult { | ) -> HttpResult { | ||||||
|     if r.user != AppConfig::get().admin_username || r.password != AppConfig::get().admin_password { |     if r.user != AppConfig::get().admin_username || r.password != AppConfig::get().admin_password { | ||||||
|         log::error!("Failed login attempt from {}!", remote_ip.0.to_string()); |         log::error!("Failed login attempt from {}!", remote_ip.0); | ||||||
|         return Ok(HttpResponse::Unauthorized().json("Invalid credentials!")); |         return Ok(HttpResponse::Unauthorized().json("Invalid credentials!")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     log::info!("Successful login attempt from {}!", remote_ip.0.to_string()); |     log::info!("Successful login attempt from {}!", remote_ip.0); | ||||||
|     Identity::login(&request.extensions(), r.user.to_string())?; |     Identity::login(&request.extensions(), r.user.to_string())?; | ||||||
|     Ok(HttpResponse::Ok().finish()) |     Ok(HttpResponse::Ok().finish()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| use crate::devices::device::{DeviceGeneralInfo, DeviceId}; | use crate::devices::device::{DeviceGeneralInfo, DeviceId}; | ||||||
| use crate::energy::energy_actor; | use crate::energy::energy_actor; | ||||||
| use crate::server::custom_error::HttpResult; |  | ||||||
| use crate::server::WebEnergyActor; | use crate::server::WebEnergyActor; | ||||||
| use actix_web::{web, HttpResponse}; | use crate::server::custom_error::HttpResult; | ||||||
|  | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| /// Get the list of pending (not accepted yet) devices | /// Get the list of pending (not accepted yet) devices | ||||||
| pub async fn list_pending(actor: WebEnergyActor) -> HttpResult { | pub async fn list_pending(actor: WebEnergyActor) -> HttpResult { | ||||||
|   | |||||||
| @@ -2,21 +2,27 @@ use crate::app_config::ConsumptionHistoryType; | |||||||
| use crate::energy::consumption::EnergyConsumption; | use crate::energy::consumption::EnergyConsumption; | ||||||
| use crate::energy::consumption_history_file::ConsumptionHistoryFile; | use crate::energy::consumption_history_file::ConsumptionHistoryFile; | ||||||
| use crate::energy::{consumption, energy_actor}; | use crate::energy::{consumption, energy_actor}; | ||||||
| use crate::server::custom_error::HttpResult; |  | ||||||
| use crate::server::WebEnergyActor; | use crate::server::WebEnergyActor; | ||||||
|  | use crate::server::custom_error::HttpResult; | ||||||
| use crate::utils::time_utils::time_secs; | use crate::utils::time_utils::time_secs; | ||||||
| use actix_web::HttpResponse; | use actix_web::HttpResponse; | ||||||
|  |  | ||||||
| #[derive(serde::Serialize)] | #[derive(serde::Serialize)] | ||||||
| struct Consumption { | struct Consumption { | ||||||
|     consumption: i32, |     consumption: Option<i32>, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get current energy consumption | /// Get current energy consumption | ||||||
| pub async fn curr_consumption() -> HttpResult { | pub async fn curr_consumption() -> HttpResult { | ||||||
|     let consumption = consumption::get_curr_consumption().await?; |     Ok(match consumption::get_curr_consumption().await { | ||||||
|  |         Ok(v) => HttpResponse::Ok().json(Consumption { | ||||||
|     Ok(HttpResponse::Ok().json(Consumption { consumption })) |             consumption: Some(v), | ||||||
|  |         }), | ||||||
|  |         Err(e) => { | ||||||
|  |             log::error!("Failed to fetch current consumption! {e}"); | ||||||
|  |             HttpResponse::Ok().json(Consumption { consumption: None }) | ||||||
|  |         } | ||||||
|  |     }) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get curr consumption history | /// Get curr consumption history | ||||||
| @@ -34,7 +40,9 @@ pub async fn curr_consumption_history() -> HttpResult { | |||||||
| pub async fn cached_consumption(energy_actor: WebEnergyActor) -> HttpResult { | pub async fn cached_consumption(energy_actor: WebEnergyActor) -> HttpResult { | ||||||
|     let consumption = energy_actor.send(energy_actor::GetCurrConsumption).await?; |     let consumption = energy_actor.send(energy_actor::GetCurrConsumption).await?; | ||||||
|  |  | ||||||
|     Ok(HttpResponse::Ok().json(Consumption { consumption })) |     Ok(HttpResponse::Ok().json(Consumption { | ||||||
|  |         consumption: Some(consumption), | ||||||
|  |     })) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get current relays consumption | /// Get current relays consumption | ||||||
| @@ -42,7 +50,9 @@ pub async fn relays_consumption(energy_actor: WebEnergyActor) -> HttpResult { | |||||||
|     let consumption = |     let consumption = | ||||||
|         energy_actor.send(energy_actor::RelaysConsumption).await? as EnergyConsumption; |         energy_actor.send(energy_actor::RelaysConsumption).await? as EnergyConsumption; | ||||||
|  |  | ||||||
|     Ok(HttpResponse::Ok().json(Consumption { consumption })) |     Ok(HttpResponse::Ok().json(Consumption { | ||||||
|  |         consumption: Some(consumption), | ||||||
|  |     })) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn relays_consumption_history() -> HttpResult { | pub async fn relays_consumption_history() -> HttpResult { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ use crate::logs::logs_manager; | |||||||
| use crate::logs::severity::LogSeverity; | use crate::logs::severity::LogSeverity; | ||||||
| use crate::server::custom_error::HttpResult; | use crate::server::custom_error::HttpResult; | ||||||
| use crate::utils::time_utils::curr_day_number; | use crate::utils::time_utils::curr_day_number; | ||||||
| use actix_web::{web, HttpResponse}; | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct LogRequest { | pub struct LogRequest { | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								central_backend/src/server/web_api/management_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								central_backend/src/server/web_api/management_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | use crate::app_config::AppConfig; | ||||||
|  | use crate::server::custom_error::HttpResult; | ||||||
|  | use crate::utils::time_utils::current_day; | ||||||
|  | use actix_web::HttpResponse; | ||||||
|  | use anyhow::Context; | ||||||
|  | use std::fs::File; | ||||||
|  | use std::io::{Cursor, Read, Write}; | ||||||
|  | use walkdir::WalkDir; | ||||||
|  | use zip::write::SimpleFileOptions; | ||||||
|  |  | ||||||
|  | /// Download a full copy of the storage data | ||||||
|  | pub async fn download_storage() -> HttpResult { | ||||||
|  |     let mut zip_buff = Cursor::new(Vec::new()); | ||||||
|  |     let mut zip = zip::ZipWriter::new(&mut zip_buff); | ||||||
|  |  | ||||||
|  |     let options = SimpleFileOptions::default() | ||||||
|  |         .compression_method(zip::CompressionMethod::Bzip2) | ||||||
|  |         .unix_permissions(0o700); | ||||||
|  |  | ||||||
|  |     let storage = AppConfig::get().storage_path(); | ||||||
|  |  | ||||||
|  |     let mut file_buff = Vec::new(); | ||||||
|  |     for entry in WalkDir::new(&storage) { | ||||||
|  |         let entry = entry?; | ||||||
|  |  | ||||||
|  |         let path = entry.path(); | ||||||
|  |         let name = path.strip_prefix(&storage).unwrap(); | ||||||
|  |         let path_as_string = name | ||||||
|  |             .to_str() | ||||||
|  |             .map(str::to_owned) | ||||||
|  |             .with_context(|| format!("{name:?} Is a Non UTF-8 Path"))?; | ||||||
|  |  | ||||||
|  |         // Write file or directory explicitly | ||||||
|  |         // Some unzip tools unzip files with directory paths correctly, some do not! | ||||||
|  |         if path.is_file() { | ||||||
|  |             log::debug!("adding file {path:?} as {name:?} ..."); | ||||||
|  |             zip.start_file(path_as_string, options)?; | ||||||
|  |             let mut f = File::open(path)?; | ||||||
|  |  | ||||||
|  |             f.read_to_end(&mut file_buff)?; | ||||||
|  |             zip.write_all(&file_buff)?; | ||||||
|  |             file_buff.clear(); | ||||||
|  |         } else if !name.as_os_str().is_empty() { | ||||||
|  |             // Only if not root! Avoids path spec / warning | ||||||
|  |             // and mapname conversion failed error on unzip | ||||||
|  |             log::debug!("adding dir {path_as_string:?} as {name:?} ..."); | ||||||
|  |             zip.add_directory(path_as_string, options)?; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Inject runtime configuration | ||||||
|  |     zip.start_file("/app_config.json", options)?; | ||||||
|  |     zip.write_all(&serde_json::to_vec_pretty(&AppConfig::get())?)?; | ||||||
|  |  | ||||||
|  |     zip.finish()?; | ||||||
|  |  | ||||||
|  |     let filename = format!("storage-{}.zip", current_day()); | ||||||
|  |  | ||||||
|  |     Ok(HttpResponse::Ok() | ||||||
|  |         .content_type("application/zip") | ||||||
|  |         .insert_header(( | ||||||
|  |             "content-disposition", | ||||||
|  |             format!("attachment; filename=\"{filename}\""), | ||||||
|  |         )) | ||||||
|  |         .body(zip_buff.into_inner())) | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ pub mod auth_controller; | |||||||
| pub mod devices_controller; | pub mod devices_controller; | ||||||
| pub mod energy_controller; | pub mod energy_controller; | ||||||
| pub mod logging_controller; | pub mod logging_controller; | ||||||
|  | pub mod management_controller; | ||||||
| pub mod ota_controller; | pub mod ota_controller; | ||||||
| pub mod relays_controller; | pub mod relays_controller; | ||||||
| pub mod server_controller; | pub mod server_controller; | ||||||
|   | |||||||
| @@ -3,11 +3,11 @@ use crate::devices::device::DeviceId; | |||||||
| use crate::energy::energy_actor; | use crate::energy::energy_actor; | ||||||
| use crate::ota::ota_manager; | use crate::ota::ota_manager; | ||||||
| use crate::ota::ota_update::OTAPlatform; | use crate::ota::ota_update::OTAPlatform; | ||||||
| use crate::server::custom_error::HttpResult; |  | ||||||
| use crate::server::WebEnergyActor; | use crate::server::WebEnergyActor; | ||||||
| use actix_multipart::form::tempfile::TempFile; | use crate::server::custom_error::HttpResult; | ||||||
| use actix_multipart::form::MultipartForm; | use actix_multipart::form::MultipartForm; | ||||||
| use actix_web::{web, HttpResponse}; | use actix_multipart::form::tempfile::TempFile; | ||||||
|  | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| pub async fn supported_platforms() -> HttpResult { | pub async fn supported_platforms() -> HttpResult { | ||||||
|     Ok(HttpResponse::Ok().json(OTAPlatform::supported_platforms())) |     Ok(HttpResponse::Ok().json(OTAPlatform::supported_platforms())) | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| use crate::devices::device::{DeviceId, DeviceRelay, DeviceRelayID}; | use crate::devices::device::{DeviceId, DeviceRelay, DeviceRelayID}; | ||||||
| use crate::energy::energy_actor; | use crate::energy::energy_actor; | ||||||
| use crate::server::custom_error::HttpResult; |  | ||||||
| use crate::server::WebEnergyActor; | use crate::server::WebEnergyActor; | ||||||
| use actix_web::{web, HttpResponse}; | use crate::server::custom_error::HttpResult; | ||||||
|  | use actix_web::{HttpResponse, web}; | ||||||
|  |  | ||||||
| /// Get the full list of relays | /// Get the full list of relays | ||||||
| pub async fn get_list(actor: WebEnergyActor) -> HttpResult { | pub async fn get_list(actor: WebEnergyActor) -> HttpResult { | ||||||
|   | |||||||
| @@ -13,6 +13,10 @@ struct ServerConfig { | |||||||
|     auth_disabled: bool, |     auth_disabled: bool, | ||||||
|     constraints: StaticConstraints, |     constraints: StaticConstraints, | ||||||
|     unsecure_origin: String, |     unsecure_origin: String, | ||||||
|  |     backend_version: &'static str, | ||||||
|  |     dashboard_custom_current_consumption_title: Option<&'static str>, | ||||||
|  |     dashboard_custom_relays_consumption_title: Option<&'static str>, | ||||||
|  |     dashboard_custom_cached_consumption_title: Option<&'static str>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ServerConfig { | impl Default for ServerConfig { | ||||||
| @@ -21,6 +25,16 @@ impl Default for ServerConfig { | |||||||
|             auth_disabled: AppConfig::get().unsecure_disable_login, |             auth_disabled: AppConfig::get().unsecure_disable_login, | ||||||
|             constraints: Default::default(), |             constraints: Default::default(), | ||||||
|             unsecure_origin: AppConfig::get().unsecure_origin(), |             unsecure_origin: AppConfig::get().unsecure_origin(), | ||||||
|  |             backend_version: env!("CARGO_PKG_VERSION"), | ||||||
|  |             dashboard_custom_current_consumption_title: AppConfig::get() | ||||||
|  |                 .dashboard_custom_current_consumption_title | ||||||
|  |                 .as_deref(), | ||||||
|  |             dashboard_custom_relays_consumption_title: AppConfig::get() | ||||||
|  |                 .dashboard_custom_relays_consumption_title | ||||||
|  |                 .as_deref(), | ||||||
|  |             dashboard_custom_cached_consumption_title: AppConfig::get() | ||||||
|  |                 .dashboard_custom_cached_consumption_title | ||||||
|  |                 .as_deref(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ mod serve_static_debug { | |||||||
|  |  | ||||||
| #[cfg(not(debug_assertions))] | #[cfg(not(debug_assertions))] | ||||||
| mod serve_static_release { | mod serve_static_release { | ||||||
|     use actix_web::{web, HttpResponse, Responder}; |     use actix_web::{HttpResponse, Responder, web}; | ||||||
|     use rust_embed::RustEmbed; |     use rust_embed::RustEmbed; | ||||||
|  |  | ||||||
|     #[derive(RustEmbed)] |     #[derive(RustEmbed)] | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| use chrono::prelude::*; | use chrono::prelude::*; | ||||||
| use std::time::{SystemTime, UNIX_EPOCH}; | use std::time::{SystemTime, UNIX_EPOCH}; | ||||||
|  |  | ||||||
| /// Get the current time since epoch | /// Get the current time since epoch, in seconds | ||||||
| pub fn time_secs() -> u64 { | pub fn time_secs() -> u64 { | ||||||
|     SystemTime::now() |     SystemTime::now() | ||||||
|         .duration_since(UNIX_EPOCH) |         .duration_since(UNIX_EPOCH) | ||||||
| @@ -41,6 +41,12 @@ pub fn time_start_of_day() -> anyhow::Result<u64> { | |||||||
|     Ok(local.timestamp() as u64) |     Ok(local.timestamp() as u64) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Get formatted string containing current day information | ||||||
|  | pub fn current_day() -> String { | ||||||
|  |     let dt = Local::now(); | ||||||
|  |     format!("{}-{:0>2}-{:0>2}", dt.year(), dt.month(), dt.day()) | ||||||
|  | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod test { | mod test { | ||||||
|     use crate::utils::time_utils::day_number; |     use crate::utils::time_utils::day_number; | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								central_frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								central_frontend/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import js from '@eslint/js' | ||||||
|  | import globals from 'globals' | ||||||
|  | import reactHooks from 'eslint-plugin-react-hooks' | ||||||
|  | import reactRefresh from 'eslint-plugin-react-refresh' | ||||||
|  | import tseslint from 'typescript-eslint' | ||||||
|  |  | ||||||
|  | export default tseslint.config( | ||||||
|  |   { ignores: ['dist'] }, | ||||||
|  |   { | ||||||
|  |     extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||||||
|  |     files: ['**/*.{ts,tsx}'], | ||||||
|  |     languageOptions: { | ||||||
|  |       ecmaVersion: 2020, | ||||||
|  |       globals: globals.browser, | ||||||
|  |     }, | ||||||
|  |     plugins: { | ||||||
|  |       'react-hooks': reactHooks, | ||||||
|  |       'react-refresh': reactRefresh, | ||||||
|  |     }, | ||||||
|  |     rules: { | ||||||
|  |       ...reactHooks.configs.recommended.rules, | ||||||
|  |       'react-refresh/only-export-components': [ | ||||||
|  |         'warn', | ||||||
|  |         { allowConstantExport: true }, | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | ) | ||||||
							
								
								
									
										2411
									
								
								central_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2411
									
								
								central_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,38 +6,40 @@ | |||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |     "dev": "vite", | ||||||
|     "build": "tsc -b && vite build", |     "build": "tsc -b && vite build", | ||||||
|     "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", |     "lint": "eslint .", | ||||||
|     "preview": "vite preview" |     "preview": "vite preview" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@emotion/react": "^11.13.3", |     "@emotion/react": "^11.14.0", | ||||||
|     "@emotion/styled": "^11.13.0", |     "@emotion/styled": "^11.13.0", | ||||||
|     "@fontsource/roboto": "^5.1.0", |     "@fontsource/roboto": "^5.2.5", | ||||||
|     "@mdi/js": "^7.4.47", |     "@mdi/js": "^7.4.47", | ||||||
|     "@mdi/react": "^1.6.1", |     "@mdi/react": "^1.6.1", | ||||||
|     "@mui/icons-material": "^6.1.3", |     "@mui/icons-material": "^7.0.0", | ||||||
|     "@mui/material": "^6.1.3", |     "@mui/material": "^7.0.0", | ||||||
|     "@mui/x-charts": "^7.20.0", |     "@mui/x-charts": "^7.21.0", | ||||||
|     "@mui/x-date-pickers": "^7.20.0", |     "@mui/x-date-pickers": "^7.28.2", | ||||||
|     "@types/semver": "^7.5.8", |  | ||||||
|     "date-and-time": "^3.6.0", |     "date-and-time": "^3.6.0", | ||||||
|     "dayjs": "^1.11.13", |     "dayjs": "^1.11.13", | ||||||
|     "filesize": "^10.1.6", |     "filesize": "^10.1.6", | ||||||
|     "react": "^18.3.1", |     "react": "^19.0.0", | ||||||
|     "react-dom": "^18.3.1", |     "react-dom": "^19.0.0", | ||||||
|     "react-router-dom": "^6.27.0", |     "react-router-dom": "^7.4.0", | ||||||
|     "semver": "^7.6.3" |     "semver": "^7.7.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/react": "^18.3.11", |     "@types/react": "^19.0.0", | ||||||
|     "@types/react-dom": "^18.3.1", |     "@types/react-dom": "^19.0.4", | ||||||
|     "@typescript-eslint/eslint-plugin": "^8.8.0", |     "@types/semver": "^7.7.0", | ||||||
|     "@typescript-eslint/parser": "^8.8.0", |     "@typescript-eslint/eslint-plugin": "^8.18.2", | ||||||
|     "@vitejs/plugin-react": "^4.3.2", |     "@typescript-eslint/parser": "^8.28.0", | ||||||
|     "eslint": "^8.57.1", |     "@vitejs/plugin-react": "^4.3.4", | ||||||
|  |     "eslint": "^9.23.0", | ||||||
|     "eslint-plugin-react-hooks": "^5.0.0", |     "eslint-plugin-react-hooks": "^5.0.0", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.12", |     "eslint-plugin-react-refresh": "^0.4.19", | ||||||
|     "typescript": "^5.6.3", |     "globals": "^16.0.0", | ||||||
|     "vite": "^5.4.8" |     "typescript": "^5.8.2", | ||||||
|  |     "typescript-eslint": "^8.24.1", | ||||||
|  |     "vite": "^6.2.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import { PendingDevicesRoute } from "./routes/PendingDevicesRoute"; | |||||||
| import { RelaysListRoute } from "./routes/RelaysListRoute"; | import { RelaysListRoute } from "./routes/RelaysListRoute"; | ||||||
| import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; | import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; | ||||||
| import { OTARoute } from "./routes/OTARoute"; | import { OTARoute } from "./routes/OTARoute"; | ||||||
|  | import { ManagementRoute } from "./routes/ManagementRoute"; | ||||||
|  |  | ||||||
| export function App() { | export function App() { | ||||||
|   if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) |   if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) | ||||||
| @@ -31,6 +32,7 @@ export function App() { | |||||||
|         <Route path="relays" element={<RelaysListRoute />} /> |         <Route path="relays" element={<RelaysListRoute />} /> | ||||||
|         <Route path="ota" element={<OTARoute />} /> |         <Route path="ota" element={<OTARoute />} /> | ||||||
|         <Route path="logs" element={<LogsRoute />} /> |         <Route path="logs" element={<LogsRoute />} /> | ||||||
|  |         <Route path="management" element={<ManagementRoute />} /> | ||||||
|         <Route path="*" element={<NotFoundRoute />} /> |         <Route path="*" element={<NotFoundRoute />} /> | ||||||
|       </Route> |       </Route> | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -4,6 +4,10 @@ export interface ServerConfig { | |||||||
|   auth_disabled: boolean; |   auth_disabled: boolean; | ||||||
|   constraints: ServerConstraint; |   constraints: ServerConstraint; | ||||||
|   unsecure_origin: string; |   unsecure_origin: string; | ||||||
|  |   backend_version: string; | ||||||
|  |   dashboard_custom_current_consumption_title?: string; | ||||||
|  |   dashboard_custom_relays_consumption_title?: string; | ||||||
|  |   dashboard_custom_cached_consumption_title?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ServerConstraint { | export interface ServerConstraint { | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import { | |||||||
|   DialogTitle, |   DialogTitle, | ||||||
|   Typography, |   Typography, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import Grid from "@mui/material/Grid2"; | import Grid from "@mui/material/Grid"; | ||||||
| import { TimePicker } from "@mui/x-date-pickers"; | import { TimePicker } from "@mui/x-date-pickers"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { Device, DeviceRelay } from "../api/DeviceApi"; | import { Device, DeviceRelay } from "../api/DeviceApi"; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import DeleteIcon from "@mui/icons-material/Delete"; | import DeleteIcon from "@mui/icons-material/Delete"; | ||||||
| import RefreshIcon from "@mui/icons-material/Refresh"; | import RefreshIcon from "@mui/icons-material/Refresh"; | ||||||
| import { IconButton, Tooltip } from "@mui/material"; | import { IconButton, Tooltip } from "@mui/material"; | ||||||
| import Grid from "@mui/material/Grid2"; | import Grid from "@mui/material/Grid"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| import { useNavigate, useParams } from "react-router-dom"; | import { useNavigate, useParams } from "react-router-dom"; | ||||||
| import { Device, DeviceApi } from "../../api/DeviceApi"; | import { Device, DeviceApi } from "../../api/DeviceApi"; | ||||||
|   | |||||||
| @@ -81,7 +81,7 @@ function ValidatedDevicesList(p: { | |||||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> |       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||||
|         <TableHead> |         <TableHead> | ||||||
|           <TableRow> |           <TableRow> | ||||||
|             <TableCell>#</TableCell> |             <TableCell>Name</TableCell> | ||||||
|             <TableCell align="center">Model</TableCell> |             <TableCell align="center">Model</TableCell> | ||||||
|             <TableCell align="center">Version</TableCell> |             <TableCell align="center">Version</TableCell> | ||||||
|             <TableCell align="center">Max relays</TableCell> |             <TableCell align="center">Max relays</TableCell> | ||||||
| @@ -99,7 +99,7 @@ function ValidatedDevicesList(p: { | |||||||
|               onDoubleClick={() => navigate(DeviceURL(dev))} |               onDoubleClick={() => navigate(DeviceURL(dev))} | ||||||
|             > |             > | ||||||
|               <TableCell component="th" scope="row"> |               <TableCell component="th" scope="row"> | ||||||
|                 {dev.id} |                 {dev.name} | ||||||
|               </TableCell> |               </TableCell> | ||||||
|               <TableCell align="center">{dev.info.reference}</TableCell> |               <TableCell align="center">{dev.info.reference}</TableCell> | ||||||
|               <TableCell align="center">{dev.info.version}</TableCell> |               <TableCell align="center">{dev.info.version}</TableCell> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||||
| import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget"; | import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget"; | ||||||
| import Grid from "@mui/material/Grid2"; | import Grid from "@mui/material/Grid"; | ||||||
| import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget"; | import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget"; | ||||||
| import { RelayConsumptionWidget } from "./HomeRoute/RelayConsumptionWidget"; | import { RelayConsumptionWidget } from "./HomeRoute/RelayConsumptionWidget"; | ||||||
| import { RelaysListRoute } from "./RelaysListRoute"; | import { RelaysListRoute } from "./RelaysListRoute"; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import React from "react"; | |||||||
| import { EnergyApi } from "../../api/EnergyApi"; | import { EnergyApi } from "../../api/EnergyApi"; | ||||||
| import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | ||||||
| import StatCard from "../../widgets/StatCard"; | import StatCard from "../../widgets/StatCard"; | ||||||
|  | import { ServerApi } from "../../api/ServerApi"; | ||||||
|  |  | ||||||
| export function CachedConsumptionWidget(): React.ReactElement { | export function CachedConsumptionWidget(): React.ReactElement { | ||||||
|   const snackbar = useSnackbar(); |   const snackbar = useSnackbar(); | ||||||
| @@ -26,6 +27,12 @@ export function CachedConsumptionWidget(): React.ReactElement { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <StatCard title="Cached consumption" value={val?.toString() ?? "Loading"} /> |     <StatCard | ||||||
|  |       title={ | ||||||
|  |         ServerApi.Config.dashboard_custom_cached_consumption_title ?? | ||||||
|  |         "Cached consumption" | ||||||
|  |       } | ||||||
|  |       value={val?.toString() ?? "Loading"} | ||||||
|  |     /> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import React from "react"; | |||||||
| import { EnergyApi } from "../../api/EnergyApi"; | import { EnergyApi } from "../../api/EnergyApi"; | ||||||
| import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | ||||||
| import StatCard from "../../widgets/StatCard"; | import StatCard from "../../widgets/StatCard"; | ||||||
|  | import { ServerApi } from "../../api/ServerApi"; | ||||||
|  |  | ||||||
| export function CurrConsumptionWidget(): React.ReactElement { | export function CurrConsumptionWidget(): React.ReactElement { | ||||||
|   const snackbar = useSnackbar(); |   const snackbar = useSnackbar(); | ||||||
| @@ -29,7 +30,10 @@ export function CurrConsumptionWidget(): React.ReactElement { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <StatCard |     <StatCard | ||||||
|       title="Current consumption" |       title={ | ||||||
|  |         ServerApi.Config.dashboard_custom_current_consumption_title ?? | ||||||
|  |         "Current consumption" | ||||||
|  |       } | ||||||
|       data={history ?? []} |       data={history ?? []} | ||||||
|       interval="Last day" |       interval="Last day" | ||||||
|       value={val?.toString() ?? "Loading"} |       value={val?.toString() ?? "Loading"} | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import React from "react"; | |||||||
| import { EnergyApi } from "../../api/EnergyApi"; | import { EnergyApi } from "../../api/EnergyApi"; | ||||||
| import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; | ||||||
| import StatCard from "../../widgets/StatCard"; | import StatCard from "../../widgets/StatCard"; | ||||||
|  | import { ServerApi } from "../../api/ServerApi"; | ||||||
|  |  | ||||||
| export function RelayConsumptionWidget(): React.ReactElement { | export function RelayConsumptionWidget(): React.ReactElement { | ||||||
|   const snackbar = useSnackbar(); |   const snackbar = useSnackbar(); | ||||||
| @@ -29,7 +30,10 @@ export function RelayConsumptionWidget(): React.ReactElement { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <StatCard |     <StatCard | ||||||
|       title="Relays consumption" |       title={ | ||||||
|  |         ServerApi.Config.dashboard_custom_relays_consumption_title ?? | ||||||
|  |         "Relays consumption" | ||||||
|  |       } | ||||||
|       data={history ?? []} |       data={history ?? []} | ||||||
|       interval="Last day" |       interval="Last day" | ||||||
|       value={val?.toString() ?? "Loading"} |       value={val?.toString() ?? "Loading"} | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import Typography from "@mui/material/Typography"; | |||||||
| import * as React from "react"; | import * as React from "react"; | ||||||
| import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider"; | import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider"; | ||||||
| import { AuthApi } from "../api/AuthApi"; | import { AuthApi } from "../api/AuthApi"; | ||||||
| import Grid from "@mui/material/Grid2"; | import Grid from "@mui/material/Grid"; | ||||||
|  |  | ||||||
| function Copyright(props: any) { | function Copyright(props: any) { | ||||||
|   return ( |   return ( | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								central_frontend/src/routes/ManagementRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								central_frontend/src/routes/ManagementRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import { Button } from "@mui/material"; | ||||||
|  | import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; | ||||||
|  | import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer"; | ||||||
|  | import { APIClient } from "../api/ApiClient"; | ||||||
|  |  | ||||||
|  | export function ManagementRoute(): React.ReactElement { | ||||||
|  |   const confirm = useConfirm(); | ||||||
|  |  | ||||||
|  |   const downloadBackup = async () => { | ||||||
|  |     try { | ||||||
|  |       if ( | ||||||
|  |         !(await confirm( | ||||||
|  |           `Do you really want to download a copy of the storage? It will contain sensitive information!` | ||||||
|  |         )) | ||||||
|  |       ) | ||||||
|  |         return; | ||||||
|  |  | ||||||
|  |       location.href = APIClient.backendURL() + "/management/download_storage"; | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(`Failed to donwload a backup of the storage! Error: ${e}`); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SolarEnergyRouteContainer label="Management"> | ||||||
|  |       <Button variant="outlined" onClick={downloadBackup}> | ||||||
|  |         Download a backup of storage | ||||||
|  |       </Button> | ||||||
|  |     </SolarEnergyRouteContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { | import { | ||||||
|   mdiChip, |   mdiChip, | ||||||
|  |   mdiCog, | ||||||
|   mdiElectricSwitch, |   mdiElectricSwitch, | ||||||
|   mdiHome, |   mdiHome, | ||||||
|   mdiMonitorArrowDown, |   mdiMonitorArrowDown, | ||||||
| @@ -12,9 +13,11 @@ import { | |||||||
|   ListItemButton, |   ListItemButton, | ||||||
|   ListItemIcon, |   ListItemIcon, | ||||||
|   ListItemText, |   ListItemText, | ||||||
|  |   Typography, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { useLocation } from "react-router-dom"; | import { useLocation } from "react-router-dom"; | ||||||
| import { RouterLink } from "./RouterLink"; | import { RouterLink } from "./RouterLink"; | ||||||
|  | import { ServerApi } from "../api/ServerApi"; | ||||||
|  |  | ||||||
| export function SolarEnergyNavList(): React.ReactElement { | export function SolarEnergyNavList(): React.ReactElement { | ||||||
|   return ( |   return ( | ||||||
| @@ -52,6 +55,18 @@ export function SolarEnergyNavList(): React.ReactElement { | |||||||
|         uri="/logs" |         uri="/logs" | ||||||
|         icon={<Icon path={mdiNotebookMultiple} size={1} />} |         icon={<Icon path={mdiNotebookMultiple} size={1} />} | ||||||
|       /> |       /> | ||||||
|  |       <NavLink | ||||||
|  |         label="Management" | ||||||
|  |         uri="/management" | ||||||
|  |         icon={<Icon path={mdiCog} size={1} />} | ||||||
|  |       /> | ||||||
|  |       <Typography | ||||||
|  |         variant="caption" | ||||||
|  |         component="div" | ||||||
|  |         style={{ textAlign: "center", width: "100%", marginTop: "30px" }} | ||||||
|  |       > | ||||||
|  |         Version {ServerApi.Config.backend_version} | ||||||
|  |       </Typography> | ||||||
|     </List> |     </List> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ export function timeDiff(a: number, b: number): string { | |||||||
|   diff = Math.floor(diff / 60); |   diff = Math.floor(diff / 60); | ||||||
|  |  | ||||||
|   if (diff === 1) return "1 minute"; |   if (diff === 1) return "1 minute"; | ||||||
|   if (diff < 24) { |   if (diff < 60) { | ||||||
|     return `${diff} minutes`; |     return `${diff} minutes`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "composite": true, |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||||
|     "target": "ES2020", |     "target": "ES2020", | ||||||
|     "useDefineForClassFields": true, |     "useDefineForClassFields": true, | ||||||
| @@ -11,7 +10,6 @@ | |||||||
|     /* Bundler mode */ |     /* Bundler mode */ | ||||||
|     "moduleResolution": "bundler", |     "moduleResolution": "bundler", | ||||||
|     "allowImportingTsExtensions": true, |     "allowImportingTsExtensions": true, | ||||||
|     "resolveJsonModule": true, |  | ||||||
|     "isolatedModules": true, |     "isolatedModules": true, | ||||||
|     "moduleDetection": "force", |     "moduleDetection": "force", | ||||||
|     "noEmit": true, |     "noEmit": true, | ||||||
| @@ -21,7 +19,8 @@ | |||||||
|     "strict": true, |     "strict": true, | ||||||
|     "noUnusedLocals": true, |     "noUnusedLocals": true, | ||||||
|     "noUnusedParameters": true, |     "noUnusedParameters": true, | ||||||
|     "noFallthroughCasesInSwitch": true |     "noFallthroughCasesInSwitch": true, | ||||||
|  |     "noUncheckedSideEffectImports": true | ||||||
|   }, |   }, | ||||||
|   "include": ["src"] |   "include": ["src"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,7 @@ | |||||||
| { | { | ||||||
|   "files": [], |   "files": [], | ||||||
|   "references": [ |   "references": [ | ||||||
|     { |     { "path": "./tsconfig.app.json" }, | ||||||
|       "path": "./tsconfig.app.json" |     { "path": "./tsconfig.node.json" } | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "path": "./tsconfig.node.json" |  | ||||||
|     } |  | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,24 @@ | |||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "composite": true, |  | ||||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||||
|     "skipLibCheck": true, |     "target": "ES2022", | ||||||
|  |     "lib": ["ES2023"], | ||||||
|     "module": "ESNext", |     "module": "ESNext", | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |  | ||||||
|  |     /* Bundler mode */ | ||||||
|     "moduleResolution": "bundler", |     "moduleResolution": "bundler", | ||||||
|     "allowSyntheticDefaultImports": true, |     "allowImportingTsExtensions": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "moduleDetection": "force", | ||||||
|  |     "noEmit": true, | ||||||
|  |  | ||||||
|  |     /* Linting */ | ||||||
|     "strict": true, |     "strict": true, | ||||||
|     "noEmit": true |     "noUnusedLocals": true, | ||||||
|  |     "noUnusedParameters": true, | ||||||
|  |     "noFallthroughCasesInSwitch": true, | ||||||
|  |     "noUncheckedSideEffectImports": true | ||||||
|   }, |   }, | ||||||
|   "include": ["vite.config.ts"] |   "include": ["vite.config.ts"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { defineConfig } from 'vite' | import { defineConfig } from 'vite' | ||||||
| import react from '@vitejs/plugin-react' | import react from '@vitejs/plugin-react' | ||||||
|  |  | ||||||
| // https://vitejs.dev/config/ | // https://vite.dev/config/ | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [react()], |   plugins: [react()], | ||||||
| }) | }) | ||||||
|   | |||||||
							
								
								
									
										2431
									
								
								custom_consumption/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2431
									
								
								custom_consumption/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| [package] | [package] | ||||||
| name = "custom_consumption" | name = "custom_consumption" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| edition = "2021" | edition = "2024" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| env_logger = "0.11.5" | env_logger = "0.11.7" | ||||||
| log = "0.4.22" | log = "0.4.27" | ||||||
| clap = { version = "4.5.18", features = ["derive", "env"] } | clap = { version = "4.5.34", features = ["derive", "env"] } | ||||||
| egui = "0.28.1" | egui = "0.31.1" | ||||||
| eframe = "0.28.1" | eframe = "0.31.1" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.5.0" | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| use std::time::{SystemTime, UNIX_EPOCH}; | use std::time::{SystemTime, UNIX_EPOCH}; | ||||||
|  |  | ||||||
| /// Get the current time since epoch | /// Get the current time since epoch | ||||||
|  |  | ||||||
| pub fn time_millis() -> u128 { | pub fn time_millis() -> u128 { | ||||||
|     SystemTime::now() |     SystemTime::now() | ||||||
|         .duration_since(UNIX_EPOCH) |         .duration_since(UNIX_EPOCH) | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| # Configure project for production | # Configure project for production | ||||||
|  |  | ||||||
|  | Note: This guide assumes that you use the default hostname, `central.internal` as hostname for your central system. | ||||||
|  |  | ||||||
| ## Create production build | ## Create production build | ||||||
|  |  | ||||||
| ### Central | ### Central | ||||||
| @@ -44,6 +46,70 @@ The OTA update is then located in `build/main.bin` | |||||||
| * A server running a recent Linux (Debian / Ubuntu preferred) with `central` as hostname | * A server running a recent Linux (Debian / Ubuntu preferred) with `central` as hostname | ||||||
| * DHCP configured on the network | * DHCP configured on the network | ||||||
|  |  | ||||||
|  | ## Configure DNS server | ||||||
|  |  | ||||||
|  | If you need to setup a DNS server / proxy to point `central.internal` to the central server IP, you can follow this guide. | ||||||
|  |  | ||||||
|  | ### Retrieve DNS server binary | ||||||
|  | Use [DNSProxy](https://gitlab.com/pierre42100/dnsproxy) as DNS server. Get and compile the sources: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | git clone https://gitlab.com/pierre42100/dnsproxy | ||||||
|  | cd dnsproxy | ||||||
|  | cargo build --release | ||||||
|  | scp target/release/dns_proxy USER@CENTRAL_IP:/home/USER | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Then, on the target server, install the binary to its final destination: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | sudo mv dns_proxy /usr/local/bin/ | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Configure DNS server | ||||||
|  | Configure the server as a service `/etc/systemd/system/dns.service`: | ||||||
|  |  | ||||||
|  | ```conf | ||||||
|  | [Unit] | ||||||
|  | Description=DNS server | ||||||
|  | After=syslog.target | ||||||
|  | After=network.target | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | RestartSec=2s | ||||||
|  | Type=simple | ||||||
|  | User=root | ||||||
|  | Group=root | ||||||
|  | WorkingDirectory=/tmp | ||||||
|  | ExecStart=/usr/local/bin/dns_proxy -l "CENTRAL_IP:53" -c "central.internal. A CENTRAL_IP" | ||||||
|  | Restart=always | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Enable and start the new service: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | sudo systemctl enable dns | ||||||
|  | sudo systemctl start dns | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Check that it works correctly: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | dig central.internal. @CENTRAL_IP | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | You should get an entry like this if it works: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | ;; ANSWER SECTION: | ||||||
|  | central.internal.	0	IN	A	CENTRAL_IP | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Then, in your DHCP service, define the central as the DNS server. | ||||||
|  |  | ||||||
| ## Configure server | ## Configure server | ||||||
|  |  | ||||||
| ### Create a user dedicated to the central | ### Create a user dedicated to the central | ||||||
| @@ -82,7 +148,7 @@ COOKIE_SECURE=true | |||||||
| LISTEN_ADDRESS=0.0.0.0:443 | LISTEN_ADDRESS=0.0.0.0:443 | ||||||
| ADMIN_USERNAME=admin | ADMIN_USERNAME=admin | ||||||
| ADMIN_PASSWORD=FIXME | ADMIN_PASSWORD=FIXME | ||||||
| HOSTNAME=central.local | HOSTNAME=central.internal | ||||||
| STORAGE=/home/central/storage | STORAGE=/home/central/storage | ||||||
| FRONIUS_ORIG=http://10.0.0.10 | FRONIUS_ORIG=http://10.0.0.10 | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -2,6 +2,11 @@ | |||||||
|  |  | ||||||
| ESP32 client device, using `W32-ETH01` device | ESP32 client device, using `W32-ETH01` device | ||||||
|  |  | ||||||
|  | ## Pins for relays | ||||||
|  | The pins are the following (in the order of definition): 4, 14, 15, 2 | ||||||
|  |  | ||||||
|  | **WARNING!** The Pin 2 MUST be disconnect to reflash the card! | ||||||
|  |  | ||||||
| ## Some commands | ## Some commands | ||||||
|  |  | ||||||
| Create a new firmware build: | Create a new firmware build: | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
| /** | /** | ||||||
|  * Backend unsecure API URL |  * Backend unsecure API URL | ||||||
|  */ |  */ | ||||||
| #define BACKEND_UNSECURE_URL "http://devweb.internal:8080" | #define BACKEND_UNSECURE_URL "http://central.internal:8080" | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Device name len |  * Device name len | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ static const char *TAG = "relays"; | |||||||
| /** | /** | ||||||
|  * Device relays GPIO ids |  * Device relays GPIO ids | ||||||
|  */ |  */ | ||||||
| static int DEVICE_GPIO_IDS[3] = {4, 14, 15}; | static int DEVICE_GPIO_IDS[4] = {4, 14, 15, 2}; | ||||||
|  |  | ||||||
| int relays_count() | int relays_count() | ||||||
| { | { | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| 1.0.0 | 1.0.2 | ||||||
| @@ -1,9 +1,3 @@ | |||||||
| { | { | ||||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", |   "extends": ["local>renovate/presets"] | ||||||
|   "packageRules": [ |  | ||||||
|     { |  | ||||||
|       "matchUpdateTypes": ["major", "minor", "patch"], |  | ||||||
|       "automerge": true |  | ||||||
|     } |  | ||||||
|   ] |  | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user