Update interface with last update time
This commit is contained in:
620
Cargo.lock
generated
620
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -7,10 +7,12 @@ edition = "2021"
|
||||
dummy = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
axum = "0.6.20"
|
||||
base64 = "0.21.5"
|
||||
log = "0.4.20"
|
||||
serialport = "4.2.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
tokio = { version = "1.34.0", features = ["full"] }
|
||||
anyhow = "1.0.82"
|
||||
axum = "0.7.5"
|
||||
base64 = "0.22.0"
|
||||
chrono = "0.4.38"
|
||||
log = "0.4.21"
|
||||
serialport = "4.3.0"
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
tokio_schedule = "0.3.1"
|
||||
|
||||
183
src/index.html
183
src/index.html
@@ -1,43 +1,148 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="de">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
|
||||
|
||||
<title>ELWA DC Heizstab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Key Value Display</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}}
|
||||
.section {{
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.section-title {{
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.key-value {{
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.key {{
|
||||
font-weight: bold;
|
||||
}}
|
||||
.value {{
|
||||
margin-left: 10px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Wasser</h2>
|
||||
<p>Temperatur: {} °C</p>
|
||||
<p>Temperatur Min: {} °C</p>
|
||||
<p>Temperatur Max: {} °C</p>
|
||||
<p>Solltemperatur Solar: {} °C</p>
|
||||
<p>Solltemperatur Netz: {} °C</p>
|
||||
|
||||
<h2>Solar aktuell</h2>
|
||||
<p>Spannung: {} V</p>
|
||||
<p>Strom: {} A</p>
|
||||
<p>Leistung: {} kW ({} W)</p>
|
||||
|
||||
<h2>Historie</h2>
|
||||
<p>Solarenergie Heute: {} kWh ({} Wh)</p>
|
||||
<p>Solarenergie Gesamt: {} kWh ({} Wh)</p>
|
||||
<p>Netzenergie Heute: {} kWh ({} Wh)</p>
|
||||
|
||||
<h2>Zustand</h2>
|
||||
<p>Isolationsmessung: {}</p>
|
||||
<p>Gerätetemperatur: {} °C</p>
|
||||
<p>Status: {}</p>
|
||||
<p>DC Trenner: {}</p>
|
||||
<p>DC Relais: {}</p>
|
||||
<p>AC Relais: {}</p>
|
||||
|
||||
<h2>Misc</h2>
|
||||
<p>Betriebstag: {}</p>
|
||||
<p>Firmware: {}</p>
|
||||
<p>Seriennummer: {}</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<div class="key-value">
|
||||
<span class="key">Zuletzt aktualisiert:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Wasser</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Temperatur:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Temperatur Min:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Temperatur Max:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Solltemperatur Solar:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Solltemperatur Netz:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Solar aktuell</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Spannung:</span>
|
||||
<span class="value">{} V</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Strom:</span>
|
||||
<span class="value">{} A</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Leistung:</span>
|
||||
<span class="value">{} kW ({} W)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Historie</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Solarenergie Heute:</span>
|
||||
<span class="value">{} kWh ({} Wh)</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Solarenergie Gesamt:</span>
|
||||
<span class="value">{} kWh ({} Wh)</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Netzenergie Heute:</span>
|
||||
<span class="value">{} kWh ({} Wh)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Zustand</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Isolationsmessung:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Gerätetemperatur:</span>
|
||||
<span class="value">{} °C</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Status:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">DC Trenner:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">DC Relais:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">AC Relais:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Misc</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Betriebstag:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Firmware:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
<div class="key-value">
|
||||
<span class="key">Seriennummer:</span>
|
||||
<span class="value">{}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
119
src/main.rs
119
src/main.rs
@@ -1,12 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use chrono::Local;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -43,8 +47,10 @@ enum StatusTag {
|
||||
Dummy13,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Status<'a> {
|
||||
#[derive(Debug, Clone)]
|
||||
struct Status {
|
||||
zuletzt_aktualisiert: String,
|
||||
|
||||
// Wasser
|
||||
wassertemp: f32,
|
||||
wassertemp_min: f32,
|
||||
@@ -72,8 +78,8 @@ struct Status<'a> {
|
||||
|
||||
// Misc
|
||||
betriebstag: u32,
|
||||
firmware: &'a str,
|
||||
seriennummer: &'a str,
|
||||
firmware: String,
|
||||
seriennummer: String,
|
||||
}
|
||||
|
||||
struct AppError(anyhow::Error);
|
||||
@@ -99,49 +105,49 @@ where
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new().route("/", get(handler));
|
||||
let state = Arc::new(Mutex::new(None::<Status>));
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
// let periodic_state = Arc::clone(&state);
|
||||
// let periodic_refresh = every(5).minutes().perform(|| async {
|
||||
// log::info!("Periodic data fetch");
|
||||
// let mut lock = periodic_state.lock().unwrap();
|
||||
// if let Ok(status) = get_status_from_device() {
|
||||
// *lock = Some(status.clone());
|
||||
// }
|
||||
// });
|
||||
// tokio::spawn(async move {
|
||||
// loop {
|
||||
// log::info!("Periodic data fetch");
|
||||
// let mut lock = periodic_state.lock().unwrap();
|
||||
// if let Ok(status) = get_status_from_device() {
|
||||
// *lock = Some(status.clone());
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
let app = Router::new().route("/", get(handler)).with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn handler() -> Result<Html<String>, AppError> {
|
||||
async fn handler(
|
||||
State(state): State<Arc<Mutex<Option<Status>>>>,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
log::info!("Fetch new data");
|
||||
|
||||
let data = read_device().context("Could not retrieve device data")?;
|
||||
let data_string = std::str::from_utf8(&data).unwrap();
|
||||
let mut lock = state.lock().unwrap();
|
||||
let status = if let Ok(status) = get_status_from_device() {
|
||||
*lock = Some(status.clone());
|
||||
|
||||
let status_map = StatusTag::iter()
|
||||
.zip(data_string.split('\t'))
|
||||
.collect::<HashMap<StatusTag, &str>>();
|
||||
|
||||
let status = Status {
|
||||
wassertemp: status_map[&StatusTag::Wassertemp].parse::<f32>()? / 10.0,
|
||||
wassertemp_min: status_map[&StatusTag::WassertempMin].parse::<f32>()? / 10.0,
|
||||
wassertemp_max: status_map[&StatusTag::WassertempMax].parse::<f32>()? / 10.0,
|
||||
solltemp_solar: status_map[&StatusTag::SolltempSolar].parse::<f32>()? / 10.0,
|
||||
solltemp_netz: status_map[&StatusTag::SolltempNetz].parse::<f32>()? / 10.0,
|
||||
solarspannung: status_map[&StatusTag::Solarspannung].parse()?,
|
||||
solarstrom: status_map[&StatusTag::Solarstrom].parse()?,
|
||||
solarleistung: status_map[&StatusTag::Solarleistung].parse::<f32>()? / 1000.0,
|
||||
solarenergie_heute: status_map[&StatusTag::SolarenergieHeute].parse::<f32>()? / 1000.0,
|
||||
solarenergie_gesamt: status_map[&StatusTag::SolarenergieGesamt].parse::<f32>()? / 1000.0,
|
||||
netzenergie_heute: status_map[&StatusTag::NetzenergieHeute].parse::<f32>()? / 1000.0,
|
||||
iso_messung: status_map[&StatusTag::IsoMessung].parse()?,
|
||||
geraetetemp: status_map[&StatusTag::GeraeteTemp].parse()?,
|
||||
status: status_map[&StatusTag::Status].parse()?,
|
||||
dc_trenner: status_map[&StatusTag::DcTrenner].parse::<u8>()? != 0,
|
||||
dc_relais: status_map[&StatusTag::DcRelais].parse::<u8>()? != 0,
|
||||
ac_relais: status_map[&StatusTag::AcRelais].parse::<u8>()? != 0,
|
||||
betriebstag: status_map[&StatusTag::Betriebstag].parse()?,
|
||||
firmware: status_map[&StatusTag::Firmware],
|
||||
seriennummer: status_map[&StatusTag::Seriennummer],
|
||||
status
|
||||
} else {
|
||||
lock.clone().context("No device state yet fetched")?
|
||||
};
|
||||
|
||||
Ok(Html(format!(
|
||||
include_str!("index.html"),
|
||||
status.zuletzt_aktualisiert,
|
||||
status.wassertemp,
|
||||
status.wassertemp_min,
|
||||
status.wassertemp_max,
|
||||
@@ -169,6 +175,41 @@ async fn handler() -> Result<Html<String>, AppError> {
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_status_from_device() -> anyhow::Result<Status> {
|
||||
let data = read_device().context("Could not retrieve device data")?;
|
||||
let data_string = std::str::from_utf8(&data).unwrap();
|
||||
|
||||
let status_map = StatusTag::iter()
|
||||
.zip(data_string.split('\t'))
|
||||
.collect::<HashMap<StatusTag, &str>>();
|
||||
|
||||
let status = Status {
|
||||
zuletzt_aktualisiert: Local::now().to_string(),
|
||||
wassertemp: status_map[&StatusTag::Wassertemp].parse::<f32>()? / 10.0,
|
||||
wassertemp_min: status_map[&StatusTag::WassertempMin].parse::<f32>()? / 10.0,
|
||||
wassertemp_max: status_map[&StatusTag::WassertempMax].parse::<f32>()? / 10.0,
|
||||
solltemp_solar: status_map[&StatusTag::SolltempSolar].parse::<f32>()? / 10.0,
|
||||
solltemp_netz: status_map[&StatusTag::SolltempNetz].parse::<f32>()? / 10.0,
|
||||
solarspannung: status_map[&StatusTag::Solarspannung].parse()?,
|
||||
solarstrom: status_map[&StatusTag::Solarstrom].parse()?,
|
||||
solarleistung: status_map[&StatusTag::Solarleistung].parse::<f32>()? / 1000.0,
|
||||
solarenergie_heute: status_map[&StatusTag::SolarenergieHeute].parse::<f32>()? / 1000.0,
|
||||
solarenergie_gesamt: status_map[&StatusTag::SolarenergieGesamt].parse::<f32>()? / 1000.0,
|
||||
netzenergie_heute: status_map[&StatusTag::NetzenergieHeute].parse::<f32>()? / 1000.0,
|
||||
iso_messung: status_map[&StatusTag::IsoMessung].parse()?,
|
||||
geraetetemp: status_map[&StatusTag::GeraeteTemp].parse()?,
|
||||
status: status_map[&StatusTag::Status].parse()?,
|
||||
dc_trenner: status_map[&StatusTag::DcTrenner].parse::<u8>()? != 0,
|
||||
dc_relais: status_map[&StatusTag::DcRelais].parse::<u8>()? != 0,
|
||||
ac_relais: status_map[&StatusTag::AcRelais].parse::<u8>()? != 0,
|
||||
betriebstag: status_map[&StatusTag::Betriebstag].parse()?,
|
||||
firmware: status_map[&StatusTag::Firmware].to_string(),
|
||||
seriennummer: status_map[&StatusTag::Seriennummer].to_string(),
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "dummy"))]
|
||||
fn read_device() -> anyhow::Result<Vec<u8>> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
||||
Reference in New Issue
Block a user