feat(P4): Letzte Bezüge als Tabelle statt Textblock (1.8.2)

Die Liste war ein einziges mehrzeiliges Label: Zeitpunkt, Dauer und Gewicht
durch Leerzeichen getrennt aneinandergereiht. Weil Zahlen unterschiedlich
breit sind, stand nichts untereinander - zwei Bezüge ließen sich nicht
vergleichen, ohne jede Zeile einzeln zu lesen.

Jetzt eine Tabelle mit festen Spalten (Zeitpunkt / Dauer / Gewicht / kalt),
Ziffern gleicher Breite, Kopfzeile und leicht abgesetzten wechselnden
Zeilen statt Trennlinien. Ohne Waage bezogen bleibt die Gewichtsspalte leer
statt eine Null vorzutäuschen.

Die Zeilen werden einmal angelegt und danach nur gefüllt oder ausgeblendet -
kein Neuanlegen bei jeder Aktualisierung.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
raw-designs
2026-09-02 19:51:57 +02:00
co-authored by Claude Opus 5
parent e42d2b3de2
commit 21f273aae4
2 changed files with 127 additions and 23 deletions
+13
View File
@@ -1,3 +1,16 @@
Version 1.8.2:
- Die Liste der letzten Bezüge auf der Statistikseite ist jetzt eine Tabelle mit festen
Spalten: Zeitpunkt, Dauer, Gewicht und gegebenenfalls die Kennzeichnung „kalt".
- Vorher war es ein einziges mehrzeiliges Textfeld, in dem die Werte durch Leerzeichen
getrennt aneinandergereiht standen. Weil Zahlen unterschiedlich breit sind, stand nichts
untereinander - zwei Bezüge ließen sich nicht vergleichen, ohne jede Zeile einzeln zu
lesen. Genau das sah unruhig aus.
- Ziffern haben jetzt gleiche Breite, jede zweite Zeile ist leicht abgesetzt, und eine
Kopfzeile benennt die Spalten. Wurde ohne Waage bezogen, bleibt die Gewichtsspalte leer,
statt eine Null vorzutäuschen.
- Die Zeilen werden einmal angelegt und danach nur gefüllt oder ausgeblendet, statt bei
jeder Aktualisierung neu zu entstehen.
Version 1.8.1: Version 1.8.1:
- Die Bezugsanzeige ist da, und zwar für alle vier Darstellungen gemeinsam: Sobald ein - Die Bezugsanzeige ist da, und zwar für alle vier Darstellungen gemeinsam: Sobald ein
Bezug oder Dampfbezug läuft, schrumpft der Temperaturbereich und die untere Zeile wächst. Bezug oder Dampfbezug läuft, schrumpft der Temperaturbereich und die untere Zeile wächst.
+114 -23
View File
@@ -359,7 +359,6 @@ static String g_selProfile;
// Statistik (Nutzungsverlauf) // Statistik (Nutzungsverlauf)
static lv_obj_t* g_statSummary = nullptr; // Aggregate (Heute/Woche/Gesamt) static lv_obj_t* g_statSummary = nullptr; // Aggregate (Heute/Woche/Gesamt)
static lv_obj_t* g_statList = nullptr; // Liste der letzten Einzelbezuege
// ===================================================================================== // =====================================================================================
// Hilfen // Hilfen
@@ -578,6 +577,112 @@ static void grid_at(lv_obj_t* c, uint8_t col, uint8_t row, uint8_t col_span = 1)
} }
#endif #endif
// ------------------------------------------------------------------------------------
// Liste der letzten Bezuege
//
// Frueher war das EIN mehrzeiliges Textfeld: Zeitpunkt, Dauer und Gewicht standen in einer
// Zeile aneinandergereiht, durch Leerzeichen getrennt. Weil die Zahlen unterschiedlich
// breit sind, stand nichts untereinander - man konnte zwei Bezuege nicht vergleichen, ohne
// jede Zeile einzeln zu lesen.
//
// Jetzt eine Tabelle mit festen Spalten und Ziffern gleicher Breite. Die Zeilen werden
// einmal angelegt und danach nur noch gefuellt oder ausgeblendet - eine Statistikseite,
// die bei jeder Aktualisierung Dutzende Objekte neu anlegt, waere unnoetig teuer.
// ------------------------------------------------------------------------------------
#define STAT_ROW_COUNT 12
static lv_obj_t* g_statRow[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statTime[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statDur[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statWgt[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statCold[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statEmpty = nullptr;
static lv_obj_t* stat_cell(lv_obj_t* row, int32_t width, bool rechts, lv_color_t col) {
lv_obj_t* l = lv_label_create(row);
if (width > 0) lv_obj_set_width(l, width);
else lv_obj_set_flex_grow(l, 1);
lv_obj_set_style_text_color(l, col, 0);
lv_obj_set_style_text_align(l, rechts ? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_LEFT, 0);
lv_label_set_text(l, "");
return l;
}
static void stat_list_build(lv_obj_t* parent) {
lv_obj_t* card = th_card(parent);
lv_obj_set_width(card, LV_PCT(100));
lv_obj_set_height(card, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(card, 0, 0);
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
// Kopfzeile - benennt die Spalten, damit die Zahlen ohne Einheit auskommen
lv_obj_t* head = lv_obj_create(card);
lv_obj_set_width(head, LV_PCT(100));
lv_obj_set_height(head, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(head, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(head, 0, 0);
lv_obj_set_style_pad_all(head, 4, 0);
lv_obj_set_style_pad_column(head, 10, 0);
lv_obj_set_flex_flow(head, LV_FLEX_FLOW_ROW);
lv_obj_clear_flag(head, LV_OBJ_FLAG_SCROLLABLE);
stat_cell(head, 0, false, COL_TEXT_MUT);
lv_label_set_text(lv_obj_get_child(head, 0), "Zeitpunkt");
lv_label_set_text(stat_cell(head, 96, true, COL_TEXT_MUT), "Dauer");
lv_label_set_text(stat_cell(head, 96, true, COL_TEXT_MUT), "Gewicht");
lv_label_set_text(stat_cell(head, 58, true, COL_TEXT_MUT), "");
for (int i = 0; i < STAT_ROW_COUNT; i++) {
lv_obj_t* r = lv_obj_create(card);
lv_obj_set_width(r, LV_PCT(100));
lv_obj_set_height(r, LV_SIZE_CONTENT);
// Jede zweite Zeile leicht abgesetzt - fuehrt das Auge ueber die Zeile, ohne dass
// dafuer Trennlinien noetig waeren.
lv_obj_set_style_bg_color(r, lv_color_hex(0x242424), 0);
lv_obj_set_style_bg_opa(r, (i % 2) ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(r, 0, 0);
lv_obj_set_style_radius(r, 5, 0);
lv_obj_set_style_pad_all(r, 5, 0);
lv_obj_set_style_pad_column(r, 10, 0);
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(r, LV_OBJ_FLAG_SCROLLABLE);
g_statTime[i] = stat_cell(r, 0, false, COL_TEXT_DIM);
g_statDur[i] = stat_cell(r, 96, true, COL_TEXT);
g_statWgt[i] = stat_cell(r, 96, true, COL_TEXT);
g_statCold[i] = stat_cell(r, 58, true, lv_color_hex(0x80cbc4));
g_statRow[i] = r;
lv_obj_add_flag(r, LV_OBJ_FLAG_HIDDEN);
}
g_statEmpty = lv_label_create(card);
lv_obj_set_width(g_statEmpty, LV_PCT(100));
lv_label_set_text(g_statEmpty, "Noch keine Bezüge aufgezeichnet.");
lv_obj_set_style_text_color(g_statEmpty, COL_TEXT_MUT, 0);
lv_obj_set_style_pad_all(g_statEmpty, 5, 0);
}
// Eine Zeile fuellen. 'w' unter 0 bedeutet: ohne Waage bezogen, dann bleibt die Spalte leer
// statt eine Null vorzutaeuschen.
static void stat_list_set(int i, const char* dt, float d, float w, bool kalt) {
if (i < 0 || i >= STAT_ROW_COUNT || !g_statRow[i]) return;
lv_obj_remove_flag(g_statRow[i], LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(g_statTime[i], dt);
lv_label_set_text_fmt(g_statDur[i], "%.1f s", d);
if (w >= 0.0f) lv_label_set_text_fmt(g_statWgt[i], "%.1f g", w);
else lv_label_set_text(g_statWgt[i], "");
lv_label_set_text(g_statCold[i], kalt ? "kalt" : "");
}
static void stat_list_finish(int used) {
for (int i = used; i < STAT_ROW_COUNT; i++)
if (g_statRow[i]) lv_obj_add_flag(g_statRow[i], LV_OBJ_FLAG_HIDDEN);
if (g_statEmpty) {
if (used > 0) lv_obj_add_flag(g_statEmpty, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_flag(g_statEmpty, LV_OBJ_FLAG_HIDDEN);
}
}
static void scroll_ohne_nachlauf(lv_obj_t* o) { static void scroll_ohne_nachlauf(lv_obj_t* o) {
#if JC_PANEL_TYPE == WS_PANEL_7H #if JC_PANEL_TYPE == WS_PANEL_7H
lv_obj_clear_flag(o, LV_OBJ_FLAG_SCROLL_MOMENTUM); lv_obj_clear_flag(o, LV_OBJ_FLAG_SCROLL_MOMENTUM);
@@ -2922,11 +3027,12 @@ static void build_info(lv_obj_t* p) {
static void stats_request() { static void stats_request() {
if (!g_client) return; if (!g_client) return;
if (g_statSummary) label_set_if_changed(g_statSummary, "Lade Statistik..."); if (g_statSummary) label_set_if_changed(g_statSummary, "Lade Statistik...");
if (g_statList) label_set_if_changed(g_statList, ""); stat_list_finish(0);
g_client->sendGetUsageStats(); g_client->sendGetUsageStats();
} }
static void stats_refresh_cb(lv_event_t*) { stats_request(); } static void stats_refresh_cb(lv_event_t*) { stats_request(); }
static void build_stats(lv_obj_t* p) { static void build_stats(lv_obj_t* p) {
lv_obj_set_scroll_dir(p, LV_DIR_VER); lv_obj_set_scroll_dir(p, LV_DIR_VER);
lv_obj_set_scrollbar_mode(p, LV_SCROLLBAR_MODE_AUTO); lv_obj_set_scrollbar_mode(p, LV_SCROLLBAR_MODE_AUTO);
@@ -2951,15 +3057,7 @@ static void build_stats(lv_obj_t* p) {
// Letzte Einzelbezuege // Letzte Einzelbezuege
section_title(p, "Letzte Bezüge"); section_title(p, "Letzte Bezüge");
lv_obj_t* lc = th_card(p); stat_list_build(p);
lv_obj_set_width(lc, LV_PCT(100));
lv_obj_set_height(lc, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(lc, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(lc, LV_OBJ_FLAG_SCROLLABLE);
g_statList = lv_label_create(lc);
lv_obj_set_width(g_statList, LV_PCT(100));
lv_label_set_text(g_statList, "");
lv_obj_set_style_text_color(g_statList, COL_TEXT_DIM, 0);
} }
// ===================================================================================== // =====================================================================================
@@ -4424,13 +4522,13 @@ void ui_set_usage_stats(const String& json) {
JsonDocument doc; JsonDocument doc;
if (deserializeJson(doc, json)) { if (deserializeJson(doc, json)) {
label_set_if_changed(g_statSummary, "Statistik konnte nicht gelesen werden."); label_set_if_changed(g_statSummary, "Statistik konnte nicht gelesen werden.");
if (g_statList) label_set_if_changed(g_statList, ""); stat_list_finish(0);
return; return;
} }
bool avail = doc["available"] | false; bool avail = doc["available"] | false;
if (!avail) { if (!avail) {
label_set_if_changed(g_statSummary, "Noch keine Nutzungsdaten vorhanden."); label_set_if_changed(g_statSummary, "Noch keine Nutzungsdaten vorhanden.");
if (g_statList) label_set_if_changed(g_statList, ""); stat_list_finish(0);
return; return;
} }
@@ -4439,8 +4537,7 @@ void ui_set_usage_stats(const String& json) {
long week = doc["week"] | 0; long week = doc["week"] | 0;
bool partial = doc["partial"] | false; bool partial = doc["partial"] | false;
// Liste + Mittelwert der angezeigten Bezuege aufbauen // Mittelwert der angezeigten Bezuege; die Liste fuellt stat_list_set()
String l;
int n = 0; int n = 0;
float durSum = 0.0f; float durSum = 0.0f;
int nDur = 0; // Anzahl der Bezuege, die in den Mittelwert eingehen (ohne kalte) int nDur = 0; // Anzahl der Bezuege, die in den Mittelwert eingehen (ohne kalte)
@@ -4449,10 +4546,7 @@ void ui_set_usage_stats(const String& json) {
float d = v["d"] | 0.0f; float d = v["d"] | 0.0f;
float w = v["w"] | -1.0f; float w = v["w"] | -1.0f;
bool cold = v["cx"] | false; // kalte Extraktion (fehlt bei S3-FW < 5.3.0) bool cold = v["cx"] | false; // kalte Extraktion (fehlt bei S3-FW < 5.3.0)
if (n > 0) l += "\n"; stat_list_set(n, dt, d, w, cold);
l += String(dt) + " " + String(d, 1) + " s";
if (w >= 0.0f) l += " " + String(w, 1) + " g";
if (cold) l += " (kalt)";
// Minutenlange kalte Bezuege wuerden den Mittelwert unbrauchbar machen -> raus // Minutenlange kalte Bezuege wuerden den Mittelwert unbrauchbar machen -> raus
// (gleiche Regel wie in der S3-Statistik) // (gleiche Regel wie in der S3-Statistik)
if (!cold) { durSum += d; nDur++; } if (!cold) { durSum += d; nDur++; }
@@ -4470,10 +4564,7 @@ void ui_set_usage_stats(const String& json) {
} }
label_set_if_changed(g_statSummary, s.c_str()); label_set_if_changed(g_statSummary, s.c_str());
if (g_statList) { stat_list_finish(n);
if (n == 0) l = "Keine Einzelbezüge vorhanden.";
label_set_if_changed(g_statList, l.c_str());
}
} }
void ui_set_wifi_networks(const String& json) { void ui_set_wifi_networks(const String& json) {