From 1130f3ffa6d9c3bab7a2728f7e9240835e3e713f Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Thu, 5 Nov 2026 15:45:30 +0100 Subject: [PATCH] Add internet usage dashboard page --- esphome/energymonitor.yaml | 184 +++++++++++++++++++++++++++++++++++-- 1 file changed, 177 insertions(+), 7 deletions(-) diff --git a/esphome/energymonitor.yaml b/esphome/energymonitor.yaml index 625ab72..82b7f20 100644 --- a/esphome/energymonitor.yaml +++ b/esphome/energymonitor.yaml @@ -1,9 +1,11 @@ substitutions: - device_description: "M5Stack Core energy dashboard for Home Assistant grid, solar and consumer usage, with analog/digital clock and WireGuard" + device_description: "M5Stack Core dashboard for Home Assistant energy and internet usage, with analog/digital clock and WireGuard" name: "energymonitor" friendly_name: "Energy Monitor" grid_power_gauge_max_w: "5000.0" balcony_solar_gauge_max_w: "800.0" + internet_download_gauge_max_mbps: "1000.0" + internet_upload_gauge_max_mbps: "1000.0" esphome: comment: ${device_description} @@ -120,6 +122,11 @@ font: glyphsets: - GF_Latin_Kernel glyphs: ["äöüÄÖÜß"] + - file: "fonts/Roboto-Bold.ttf" + id: font_internet_value + size: 22 + glyphsets: + - GF_Latin_Kernel - file: "fonts/Roboto-Regular.ttf" id: font_device size: 13 @@ -159,6 +166,27 @@ globals: type: int restore_value: true initial_value: '0' + - id: internet_graph_view + type: bool + restore_value: true + initial_value: 'false' + # 361 ten-second samples span one hour; unfilled slots are never drawn. + - id: internet_download_history + type: std::array + restore_value: false + initial_value: 'std::array{}' + - id: internet_upload_history + type: std::array + restore_value: false + initial_value: 'std::array{}' + - id: internet_history_next + type: int + restore_value: false + initial_value: '0' + - id: internet_history_count + type: int + restore_value: false + initial_value: '0' sensor: # WiFi signal strength @@ -258,14 +286,40 @@ sensor: id: microwave_power entity_id: sensor.waschetrockner_leistung internal: true + # These Home Assistant template sensors already report decimal Mbps. + - platform: homeassistant + id: internet_download + entity_id: sensor.internet_speed_in + internal: true + - platform: homeassistant + id: internet_upload + entity_id: sensor.internet_speed_out + internal: true - platform: wireguard latest_handshake: name: 'WireGuard Latest Handshake' +interval: + - interval: 10s + then: + - lambda: |- + // Ignore log-only API clients when checking Home Assistant connectivity. + const bool connected = id(api_server).is_connected_with_state_subscription(); + const int slot = id(internet_history_next); + const float download = id(internet_download).state; + const float upload = id(internet_upload).state; + // Keep disconnected/invalid samples as gaps rather than false zero traffic. + id(internet_download_history)[slot] = connected && id(internet_download).has_state() + && std::isfinite(download) && download >= 0.0f ? download : NAN; + id(internet_upload_history)[slot] = connected && id(internet_upload).has_state() + && std::isfinite(upload) && upload >= 0.0f ? upload : NAN; + id(internet_history_next) = (slot + 1) % 361; + if (id(internet_history_count) < 361) id(internet_history_count)++; + script: - id: show_previous_page then: - - lambda: id(page_index) = (id(page_index) + 3 - 1) % 3; + - lambda: id(page_index) = (id(page_index) + 4 - 1) % 4; - component.update: tft - id: toggle_page_mode then: @@ -274,13 +328,15 @@ script: id(page_one_gauge_view) = !id(page_one_gauge_view); } else if (id(page_index) == 1) { id(device_usage_view) = (id(device_usage_view) + 1) % 2; - } else { + } else if (id(page_index) == 2) { id(clock_digital_view) = !id(clock_digital_view); + } else if (id(page_index) == 3) { + id(internet_graph_view) = !id(internet_graph_view); } - component.update: tft - id: show_next_page then: - - lambda: id(page_index) = (id(page_index) + 1) % 3; + - lambda: id(page_index) = (id(page_index) + 1) % 4; - component.update: tft button: @@ -633,6 +689,117 @@ display: } } + if (id(page_index) == 3) { + const Color download_color(143, 190, 223); + const Color upload_color(255, 170, 40); + const Color muted(42, 42, 42); + const bool connected = id(api_server).is_connected_with_state_subscription(); + const float download = id(internet_download).state; + const float upload = id(internet_upload).state; + const bool download_valid = connected && id(internet_download).has_state() + && std::isfinite(download) && download >= 0.0f; + const bool upload_valid = connected && id(internet_upload).has_state() + && std::isfinite(upload) && upload >= 0.0f; + auto format_rate = [](float mbps) -> std::string { + char text[32]; + if (mbps < 1.0f) + snprintf(text, sizeof(text), "%.0f kbps", mbps * 1000.0f); + else + snprintf(text, sizeof(text), "%.1f Mbps", mbps); + return std::string(text); + }; + + if (!id(internet_graph_view)) { + it.print(12, 10, id(font_small), Color::WHITE, "Internet"); + const int cx = 160, cy = 188; + const float download_fraction = download_valid + ? fminf(download / ${internet_download_gauge_max_mbps}, 1.0f) : 0.0f; + const float upload_fraction = upload_valid + ? fminf(upload / ${internet_upload_gauge_max_mbps}, 1.0f) : 0.0f; + // Match the energy gauge: download outside, upload inside. + it.filled_gauge(cx, cy, 140, 122, 100, muted); + it.filled_gauge(cx, cy, 140, 122, static_cast(download_fraction * 100), download_color); + it.filled_gauge(cx, cy, 116, 98, 100, muted); + it.filled_gauge(cx, cy, 116, 98, static_cast(upload_fraction * 100), upload_color); + it.print(cx, 102, id(font_small), Color::WHITE, TextAlign::TOP_CENTER, "Download"); + it.printf(cx, 120, id(font_internet_value), download_color, TextAlign::TOP_CENTER, + "%s", download_valid ? format_rate(download).c_str() : "--"); + it.print(cx, 151, id(font_small), Color::WHITE, TextAlign::TOP_CENTER, "Upload"); + it.printf(cx, 169, id(font_internet_value), upload_color, TextAlign::TOP_CENTER, + "%s", upload_valid ? format_rate(upload).c_str() : "--"); + it.printf(cx, 204, id(font_device), Color::WHITE, TextAlign::TOP_CENTER, + "Max: DL %.0f / UL %.0f Mbps", + ${internet_download_gauge_max_mbps}, ${internet_upload_gauge_max_mbps}); + } else { + it.print(12, 10, id(font_device), Color::WHITE, "Internet · 1 h"); + const int left = 28, right = 308, top = 62, zero = 122, bottom = 182; + const int count = id(internet_history_count); + // A shared symmetric scale keeps upload/download magnitudes comparable. + float peak = 0.001f; + for (int sample = 0; sample < count; sample++) { + const int slot = (id(internet_history_next) - count + sample + 361) % 361; + const float down = id(internet_download_history)[slot]; + const float up = id(internet_upload_history)[slot]; + if (std::isfinite(down) && down > peak) peak = down; + if (std::isfinite(up) && up > peak) peak = up; + } + const float magnitude = powf(10.0f, floorf(log10f(peak))); + const float normalized = peak / magnitude; + const float maximum = magnitude * (normalized <= 1 ? 1 : normalized <= 2 ? 2 + : normalized <= 5 ? 5 : 10); + const std::string scale = format_rate(maximum); + it.print(left, 40, id(font_device), upload_color, "Upload"); + it.printf(right, 40, id(font_device), upload_color, TextAlign::TOP_RIGHT, + "+%s", scale.c_str()); + it.print(left, 184, id(font_device), download_color, "Download"); + it.printf(right, 184, id(font_device), download_color, TextAlign::TOP_RIGHT, + "-%s", scale.c_str()); + for (int minute = 0; minute <= 60; minute += 15) { + const int x = left + minute * (right - left) / 60; + it.line(x, top, x, bottom, muted); + } + it.line(left, top, right, top, muted); + it.line(left, bottom, right, bottom, muted); + it.line(left, zero, right, zero, Color(100, 100, 100)); + it.print(12, zero, id(font_device), Color::WHITE, TextAlign::CENTER, "0"); + it.print(left, 202, id(font_device), Color::WHITE, "-60 min"); + it.print((left + right) / 2, 202, id(font_device), Color::WHITE, + TextAlign::TOP_CENTER, "-30 min"); + it.print(right, 202, id(font_device), Color::WHITE, TextAlign::TOP_RIGHT, "Jetzt"); + + int previous_x = 0, previous_down_y = 0, previous_up_y = 0; + bool previous_down_valid = false, previous_up_valid = false; + for (int sample = 0; sample < count; sample++) { + const int slot = (id(internet_history_next) - count + sample + 361) % 361; + // Right-align a partially filled buffer; do not invent pre-boot history. + const int x = right - (count - 1 - sample) * (right - left) / 360; + const float down = id(internet_download_history)[slot]; + const float up = id(internet_upload_history)[slot]; + const bool down_valid = std::isfinite(down); + const bool up_valid = std::isfinite(up); + const int down_y = down_valid ? zero + lroundf(down / maximum * (bottom - zero)) : zero; + const int up_y = up_valid ? zero - lroundf(up / maximum * (zero - top)) : zero; + if (down_valid) { + it.line(previous_down_valid ? previous_x : x, + previous_down_valid ? previous_down_y : down_y, x, down_y, download_color); + } + if (up_valid) { + it.line(previous_up_valid ? previous_x : x, + previous_up_valid ? previous_up_y : up_y, x, up_y, upload_color); + } + previous_x = x; + previous_down_y = down_y; + previous_up_y = up_y; + previous_down_valid = down_valid; + previous_up_valid = up_valid; + } + if (count == 0) { + it.print(160, 90, id(font_small), Color::WHITE, TextAlign::TOP_CENTER, + "Warte auf Messwerte"); + } + } + } + // Labels correspond to the left (A) and right (C) hardware buttons. it.printf(8, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_LEFT, "< Zurück"); @@ -640,11 +807,14 @@ display: "Weiter >"); if (id(page_index) == 0) { it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER, - "Ansicht · 1 / 3"); + "Ansicht · 1 / 4"); } else if (id(page_index) == 1) { it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER, "Geräte %d / 2", id(device_usage_view) + 1); - } else { + } else if (id(page_index) == 2) { it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER, - "%s · 3 / 3", id(clock_digital_view) ? "Analog" : "Digital"); + "%s · 3 / 4", id(clock_digital_view) ? "Analog" : "Digital"); + } else if (id(page_index) == 3) { + it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER, + "%s · 4 / 4", id(internet_graph_view) ? "Live" : "Verlauf"); }