Merge branch 'master' of ssh://gitea.communiquons.org:52001/pierre/SolarEnergy

This commit is contained in:
Pierre HUBERT 2024-09-28 09:22:52 +02:00
commit b800b90337
35 changed files with 893 additions and 234 deletions

View File

@ -523,6 +523,25 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
dependencies = [
"bincode_derive",
"serde",
]
[[package]]
name = "bincode_derive"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "2.6.0"
@ -608,6 +627,7 @@ dependencies = [
"actix-web",
"anyhow",
"asn1",
"bincode",
"chrono",
"clap",
"env_logger",
@ -2697,6 +2717,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
[[package]]
name = "walkdir"
version = "2.5.0"

View File

@ -37,4 +37,5 @@ rust-embed = "8.5.0"
jsonwebtoken = { version = "9.3.0", features = ["use_pem"] }
prettytable-rs = "0.10.0"
chrono = "0.4.38"
serde_yml = "0.0.12"
serde_yml = "0.0.12"
bincode = "=2.0.0-rc.3"

View File

@ -2,6 +2,12 @@ use crate::devices::device::{DeviceId, DeviceRelayID};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
#[derive(Copy, Clone, Debug)]
pub enum ConsumptionHistoryType {
GridConsumption,
RelayConsumption,
}
/// Electrical consumption fetcher backend
#[derive(Subcommand, Debug, Clone)]
pub enum ConsumptionBackend {
@ -81,9 +87,13 @@ pub struct AppConfig {
pub production_margin: i32,
/// Energy refresh operations interval, in seconds
#[arg(short('i'), long, env, default_value_t = 20)]
#[arg(short('i'), long, env, default_value_t = 25)]
pub refresh_interval: u64,
/// Energy refresh operations interval, in seconds
#[arg(short('f'), long, env, default_value_t = 5)]
pub energy_fetch_interval: u64,
/// Consumption backend provider
#[clap(subcommand)]
pub consumption_backend: Option<ConsumptionBackend>,
@ -251,6 +261,28 @@ impl AppConfig {
pub fn relay_runtime_day_file_path(&self, relay_id: DeviceRelayID, day: u64) -> PathBuf {
self.relay_runtime_stats_dir(relay_id).join(day.to_string())
}
/// Get energy consumption history path
pub fn energy_consumption_history(&self) -> PathBuf {
self.storage_path().join("consumption_history")
}
/// Get energy consumption history file path for a given day
pub fn energy_consumption_history_day(
&self,
number: u64,
r#type: ConsumptionHistoryType,
) -> PathBuf {
self.storage_path()
.join("consumption_history")
.join(format!(
"{number}-{}",
match r#type {
ConsumptionHistoryType::GridConsumption => "grid",
ConsumptionHistoryType::RelayConsumption => "relay-consumption",
}
))
}
}
#[cfg(test)]

View File

@ -115,6 +115,11 @@ impl DevicesList {
self.0.clone().into_values().collect()
}
/// Get a reference on the full list of devices
pub fn full_list_ref(&self) -> Vec<&Device> {
self.0.values().collect()
}
/// Get the information about a single device
pub fn get_single(&self, id: &DeviceId) -> Option<Device> {
self.0.get(id).cloned()

View File

@ -0,0 +1,79 @@
use crate::constants;
use crate::energy::consumption::EnergyConsumption;
use crate::utils::math_utils::median;
pub struct ConsumptionCache {
nb_vals: usize,
values: Vec<EnergyConsumption>,
}
impl ConsumptionCache {
pub fn new(nb_vals: usize) -> Self {
Self {
nb_vals,
values: vec![],
}
}
pub fn add_value(&mut self, value: EnergyConsumption) {
if self.values.len() >= self.nb_vals {
self.values.remove(0);
}
self.values.push(value);
}
pub fn median_value(&self) -> EnergyConsumption {
if self.values.is_empty() {
return constants::FALLBACK_PRODUCTION_VALUE;
}
median(&self.values)
}
}
#[cfg(test)]
pub mod test {
use crate::constants;
use crate::energy::consumption_cache::ConsumptionCache;
#[test]
fn empty_vec() {
let cache = ConsumptionCache::new(10);
assert_eq!(cache.median_value(), constants::FALLBACK_PRODUCTION_VALUE);
}
#[test]
fn single_value() {
let mut cache = ConsumptionCache::new(10);
cache.add_value(-10);
assert_eq!(cache.median_value(), -10);
}
#[test]
fn four_values() {
let mut cache = ConsumptionCache::new(10);
cache.add_value(50);
cache.add_value(-10);
cache.add_value(-10);
cache.add_value(-10000);
assert_eq!(cache.median_value(), -10);
}
#[test]
fn many_values() {
let mut cache = ConsumptionCache::new(6);
for i in 0..1000 {
cache.add_value(-i);
}
cache.add_value(10);
cache.add_value(50);
cache.add_value(-10);
cache.add_value(-10);
cache.add_value(-30);
cache.add_value(-10000);
assert_eq!(cache.median_value(), -10);
}
}

View File

@ -0,0 +1,162 @@
use crate::app_config::{AppConfig, ConsumptionHistoryType};
use crate::energy::consumption::EnergyConsumption;
use crate::utils::math_utils::median;
use crate::utils::time_utils::day_number;
const TIME_INTERVAL: usize = 10;
#[derive(thiserror::Error, Debug)]
pub enum ConsumptionHistoryError {
#[error("Given time is out of file bounds!")]
TimeOutOfFileBound,
}
/// # ConsumptionHistoryFile
///
/// Stores the history of house consumption
pub struct ConsumptionHistoryFile {
day: u64,
buff: Vec<EnergyConsumption>,
r#type: ConsumptionHistoryType,
}
impl ConsumptionHistoryFile {
/// Open consumption history file, if it exists, or create an empty one
pub fn open(time: u64, r#type: ConsumptionHistoryType) -> anyhow::Result<Self> {
let day = day_number(time);
let path = AppConfig::get().energy_consumption_history_day(day, r#type);
if path.exists() {
Ok(Self {
day,
buff: bincode::decode_from_slice(
&std::fs::read(path)?,
bincode::config::standard(),
)?
.0,
r#type,
})
} else {
log::debug!(
"Energy consumption stats for day {day} does not exists yet, creating memory buffer"
);
Ok(Self::new_memory(day, r#type))
}
}
/// Create a new in memory consumption history
fn new_memory(day: u64, r#type: ConsumptionHistoryType) -> Self {
Self {
day,
buff: vec![0; (3600 * 24 / TIME_INTERVAL) + 1],
r#type,
}
}
/// Resolve time offset of a given time in buffer
fn resolve_offset(&self, time: u64) -> anyhow::Result<usize> {
let start_of_day = self.day * 3600 * 24;
if time < start_of_day || time >= start_of_day + 3600 * 24 {
return Err(ConsumptionHistoryError::TimeOutOfFileBound.into());
}
let relative_time = (time - start_of_day) / TIME_INTERVAL as u64;
Ok(relative_time as usize)
}
/// Check if a time is contained in this history
pub fn contains_time(&self, time: u64) -> bool {
self.resolve_offset(time).is_ok()
}
/// Set new state of relay
pub fn set_consumption(
&mut self,
time: u64,
consumption: EnergyConsumption,
) -> anyhow::Result<()> {
let idx = self.resolve_offset(time)?;
self.buff[idx] = consumption;
Ok(())
}
/// Get the consumption recorded at a given time
pub fn get_consumption(&self, time: u64) -> anyhow::Result<EnergyConsumption> {
let idx = self.resolve_offset(time)?;
Ok(self.buff[idx])
}
/// Persist device relay state history
pub fn save(&self) -> anyhow::Result<()> {
let path = AppConfig::get().energy_consumption_history_day(self.day, self.r#type);
std::fs::write(
path,
bincode::encode_to_vec(&self.buff, bincode::config::standard())?,
)?;
Ok(())
}
/// Get the total runtime of a relay during a given time window
pub fn get_history(
r#type: ConsumptionHistoryType,
from: u64,
to: u64,
interval: u64,
) -> anyhow::Result<Vec<EnergyConsumption>> {
let mut res = Vec::with_capacity(((to - from) / interval) as usize);
let mut file = Self::open(from, r#type)?;
let mut curr_time = from;
let mut intermediate_values = Vec::new();
while curr_time < to {
if !file.contains_time(curr_time) {
file = Self::open(curr_time, r#type)?;
}
intermediate_values.push(file.get_consumption(curr_time)?);
if curr_time % interval == from % interval {
res.push(median(&intermediate_values));
intermediate_values = Vec::new();
}
curr_time += TIME_INTERVAL as u64;
}
Ok(res)
}
}
#[cfg(test)]
mod tests {
use crate::energy::consumption::EnergyConsumption;
use crate::energy::consumption_history_file::{ConsumptionHistoryFile, TIME_INTERVAL};
#[test]
fn test_consumption_history() {
let mut history = ConsumptionHistoryFile::new_memory(0);
for i in 0..50 {
assert_eq!(
history.get_consumption(i * TIME_INTERVAL as u64).unwrap(),
0
);
}
for i in 0..50 {
history
.set_consumption(i * TIME_INTERVAL as u64, i as EnergyConsumption * 2)
.unwrap();
}
for i in 0..50 {
assert_eq!(
history.get_consumption(i * TIME_INTERVAL as u64).unwrap(),
i as EnergyConsumption * 2
);
}
}
}

View File

@ -1,4 +1,4 @@
use crate::app_config::AppConfig;
use crate::app_config::{AppConfig, ConsumptionHistoryType};
use crate::constants;
use crate::devices::device::{
Device, DeviceGeneralInfo, DeviceId, DeviceInfo, DeviceRelay, DeviceRelayID,
@ -6,6 +6,8 @@ use crate::devices::device::{
use crate::devices::devices_list::DevicesList;
use crate::energy::consumption;
use crate::energy::consumption::EnergyConsumption;
use crate::energy::consumption_cache::ConsumptionCache;
use crate::energy::consumption_history_file::ConsumptionHistoryFile;
use crate::energy::engine::EnergyEngine;
use crate::utils::time_utils::time_secs;
use actix::prelude::*;
@ -13,23 +15,35 @@ use openssl::x509::X509Req;
use std::time::Duration;
pub struct EnergyActor {
curr_consumption: EnergyConsumption,
consumption_cache: ConsumptionCache,
devices: DevicesList,
engine: EnergyEngine,
last_engine_refresh: u64,
}
impl EnergyActor {
pub async fn new() -> anyhow::Result<Self> {
let consumption_cache_size =
AppConfig::get().refresh_interval / AppConfig::get().energy_fetch_interval;
let curr_consumption = consumption::get_curr_consumption().await?;
let mut consumption_cache = ConsumptionCache::new(consumption_cache_size as usize);
consumption_cache.add_value(curr_consumption);
if consumption_cache_size < 1 {
panic!("Energy fetch interval must be equal or smaller than refresh interval!");
}
Ok(Self {
curr_consumption: consumption::get_curr_consumption().await?,
consumption_cache,
devices: DevicesList::load()?,
engine: EnergyEngine::default(),
last_engine_refresh: 0,
})
}
async fn refresh(&mut self) -> anyhow::Result<()> {
// Refresh energy
self.curr_consumption = consumption::get_curr_consumption()
let latest_consumption = consumption::get_curr_consumption()
.await
.unwrap_or_else(|e| {
log::error!(
@ -37,10 +51,30 @@ impl EnergyActor {
);
constants::FALLBACK_PRODUCTION_VALUE
});
self.consumption_cache.add_value(latest_consumption);
let devices_list = self.devices.full_list();
let devices_list = self.devices.full_list_ref();
self.engine.refresh(self.curr_consumption, &devices_list);
let mut history =
ConsumptionHistoryFile::open(time_secs(), ConsumptionHistoryType::GridConsumption)?;
history.set_consumption(time_secs(), latest_consumption)?;
history.save()?;
let mut relays_consumption =
ConsumptionHistoryFile::open(time_secs(), ConsumptionHistoryType::RelayConsumption)?;
relays_consumption.set_consumption(
time_secs(),
self.engine.sum_relays_consumption(&devices_list) as EnergyConsumption,
)?;
relays_consumption.save()?;
if self.last_engine_refresh + AppConfig::get().refresh_interval > time_secs() {
return Ok(());
}
self.last_engine_refresh = time_secs();
self.engine
.refresh(self.consumption_cache.median_value(), &devices_list);
self.engine.persist_relays_state(&devices_list)?;
@ -55,7 +89,7 @@ impl Actor for EnergyActor {
log::info!("Energy actor successfully started!");
ctx.run_interval(
Duration::from_secs(AppConfig::get().refresh_interval),
Duration::from_secs(AppConfig::get().energy_fetch_interval),
|act, _ctx| {
log::info!("Performing energy refresh operation");
if let Err(e) = futures::executor::block_on(act.refresh()) {
@ -81,11 +115,25 @@ impl Handler<GetCurrConsumption> for EnergyActor {
type Result = EnergyConsumption;
fn handle(&mut self, _msg: GetCurrConsumption, _ctx: &mut Context<Self>) -> Self::Result {
self.curr_consumption
self.consumption_cache.median_value()
}
}
/// Get current consumption
/// Get relays consumption
#[derive(Message)]
#[rtype(result = "usize")]
pub struct RelaysConsumption;
impl Handler<RelaysConsumption> for EnergyActor {
type Result = usize;
fn handle(&mut self, _msg: RelaysConsumption, _ctx: &mut Context<Self>) -> Self::Result {
self.engine
.sum_relays_consumption(&self.devices.full_list_ref())
}
}
/// Check if device exists
#[derive(Message)]
#[rtype(result = "bool")]
pub struct CheckDeviceExists(pub DeviceId);
@ -326,3 +374,34 @@ impl Handler<GetDevicesState> for EnergyActor {
.collect()
}
}
#[derive(serde::Serialize)]
pub struct ResRelayState {
pub id: DeviceRelayID,
on: bool,
r#for: usize,
}
/// Get the state of all relays
#[derive(Message)]
#[rtype(result = "Vec<ResRelayState>")]
pub struct GetAllRelaysState;
impl Handler<GetAllRelaysState> for EnergyActor {
type Result = Vec<ResRelayState>;
fn handle(&mut self, _msg: GetAllRelaysState, _ctx: &mut Context<Self>) -> Self::Result {
let mut list = vec![];
for d in &self.devices.relays_list() {
let state = self.engine.relay_state(d.id);
list.push(ResRelayState {
id: d.id,
on: state.is_on(),
r#for: state.state_for(),
})
}
list
}
}

View File

@ -39,6 +39,10 @@ impl RelayState {
fn is_off(&self) -> bool {
!self.on
}
pub fn state_for(&self) -> usize {
(time_secs() - self.since as u64) as usize
}
}
type RelaysState = HashMap<DeviceRelayID, RelayState>;
@ -51,7 +55,7 @@ pub struct EnergyEngine {
impl DeviceRelay {
// Note : this function is not recursive
fn has_running_dependencies(&self, s: &RelaysState, devices: &[Device]) -> bool {
fn has_running_dependencies(&self, s: &RelaysState, devices: &[&Device]) -> bool {
for d in devices {
for r in &d.relays {
if r.depends_on.contains(&self.id) && s.get(&r.id).unwrap().is_on() {
@ -68,7 +72,7 @@ impl DeviceRelay {
self.depends_on.iter().any(|id| s.get(id).unwrap().is_off())
}
fn is_having_conflict(&self, s: &RelaysState, devices: &[Device]) -> bool {
fn is_having_conflict(&self, s: &RelaysState, devices: &[&Device]) -> bool {
if self
.conflicts_with
.iter()
@ -90,7 +94,7 @@ impl DeviceRelay {
}
}
fn sum_relays_consumption(state: &RelaysState, devices: &[Device]) -> usize {
fn sum_relays_consumption(state: &RelaysState, devices: &[&Device]) -> usize {
let mut consumption = 0;
for d in devices {
@ -115,7 +119,11 @@ impl EnergyEngine {
self.relays_state.get_mut(&relay_id).unwrap()
}
fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) {
pub fn sum_relays_consumption(&self, devices: &[&Device]) -> usize {
sum_relays_consumption(&self.relays_state, devices)
}
fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) {
log::info!("Current consumption: {curr_consumption}");
let mut table = Table::new();
@ -164,13 +172,13 @@ impl EnergyEngine {
pub fn estimated_consumption_without_relays(
&self,
curr_consumption: EnergyConsumption,
devices: &[Device],
devices: &[&Device],
) -> EnergyConsumption {
curr_consumption - sum_relays_consumption(&self.relays_state, devices) as i32
curr_consumption - self.sum_relays_consumption(devices) as i32
}
/// Refresh energy engine; this method shall never fail !
pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[Device]) {
pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) {
let base_production = self.estimated_consumption_without_relays(curr_consumption, devices);
log::info!("Estimated base production: {base_production}");
@ -358,7 +366,7 @@ impl EnergyEngine {
}
/// Save relays state to disk
pub fn persist_relays_state(&mut self, devices: &[Device]) -> anyhow::Result<()> {
pub fn persist_relays_state(&mut self, devices: &[&Device]) -> anyhow::Result<()> {
// Save all relays state
for d in devices {
for r in &d.relays {

View File

@ -1,4 +1,6 @@
pub mod consumption;
pub mod consumption_cache;
pub mod consumption_history_file;
pub mod energy_actor;
pub mod engine;
pub mod relay_state_history;

View File

@ -47,7 +47,7 @@ impl RelayStateHistory {
Self {
id,
day,
buff: vec![0; 3600 * 24 / TIME_INTERVAL],
buff: vec![0; (3600 * 24 / (TIME_INTERVAL * 8)) + 1],
}
}

View File

@ -18,6 +18,7 @@ async fn main() -> std::io::Result<()> {
create_directory_if_missing(AppConfig::get().pki_path()).unwrap();
create_directory_if_missing(AppConfig::get().devices_config_path()).unwrap();
create_directory_if_missing(AppConfig::get().relays_runtime_stats_storage_path()).unwrap();
create_directory_if_missing(AppConfig::get().energy_consumption_history()).unwrap();
// Initialize PKI
pki::initialize_root_ca().expect("Failed to initialize Root CA!");

View File

@ -131,10 +131,22 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
"/web_api/energy/curr_consumption",
web::get().to(energy_controller::curr_consumption),
)
.route(
"/web_api/energy/curr_consumption/history",
web::get().to(energy_controller::curr_consumption_history),
)
.route(
"/web_api/energy/cached_consumption",
web::get().to(energy_controller::cached_consumption),
)
.route(
"/web_api/energy/relays_consumption",
web::get().to(energy_controller::relays_consumption),
)
.route(
"/web_api/energy/relays_consumption/history",
web::get().to(energy_controller::relays_consumption_history),
)
// Devices controller
.route(
"/web_api/devices/list_pending",
@ -185,6 +197,14 @@ pub async fn secure_server(energy_actor: EnergyActorAddr) -> anyhow::Result<()>
"/web_api/relay/{id}",
web::delete().to(relays_controller::delete),
)
.route(
"/web_api/relays/status",
web::get().to(relays_controller::status_all),
)
.route(
"/web_api/relay/{id}/status",
web::get().to(relays_controller::status_single),
)
// Devices API
.route(
"/devices_api/utils/time",

View File

@ -1,6 +1,10 @@
use crate::app_config::ConsumptionHistoryType;
use crate::energy::consumption::EnergyConsumption;
use crate::energy::consumption_history_file::ConsumptionHistoryFile;
use crate::energy::{consumption, energy_actor};
use crate::server::custom_error::HttpResult;
use crate::server::WebEnergyActor;
use crate::utils::time_utils::time_secs;
use actix_web::HttpResponse;
#[derive(serde::Serialize)]
@ -15,9 +19,38 @@ pub async fn curr_consumption() -> HttpResult {
Ok(HttpResponse::Ok().json(Consumption { consumption }))
}
/// Get curr consumption history
pub async fn curr_consumption_history() -> HttpResult {
let history = ConsumptionHistoryFile::get_history(
ConsumptionHistoryType::GridConsumption,
time_secs() - 3600 * 24,
time_secs(),
60 * 10,
)?;
Ok(HttpResponse::Ok().json(history))
}
/// Get cached energy consumption
pub async fn cached_consumption(energy_actor: WebEnergyActor) -> HttpResult {
let consumption = energy_actor.send(energy_actor::GetCurrConsumption).await?;
Ok(HttpResponse::Ok().json(Consumption { consumption }))
}
/// Get current relays consumption
pub async fn relays_consumption(energy_actor: WebEnergyActor) -> HttpResult {
let consumption =
energy_actor.send(energy_actor::RelaysConsumption).await? as EnergyConsumption;
Ok(HttpResponse::Ok().json(Consumption { consumption }))
}
pub async fn relays_consumption_history() -> HttpResult {
let history = ConsumptionHistoryFile::get_history(
ConsumptionHistoryType::RelayConsumption,
time_secs() - 3600 * 24,
time_secs(),
60 * 10,
)?;
Ok(HttpResponse::Ok().json(history))
}

View File

@ -93,3 +93,20 @@ pub async fn delete(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> Ht
Ok(HttpResponse::Accepted().finish())
}
/// Get the status of all relays
pub async fn status_all(actor: WebEnergyActor) -> HttpResult {
let list = actor.send(energy_actor::GetAllRelaysState).await?;
Ok(HttpResponse::Ok().json(list))
}
/// Get the state of a single relay
pub async fn status_single(actor: WebEnergyActor, path: web::Path<RelayIDInPath>) -> HttpResult {
let list = actor.send(energy_actor::GetAllRelaysState).await?;
let Some(state) = list.into_iter().find(|r| r.id == path.id) else {
return Ok(HttpResponse::NotFound().json("Relay not found!"));
};
Ok(HttpResponse::Ok().json(state))
}

View File

@ -0,0 +1,8 @@
use std::ops::Div;
pub fn median<E: Div + Copy + Ord>(numbers: &[E]) -> E {
let mut numbers = numbers.to_vec();
numbers.sort();
let mid = numbers.len() / 2;
numbers[mid]
}

View File

@ -1,2 +1,3 @@
pub mod files_utils;
pub mod math_utils;
pub mod time_utils;

View File

@ -2,9 +2,9 @@ import { APIClient } from "./ApiClient";
export class EnergyApi {
/**
* Get current house consumption
* Get current grid consumption
*/
static async CurrConsumption(): Promise<number> {
static async GridConsumption(): Promise<number> {
const data = await APIClient.exec({
method: "GET",
uri: "/energy/curr_consumption",
@ -12,6 +12,18 @@ export class EnergyApi {
return data.data.consumption;
}
/**
* Get grid consumption history
*/
static async GridConsumptionHistory(): Promise<number[]> {
return (
await APIClient.exec({
method: "GET",
uri: "/energy/curr_consumption/history",
})
).data;
}
/**
* Get current cached consumption
*/
@ -22,4 +34,28 @@ export class EnergyApi {
});
return data.data.consumption;
}
/**
* Get relays consumption
*/
static async RelaysConsumption(): Promise<number> {
return (
await APIClient.exec({
method: "GET",
uri: "/energy/relays_consumption",
})
).data.consumption;
}
/**
* Get relays consumption history
*/
static async RelaysConsumptionHistory(): Promise<number[]> {
return (
await APIClient.exec({
method: "GET",
uri: "/energy/relays_consumption/history",
})
).data;
}
}

View File

@ -1,6 +1,14 @@
import { APIClient } from "./ApiClient";
import { Device, DeviceRelay } from "./DeviceApi";
export interface RelayStatus {
id: string;
on: boolean;
for: number;
}
export type RelaysStatus = Map<string, RelayStatus>;
export class RelayApi {
/**
* Get the full list of relays
@ -49,4 +57,34 @@ export class RelayApi {
uri: `/relay/${relay.id}`,
});
}
/**
* Get the status of all relays
*/
static async GetRelaysStatus(): Promise<RelaysStatus> {
const data: any[] = (
await APIClient.exec({
method: "GET",
uri: `/relays/status`,
})
).data;
const map = new Map();
for (let r of data) {
map.set(r.id, r);
}
return map;
}
/**
* Get the status of a single relay
*/
static async SingleStatus(relay: DeviceRelay): Promise<RelayStatus> {
return (
await APIClient.exec({
method: "GET",
uri: `/relay/${relay.id}/status`,
})
).data;
}
}

View File

@ -3,7 +3,7 @@ import { TableCell, TableRow } from "@mui/material";
export function DeviceInfoProperty(p: {
icon?: React.ReactElement;
label: string;
value: string;
value: string | React.ReactElement;
color?: string;
}): React.ReactElement {
return (

View File

@ -14,9 +14,12 @@ import { EditDeviceRelaysDialog } from "../../dialogs/EditDeviceRelaysDialog";
import { DeviceRouteCard } from "./DeviceRouteCard";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
import { RelayApi } from "../../api/RelayApi";
import { RelayApi, RelayStatus } from "../../api/RelayApi";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { AsyncWidget } from "../../widgets/AsyncWidget";
import { TimeWidget } from "../../widgets/TimeWidget";
import { BoolText } from "../../widgets/BoolText";
export function DeviceRelays(p: {
device: Device;
@ -115,10 +118,35 @@ export function DeviceRelays(p: {
</>
}
>
<ListItemText primary={r.name} secondary={"TODO: status"} />
<ListItemText
primary={r.name}
secondary={<RelayEntryStatus relay={r} />}
/>
</ListItem>
))}
</DeviceRouteCard>
</>
);
}
function RelayEntryStatus(p: { relay: DeviceRelay }): React.ReactElement {
const [state, setState] = React.useState<RelayStatus | undefined>();
const load = async () => {
setState(await RelayApi.SingleStatus(p.relay));
};
return (
<AsyncWidget
loadKey={p.relay.id}
load={load}
errMsg="Failed to load relay status!"
build={() => (
<>
<BoolText val={state!.on} positive="ON" negative="OFF" /> for{" "}
<TimeWidget diff time={state!.for} />
</>
)}
/>
);
}

View File

@ -1,10 +1,11 @@
import { Table, TableBody } from "@mui/material";
import React from "react";
import { Device, DeviceApi, DeviceState } from "../../api/DeviceApi";
import { AsyncWidget } from "../../widgets/AsyncWidget";
import { DeviceRouteCard } from "./DeviceRouteCard";
import { Table, TableBody } from "@mui/material";
import { DeviceInfoProperty } from "./DeviceInfoProperty";
import { BoolText } from "../../widgets/BoolText";
import { timeDiff } from "../../widgets/TimeWidget";
import { DeviceInfoProperty } from "./DeviceInfoProperty";
import { DeviceRouteCard } from "./DeviceRouteCard";
export function DeviceStateBlock(p: { device: Device }): React.ReactElement {
const [state, setState] = React.useState<DeviceState>();
@ -32,7 +33,13 @@ function DeviceStateInner(p: { state: DeviceState }): React.ReactElement {
<TableBody>
<DeviceInfoProperty
label="Status"
value={p.state.online ? "Online" : "Offline"}
value={
<BoolText
val={p.state.online}
positive="ONLINE"
negative="Offline"
/>
}
/>
<DeviceInfoProperty
label="Last ping"

View File

@ -3,6 +3,7 @@ import { IconButton, Table, TableBody, Tooltip } from "@mui/material";
import React from "react";
import { Device } from "../../api/DeviceApi";
import { EditDeviceMetadataDialog } from "../../dialogs/EditDeviceMetadataDialog";
import { BoolText } from "../../widgets/BoolText";
import { formatDate } from "../../widgets/TimeWidget";
import { DeviceInfoProperty } from "./DeviceInfoProperty";
import { DeviceRouteCard } from "./DeviceRouteCard";
@ -55,8 +56,9 @@ export function GeneralDeviceInfo(p: {
/>
<DeviceInfoProperty
label="Enabled"
value={p.device.enabled ? "YES" : "NO"}
color={p.device.enabled ? "green" : "red"}
value={
<BoolText val={p.device.enabled} positive="YES" negative="NO" />
}
/>
<DeviceInfoProperty
label="Maximum number of relays"

View File

@ -15,10 +15,11 @@ import React from "react";
import { Link, useNavigate } from "react-router-dom";
import { Device, DeviceApi, DevicesState, DeviceURL } from "../api/DeviceApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { BoolText } from "../widgets/BoolText";
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
import { TimeWidget } from "../widgets/TimeWidget";
export function DevicesRoute(): React.ReactElement {
export function DevicesRoute(p: { homeWidget?: boolean }): React.ReactElement {
const loadKey = React.useRef(1);
const [list, setList] = React.useState<Device[] | undefined>();
@ -37,6 +38,7 @@ export function DevicesRoute(): React.ReactElement {
return (
<SolarEnergyRouteContainer
homeWidget={p.homeWidget}
label="Devices"
actions={
<Tooltip title="Refresh table">
@ -80,12 +82,12 @@ function ValidatedDevicesList(p: {
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>Model</TableCell>
<TableCell>Version</TableCell>
<TableCell>Max number of relays</TableCell>
<TableCell>Created</TableCell>
<TableCell>Updated</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Model</TableCell>
<TableCell align="center">Version</TableCell>
<TableCell align="center">Max relays</TableCell>
<TableCell align="center">Created</TableCell>
<TableCell align="center">Updated</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
@ -99,21 +101,21 @@ function ValidatedDevicesList(p: {
<TableCell component="th" scope="row">
{dev.id}
</TableCell>
<TableCell>{dev.info.reference}</TableCell>
<TableCell>{dev.info.version}</TableCell>
<TableCell>{dev.info.max_relays}</TableCell>
<TableCell>
<TableCell align="center">{dev.info.reference}</TableCell>
<TableCell align="center">{dev.info.version}</TableCell>
<TableCell align="center">{dev.info.max_relays}</TableCell>
<TableCell align="center">
<TimeWidget time={dev.time_create} />
</TableCell>
<TableCell>
<TableCell align="center">
<TimeWidget time={dev.time_update} />
</TableCell>
<TableCell align="center">
{p.states.get(dev.id)!.online ? (
<strong>Online</strong>
) : (
<em>Offline</em>
)}
<BoolText
val={p.states.get(dev.id)!.online}
positive="Online"
negative="Offline"
/>
<br />
<TimeWidget diff time={p.states.get(dev.id)!.last_ping} />
</TableCell>

View File

@ -2,6 +2,9 @@ import { Typography } from "@mui/material";
import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget";
import Grid from "@mui/material/Grid2";
import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget";
import { RelayConsumptionWidget } from "./HomeRoute/RelayConsumptionWidget";
import { RelaysListRoute } from "./RelaysListRoute";
import { DevicesRoute } from "./DevicesRoute";
export function HomeRoute(): React.ReactElement {
return (
@ -18,9 +21,20 @@ export function HomeRoute(): React.ReactElement {
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<CurrConsumptionWidget />
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<RelayConsumptionWidget />
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<CachedConsumptionWidget />
</Grid>
<Grid size={{ xs: 12, sm: 12, lg: 9 }}>
<DevicesRoute homeWidget />
</Grid>
<Grid size={{ xs: 12, sm: 12, lg: 9 }}>
<RelaysListRoute homeWidget />
</Grid>
</Grid>
</div>
);

View File

@ -26,12 +26,6 @@ export function CachedConsumptionWidget(): React.ReactElement {
});
return (
<StatCard
title="Cached consumption"
data={[]}
interval="Current data"
trend="neutral"
value={val?.toString() ?? "Loading"}
/>
<StatCard title="Cached consumption" value={val?.toString() ?? "Loading"} />
);
}

View File

@ -7,11 +7,14 @@ export function CurrConsumptionWidget(): React.ReactElement {
const snackbar = useSnackbar();
const [val, setVal] = React.useState<undefined | number>();
const [history, setHistory] = React.useState<number[] | undefined>();
const refresh = async () => {
try {
const s = await EnergyApi.CurrConsumption();
const s = await EnergyApi.GridConsumption();
const history = await EnergyApi.GridConsumptionHistory();
setVal(s);
setHistory(history);
} catch (e) {
console.error(e);
snackbar("Failed to refresh current consumption!");
@ -19,7 +22,6 @@ export function CurrConsumptionWidget(): React.ReactElement {
};
React.useEffect(() => {
refresh();
const i = setInterval(() => refresh(), 3000);
return () => clearInterval(i);
@ -28,9 +30,8 @@ export function CurrConsumptionWidget(): React.ReactElement {
return (
<StatCard
title="Current consumption"
data={[]}
interval="Current data"
trend="neutral"
data={history ?? []}
interval="Last day"
value={val?.toString() ?? "Loading"}
/>
);

View File

@ -0,0 +1,38 @@
import React from "react";
import { EnergyApi } from "../../api/EnergyApi";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
import StatCard from "../../widgets/StatCard";
export function RelayConsumptionWidget(): React.ReactElement {
const snackbar = useSnackbar();
const [val, setVal] = React.useState<undefined | number>();
const [history, setHistory] = React.useState<number[] | undefined>();
const refresh = async () => {
try {
const s = await EnergyApi.RelaysConsumption();
const history = await EnergyApi.RelaysConsumptionHistory();
setVal(s);
setHistory(history);
} catch (e) {
console.error(e);
snackbar("Failed to refresh current relays consumption!");
}
};
React.useEffect(() => {
const i = setInterval(() => refresh(), 3000);
return () => clearInterval(i);
});
return (
<StatCard
title="Relays consumption"
data={history ?? []}
interval="Last day"
value={val?.toString() ?? "Loading"}
/>
);
}

View File

@ -11,18 +11,28 @@ import {
Tooltip,
} from "@mui/material";
import React from "react";
import { DeviceRelay } from "../api/DeviceApi";
import { RelayApi } from "../api/RelayApi";
import { Device, DeviceApi, DeviceRelay, DeviceURL } from "../api/DeviceApi";
import { RelayApi, RelaysStatus } from "../api/RelayApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { BoolText } from "../widgets/BoolText";
import { SolarEnergyRouteContainer } from "../widgets/SolarEnergyRouteContainer";
import { TimeWidget } from "../widgets/TimeWidget";
import { EditDeviceRelaysDialog } from "../dialogs/EditDeviceRelaysDialog";
import { useNavigate } from "react-router-dom";
export function RelaysListRoute(): React.ReactElement {
export function RelaysListRoute(p: {
homeWidget?: boolean;
}): React.ReactElement {
const loadKey = React.useRef(1);
const [list, setList] = React.useState<DeviceRelay[] | undefined>();
const [devices, setDevices] = React.useState<Device[] | undefined>();
const [status, setStatus] = React.useState<RelaysStatus | undefined>();
const load = async () => {
setList(await RelayApi.GetList());
setDevices(await DeviceApi.ValidatedList());
setStatus(await RelayApi.GetRelaysStatus());
list?.sort((a, b) => b.priority - a.priority);
};
@ -33,34 +43,53 @@ export function RelaysListRoute(): React.ReactElement {
};
return (
<SolarEnergyRouteContainer
label="Relays list"
actions={
<Tooltip title="Refresh list">
<IconButton onClick={reload}>
<RefreshIcon />
</IconButton>
</Tooltip>
}
>
<AsyncWidget
loadKey={loadKey.current}
ready={!!list}
errMsg="Failed to load the list of relays!"
load={load}
build={() => <RelaysList onReload={reload} list={list!} />}
/>
</SolarEnergyRouteContainer>
<>
<SolarEnergyRouteContainer
label="Relays list"
homeWidget={p.homeWidget}
actions={
<Tooltip title="Refresh list">
<IconButton onClick={reload}>
<RefreshIcon />
</IconButton>
</Tooltip>
}
>
<AsyncWidget
loadKey={loadKey.current}
ready={!!list}
errMsg="Failed to load the list of relays!"
load={load}
build={() => (
<RelaysList
onReload={reload}
list={list!}
devices={devices!}
status={status!}
/>
)}
/>
</SolarEnergyRouteContainer>
</>
);
}
function RelaysList(p: {
list: DeviceRelay[];
devices: Device[];
status: RelaysStatus;
onReload: () => void;
}): React.ReactElement {
const navigate = useNavigate();
const openDevicePage = (relay: DeviceRelay) => {
const dev = p.devices.find((d) => d.relays.find((r) => r.id === relay.id));
navigate(DeviceURL(dev!));
};
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
@ -75,18 +104,23 @@ function RelaysList(p: {
<TableRow
key={row.name}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
hover
onDoubleClick={() => openDevicePage(row)}
>
<TableCell>{row.name}</TableCell>
<TableCell>
{row.enabled ? (
<span style={{ color: "green" }}>YES</span>
) : (
<span style={{ color: "red" }}>NO</span>
)}
<BoolText val={row.enabled} positive="YES" negative="NO" />
</TableCell>
<TableCell>{row.priority}</TableCell>
<TableCell>{row.consumption}</TableCell>
<TableCell>TODO</TableCell>
<TableCell>
<BoolText
val={p.status.get(row.id)!.on}
positive="ON"
negative="OFF"
/>{" "}
for <TimeWidget diff time={p.status.get(row.id)!.for} />
</TableCell>
</TableRow>
))}
</TableBody>

View File

@ -0,0 +1,11 @@
export function BoolText(p: {
val: boolean;
positive: string;
negative: string;
}): React.ReactElement {
return p.val ? (
<span style={{ color: "green" }}>{p.positive}</span>
) : (
<span style={{ color: "red" }}>{p.negative}</span>
);
}

View File

@ -4,11 +4,12 @@ import React, { PropsWithChildren } from "react";
export function SolarEnergyRouteContainer(
p: {
label: string;
homeWidget?: boolean;
actions?: React.ReactElement;
} & PropsWithChildren
): React.ReactElement {
return (
<div style={{ margin: "50px" }}>
<div style={{ margin: p.homeWidget ? "0px" : "50px" }}>
<div
style={{
display: "flex",
@ -17,7 +18,7 @@ export function SolarEnergyRouteContainer(
marginBottom: "20px",
}}
>
<Typography variant="h4">{p.label}</Typography>
<Typography variant={p.homeWidget ? "h6" : "h4"}>{p.label}</Typography>
{p.actions ?? <></>}
</div>

View File

@ -11,24 +11,25 @@ import { areaElementClasses } from "@mui/x-charts/LineChart";
export type StatCardProps = {
title: string;
value: string;
interval: string;
trend: "up" | "down" | "neutral";
data: number[];
interval?: string;
trend?: "up" | "down" | "neutral";
data?: number[];
};
function getDaysInMonth(month: number, year: number) {
const date = new Date(year, month, 0);
const monthName = date.toLocaleDateString("en-US", {
month: "short",
});
const daysInMonth = date.getDate();
const days = [];
let i = 1;
while (days.length < daysInMonth) {
days.push(`${monthName} ${i}`);
i += 1;
function last24Hours(): string[] {
let res: Array<string> = [];
for (let index = 0; index < 3600 * 24; index += 60 * 10) {
const date = new Date();
date.setTime(date.getTime() - index * 1000);
res.push(date.getHours() + "h" + date.getMinutes());
}
return days;
res.reverse();
console.log(res);
return res;
}
function AreaGradient({ color, id }: { color: string; id: string }) {
@ -50,7 +51,6 @@ export default function StatCard({
data,
}: StatCardProps) {
const theme = useTheme();
const daysInWeek = getDaysInMonth(4, 2024);
const trendColors = {
up:
@ -73,8 +73,8 @@ export default function StatCard({
neutral: "default" as const,
};
const color = labelColors[trend];
const chartColor = trendColors[trend];
const color = labelColors[trend ?? "neutral"];
const chartColor = trendColors[trend ?? "neutral"];
const trendValues = { up: "+25%", down: "-25%", neutral: "+5%" };
return (
@ -95,31 +95,38 @@ export default function StatCard({
<Typography variant="h4" component="p">
{value}
</Typography>
<Chip size="small" color={color} label={trendValues[trend]} />
{trend && (
<Chip size="small" color={color} label={trendValues[trend]} />
)}
</Stack>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{interval}
</Typography>
</Stack>
<Box sx={{ width: "100%", height: 50 }}>
<SparkLineChart
colors={[chartColor]}
data={data}
area
showHighlight
showTooltip
xAxis={{
scaleType: "band",
data: daysInWeek, // Use the correct property 'data' for xAxis
}}
sx={{
[`& .${areaElementClasses.root}`]: {
fill: `url(#area-gradient-${value})`,
},
}}
>
<AreaGradient color={chartColor} id={`area-gradient-${value}`} />
</SparkLineChart>
<Box sx={{ width: "100%", height: 100 }}>
{data && interval && (
<SparkLineChart
colors={[chartColor]}
data={data}
area
showHighlight
showTooltip
xAxis={{
scaleType: "band",
data: last24Hours(),
}}
sx={{
[`& .${areaElementClasses.root}`]: {
fill: `url(#area-gradient-${value})`,
},
}}
>
<AreaGradient
color={chartColor}
id={`area-gradient-${value}`}
/>
</SparkLineChart>
)}
</Box>
</Stack>
</CardContent>

View File

@ -61,7 +61,10 @@ export function TimeWidget(p: {
}): React.ReactElement {
if (!p.time) return <></>;
return (
<Tooltip title={formatDate(p.time)} arrow>
<Tooltip
title={formatDate(p.diff ? new Date().getTime() / 1000 - p.time : p.time)}
arrow
>
<span>{p.diff ? timeDiff(0, p.time) : timeDiffFromNow(p.time)}</span>
</Tooltip>
);

View File

@ -176,9 +176,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
@ -619,6 +619,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.6.0"
@ -691,9 +697,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.8"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [
"clap_builder",
"clap_derive",
@ -701,9 +707,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.8"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [
"anstream",
"anstyle",
@ -713,9 +719,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.8"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
@ -738,36 +744,6 @@ dependencies = [
"error-code",
]
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
"libc",
"objc",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@ -778,12 +754,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.1"
@ -985,21 +955,22 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "ecolor"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10"
checksum = "2e6b451ff1143f6de0f33fc7f1b68fecfd2c7de06e104de96c4514de3f5396f8"
dependencies = [
"bytemuck",
"emath",
]
[[package]]
name = "eframe"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020e2ccef6bbcec71dbc542f7eed64a5846fc3076727f5746da8fd307c91bab2"
checksum = "6490ef800b2e41ee129b1f32f9ac15f713233fe3bc18e241a1afe1e4fb6811e0"
dependencies = [
"ahash",
"bytemuck",
"cocoa",
"document-features",
"egui",
"egui-wgpu",
@ -1011,13 +982,14 @@ dependencies = [
"image",
"js-sys",
"log",
"objc",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.2",
"static_assertions",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -1028,12 +1000,13 @@ dependencies = [
[[package]]
name = "egui"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a"
checksum = "20c97e70a2768de630f161bb5392cbd3874fcf72868f14df0e002e82e06cb798"
dependencies = [
"accesskit",
"ahash",
"emath",
"epaint",
"log",
"nohash-hasher",
@ -1041,10 +1014,11 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469ff65843f88a702b731a1532b7d03b0e8e96d283e70f3a22b0e06c46cb9b37"
checksum = "47c7a7c707877c3362a321ebb4f32be811c0b91f7aebf345fb162405c0218b4c"
dependencies = [
"ahash",
"bytemuck",
"document-features",
"egui",
@ -1059,11 +1033,12 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609"
checksum = "fac4e066af341bf92559f60dbdf2020b2a03c963415349af5f3f8d79ff7a4926"
dependencies = [
"accesskit_winit",
"ahash",
"arboard",
"egui",
"log",
@ -1076,10 +1051,11 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e5d975f3c86edc3d35b1db88bb27c15dde7c55d3b5af164968ab5ede3f44ca"
checksum = "4e2bdc8b38cfa17cc712c4ae079e30c71c00cd4c2763c9e16dc7860a02769103"
dependencies = [
"ahash",
"bytemuck",
"egui",
"glow",
@ -1092,9 +1068,9 @@ dependencies = [
[[package]]
name = "emath"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f"
checksum = "0a6a21708405ea88f63d8309650b4d77431f4bc28fb9d8e6f77d3963b51249e6"
dependencies = [
"bytemuck",
]
@ -1132,9 +1108,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.11.3"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
@ -1145,9 +1121,9 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.27.2"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176"
checksum = "3f0dcc0a0771e7500e94cd1cb797bd13c9f23b9409bdc3c824e2cbc562b7fa01"
dependencies = [
"ab_glyph",
"ahash",
@ -1510,9 +1486,9 @@ dependencies = [
[[package]]
name = "gpu-descriptor"
version = "0.2.4"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557"
dependencies = [
"bitflags 2.6.0",
"gpu-descriptor-types",
@ -1521,9 +1497,9 @@ dependencies = [
[[package]]
name = "gpu-descriptor-types"
version = "0.1.2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c"
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
"bitflags 2.6.0",
]
@ -1621,13 +1597,12 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.9"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"byteorder-lite",
"num-traits",
"png",
]
@ -1846,9 +1821,9 @@ dependencies = [
[[package]]
name = "metal"
version = "0.27.0"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25"
checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb"
dependencies = [
"bitflags 2.6.0",
"block",
@ -1871,10 +1846,11 @@ dependencies = [
[[package]]
name = "naga"
version = "0.19.2"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843"
checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.6.0",
"codespan-reporting",
@ -1975,7 +1951,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
"objc_exception",
]
[[package]]
@ -2119,15 +2094,6 @@ dependencies = [
"objc2-metal",
]
[[package]]
name = "objc_exception"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
dependencies = [
"cc",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@ -3149,30 +3115,32 @@ dependencies = [
[[package]]
name = "webbrowser"
version = "0.8.15"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b"
checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e"
dependencies = [
"block2 0.5.1",
"core-foundation",
"home",
"jni",
"log",
"ndk-context",
"objc",
"raw-window-handle 0.5.2",
"objc2 0.5.2",
"objc2-foundation",
"url",
"web-sys",
]
[[package]]
name = "wgpu"
version = "0.19.4"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01"
checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c"
dependencies = [
"arrayvec",
"cfg-if",
"cfg_aliases",
"document-features",
"js-sys",
"log",
"parking_lot",
@ -3190,15 +3158,16 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "0.19.4"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a"
checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39"
dependencies = [
"arrayvec",
"bit-vec",
"bitflags 2.6.0",
"cfg_aliases",
"codespan-reporting",
"document-features",
"indexmap",
"log",
"naga",
@ -3216,9 +3185,9 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "0.19.4"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3"
checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222"
dependencies = [
"android_system_properties",
"arrayvec",
@ -3257,9 +3226,9 @@ dependencies = [
[[package]]
name = "wgpu-types"
version = "0.19.2"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805"
checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef"
dependencies = [
"bitflags 2.6.0",
"js-sys",

View File

@ -4,9 +4,9 @@ version = "0.1.0"
edition = "2021"
[dependencies]
env_logger = "0.11.3"
env_logger = "0.11.5"
log = "0.4.22"
clap = { version = "4.5.8", features = ["derive", "env"] }
egui = "0.27.2"
eframe = "0.27.2"
lazy_static = "1.5.0"
clap = { version = "4.5.18", features = ["derive", "env"] }
egui = "0.28.1"
eframe = "0.28.1"
lazy_static = "1.5.0"

View File

@ -11,7 +11,7 @@ fn main() {
eframe::run_native(
"Custom consumption",
options,
Box::new(|_cc| Box::<MyApp>::default()),
Box::new(|_cc| Ok(Box::<MyApp>::default())),
)
.unwrap()
}