Compare commits
251 Commits
1.0.1
...
568e0a4076
Author | SHA1 | Date | |
---|---|---|---|
568e0a4076 | |||
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 |
@ -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.173"
|
||||||
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 {
|
||||||
@ -49,7 +49,7 @@ pub enum ConsumptionBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Solar system central backend
|
/// Solar system central backend
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug, serde::Serialize)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
/// Read arguments from env file
|
/// Read arguments from env file
|
||||||
@ -110,6 +110,18 @@ pub struct AppConfig {
|
|||||||
#[arg(short('f'), long, env, default_value_t = 5)]
|
#[arg(short('f'), long, env, default_value_t = 5)]
|
||||||
pub energy_fetch_interval: u64,
|
pub energy_fetch_interval: u64,
|
||||||
|
|
||||||
|
/// Custom current consumption title in dashboard
|
||||||
|
#[arg(long, env)]
|
||||||
|
pub dashboard_custom_current_consumption_title: Option<String>,
|
||||||
|
|
||||||
|
/// Custom relays consumption title in dashboard
|
||||||
|
#[arg(long, env)]
|
||||||
|
pub dashboard_custom_relays_consumption_title: Option<String>,
|
||||||
|
|
||||||
|
/// Custom cached consumption title in dashboard
|
||||||
|
#[arg(long, env)]
|
||||||
|
pub dashboard_custom_cached_consumption_title: Option<String>,
|
||||||
|
|
||||||
/// Consumption backend provider
|
/// Consumption backend provider
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub consumption_backend: Option<ConsumptionBackend>,
|
pub consumption_backend: Option<ConsumptionBackend>,
|
||||||
|
@ -13,10 +13,10 @@ use openssl::pkey::{PKey, Private};
|
|||||||
use openssl::x509::extension::{
|
use openssl::x509::extension::{
|
||||||
BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier,
|
BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier,
|
||||||
};
|
};
|
||||||
use openssl::x509::{CrlStatus, X509Crl, X509Name, X509NameBuilder, X509Req, X509};
|
use openssl::x509::{CrlStatus, X509, X509Crl, X509Name, X509NameBuilder, X509Req};
|
||||||
use openssl_sys::{
|
use openssl_sys::{
|
||||||
X509_CRL_add0_revoked, X509_CRL_new, X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate,
|
X509_CRL_add0_revoked, X509_CRL_new, X509_CRL_set_issuer_name, X509_CRL_set_version,
|
||||||
X509_CRL_set_issuer_name, X509_CRL_set_version, X509_CRL_sign, X509_REVOKED_dup,
|
X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate, X509_CRL_sign, X509_REVOKED_dup,
|
||||||
X509_REVOKED_new, X509_REVOKED_set_revocationDate, X509_REVOKED_set_serialNumber,
|
X509_REVOKED_new, X509_REVOKED_set_revocationDate, X509_REVOKED_set_serialNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ enum GenCertificatSubjectReq<'a> {
|
|||||||
CSR { csr: &'a X509Req },
|
CSR { csr: &'a X509Req },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Default for GenCertificatSubjectReq<'a> {
|
impl Default for GenCertificatSubjectReq<'_> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Subject { cn: "" }
|
Self::Subject { cn: "" }
|
||||||
}
|
}
|
||||||
|
@ -325,9 +325,11 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
dep_cycle_1.depends_on = vec![dep_cycle_3.id];
|
dep_cycle_1.depends_on = vec![dep_cycle_3.id];
|
||||||
assert!(dep_cycle_1
|
assert!(
|
||||||
.error(&[dep_cycle_2.clone(), dep_cycle_3.clone()])
|
dep_cycle_1
|
||||||
.is_some());
|
.error(&[dep_cycle_2.clone(), dep_cycle_3.clone()])
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
dep_cycle_1.depends_on = vec![];
|
dep_cycle_1.depends_on = vec![];
|
||||||
assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).is_none());
|
assert!(dep_cycle_1.error(&[dep_cycle_2, dep_cycle_3]).is_none());
|
||||||
@ -351,21 +353,29 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(target_relay
|
assert!(
|
||||||
.error(&[other_dep.clone(), second_dep.clone()])
|
target_relay
|
||||||
.is_some());
|
.error(&[other_dep.clone(), second_dep.clone()])
|
||||||
assert!(target_relay
|
.is_some()
|
||||||
.error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()])
|
);
|
||||||
.is_some());
|
assert!(
|
||||||
|
target_relay
|
||||||
|
.error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()])
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
second_dep.conflicts_with = vec![];
|
second_dep.conflicts_with = vec![];
|
||||||
|
|
||||||
assert!(target_relay
|
assert!(
|
||||||
.error(&[other_dep.clone(), second_dep.clone()])
|
target_relay
|
||||||
.is_none());
|
.error(&[other_dep.clone(), second_dep.clone()])
|
||||||
assert!(target_relay
|
.is_none()
|
||||||
.error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()])
|
);
|
||||||
.is_none());
|
assert!(
|
||||||
|
target_relay
|
||||||
|
.error(&[other_dep.clone(), second_dep.clone(), target_relay.clone()])
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
// self loop
|
// self loop
|
||||||
let mut self_loop = DeviceRelay {
|
let mut self_loop = DeviceRelay {
|
||||||
|
@ -4,7 +4,7 @@ use crate::devices::device::{
|
|||||||
Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID,
|
Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID,
|
||||||
};
|
};
|
||||||
use crate::utils::time_utils::time_secs;
|
use crate::utils::time_utils::time_secs;
|
||||||
use openssl::x509::{X509Req, X509};
|
use openssl::x509::{X509, X509Req};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::app_config::{AppConfig, ConsumptionBackend};
|
use crate::app_config::{AppConfig, ConsumptionBackend};
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{Rng, rng};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ pub async fn get_curr_consumption() -> anyhow::Result<EnergyConsumption> {
|
|||||||
match backend {
|
match backend {
|
||||||
ConsumptionBackend::Constant { value } => Ok(*value),
|
ConsumptionBackend::Constant { value } => Ok(*value),
|
||||||
|
|
||||||
ConsumptionBackend::Random { min, max } => Ok(thread_rng().gen_range(*min..*max)),
|
ConsumptionBackend::Random { min, max } => Ok(rng().random_range(*min..*max)),
|
||||||
|
|
||||||
ConsumptionBackend::File { path } => {
|
ConsumptionBackend::File { path } => {
|
||||||
let path = Path::new(path);
|
let path = Path::new(path);
|
||||||
@ -71,7 +71,11 @@ pub async fn get_curr_consumption() -> anyhow::Result<EnergyConsumption> {
|
|||||||
let response = match curl {
|
let response = match curl {
|
||||||
false => reqwest::get(url).await?.json::<FroniusResponse>().await?,
|
false => reqwest::get(url).await?.json::<FroniusResponse>().await?,
|
||||||
true => {
|
true => {
|
||||||
let res = std::process::Command::new("curl").arg(url).output()?;
|
let res = std::process::Command::new("curl")
|
||||||
|
.arg("--connect-timeout")
|
||||||
|
.arg("1.5")
|
||||||
|
.arg(url)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
if !res.status.success() {
|
if !res.status.success() {
|
||||||
return Err(ConsumptionError::CurlReqFailed.into());
|
return Err(ConsumptionError::CurlReqFailed.into());
|
||||||
|
@ -25,7 +25,14 @@ impl EnergyActor {
|
|||||||
pub async fn new() -> anyhow::Result<Self> {
|
pub async fn new() -> anyhow::Result<Self> {
|
||||||
let consumption_cache_size =
|
let consumption_cache_size =
|
||||||
AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval;
|
AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval;
|
||||||
let curr_consumption = consumption::get_curr_consumption().await?;
|
let curr_consumption = match consumption::get_curr_consumption().await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to fetch consumption, using default value! {e}");
|
||||||
|
constants::FALLBACK_PRODUCTION_VALUE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log::info!("Initial consumption value: {curr_consumption}");
|
||||||
let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize);
|
let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize);
|
||||||
consumption_cache.add_value(curr_consumption);
|
consumption_cache.add_value(curr_consumption);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use prettytable::{row, Table};
|
use prettytable::{Table, row};
|
||||||
|
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID};
|
use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID};
|
||||||
@ -289,7 +289,11 @@ impl EnergyEngine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Forcefully turn on relay {} to catch up running constraints (only {}s this day)", r.name, total_runtime);
|
log::info!(
|
||||||
|
"Forcefully turn on relay {} to catch up running constraints (only {}s this day)",
|
||||||
|
r.name,
|
||||||
|
total_runtime
|
||||||
|
);
|
||||||
new_relays_state.get_mut(&r.id).unwrap().on = true;
|
new_relays_state.get_mut(&r.id).unwrap().on = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use crate::devices::device::{DeviceRelay, DeviceRelayID};
|
use crate::devices::device::{DeviceRelay, DeviceRelayID};
|
||||||
use crate::utils::files_utils;
|
use crate::utils::files_utils;
|
||||||
use crate::utils::time_utils::{day_number, time_start_of_day};
|
use crate::utils::time_utils::{day_number, time_secs, time_start_of_day};
|
||||||
|
|
||||||
const TIME_INTERVAL: usize = 30;
|
const TIME_INTERVAL: usize = 30;
|
||||||
|
|
||||||
@ -128,15 +128,26 @@ pub fn relay_total_runtime_adjusted(relay: &DeviceRelay) -> usize {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let time_start_day = time_start_of_day().unwrap_or(1726696800);
|
let time_start_day = time_start_of_day().unwrap_or(1726696800);
|
||||||
let start_time = time_start_day + reset_time as u64;
|
|
||||||
let end_time = time_start_day + 3600 * 24 + reset_time as u64;
|
// Check if we have reached reset_time today yet or not
|
||||||
relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24)
|
if time_start_day + reset_time as u64 <= time_secs() {
|
||||||
|
let start_time = time_start_day + reset_time as u64;
|
||||||
|
let end_time = time_start_day + 3600 * 24 + reset_time as u64;
|
||||||
|
relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24)
|
||||||
|
}
|
||||||
|
// If we have not reached reset time yet, we need to focus on previous day
|
||||||
|
else {
|
||||||
|
let time_start_yesterday = time_start_day - 3600 * 24;
|
||||||
|
let start_time = time_start_yesterday + reset_time as u64;
|
||||||
|
let end_time = time_start_day + reset_time as u64;
|
||||||
|
relay_total_runtime(relay.id, start_time, end_time).unwrap_or(3600 * 24)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::devices::device::DeviceRelayID;
|
use crate::devices::device::DeviceRelayID;
|
||||||
use crate::energy::relay_state_history::{relay_total_runtime, RelayStateHistory};
|
use crate::energy::relay_state_history::{RelayStateHistory, relay_total_runtime};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_relay_state_history() {
|
fn test_relay_state_history() {
|
||||||
|
@ -35,7 +35,7 @@ pub fn save_log(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
file.unlock()?;
|
fs4::fs_std::FileExt::unlock(&file)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use central_backend::energy::energy_actor::EnergyActor;
|
|||||||
use central_backend::server::servers;
|
use central_backend::server::servers;
|
||||||
use central_backend::utils::files_utils::create_directory_if_missing;
|
use central_backend::utils::files_utils::create_directory_if_missing;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use tokio_schedule::{every, Job};
|
use tokio_schedule::{Job, every};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use std::future::{ready, Ready};
|
use std::future::{Ready, ready};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
@ -7,8 +7,8 @@ use crate::constants;
|
|||||||
use actix_web::body::EitherBody;
|
use actix_web::body::EitherBody;
|
||||||
use actix_web::dev::Payload;
|
use actix_web::dev::Payload;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
|
||||||
Error, FromRequest, HttpResponse,
|
Error, FromRequest, HttpResponse,
|
||||||
|
dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready},
|
||||||
};
|
};
|
||||||
use futures_util::future::LocalBoxFuture;
|
use futures_util::future::LocalBoxFuture;
|
||||||
|
|
||||||
|
@ -1,9 +1,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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
3758
central_frontend/package-lock.json
generated
3758
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.32.1",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@typescript-eslint/parser": "^8.34.1",
|
||||||
"eslint": "^8.57.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint": "^9.26.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,6 +46,70 @@ The OTA update is then located in `build/main.bin`
|
|||||||
* A server running a recent Linux (Debian / Ubuntu preferred) with `central` as hostname
|
* A server running a recent Linux (Debian / Ubuntu preferred) with `central` as hostname
|
||||||
* DHCP configured on the network
|
* DHCP configured on the network
|
||||||
|
|
||||||
|
## Configure DNS server
|
||||||
|
|
||||||
|
If you need to setup a DNS server / proxy to point `central.internal` to the central server IP, you can follow this guide.
|
||||||
|
|
||||||
|
### Retrieve DNS server binary
|
||||||
|
Use [DNSProxy](https://gitlab.com/pierre42100/dnsproxy) as DNS server. Get and compile the sources:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitlab.com/pierre42100/dnsproxy
|
||||||
|
cd dnsproxy
|
||||||
|
cargo build --release
|
||||||
|
scp target/release/dns_proxy USER@CENTRAL_IP:/home/USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, on the target server, install the binary to its final destination:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mv dns_proxy /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure DNS server
|
||||||
|
Configure the server as a service `/etc/systemd/system/dns.service`:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
[Unit]
|
||||||
|
Description=DNS server
|
||||||
|
After=syslog.target
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
RestartSec=2s
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
WorkingDirectory=/tmp
|
||||||
|
ExecStart=/usr/local/bin/dns_proxy -l "CENTRAL_IP:53" -c "central.internal. A CENTRAL_IP"
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable and start the new service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable dns
|
||||||
|
sudo systemctl start dns
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that it works correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dig central.internal. @CENTRAL_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
You should get an entry like this if it works:
|
||||||
|
|
||||||
|
```
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
central.internal. 0 IN A CENTRAL_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in your DHCP service, define the central as the DNS server.
|
||||||
|
|
||||||
## Configure server
|
## Configure server
|
||||||
|
|
||||||
### Create a user dedicated to the central
|
### Create a user dedicated to the central
|
||||||
@ -82,7 +148,7 @@ COOKIE_SECURE=true
|
|||||||
LISTEN_ADDRESS=0.0.0.0:443
|
LISTEN_ADDRESS=0.0.0.0:443
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=FIXME
|
ADMIN_PASSWORD=FIXME
|
||||||
HOSTNAME=central.local
|
HOSTNAME=central.internal
|
||||||
STORAGE=/home/central/storage
|
STORAGE=/home/central/storage
|
||||||
FRONIUS_ORIG=http://10.0.0.10
|
FRONIUS_ORIG=http://10.0.0.10
|
||||||
```
|
```
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
ESP32 client device, using `W32-ETH01` device
|
ESP32 client device, using `W32-ETH01` device
|
||||||
|
|
||||||
|
## Pins for relays
|
||||||
|
The pins are the following (in the order of definition): 4, 14, 15, 2
|
||||||
|
|
||||||
|
**WARNING!** The Pin 2 MUST be disconnect to reflash the card!
|
||||||
|
|
||||||
## Some commands
|
## Some commands
|
||||||
|
|
||||||
Create a new firmware build:
|
Create a new firmware build:
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
/**
|
/**
|
||||||
* Backend unsecure API URL
|
* Backend unsecure API URL
|
||||||
*/
|
*/
|
||||||
#define BACKEND_UNSECURE_URL "http://devweb.internal:8080"
|
#define BACKEND_UNSECURE_URL "http://central.internal:8080"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device name len
|
* Device name len
|
||||||
|
@ -9,7 +9,7 @@ static const char *TAG = "relays";
|
|||||||
/**
|
/**
|
||||||
* Device relays GPIO ids
|
* Device relays GPIO ids
|
||||||
*/
|
*/
|
||||||
static int DEVICE_GPIO_IDS[3] = {4, 14, 15};
|
static int DEVICE_GPIO_IDS[4] = {4, 14, 15, 2};
|
||||||
|
|
||||||
int relays_count()
|
int relays_count()
|
||||||
{
|
{
|
||||||
|
@ -1 +1 @@
|
|||||||
1.0.0
|
1.0.2
|
@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"extends": ["local>renovate/presets"]
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["major", "minor", "patch"],
|
|
||||||
"automerge": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user