Add internet usage dashboard page

This commit is contained in:
2026-11-05 15:45:30 +01:00
parent cf804893d5
commit 1130f3ffa6
+177 -7
View File
@@ -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<float, 361>
restore_value: false
initial_value: 'std::array<float, 361>{}'
- id: internet_upload_history
type: std::array<float, 361>
restore_value: false
initial_value: 'std::array<float, 361>{}'
- 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<int>(download_fraction * 100), download_color);
it.filled_gauge(cx, cy, 116, 98, 100, muted);
it.filled_gauge(cx, cy, 116, 98, static_cast<int>(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");
}