Add internet usage dashboard page
This commit is contained in:
+177
-7
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user