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:
@@ -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
|
||||
Reference in New Issue
Block a user