Compare commits
	
		
			260 Commits
		
	
	
		
			1.0.0
			...
			818433a7ce
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 818433a7ce | |||
| 593719e373 | |||
| 13a9905281 | |||
| 55b354228f | |||
| d751ddc671 | |||
| c5061fdb4d | |||
| 7adbafb831 | |||
| 02397d10f0 | |||
| e3ae017279 | |||
| 30b5155a4d | |||
| 0d04f5d7b2 | |||
| a40dff2820 | |||
| 6fbec9f0cd | |||
| 055e512f77 | |||
| ee769f043f | |||
| 926b265f91 | |||
| b115ba9307 | |||
| 8ada40a5ee | |||
| 100e42ec6d | |||
| cab51c9623 | |||
| 76df0ecf3e | |||
| 85cb7d6a75 | |||
| 69a51e11d3 | |||
| 0ff1d48b90 | |||
| 0d478a10f7 | |||
| a8e2f2d7bf | |||
| e961ea0911 | |||
| 1c1eb53b6e | |||
| 1a2badc138 | |||
| 9323a4a3f5 | |||
| 35cfc73c9d | |||
| dad54c638b | |||
| 4f5be4d08c | |||
| b89aee2dcc | |||
| bbe2c3ebc5 | |||
| 62037db6e3 | |||
| 0bf3bdbaea | |||
| f65df5f22a | |||
| 406a920d7e | |||
| 889ba9b85f | |||
| 12606ba336 | |||
| cb2e17581a | |||
| 4dd5fb4e55 | |||
| 0a162e4a78 | |||
| ba45faf017 | |||
| c4dedb946f | |||
| 5004194567 | |||
| 768f8fc112 | |||
| adf1477c4b | |||
| 7474e25209 | |||
| f33c408c67 | |||
| ccd4125500 | |||
| 9825f2628b | |||
| 9e5797e4ca | |||
| fb562f908c | |||
| ffb00ee668 | |||
| 3ad64e55b8 | |||
| 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 | |||
| 5aaad54de3 | |||
| b3edfb05d9 | |||
| 45029f24cc | |||
| ec594c0e4d | |||
| fdfbdf093f | 
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1447
									
								
								central_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1447
									
								
								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.8"
 | 
				
			||||||
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.40", features = ["derive", "env"] }
 | 
				
			||||||
anyhow = "1.0.89"
 | 
					anyhow = "1.0.98"
 | 
				
			||||||
thiserror = "1.0.64"
 | 
					thiserror = "2.0.12"
 | 
				
			||||||
openssl = { version = "0.10.66" }
 | 
					openssl = { version = "0.10.73" }
 | 
				
			||||||
openssl-sys = "0.9.102"
 | 
					openssl-sys = "0.9.109"
 | 
				
			||||||
libc = "0.2.159"
 | 
					libc = "0.2.174"
 | 
				
			||||||
foreign-types-shared = "0.1.1"
 | 
					foreign-types-shared = "0.1.1"
 | 
				
			||||||
asn1 = "0.17"
 | 
					asn1 = "0.21.3"
 | 
				
			||||||
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.219", features = ["derive"] }
 | 
				
			||||||
reqwest = { version = "0.12.7", features = ["json"] }
 | 
					reqwest = { version = "0.12.20", features = ["json"] }
 | 
				
			||||||
serde_json = "1.0.128"
 | 
					serde_json = "1.0.140"
 | 
				
			||||||
rand = "0.8.5"
 | 
					rand = "0.9.1"
 | 
				
			||||||
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.16.0", features = ["v4", "serde"] }
 | 
				
			||||||
semver = { version = "1.0.23", features = ["serde"] }
 | 
					semver = { version = "1.0.26", features = ["serde"] }
 | 
				
			||||||
lazy-regex = "3.3.0"
 | 
					lazy-regex = "3.4.1"
 | 
				
			||||||
tokio = { version = "1.40.0", features = ["full"] }
 | 
					tokio = { version = "1.44.2", 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.41"
 | 
				
			||||||
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 {
 | 
				
			||||||
@@ -39,13 +39,17 @@ pub enum ConsumptionBackend {
 | 
				
			|||||||
    /// Fronius inverter consumption
 | 
					    /// Fronius inverter consumption
 | 
				
			||||||
    Fronius {
 | 
					    Fronius {
 | 
				
			||||||
        /// The origin of the domain where the webserver of the Fronius Symo can be reached
 | 
					        /// The origin of the domain where the webserver of the Fronius Symo can be reached
 | 
				
			||||||
        #[clap(short, long, env = "FRONIUS_ORIG")]
 | 
					        #[clap(short, long, env)]
 | 
				
			||||||
        origin: String,
 | 
					        fronius_orig: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// Use cURL instead of reqwest to perform request
 | 
				
			||||||
 | 
					        #[clap(short, long)]
 | 
				
			||||||
 | 
					        curl: bool,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// 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
 | 
				
			||||||
@@ -106,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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,6 +9,8 @@ pub enum ConsumptionError {
 | 
				
			|||||||
    NonExistentFile,
 | 
					    NonExistentFile,
 | 
				
			||||||
    #[error("The file that should contain the consumption has an invalid content!")]
 | 
					    #[error("The file that should contain the consumption has an invalid content!")]
 | 
				
			||||||
    FileInvalidContent(#[source] ParseIntError),
 | 
					    FileInvalidContent(#[source] ParseIntError),
 | 
				
			||||||
 | 
					    #[error("Failed to execute cURL request!")]
 | 
				
			||||||
 | 
					    CurlReqFailed,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub type EnergyConsumption = i32;
 | 
					pub type EnergyConsumption = i32;
 | 
				
			||||||
@@ -47,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);
 | 
				
			||||||
@@ -63,9 +65,25 @@ pub async fn get_curr_consumption() -> anyhow::Result<EnergyConsumption> {
 | 
				
			|||||||
                .map_err(ConsumptionError::FileInvalidContent)?)
 | 
					                .map_err(ConsumptionError::FileInvalidContent)?)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ConsumptionBackend::Fronius { origin } => {
 | 
					        ConsumptionBackend::Fronius { fronius_orig, curl } => {
 | 
				
			||||||
            let url = format!("{origin}/solar_api/v1/GetPowerFlowRealtimeData.fcgi");
 | 
					            let url = format!("{fronius_orig}/solar_api/v1/GetPowerFlowRealtimeData.fcgi");
 | 
				
			||||||
            let response = reqwest::get(url).await?.json::<FroniusResponse>().await?;
 | 
					
 | 
				
			||||||
 | 
					            let response = match curl {
 | 
				
			||||||
 | 
					                false => reqwest::get(url).await?.json::<FroniusResponse>().await?,
 | 
				
			||||||
 | 
					                true => {
 | 
				
			||||||
 | 
					                    let res = std::process::Command::new("curl")
 | 
				
			||||||
 | 
					                        .arg("--connect-timeout")
 | 
				
			||||||
 | 
					                        .arg("1.5")
 | 
				
			||||||
 | 
					                        .arg(url)
 | 
				
			||||||
 | 
					                        .output()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if !res.status.success() {
 | 
				
			||||||
 | 
					                        return Err(ConsumptionError::CurlReqFailed.into());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    serde_json::from_slice::<FroniusResponse>(&res.stdout)?
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(response.body.data.site.grid_production as i32)
 | 
					            Ok(response.body.data.site.grid_production as i32)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,9 @@
 | 
				
			|||||||
 | 
					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 zip::result::ZipError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Custom error to ease controller writing
 | 
					/// Custom error to ease controller writing
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
@@ -51,7 +51,7 @@ impl From<serde_json::Error> for HttpErr {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl From<Box<dyn Error>> for HttpErr {
 | 
					impl From<Box<dyn Error>> for HttpErr {
 | 
				
			||||||
    fn from(value: Box<dyn Error>) -> Self {
 | 
					    fn from(value: Box<dyn Error>) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,31 +81,43 @@ impl From<reqwest::header::ToStrError> for HttpErr {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl From<actix_web::Error> for HttpErr {
 | 
					impl From<actix_web::Error> for HttpErr {
 | 
				
			||||||
    fn from(value: actix_web::Error) -> Self {
 | 
					    fn from(value: actix_web::Error) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<actix::MailboxError> for HttpErr {
 | 
					impl From<actix::MailboxError> for HttpErr {
 | 
				
			||||||
    fn from(value: actix::MailboxError) -> Self {
 | 
					    fn from(value: actix::MailboxError) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<actix_identity::error::GetIdentityError> for HttpErr {
 | 
					impl From<actix_identity::error::GetIdentityError> for HttpErr {
 | 
				
			||||||
    fn from(value: actix_identity::error::GetIdentityError) -> Self {
 | 
					    fn from(value: actix_identity::error::GetIdentityError) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<actix_identity::error::LoginError> for HttpErr {
 | 
					impl From<actix_identity::error::LoginError> for HttpErr {
 | 
				
			||||||
    fn from(value: actix_identity::error::LoginError) -> Self {
 | 
					    fn from(value: actix_identity::error::LoginError) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl From<openssl::error::ErrorStack> for HttpErr {
 | 
					impl From<openssl::error::ErrorStack> for HttpErr {
 | 
				
			||||||
    fn from(value: openssl::error::ErrorStack) -> Self {
 | 
					    fn from(value: openssl::error::ErrorStack) -> Self {
 | 
				
			||||||
        HttpErr::Err(std::io::Error::new(ErrorKind::Other, value.to_string()).into())
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipError> for HttpErr {
 | 
				
			||||||
 | 
					    fn from(value: ZipError) -> Self {
 | 
				
			||||||
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<walkdir::Error> for HttpErr {
 | 
				
			||||||
 | 
					    fn from(value: walkdir::Error) -> Self {
 | 
				
			||||||
 | 
					        HttpErr::Err(std::io::Error::other(value.to_string()).into())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										2957
									
								
								central_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2957
									
								
								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.14.0",
 | 
				
			||||||
    "@fontsource/roboto": "^5.1.0",
 | 
					    "@fontsource/roboto": "^5.2.6",
 | 
				
			||||||
    "@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.2",
 | 
				
			||||||
    "@mui/material": "^6.1.3",
 | 
					    "@mui/material": "^7.0.2",
 | 
				
			||||||
    "@mui/x-charts": "^7.20.0",
 | 
					    "@mui/x-charts": "^7.29.1",
 | 
				
			||||||
    "@mui/x-date-pickers": "^7.20.0",
 | 
					    "@mui/x-date-pickers": "^7.29.4",
 | 
				
			||||||
    "@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.6.2",
 | 
				
			||||||
    "semver": "^7.6.3"
 | 
					    "semver": "^7.7.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "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.34.1",
 | 
				
			||||||
    "@vitejs/plugin-react": "^4.3.2",
 | 
					    "@typescript-eslint/parser": "^8.34.1",
 | 
				
			||||||
    "eslint": "^8.57.1",
 | 
					    "@vitejs/plugin-react": "^4.5.2",
 | 
				
			||||||
    "eslint-plugin-react-hooks": "^5.0.0",
 | 
					    "eslint": "^9.29.0",
 | 
				
			||||||
    "eslint-plugin-react-refresh": "^0.4.12",
 | 
					    "eslint-plugin-react-hooks": "^5.2.0",
 | 
				
			||||||
    "typescript": "^5.6.3",
 | 
					    "eslint-plugin-react-refresh": "^0.4.20",
 | 
				
			||||||
    "vite": "^5.4.8"
 | 
					    "globals": "^16.1.0",
 | 
				
			||||||
 | 
					    "typescript": "^5.8.3",
 | 
				
			||||||
 | 
					    "typescript-eslint": "^8.24.1",
 | 
				
			||||||
 | 
					    "vite": "^6.3.5"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.8"
 | 
				
			||||||
log = "0.4.22"
 | 
					log = "0.4.27"
 | 
				
			||||||
clap = { version = "4.5.18", features = ["derive", "env"] }
 | 
					clap = { version = "4.5.40", 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,5 +46,146 @@ 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
 | 
				
			||||||
TODO
 | 
					
 | 
				
			||||||
 | 
					### Create a user dedicated to the central
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo adduser --disabled-login central
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Install binary
 | 
				
			||||||
 | 
					You can use `scp` to copy the binary to the target server:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					scp central_backend/target/release/central_backend pierre@central:/home/pierre
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Then the executable must be installed system-wide:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo mv central_backend /usr/local/bin/
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Create configuration file
 | 
				
			||||||
 | 
					Create a configuration file in `/home/central/config.yaml`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo touch /home/central/config.yaml
 | 
				
			||||||
 | 
					sudo chown central:central /home/central/config.yaml
 | 
				
			||||||
 | 
					sudo chmod 400 /home/central/config.yaml
 | 
				
			||||||
 | 
					sudo nano /home/central/config.yaml
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Sample configuration:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```conf
 | 
				
			||||||
 | 
					SECRET=RANDOM_VALUE
 | 
				
			||||||
 | 
					COOKIE_SECURE=true
 | 
				
			||||||
 | 
					LISTEN_ADDRESS=0.0.0.0:443
 | 
				
			||||||
 | 
					ADMIN_USERNAME=admin
 | 
				
			||||||
 | 
					ADMIN_PASSWORD=FIXME
 | 
				
			||||||
 | 
					HOSTNAME=central.internal
 | 
				
			||||||
 | 
					STORAGE=/home/central/storage
 | 
				
			||||||
 | 
					FRONIUS_ORIG=http://10.0.0.10
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Test configuration
 | 
				
			||||||
 | 
					Run the following command to check if the configuration is working:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo -u central central_backend -c /home/central/config.yaml fronius -c
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Create systemd unit file
 | 
				
			||||||
 | 
					Once you confirmed the configuration is working, you can configure a system service, in `/etc/systemd/system/central.service`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```conf
 | 
				
			||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=Central backend server
 | 
				
			||||||
 | 
					After=syslog.target
 | 
				
			||||||
 | 
					After=network.target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					RestartSec=2s
 | 
				
			||||||
 | 
					Type=simple
 | 
				
			||||||
 | 
					User=central
 | 
				
			||||||
 | 
					Group=central
 | 
				
			||||||
 | 
					WorkingDirectory=/home/central
 | 
				
			||||||
 | 
					ExecStart=/usr/local/bin/central_backend -c /home/central/config.yaml fronius -c
 | 
				
			||||||
 | 
					Restart=always
 | 
				
			||||||
 | 
					Environment=USER=central 
 | 
				
			||||||
 | 
					HOME=/home/central
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=multi-user.target
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Enable & start service:
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					sudo systemctl enable central
 | 
				
			||||||
 | 
					sudo systemctl start central
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
@@ -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