Update interface with last update time

This commit is contained in:
2024-04-20 17:09:15 +02:00
parent 2038f9c88c
commit bd4d8e5ba9
4 changed files with 667 additions and 271 deletions

620
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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>

View File

@@ -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};