Merge pull request 'feat(P4): Letzte Bezüge als Tabelle statt Textblock (1.8.2)' (#62) from feat/statistik-tabelle into main

This commit was merged in pull request #62.
This commit is contained in:
2026-09-02 19:52:14 +02:00
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) {