Add internet usage dashboard page
This commit is contained in:
+177
-7
@@ -1,9 +1,11 @@
|
|||||||
substitutions:
|
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"
|
name: "energymonitor"
|
||||||
friendly_name: "Energy Monitor"
|
friendly_name: "Energy Monitor"
|
||||||
grid_power_gauge_max_w: "5000.0"
|
grid_power_gauge_max_w: "5000.0"
|
||||||
balcony_solar_gauge_max_w: "800.0"
|
balcony_solar_gauge_max_w: "800.0"
|
||||||
|
internet_download_gauge_max_mbps: "1000.0"
|
||||||
|
internet_upload_gauge_max_mbps: "1000.0"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
comment: ${device_description}
|
comment: ${device_description}
|
||||||
@@ -120,6 +122,11 @@ font:
|
|||||||
glyphsets:
|
glyphsets:
|
||||||
- GF_Latin_Kernel
|
- GF_Latin_Kernel
|
||||||
glyphs: ["äöüÄÖÜß"]
|
glyphs: ["äöüÄÖÜß"]
|
||||||
|
- file: "fonts/Roboto-Bold.ttf"
|
||||||
|
id: font_internet_value
|
||||||
|
size: 22
|
||||||
|
glyphsets:
|
||||||
|
- GF_Latin_Kernel
|
||||||
- file: "fonts/Roboto-Regular.ttf"
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
id: font_device
|
id: font_device
|
||||||
size: 13
|
size: 13
|
||||||
@@ -159,6 +166,27 @@ globals:
|
|||||||
type: int
|
type: int
|
||||||
restore_value: true
|
restore_value: true
|
||||||
initial_value: '0'
|
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:
|
sensor:
|
||||||
# WiFi signal strength
|
# WiFi signal strength
|
||||||
@@ -258,14 +286,40 @@ sensor:
|
|||||||
id: microwave_power
|
id: microwave_power
|
||||||
entity_id: sensor.waschetrockner_leistung
|
entity_id: sensor.waschetrockner_leistung
|
||||||
internal: true
|
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
|
- platform: wireguard
|
||||||
latest_handshake:
|
latest_handshake:
|
||||||
name: '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:
|
script:
|
||||||
- id: show_previous_page
|
- id: show_previous_page
|
||||||
then:
|
then:
|
||||||
- lambda: id(page_index) = (id(page_index) + 3 - 1) % 3;
|
- lambda: id(page_index) = (id(page_index) + 4 - 1) % 4;
|
||||||
- component.update: tft
|
- component.update: tft
|
||||||
- id: toggle_page_mode
|
- id: toggle_page_mode
|
||||||
then:
|
then:
|
||||||
@@ -274,13 +328,15 @@ script:
|
|||||||
id(page_one_gauge_view) = !id(page_one_gauge_view);
|
id(page_one_gauge_view) = !id(page_one_gauge_view);
|
||||||
} else if (id(page_index) == 1) {
|
} else if (id(page_index) == 1) {
|
||||||
id(device_usage_view) = (id(device_usage_view) + 1) % 2;
|
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);
|
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
|
- component.update: tft
|
||||||
- id: show_next_page
|
- id: show_next_page
|
||||||
then:
|
then:
|
||||||
- lambda: id(page_index) = (id(page_index) + 1) % 3;
|
- lambda: id(page_index) = (id(page_index) + 1) % 4;
|
||||||
- component.update: tft
|
- component.update: tft
|
||||||
|
|
||||||
button:
|
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.
|
// Labels correspond to the left (A) and right (C) hardware buttons.
|
||||||
it.printf(8, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_LEFT,
|
it.printf(8, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_LEFT,
|
||||||
"< Zurück");
|
"< Zurück");
|
||||||
@@ -640,11 +807,14 @@ display:
|
|||||||
"Weiter >");
|
"Weiter >");
|
||||||
if (id(page_index) == 0) {
|
if (id(page_index) == 0) {
|
||||||
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
||||||
"Ansicht · 1 / 3");
|
"Ansicht · 1 / 4");
|
||||||
} else if (id(page_index) == 1) {
|
} else if (id(page_index) == 1) {
|
||||||
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
||||||
"Geräte %d / 2", id(device_usage_view) + 1);
|
"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,
|
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