Compare commits
58 Commits
88a24565b4
...
renovate/e
| Author | SHA1 | Date | |
|---|---|---|---|
| 5319276be5 | |||
| c217e5f944 | |||
| 247d1dd4ec | |||
| c6a43ec868 | |||
| 53a1c7e0e9 | |||
| 852c58b560 | |||
| cb28426b23 | |||
| 4a2828aa0c | |||
| 26502589ac | |||
| f0d210782e | |||
| 1f1c16fc9c | |||
| 16f0c9cee2 | |||
| ae001316d5 | |||
| 572b43d9ac | |||
| c6c4eae7a2 | |||
| 831d4d444c | |||
| cd19f1d69b | |||
| 4ca2d7a763 | |||
| 9c7df89bce | |||
| 040def4089 | |||
| ad27b656f4 | |||
| 53e4d09d6e | |||
| e0199b8ce9 | |||
| 26c301d8ac | |||
| ab9ed2ad63 | |||
| 38f06b45b9 | |||
| bfa6dfad0c | |||
| cb06524706 | |||
| 517b6a2575 | |||
| 98c30ca164 | |||
| a6b5151120 | |||
| 8ecbcf7177 | |||
| c861f84967 | |||
| 75d566e4be | |||
| 42457901b1 | |||
| 849ee1121d | |||
| af9d1a9a74 | |||
| 07e3a7b371 | |||
| be6f57ce17 | |||
| 46870340a7 | |||
| 527ef86714 | |||
| b7d97218d7 | |||
| d8ebf6845d | |||
| ba8bfffc1b | |||
| ff44859c2d | |||
| 66923ab226 | |||
| 3f3b1282bd | |||
| 95a1a446db | |||
| f5fe5b5339 | |||
| c844490c4e | |||
| fe951f4004 | |||
| 8c92b8bf27 | |||
| 8d715b407e | |||
| f11d25b2f0 | |||
| 35e64e977b | |||
| 4d46f9c52c | |||
| c90a05fcfd | |||
| abdca20a66 |
35
.drone.yml
35
.drone.yml
@@ -46,6 +46,11 @@ steps:
|
|||||||
path: /usr/local/cargo/registry
|
path: /usr/local/cargo/registry
|
||||||
- name: web_app
|
- name: web_app
|
||||||
path: /tmp/web_build
|
path: /tmp/web_build
|
||||||
|
- name: releases
|
||||||
|
path: /tmp/releases
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend_check
|
- backend_check
|
||||||
- web_build
|
- web_build
|
||||||
@@ -54,13 +59,41 @@ steps:
|
|||||||
- mv /tmp/web_build/dist static
|
- mv /tmp/web_build/dist static
|
||||||
- cargo build --release
|
- cargo build --release
|
||||||
- ls -lah target/release/central_backend
|
- ls -lah target/release/central_backend
|
||||||
|
- mv target/release/central_backend /tmp/releases/central_backend
|
||||||
|
|
||||||
|
# Build ESP32 program
|
||||||
- name: esp32_compile
|
- name: esp32_compile
|
||||||
image: espressif/idf:v5.5.1
|
image: espressif/idf:v5.5.1
|
||||||
|
volumes:
|
||||||
|
- name: releases
|
||||||
|
path: /tmp/releases
|
||||||
commands:
|
commands:
|
||||||
- cd esp32_device
|
- cd esp32_device
|
||||||
- /opt/esp/entrypoint.sh idf.py build
|
- /opt/esp/entrypoint.sh idf.py build
|
||||||
- ls -lah build/main.bin
|
- ls -lah build/main.bin
|
||||||
|
- cp build/main.bin /tmp/releases/wt32-eth01.bin
|
||||||
|
|
||||||
|
# Auto-release to Gitea
|
||||||
|
- name: gitea_release
|
||||||
|
image: plugins/gitea-release
|
||||||
|
depends_on:
|
||||||
|
- backend_compile
|
||||||
|
- esp32_compile
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
volumes:
|
||||||
|
- name: releases
|
||||||
|
path: /tmp/releases
|
||||||
|
environment:
|
||||||
|
PLUGIN_API_KEY:
|
||||||
|
from_secret: GITEA_API_KEY # needs permission write:repository
|
||||||
|
settings:
|
||||||
|
base_url: https://gitea.communiquons.org
|
||||||
|
files:
|
||||||
|
- /tmp/releases/central_backend
|
||||||
|
- /tmp/releases/wt32-eth01.bin
|
||||||
|
checksum: sha512
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
@@ -68,3 +101,5 @@ volumes:
|
|||||||
temp: {}
|
temp: {}
|
||||||
- name: web_app
|
- name: web_app
|
||||||
temp: {}
|
temp: {}
|
||||||
|
- name: releases
|
||||||
|
temp: {}
|
||||||
|
|||||||
47
central_backend/Cargo.lock
generated
47
central_backend/Cargo.lock
generated
@@ -640,7 +640,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "central_backend"
|
name = "central_backend"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
@@ -669,7 +669,7 @@ dependencies = [
|
|||||||
"openssl",
|
"openssl",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"prettytable-rs",
|
"prettytable-rs",
|
||||||
"rand 0.10.0-rc.0",
|
"rand 0.10.0-rc.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -692,13 +692,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.0-rc.2"
|
version = "0.10.0-rc.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9bd162f2b8af3e0639d83f28a637e4e55657b7a74508dba5a9bf4da523d5c9e9"
|
checksum = "99cbf41c6ec3c4b9eaf7f8f5c11a72cd7d3aa0428125c20d5ef4d09907a0f019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.10.0-rc-2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -726,9 +726,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.50"
|
version = "4.5.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -736,9 +736,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.50"
|
version = "4.5.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -1959,9 +1959,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy-regex"
|
name = "lazy-regex"
|
||||||
version = "3.4.1"
|
version = "3.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126"
|
checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy-regex-proc_macros",
|
"lazy-regex-proc_macros",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1970,9 +1970,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy-regex-proc_macros"
|
name = "lazy-regex-proc_macros"
|
||||||
version = "3.4.1"
|
version = "3.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1"
|
checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2235,9 +2235,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.74"
|
version = "0.10.75"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
|
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -2267,9 +2267,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.110"
|
version = "0.9.111"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2522,12 +2522,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.0-rc.0"
|
version = "0.10.0-rc.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ec474812b9de55111b29da8a1559f1718ef3dc20fa36f031f1b5d9e3836ad6c"
|
checksum = "be866deebbade98028b705499827ad6967c8bb1e21f96a2609913c8c076e9307"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"rand_core 0.9.3",
|
"getrandom 0.3.2",
|
||||||
|
"rand_core 0.10.0-rc-2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2568,6 +2569,12 @@ dependencies = [
|
|||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.0-rc-2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "104a23e4e8b77312a823b6b5613edbac78397e2f34320bc7ac4277013ec4478e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.10"
|
version = "0.5.10"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "central_backend"
|
name = "central_backend"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -8,11 +8,11 @@ log = "0.4.28"
|
|||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
clap = { version = "4.5.50", features = ["derive", "env"] }
|
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
openssl = { version = "0.10.74" }
|
openssl = { version = "0.10.75" }
|
||||||
openssl-sys = "0.9.110"
|
openssl-sys = "0.9.111"
|
||||||
libc = "0.2.177"
|
libc = "0.2.177"
|
||||||
foreign-types-shared = "0.1.1"
|
foreign-types-shared = "0.1.1"
|
||||||
asn1 = "0.23.0"
|
asn1 = "0.23.0"
|
||||||
@@ -21,7 +21,7 @@ futures = "0.3.31"
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.24", features = ["json"] }
|
reqwest = { version = "0.12.24", features = ["json"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
rand = "0.10.0-rc.0"
|
rand = "0.10.0-rc.5"
|
||||||
actix = "0.13.5"
|
actix = "0.13.5"
|
||||||
actix-identity = "0.9.0"
|
actix-identity = "0.9.0"
|
||||||
actix-session = { version = "0.11.0", features = ["cookie-session"] }
|
actix-session = { version = "0.11.0", features = ["cookie-session"] }
|
||||||
@@ -31,7 +31,7 @@ actix-remote-ip = "0.1.0"
|
|||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
||||||
semver = { version = "1.0.27", features = ["serde"] }
|
semver = { version = "1.0.27", features = ["serde"] }
|
||||||
lazy-regex = "3.4.1"
|
lazy-regex = "3.4.2"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
tokio_schedule = "0.3.2"
|
tokio_schedule = "0.3.2"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|||||||
936
central_frontend/package-lock.json
generated
936
central_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,34 +12,34 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.8",
|
"@fontsource/roboto": "^5.2.9",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^7.3.4",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.4",
|
"@mui/material": "^7.3.5",
|
||||||
"@mui/x-charts": "^8.15.0",
|
"@mui/x-charts": "^8.19.0",
|
||||||
"@mui/x-date-pickers": "^8.15.0",
|
"@mui/x-date-pickers": "^8.19.0",
|
||||||
"date-and-time": "^4.1.0",
|
"date-and-time": "^4.1.1",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"filesize": "^11.0.13",
|
"filesize": "^11.0.13",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.6",
|
||||||
"semver": "^7.7.3"
|
"semver": "^7.7.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.46.4",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "^8.46.3",
|
||||||
"vite": "^7.1.12"
|
"vite": "^7.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { APIClient } from "./ApiClient";
|
import { APIClient } from "./ApiClient";
|
||||||
import { Device, DeviceRelay } from "./DeviceApi";
|
import { Device, DeviceRelay } from "./DeviceApi";
|
||||||
|
|
||||||
|
export type RelayForcedState =
|
||||||
|
| { type: "None" }
|
||||||
|
| { type: "Off" | "On"; until: number };
|
||||||
|
|
||||||
|
export type SetRelayForcedState =
|
||||||
|
| { type: "None" }
|
||||||
|
| { type: "Off" | "On"; for_secs: number };
|
||||||
|
|
||||||
export interface RelayStatus {
|
export interface RelayStatus {
|
||||||
id: string;
|
id: string;
|
||||||
on: boolean;
|
on: boolean;
|
||||||
for: number;
|
for: number;
|
||||||
|
forced_state: RelayForcedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelaysStatus = Map<string, RelayStatus>;
|
export type RelaysStatus = Map<string, RelayStatus>;
|
||||||
@@ -48,6 +57,20 @@ export class RelayApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set relay forced state
|
||||||
|
*/
|
||||||
|
static async SetForcedState(
|
||||||
|
relay: DeviceRelay,
|
||||||
|
forced: SetRelayForcedState
|
||||||
|
): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "PUT",
|
||||||
|
uri: `/relay/${relay.id}/forced_state`,
|
||||||
|
jsonData: forced,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a relay configuration
|
* Delete a relay configuration
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DeviceRelay } from "../api/DeviceApi";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function SelectForcedStateDurationDialog(p: {
|
||||||
|
relay: DeviceRelay;
|
||||||
|
forcedState: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (duration: number) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [duration, setDuration] = React.useState(60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onClose={p.onCancel}>
|
||||||
|
<DialogTitle>Set forced relay state</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Please specify the number of minutes the relay <i>{p.relay.name}</i>{" "}
|
||||||
|
will remain in forced state <i>{p.forcedState}</i>:
|
||||||
|
</DialogContentText>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Duration (min)"
|
||||||
|
variant="standard"
|
||||||
|
value={Math.floor(duration / 60)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Number.parseInt(e.target.value);
|
||||||
|
setDuration((Number.isNaN(val) ? 1 : val) * 60);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
style={{ marginTop: "5px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>Equivalent in seconds: {duration} secs</p>
|
||||||
|
<p>Equivalent in hours: {duration / 3600} hours</p>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onCancel}>Cancel</Button>
|
||||||
|
<Button onClick={() => p.onSubmit(duration)} autoFocus>
|
||||||
|
Start timer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,16 +10,16 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
import { Device, DeviceRelay } from "../../api/DeviceApi";
|
||||||
|
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
||||||
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
|
||||||
import { DeviceRouteCard } from "./DeviceRouteCard";
|
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||||
import { RelayApi, RelayStatus } from "../../api/RelayApi";
|
|
||||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
|
||||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||||
import { TimeWidget } from "../../widgets/TimeWidget";
|
|
||||||
import { BoolText } from "../../widgets/BoolText";
|
import { BoolText } from "../../widgets/BoolText";
|
||||||
|
import { TimeWidget } from "../../widgets/TimeWidget";
|
||||||
|
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||||
|
|
||||||
export function DeviceRelays(p: {
|
export function DeviceRelays(p: {
|
||||||
device: Device;
|
device: Device;
|
||||||
@@ -145,7 +145,8 @@ function RelayEntryStatus(
|
|||||||
errMsg="Failed to load relay status!"
|
errMsg="Failed to load relay status!"
|
||||||
build={() => (
|
build={() => (
|
||||||
<>
|
<>
|
||||||
<BoolText val={state!.on} positive="ON" negative="OFF" /> for{" "}
|
<BoolText val={state!.on} positive="ON" negative="OFF" />{" "}
|
||||||
|
{state?.forced_state.type !== "None" && <b>Forced</b>} for{" "}
|
||||||
<TimeWidget diff time={state!.for} />
|
<TimeWidget diff time={state!.for} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import React from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
|
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
|
||||||
import { RelayApi, RelaysStatus } from "../api/RelayApi";
|
import { RelayApi, RelaysStatus } from "../api/RelayApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { BoolText } from "../widgets/BoolText";
|
import { BoolText } from "../widgets/BoolText";
|
||||||
|
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
||||||
|
import { RelayForcedState } from "../widgets/RelayForcedState";
|
||||||
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
|
||||||
import { TimeWidget } from "../widgets/TimeWidget";
|
import { TimeWidget } from "../widgets/TimeWidget";
|
||||||
import { CopyToClipboard } from "../widgets/CopyToClipboard";
|
|
||||||
import { ServerApi } from "../api/ServerApi";
|
|
||||||
|
|
||||||
export function RelaysListRoute(p: {
|
export function RelaysListRoute(p: {
|
||||||
homeWidget?: boolean;
|
homeWidget?: boolean;
|
||||||
@@ -104,6 +105,7 @@ function RelaysList(p: {
|
|||||||
<TableCell>Priority</TableCell>
|
<TableCell>Priority</TableCell>
|
||||||
<TableCell>Consumption</TableCell>
|
<TableCell>Consumption</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell>Forced state</TableCell>
|
||||||
<TableCell></TableCell>
|
<TableCell></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -129,6 +131,13 @@ function RelaysList(p: {
|
|||||||
/>{" "}
|
/>{" "}
|
||||||
for <TimeWidget diff time={p.status.get(row.id)!.for} />
|
for <TimeWidget diff time={p.status.get(row.id)!.for} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<RelayForcedState
|
||||||
|
relay={row}
|
||||||
|
state={p.status.get(row.id)!}
|
||||||
|
onUpdated={p.onReload}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Tooltip title="Copy legacy api status">
|
<Tooltip title="Copy legacy api status">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
|
|||||||
79
central_frontend/src/widgets/RelayForcedState.tsx
Normal file
79
central_frontend/src/widgets/RelayForcedState.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
|
||||||
|
import { DeviceRelay } from "../api/DeviceApi";
|
||||||
|
import { RelayApi, RelayStatus, SetRelayForcedState } from "../api/RelayApi";
|
||||||
|
import { TimeWidget } from "./TimeWidget";
|
||||||
|
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
|
||||||
|
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||||
|
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||||
|
import React from "react";
|
||||||
|
import { SelectForcedStateDurationDialog } from "../dialogs/SelectForcedStateDurationDialog";
|
||||||
|
|
||||||
|
export function RelayForcedState(p: {
|
||||||
|
relay: DeviceRelay;
|
||||||
|
state: RelayStatus;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [futureStateType, setFutureStateType] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const handleChange = (event: SelectChangeEvent) => {
|
||||||
|
if (event.target.value == "None") {
|
||||||
|
submitChange({ type: "None" });
|
||||||
|
} else {
|
||||||
|
setFutureStateType(event.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitChange = async (state: SetRelayForcedState) => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Setting forced state...");
|
||||||
|
await RelayApi.SetForcedState(p.relay, state);
|
||||||
|
p.onUpdated();
|
||||||
|
snackbar("Forced state successfully updated!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to set relay forced state! ${e}`);
|
||||||
|
alert(`Failed to set loading state for relay! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
value={p.state.forced_state.type}
|
||||||
|
onChange={handleChange}
|
||||||
|
size="small"
|
||||||
|
variant="standard"
|
||||||
|
>
|
||||||
|
<MenuItem value={"None"}>None</MenuItem>
|
||||||
|
<MenuItem value={"Off"}>Off</MenuItem>
|
||||||
|
<MenuItem value={"On"}>On</MenuItem>
|
||||||
|
</Select>
|
||||||
|
{p.state.forced_state.type !== "None" && (
|
||||||
|
<>
|
||||||
|
<TimeWidget future time={p.state.forced_state.until} /> left
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{futureStateType !== undefined && (
|
||||||
|
<SelectForcedStateDurationDialog
|
||||||
|
{...p}
|
||||||
|
forcedState={futureStateType}
|
||||||
|
onCancel={() => setFutureStateType(undefined)}
|
||||||
|
onSubmit={(d) =>
|
||||||
|
submitChange({
|
||||||
|
type: futureStateType as any,
|
||||||
|
for_secs: d,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -51,13 +51,14 @@ export function timeDiff(a: number, b: number): string {
|
|||||||
return `${diffYears} years`;
|
return `${diffYears} years`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeDiffFromNow(t: number): string {
|
export function timeDiffFromNow(t: number, future?: boolean): string {
|
||||||
return timeDiff(t, time());
|
return future ? timeDiff(time(), t) : timeDiff(t, time());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimeWidget(p: {
|
export function TimeWidget(p: {
|
||||||
time?: number;
|
time?: number;
|
||||||
diff?: boolean;
|
diff?: boolean;
|
||||||
|
future?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.time) return <></>;
|
if (!p.time) return <></>;
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +66,9 @@ export function TimeWidget(p: {
|
|||||||
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
|
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<span>{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time)}</span>
|
<span>
|
||||||
|
{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time, p.future)}
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
627
custom_consumption/Cargo.lock
generated
627
custom_consumption/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
clap = { version = "4.5.50", features = ["derive", "env"] }
|
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||||
egui = "0.32.3"
|
egui = "0.32.3"
|
||||||
eframe = "0.32.3"
|
eframe = "0.33.2"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.2
|
1.0.3
|
||||||
Reference in New Issue
Block a user