Compare commits
1 Commits
renovate/e
...
95e5797f76
| Author | SHA1 | Date | |
|---|---|---|---|
| 95e5797f76 |
37
.drone.yml
37
.drone.yml
@@ -46,11 +46,6 @@ 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
|
||||||
@@ -59,41 +54,13 @@ 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.4.2
|
||||||
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:
|
||||||
@@ -101,5 +68,3 @@ volumes:
|
|||||||
temp: {}
|
temp: {}
|
||||||
- name: web_app
|
- name: web_app
|
||||||
temp: {}
|
temp: {}
|
||||||
- name: releases
|
|
||||||
temp: {}
|
|
||||||
|
|||||||
694
central_backend/Cargo.lock
generated
694
central_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,46 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "central_backend"
|
name = "central_backend"
|
||||||
version = "1.0.3"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.28"
|
log = "0.4.27"
|
||||||
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.46", features = ["derive", "env"] }
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.99"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.12"
|
||||||
openssl = { version = "0.10.74" }
|
openssl = { version = "0.10.73" }
|
||||||
openssl-sys = "0.9.110"
|
openssl-sys = "0.9.109"
|
||||||
libc = "0.2.177"
|
libc = "0.2.175"
|
||||||
foreign-types-shared = "0.1.1"
|
foreign-types-shared = "0.1.1"
|
||||||
asn1 = "0.23.0"
|
asn1 = "0.22.0"
|
||||||
actix-web = { version = "4.11.0", features = ["openssl"] }
|
actix-web = { version = "4.11.0", features = ["openssl"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.24", features = ["json"] }
|
reqwest = { version = "0.12.22", features = ["json"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.142"
|
||||||
rand = "0.10.0-rc.0"
|
rand = "0.9.2"
|
||||||
actix = "0.13.5"
|
actix = "0.13.5"
|
||||||
actix-identity = "0.9.0"
|
actix-identity = "0.8.0"
|
||||||
actix-session = { version = "0.11.0", features = ["cookie-session"] }
|
actix-session = { version = "0.10.1", features = ["cookie-session"] }
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-multipart = { version = "0.7.2", features = ["derive"] }
|
actix-multipart = { version = "0.7.2", features = ["derive"] }
|
||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||||
semver = { version = "1.0.27", features = ["serde"] }
|
semver = { version = "1.0.26", features = ["serde"] }
|
||||||
lazy-regex = "3.4.1"
|
lazy-regex = "3.4.1"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
tokio_schedule = "0.3.2"
|
tokio_schedule = "0.3.2"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
rust-embed = "8.8.0"
|
rust-embed = "8.7.2"
|
||||||
jsonwebtoken = { version = "10.1.0", features = ["use_pem", "rust_crypto"] }
|
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
|
||||||
prettytable-rs = "0.10.0"
|
prettytable-rs = "0.10.0"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.41"
|
||||||
serde_yml = "0.0.12"
|
serde_yml = "0.0.12"
|
||||||
bincode = "2.0.1"
|
bincode = "2.0.1"
|
||||||
fs4 = { version = "0.13.1", features = ["sync"] }
|
fs4 = { version = "0.13.1", features = ["sync"] }
|
||||||
zip = { version = "6.0.0", features = ["bzip2"] }
|
zip = { version = "2.2.0", features = ["bzip2"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
devices:
|
|
||||||
- id: dev1
|
|
||||||
info:
|
|
||||||
reference: A
|
|
||||||
version: 0.0.1
|
|
||||||
max_relays: 1
|
|
||||||
time_create: 1
|
|
||||||
time_update: 1
|
|
||||||
name: Dev1
|
|
||||||
description: Day1
|
|
||||||
validated: true
|
|
||||||
enabled: true
|
|
||||||
relays:
|
|
||||||
- id: dcb3fd91-bf9b-4de3-99e5-92c1c7dd72e9
|
|
||||||
name: R1
|
|
||||||
enabled: true
|
|
||||||
priority: 1
|
|
||||||
consumption: 100
|
|
||||||
minimal_uptime: 10
|
|
||||||
minimal_downtime: 10
|
|
||||||
depends_on: []
|
|
||||||
conflicts_with: []
|
|
||||||
|
|
||||||
on: false
|
|
||||||
for: 5000
|
|
||||||
forced_state:
|
|
||||||
type: Off
|
|
||||||
for_secs: 500
|
|
||||||
should_be_on: false
|
|
||||||
|
|
||||||
online: true
|
|
||||||
|
|
||||||
curr_consumption: -10000
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
devices:
|
|
||||||
- id: dev1
|
|
||||||
info:
|
|
||||||
reference: A
|
|
||||||
version: 0.0.1
|
|
||||||
max_relays: 1
|
|
||||||
time_create: 1
|
|
||||||
time_update: 1
|
|
||||||
name: Dev1
|
|
||||||
description: Day1
|
|
||||||
validated: true
|
|
||||||
enabled: true
|
|
||||||
relays:
|
|
||||||
- id: dcb3fd91-bf9b-4de3-99e5-92c1c7dd72e9
|
|
||||||
name: R1
|
|
||||||
enabled: true
|
|
||||||
priority: 1
|
|
||||||
consumption: 100
|
|
||||||
minimal_uptime: 10
|
|
||||||
minimal_downtime: 10
|
|
||||||
depends_on: []
|
|
||||||
conflicts_with: []
|
|
||||||
|
|
||||||
on: false
|
|
||||||
for: 500
|
|
||||||
forced_state:
|
|
||||||
type: On
|
|
||||||
for_secs: 500
|
|
||||||
should_be_on: true
|
|
||||||
|
|
||||||
- id: dcb3fd91-bf9b-4de3-99e5-92c1c7dd72f0
|
|
||||||
name: R2
|
|
||||||
enabled: true
|
|
||||||
priority: 1
|
|
||||||
consumption: 100
|
|
||||||
minimal_uptime: 10
|
|
||||||
minimal_downtime: 10
|
|
||||||
depends_on: [ ]
|
|
||||||
conflicts_with: [ ]
|
|
||||||
|
|
||||||
on: false
|
|
||||||
for: 500
|
|
||||||
forced_state:
|
|
||||||
type: None
|
|
||||||
should_be_on: false
|
|
||||||
|
|
||||||
online: true
|
|
||||||
|
|
||||||
curr_consumption: 10000
|
|
||||||
@@ -8,7 +8,7 @@ use crate::energy::consumption;
|
|||||||
use crate::energy::consumption::EnergyConsumption;
|
use crate::energy::consumption::EnergyConsumption;
|
||||||
use crate::energy::consumption_cache::ConsumptionCache;
|
use crate::energy::consumption_cache::ConsumptionCache;
|
||||||
use crate::energy::consumption_history_file::ConsumptionHistoryFile;
|
use crate::energy::consumption_history_file::ConsumptionHistoryFile;
|
||||||
use crate::energy::engine::{EnergyEngine, RelayForcedState};
|
use crate::energy::engine::EnergyEngine;
|
||||||
use crate::utils::time_utils::time_secs;
|
use crate::utils::time_utils::time_secs;
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use openssl::x509::X509Req;
|
use openssl::x509::X509Req;
|
||||||
@@ -328,19 +328,6 @@ impl Handler<UpdateDeviceRelay> for EnergyActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message)]
|
|
||||||
#[rtype(result = "anyhow::Result<()>")]
|
|
||||||
pub struct SetRelayForcedState(pub DeviceRelayID, pub RelayForcedState);
|
|
||||||
|
|
||||||
impl Handler<SetRelayForcedState> for EnergyActor {
|
|
||||||
type Result = anyhow::Result<()>;
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: SetRelayForcedState, _ctx: &mut Context<Self>) -> Self::Result {
|
|
||||||
self.engine.relay_state(msg.0).set_forced(msg.1);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a device relay
|
/// Delete a device relay
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
#[rtype(result = "anyhow::Result<()>")]
|
#[rtype(result = "anyhow::Result<()>")]
|
||||||
@@ -421,7 +408,6 @@ pub struct ResRelayState {
|
|||||||
pub id: DeviceRelayID,
|
pub id: DeviceRelayID,
|
||||||
pub on: bool,
|
pub on: bool,
|
||||||
pub r#for: usize,
|
pub r#for: usize,
|
||||||
pub forced_state: RelayForcedState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the state of all relays
|
/// Get the state of all relays
|
||||||
@@ -441,7 +427,6 @@ impl Handler<GetAllRelaysState> for EnergyActor {
|
|||||||
id: d.id,
|
id: d.id,
|
||||||
on: state.is_on(),
|
on: state.is_on(),
|
||||||
r#for: state.state_for(),
|
r#for: state.state_for(),
|
||||||
forced_state: state.actual_forced_state(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,83 +25,19 @@ impl DeviceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum SetRelayForcedStateReq {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Off {
|
|
||||||
for_secs: u64,
|
|
||||||
},
|
|
||||||
On {
|
|
||||||
for_secs: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SetRelayForcedStateReq {
|
|
||||||
pub fn to_forced_state(&self) -> RelayForcedState {
|
|
||||||
match &self {
|
|
||||||
SetRelayForcedStateReq::None => RelayForcedState::None,
|
|
||||||
SetRelayForcedStateReq::Off { for_secs } => RelayForcedState::Off {
|
|
||||||
until: time_secs() + for_secs,
|
|
||||||
},
|
|
||||||
SetRelayForcedStateReq::On { for_secs } => RelayForcedState::On {
|
|
||||||
until: time_secs() + for_secs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum RelayForcedState {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Off {
|
|
||||||
until: u64,
|
|
||||||
},
|
|
||||||
On {
|
|
||||||
until: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct RelayState {
|
pub struct RelayState {
|
||||||
on: bool,
|
on: bool,
|
||||||
since: usize,
|
since: usize,
|
||||||
forced_state: RelayForcedState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayState {
|
impl RelayState {
|
||||||
/// Get actual forced state (returns None if state is expired)
|
|
||||||
pub fn actual_forced_state(&self) -> RelayForcedState {
|
|
||||||
match self.forced_state {
|
|
||||||
RelayForcedState::Off { until } if until > time_secs() => {
|
|
||||||
RelayForcedState::Off { until }
|
|
||||||
}
|
|
||||||
RelayForcedState::On { until } if until > time_secs() => RelayForcedState::On { until },
|
|
||||||
_ => RelayForcedState::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_on(&self) -> bool {
|
pub fn is_on(&self) -> bool {
|
||||||
let forced_state = self.actual_forced_state();
|
self.on
|
||||||
(self.on || matches!(forced_state, RelayForcedState::On { .. }))
|
|
||||||
&& !matches!(forced_state, RelayForcedState::Off { .. })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_off(&self) -> bool {
|
fn is_off(&self) -> bool {
|
||||||
!self.is_on()
|
!self.on
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if relay state is enforced
|
|
||||||
pub fn is_forced(&self) -> bool {
|
|
||||||
self.actual_forced_state() != RelayForcedState::None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_forced(&mut self, s: RelayForcedState) {
|
|
||||||
self.since = time_secs() as usize;
|
|
||||||
self.forced_state = s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state_for(&self) -> usize {
|
pub fn state_for(&self) -> usize {
|
||||||
@@ -210,11 +146,7 @@ impl EnergyEngine {
|
|||||||
r.name,
|
r.name,
|
||||||
r.consumption,
|
r.consumption,
|
||||||
format!("{} / {}", r.minimal_downtime, r.minimal_uptime),
|
format!("{} / {}", r.minimal_downtime, r.minimal_uptime),
|
||||||
status.is_on().to_string()
|
status.is_on().to_string(),
|
||||||
+ match status.is_forced() {
|
|
||||||
true => " (Forced)",
|
|
||||||
false => "",
|
|
||||||
},
|
|
||||||
status.since,
|
status.since,
|
||||||
match dev_online {
|
match dev_online {
|
||||||
true => "Online",
|
true => "Online",
|
||||||
@@ -260,28 +192,19 @@ impl EnergyEngine {
|
|||||||
|
|
||||||
let mut new_relays_state = self.relays_state.clone();
|
let mut new_relays_state = self.relays_state.clone();
|
||||||
|
|
||||||
// Forcefully turn off disabled relays
|
// Forcefully turn off relays that belongs to offline devices
|
||||||
for d in devices {
|
for d in devices {
|
||||||
for r in &d.relays {
|
if !self.device_state(&d.id).is_online() {
|
||||||
if !r.enabled || !d.enabled {
|
for r in &d.relays {
|
||||||
new_relays_state.get_mut(&r.id).unwrap().on = false;
|
new_relays_state.get_mut(&r.id).unwrap().on = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply forced relays state
|
// Forcefully turn off disabled relays
|
||||||
for d in devices {
|
for d in devices {
|
||||||
for r in &d.relays {
|
for r in &d.relays {
|
||||||
if self.relay_state(r.id).is_forced() {
|
if !r.enabled || !d.enabled {
|
||||||
new_relays_state.get_mut(&r.id).unwrap().on = self.relay_state(r.id).is_on();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forcefully turn off relays that belongs to offline devices
|
|
||||||
for d in devices {
|
|
||||||
if !self.device_state(&d.id).is_online() {
|
|
||||||
for r in &d.relays {
|
|
||||||
new_relays_state.get_mut(&r.id).unwrap().on = false;
|
new_relays_state.get_mut(&r.id).unwrap().on = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,9 +216,7 @@ impl EnergyEngine {
|
|||||||
|
|
||||||
for d in devices {
|
for d in devices {
|
||||||
for r in &d.relays {
|
for r in &d.relays {
|
||||||
if new_relays_state.get(&r.id).unwrap().is_off()
|
if new_relays_state.get(&r.id).unwrap().is_off() {
|
||||||
|| new_relays_state.get(&r.id).unwrap().is_forced()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +240,7 @@ impl EnergyEngine {
|
|||||||
for d in devices {
|
for d in devices {
|
||||||
for r in &d.relays {
|
for r in &d.relays {
|
||||||
let state = new_relays_state.get(&r.id).unwrap();
|
let state = new_relays_state.get(&r.id).unwrap();
|
||||||
if state.is_off() || state.is_forced() {
|
if state.is_off() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,9 +271,7 @@ impl EnergyEngine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_relays_state.get(&r.id).unwrap().is_on()
|
if new_relays_state.get(&r.id).unwrap().is_on() {
|
||||||
|| new_relays_state.get(&r.id).unwrap().is_forced()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +298,7 @@ impl EnergyEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order relays to select the ones with the most elevated priorities
|
// Order relays
|
||||||
let mut ordered_relays = devices
|
let mut ordered_relays = devices
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| self.device_state(&d.id).is_online() && d.enabled)
|
.filter(|d| self.device_state(&d.id).is_online() && d.enabled)
|
||||||
@@ -389,13 +308,10 @@ impl EnergyEngine {
|
|||||||
ordered_relays.sort_by_key(|r| r.priority);
|
ordered_relays.sort_by_key(|r| r.priority);
|
||||||
ordered_relays.reverse();
|
ordered_relays.reverse();
|
||||||
|
|
||||||
// Select relays to start, starting with those with highest priorities
|
|
||||||
loop {
|
loop {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for relay in &ordered_relays {
|
for relay in &ordered_relays {
|
||||||
if new_relays_state.get(&relay.id).unwrap().is_on()
|
if new_relays_state.get(&relay.id).unwrap().is_on() {
|
||||||
|| new_relays_state.get(&relay.id).unwrap().is_forced()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +383,7 @@ impl EnergyEngine {
|
|||||||
mod test {
|
mod test {
|
||||||
use crate::devices::device::{Device, DeviceId, DeviceRelayID};
|
use crate::devices::device::{Device, DeviceId, DeviceRelayID};
|
||||||
use crate::energy::consumption::EnergyConsumption;
|
use crate::energy::consumption::EnergyConsumption;
|
||||||
use crate::energy::engine::{EnergyEngine, SetRelayForcedStateReq};
|
use crate::energy::engine::EnergyEngine;
|
||||||
use crate::utils::time_utils::time_secs;
|
use crate::utils::time_utils::time_secs;
|
||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
|
|
||||||
@@ -476,8 +392,6 @@ mod test {
|
|||||||
id: DeviceRelayID,
|
id: DeviceRelayID,
|
||||||
on: bool,
|
on: bool,
|
||||||
r#for: usize,
|
r#for: usize,
|
||||||
#[serde(default)]
|
|
||||||
forced_state: SetRelayForcedStateReq,
|
|
||||||
should_be_on: bool,
|
should_be_on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +439,6 @@ mod test {
|
|||||||
let s = engine.relay_state(r.id);
|
let s = engine.relay_state(r.id);
|
||||||
s.on = r.on;
|
s.on = r.on;
|
||||||
s.since = time_secs() as usize - r.r#for;
|
s.since = time_secs() as usize - r.r#for;
|
||||||
s.forced_state = r.forced_state.to_forced_state()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::server::custom_error::HttpResult;
|
|||||||
use crate::server::devices_api::jwt_parser::JWTRequest;
|
use crate::server::devices_api::jwt_parser::JWTRequest;
|
||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{HttpResponse, web};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, Clone)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct LogRequest {
|
pub struct LogRequest {
|
||||||
severity: LogSeverity,
|
severity: LogSeverity,
|
||||||
message: String,
|
message: String,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub struct JWTRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl JWTRequest {
|
impl JWTRequest {
|
||||||
pub async fn parse_jwt<E: DeserializeOwned + std::clone::Clone>(
|
pub async fn parse_jwt<E: DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
actor: WebEnergyActor,
|
actor: WebEnergyActor,
|
||||||
) -> anyhow::Result<(Device, E)> {
|
) -> anyhow::Result<(Device, E)> {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ pub async fn get_certificate(query: web::Query<ReqWithDevID>, actor: WebEnergyAc
|
|||||||
.body(cert))
|
.body(cert))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
struct Claims {
|
struct Claims {
|
||||||
info: DeviceInfo,
|
info: DeviceInfo,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::server::web_api::*;
|
|||||||
use crate::server::web_app_controller;
|
use crate::server::web_app_controller;
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_identity::config::LogoutBehavior;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_remote_ip::RemoteIPConfig;
|
use actix_remote_ip::RemoteIPConfig;
|
||||||
use actix_session::SessionMiddleware;
|
use actix_session::SessionMiddleware;
|
||||||
use actix_session::storage::CookieSessionStore;
|
use actix_session::storage::CookieSessionStore;
|
||||||
@@ -84,7 +84,7 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
let identity_middleware = IdentityMiddleware::builder()
|
let identity_middleware = IdentityMiddleware::builder()
|
||||||
.logout_behavior(LogoutBehavior::PurgeSession)
|
.logout_behaviour(LogoutBehaviour::PurgeSession)
|
||||||
.visit_deadline(Some(Duration::from_secs(
|
.visit_deadline(Some(Duration::from_secs(
|
||||||
constants::MAX_INACTIVITY_DURATION,
|
constants::MAX_INACTIVITY_DURATION,
|
||||||
)))
|
)))
|
||||||
@@ -231,10 +231,6 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
|
|||||||
"/web_api/relay/{id}",
|
"/web_api/relay/{id}",
|
||||||
web::put().to(relays_controller::update),
|
web::put().to(relays_controller::update),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/web_api/relay/{id}/forced_state",
|
|
||||||
web::put().to(relays_controller::set_forced_state),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/web_api/relay/{id}",
|
"/web_api/relay/{id}",
|
||||||
web::delete().to(relays_controller::delete),
|
web::delete().to(relays_controller::delete),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::devices::device::{DeviceId, DeviceRelay, DeviceRelayID};
|
use crate::devices::device::{DeviceId, DeviceRelay, DeviceRelayID};
|
||||||
use crate::energy::energy_actor;
|
use crate::energy::energy_actor;
|
||||||
use crate::energy::engine::SetRelayForcedStateReq;
|
|
||||||
use crate::server::WebEnergyActor;
|
use crate::server::WebEnergyActor;
|
||||||
use crate::server::custom_error::HttpResult;
|
use crate::server::custom_error::HttpResult;
|
||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{HttpResponse, web};
|
||||||
@@ -86,29 +85,6 @@ pub async fn update(
|
|||||||
Ok(HttpResponse::Accepted().finish())
|
Ok(HttpResponse::Accepted().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set relay forced status
|
|
||||||
pub async fn set_forced_state(
|
|
||||||
actor: WebEnergyActor,
|
|
||||||
req: web::Json<SetRelayForcedStateReq>,
|
|
||||||
path: web::Path<RelayIDInPath>,
|
|
||||||
) -> HttpResult {
|
|
||||||
// Check if relay exists first
|
|
||||||
let list = actor.send(energy_actor::GetAllRelaysState).await?;
|
|
||||||
if !list.into_iter().any(|r| r.id == path.id) {
|
|
||||||
return Ok(HttpResponse::NotFound().json("Relay not found!"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update relay forced state
|
|
||||||
actor
|
|
||||||
.send(energy_actor::SetRelayForcedState(
|
|
||||||
path.id,
|
|
||||||
req.to_forced_state(),
|
|
||||||
))
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Accepted().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an existing relay
|
/// Delete an existing relay
|
||||||
pub async fn delete(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> HttpResult {
|
pub async fn delete(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> HttpResult {
|
||||||
actor
|
actor
|
||||||
|
|||||||
1883
central_frontend/package-lock.json
generated
1883
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.6",
|
||||||
"@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.2",
|
||||||
"@mui/material": "^7.3.4",
|
"@mui/material": "^7.3.2",
|
||||||
"@mui/x-charts": "^8.15.0",
|
"@mui/x-charts": "^7.29.1",
|
||||||
"@mui/x-date-pickers": "^8.15.0",
|
"@mui/x-date-pickers": "^7.29.4",
|
||||||
"date-and-time": "^4.1.0",
|
"date-and-time": "^3.6.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"filesize": "^11.0.13",
|
"filesize": "^10.1.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.7.1",
|
||||||
"semver": "^7.7.3"
|
"semver": "^7.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "^8.24.1",
|
||||||
"vite": "^7.1.12"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
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>;
|
||||||
@@ -57,20 +48,6 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
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 { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
import { DeviceRouteCard } from "./DeviceRouteCard";
|
||||||
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 { BoolText } from "../../widgets/BoolText";
|
|
||||||
import { TimeWidget } from "../../widgets/TimeWidget";
|
import { TimeWidget } from "../../widgets/TimeWidget";
|
||||||
import { DeviceRouteCard } from "./DeviceRouteCard";
|
import { BoolText } from "../../widgets/BoolText";
|
||||||
|
|
||||||
export function DeviceRelays(p: {
|
export function DeviceRelays(p: {
|
||||||
device: Device;
|
device: Device;
|
||||||
@@ -145,8 +145,7 @@ function RelayEntryStatus(
|
|||||||
errMsg="Failed to load relay status!"
|
errMsg="Failed to load relay status!"
|
||||||
build={() => (
|
build={() => (
|
||||||
<>
|
<>
|
||||||
<BoolText val={state!.on} positive="ON" negative="OFF" />{" "}
|
<BoolText val={state!.on} positive="ON" negative="OFF" /> for{" "}
|
||||||
{state?.forced_state.type !== "None" && <b>Forced</b>} for{" "}
|
|
||||||
<TimeWidget diff time={state!.for} />
|
<TimeWidget diff time={state!.for} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ 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;
|
||||||
@@ -105,7 +104,6 @@ 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>
|
||||||
@@ -131,13 +129,6 @@ 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
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -106,7 +106,7 @@ export default function StatCard({
|
|||||||
<Box sx={{ width: "100%", height: 100 }}>
|
<Box sx={{ width: "100%", height: 100 }}>
|
||||||
{data && interval && (
|
{data && interval && (
|
||||||
<SparkLineChart
|
<SparkLineChart
|
||||||
color={chartColor}
|
colors={[chartColor]}
|
||||||
data={data}
|
data={data}
|
||||||
area
|
area
|
||||||
showHighlight
|
showHighlight
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
import { format } from "date-and-time";
|
import date from "date-and-time";
|
||||||
import { time } from "../utils/DateUtils";
|
import { time } from "../utils/DateUtils";
|
||||||
|
|
||||||
export function formatDate(time: number): string {
|
export function formatDate(time: number): string {
|
||||||
const t = new Date();
|
const t = new Date();
|
||||||
t.setTime(1000 * time);
|
t.setTime(1000 * time);
|
||||||
return format(t, "DD/MM/YYYY HH:mm:ss");
|
return date.format(t, "DD/MM/YYYY HH:mm:ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeDiff(a: number, b: number): string {
|
export function timeDiff(a: number, b: number): string {
|
||||||
@@ -51,14 +51,13 @@ export function timeDiff(a: number, b: number): string {
|
|||||||
return `${diffYears} years`;
|
return `${diffYears} years`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeDiffFromNow(t: number, future?: boolean): string {
|
export function timeDiffFromNow(t: number): string {
|
||||||
return future ? timeDiff(time(), t) : timeDiff(t, time());
|
return 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 (
|
||||||
@@ -66,9 +65,7 @@ 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>
|
<span>{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time)}</span>
|
||||||
{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time, p.future)}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
655
custom_consumption/Cargo.lock
generated
655
custom_consumption/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
log = "0.4.28"
|
log = "0.4.27"
|
||||||
clap = { version = "4.5.50", features = ["derive", "env"] }
|
clap = { version = "4.5.46", features = ["derive", "env"] }
|
||||||
egui = "0.32.3"
|
egui = "0.32.1"
|
||||||
eframe = "0.33.0"
|
eframe = "0.32.1"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#define CONFIG_ETH_USE_ESP32_EMAC
|
||||||
|
|
||||||
#include "esp_eth.h"
|
#include "esp_eth.h"
|
||||||
#include "esp_eth_mac.h"
|
#include "esp_eth_mac.h"
|
||||||
#include "esp_eth_com.h"
|
#include "esp_eth_com.h"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.3
|
1.0.2
|
||||||
Reference in New Issue
Block a user