Show an alert from a Home Assistant automation (0.2.36)
Every screen gets the user action esphome.<device>_show_alert with title, subtitle, icon, color, button_text, timeout and flash. The card sits on LVGL's top layer above every page, card and the dim overlay, wakes the screen and keeps the backlight at its normal brightness until the button (or the timeout; 0 waits for the button, the button always closes at once). flash: true blinks the backlight four times on arrival. Icons are the tile picker names (also mdi:name or the hex codepoint of a compiled glyph, else alert-outline), colours the tile pastels. A new alert replaces the current one; every ending fires esphome.screen_alert with action, title and screen. dismiss_alert removes the card remotely. The field is button_text rather than button: ESPHome reserves the names of loaded components as ids and the CYD profile loads button:. The C++ namespace is screen_alert because the widget id alert_overlay becomes a global lv_obj_t*. tools/generate_icons.py now also writes tile_icon_names.h (name to codepoint) from the one icon list. Firmware 0.2.31 for both boards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,44 @@ De feedback stopt zodra Home Assistant de gewijzigde stand meldt, met een
|
||||
minimum van 150 ms voor switches. Tegels met een mini-slider behouden hun
|
||||
icoon; op CYD staan het icoon en tekstblok verticaal gecentreerd.
|
||||
|
||||
## Alert vanuit een automatisering
|
||||
|
||||
Elk scherm heeft de actie **`esphome.<scherm>_show_alert`** (firmware 0.2.31+). Die legt
|
||||
een kaart over het hele scherm, wekt het scherm en houdt de backlight op de normale
|
||||
helderheid tot iemand op **Oké** tikt. In een automatisering:
|
||||
|
||||
```yaml
|
||||
action: esphome.keuken_scherm_show_alert
|
||||
data:
|
||||
title: "Iemand belt aan"
|
||||
subtitle: "Deur 3, achterkant"
|
||||
icon: doorbell
|
||||
color: orange
|
||||
button_text: "Ik kom"
|
||||
timeout: 0
|
||||
flash: true
|
||||
```
|
||||
|
||||
- **`title`** en **`subtitle`**: een titel op één regel (te lang krijgt puntjes) en een
|
||||
toelichting die over meerdere regels loopt. Leeg mag; een lege titel wordt "Melding".
|
||||
- **`icon`**: een naam uit de tegelkiezer, zoals `doorbell`, `bell`, `alert-outline`,
|
||||
`lock`, `door-open`, `motion-sensor`, `smoke-detector`, `water-alert`, `mailbox`, `car`
|
||||
of `account`. `mdi:doorbell` en de hex-codepoint (`F12E6`) mogen ook, zolang het glyph
|
||||
in de firmware zit. Onbekend geeft de waarschuwingsdriehoek.
|
||||
- **`color`**: `red`, `orange`, `yellow`, `green`, `mint`, `blue`, `purple`, `pink` of
|
||||
`gray`, dezelfde pasteltinten als de tegels. Leeg geeft de witte kaart.
|
||||
- **`button_text`**: de tekst op de knop; leeg is "Oké".
|
||||
- **`timeout`**: seconden waarna de kaart vanzelf verdwijnt; `0` is tot de knop, hoe lang
|
||||
dat ook duurt. De knop sluit de kaart altijd direct, ook met een timeout. Standby en
|
||||
nachtstand wachten zolang de kaart staat.
|
||||
- **`flash`**: `true` laat de backlight vier keer knipperen bij binnenkomst.
|
||||
|
||||
Home Assistant vraagt alle zeven velden; laat een veld leeg (`""`, `0`, `false`) als je het
|
||||
niet gebruikt. Een nieuwe alert vervangt de huidige. Elk einde meldt zich als event
|
||||
**`esphome.screen_alert`** met `action` (`ok`, `timeout`, `replaced` of `remote`), `title`,
|
||||
`screen` en het `device_id` dat Home Assistant toevoegt, zodat een automatisering op Oké
|
||||
kan wachten. **`esphome.<scherm>_dismiss_alert`** haalt de kaart op afstand weg.
|
||||
|
||||
## Installeren vanuit Home Assistant
|
||||
|
||||
### Heb ik ESPHome nodig?
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#if __has_include("esphome/components/smart_display/alert_overlay.h")
|
||||
#include "esphome/components/smart_display/alert_overlay.h"
|
||||
#else
|
||||
#include "components/smart_display/alert_overlay.h"
|
||||
#endif
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "tile_icon.h"
|
||||
#include "tile_icon_names.h"
|
||||
#include "tile_palette.h"
|
||||
|
||||
// The card a Home Assistant automation puts over the whole screen (api action
|
||||
// show_alert). Pure text and lookups so both boards apply the same rules and
|
||||
// tests/test_alert_overlay.cpp can check them; the profiles do the LVGL work.
|
||||
namespace screen_alert {
|
||||
constexpr const char *FALLBACK_ICON = "alert-outline";
|
||||
constexpr const char *FALLBACK_TITLE = "Melding";
|
||||
constexpr const char *FALLBACK_BUTTON = "Oké";
|
||||
constexpr uint32_t DEFAULT_CARD_COLOR = 0xFFFFFF;
|
||||
constexpr int MAX_TIMEOUT_SECONDS = 86400;
|
||||
|
||||
struct Alert {
|
||||
std::string title, subtitle, icon, button; // icon: UTF-8 glyph for the icon font
|
||||
uint32_t color = DEFAULT_CARD_COLOR;
|
||||
int timeout_seconds = 0; // 0: stays until Oké
|
||||
bool flash = false;
|
||||
};
|
||||
|
||||
inline bool blank(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
|
||||
inline std::string trimmed(const std::string &text) {
|
||||
size_t begin = 0, end = text.size();
|
||||
while (begin < end && blank(text[begin])) ++begin;
|
||||
while (end > begin && blank(text[end - 1])) --end;
|
||||
return text.substr(begin, end - begin);
|
||||
}
|
||||
inline std::string lowercase(std::string text) {
|
||||
for (auto &c : text) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||
return text;
|
||||
}
|
||||
// Cuts on a UTF-8 boundary: LVGL must never get half a code point.
|
||||
inline std::string clipped(const std::string &text, size_t max_bytes) {
|
||||
if (text.size() <= max_bytes) return text;
|
||||
size_t end = max_bytes;
|
||||
while (end > 0 && (static_cast<unsigned char>(text[end]) & 0xC0) == 0x80) --end;
|
||||
return text.substr(0, end);
|
||||
}
|
||||
// "doorbell", "mdi:doorbell" or the hex codepoint "F12E6" of a glyph the fonts carry;
|
||||
// anything else draws the warning triangle.
|
||||
inline uint32_t icon_codepoint(const std::string &value) {
|
||||
std::string name = lowercase(trimmed(value));
|
||||
if (name.rfind("mdi:", 0) == 0) name.erase(0, 4);
|
||||
if (uint32_t code = tile_icon::named(name)) return code;
|
||||
const uint32_t code = tile_icon::codepoint(name);
|
||||
if (code && tile_icon::carried(code)) return code;
|
||||
return tile_icon::named(FALLBACK_ICON);
|
||||
}
|
||||
inline int clamp_timeout(int seconds) {
|
||||
return seconds < 0 ? 0 : seconds > MAX_TIMEOUT_SECONDS ? MAX_TIMEOUT_SECONDS : seconds;
|
||||
}
|
||||
inline Alert make(const std::string &title, const std::string &subtitle, const std::string &icon,
|
||||
const std::string &color, const std::string &button, int timeout, bool flash,
|
||||
size_t title_max, size_t subtitle_max, size_t button_max) {
|
||||
Alert alert;
|
||||
alert.title = clipped(trimmed(title), title_max);
|
||||
if (alert.title.empty()) alert.title = FALLBACK_TITLE;
|
||||
alert.subtitle = clipped(trimmed(subtitle), subtitle_max);
|
||||
alert.button = clipped(trimmed(button), button_max);
|
||||
if (alert.button.empty()) alert.button = FALLBACK_BUTTON;
|
||||
alert.icon = tile_icon::utf8(icon_codepoint(icon));
|
||||
const uint32_t card = tile_palette::color(lowercase(trimmed(color)));
|
||||
alert.color = card ? card : DEFAULT_CARD_COLOR;
|
||||
alert.timeout_seconds = clamp_timeout(timeout);
|
||||
alert.flash = flash;
|
||||
return alert;
|
||||
}
|
||||
} // namespace screen_alert
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
// GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py; do not edit.
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace tile_icon {
|
||||
struct Named { const char *name; uint32_t codepoint; };
|
||||
// Every glyph the three icon fonts of both boards carry, by Material Design Icons name.
|
||||
inline constexpr Named NAMES[] = {
|
||||
{"lightbulb", 0xF0335},
|
||||
{"lightbulb-group", 0xF1253},
|
||||
{"ceiling-light", 0xF0769},
|
||||
{"chandelier", 0xF1793},
|
||||
{"lamp", 0xF06B5},
|
||||
{"floor-lamp", 0xF08DD},
|
||||
{"desk-lamp", 0xF095F},
|
||||
{"wall-sconce-flat", 0xF091D},
|
||||
{"light-recessed", 0xF179B},
|
||||
{"spotlight-beam", 0xF04C9},
|
||||
{"led-strip-variant", 0xF1051},
|
||||
{"outdoor-lamp", 0xF1054},
|
||||
{"home", 0xF02DC},
|
||||
{"sofa", 0xF04B9},
|
||||
{"bed", 0xF02E3},
|
||||
{"silverware-fork-knife", 0xF0A70},
|
||||
{"chef-hat", 0xF0B7C},
|
||||
{"shower", 0xF09A0},
|
||||
{"bathtub", 0xF1818},
|
||||
{"toilet", 0xF09AB},
|
||||
{"desk", 0xF1239},
|
||||
{"garage", 0xF06D9},
|
||||
{"fireplace", 0xF0E2E},
|
||||
{"thermometer", 0xF050F},
|
||||
{"thermostat", 0xF0393},
|
||||
{"air-conditioner", 0xF001B},
|
||||
{"radiator", 0xF0438},
|
||||
{"heat-pump", 0xF1A43},
|
||||
{"fire", 0xF0238},
|
||||
{"snowflake", 0xF0717},
|
||||
{"water-percent", 0xF058E},
|
||||
{"fan", 0xF0210},
|
||||
{"ceiling-fan", 0xF1797},
|
||||
{"air-purifier", 0xF0D44},
|
||||
{"molecule-co2", 0xF07E4},
|
||||
{"weather-sunny", 0xF0599},
|
||||
{"weather-partly-cloudy", 0xF0595},
|
||||
{"weather-cloudy", 0xF0590},
|
||||
{"weather-rainy", 0xF0597},
|
||||
{"weather-snowy", 0xF0598},
|
||||
{"weather-windy", 0xF059D},
|
||||
{"weather-night", 0xF0594},
|
||||
{"weather-sunset-up", 0xF059C},
|
||||
{"music-note", 0xF0387},
|
||||
{"music", 0xF075A},
|
||||
{"playlist-music", 0xF0CB8},
|
||||
{"album", 0xF0025},
|
||||
{"record-player", 0xF099A},
|
||||
{"spotify", 0xF04C7},
|
||||
{"radio", 0xF0439},
|
||||
{"podcast", 0xF0994},
|
||||
{"speaker", 0xF04C3},
|
||||
{"speaker-multiple", 0xF0D38},
|
||||
{"soundbar", 0xF17DB},
|
||||
{"headphones", 0xF02CB},
|
||||
{"piano", 0xF067D},
|
||||
{"guitar-acoustic", 0xF0771},
|
||||
{"television", 0xF0502},
|
||||
{"projector", 0xF042E},
|
||||
{"movie-open", 0xF0FCE},
|
||||
{"cast", 0xF0118},
|
||||
{"gamepad-variant", 0xF0297},
|
||||
{"remote-tv", 0xF0EC5},
|
||||
{"lock", 0xF033E},
|
||||
{"lock-open-variant", 0xF0FC6},
|
||||
{"shield-home", 0xF068A},
|
||||
{"alarm-light", 0xF078F},
|
||||
{"bell", 0xF009A},
|
||||
{"doorbell", 0xF12E6},
|
||||
{"cctv", 0xF07AE},
|
||||
{"motion-sensor", 0xF0D91},
|
||||
{"door-closed", 0xF081B},
|
||||
{"door-open", 0xF081C},
|
||||
{"window-closed-variant", 0xF11DB},
|
||||
{"smoke-detector", 0xF0392},
|
||||
{"water-alert", 0xF1502},
|
||||
{"washing-machine", 0xF072A},
|
||||
{"tumble-dryer", 0xF0917},
|
||||
{"dishwasher", 0xF0AAC},
|
||||
{"fridge", 0xF0290},
|
||||
{"stove", 0xF04DE},
|
||||
{"microwave", 0xF0C99},
|
||||
{"toaster-oven", 0xF0CD3},
|
||||
{"coffee-maker", 0xF109F},
|
||||
{"kettle", 0xF05FA},
|
||||
{"robot-vacuum", 0xF070D},
|
||||
{"robot-mower", 0xF11F7},
|
||||
{"printer-3d", 0xF042B},
|
||||
{"power-plug", 0xF06A5},
|
||||
{"power-socket-eu", 0xF07E7},
|
||||
{"laptop", 0xF0322},
|
||||
{"router-wireless", 0xF0469},
|
||||
{"flash", 0xF0241},
|
||||
{"solar-power", 0xF0A72},
|
||||
{"solar-panel", 0xF0D9B},
|
||||
{"home-battery", 0xF1901},
|
||||
{"battery-high", 0xF12A3},
|
||||
{"ev-station", 0xF05F1},
|
||||
{"car-electric", 0xF0B6C},
|
||||
{"meter-electric", 0xF1A57},
|
||||
{"meter-gas", 0xF1A59},
|
||||
{"water", 0xF058C},
|
||||
{"water-boiler", 0xF0F92},
|
||||
{"gauge", 0xF029A},
|
||||
{"window-shutter", 0xF111C},
|
||||
{"window-shutter-open", 0xF111E},
|
||||
{"blinds", 0xF00AC},
|
||||
{"blinds-open", 0xF1011},
|
||||
{"roller-shade-closed", 0xF1A6C},
|
||||
{"curtains", 0xF1846},
|
||||
{"curtains-closed", 0xF1847},
|
||||
{"awning-outline", 0xF1B88},
|
||||
{"flower", 0xF024A},
|
||||
{"tree", 0xF0531},
|
||||
{"sprout", 0xF0E66},
|
||||
{"grass", 0xF1510},
|
||||
{"sprinkler", 0xF105F},
|
||||
{"pool", 0xF0606},
|
||||
{"grill", 0xF0E45},
|
||||
{"mailbox", 0xF06EE},
|
||||
{"trash-can", 0xF0A79},
|
||||
{"recycle", 0xF044C},
|
||||
{"dog", 0xF0A43},
|
||||
{"cat", 0xF011B},
|
||||
{"account", 0xF0004},
|
||||
{"account-group", 0xF0849},
|
||||
{"account-child", 0xF0A89},
|
||||
{"home-account", 0xF0826},
|
||||
{"sleep", 0xF04B2},
|
||||
{"run", 0xF070E},
|
||||
{"briefcase", 0xF00D6},
|
||||
{"car", 0xF010B},
|
||||
{"bike", 0xF00A3},
|
||||
{"airplane", 0xF001D},
|
||||
{"map-marker", 0xF034E},
|
||||
{"power", 0xF0425},
|
||||
{"toggle-switch", 0xF0521},
|
||||
{"gesture-tap-button", 0xF12A8},
|
||||
{"play", 0xF040A},
|
||||
{"script-text", 0xF0BC2},
|
||||
{"palette", 0xF03D8},
|
||||
{"alarm", 0xF0020},
|
||||
{"clock-outline", 0xF0150},
|
||||
{"timer-outline", 0xF051B},
|
||||
{"calendar", 0xF00ED},
|
||||
{"bell-ring", 0xF009E},
|
||||
{"heart", 0xF02D1},
|
||||
{"alert-outline", 0xF002A},
|
||||
{"cog", 0xF0493},
|
||||
{"broom", 0xF00E2},
|
||||
{"alert-circle-outline", 0xF05D6},
|
||||
{"check", 0xF012C},
|
||||
{"pause", 0xF03E4},
|
||||
{"stop", 0xF04DB},
|
||||
{"skip-next", 0xF04AD},
|
||||
{"skip-previous", 0xF04AE},
|
||||
{"volume-high", 0xF057E},
|
||||
{"volume-off", 0xF0581},
|
||||
{"arrow-up", 0xF005D},
|
||||
{"arrow-down", 0xF0045},
|
||||
{"arrow-expand-horizontal", 0xF084E},
|
||||
{"arrow-collapse-horizontal", 0xF084C},
|
||||
{"home-map-marker", 0xF05F8},
|
||||
{"plus", 0xF0415},
|
||||
{"minus", 0xF0374},
|
||||
{"chevron-left", 0xF0141},
|
||||
{"chevron-right", 0xF0142},
|
||||
{"close", 0xF0156},
|
||||
{"sun-snowflake-variant", 0xF1A79},
|
||||
{"thermostat-auto", 0xF1B17},
|
||||
{"weather-fog", 0xF0591},
|
||||
{"weather-hail", 0xF0592},
|
||||
{"weather-lightning", 0xF0593},
|
||||
{"weather-lightning-rainy", 0xF067E},
|
||||
{"weather-pouring", 0xF0596},
|
||||
{"weather-snowy-rainy", 0xF067F},
|
||||
{"weather-sunset-down", 0xF059B},
|
||||
};
|
||||
inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);
|
||||
// Codepoint of a name the fonts carry; 0 for any other name.
|
||||
inline uint32_t named(const std::string &name) {
|
||||
for (const auto &entry : NAMES) if (name == entry.name) return entry.codepoint;
|
||||
return 0;
|
||||
}
|
||||
// True when the fonts carry this codepoint, so a label can draw it.
|
||||
inline bool carried(uint32_t codepoint) {
|
||||
for (const auto &entry : NAMES) if (entry.codepoint == codepoint) return true;
|
||||
return false;
|
||||
}
|
||||
} // namespace tile_icon
|
||||
@@ -0,0 +1,54 @@
|
||||
# Testresultaten app 0.2.36 / firmware 0.2.31 (2026-09-14)
|
||||
|
||||
Nieuw: een alert vanuit een Home Assistant-automatisering (`esphome.<scherm>_show_alert`),
|
||||
zie README "Alert vanuit een automatisering". De kaart staat op LVGL's toplaag boven elke
|
||||
pagina, kaart en de standby-laag; het scherm wordt gewekt en blijft op de normale
|
||||
helderheid tot de knop of de timeout.
|
||||
|
||||
## Wat veranderde
|
||||
|
||||
- `components/smart_display/alert_overlay.h` (namespace `screen_alert`): titel/subtitle
|
||||
trimmen en afkappen op een UTF-8-grens, knoptekst met fallback "Oké", icoonnaam,
|
||||
`mdi:`-prefix of hex-codepoint alleen als het glyph in de fonts zit (anders
|
||||
`alert-outline`), paletkleur of witte kaart, timeout 0..86400.
|
||||
- `components/smart_display/tile_icon_names.h` wordt door `tools/generate_icons.py`
|
||||
gegenereerd uit dezelfde lijst als de fonts (naam naar codepoint).
|
||||
- Beide profielen: `top_layer` met `alert_overlay`, scripts `alert_show`, `alert_dismiss`,
|
||||
`alert_flash` (vier backlight-knippers), `alert_timeout`, `alert_report`
|
||||
(`esphome.screen_alert` met `action`, `title`, `screen`); API-acties `show_alert` en
|
||||
`dismiss_alert`; de standby-guard wacht op `alert_active`. Het veld heet `button_text`:
|
||||
ESPHome reserveert namen van geladen componenten als id en de CYD heeft `button:`.
|
||||
|
||||
## Geautomatiseerd
|
||||
|
||||
- C++ (clang++ -std=c++17 -Wall -Wextra -Werror -I.): 12/12 PASS; nieuw
|
||||
`test_alert_overlay` (iconen, kleuren, timeout-grenzen, afkappen, knoptekst).
|
||||
- Python: 98 tests OK (`FIRMWARE_VERSION` 0.2.31 in app, profielen en pakketten); nieuw
|
||||
`test_alert.py` (actievelden, toplaag vóór de pagina's, standby-guard, geen ander
|
||||
sluitpad, events, zelftest zonder HA-actie, kaartgeometrie op beide borden);
|
||||
`generate_packages.py --check` en `generate_icons.py --check` groen; bench-kopieën
|
||||
opnieuw gegenereerd.
|
||||
- ESPHome 2026.6.2 compile: `easy-cyd-device.yaml` SUCCESS in 27 s (RAM 31,3%, flash
|
||||
76,8%; 0.2.29 was 31,0% / 76,1%), `easy-guition-device.yaml` SUCCESS in 28 s (RAM
|
||||
32,3%, flash 18,5%; was 32,0% / 18,3%). Eén waarschuwing, dezelfde als eerder
|
||||
(`runtime_tiles.h:546`, enum-conversie in de detailkaart), niet uit deze wijziging.
|
||||
|
||||
## Niet getest
|
||||
|
||||
- Geen scherm aan USB tijdens deze build: de alert is niet op glas gezien. Wekken, de
|
||||
knippers, de knop en de events zijn alleen door validatie, compile en de tekst-tests
|
||||
gedekt.
|
||||
|
||||
## Te controleren op Studio 1 na de update
|
||||
|
||||
- `esphome.studio_1_show_alert` met en zonder `flash`, met `timeout: 0` en `timeout: 10`;
|
||||
de kaart moet boven een open lichtkaart en op elke pagina liggen; de knop sluit direct,
|
||||
ook met een timeout.
|
||||
- Backlight blijft op de normale helderheid voorbij de standby-tijd zolang de kaart staat;
|
||||
na de knop dimt het scherm weer na de gewone standby-tijd.
|
||||
- Event `esphome.screen_alert` (Ontwikkelhulpmiddelen → Events) met `action` ok, timeout,
|
||||
replaced of remote, `title`, `screen` en `device_id`.
|
||||
- Iconen (`doorbell`, `mdi:bell`, `F002A`, onbekend) en kleuren; een lange titel krijgt
|
||||
puntjes, een lange subtitle wikkelt binnen de kaart; `button_text` op de knop.
|
||||
- `esphome.studio_1_dismiss_alert` haalt de kaart weg.
|
||||
- Guition: rotatie 90/180/270 met een alert op het scherm.
|
||||
+264
-2
@@ -1,7 +1,7 @@
|
||||
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
|
||||
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.30"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.31"
|
||||
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
|
||||
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
|
||||
TOUCH_MOVE_LIMIT_PX: "0"
|
||||
@@ -549,6 +549,23 @@ substitutions:
|
||||
TILE_W: "218"
|
||||
TILE_H: "108"
|
||||
HEADER_Y: "22"
|
||||
|
||||
# Alert overlay (HA action show_alert), 480x480: one card centred on LVGL's top layer.
|
||||
ALERT_CARD_W: "420"
|
||||
ALERT_CARD_H: "320"
|
||||
ALERT_ICON_X: "26"
|
||||
ALERT_ICON_Y: "22"
|
||||
ALERT_TEXT_X: "90"
|
||||
ALERT_TEXT_W: "304"
|
||||
ALERT_TITLE_Y: "26"
|
||||
ALERT_SUBTITLE_Y: "68"
|
||||
ALERT_SUBTITLE_H: "150"
|
||||
ALERT_BUTTON_W: "150"
|
||||
ALERT_BUTTON_H: "60"
|
||||
ALERT_BUTTON_INSET: "22"
|
||||
ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis
|
||||
ALERT_BUTTON_MAX: "16" # bytes on the Oké button
|
||||
ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has
|
||||
OVERLAY_SLIDER_W: "144"
|
||||
OVERLAY_SLIDER_H: "300"
|
||||
OVERLAY_ICON_Y: "148"
|
||||
@@ -590,6 +607,7 @@ esphome:
|
||||
- light_controls.h
|
||||
- guition_diagnostics.h
|
||||
- backlight_fade.h
|
||||
- alert_overlay.h
|
||||
name: ${DEVICE_NAME}
|
||||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||||
on_boot:
|
||||
@@ -829,6 +847,32 @@ api:
|
||||
- action: ui_self_test
|
||||
then:
|
||||
- script.execute: ui_self_test
|
||||
# Home Assistant automations: esphome.<device>_show_alert puts a card over the whole
|
||||
# screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again.
|
||||
- action: show_alert
|
||||
variables:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_show
|
||||
title: !lambda 'return title;'
|
||||
subtitle: !lambda 'return subtitle;'
|
||||
icon: !lambda 'return icon;'
|
||||
color: !lambda 'return color;'
|
||||
button_text: !lambda 'return button_text;'
|
||||
timeout: !lambda 'return timeout;'
|
||||
flash: !lambda 'return flash;'
|
||||
- action: dismiss_alert
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "remote"
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
@@ -862,7 +906,7 @@ interval:
|
||||
condition:
|
||||
lambda: |-
|
||||
if (!screen_settings::current.standby_enabled) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||||
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
|
||||
then:
|
||||
- script.execute: dim_display
|
||||
@@ -884,6 +928,18 @@ time:
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
|
||||
globals:
|
||||
# Alert overlay (HA action show_alert): standby waits while one is up.
|
||||
- id: alert_active
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: alert_title_text
|
||||
type: std::string
|
||||
- id: alert_timeout_seconds
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: alert_flash_requested
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rendered_frames
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
@@ -2082,6 +2138,111 @@ lvgl:
|
||||
text_color: 0x1B1B1B
|
||||
text_opa: 100%
|
||||
|
||||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||||
# is filled by the alert_show script. Fixed positions, no layout engine.
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_overlay
|
||||
x: 0
|
||||
y: 0
|
||||
width: ${DISPLAY_W}
|
||||
height: ${DISPLAY_H}
|
||||
hidden: true
|
||||
bg_color: 0x101820
|
||||
bg_opa: 60%
|
||||
radius: 0
|
||||
border_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
# Same look when pressed: a tap next to the card must not repaint the screen.
|
||||
pressed:
|
||||
bg_opa: 60%
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_card
|
||||
align: CENTER
|
||||
width: ${ALERT_CARD_W}
|
||||
height: ${ALERT_CARD_H}
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
radius: 18
|
||||
border_width: 1
|
||||
border_color: 0xDDDDDD
|
||||
border_opa: COVER
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: false
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_icon
|
||||
x: ${ALERT_ICON_X}
|
||||
y: ${ALERT_ICON_Y}
|
||||
text: "\U000F002A"
|
||||
text_font: materialdesign_icons
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_title
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_TITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
long_mode: DOT
|
||||
text: "Melding"
|
||||
text_font: headline
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_subtitle
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_SUBTITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
height: ${ALERT_SUBTITLE_H}
|
||||
long_mode: WRAP
|
||||
text: ""
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A3A3C
|
||||
clickable: false
|
||||
- obj:
|
||||
id: alert_ok
|
||||
align: BOTTOM_RIGHT
|
||||
x: -${ALERT_BUTTON_INSET}
|
||||
y: -${ALERT_BUTTON_INSET}
|
||||
width: ${ALERT_BUTTON_W}
|
||||
height: ${ALERT_BUTTON_H}
|
||||
radius: 999
|
||||
bg_color: 0x1B1B1B
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pressed:
|
||||
bg_color: 0x3A3A3C
|
||||
bg_opa: COVER
|
||||
on_click:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "ok"
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_ok_label
|
||||
align: CENTER
|
||||
text: "Oké"
|
||||
text_font: headline
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
pages:
|
||||
- id: home_page
|
||||
styles: style_bg_grad
|
||||
@@ -4993,6 +5154,9 @@ script:
|
||||
- delay: 150ms
|
||||
- lambda: guition::check_geometry(id(vacuum_overlay));
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
- logger.log: "UI_TEST COMPLETE"
|
||||
|
||||
@@ -5069,6 +5233,104 @@ script:
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
|
||||
# ---------- ALERT (HA action show_alert) ----------
|
||||
# The screen wakes, keeps its normal brightness and shows the card until Oké or the
|
||||
# timeout (0: until Oké); the standby interval waits on alert_active. A new alert
|
||||
# replaces the current one; every ending reports itself as esphome.screen_alert.
|
||||
- id: alert_show
|
||||
mode: restart
|
||||
parameters:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||||
id(alert_active) = true;
|
||||
id(alert_title_text) = alert.title;
|
||||
id(alert_timeout_seconds) = alert.timeout_seconds;
|
||||
id(alert_flash_requested) = alert.flash;
|
||||
lv_label_set_text(id(alert_icon), alert.icon.c_str());
|
||||
lv_label_set_text(id(alert_title), alert.title.c_str());
|
||||
lv_label_set_text(id(alert_subtitle), alert.subtitle.c_str());
|
||||
lv_label_set_text(id(alert_ok_label), alert.button.c_str());
|
||||
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(alert.color), LV_PART_MAIN);
|
||||
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
|
||||
- script.execute: wake_display
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_flash_requested);'
|
||||
then:
|
||||
- script.execute: alert_flash
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_timeout_seconds) > 0;'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_timeout
|
||||
seconds: !lambda 'return id(alert_timeout_seconds);'
|
||||
|
||||
- id: alert_dismiss
|
||||
parameters:
|
||||
reason: string
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_active);'
|
||||
then:
|
||||
- lambda: |-
|
||||
id(alert_active) = false;
|
||||
id(last_touch_ms) = millis();
|
||||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- script.stop: alert_flash
|
||||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 80);'
|
||||
- script.execute:
|
||||
id: alert_report
|
||||
action: !lambda 'return reason;'
|
||||
|
||||
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
|
||||
- id: alert_flash
|
||||
mode: restart
|
||||
then:
|
||||
- repeat:
|
||||
count: 4
|
||||
then:
|
||||
- lambda: 'id(set_backlight).execute(0, 60);'
|
||||
- delay: 140ms
|
||||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 60);'
|
||||
- delay: 180ms
|
||||
|
||||
- id: alert_timeout
|
||||
mode: restart
|
||||
parameters:
|
||||
seconds: int
|
||||
then:
|
||||
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "timeout"
|
||||
|
||||
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
|
||||
- id: alert_report
|
||||
parameters:
|
||||
action: string
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.screen_alert
|
||||
data:
|
||||
action: !lambda 'return action;'
|
||||
title: !lambda 'return id(alert_title_text);'
|
||||
screen: !lambda 'return App.get_name();'
|
||||
|
||||
- id: wake_display
|
||||
mode: restart
|
||||
then:
|
||||
|
||||
+272
-2
@@ -58,7 +58,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.30"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.31"
|
||||
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
|
||||
TOUCH_MOVE_LIMIT_PX: "56"
|
||||
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
|
||||
@@ -635,6 +635,23 @@ substitutions:
|
||||
OVERLAY_SLIDER_W: "96"
|
||||
OVERLAY_SLIDER_H: "160"
|
||||
OVERLAY_ICON_Y: "74"
|
||||
|
||||
# Alert overlay (HA action show_alert), landscape 320x240: one card centred on LVGL's top layer.
|
||||
ALERT_CARD_W: "292"
|
||||
ALERT_CARD_H: "196"
|
||||
ALERT_ICON_X: "18"
|
||||
ALERT_ICON_Y: "16"
|
||||
ALERT_TEXT_X: "62"
|
||||
ALERT_TEXT_W: "212"
|
||||
ALERT_TITLE_Y: "16"
|
||||
ALERT_SUBTITLE_Y: "44"
|
||||
ALERT_SUBTITLE_H: "88"
|
||||
ALERT_BUTTON_W: "100"
|
||||
ALERT_BUTTON_H: "40"
|
||||
ALERT_BUTTON_INSET: "14"
|
||||
ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis
|
||||
ALERT_BUTTON_MAX: "12" # bytes on the Oké button
|
||||
ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has
|
||||
BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition
|
||||
BG_BOTTOM_COLOR: "0xE7E7E7"
|
||||
TILE_ICON_X: "0"
|
||||
@@ -777,6 +794,7 @@ esphome:
|
||||
includes:
|
||||
- cyd_ui.h
|
||||
- light_controls.h
|
||||
- alert_overlay.h
|
||||
name: ${DEVICE_NAME}
|
||||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||||
on_boot:
|
||||
@@ -1024,6 +1042,32 @@ api:
|
||||
- action: ui_self_test
|
||||
then:
|
||||
- script.execute: ui_self_test
|
||||
# Home Assistant automations: esphome.<device>_show_alert puts a card over the whole
|
||||
# screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again.
|
||||
- action: show_alert
|
||||
variables:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_show
|
||||
title: !lambda 'return title;'
|
||||
subtitle: !lambda 'return subtitle;'
|
||||
icon: !lambda 'return icon;'
|
||||
color: !lambda 'return color;'
|
||||
button_text: !lambda 'return button_text;'
|
||||
timeout: !lambda 'return timeout;'
|
||||
flash: !lambda 'return flash;'
|
||||
- action: dismiss_alert
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "remote"
|
||||
encryption:
|
||||
key: !secret api_encryption_key
|
||||
|
||||
@@ -1057,7 +1101,7 @@ interval:
|
||||
condition:
|
||||
lambda: |-
|
||||
if (!screen_settings::current.standby_enabled) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||||
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
|
||||
then:
|
||||
- script.execute: dim_display
|
||||
@@ -1079,6 +1123,18 @@ time:
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
|
||||
globals:
|
||||
# Alert overlay (HA action show_alert): standby waits while one is up.
|
||||
- id: alert_active
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: alert_title_text
|
||||
type: std::string
|
||||
- id: alert_timeout_seconds
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: alert_flash_requested
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rendered_frames
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
@@ -2262,6 +2318,111 @@ lvgl:
|
||||
text_color: 0x1B1B1B
|
||||
text_opa: 100%
|
||||
|
||||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||||
# is filled by the alert_show script. Fixed positions, no layout engine.
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_overlay
|
||||
x: 0
|
||||
y: 0
|
||||
width: ${DISPLAY_W}
|
||||
height: ${DISPLAY_H}
|
||||
hidden: true
|
||||
bg_color: 0x101820
|
||||
bg_opa: 60%
|
||||
radius: 0
|
||||
border_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
# Same look when pressed: a tap next to the card must not repaint the screen.
|
||||
pressed:
|
||||
bg_opa: 60%
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_card
|
||||
align: CENTER
|
||||
width: ${ALERT_CARD_W}
|
||||
height: ${ALERT_CARD_H}
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
radius: 18
|
||||
border_width: 1
|
||||
border_color: 0xDDDDDD
|
||||
border_opa: COVER
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: false
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_icon
|
||||
x: ${ALERT_ICON_X}
|
||||
y: ${ALERT_ICON_Y}
|
||||
text: "\U000F002A"
|
||||
text_font: materialdesign_icons
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_title
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_TITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
long_mode: DOT
|
||||
text: "Melding"
|
||||
text_font: headline
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_subtitle
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_SUBTITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
height: ${ALERT_SUBTITLE_H}
|
||||
long_mode: WRAP
|
||||
text: ""
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A3A3C
|
||||
clickable: false
|
||||
- obj:
|
||||
id: alert_ok
|
||||
align: BOTTOM_RIGHT
|
||||
x: -${ALERT_BUTTON_INSET}
|
||||
y: -${ALERT_BUTTON_INSET}
|
||||
width: ${ALERT_BUTTON_W}
|
||||
height: ${ALERT_BUTTON_H}
|
||||
radius: 999
|
||||
bg_color: 0x1B1B1B
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pressed:
|
||||
bg_color: 0x3A3A3C
|
||||
bg_opa: COVER
|
||||
on_click:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "ok"
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_ok_label
|
||||
align: CENTER
|
||||
text: "Oké"
|
||||
text_font: headline
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
pages:
|
||||
- id: home_page
|
||||
styles: style_bg_grad
|
||||
@@ -5099,6 +5260,9 @@ script:
|
||||
- lvgl.widget.show: vacuum_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
- logger.log: "UI_TEST COMPLETE"
|
||||
|
||||
@@ -5169,6 +5333,112 @@ script:
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
|
||||
# ---------- ALERT (HA action show_alert) ----------
|
||||
# The screen wakes, keeps its normal brightness and shows the card until Oké or the
|
||||
# timeout (0: until Oké); the standby interval waits on alert_active. A new alert
|
||||
# replaces the current one; every ending reports itself as esphome.screen_alert.
|
||||
- id: alert_show
|
||||
mode: restart
|
||||
parameters:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||||
id(alert_active) = true;
|
||||
id(alert_title_text) = alert.title;
|
||||
id(alert_timeout_seconds) = alert.timeout_seconds;
|
||||
id(alert_flash_requested) = alert.flash;
|
||||
lv_label_set_text(id(alert_icon), alert.icon.c_str());
|
||||
lv_label_set_text(id(alert_title), alert.title.c_str());
|
||||
lv_label_set_text(id(alert_subtitle), alert.subtitle.c_str());
|
||||
lv_label_set_text(id(alert_ok_label), alert.button.c_str());
|
||||
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(alert.color), LV_PART_MAIN);
|
||||
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
|
||||
- script.execute: wake_display
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_flash_requested);'
|
||||
then:
|
||||
- script.execute: alert_flash
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_timeout_seconds) > 0;'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_timeout
|
||||
seconds: !lambda 'return id(alert_timeout_seconds);'
|
||||
|
||||
- id: alert_dismiss
|
||||
parameters:
|
||||
reason: string
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_active);'
|
||||
then:
|
||||
- lambda: |-
|
||||
id(alert_active) = false;
|
||||
id(last_touch_ms) = millis();
|
||||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- script.stop: alert_flash
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
transition_length: 80ms
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
- script.execute:
|
||||
id: alert_report
|
||||
action: !lambda 'return reason;'
|
||||
|
||||
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
|
||||
- id: alert_flash
|
||||
mode: restart
|
||||
then:
|
||||
- repeat:
|
||||
count: 4
|
||||
then:
|
||||
- light.turn_off:
|
||||
id: back_light
|
||||
transition_length: 0s
|
||||
- delay: 120ms
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
transition_length: 0s
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
- delay: 170ms
|
||||
|
||||
- id: alert_timeout
|
||||
mode: restart
|
||||
parameters:
|
||||
seconds: int
|
||||
then:
|
||||
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "timeout"
|
||||
|
||||
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
|
||||
- id: alert_report
|
||||
parameters:
|
||||
action: string
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.screen_alert
|
||||
data:
|
||||
action: !lambda 'return action;'
|
||||
title: !lambda 'return id(alert_title_text);'
|
||||
screen: !lambda 'return App.get_name();'
|
||||
|
||||
- id: wake_display
|
||||
mode: restart
|
||||
then:
|
||||
|
||||
+272
-2
@@ -59,7 +59,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.30"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.31"
|
||||
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
|
||||
TOUCH_MOVE_LIMIT_PX: "56"
|
||||
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
|
||||
@@ -636,6 +636,23 @@ substitutions:
|
||||
OVERLAY_SLIDER_W: "96"
|
||||
OVERLAY_SLIDER_H: "160"
|
||||
OVERLAY_ICON_Y: "74"
|
||||
|
||||
# Alert overlay (HA action show_alert), landscape 320x240: one card centred on LVGL's top layer.
|
||||
ALERT_CARD_W: "292"
|
||||
ALERT_CARD_H: "196"
|
||||
ALERT_ICON_X: "18"
|
||||
ALERT_ICON_Y: "16"
|
||||
ALERT_TEXT_X: "62"
|
||||
ALERT_TEXT_W: "212"
|
||||
ALERT_TITLE_Y: "16"
|
||||
ALERT_SUBTITLE_Y: "44"
|
||||
ALERT_SUBTITLE_H: "88"
|
||||
ALERT_BUTTON_W: "100"
|
||||
ALERT_BUTTON_H: "40"
|
||||
ALERT_BUTTON_INSET: "14"
|
||||
ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis
|
||||
ALERT_BUTTON_MAX: "12" # bytes on the Oké button
|
||||
ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has
|
||||
BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition
|
||||
BG_BOTTOM_COLOR: "0xE7E7E7"
|
||||
TILE_ICON_X: "0"
|
||||
@@ -781,6 +798,7 @@ esphome:
|
||||
includes:
|
||||
- <esphome/components/smart_display/cyd_ui.h>
|
||||
- <esphome/components/smart_display/light_controls.h>
|
||||
- <esphome/components/smart_display/alert_overlay.h>
|
||||
name: ${DEVICE_NAME}
|
||||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||||
on_boot:
|
||||
@@ -1028,6 +1046,32 @@ api:
|
||||
- action: ui_self_test
|
||||
then:
|
||||
- script.execute: ui_self_test
|
||||
# Home Assistant automations: esphome.<device>_show_alert puts a card over the whole
|
||||
# screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again.
|
||||
- action: show_alert
|
||||
variables:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_show
|
||||
title: !lambda 'return title;'
|
||||
subtitle: !lambda 'return subtitle;'
|
||||
icon: !lambda 'return icon;'
|
||||
color: !lambda 'return color;'
|
||||
button_text: !lambda 'return button_text;'
|
||||
timeout: !lambda 'return timeout;'
|
||||
flash: !lambda 'return flash;'
|
||||
- action: dismiss_alert
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "remote"
|
||||
|
||||
captive_portal:
|
||||
|
||||
@@ -1046,7 +1090,7 @@ interval:
|
||||
condition:
|
||||
lambda: |-
|
||||
if (!screen_settings::current.standby_enabled) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||||
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
|
||||
then:
|
||||
- script.execute: dim_display
|
||||
@@ -1068,6 +1112,18 @@ time:
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
|
||||
globals:
|
||||
# Alert overlay (HA action show_alert): standby waits while one is up.
|
||||
- id: alert_active
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: alert_title_text
|
||||
type: std::string
|
||||
- id: alert_timeout_seconds
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: alert_flash_requested
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rendered_frames
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
@@ -2193,6 +2249,111 @@ lvgl:
|
||||
text_color: 0x1B1B1B
|
||||
text_opa: 100%
|
||||
|
||||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||||
# is filled by the alert_show script. Fixed positions, no layout engine.
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_overlay
|
||||
x: 0
|
||||
y: 0
|
||||
width: ${DISPLAY_W}
|
||||
height: ${DISPLAY_H}
|
||||
hidden: true
|
||||
bg_color: 0x101820
|
||||
bg_opa: 60%
|
||||
radius: 0
|
||||
border_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
# Same look when pressed: a tap next to the card must not repaint the screen.
|
||||
pressed:
|
||||
bg_opa: 60%
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_card
|
||||
align: CENTER
|
||||
width: ${ALERT_CARD_W}
|
||||
height: ${ALERT_CARD_H}
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
radius: 18
|
||||
border_width: 1
|
||||
border_color: 0xDDDDDD
|
||||
border_opa: COVER
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: false
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_icon
|
||||
x: ${ALERT_ICON_X}
|
||||
y: ${ALERT_ICON_Y}
|
||||
text: "\U000F002A"
|
||||
text_font: materialdesign_icons
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_title
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_TITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
long_mode: DOT
|
||||
text: "Melding"
|
||||
text_font: headline
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_subtitle
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_SUBTITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
height: ${ALERT_SUBTITLE_H}
|
||||
long_mode: WRAP
|
||||
text: ""
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A3A3C
|
||||
clickable: false
|
||||
- obj:
|
||||
id: alert_ok
|
||||
align: BOTTOM_RIGHT
|
||||
x: -${ALERT_BUTTON_INSET}
|
||||
y: -${ALERT_BUTTON_INSET}
|
||||
width: ${ALERT_BUTTON_W}
|
||||
height: ${ALERT_BUTTON_H}
|
||||
radius: 999
|
||||
bg_color: 0x1B1B1B
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pressed:
|
||||
bg_color: 0x3A3A3C
|
||||
bg_opa: COVER
|
||||
on_click:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "ok"
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_ok_label
|
||||
align: CENTER
|
||||
text: "Oké"
|
||||
text_font: headline
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
pages:
|
||||
- id: home_page
|
||||
styles: style_bg_grad
|
||||
@@ -5030,6 +5191,9 @@ script:
|
||||
- lvgl.widget.show: vacuum_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
- logger.log: "UI_TEST COMPLETE"
|
||||
|
||||
@@ -5100,6 +5264,112 @@ script:
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
|
||||
# ---------- ALERT (HA action show_alert) ----------
|
||||
# The screen wakes, keeps its normal brightness and shows the card until Oké or the
|
||||
# timeout (0: until Oké); the standby interval waits on alert_active. A new alert
|
||||
# replaces the current one; every ending reports itself as esphome.screen_alert.
|
||||
- id: alert_show
|
||||
mode: restart
|
||||
parameters:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||||
id(alert_active) = true;
|
||||
id(alert_title_text) = alert.title;
|
||||
id(alert_timeout_seconds) = alert.timeout_seconds;
|
||||
id(alert_flash_requested) = alert.flash;
|
||||
lv_label_set_text(id(alert_icon), alert.icon.c_str());
|
||||
lv_label_set_text(id(alert_title), alert.title.c_str());
|
||||
lv_label_set_text(id(alert_subtitle), alert.subtitle.c_str());
|
||||
lv_label_set_text(id(alert_ok_label), alert.button.c_str());
|
||||
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(alert.color), LV_PART_MAIN);
|
||||
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
|
||||
- script.execute: wake_display
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_flash_requested);'
|
||||
then:
|
||||
- script.execute: alert_flash
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_timeout_seconds) > 0;'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_timeout
|
||||
seconds: !lambda 'return id(alert_timeout_seconds);'
|
||||
|
||||
- id: alert_dismiss
|
||||
parameters:
|
||||
reason: string
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_active);'
|
||||
then:
|
||||
- lambda: |-
|
||||
id(alert_active) = false;
|
||||
id(last_touch_ms) = millis();
|
||||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- script.stop: alert_flash
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
transition_length: 80ms
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
- script.execute:
|
||||
id: alert_report
|
||||
action: !lambda 'return reason;'
|
||||
|
||||
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
|
||||
- id: alert_flash
|
||||
mode: restart
|
||||
then:
|
||||
- repeat:
|
||||
count: 4
|
||||
then:
|
||||
- light.turn_off:
|
||||
id: back_light
|
||||
transition_length: 0s
|
||||
- delay: 120ms
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
transition_length: 0s
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
- delay: 170ms
|
||||
|
||||
- id: alert_timeout
|
||||
mode: restart
|
||||
parameters:
|
||||
seconds: int
|
||||
then:
|
||||
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "timeout"
|
||||
|
||||
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
|
||||
- id: alert_report
|
||||
parameters:
|
||||
action: string
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.screen_alert
|
||||
data:
|
||||
action: !lambda 'return action;'
|
||||
title: !lambda 'return id(alert_title_text);'
|
||||
screen: !lambda 'return App.get_name();'
|
||||
|
||||
- id: wake_display
|
||||
mode: restart
|
||||
then:
|
||||
|
||||
+264
-2
@@ -2,7 +2,7 @@
|
||||
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
|
||||
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.30"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.31"
|
||||
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
|
||||
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
|
||||
TOUCH_MOVE_LIMIT_PX: "0"
|
||||
@@ -550,6 +550,23 @@ substitutions:
|
||||
TILE_W: "218"
|
||||
TILE_H: "108"
|
||||
HEADER_Y: "22"
|
||||
|
||||
# Alert overlay (HA action show_alert), 480x480: one card centred on LVGL's top layer.
|
||||
ALERT_CARD_W: "420"
|
||||
ALERT_CARD_H: "320"
|
||||
ALERT_ICON_X: "26"
|
||||
ALERT_ICON_Y: "22"
|
||||
ALERT_TEXT_X: "90"
|
||||
ALERT_TEXT_W: "304"
|
||||
ALERT_TITLE_Y: "26"
|
||||
ALERT_SUBTITLE_Y: "68"
|
||||
ALERT_SUBTITLE_H: "150"
|
||||
ALERT_BUTTON_W: "150"
|
||||
ALERT_BUTTON_H: "60"
|
||||
ALERT_BUTTON_INSET: "22"
|
||||
ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis
|
||||
ALERT_BUTTON_MAX: "16" # bytes on the Oké button
|
||||
ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has
|
||||
OVERLAY_SLIDER_W: "144"
|
||||
OVERLAY_SLIDER_H: "300"
|
||||
OVERLAY_ICON_Y: "148"
|
||||
@@ -594,6 +611,7 @@ esphome:
|
||||
- <esphome/components/smart_display/light_controls.h>
|
||||
- <esphome/components/smart_display/guition_diagnostics.h>
|
||||
- <esphome/components/smart_display/backlight_fade.h>
|
||||
- <esphome/components/smart_display/alert_overlay.h>
|
||||
name: ${DEVICE_NAME}
|
||||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||||
on_boot:
|
||||
@@ -833,6 +851,32 @@ api:
|
||||
- action: ui_self_test
|
||||
then:
|
||||
- script.execute: ui_self_test
|
||||
# Home Assistant automations: esphome.<device>_show_alert puts a card over the whole
|
||||
# screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again.
|
||||
- action: show_alert
|
||||
variables:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_show
|
||||
title: !lambda 'return title;'
|
||||
subtitle: !lambda 'return subtitle;'
|
||||
icon: !lambda 'return icon;'
|
||||
color: !lambda 'return color;'
|
||||
button_text: !lambda 'return button_text;'
|
||||
timeout: !lambda 'return timeout;'
|
||||
flash: !lambda 'return flash;'
|
||||
- action: dismiss_alert
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "remote"
|
||||
|
||||
captive_portal:
|
||||
|
||||
@@ -851,7 +895,7 @@ interval:
|
||||
condition:
|
||||
lambda: |-
|
||||
if (!screen_settings::current.standby_enabled) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
|
||||
if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||||
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
|
||||
then:
|
||||
- script.execute: dim_display
|
||||
@@ -873,6 +917,18 @@ time:
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
|
||||
globals:
|
||||
# Alert overlay (HA action show_alert): standby waits while one is up.
|
||||
- id: alert_active
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: alert_title_text
|
||||
type: std::string
|
||||
- id: alert_timeout_seconds
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: alert_flash_requested
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rendered_frames
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
@@ -2013,6 +2069,111 @@ lvgl:
|
||||
text_color: 0x1B1B1B
|
||||
text_opa: 100%
|
||||
|
||||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||||
# is filled by the alert_show script. Fixed positions, no layout engine.
|
||||
top_layer:
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_overlay
|
||||
x: 0
|
||||
y: 0
|
||||
width: ${DISPLAY_W}
|
||||
height: ${DISPLAY_H}
|
||||
hidden: true
|
||||
bg_color: 0x101820
|
||||
bg_opa: 60%
|
||||
radius: 0
|
||||
border_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
# Same look when pressed: a tap next to the card must not repaint the screen.
|
||||
pressed:
|
||||
bg_opa: 60%
|
||||
widgets:
|
||||
- obj:
|
||||
id: alert_card
|
||||
align: CENTER
|
||||
width: ${ALERT_CARD_W}
|
||||
height: ${ALERT_CARD_H}
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
radius: 18
|
||||
border_width: 1
|
||||
border_color: 0xDDDDDD
|
||||
border_opa: COVER
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: false
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_icon
|
||||
x: ${ALERT_ICON_X}
|
||||
y: ${ALERT_ICON_Y}
|
||||
text: "\U000F002A"
|
||||
text_font: materialdesign_icons
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_title
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_TITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
long_mode: DOT
|
||||
text: "Melding"
|
||||
text_font: headline
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: alert_subtitle
|
||||
x: ${ALERT_TEXT_X}
|
||||
y: ${ALERT_SUBTITLE_Y}
|
||||
width: ${ALERT_TEXT_W}
|
||||
height: ${ALERT_SUBTITLE_H}
|
||||
long_mode: WRAP
|
||||
text: ""
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A3A3C
|
||||
clickable: false
|
||||
- obj:
|
||||
id: alert_ok
|
||||
align: BOTTOM_RIGHT
|
||||
x: -${ALERT_BUTTON_INSET}
|
||||
y: -${ALERT_BUTTON_INSET}
|
||||
width: ${ALERT_BUTTON_W}
|
||||
height: ${ALERT_BUTTON_H}
|
||||
radius: 999
|
||||
bg_color: 0x1B1B1B
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pressed:
|
||||
bg_color: 0x3A3A3C
|
||||
bg_opa: COVER
|
||||
on_click:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "ok"
|
||||
widgets:
|
||||
- label:
|
||||
id: alert_ok_label
|
||||
align: CENTER
|
||||
text: "Oké"
|
||||
text_font: headline
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
pages:
|
||||
- id: home_page
|
||||
styles: style_bg_grad
|
||||
@@ -4924,6 +5085,9 @@ script:
|
||||
- delay: 150ms
|
||||
- lambda: guition::check_geometry(id(vacuum_overlay));
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- delay: 150ms
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||||
- logger.log: "UI_TEST COMPLETE"
|
||||
|
||||
@@ -5000,6 +5164,104 @@ script:
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- lvgl.widget.hide: vacuum_overlay
|
||||
|
||||
# ---------- ALERT (HA action show_alert) ----------
|
||||
# The screen wakes, keeps its normal brightness and shows the card until Oké or the
|
||||
# timeout (0: until Oké); the standby interval waits on alert_active. A new alert
|
||||
# replaces the current one; every ending reports itself as esphome.screen_alert.
|
||||
- id: alert_show
|
||||
mode: restart
|
||||
parameters:
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
color: string
|
||||
button_text: string
|
||||
timeout: int
|
||||
flash: bool
|
||||
then:
|
||||
- lambda: |-
|
||||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||||
id(alert_active) = true;
|
||||
id(alert_title_text) = alert.title;
|
||||
id(alert_timeout_seconds) = alert.timeout_seconds;
|
||||
id(alert_flash_requested) = alert.flash;
|
||||
lv_label_set_text(id(alert_icon), alert.icon.c_str());
|
||||
lv_label_set_text(id(alert_title), alert.title.c_str());
|
||||
lv_label_set_text(id(alert_subtitle), alert.subtitle.c_str());
|
||||
lv_label_set_text(id(alert_ok_label), alert.button.c_str());
|
||||
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(alert.color), LV_PART_MAIN);
|
||||
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
|
||||
- script.execute: wake_display
|
||||
- lvgl.widget.show: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_flash_requested);'
|
||||
then:
|
||||
- script.execute: alert_flash
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_timeout_seconds) > 0;'
|
||||
then:
|
||||
- script.execute:
|
||||
id: alert_timeout
|
||||
seconds: !lambda 'return id(alert_timeout_seconds);'
|
||||
|
||||
- id: alert_dismiss
|
||||
parameters:
|
||||
reason: string
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(alert_active);'
|
||||
then:
|
||||
- lambda: |-
|
||||
id(alert_active) = false;
|
||||
id(last_touch_ms) = millis();
|
||||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||||
- lvgl.widget.hide: alert_overlay
|
||||
- script.stop: alert_timeout
|
||||
- script.stop: alert_flash
|
||||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 80);'
|
||||
- script.execute:
|
||||
id: alert_report
|
||||
action: !lambda 'return reason;'
|
||||
|
||||
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
|
||||
- id: alert_flash
|
||||
mode: restart
|
||||
then:
|
||||
- repeat:
|
||||
count: 4
|
||||
then:
|
||||
- lambda: 'id(set_backlight).execute(0, 60);'
|
||||
- delay: 140ms
|
||||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 60);'
|
||||
- delay: 180ms
|
||||
|
||||
- id: alert_timeout
|
||||
mode: restart
|
||||
parameters:
|
||||
seconds: int
|
||||
then:
|
||||
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
|
||||
- script.execute:
|
||||
id: alert_dismiss
|
||||
reason: "timeout"
|
||||
|
||||
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
|
||||
- id: alert_report
|
||||
parameters:
|
||||
action: string
|
||||
then:
|
||||
- homeassistant.event:
|
||||
event: esphome.screen_alert
|
||||
data:
|
||||
action: !lambda 'return action;'
|
||||
title: !lambda 'return id(alert_title_text);'
|
||||
screen: !lambda 'return App.get_name();'
|
||||
|
||||
- id: wake_display
|
||||
mode: restart
|
||||
then:
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
## 0.2.36 (firmware 0.2.31)
|
||||
|
||||
- **Alert vanuit een automatisering.** Elk scherm heeft nu de actie `esphome.<scherm>_show_alert` met `title`, `subtitle`, `icon`, `color`, `button_text`, `timeout` en `flash`. De kaart ligt op LVGL's toplaag over alles heen (pagina's, kaarten, de standby-laag), wekt het scherm en houdt de backlight op de normale helderheid tot iemand op de knop tikt (`button_text`, standaard Oké), of tot `timeout` seconden (0 is tot de knop; de knop sluit ook een alert met timeout direct). `flash: true` laat de backlight vier keer knipperen bij binnenkomst. Iconen zijn de namen uit de tegelkiezer (ook als `mdi:naam`, of als hex-codepoint van een meegecompileerd glyph); onbekend wordt `alert-outline`. Kleuren zijn de pasteltinten van de tegels; leeg is de witte kaart. Een nieuwe alert vervangt de huidige. Oké, timeout, vervangen en `dismiss_alert` melden zich als event `esphome.screen_alert` met `action`, `title` en `screen`.
|
||||
- De actie `esphome.<scherm>_dismiss_alert` haalt een alert op afstand weg, bijvoorbeeld als de deur al open is.
|
||||
- De iconenlijst schrijft nu ook `components/smart_display/tile_icon_names.h` (naam naar codepoint) via `tools/generate_icons.py`.
|
||||
- Firmware 0.2.31 voor beide borden; de app biedt de update aan. Alleen de doelversie verandert in de app.
|
||||
|
||||
## 0.2.35 (firmware 0.2.30)
|
||||
|
||||
- **Snelle randveeg pakt nu ook in één keer (Guition).** Op 0.2.29 wisselde de veeg wel, maar ongeveer de helft van de snelle vegen eindigde met `randveeg niet gevuurd: 5 px naar binnen` terwijl de vinger duidelijk verder ging; elke aanraking duurde in de firmware bijna exact 160 ms en leverde maar één meting op. Een druk in de lege rand landde op de achtergrondcontainer en de pagina, en het thema geeft elk object een ingedrukte-stijl (45% dekking): bijna het hele scherm werd bij indrukken en loslaten opnieuw getekend, wat de touch-polling precies zo lang blokkeerde als een flick duurt. De pagina en de tegelcontainer nemen nu geen drukken meer aan (`LV_OBJ_FLAG_CLICKABLE` uit), dus geen hertekening en de metingen komen elke 20 ms door.
|
||||
|
||||
@@ -16,7 +16,7 @@ WEEKDAYS = ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo']
|
||||
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
|
||||
REFS = {'cyd': 'main', 'guition': 'main'}
|
||||
# Firmware shipped with this app release; screens below it get an update offer.
|
||||
FIRMWARE_VERSION = '0.2.30'
|
||||
FIRMWARE_VERSION = '0.2.31'
|
||||
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split())
|
||||
# Attributes whose boolean value the screen needs; every other bool stays behind.
|
||||
BOOL_ATTRS = frozenset(['is_volume_muted'])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ESP Screen Manager
|
||||
version: "0.2.35"
|
||||
version: "0.2.36"
|
||||
slug: esp_screen_manager
|
||||
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""The alert a Home Assistant action puts over the whole screen: both board profiles and both remote packages."""
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
||||
import tile_icons # noqa: E402
|
||||
|
||||
PROFILES = {'cyd': 'home-like-2432s028.yaml', 'guition': 'guition-4848s040.yaml'}
|
||||
PACKAGES = {'cyd': 'packages/cyd.yaml', 'guition': 'packages/guition.yaml'}
|
||||
FIELDS = [('title', 'string'), ('subtitle', 'string'), ('icon', 'string'), ('color', 'string'), ('button_text', 'string'), ('timeout', 'int'), ('flash', 'bool')]
|
||||
|
||||
def section(text, start, end):
|
||||
return text.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
def script(text, name):
|
||||
"""One script block without its trailing comment lines, which introduce the next script."""
|
||||
return re.sub(r'^\s*#.*\n?', '', section(text, f' - id: {name}\n', '\n - id: '), flags=re.M)
|
||||
|
||||
class AlertTests(unittest.TestCase):
|
||||
def sources(self):
|
||||
for name in list(PROFILES.values()) + list(PACKAGES.values()):
|
||||
yield name, (ROOT / name).read_text()
|
||||
|
||||
def test_action_takes_the_six_fields_and_hands_them_to_one_script(self):
|
||||
for name, text in self.sources():
|
||||
block = section(text, ' - action: show_alert\n', ' - action: dismiss_alert\n')
|
||||
self.assertEqual(re.findall(r'^ (\w+): (\w+)$', block, re.M), FIELDS, name)
|
||||
self.assertIn('id: alert_show', block, name)
|
||||
for field, _ in FIELDS:
|
||||
self.assertIn(f"{field}: !lambda 'return {field};'", block, name)
|
||||
remote = text.split(' - action: dismiss_alert\n', 1)[1][:200]
|
||||
self.assertIn('id: alert_dismiss', remote, name)
|
||||
self.assertIn('reason: "remote"', remote, name)
|
||||
|
||||
def test_overlay_lives_on_the_top_layer_above_every_page(self):
|
||||
for name, text in self.sources():
|
||||
lvgl = section(text, '\nlvgl:\n', '\nscript:\n')
|
||||
self.assertLess(lvgl.index(' top_layer:\n'), lvgl.index(' pages:\n'), name)
|
||||
top = section(lvgl, ' top_layer:\n', ' pages:\n')
|
||||
for widget in ('alert_overlay', 'alert_card', 'alert_icon', 'alert_title', 'alert_subtitle', 'alert_ok', 'alert_ok_label'):
|
||||
self.assertIn(f'id: {widget}\n', top, (name, widget))
|
||||
self.assertIn('hidden: true', section(top, 'id: alert_overlay\n', 'widgets:'), name)
|
||||
self.assertIn(f'text: "\\U000{tile_icons.GLYPHS["alert-outline"]}"', top, name)
|
||||
self.assertIn('long_mode: DOT', section(top, 'id: alert_title\n', 'id: alert_subtitle'), name)
|
||||
self.assertIn('long_mode: WRAP', section(top, 'id: alert_subtitle\n', 'id: alert_ok'), name)
|
||||
ok = section(top, 'id: alert_ok\n', 'widgets:')
|
||||
self.assertIn('lv_label_set_text(id(alert_ok_label), alert.button.c_str());', script(text, 'alert_show'), name)
|
||||
self.assertIn('cyd::touch_guard.accept(millis(), 13)', ok, name)
|
||||
self.assertLess(ok.index('touch_guard.accept'), ok.index('id: alert_dismiss'), name)
|
||||
self.assertIn('reason: "ok"', ok, name)
|
||||
|
||||
def test_standby_waits_and_no_other_path_closes_the_card(self):
|
||||
for name, text in self.sources():
|
||||
guard = re.search(r'if \(id\(display_dimmed\) \|\| id\(touch_down\) \|\| id\(calibration_active\)([^\n]*)\) return false;', text)
|
||||
self.assertIsNotNone(guard, name)
|
||||
self.assertIn('|| id(alert_active)', guard[1], name)
|
||||
for other in ('close_cards', 'dim_display', 'wake_display', 'apply_screen_settings'):
|
||||
self.assertNotIn('alert', script(text, other), (name, other))
|
||||
dismiss = script(text, 'alert_dismiss')
|
||||
self.assertIn('id(alert_active) = false;', dismiss, name)
|
||||
self.assertIn('id(last_touch_ms) = millis();', dismiss, name)
|
||||
for stopped in ('alert_timeout', 'alert_flash'):
|
||||
self.assertIn(f'script.stop: {stopped}', dismiss, name)
|
||||
self.assertIn('screen_settings::current.brightness', dismiss, name)
|
||||
|
||||
def test_every_ending_reports_to_home_assistant_but_the_self_test_does_not(self):
|
||||
for name, text in self.sources():
|
||||
report = script(text, 'alert_report')
|
||||
self.assertIn('event: esphome.screen_alert', report, name)
|
||||
for key in ('action', 'title', 'screen'):
|
||||
self.assertRegex(report, rf'(?m)^\s+{key}: !lambda', name)
|
||||
self.assertIn('id: alert_report', script(text, 'alert_dismiss'), name)
|
||||
self.assertIn('reason: "timeout"', script(text, 'alert_timeout'), name)
|
||||
show = script(text, 'alert_show')
|
||||
self.assertIn('id(alert_report).execute("replaced")', show, name)
|
||||
self.assertIn('screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX})', show, name)
|
||||
self.assertLess(show.index('script.execute: wake_display'), show.index('lvgl.widget.show: alert_overlay'), name)
|
||||
self.assertLess(show.index('lvgl.widget.show: alert_overlay'), show.index('script.execute: alert_flash'), name)
|
||||
self_test = section(text, ' - id: ui_self_test\n', ' - id: show_tile_page')
|
||||
self.assertIn('lvgl.widget.show: alert_overlay', self_test, name)
|
||||
self.assertIn('lvgl.widget.hide: alert_overlay', self_test, name)
|
||||
for forbidden in ('alert_show', 'alert_dismiss', 'homeassistant.event'):
|
||||
self.assertNotIn(forbidden, self_test, name)
|
||||
|
||||
def test_flash_blinks_four_times_and_the_card_fits_the_screen(self):
|
||||
for name, text in self.sources():
|
||||
flash = script(text, 'alert_flash')
|
||||
self.assertIn('count: 4', flash, name)
|
||||
self.assertEqual(flash.count('delay:'), 2, name)
|
||||
v = lambda key: int(re.search(rf'^ {key}: "(-?\d+)"', text, re.M)[1])
|
||||
self.assertLessEqual(v('ALERT_CARD_W'), v('DISPLAY_W') - 16, name)
|
||||
self.assertLessEqual(v('ALERT_CARD_H'), v('DISPLAY_H') - 16, name)
|
||||
self.assertLessEqual(v('ALERT_TEXT_X') + v('ALERT_TEXT_W'), v('ALERT_CARD_W') - v('ALERT_ICON_X'), name)
|
||||
self.assertLessEqual(v('ALERT_SUBTITLE_Y') + v('ALERT_SUBTITLE_H'),
|
||||
v('ALERT_CARD_H') - v('ALERT_BUTTON_INSET') - v('ALERT_BUTTON_H'), name)
|
||||
self.assertGreater(v('ALERT_TITLE_MAX'), 0, name)
|
||||
self.assertGreater(v('ALERT_BUTTON_MAX'), 0, name)
|
||||
self.assertGreater(v('ALERT_SUBTITLE_MAX'), v('ALERT_TITLE_MAX'), name)
|
||||
|
||||
def test_helper_header_is_included_locally_and_in_the_remote_package(self):
|
||||
for board, name in PROFILES.items():
|
||||
self.assertIn(' - alert_overlay.h\n', (ROOT / name).read_text(), name)
|
||||
self.assertIn(' - <esphome/components/smart_display/alert_overlay.h>\n', (ROOT / PACKAGES[board]).read_text(), board)
|
||||
self.assertIn('components/smart_display/alert_overlay.h', (ROOT / 'alert_overlay.h').read_text())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "components/smart_display/alert_overlay.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
int main() {
|
||||
using screen_alert::make;
|
||||
const auto alert = make("Iemand belt aan", "Deur 3, achterkant", "doorbell", "orange", "Ik kom", 60, true, 48, 160, 12);
|
||||
assert(alert.title == "Iemand belt aan" && alert.subtitle == "Deur 3, achterkant" && alert.button == "Ik kom");
|
||||
assert(alert.icon == tile_icon::utf8(0xF12E6) && alert.color == 0xFFE1C6 && alert.timeout_seconds == 60 && alert.flash);
|
||||
// Icon: a name from the set, with mdi: prefix, spaces or capitals, or the hex codepoint of a carried glyph.
|
||||
for (const char *icon : {"doorbell", "mdi:doorbell", " Doorbell ", "F12E6", "f12e6", "mdi:F12E6"})
|
||||
assert(screen_alert::icon_codepoint(icon) == 0xF12E6);
|
||||
// Unknown names, a codepoint the fonts lack and junk draw the warning triangle.
|
||||
for (const char *icon : {"", "unknown", "mdi:", "F0000", "F12E", "\xF0\x9F\x94\x94", "mdi:not-in-the-set"})
|
||||
assert(screen_alert::icon_codepoint(icon) == 0xF002A);
|
||||
// Colour: palette names in any case; empty, none and anything unknown keep the white card.
|
||||
assert(make("x", "", "", "Red", "", 0, false, 48, 160, 12).color == 0xFADADD);
|
||||
assert(make("x", "", "", " mint ", "", 0, false, 48, 160, 12).color == 0xD5F0EA);
|
||||
for (const char *color : {"", "none", "auto", "#ff0000", "0xFF0000"})
|
||||
assert(make("x", "", "", color, "", 0, false, 48, 160, 12).color == 0xFFFFFF);
|
||||
// Timeout: 0 and negatives wait for Oké; a day is the ceiling.
|
||||
assert(make("x", "", "", "", "", 0, false, 48, 160, 12).timeout_seconds == 0);
|
||||
assert(make("x", "", "", "", "", -5, false, 48, 160, 12).timeout_seconds == 0);
|
||||
assert(make("x", "", "", "", "", 999999, false, 48, 160, 12).timeout_seconds == 86400);
|
||||
assert(!make("x", "", "", "", "", 0, false, 48, 160, 12).flash);
|
||||
// Title: trimmed, never empty, cut on a UTF-8 boundary; the subtitle keeps its line breaks.
|
||||
assert(make(" ", "", "", "", "", 0, false, 48, 160, 12).title == "Melding");
|
||||
assert(make(" Hoi \n", "", "", "", "", 0, false, 48, 160, 12).title == "Hoi");
|
||||
assert(make("\xC3\xA9\xC3\xA9\xC3\xA9", "", "", "", "", 0, false, 3, 160, 12).title == "\xC3\xA9");
|
||||
assert(make("x", "regel 1\nregel 2", "", "", "", 0, false, 48, 160, 12).subtitle == "regel 1\nregel 2");
|
||||
assert(make("x", std::string(300, 'a'), "", "", "", 0, false, 48, 160, 12).subtitle.size() == 160);
|
||||
assert(make(std::string(100, 'b'), "", "", "", "", 0, false, 48, 160, 12).title.size() == 48);
|
||||
// Button: trimmed and capped; empty falls back to Oké.
|
||||
assert(make("x", "", "", "", "", 0, false, 48, 160, 12).button == "Ok\xC3\xA9");
|
||||
assert(make("x", "", "", "", " Open ", 0, false, 48, 160, 12).button == "Open");
|
||||
assert(make("x", "", "", "", std::string(40, 'c'), 0, false, 48, 160, 12).button.size() == 12);
|
||||
// The generated table carries every font glyph by name.
|
||||
assert(tile_icon::named("lightbulb") == 0xF0335 && tile_icon::named("alert-outline") == 0xF002A);
|
||||
assert(tile_icon::named("nope") == 0 && tile_icon::named("") == 0);
|
||||
assert(tile_icon::carried(0xF0335) && tile_icon::carried(0xF002A) && !tile_icon::carried(0xF0000) && !tile_icon::carried(0));
|
||||
assert(tile_icon::NAME_COUNT > 150);
|
||||
std::puts("test_alert_overlay: PASS");
|
||||
}
|
||||
@@ -30,6 +30,13 @@ class IconSetTests(unittest.TestCase):
|
||||
self.assertNotIn('MDI_GLYPH', text, name)
|
||||
self.assertTrue((ROOT / 'screen_manager/app/static/tile-icons.woff').exists())
|
||||
|
||||
def test_firmware_name_table_mirrors_the_set(self):
|
||||
header = (ROOT / 'components/smart_display/tile_icon_names.h').read_text()
|
||||
self.assertIn('GENERATED by tools/generate_icons.py', header)
|
||||
entries = re.findall(r'^ \{"([a-z0-9-]+)", 0x(F[0-9A-F]{4})\},$', header, re.M)
|
||||
self.assertEqual(dict(entries), tile_icons.GLYPHS)
|
||||
self.assertEqual(len(entries), len(tile_icons.GLYPHS))
|
||||
|
||||
def test_every_glyph_the_firmware_draws_is_in_the_set(self):
|
||||
sources = [HEADER] + [(ROOT / name).read_text().split('glyphs: &tile_icons', 1)[0] for name in ('home-like-2432s028.yaml', 'guition-4848s040.yaml')]
|
||||
used = {code.upper() for source in sources for code in re.findall(r'\\U000(F[0-9A-Fa-f]{4})', source)}
|
||||
|
||||
@@ -23,6 +23,7 @@ import tile_icons # noqa: E402
|
||||
TTF = ROOT / 'fonts/materialdesignicons-webfont.ttf'
|
||||
PROFILES = [ROOT / 'home-like-2432s028.yaml', ROOT / 'guition-4848s040.yaml']
|
||||
WEB_FONT = ROOT / 'screen_manager/app/static/tile-icons.woff'
|
||||
NAME_TABLE = ROOT / 'components/smart_display/tile_icon_names.h'
|
||||
FONT_BLOCK = re.compile(r"( - file: 'fonts/materialdesignicons-webfont\.ttf'\n id: \w+\n size: \d+\n) glyphs:.*\n(?: .*\n)*")
|
||||
|
||||
def verify():
|
||||
@@ -56,6 +57,30 @@ def profile(text):
|
||||
last = block.end()
|
||||
return ''.join(out) + text[last:]
|
||||
|
||||
def name_table():
|
||||
"""Name -> codepoint for the firmware, so a Home Assistant action can say icon: doorbell."""
|
||||
lines = ['#pragma once',
|
||||
'// GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py; do not edit.',
|
||||
'#include <cstdint>', '#include <string>', '', 'namespace tile_icon {',
|
||||
'struct Named { const char *name; uint32_t codepoint; };',
|
||||
'// Every glyph the three icon fonts of both boards carry, by Material Design Icons name.',
|
||||
'inline constexpr Named NAMES[] = {']
|
||||
lines += [f' {{"{name}", 0x{code}}},' for name, code in tile_icons.GLYPHS.items()]
|
||||
lines += ['};',
|
||||
'inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);',
|
||||
'// Codepoint of a name the fonts carry; 0 for any other name.',
|
||||
'inline uint32_t named(const std::string &name) {',
|
||||
' for (const auto &entry : NAMES) if (name == entry.name) return entry.codepoint;',
|
||||
' return 0;',
|
||||
'}',
|
||||
'// True when the fonts carry this codepoint, so a label can draw it.',
|
||||
'inline bool carried(uint32_t codepoint) {',
|
||||
' for (const auto &entry : NAMES) if (entry.codepoint == codepoint) return true;',
|
||||
' return false;',
|
||||
'}',
|
||||
'} // namespace tile_icon']
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
def web_font():
|
||||
font = TTFont(TTF)
|
||||
options = subset.Options()
|
||||
@@ -90,11 +115,13 @@ def main():
|
||||
else:
|
||||
path.write_text(expected)
|
||||
if args.check:
|
||||
if not NAME_TABLE.exists() or NAME_TABLE.read_text() != name_table(): raise SystemExit(f'Outdated: {NAME_TABLE.name}')
|
||||
# Compare glyph coverage, not bytes: font output differs between fontTools versions.
|
||||
wanted = {int(code, 16) for code in tile_icons.GLYPHS.values()}
|
||||
if not WEB_FONT.exists() or set(TTFont(WEB_FONT).getBestCmap()) != wanted:
|
||||
raise SystemExit(f'Outdated: {WEB_FONT.name}')
|
||||
else:
|
||||
NAME_TABLE.write_text(name_table())
|
||||
WEB_FONT.write_bytes(web_font())
|
||||
print(f'{len(tile_icons.ICONS)} pickable icons, {len(tile_icons.GLYPHS)} glyphs: ' + ('verified' if args.check else 'generated'))
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def generate(board):
|
||||
s = re.sub(r'^ota:\n.*?(?=^\w)', '', s, flags=re.M | re.S)
|
||||
s = re.sub(r'^wifi:\n.*?(?=^\w)', '', s, flags=re.M | re.S)
|
||||
# Referenced files are fetched as an external component or over HTTPS.
|
||||
s = re.sub(r'^ - (cyd_ui.h|light_controls.h|guition_diagnostics.h|backlight_fade.h)$', r' - <esphome/components/smart_display/\1>', s, flags=re.M)
|
||||
s = re.sub(r'^ - (cyd_ui.h|light_controls.h|guition_diagnostics.h|backlight_fade.h|alert_overlay.h)$', r' - <esphome/components/smart_display/\1>', s, flags=re.M)
|
||||
components = '[xpt2046, smart_display]' if board == 'cyd' else '[smart_display]'
|
||||
external = f'''external_components:
|
||||
- source:
|
||||
|
||||
Reference in New Issue
Block a user