Merge branch 'master' of ssh://gitea.communiquons.org:52001/pierre/SolarEnergy
This commit is contained in:
commit
6b9d5e9d85
11
Makefile
Normal file
11
Makefile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
DOCKER_TEMP_DIR=temp
|
||||||
|
|
||||||
|
all: frontend backend
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
cd central_frontend && npm run build && cd ..
|
||||||
|
rm -rf central_backend/static
|
||||||
|
mv central_frontend/dist central_backend/static
|
||||||
|
|
||||||
|
backend: frontend
|
||||||
|
cd central_backend && cargo clippy -- -D warnings && cargo build --release
|
1
central_backend/.gitignore
vendored
1
central_backend/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
target
|
target
|
||||||
.idea
|
.idea
|
||||||
storage
|
storage
|
||||||
|
static
|
||||||
|
198
central_backend/Cargo.lock
generated
198
central_backend/Cargo.lock
generated
@ -386,6 +386,21 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.14"
|
version = "0.6.14"
|
||||||
@ -595,14 +610,18 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"openssl",
|
"openssl",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rust-embed",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio_schedule",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -612,6 +631,20 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets 0.52.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -1064,6 +1097,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@ -1218,6 +1257,29 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1381,6 +1443,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@ -1425,6 +1497,25 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.0"
|
version = "0.36.0"
|
||||||
@ -1737,6 +1828,40 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@ -1811,6 +1936,15 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
@ -2093,21 +2227,34 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.38.0"
|
version = "1.38.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"num_cpus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -2154,6 +2301,16 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio_schedule"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c291c554da3518d6ef69c76ea35aabc78f736185a16b6017f6d1c224dac2e0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@ -2213,6 +2370,15 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -2289,6 +2455,16 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -2380,6 +2556,24 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
@ -30,3 +30,7 @@ futures-util = "0.3.30"
|
|||||||
uuid = { version = "1.9.1", features = ["v4", "serde"] }
|
uuid = { version = "1.9.1", features = ["v4", "serde"] }
|
||||||
semver = { version = "1.0.23", features = ["serde"] }
|
semver = { version = "1.0.23", features = ["serde"] }
|
||||||
lazy-regex = "3.1.0"
|
lazy-regex = "3.1.0"
|
||||||
|
tokio = { version = "1.38.1", features = ["full"] }
|
||||||
|
tokio_schedule = "0.3.2"
|
||||||
|
mime_guess = "2.0.5"
|
||||||
|
rust-embed = "8.5.0"
|
@ -18,3 +18,62 @@ pub const MAX_SESSION_DURATION: u64 = 3600 * 24;
|
|||||||
/// List of routes that do not require authentication
|
/// List of routes that do not require authentication
|
||||||
pub const ROUTES_WITHOUT_AUTH: [&str; 2] =
|
pub const ROUTES_WITHOUT_AUTH: [&str; 2] =
|
||||||
["/web_api/server/config", "/web_api/auth/password_auth"];
|
["/web_api/server/config", "/web_api/auth/password_auth"];
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct SizeConstraint {
|
||||||
|
/// Minimal string length
|
||||||
|
min: usize,
|
||||||
|
/// Maximal string length
|
||||||
|
max: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeConstraint {
|
||||||
|
pub fn new(min: usize, max: usize) -> Self {
|
||||||
|
Self { min, max }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self, val: &str) -> bool {
|
||||||
|
let len = val.trim().len();
|
||||||
|
len >= self.min && len <= self.max
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_usize(&self, val: usize) -> bool {
|
||||||
|
val >= self.min && val <= self.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backend static constraints
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct StaticConstraints {
|
||||||
|
/// Device name constraint
|
||||||
|
pub dev_name_len: SizeConstraint,
|
||||||
|
/// Device description constraint
|
||||||
|
pub dev_description_len: SizeConstraint,
|
||||||
|
/// Relay name constraint
|
||||||
|
pub relay_name_len: SizeConstraint,
|
||||||
|
/// Relay priority constraint
|
||||||
|
pub relay_priority: SizeConstraint,
|
||||||
|
/// Relay consumption constraint
|
||||||
|
pub relay_consumption: SizeConstraint,
|
||||||
|
/// Relay minimal uptime
|
||||||
|
pub relay_minimal_uptime: SizeConstraint,
|
||||||
|
/// Relay minimal downtime
|
||||||
|
pub relay_minimal_downtime: SizeConstraint,
|
||||||
|
/// Relay daily minimal uptime
|
||||||
|
pub relay_daily_minimal_runtime: SizeConstraint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StaticConstraints {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
dev_name_len: SizeConstraint::new(1, 50),
|
||||||
|
dev_description_len: SizeConstraint::new(0, 100),
|
||||||
|
relay_name_len: SizeConstraint::new(1, 100),
|
||||||
|
relay_priority: SizeConstraint::new(0, 999999),
|
||||||
|
relay_consumption: SizeConstraint::new(0, 999999),
|
||||||
|
relay_minimal_uptime: SizeConstraint::new(0, 9999999),
|
||||||
|
relay_minimal_downtime: SizeConstraint::new(0, 9999999),
|
||||||
|
relay_daily_minimal_runtime: SizeConstraint::new(0, 3600 * 24),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,10 +13,11 @@ 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::{X509Crl, X509Name, X509NameBuilder, X509Req, X509};
|
use openssl::x509::{CrlStatus, X509Crl, X509Name, X509NameBuilder, X509Req, X509};
|
||||||
use openssl_sys::{
|
use openssl_sys::{
|
||||||
X509_CRL_add0_revoked, X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate,
|
X509_CRL_add0_revoked, X509_CRL_new, X509_CRL_set1_lastUpdate, X509_CRL_set1_nextUpdate,
|
||||||
X509_CRL_set_issuer_name, X509_CRL_set_version, X509_CRL_sign, X509_REVOKED_dup,
|
X509_CRL_set_issuer_name, X509_CRL_set_version, X509_CRL_sign, X509_REVOKED_dup,
|
||||||
|
X509_REVOKED_new, X509_REVOKED_set_revocationDate, X509_REVOKED_set_serialNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
@ -365,7 +366,7 @@ pub fn initialize_server_ca() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize or refresh a CRL
|
/// Initialize or refresh a CRL
|
||||||
fn refresh_crl(d: &CertData) -> anyhow::Result<()> {
|
fn refresh_crl(d: &CertData, new_cert: Option<&X509>) -> anyhow::Result<()> {
|
||||||
let crl_path = d.crl.as_ref().ok_or(PKIError::MissingCRL)?;
|
let crl_path = d.crl.as_ref().ok_or(PKIError::MissingCRL)?;
|
||||||
|
|
||||||
let old_crl = if crl_path.exists() {
|
let old_crl = if crl_path.exists() {
|
||||||
@ -373,7 +374,9 @@ fn refresh_crl(d: &CertData) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Check if revocation is un-needed
|
// Check if revocation is un-needed
|
||||||
let next_update = crl.next_update().ok_or(PKIError::MissingCRLNextUpdate)?;
|
let next_update = crl.next_update().ok_or(PKIError::MissingCRLNextUpdate)?;
|
||||||
if next_update.compare(Asn1Time::days_from_now(0)?.as_ref())? == Ordering::Greater {
|
if next_update.compare(Asn1Time::days_from_now(0)?.as_ref())? == Ordering::Greater
|
||||||
|
&& new_cert.is_none()
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +389,7 @@ fn refresh_crl(d: &CertData) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// based on https://github.com/openssl/openssl/blob/master/crypto/x509/x509_vfy.c
|
// based on https://github.com/openssl/openssl/blob/master/crypto/x509/x509_vfy.c
|
||||||
unsafe {
|
unsafe {
|
||||||
let crl = openssl_sys::X509_CRL_new();
|
let crl = X509_CRL_new();
|
||||||
if crl.is_null() {
|
if crl.is_null() {
|
||||||
return Err(PKIError::GenCRLError("Could not construct CRL!").into());
|
return Err(PKIError::GenCRLError("Could not construct CRL!").into());
|
||||||
}
|
}
|
||||||
@ -420,6 +423,31 @@ fn refresh_crl(d: &CertData) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If requested, add new entry
|
||||||
|
if let Some(new_cert) = new_cert {
|
||||||
|
let entry = X509_REVOKED_new();
|
||||||
|
if entry.is_null() {
|
||||||
|
return Err(PKIError::GenCRLError("X509_CRL_new for new entry").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if X509_REVOKED_set_serialNumber(entry, new_cert.serial_number().as_ptr()) == 0 {
|
||||||
|
return Err(
|
||||||
|
PKIError::GenCRLError("X509_REVOKED_set_serialNumber for new entry").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let revocation_date = Asn1Time::days_from_now(0)?;
|
||||||
|
if X509_REVOKED_set_revocationDate(entry, revocation_date.as_ptr()) == 0 {
|
||||||
|
return Err(
|
||||||
|
PKIError::GenCRLError("X509_REVOKED_set_revocationDate for new entry").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if X509_CRL_add0_revoked(crl, X509_REVOKED_dup(entry)) == 0 {
|
||||||
|
return Err(PKIError::GenCRLError("X509_CRL_add0_revoked for new entry").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let md = MessageDigest::sha256();
|
let md = MessageDigest::sha256();
|
||||||
if X509_CRL_sign(crl, d.key.as_ptr(), md.as_ptr()) == 0 {
|
if X509_CRL_sign(crl, d.key.as_ptr(), md.as_ptr()) == 0 {
|
||||||
return Err(PKIError::GenCRLError("X509_CRL_sign").into());
|
return Err(PKIError::GenCRLError("X509_CRL_sign").into());
|
||||||
@ -434,9 +462,9 @@ fn refresh_crl(d: &CertData) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
/// Refresh revocation lists
|
/// Refresh revocation lists
|
||||||
pub fn refresh_crls() -> anyhow::Result<()> {
|
pub fn refresh_crls() -> anyhow::Result<()> {
|
||||||
refresh_crl(&CertData::load_root_ca()?)?;
|
refresh_crl(&CertData::load_root_ca()?, None)?;
|
||||||
refresh_crl(&CertData::load_web_ca()?)?;
|
refresh_crl(&CertData::load_web_ca()?, None)?;
|
||||||
refresh_crl(&CertData::load_devices_ca()?)?;
|
refresh_crl(&CertData::load_devices_ca()?, None)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,3 +479,31 @@ pub fn gen_certificate_for_device(csr: &X509Req) -> anyhow::Result<String> {
|
|||||||
|
|
||||||
Ok(String::from_utf8(cert)?)
|
Ok(String::from_utf8(cert)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a certificate is revoked
|
||||||
|
fn is_revoked(cert: &X509, ca: &CertData) -> anyhow::Result<bool> {
|
||||||
|
let crl = X509Crl::from_pem(&std::fs::read(
|
||||||
|
ca.crl.as_ref().ok_or(PKIError::MissingCRL)?,
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
let res = crl.get_by_cert(cert);
|
||||||
|
|
||||||
|
Ok(matches!(res, CrlStatus::Revoked(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a certificate
|
||||||
|
pub fn revoke(cert: &X509, ca: &CertData) -> anyhow::Result<()> {
|
||||||
|
// Check if certificate is already revoked
|
||||||
|
if is_revoked(cert, ca)? {
|
||||||
|
// No op
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_crl(ca, Some(cert))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a device certificate
|
||||||
|
pub fn revoke_device_cert(cert: &X509) -> anyhow::Result<()> {
|
||||||
|
revoke(cert, &CertData::load_devices_ca()?)
|
||||||
|
}
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
|
//! # Devices entities definition
|
||||||
|
|
||||||
|
use crate::constants::StaticConstraints;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Device information provided directly by the device during syncrhonisation.
|
||||||
|
///
|
||||||
|
/// It should not be editable fro the Web UI
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct DeviceInfo {
|
pub struct DeviceInfo {
|
||||||
|
/// Device reference
|
||||||
reference: String,
|
reference: String,
|
||||||
|
/// Device firmware / software version
|
||||||
version: semver::Version,
|
version: semver::Version,
|
||||||
|
/// Maximum number of relay that the device can support
|
||||||
max_relays: usize,
|
max_relays: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceInfo {
|
impl DeviceInfo {
|
||||||
|
/// Identify errors in device information definition
|
||||||
pub fn error(&self) -> Option<&str> {
|
pub fn error(&self) -> Option<&str> {
|
||||||
if self.reference.trim().is_empty() {
|
if self.reference.trim().is_empty() {
|
||||||
return Some("Given device reference is empty or blank!");
|
return Some("Given device reference is empty or blank!");
|
||||||
@ -19,14 +31,19 @@ impl DeviceInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Device identifier
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct DeviceId(pub String);
|
pub struct DeviceId(pub String);
|
||||||
|
|
||||||
|
/// Single device information
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Device {
|
pub struct Device {
|
||||||
/// The device ID
|
/// The device ID
|
||||||
pub id: DeviceId,
|
pub id: DeviceId,
|
||||||
/// Information about the device
|
/// Information about the device
|
||||||
|
///
|
||||||
|
/// These information shall not be editable from the webui. They are automatically updated during
|
||||||
|
/// device synchronization
|
||||||
pub info: DeviceInfo,
|
pub info: DeviceInfo,
|
||||||
/// Time at which device was initially enrolled
|
/// Time at which device was initially enrolled
|
||||||
pub time_create: u64,
|
pub time_create: u64,
|
||||||
@ -42,6 +59,8 @@ pub struct Device {
|
|||||||
/// Specify whether the device is enabled or not
|
/// Specify whether the device is enabled or not
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Information about the relays handled by the device
|
/// Information about the relays handled by the device
|
||||||
|
///
|
||||||
|
/// There cannot be more than [info.max_relays] relays
|
||||||
pub relays: Vec<DeviceRelay>,
|
pub relays: Vec<DeviceRelay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,24 +68,175 @@ pub struct Device {
|
|||||||
/// time of a device
|
/// time of a device
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct DailyMinRuntime {
|
pub struct DailyMinRuntime {
|
||||||
|
/// Minimum time, in seconds, that this relay should run each day
|
||||||
pub min_runtime: usize,
|
pub min_runtime: usize,
|
||||||
|
/// The seconds in the days (from 00:00) where the counter is reset
|
||||||
pub reset_time: usize,
|
pub reset_time: usize,
|
||||||
|
/// The hours during which the relay should be turned on to reach expected runtime
|
||||||
pub catch_up_hours: Vec<usize>,
|
pub catch_up_hours: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct DeviceRelayID(uuid::Uuid);
|
pub struct DeviceRelayID(uuid::Uuid);
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
impl Default for DeviceRelayID {
|
||||||
pub struct DeviceRelay {
|
fn default() -> Self {
|
||||||
id: DeviceRelayID,
|
Self(uuid::Uuid::new_v4())
|
||||||
name: String,
|
}
|
||||||
enabled: bool,
|
}
|
||||||
priority: usize,
|
|
||||||
consumption: usize,
|
/// Single device relay information
|
||||||
minimal_uptime: usize,
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
|
||||||
minimal_downtime: usize,
|
pub struct DeviceRelay {
|
||||||
daily_runtime: Option<DailyMinRuntime>,
|
/// Device relay id. Should be unique across the whole application
|
||||||
depends_on: Vec<DeviceRelay>,
|
#[serde(default)]
|
||||||
conflicts_with: Vec<DeviceRelay>,
|
id: DeviceRelayID,
|
||||||
|
/// Human-readable name for the relay
|
||||||
|
name: String,
|
||||||
|
/// Whether this relay can be turned on or not
|
||||||
|
enabled: bool,
|
||||||
|
/// Relay priority when selecting relays to turn on. 0 = lowest priority
|
||||||
|
priority: usize,
|
||||||
|
/// Estimated consumption of the electrical equipment triggered by the relay
|
||||||
|
consumption: usize,
|
||||||
|
/// Minimal time this relay shall be left on before it can be turned off (in seconds)
|
||||||
|
minimal_uptime: usize,
|
||||||
|
/// Minimal time this relay shall be left off before it can be turned on again (in seconds)
|
||||||
|
minimal_downtime: usize,
|
||||||
|
/// Optional minimal runtime requirements for this relay
|
||||||
|
daily_runtime: Option<DailyMinRuntime>,
|
||||||
|
/// Specify relay that must be turned on before this relay can be started
|
||||||
|
depends_on: Vec<DeviceRelayID>,
|
||||||
|
/// Specify relays that must be turned off before this relay can be started
|
||||||
|
conflicts_with: Vec<DeviceRelayID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceRelay {
|
||||||
|
/// Check device relay for errors
|
||||||
|
pub fn error(&self, list: &[DeviceRelay]) -> Option<&'static str> {
|
||||||
|
let constraints = StaticConstraints::default();
|
||||||
|
if !constraints.relay_name_len.validate(&self.name) {
|
||||||
|
return Some("Invalid relay name length!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints.relay_priority.validate_usize(self.priority) {
|
||||||
|
return Some("Invalid relay priority!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints
|
||||||
|
.relay_consumption
|
||||||
|
.validate_usize(self.consumption)
|
||||||
|
{
|
||||||
|
return Some("Invalid consumption!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints
|
||||||
|
.relay_minimal_uptime
|
||||||
|
.validate_usize(self.minimal_uptime)
|
||||||
|
{
|
||||||
|
return Some("Invalid minimal uptime!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints
|
||||||
|
.relay_minimal_downtime
|
||||||
|
.validate_usize(self.minimal_downtime)
|
||||||
|
{
|
||||||
|
return Some("Invalid minimal uptime!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(daily) = &self.daily_runtime {
|
||||||
|
if !constraints
|
||||||
|
.relay_daily_minimal_runtime
|
||||||
|
.validate_usize(daily.min_runtime)
|
||||||
|
{
|
||||||
|
return Some("Invalid minimal daily runtime!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if daily.reset_time > 3600 * 24 {
|
||||||
|
return Some("Invalid daily reset time!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if daily.catch_up_hours.is_empty() {
|
||||||
|
return Some("No catchup hours defined!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if daily.catch_up_hours.iter().any(|h| h > &23) {
|
||||||
|
return Some("At least one catch up hour is invalid!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let relays_map = list.iter().map(|r| (r.id, r)).collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
if self.depends_on.iter().any(|d| !relays_map.contains_key(d)) {
|
||||||
|
return Some("A specified dependent relay does not exists!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.conflicts_with
|
||||||
|
.iter()
|
||||||
|
.any(|d| !relays_map.contains_key(d))
|
||||||
|
{
|
||||||
|
return Some("A specified conflicting relay does not exists!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : check for loops
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device general information
|
||||||
|
///
|
||||||
|
/// This structure is used to update device information
|
||||||
|
#[derive(serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct DeviceGeneralInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceGeneralInfo {
|
||||||
|
/// Check for errors in the structure
|
||||||
|
pub fn error(&self) -> Option<&'static str> {
|
||||||
|
let constraints = StaticConstraints::default();
|
||||||
|
if !constraints.dev_name_len.validate(&self.name) {
|
||||||
|
return Some("Invalid device name length!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !constraints.dev_description_len.validate(&self.description) {
|
||||||
|
return Some("Invalid device description length!");
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::devices::device::DeviceRelay;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_device_relay_error() {
|
||||||
|
let unitary = DeviceRelay {
|
||||||
|
name: "unitary".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let bad_name = DeviceRelay {
|
||||||
|
name: "".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dep_on_unitary = DeviceRelay {
|
||||||
|
name: "dep_on_unitary".to_string(),
|
||||||
|
depends_on: vec![unitary.id],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(unitary.error(&[]), None);
|
||||||
|
assert_eq!(unitary.error(&[unitary.clone(), bad_name.clone()]), None);
|
||||||
|
assert!(bad_name.error(&[]).is_some());
|
||||||
|
assert_eq!(dep_on_unitary.error(&[unitary.clone()]), None);
|
||||||
|
assert!(dep_on_unitary.error(&[]).is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use crate::crypto::pki;
|
use crate::crypto::pki;
|
||||||
use crate::devices::device::{Device, DeviceId, DeviceInfo};
|
use crate::devices::device::{Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay};
|
||||||
use crate::utils::time_utils::time_secs;
|
use crate::utils::time_utils::time_secs;
|
||||||
use openssl::x509::X509Req;
|
use openssl::x509::{X509Req, X509};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -15,6 +15,12 @@ pub enum DevicesListError {
|
|||||||
ValidateDeviceFailedDeviceNotFound,
|
ValidateDeviceFailedDeviceNotFound,
|
||||||
#[error("Validated device failed: the device is already validated!")]
|
#[error("Validated device failed: the device is already validated!")]
|
||||||
ValidateDeviceFailedDeviceAlreadyValidated,
|
ValidateDeviceFailedDeviceAlreadyValidated,
|
||||||
|
#[error("Update device failed: the device does not exists!")]
|
||||||
|
UpdateDeviceFailedDeviceNotFound,
|
||||||
|
#[error("Requested device was not found")]
|
||||||
|
DeviceNotFound,
|
||||||
|
#[error("Requested device is not validated")]
|
||||||
|
DeviceNotValidated,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DevicesList(HashMap<DeviceId, Device>);
|
pub struct DevicesList(HashMap<DeviceId, Device>);
|
||||||
@ -129,12 +135,47 @@ impl DevicesList {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a device general information
|
||||||
|
pub fn update_general_info(
|
||||||
|
&mut self,
|
||||||
|
id: &DeviceId,
|
||||||
|
general_info: DeviceGeneralInfo,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let dev = self
|
||||||
|
.0
|
||||||
|
.get_mut(id)
|
||||||
|
.ok_or(DevicesListError::UpdateDeviceFailedDeviceNotFound)?;
|
||||||
|
|
||||||
|
dev.name = general_info.name;
|
||||||
|
dev.description = general_info.description;
|
||||||
|
dev.enabled = general_info.enabled;
|
||||||
|
dev.time_update = time_secs();
|
||||||
|
|
||||||
|
self.persist_dev_config(id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get single certificate information
|
||||||
|
fn get_cert(&self, id: &DeviceId) -> anyhow::Result<X509> {
|
||||||
|
let dev = self
|
||||||
|
.get_single(id)
|
||||||
|
.ok_or(DevicesListError::DeviceNotFound)?;
|
||||||
|
if !dev.validated {
|
||||||
|
return Err(DevicesListError::DeviceNotValidated.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(X509::from_pem(&std::fs::read(
|
||||||
|
AppConfig::get().device_cert_path(id),
|
||||||
|
)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a device
|
/// Delete a device
|
||||||
pub fn delete(&mut self, id: &DeviceId) -> anyhow::Result<()> {
|
pub fn delete(&mut self, id: &DeviceId) -> anyhow::Result<()> {
|
||||||
let crt_path = AppConfig::get().device_cert_path(id);
|
let crt_path = AppConfig::get().device_cert_path(id);
|
||||||
if crt_path.is_file() {
|
if crt_path.is_file() {
|
||||||
// TODO : implement
|
let cert = self.get_cert(id)?;
|
||||||
unimplemented!("Certificate revocation not implemented yet!");
|
pki::revoke_device_cert(&cert)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let csr_path = AppConfig::get().device_csr_path(id);
|
let csr_path = AppConfig::get().device_csr_path(id);
|
||||||
@ -151,4 +192,12 @@ impl DevicesList {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the full list of relays
|
||||||
|
pub fn relays_list(&mut self) -> Vec<DeviceRelay> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_id, d)| d.relays.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::devices::device::{Device, DeviceId, DeviceInfo};
|
use crate::devices::device::{Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay};
|
||||||
use crate::devices::devices_list::DevicesList;
|
use crate::devices::devices_list::DevicesList;
|
||||||
use crate::energy::consumption;
|
use crate::energy::consumption;
|
||||||
use crate::energy::consumption::EnergyConsumption;
|
use crate::energy::consumption::EnergyConsumption;
|
||||||
@ -109,6 +109,27 @@ impl Handler<ValidateDevice> for EnergyActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a device general information
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "anyhow::Result<()>")]
|
||||||
|
pub struct UpdateDeviceGeneralInfo(pub DeviceId, pub DeviceGeneralInfo);
|
||||||
|
|
||||||
|
impl Handler<UpdateDeviceGeneralInfo> for EnergyActor {
|
||||||
|
type Result = anyhow::Result<()>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: UpdateDeviceGeneralInfo, _ctx: &mut Context<Self>) -> Self::Result {
|
||||||
|
log::info!(
|
||||||
|
"Requested to update device general info {:?}... {:#?}",
|
||||||
|
&msg.0,
|
||||||
|
&msg.1
|
||||||
|
);
|
||||||
|
|
||||||
|
self.devices.update_general_info(&msg.0, msg.1)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a device
|
/// Delete a device
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "anyhow::Result<()>")]
|
#[rtype(result = "anyhow::Result<()>")]
|
||||||
@ -150,3 +171,16 @@ impl Handler<GetSingleDevice> for EnergyActor {
|
|||||||
self.devices.get_single(&msg.0)
|
self.devices.get_single(&msg.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the full list of relays
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Vec<DeviceRelay>")]
|
||||||
|
pub struct GetRelaysList;
|
||||||
|
|
||||||
|
impl Handler<GetRelaysList> for EnergyActor {
|
||||||
|
type Result = Vec<DeviceRelay>;
|
||||||
|
|
||||||
|
fn handle(&mut self, _msg: GetRelaysList, _ctx: &mut Context<Self>) -> Self::Result {
|
||||||
|
self.devices.relays_list()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +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};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@ -23,7 +24,15 @@ async fn main() -> std::io::Result<()> {
|
|||||||
pki::initialize_devices_ca().expect("Failed to initialize devices CA!");
|
pki::initialize_devices_ca().expect("Failed to initialize devices CA!");
|
||||||
pki::initialize_server_ca().expect("Failed to initialize server certificate!");
|
pki::initialize_server_ca().expect("Failed to initialize server certificate!");
|
||||||
|
|
||||||
|
// Initialize CRL
|
||||||
pki::refresh_crls().expect("Failed to initialize Root CA!");
|
pki::refresh_crls().expect("Failed to initialize Root CA!");
|
||||||
|
let refresh_crl = every(1).hour().perform(|| async {
|
||||||
|
log::info!("Periodic refresh of CRLs...");
|
||||||
|
if let Err(e) = pki::refresh_crls() {
|
||||||
|
log::error!("Failed to perform auto refresh of CRLs! {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tokio::spawn(refresh_crl);
|
||||||
|
|
||||||
// Initialize energy actor
|
// Initialize energy actor
|
||||||
let actor = EnergyActor::new()
|
let actor = EnergyActor::new()
|
||||||
|
@ -8,5 +8,6 @@ pub mod devices_api;
|
|||||||
pub mod servers;
|
pub mod servers;
|
||||||
pub mod unsecure_server;
|
pub mod unsecure_server;
|
||||||
pub mod web_api;
|
pub mod web_api;
|
||||||
|
pub mod web_app_controller;
|
||||||
|
|
||||||
pub type WebEnergyActor = web::Data<EnergyActorAddr>;
|
pub type WebEnergyActor = web::Data<EnergyActorAddr>;
|
||||||
|
@ -6,6 +6,7 @@ use crate::server::auth_middleware::AuthChecker;
|
|||||||
use crate::server::devices_api::{mgmt_controller, utils_controller};
|
use crate::server::devices_api::{mgmt_controller, utils_controller};
|
||||||
use crate::server::unsecure_server::*;
|
use crate::server::unsecure_server::*;
|
||||||
use crate::server::web_api::*;
|
use crate::server::web_api::*;
|
||||||
|
use crate::server::web_app_controller;
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_identity::config::LogoutBehaviour;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
@ -105,12 +106,14 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
.app_data(web::Data::new(RemoteIPConfig {
|
.app_data(web::Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
}))
|
}))
|
||||||
.route("/", web::get().to(server_controller::secure_home))
|
//.route("/", web::get().to(server_controller::secure_home))
|
||||||
// Web API
|
// Web API
|
||||||
|
// Server controller
|
||||||
.route(
|
.route(
|
||||||
"/web_api/server/config",
|
"/web_api/server/config",
|
||||||
web::get().to(server_controller::config),
|
web::get().to(server_controller::config),
|
||||||
)
|
)
|
||||||
|
// Auth controller
|
||||||
.route(
|
.route(
|
||||||
"/web_api/auth/password_auth",
|
"/web_api/auth/password_auth",
|
||||||
web::post().to(auth_controller::password_auth),
|
web::post().to(auth_controller::password_auth),
|
||||||
@ -123,6 +126,7 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/web_api/auth/sign_out",
|
"/web_api/auth/sign_out",
|
||||||
web::get().to(auth_controller::sign_out),
|
web::get().to(auth_controller::sign_out),
|
||||||
)
|
)
|
||||||
|
// Energy controller
|
||||||
.route(
|
.route(
|
||||||
"/web_api/energy/curr_consumption",
|
"/web_api/energy/curr_consumption",
|
||||||
web::get().to(energy_controller::curr_consumption),
|
web::get().to(energy_controller::curr_consumption),
|
||||||
@ -131,6 +135,7 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/web_api/energy/cached_consumption",
|
"/web_api/energy/cached_consumption",
|
||||||
web::get().to(energy_controller::cached_consumption),
|
web::get().to(energy_controller::cached_consumption),
|
||||||
)
|
)
|
||||||
|
// Devices controller
|
||||||
.route(
|
.route(
|
||||||
"/web_api/devices/list_pending",
|
"/web_api/devices/list_pending",
|
||||||
web::get().to(devices_controller::list_pending),
|
web::get().to(devices_controller::list_pending),
|
||||||
@ -139,14 +144,27 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/web_api/devices/list_validated",
|
"/web_api/devices/list_validated",
|
||||||
web::get().to(devices_controller::list_validated),
|
web::get().to(devices_controller::list_validated),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/device/{id}",
|
||||||
|
web::get().to(devices_controller::get_single),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/web_api/device/{id}/validate",
|
"/web_api/device/{id}/validate",
|
||||||
web::post().to(devices_controller::validate_device),
|
web::post().to(devices_controller::validate_device),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/web_api/device/{id}",
|
||||||
|
web::patch().to(devices_controller::update_device),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/web_api/device/{id}",
|
"/web_api/device/{id}",
|
||||||
web::delete().to(devices_controller::delete_device),
|
web::delete().to(devices_controller::delete_device),
|
||||||
)
|
)
|
||||||
|
// Relays API
|
||||||
|
.route(
|
||||||
|
"/web_api/relays/list",
|
||||||
|
web::get().to(relays_controller::get_list),
|
||||||
|
)
|
||||||
// Devices API
|
// Devices API
|
||||||
.route(
|
.route(
|
||||||
"/devices_api/utils/time",
|
"/devices_api/utils/time",
|
||||||
@ -164,6 +182,13 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/devices_api/mgmt/get_certificate",
|
"/devices_api/mgmt/get_certificate",
|
||||||
web::get().to(mgmt_controller::get_certificate),
|
web::get().to(mgmt_controller::get_certificate),
|
||||||
)
|
)
|
||||||
|
// Web app
|
||||||
|
.route("/", web::get().to(web_app_controller::root_index))
|
||||||
|
.route(
|
||||||
|
"/assets/{tail:.*}",
|
||||||
|
web::get().to(web_app_controller::serve_assets_content),
|
||||||
|
)
|
||||||
|
.route("/{tail:.*}", web::get().to(web_app_controller::root_index))
|
||||||
})
|
})
|
||||||
.bind_openssl(&AppConfig::get().listen_address, builder)?
|
.bind_openssl(&AppConfig::get().listen_address, builder)?
|
||||||
.run()
|
.run()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::devices::device::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::custom_error::HttpResult;
|
||||||
use crate::server::WebEnergyActor;
|
use crate::server::WebEnergyActor;
|
||||||
@ -33,6 +33,18 @@ pub struct DeviceInPath {
|
|||||||
id: DeviceId,
|
id: DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a single device information
|
||||||
|
pub async fn get_single(actor: WebEnergyActor, id: web::Path<DeviceInPath>) -> HttpResult {
|
||||||
|
let Some(dev) = actor
|
||||||
|
.send(energy_actor::GetSingleDevice(id.id.clone()))
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Requested device was not found!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(dev))
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate a device
|
/// Validate a device
|
||||||
pub async fn validate_device(actor: WebEnergyActor, id: web::Path<DeviceInPath>) -> HttpResult {
|
pub async fn validate_device(actor: WebEnergyActor, id: web::Path<DeviceInPath>) -> HttpResult {
|
||||||
actor
|
actor
|
||||||
@ -42,6 +54,26 @@ pub async fn validate_device(actor: WebEnergyActor, id: web::Path<DeviceInPath>)
|
|||||||
Ok(HttpResponse::Accepted().finish())
|
Ok(HttpResponse::Accepted().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a device information
|
||||||
|
pub async fn update_device(
|
||||||
|
actor: WebEnergyActor,
|
||||||
|
id: web::Path<DeviceInPath>,
|
||||||
|
update: web::Json<DeviceGeneralInfo>,
|
||||||
|
) -> HttpResult {
|
||||||
|
if let Some(e) = update.error() {
|
||||||
|
return Ok(HttpResponse::BadRequest().json(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
actor
|
||||||
|
.send(energy_actor::UpdateDeviceGeneralInfo(
|
||||||
|
id.id.clone(),
|
||||||
|
update.0.clone(),
|
||||||
|
))
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Accepted().finish())
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a device
|
/// Delete a device
|
||||||
pub async fn delete_device(actor: WebEnergyActor, id: web::Path<DeviceInPath>) -> HttpResult {
|
pub async fn delete_device(actor: WebEnergyActor, id: web::Path<DeviceInPath>) -> HttpResult {
|
||||||
actor
|
actor
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
pub mod devices_controller;
|
pub mod devices_controller;
|
||||||
pub mod energy_controller;
|
pub mod energy_controller;
|
||||||
|
pub mod relays_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
10
central_backend/src/server/web_api/relays_controller.rs
Normal file
10
central_backend/src/server/web_api/relays_controller.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use crate::energy::energy_actor;
|
||||||
|
use crate::server::custom_error::HttpResult;
|
||||||
|
use crate::server::WebEnergyActor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
/// Get the full list of relays
|
||||||
|
pub async fn get_list(actor: WebEnergyActor) -> HttpResult {
|
||||||
|
let list = actor.send(energy_actor::GetRelaysList).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(list))
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants::StaticConstraints;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
pub async fn secure_home() -> HttpResponse {
|
pub async fn secure_home() -> HttpResponse {
|
||||||
@ -10,12 +11,14 @@ pub async fn secure_home() -> HttpResponse {
|
|||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct ServerConfig {
|
struct ServerConfig {
|
||||||
auth_disabled: bool,
|
auth_disabled: bool,
|
||||||
|
constraints: StaticConstraints,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConfig {
|
impl Default for ServerConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
auth_disabled: AppConfig::get().unsecure_disable_login,
|
auth_disabled: AppConfig::get().unsecure_disable_login,
|
||||||
|
constraints: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
central_backend/src/server/web_app_controller.rs
Normal file
46
central_backend/src/server/web_app_controller.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub use serve_static_debug::{root_index, serve_assets_content};
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub use serve_static_release::{root_index, serve_assets_content};
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
mod serve_static_debug {
|
||||||
|
use actix_web::{HttpResponse, Responder};
|
||||||
|
|
||||||
|
pub async fn root_index() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.body("Solar energy secure home: Hello world! Debug=on for Solar platform!")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_assets_content() -> impl Responder {
|
||||||
|
HttpResponse::NotFound().body("Hello world! Static assets are not served in debug mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
mod serve_static_release {
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "static/"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse {
|
||||||
|
match (Asset::get(path), can_fallback) {
|
||||||
|
(Some(content), _) => HttpResponse::Ok()
|
||||||
|
.content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref())
|
||||||
|
.body(content.data.into_owned()),
|
||||||
|
(None, false) => HttpResponse::NotFound().body("404 Not Found"),
|
||||||
|
(None, true) => handle_embedded_file("index.html", false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn root_index() -> impl Responder {
|
||||||
|
handle_embedded_file("index.html", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_assets_content(path: web::Path<String>) -> impl Responder {
|
||||||
|
handle_embedded_file(&format!("assets/{}", path.as_ref()), false)
|
||||||
|
}
|
||||||
|
}
|
124
central_frontend/package-lock.json
generated
124
central_frontend/package-lock.json
generated
@ -15,7 +15,9 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^5.15.21",
|
"@mui/icons-material": "^5.15.21",
|
||||||
"@mui/material": "^5.15.21",
|
"@mui/material": "^5.15.21",
|
||||||
|
"@mui/x-date-pickers": "^7.11.1",
|
||||||
"date-and-time": "^3.3.0",
|
"date-and-time": "^3.3.0",
|
||||||
|
"dayjs": "^1.11.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.0"
|
"react-router-dom": "^6.24.0"
|
||||||
@ -337,9 +339,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||||
"integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
|
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
@ -1265,12 +1267,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
"node_modules/@mui/private-theming": {
|
||||||
"version": "5.15.20",
|
"version": "5.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.20.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.5.tgz",
|
||||||
"integrity": "sha512-BK8F94AIqSrnaPYXf2KAOjGZJgWfvqAVQ2gVR3EryvQFtuBnG6RwodxrCvd3B48VuMy6Wsk897+lQMUxJyk+6g==",
|
"integrity": "sha512-CSLg0YkpDqg0aXOxtjo3oTMd3XWMxvNb5d0v4AYVqwOltU8q6GvnZjhWyCLjGSCrcgfwm6/VDjaKLPlR14wxIA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
"@mui/utils": "^5.15.20",
|
"@mui/utils": "^5.16.5",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -1291,9 +1293,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/styled-engine": {
|
"node_modules/@mui/styled-engine": {
|
||||||
"version": "5.15.14",
|
"version": "5.16.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz",
|
||||||
"integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
|
"integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
@ -1322,15 +1324,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "5.15.20",
|
"version": "5.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.20.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.5.tgz",
|
||||||
"integrity": "sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==",
|
"integrity": "sha512-uzIUGdrWddUx1HPxW4+B2o4vpgKyRxGe/8BxbfXVDPNPHX75c782TseoCnR/VyfnZJfqX87GcxDmnZEE1c031g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
"@mui/private-theming": "^5.15.20",
|
"@mui/private-theming": "^5.16.5",
|
||||||
"@mui/styled-engine": "^5.15.14",
|
"@mui/styled-engine": "^5.16.4",
|
||||||
"@mui/types": "^7.2.14",
|
"@mui/types": "^7.2.15",
|
||||||
"@mui/utils": "^5.15.20",
|
"@mui/utils": "^5.16.5",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"csstype": "^3.1.3",
|
"csstype": "^3.1.3",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
@ -1361,9 +1363,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.2.14",
|
"version": "7.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz",
|
||||||
"integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
|
"integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^17.0.0 || ^18.0.0"
|
"@types/react": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
@ -1374,14 +1376,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/utils": {
|
"node_modules/@mui/utils": {
|
||||||
"version": "5.15.20",
|
"version": "5.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.20.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.5.tgz",
|
||||||
"integrity": "sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==",
|
"integrity": "sha512-CwhcA9y44XwK7k2joL3Y29mRUnoBt+gOZZdGyw7YihbEwEErJYBtDwbZwVgH68zAljGe/b+Kd5bzfl63Gi3R2A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
"@types/prop-types": "^15.7.11",
|
"@mui/types": "^7.2.15",
|
||||||
|
"@types/prop-types": "^15.7.12",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-is": "^18.2.0"
|
"react-is": "^18.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@ -1400,6 +1404,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/x-date-pickers": {
|
||||||
|
"version": "7.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.11.1.tgz",
|
||||||
|
"integrity": "sha512-CflouzTNSv0YeOA8iiYpJMtqGlwGC8LI9EE9egDGhatR9Mn5geRDTXsm0rRG/4pMOfaRxyJc6Yzr/axBhEXM7w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.24.8",
|
||||||
|
"@mui/base": "^5.0.0-beta.40",
|
||||||
|
"@mui/system": "^5.16.5",
|
||||||
|
"@mui/utils": "^5.16.5",
|
||||||
|
"@types/react-transition-group": "^4.4.10",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"@mui/material": "^5.15.14",
|
||||||
|
"date-fns": "^2.25.0 || ^3.2.0",
|
||||||
|
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"luxon": "^3.0.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"moment-hijri": "^2.1.2",
|
||||||
|
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@emotion/styled": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns-jalali": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"luxon": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-hijri": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-jalaali": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -2211,6 +2280,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.3.0.tgz",
|
||||||
"integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg=="
|
"integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz",
|
||||||
|
"integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^5.15.21",
|
"@mui/icons-material": "^5.15.21",
|
||||||
"@mui/material": "^5.15.21",
|
"@mui/material": "^5.15.21",
|
||||||
|
"@mui/x-date-pickers": "^7.11.1",
|
||||||
"date-and-time": "^3.3.0",
|
"date-and-time": "^3.3.0",
|
||||||
|
"dayjs": "^1.11.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.0"
|
"react-router-dom": "^6.24.0"
|
||||||
|
@ -12,6 +12,7 @@ import { HomeRoute } from "./routes/HomeRoute";
|
|||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
|
import { PendingDevicesRoute } from "./routes/PendingDevicesRoute";
|
||||||
import { DevicesRoute } from "./routes/DevicesRoute";
|
import { DevicesRoute } from "./routes/DevicesRoute";
|
||||||
|
import { DeviceRoute } from "./routes/DeviceRoute/DeviceRoute";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
|
||||||
@ -21,8 +22,9 @@ export function App() {
|
|||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||||
<Route path="" element={<HomeRoute />} />
|
<Route path="" element={<HomeRoute />} />
|
||||||
<Route path="devices" element={<DevicesRoute />} />
|
|
||||||
<Route path="pending_devices" element={<PendingDevicesRoute />} />
|
<Route path="pending_devices" element={<PendingDevicesRoute />} />
|
||||||
|
<Route path="devices" element={<DevicesRoute />} />
|
||||||
|
<Route path="dev/:id" element={<DeviceRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
)
|
)
|
||||||
|
@ -12,8 +12,10 @@ export interface DailyMinRuntime {
|
|||||||
catch_up_hours: number[];
|
catch_up_hours: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RelayID = string;
|
||||||
|
|
||||||
export interface DeviceRelay {
|
export interface DeviceRelay {
|
||||||
id: string;
|
id: RelayID;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
priority: number;
|
priority: number;
|
||||||
@ -21,8 +23,8 @@ export interface DeviceRelay {
|
|||||||
minimal_uptime: number;
|
minimal_uptime: number;
|
||||||
minimal_downtime: number;
|
minimal_downtime: number;
|
||||||
daily_runtime?: DailyMinRuntime;
|
daily_runtime?: DailyMinRuntime;
|
||||||
depends_on: DeviceRelay[];
|
depends_on: RelayID[];
|
||||||
conflicts_with: DeviceRelay[];
|
conflicts_with: RelayID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
@ -37,6 +39,16 @@ export interface Device {
|
|||||||
relays: DeviceRelay[];
|
relays: DeviceRelay[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdatedInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceURL(d: Device): string {
|
||||||
|
return `/dev/${encodeURIComponent(d.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export class DeviceApi {
|
export class DeviceApi {
|
||||||
/**
|
/**
|
||||||
* Get the list of pending devices
|
* Get the list of pending devices
|
||||||
@ -72,6 +84,29 @@ export class DeviceApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the information about a single device
|
||||||
|
*/
|
||||||
|
static async GetSingle(id: string): Promise<Device> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: `/device/${encodeURIComponent(id)}`,
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a device general information
|
||||||
|
*/
|
||||||
|
static async Update(d: Device, info: UpdatedInfo): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: `/device/${encodeURIComponent(d.id)}`,
|
||||||
|
method: "PATCH",
|
||||||
|
jsonData: info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a device
|
* Delete a device
|
||||||
*/
|
*/
|
||||||
|
41
central_frontend/src/api/RelayApi.ts
Normal file
41
central_frontend/src/api/RelayApi.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
import { Device, DeviceRelay } from "./DeviceApi";
|
||||||
|
|
||||||
|
export class RelayApi {
|
||||||
|
/**
|
||||||
|
* Get the full list of relays
|
||||||
|
*/
|
||||||
|
static async GetList(): Promise<DeviceRelay[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/relays/list",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new relay
|
||||||
|
*/
|
||||||
|
static async Create(device: Device, relay: DeviceRelay): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/relay/create",
|
||||||
|
jsonData: {
|
||||||
|
...relay,
|
||||||
|
device_id: device.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a relay information
|
||||||
|
*/
|
||||||
|
static async Update(relay: DeviceRelay): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "PUT",
|
||||||
|
uri: `/relay/${relay.id}`,
|
||||||
|
jsonData: relay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,23 @@ import { APIClient } from "./ApiClient";
|
|||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
auth_disabled: boolean;
|
auth_disabled: boolean;
|
||||||
|
constraints: ServerConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerConstraint {
|
||||||
|
dev_name_len: LenConstraint;
|
||||||
|
dev_description_len: LenConstraint;
|
||||||
|
relay_name_len: LenConstraint;
|
||||||
|
relay_priority: LenConstraint;
|
||||||
|
relay_consumption: LenConstraint;
|
||||||
|
relay_minimal_uptime: LenConstraint;
|
||||||
|
relay_minimal_downtime: LenConstraint;
|
||||||
|
relay_daily_minimal_runtime: LenConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LenConstraint {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: ServerConfig | null = null;
|
let config: ServerConfig | null = null;
|
||||||
|
87
central_frontend/src/dialogs/EditDeviceMetadataDialog.tsx
Normal file
87
central_frontend/src/dialogs/EditDeviceMetadataDialog.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { Device, DeviceApi } from "../api/DeviceApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||||
|
import { lenValid } from "../utils/StringsUtils";
|
||||||
|
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
|
||||||
|
import { TextInput } from "../widgets/forms/TextInput";
|
||||||
|
|
||||||
|
export function EditDeviceMetadataDialog(p: {
|
||||||
|
onClose: () => void;
|
||||||
|
device: Device;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [name, setName] = React.useState(p.device.name);
|
||||||
|
const [description, setDescription] = React.useState(p.device.description);
|
||||||
|
const [enabled, setEnabled] = React.useState(p.device.enabled);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Updating device information");
|
||||||
|
await DeviceApi.Update(p.device, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
snackbar("The device information have been successfully updated!");
|
||||||
|
p.onUpdated();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update device general information!" + e);
|
||||||
|
alert(`Failed to update device general information! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSubmit =
|
||||||
|
lenValid(name, ServerApi.Config.constraints.dev_name_len) &&
|
||||||
|
lenValid(description, ServerApi.Config.constraints.dev_description_len);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open>
|
||||||
|
<DialogTitle>Edit device general information</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Device name"
|
||||||
|
value={name}
|
||||||
|
onValueChange={(s) => setName(s ?? "")}
|
||||||
|
size={ServerApi.Config.constraints.dev_name_len}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Device description"
|
||||||
|
value={description}
|
||||||
|
onValueChange={(s) => setDescription(s ?? "")}
|
||||||
|
size={ServerApi.Config.constraints.dev_description_len}
|
||||||
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
editable
|
||||||
|
label="Enable device"
|
||||||
|
checked={enabled}
|
||||||
|
onValueChange={setEnabled}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onClose}>Cancel</Button>
|
||||||
|
<Button onClick={onSubmit} autoFocus disabled={!canSubmit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
332
central_frontend/src/dialogs/EditDeviceRelaysDialog.tsx
Normal file
332
central_frontend/src/dialogs/EditDeviceRelaysDialog.tsx
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { TimePicker } from "@mui/x-date-pickers";
|
||||||
|
import React from "react";
|
||||||
|
import { Device, DeviceRelay } from "../api/DeviceApi";
|
||||||
|
import { RelayApi } from "../api/RelayApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||||
|
import { dayjsToTimeOfDay, timeOfDay } from "../utils/DateUtils";
|
||||||
|
import { lenValid } from "../utils/StringsUtils";
|
||||||
|
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
|
||||||
|
import { MultipleSelectInput } from "../widgets/forms/MultipleSelectInput";
|
||||||
|
import { SelectMultipleRelaysInput } from "../widgets/forms/SelectMultipleRelaysInput";
|
||||||
|
import { TextInput } from "../widgets/forms/TextInput";
|
||||||
|
|
||||||
|
export function EditDeviceRelaysDialog(p: {
|
||||||
|
onClose: () => void;
|
||||||
|
relay?: DeviceRelay;
|
||||||
|
device: Device;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [relay, setRelay] = React.useState<DeviceRelay>(
|
||||||
|
p.relay ?? {
|
||||||
|
id: "",
|
||||||
|
name: "relay",
|
||||||
|
enabled: false,
|
||||||
|
priority: 1,
|
||||||
|
consumption: 500,
|
||||||
|
minimal_downtime: 60 * 5,
|
||||||
|
minimal_uptime: 60 * 5,
|
||||||
|
depends_on: [],
|
||||||
|
conflicts_with: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const creating = !p.relay;
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show(
|
||||||
|
`${creating ? "Creating" : "Updating"} relay information`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (creating) await RelayApi.Create(p.device, relay);
|
||||||
|
else await RelayApi.Update(relay);
|
||||||
|
|
||||||
|
snackbar(
|
||||||
|
`The relay have been successfully ${creating ? "created" : "updated"}!`
|
||||||
|
);
|
||||||
|
p.onUpdated();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update device relay information!" + e);
|
||||||
|
alert(`Failed to ${creating ? "create" : "update"} relay! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSubmit =
|
||||||
|
lenValid(relay.name, ServerApi.Config.constraints.relay_name_len) &&
|
||||||
|
relay.priority >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open>
|
||||||
|
<DialogTitle>
|
||||||
|
{creating ? "Create a new relay" : "Edit relay information"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogFormTitle>General info</DialogFormTitle>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Relay name"
|
||||||
|
value={relay.name}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
name: v ?? "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={ServerApi.Config.constraints.dev_name_len}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<CheckboxInput
|
||||||
|
editable
|
||||||
|
label="Enable relay"
|
||||||
|
checked={relay.enabled}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
enabled: v,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Priority"
|
||||||
|
value={relay.priority.toString()}
|
||||||
|
type="number"
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
priority: Number(v) ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={ServerApi.Config.constraints.relay_priority}
|
||||||
|
helperText="Relay priority when selecting relays to turn on. 0 = lowest priority"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Consumption"
|
||||||
|
value={relay.consumption.toString()}
|
||||||
|
type="number"
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
consumption: Number(v) ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={ServerApi.Config.constraints.relay_consumption}
|
||||||
|
helperText="Estimated consumption of device powered by relay"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Minimal uptime"
|
||||||
|
value={relay.minimal_uptime.toString()}
|
||||||
|
type="number"
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
minimal_uptime: Number(v) ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={ServerApi.Config.constraints.relay_minimal_uptime}
|
||||||
|
helperText="Minimal time this relay shall be left on before it can be turned off (in seconds)"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Minimal downtime"
|
||||||
|
value={relay.minimal_downtime.toString()}
|
||||||
|
type="number"
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
minimal_downtime: Number(v) ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={ServerApi.Config.constraints.relay_minimal_downtime}
|
||||||
|
helperText="Minimal time this relay shall be left on before it can be turned off (in seconds)"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<DialogFormTitle>Daily runtime</DialogFormTitle>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<CheckboxInput
|
||||||
|
editable
|
||||||
|
label="Enable minimal runtime"
|
||||||
|
checked={!!relay.daily_runtime}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
daily_runtime: v
|
||||||
|
? { reset_time: 0, min_runtime: 3600, catch_up_hours: [] }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{!!relay.daily_runtime && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
label="Minimal daily runtime"
|
||||||
|
value={relay.daily_runtime!.min_runtime.toString()}
|
||||||
|
type="number"
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
daily_runtime: {
|
||||||
|
...r.daily_runtime!,
|
||||||
|
min_runtime: Number(v),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size={
|
||||||
|
ServerApi.Config.constraints.relay_daily_minimal_runtime
|
||||||
|
}
|
||||||
|
helperText="Minimum time, in seconds, that this relay should run each day"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TimePicker
|
||||||
|
label="Reset time"
|
||||||
|
value={timeOfDay(relay.daily_runtime!.reset_time)}
|
||||||
|
onChange={(d) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
daily_runtime: {
|
||||||
|
...r.daily_runtime!,
|
||||||
|
reset_time: d ? dayjsToTimeOfDay(d) : 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<MultipleSelectInput
|
||||||
|
label="Catchup hours"
|
||||||
|
helperText="The hours during which the relay should be turned on to reach expected runtime"
|
||||||
|
values={Array.apply(null, Array(24)).map((_y, i) => {
|
||||||
|
return {
|
||||||
|
label: `${i.toString().padStart(2, "0")}:00`,
|
||||||
|
value: i,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
selected={relay.daily_runtime!.catch_up_hours}
|
||||||
|
onChange={(d) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
daily_runtime: {
|
||||||
|
...r.daily_runtime!,
|
||||||
|
catch_up_hours: d,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<DialogFormTitle>Constraints</DialogFormTitle>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<SelectMultipleRelaysInput
|
||||||
|
label="Required relays"
|
||||||
|
exclude={[relay.id]}
|
||||||
|
value={relay.depends_on}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
depends_on: v,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
helperText="Relays that must be already up for this relay to be started"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<SelectMultipleRelaysInput
|
||||||
|
label="Conflicting relays"
|
||||||
|
exclude={[relay.id]}
|
||||||
|
value={relay.conflicts_with}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setRelay((r) => {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
conflicts_with: v,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
helperText="Relays that must be off before this relay can be started"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onClose}>Cancel</Button>
|
||||||
|
<Button onClick={onSubmit} autoFocus disabled={!canSubmit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFormTitle(p: React.PropsWithChildren): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span style={{ height: "2px" }}></span>
|
||||||
|
<Typography variant="h6">{p.children}</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -13,9 +13,12 @@ import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { ServerApi } from "./api/ServerApi";
|
import { ServerApi } from "./api/ServerApi";
|
||||||
import { AsyncWidget } from "./widgets/AsyncWidget";
|
import { AsyncWidget } from "./widgets/AsyncWidget";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
<DarkThemeProvider>
|
<DarkThemeProvider>
|
||||||
<AlertDialogProvider>
|
<AlertDialogProvider>
|
||||||
<ConfirmDialogProvider>
|
<ConfirmDialogProvider>
|
||||||
@ -32,5 +35,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
</ConfirmDialogProvider>
|
</ConfirmDialogProvider>
|
||||||
</AlertDialogProvider>
|
</AlertDialogProvider>
|
||||||
</DarkThemeProvider>
|
</DarkThemeProvider>
|
||||||
|
</LocalizationProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
50
central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx
Normal file
50
central_frontend/src/routes/DeviceRoute/DeviceRelays.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
||||||
|
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||||
|
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
||||||
|
|
||||||
|
export function DeviceRelays(p: {
|
||||||
|
device: Device;
|
||||||
|
onReload: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||||
|
const [currRelay, setCurrRelay] = React.useState<DeviceRelay | undefined>();
|
||||||
|
|
||||||
|
const createNewRelay = () => {
|
||||||
|
setDialogOpen(true);
|
||||||
|
setCurrRelay(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dialogOpen && (
|
||||||
|
<EditDeviceRelaysDialog
|
||||||
|
device={p.device}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
relay={currRelay}
|
||||||
|
onUpdated={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
p.onReload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DeviceRouteCard
|
||||||
|
title="Device relays"
|
||||||
|
actions={
|
||||||
|
<Tooltip title="Create new relay">
|
||||||
|
<IconButton
|
||||||
|
onClick={createNewRelay}
|
||||||
|
disabled={p.device.relays.length >= p.device.info.max_relays}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
TODO : relays list ({p.device.relays.length}) relays now)
|
||||||
|
</DeviceRouteCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
93
central_frontend/src/routes/DeviceRoute/DeviceRoute.tsx
Normal file
93
central_frontend/src/routes/DeviceRoute/DeviceRoute.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { Grid, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { Device, DeviceApi } from "../../api/DeviceApi";
|
||||||
|
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||||
|
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||||
|
import { SolarEnergyRouteContainer } from "../../widgets/SolarEnergyRouteContainer";
|
||||||
|
import { GeneralDeviceInfo } from "./GeneralDeviceInfo";
|
||||||
|
import { DeviceRelays } from "./DeviceRelays";
|
||||||
|
|
||||||
|
export function DeviceRoute(): React.ReactElement {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [device, setDevice] = React.useState<Device | undefined>();
|
||||||
|
|
||||||
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setDevice(await DeviceApi.GetSingle(id!));
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
loadKey.current += 1;
|
||||||
|
setDevice(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={loadKey.current}
|
||||||
|
errMsg="Failed to load device information"
|
||||||
|
load={load}
|
||||||
|
ready={!!device}
|
||||||
|
build={() => <DeviceRouteInner device={device!} onReload={reload} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceRouteInner(p: {
|
||||||
|
device: Device;
|
||||||
|
onReload: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const deleteDevice = async (d: Device) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!(await confirm(
|
||||||
|
`Do you really want to delete the device ${d.id}? The operation cannot be reverted!`
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loadingMessage.show("Deleting device...");
|
||||||
|
await DeviceApi.Delete(d);
|
||||||
|
|
||||||
|
snackbar("The device has been successfully deleted!");
|
||||||
|
navigate("/devices");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to delete device! ${e})`);
|
||||||
|
alert("Failed to delete device!");
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SolarEnergyRouteContainer
|
||||||
|
label={`Device ${p.device.name}`}
|
||||||
|
actions={
|
||||||
|
<Tooltip title="Delete device">
|
||||||
|
<IconButton onClick={() => deleteDevice(p.device)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<GeneralDeviceInfo {...p} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<DeviceRelays {...p} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</SolarEnergyRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
24
central_frontend/src/routes/DeviceRoute/DeviceRouteCard.tsx
Normal file
24
central_frontend/src/routes/DeviceRoute/DeviceRouteCard.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Card, Paper, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export function DeviceRouteCard(
|
||||||
|
p: React.PropsWithChildren<{ title: string; actions?: React.ReactElement }>
|
||||||
|
): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Card component={Paper}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" style={{ padding: "6px" }}>
|
||||||
|
{p.title}
|
||||||
|
</Typography>
|
||||||
|
{p.actions}
|
||||||
|
</div>
|
||||||
|
{p.children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { Device } from "../../api/DeviceApi";
|
||||||
|
import { EditDeviceMetadataDialog } from "../../dialogs/EditDeviceMetadataDialog";
|
||||||
|
import { formatDate } from "../../widgets/TimeWidget";
|
||||||
|
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||||
|
|
||||||
|
export function GeneralDeviceInfo(p: {
|
||||||
|
device: Device;
|
||||||
|
onReload: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dialogOpen && (
|
||||||
|
<EditDeviceMetadataDialog
|
||||||
|
device={p.device}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
onUpdated={p.onReload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DeviceRouteCard
|
||||||
|
title="General device information"
|
||||||
|
actions={
|
||||||
|
<Tooltip title="Edit device information">
|
||||||
|
<IconButton onClick={() => setDialogOpen(true)}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table size="small">
|
||||||
|
<TableBody>
|
||||||
|
<DeviceInfoProperty label="ID" value={p.device.id} />
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Reference"
|
||||||
|
value={p.device.info.reference}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty label="Version" value={p.device.info.version} />
|
||||||
|
<DeviceInfoProperty label="Name" value={p.device.name} />
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Description"
|
||||||
|
value={p.device.description}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Created"
|
||||||
|
value={formatDate(p.device.time_create)}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Updated"
|
||||||
|
value={formatDate(p.device.time_update)}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Enabled"
|
||||||
|
value={p.device.enabled ? "YES" : "NO"}
|
||||||
|
color={p.device.enabled ? "green" : "red"}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Maximum number of relays"
|
||||||
|
value={p.device.info.max_relays.toString()}
|
||||||
|
/>
|
||||||
|
<DeviceInfoProperty
|
||||||
|
label="Number of configured relays"
|
||||||
|
value={p.device.relays.length.toString()}
|
||||||
|
/>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DeviceRouteCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceInfoProperty(p: {
|
||||||
|
icon?: React.ReactElement;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color?: string;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<TableRow hover sx={{ "&:last-child td, &:last-child th": { border: 0 } }}>
|
||||||
|
<TableCell>{p.label}</TableCell>
|
||||||
|
<TableCell style={{ color: p.color }}>{p.value}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
Table,
|
Table,
|
||||||
@ -8,18 +9,14 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Device, DeviceApi } from "../api/DeviceApi";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { Device, DeviceApi, DeviceURL } from "../api/DeviceApi";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
||||||
import { TimeWidget } from "../widgets/TimeWidget";
|
import { TimeWidget } from "../widgets/TimeWidget";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
|
||||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
|
|
||||||
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
|
||||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
|
||||||
|
|
||||||
export function DevicesRoute(): React.ReactElement {
|
export function DevicesRoute(): React.ReactElement {
|
||||||
const loadKey = React.useRef(1);
|
const loadKey = React.useRef(1);
|
||||||
@ -61,32 +58,7 @@ function ValidatedDevicesList(p: {
|
|||||||
list: Device[];
|
list: Device[];
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const alert = useAlert();
|
const navigate = useNavigate();
|
||||||
const confirm = useConfirm();
|
|
||||||
const snackbar = useSnackbar();
|
|
||||||
const loadingMessage = useLoadingMessage();
|
|
||||||
|
|
||||||
const deleteDevice = async (d: Device) => {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!(await confirm(
|
|
||||||
`Do you really want to delete the device ${d.id}? The operation cannot be reverted!`
|
|
||||||
))
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
loadingMessage.show("Deleting device...");
|
|
||||||
await DeviceApi.Delete(d);
|
|
||||||
|
|
||||||
snackbar("The device has been successfully deleted!");
|
|
||||||
p.onReload();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to delete device! ${e})`);
|
|
||||||
alert("Failed to delete device!");
|
|
||||||
} finally {
|
|
||||||
loadingMessage.hide();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (p.list.length === 0) {
|
if (p.list.length === 0) {
|
||||||
return <p>There is no device validated yet.</p>;
|
return <p>There is no device validated yet.</p>;
|
||||||
@ -108,7 +80,11 @@ function ValidatedDevicesList(p: {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{p.list.map((dev) => (
|
{p.list.map((dev) => (
|
||||||
<TableRow key={dev.id}>
|
<TableRow
|
||||||
|
hover
|
||||||
|
key={dev.id}
|
||||||
|
onDoubleClick={() => navigate(DeviceURL(dev))}
|
||||||
|
>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{dev.id}
|
{dev.id}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -122,10 +98,12 @@ function ValidatedDevicesList(p: {
|
|||||||
<TimeWidget time={dev.time_update} />
|
<TimeWidget time={dev.time_update} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Tooltip title="Delete device">
|
<Tooltip title="Open device page">
|
||||||
<IconButton onClick={() => deleteDevice(dev)}>
|
<Link to={DeviceURL(dev)}>
|
||||||
<DeleteIcon />
|
<IconButton>
|
||||||
|
<VisibilityIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -10,7 +10,6 @@ import Paper from "@mui/material/Paper";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
|
||||||
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
import { AuthApi } from "../api/AuthApi";
|
import { AuthApi } from "../api/AuthApi";
|
||||||
|
|
||||||
|
@ -1,6 +1,29 @@
|
|||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current UNIX time, in seconds
|
* Get current UNIX time, in seconds
|
||||||
*/
|
*/
|
||||||
export function time(): number {
|
export function time(): number {
|
||||||
return Math.floor(new Date().getTime() / 1000);
|
return Math.floor(new Date().getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dayjs representation of given time of day
|
||||||
|
*/
|
||||||
|
export function timeOfDay(time: number): Dayjs {
|
||||||
|
const hours = Math.floor(time / 3600);
|
||||||
|
const minutes = Math.floor(time / 60) - hours * 60;
|
||||||
|
|
||||||
|
return dayjs(
|
||||||
|
`2022-04-17T${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get time of day (in secs) from a given dayjs representation
|
||||||
|
*/
|
||||||
|
export function dayjsToTimeOfDay(d: Dayjs): number {
|
||||||
|
return d.hour() * 3600 + d.minute() * 60 + d.second();
|
||||||
|
}
|
||||||
|
8
central_frontend/src/utils/StringsUtils.ts
Normal file
8
central_frontend/src/utils/StringsUtils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { LenConstraint } from "../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a string length is valid or not
|
||||||
|
*/
|
||||||
|
export function lenValid(s: string, c: LenConstraint): boolean {
|
||||||
|
return s.length >= c.min && s.length <= c.max;
|
||||||
|
}
|
21
central_frontend/src/widgets/forms/CheckboxInput.tsx
Normal file
21
central_frontend/src/widgets/forms/CheckboxInput.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||||
|
|
||||||
|
export function CheckboxInput(p: {
|
||||||
|
editable: boolean;
|
||||||
|
label: string;
|
||||||
|
checked: boolean | undefined;
|
||||||
|
onValueChange: (v: boolean) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!p.editable}
|
||||||
|
checked={p.checked}
|
||||||
|
onChange={(e) => p.onValueChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={p.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
113
central_frontend/src/widgets/forms/MultipleSelectInput.tsx
Normal file
113
central_frontend/src/widgets/forms/MultipleSelectInput.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
OutlinedInput,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
MenuItem,
|
||||||
|
Theme,
|
||||||
|
useTheme,
|
||||||
|
SelectChangeEvent,
|
||||||
|
FormHelperText,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface Value<E> {
|
||||||
|
label: string;
|
||||||
|
value: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 48;
|
||||||
|
const ITEM_PADDING_TOP = 8;
|
||||||
|
|
||||||
|
const MenuProps = {
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStyles<E>(v: Value<E>, selected: readonly E[], theme: Theme) {
|
||||||
|
return {
|
||||||
|
fontWeight:
|
||||||
|
selected.find((e) => e === v.value) === undefined
|
||||||
|
? theme.typography.fontWeightRegular
|
||||||
|
: theme.typography.fontWeightMedium,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultipleSelectInput<E>(p: {
|
||||||
|
values: Value<E>[];
|
||||||
|
selected: E[];
|
||||||
|
label: string;
|
||||||
|
onChange: (selected: E[]) => void;
|
||||||
|
helperText?: string;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [labelId] = React.useState(`id-multi-${Math.random()}`);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const handleChange = (event: SelectChangeEvent<E>) => {
|
||||||
|
const {
|
||||||
|
target: { value },
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
const values: any[] =
|
||||||
|
typeof value === "string" ? value.split(",") : (value as any);
|
||||||
|
|
||||||
|
const newVals = values.map(
|
||||||
|
(v) => p.values.find((e) => String(e.value) === String(v))!.value
|
||||||
|
);
|
||||||
|
|
||||||
|
// Values that appear multiple times are toggled
|
||||||
|
const setVal = new Set<E>();
|
||||||
|
for (const el of newVals) {
|
||||||
|
if (!setVal.has(el)) setVal.add(el);
|
||||||
|
else setVal.delete(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.onChange([...setVal]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id={labelId}>{p.label}</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
labelId={labelId}
|
||||||
|
id="bad"
|
||||||
|
label={p.label}
|
||||||
|
value={p.selected as any}
|
||||||
|
onChange={handleChange}
|
||||||
|
input={<OutlinedInput id="select-multiple-chip" label="Chip" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||||
|
{(selected as Array<E>).map((value) => (
|
||||||
|
<Chip
|
||||||
|
key={String(value)}
|
||||||
|
label={p.values.find((e) => e.value === value)!.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
MenuProps={MenuProps}
|
||||||
|
>
|
||||||
|
{p.values.map((v) => (
|
||||||
|
<MenuItem
|
||||||
|
key={v.label + String(v)}
|
||||||
|
value={String(v.value)}
|
||||||
|
style={getStyles(v, p.selected, theme)}
|
||||||
|
>
|
||||||
|
{v.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{p.helperText && <FormHelperText>{p.helperText}</FormHelperText>}
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { DeviceRelay, RelayID } from "../../api/DeviceApi";
|
||||||
|
import { RelayApi } from "../../api/RelayApi";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
|
import { MultipleSelectInput } from "./MultipleSelectInput";
|
||||||
|
|
||||||
|
export function SelectMultipleRelaysInput(p: {
|
||||||
|
label: string;
|
||||||
|
value: RelayID[];
|
||||||
|
onValueChange: (ids: RelayID[]) => void;
|
||||||
|
exclude?: RelayID[];
|
||||||
|
helperText?: string;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [list, setList] = React.useState<DeviceRelay[]>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setList(await RelayApi.GetList());
|
||||||
|
};
|
||||||
|
|
||||||
|
const values =
|
||||||
|
list?.map((r) => {
|
||||||
|
return {
|
||||||
|
label: r.name,
|
||||||
|
value: r.id,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={1}
|
||||||
|
load={load}
|
||||||
|
errMsg="Failed to load the list of relays!"
|
||||||
|
build={() => (
|
||||||
|
<MultipleSelectInput
|
||||||
|
label={p.label}
|
||||||
|
onChange={p.onValueChange}
|
||||||
|
selected={p.value}
|
||||||
|
helperText={p.helperText}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
61
central_frontend/src/widgets/forms/TextInput.tsx
Normal file
61
central_frontend/src/widgets/forms/TextInput.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { TextField } from "@mui/material";
|
||||||
|
import { LenConstraint } from "../../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text property edition
|
||||||
|
*/
|
||||||
|
export function TextInput(p: {
|
||||||
|
label?: string;
|
||||||
|
editable: boolean;
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (newVal: string | undefined) => void;
|
||||||
|
size?: LenConstraint;
|
||||||
|
checkValue?: (s: string) => boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
type?: React.HTMLInputTypeAttribute;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
helperText?: string;
|
||||||
|
}): React.ReactElement {
|
||||||
|
if (!p.editable && (p.value ?? "") === "") return <></>;
|
||||||
|
|
||||||
|
let valueError = undefined;
|
||||||
|
if (p.value && p.value.length > 0) {
|
||||||
|
if (p.size?.min && p.type !== "number" && p.value.length < p.size.min)
|
||||||
|
valueError = "Value is too short!";
|
||||||
|
if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!";
|
||||||
|
if (
|
||||||
|
p.type === "number" &&
|
||||||
|
p.size &&
|
||||||
|
(Number(p.value) > p.size.max || Number(p.value) < p.size.min)
|
||||||
|
)
|
||||||
|
valueError = "Invalide size range!";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
label={p.label}
|
||||||
|
value={p.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
p.onValueChange?.(
|
||||||
|
e.target.value.length === 0 ? undefined : e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
maxLength: p.size?.max,
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: !p.editable,
|
||||||
|
type: p.type,
|
||||||
|
}}
|
||||||
|
variant={"standard"}
|
||||||
|
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
||||||
|
multiline={p.multiline}
|
||||||
|
minRows={p.minRows}
|
||||||
|
maxRows={p.maxRows}
|
||||||
|
error={valueError !== undefined}
|
||||||
|
helperText={valueError ?? p.helperText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user