Add persistent per-screen brightness, standby and clock settings

This commit is contained in:
Max Gramser
2026-09-12 13:31:18 +02:00
parent 747ec4cceb
commit 917b3d6d09
18 changed files with 634 additions and 240 deletions
+36 -1
View File
@@ -1,5 +1,7 @@
#pragma once
#include "runtime_model.h"
#include "screen_settings.h"
#include "esphome/core/preferences.h"
#include "cyd_ui.h"
#include "light_controls.h"
#include "esphome/components/json/json_util.h"
@@ -14,7 +16,30 @@ inline bool enabled = false;
inline Model model;
inline int active_index = -1;
inline uint32_t last_received = 0;
inline std::function<void()> layout_changed, refresh, dismiss;
inline std::function<void()> layout_changed, refresh, dismiss, settings_changed;
inline esphome::ESPPreferenceObject settings_preference;
inline void load_settings() {
settings_preference = esphome::global_preferences->make_preference<screen_settings::Settings>(0x53435231);
screen_settings::Settings saved;
if (settings_preference.load(&saved) && saved.valid()) screen_settings::current = saved;
}
inline bool parse_settings(JsonObject obj, screen_settings::Settings &s) {
// Require the complete known schema; validate before touching any runtime state.
if (obj.size() != 11) return false;
const char *flags[] = {"standby_enabled", "night_enabled", "show_clock", "clock_24h", "home_on_standby"};
int32_t *flag_values[] = {&s.standby_enabled, &s.night_enabled, &s.show_clock, &s.clock_24h, &s.home_on_standby};
for (int i = 0; i < 5; ++i) {
if (!obj[flags[i]].is<bool>()) return false;
*flag_values[i] = obj[flags[i]].as<bool>();
}
const char *numbers[] = {"standby_seconds", "brightness", "standby_brightness", "night_start", "night_end", "night_brightness"};
int32_t *values[] = {&s.standby_seconds, &s.brightness, &s.standby_brightness, &s.night_start, &s.night_end, &s.night_brightness};
for (int i = 0; i < 6; ++i) {
if (!obj[numbers[i]].is<int>() || obj[numbers[i]].is<bool>()) return false;
*values[i] = obj[numbers[i]].as<int>();
}
return s.valid();
}
inline std::function<void(Tile &)> detail, detail_update;
struct Widgets { lv_obj_t *tile{}, *title{}, *value{}, *circle{}, *icon{}; size_t index{}; int cached_active = -1; };
inline std::array<Widgets, MAX_TILES> widgets;
@@ -48,6 +73,11 @@ inline std::string receive(const std::string &payload) {
auto op = string(root["op"]);
if (op == "layout") {
if (!root["entities"].is<JsonArray>() || !root["title"].is<const char *>()) return false;
auto settings = screen_settings::current;
if (!root["settings"].isNull() && (!root["settings"].is<JsonObject>() ||
!parse_settings(root["settings"].as<JsonObject>(), settings))) {
result = "Fout: scherminstellingen"; return false;
}
std::vector<std::string> entities;
for (JsonVariant entity : root["entities"].as<JsonArray>()) {
if (!entity.is<const char *>() || entities.size() == MAX_TILES) return false;
@@ -55,6 +85,11 @@ inline std::string receive(const std::string &payload) {
}
bool changed = false;
if (!model.set_layout(entities, string(root["title"], 96), changed)) return false;
if (!(settings == screen_settings::current)) {
screen_settings::current = settings;
settings_preference.save(&settings); // ESPHome batches flash writes; no write on keepalive.
if (settings_changed) settings_changed();
}
if (changed) { active_index = -1; for (auto &w : widgets) w.cached_active = -1; if (dismiss) dismiss(); }
last_received = esphome::millis();
if (layout_changed) layout_changed();
@@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
namespace screen_settings {
// Stable preference format. Change its version/key only with a migration.
struct Settings {
uint32_t version = 1;
int32_t standby_enabled = 1, standby_seconds = 600, brightness = 100;
int32_t standby_brightness = 20, night_enabled = 1, night_start = 1320;
int32_t night_end = 420, night_brightness = 10, show_clock = 1;
int32_t clock_24h = 1, home_on_standby = 0;
bool valid() const {
return version == 1 && (standby_enabled == 0 || standby_enabled == 1) &&
standby_seconds >= 60 && standby_seconds <= 86400 &&
brightness >= 5 && brightness <= 100 && standby_brightness >= 0 &&
standby_brightness <= brightness && (night_enabled == 0 || night_enabled == 1) &&
night_start >= 0 && night_start < 1440 && night_end >= 0 && night_end < 1440 &&
night_brightness >= 0 && night_brightness <= brightness &&
(show_clock == 0 || show_clock == 1) && (clock_24h == 0 || clock_24h == 1) &&
(home_on_standby == 0 || home_on_standby == 1);
}
bool operator==(const Settings &b) const {
return version == b.version && standby_enabled == b.standby_enabled &&
standby_seconds == b.standby_seconds && brightness == b.brightness &&
standby_brightness == b.standby_brightness && night_enabled == b.night_enabled &&
night_start == b.night_start && night_end == b.night_end &&
night_brightness == b.night_brightness && show_clock == b.show_clock &&
clock_24h == b.clock_24h && home_on_standby == b.home_on_standby;
}
bool night(int minute) const {
if (!night_enabled || minute < 0 || minute >= 1440 || night_start == night_end) return false;
return night_start < night_end ? minute >= night_start && minute < night_end
: minute >= night_start || minute < night_end;
}
int dim_level(int minute) const { return night(minute) ? night_brightness : standby_brightness; }
};
inline Settings current;
}
+42
View File
@@ -162,3 +162,45 @@ een los openbaar portal.
Gebruikte HA-mechanismen: [Ingress](https://developers.home-assistant.io/docs/apps/presentation/),
[interne HA-API](https://developers.home-assistant.io/docs/apps/communication/) en
[ESPHome-pakketten](https://esphome.io/components/packages/).
## Scherminstellingen aanpassen (vanaf 0.1.2)
Update ESP Screen Manager naar 0.1.2 en installeer één keer de nieuwe firmware
via je **bestaande** ESPHome-apparaat → Install → Wirelessly. Behoud de eigen YAML
met wifi en sleutels. Open daarna het scherm in ESP Screen Manager en klap
**Scherminstellingen** open. Klik na aanpassen op **Opslaan & naar scherm**.
Hierna vragen wijzigingen aan deze instellingen geen nieuwe firmwareflash.
| Instelling | Mogelijkheden | Standaard |
|---|---|---|
| Automatisch standby | Aan/uit | Aan |
| Standby na | 1–1440 minuten na de laatste aanraking | 10 minuten |
| Helderheid normaal | 5–100% | 100% |
| Helderheid standby | 0–100%, maximaal normale helderheid | 20% |
| Nachtstand | Aan/uit; geldt tijdens standby | Aan |
| Begin/einde nacht | Uur en minuut, ook over middernacht | 22:00–07:00 |
| Helderheid nacht | 0–100%, maximaal normale helderheid | 10% |
| Klok tonen | Aan/uit | Aan |
| Tijdnotatie | 24 of 12 uur, zonder AM/PM | 24 uur |
| Terug naar pagina 1 | Bij standby; anders huidige pagina onthouden | Uit |
Nachturen gebruiken de tijdzone van het ESPHome-apparaat en de tijd uit HA.
Zonder geldige tijd gebruikt het scherm de gewone standbyhelderheid; gelijke
begin- en eindtijd schakelen het nachtvenster uit. Op 0% gaat alleen de
achtergrondverlichting uit: dit is geen deep sleep en geen schermbeveiliging.
De eerste tik wekt het scherm zonder een apparaat te bedienen.
De add-on bewaart alles per scherm in zijn permanente gegevens. Het scherm bewaart
de laatst ontvangen instellingen ook in preferences; ESPHome bundelt die
schrijfacties (normaal maximaal een minuut). Trek daarom niet direct na opslaan
de voeding los. Bestaande CYD-kalibratie, tegels, API- en OTA-sleutels blijven staan.
Een offline scherm krijgt wijzigingen zodra het terugkomt. Gewone HA-statusupdates
wekken het scherm niet en veranderen de standbytimer niet.
Bij oudere firmware toont de beheerpagina dat eerst een update nodig is.
De tegels blijven bruikbaar. Handmatige YAML-profielen blijven hun substitutions
gebruiken; voor beheer via deze app gebruik je het Easy Setup-pakket.
De gemelde Guition-paneelstrepen worden apart onderzocht. Deze instellingenupdate
wijzigt geen displaytimings of driver en is geen bewezen oplossing voor die strepen.
+14
View File
@@ -56,3 +56,17 @@ Een tijdelijke ontwikkelserver ondersteunt `SCREEN_DEV=1`, `HA_API` (eindigt op
`/api`), `HA_TOKEN_FILE` en `SCREEN_DATA`. Hij bindt alleen op localhost. Zet een
token nooit in broncode, URL's of Git. Productie gebruikt Supervisor en accepteert
alleen het Ingress-proxyadres; er is geen extra publieke poort.
### Instellingencompatibiliteit 0.1.2
`settings` is een optioneel object binnen een schermindeling en binnen het bestaande
`v: 1, op: layout`-bericht. Een ontbrekend object behoudt eerder gedrag. Oude
firmware negeert dit extra veld en blijft tegels ontvangen. De beheerpagina meldt
dat nieuwe firmware nodig is. Een oude browser die alleen tegels opslaat, wist
opgeslagen instellingen niet. Onbekende instellingen worden geweigerd.
Firmware gebruikt een afzonderlijke preferences-key `0x53435231` met een vaste
versie-1 structuur (elf int32-velden en een uint32-versie). Wijzig key, structuur
of versie niet zonder migratie. Alleen gewijzigde instellingen worden opgeslagen;
periodieke herhaling veroorzaakt geen flashwrites en geen nieuwe idle-timer.
+21
View File
@@ -0,0 +1,21 @@
# Scherminstellingen 0.1.2 — 12 september 2026
- 33 Python-tests geslaagd: invoergrenzen, oude indelingen, behoud van instellingen
bij herstart en opslaan vanuit een oudere UI, protocol en HTTP/CSRF.
- Zeven native C++-tests geslaagd, inclusief grenzen, nachttijden over middernacht,
ontbreken van tijdsync en ongeldige opgeslagen settings.
- Easy Setup-firmware voor CYD en Guition gebouwd met ESPHome 2026.6.2.
- Beide handmatige YAML-profielen gevalideerd en volledig gebouwd.
- Browser: standby 20 minuten opslaan en herladen; helderheid naar minimum zetten
verlaagt hogere dimniveaus mee; waarden blijven behouden na herladen.
- Guition via bestaande versleutelde OTA bijgewerkt naar 0.1.2.
- Op het echte scherm: 55% helderheid direct toegepast; eigenaar bevestigt fysiek
dimmen. Standby na 60 seconden ondanks herhaalde indelingsberichten. Ongeldige
helderheid 0 geweigerd zonder de geldige instellingen te veranderen.
- Guition herstart: 55%, 60 seconden en verborgen klok uit preferences teruggelezen.
Daarna oorspronkelijke instellingen teruggezet (100%, 600 seconden).
CYD was niet aangesloten; de fysieke instellingenacceptatie betreft alleen Guition.
Dit is geen lange stabiliteitstest en geen oplossing voor de bestaande paneelstrepen.
De RGB-driver is bytegelijk aan de oorspronkelijke ESPHome-driver; experimentele
wijzigingen aan timing, spiegeling en reset zijn niet onderdeel van deze release.
+52 -59
View File
@@ -582,7 +582,7 @@ esphome:
min_version: 2026.6.2
project:
name: maxgramser.esp_screen
version: "0.1.0"
version: "0.1.2"
platformio_options:
build_flags:
- -DLV_USE_SNAPSHOT=1
@@ -603,8 +603,19 @@ esphome:
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
- lambda: |-
auto &settings = screen_settings::current;
settings.standby_enabled = std::string("${AUTO_DIM}") == "true";
settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}");
settings.brightness = atoi("${MAX_BRIGHTNESS}");
settings.standby_brightness = atoi("${DIM_BRIGHTNESS}");
settings.night_enabled = std::string("${NIGHT_MODE}") == "true";
settings.night_start = atoi("${NIGHT_START_HOUR}") * 60;
settings.night_end = atoi("${NIGHT_END_HOUR}") * 60;
settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}");
runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true";
if (!runtime_tiles::enabled) return;
runtime_tiles::load_settings();
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -678,7 +689,7 @@ esphome:
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
- light.turn_on:
id: back_light
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- script.execute: show_tile_page
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- if:
@@ -748,6 +759,12 @@ api:
- action: ui_state
then:
- lambda: |-
const auto &cfg = screen_settings::current;
ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u dimmed=%d",
cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness,
cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness,
cfg.show_clock, cfg.clock_24h, cfg.home_on_standby,
(unsigned)(millis() - id(last_touch_ms)), id(display_dimmed));
lv_area_t nav, grid;
lv_obj_update_layout(id(home_page)->obj);
lv_obj_get_coords(id(page_next), &nav);
@@ -788,9 +805,9 @@ interval:
- if:
condition:
lambda: |-
if (std::string("${AUTO_DIM}") != "true") return false;
if (!screen_settings::current.standby_enabled) return false;
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL);
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
then:
- script.execute: dim_display
@@ -801,33 +818,15 @@ time:
update_interval: 60s
on_time_sync:
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- seconds: 0
minutes: 0
hours: ${NIGHT_START_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
- seconds: 0
minutes: 0
hours: ${NIGHT_END_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
globals:
- id: rendered_frames
type: uint32_t
@@ -1312,7 +1311,7 @@ text_sensor:
- platform: template
name: "Schermfirmware"
entity_category: diagnostic
lambda: 'return {"0.1.0"};'
lambda: 'return {"0.1.2"};'
update_interval: 60s
- platform: template
id: smartdisplay_action
@@ -4448,6 +4447,25 @@ script:
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
}
# Runtime changes do not count as touches and do not reset the idle timer.
- id: apply_screen_settings
mode: restart
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
id(back_light).turn_on().set_transition_length(300).set_brightness(level / 100.0f).perform();
if (id(display_dimmed)) lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
if (settings.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: wake_display
mode: restart
then:
@@ -4459,7 +4477,7 @@ script:
# Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag
# als je het scherm aanraakt en op licht staat te wachten.
transition_length: 80ms
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
@@ -4471,41 +4489,14 @@ script:
then:
- if:
condition:
lambda: 'return std::string("${AUTO_DIM}") == "true";'
lambda: 'return screen_settings::current.standby_enabled;'
then:
- lambda: |-
int dim_value = atoi("${DIM_BRIGHTNESS}");
int max_value = atoi("${MAX_BRIGHTNESS}");
if (std::string("${NIGHT_MODE}") == "true") {
auto now = id(homeassistant_time).now();
// Vóór de eerste tijd-sync (zie time: hierboven) is now()
// epoch 0 — dan geen nachtmodus-beslissing nemen op basis
// van een ongeldig uur, gewoon normale DIM_BRIGHTNESS.
if (now.is_valid()) {
int hour = now.hour;
int start_hour = atoi("${NIGHT_START_HOUR}");
int end_hour = atoi("${NIGHT_END_HOUR}");
bool is_night = false;
if (start_hour < end_hour) {
is_night = (hour >= start_hour && hour < end_hour);
} else {
is_night = (hour >= start_hour || hour < end_hour);
}
if (is_night) {
dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}");
}
}
}
// Rustig omlaag faden is prettig; omhoog moet juist snel.
id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform();
// Always mark as dimmed to stop the interval from re-firing
id(display_dimmed) = true;
// Only show the touch-blocking overlay when brightness actually changes
if (dim_value < max_value) {
lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
id(apply_screen_settings).execute();
if (screen_settings::current.home_on_standby) {
id(tile_page) = 0;
id(show_tile_page).execute();
}
# Terug naar het basisscherm bij standby. Bewust via de ESPHome
@@ -5394,7 +5385,9 @@ script:
if (runtime_tiles::enabled) {
runtime_tiles::render(id(lbl_room));
auto now = id(homeassistant_time).now();
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime("%H:%M"));
if (screen_settings::current.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M"));
return;
}
auto set_lbl = [](lv_obj_t* lbl, const std::string& s) {
+52 -59
View File
@@ -777,7 +777,7 @@ esphome:
min_version: 2026.6.2
project:
name: maxgramser.esp_screen
version: "0.1.0"
version: "0.1.2"
includes:
- cyd_ui.h
- light_controls.h
@@ -796,8 +796,19 @@ esphome:
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
- lambda: |-
auto &settings = screen_settings::current;
settings.standby_enabled = std::string("${AUTO_DIM}") == "true";
settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}");
settings.brightness = atoi("${MAX_BRIGHTNESS}");
settings.standby_brightness = atoi("${DIM_BRIGHTNESS}");
settings.night_enabled = std::string("${NIGHT_MODE}") == "true";
settings.night_start = atoi("${NIGHT_START_HOUR}") * 60;
settings.night_end = atoi("${NIGHT_END_HOUR}") * 60;
settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}");
runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true";
if (!runtime_tiles::enabled) return;
runtime_tiles::load_settings();
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -879,7 +890,7 @@ esphome:
${TOUCH_CAL_X_MIN}, ${TOUCH_CAL_X_MAX}, ${TOUCH_CAL_Y_MIN}, ${TOUCH_CAL_Y_MAX});
- light.turn_on:
id: back_light
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- script.execute: show_tile_page
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- if:
@@ -940,6 +951,12 @@ api:
- action: ui_state
then:
- lambda: |-
const auto &cfg = screen_settings::current;
ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u dimmed=%d",
cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness,
cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness,
cfg.show_clock, cfg.clock_24h, cfg.home_on_standby,
(unsigned)(millis() - id(last_touch_ms)), id(display_dimmed));
lv_area_t nav, grid;
lv_obj_update_layout(id(home_page)->obj);
lv_obj_get_coords(id(page_next), &nav);
@@ -980,9 +997,9 @@ interval:
- if:
condition:
lambda: |-
if (std::string("${AUTO_DIM}") != "true") return false;
if (!screen_settings::current.standby_enabled) return false;
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL);
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
then:
- script.execute: dim_display
@@ -993,33 +1010,15 @@ time:
update_interval: 60s
on_time_sync:
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- seconds: 0
minutes: 0
hours: ${NIGHT_START_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
- seconds: 0
minutes: 0
hours: ${NIGHT_END_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
globals:
- id: rendered_frames
type: uint32_t
@@ -1499,7 +1498,7 @@ text_sensor:
- platform: template
name: "Schermfirmware"
entity_category: diagnostic
lambda: 'return {"0.1.0"};'
lambda: 'return {"0.1.2"};'
update_interval: 60s
- platform: template
id: smartdisplay_action
@@ -4636,6 +4635,25 @@ script:
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
}
# Runtime changes do not count as touches and do not reset the idle timer.
- id: apply_screen_settings
mode: restart
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
id(back_light).turn_on().set_transition_length(300).set_brightness(level / 100.0f).perform();
if (id(display_dimmed)) lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
if (settings.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: wake_display
mode: restart
then:
@@ -4647,7 +4665,7 @@ script:
# Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag
# als je het scherm aanraakt en op licht staat te wachten.
transition_length: 80ms
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
@@ -4659,41 +4677,14 @@ script:
then:
- if:
condition:
lambda: 'return std::string("${AUTO_DIM}") == "true";'
lambda: 'return screen_settings::current.standby_enabled;'
then:
- lambda: |-
int dim_value = atoi("${DIM_BRIGHTNESS}");
int max_value = atoi("${MAX_BRIGHTNESS}");
if (std::string("${NIGHT_MODE}") == "true") {
auto now = id(homeassistant_time).now();
// Vóór de eerste tijd-sync (zie time: hierboven) is now()
// epoch 0 — dan geen nachtmodus-beslissing nemen op basis
// van een ongeldig uur, gewoon normale DIM_BRIGHTNESS.
if (now.is_valid()) {
int hour = now.hour;
int start_hour = atoi("${NIGHT_START_HOUR}");
int end_hour = atoi("${NIGHT_END_HOUR}");
bool is_night = false;
if (start_hour < end_hour) {
is_night = (hour >= start_hour && hour < end_hour);
} else {
is_night = (hour >= start_hour || hour < end_hour);
}
if (is_night) {
dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}");
}
}
}
// Rustig omlaag faden is prettig; omhoog moet juist snel.
id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform();
// Always mark as dimmed to stop the interval from re-firing
id(display_dimmed) = true;
// Only show the touch-blocking overlay when brightness actually changes
if (dim_value < max_value) {
lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
id(apply_screen_settings).execute();
if (screen_settings::current.home_on_standby) {
id(tile_page) = 0;
id(show_tile_page).execute();
}
# Terug naar het basisscherm bij standby. Bewust via de ESPHome
@@ -5582,7 +5573,9 @@ script:
if (runtime_tiles::enabled) {
runtime_tiles::render(id(lbl_room));
auto now = id(homeassistant_time).now();
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime("%H:%M"));
if (screen_settings::current.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M"));
return;
}
auto set_lbl = [](lv_obj_t* lbl, const std::string& s) {
+52 -59
View File
@@ -781,7 +781,7 @@ esphome:
min_version: 2026.6.2
project:
name: maxgramser.esp_screen
version: "0.1.0"
version: "0.1.2"
includes:
- <esphome/components/smart_display/cyd_ui.h>
- <esphome/components/smart_display/light_controls.h>
@@ -800,8 +800,19 @@ esphome:
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
- lambda: |-
auto &settings = screen_settings::current;
settings.standby_enabled = std::string("${AUTO_DIM}") == "true";
settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}");
settings.brightness = atoi("${MAX_BRIGHTNESS}");
settings.standby_brightness = atoi("${DIM_BRIGHTNESS}");
settings.night_enabled = std::string("${NIGHT_MODE}") == "true";
settings.night_start = atoi("${NIGHT_START_HOUR}") * 60;
settings.night_end = atoi("${NIGHT_END_HOUR}") * 60;
settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}");
runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true";
if (!runtime_tiles::enabled) return;
runtime_tiles::load_settings();
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -883,7 +894,7 @@ esphome:
${TOUCH_CAL_X_MIN}, ${TOUCH_CAL_X_MAX}, ${TOUCH_CAL_Y_MIN}, ${TOUCH_CAL_Y_MAX});
- light.turn_on:
id: back_light
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- script.execute: show_tile_page
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- if:
@@ -944,6 +955,12 @@ api:
- action: ui_state
then:
- lambda: |-
const auto &cfg = screen_settings::current;
ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u dimmed=%d",
cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness,
cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness,
cfg.show_clock, cfg.clock_24h, cfg.home_on_standby,
(unsigned)(millis() - id(last_touch_ms)), id(display_dimmed));
lv_area_t nav, grid;
lv_obj_update_layout(id(home_page)->obj);
lv_obj_get_coords(id(page_next), &nav);
@@ -971,9 +988,9 @@ interval:
- if:
condition:
lambda: |-
if (std::string("${AUTO_DIM}") != "true") return false;
if (!screen_settings::current.standby_enabled) return false;
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL);
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
then:
- script.execute: dim_display
@@ -984,33 +1001,15 @@ time:
update_interval: 60s
on_time_sync:
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- seconds: 0
minutes: 0
hours: ${NIGHT_START_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
- seconds: 0
minutes: 0
hours: ${NIGHT_END_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
globals:
- id: rendered_frames
type: uint32_t
@@ -1452,7 +1451,7 @@ text_sensor:
- platform: template
name: "Schermfirmware"
entity_category: diagnostic
lambda: 'return {"0.1.0"};'
lambda: 'return {"0.1.2"};'
update_interval: 60s
- platform: template
id: smartdisplay_action
@@ -4569,6 +4568,25 @@ script:
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
}
# Runtime changes do not count as touches and do not reset the idle timer.
- id: apply_screen_settings
mode: restart
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
id(back_light).turn_on().set_transition_length(300).set_brightness(level / 100.0f).perform();
if (id(display_dimmed)) lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
if (settings.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: wake_display
mode: restart
then:
@@ -4580,7 +4598,7 @@ script:
# Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag
# als je het scherm aanraakt en op licht staat te wachten.
transition_length: 80ms
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
@@ -4592,41 +4610,14 @@ script:
then:
- if:
condition:
lambda: 'return std::string("${AUTO_DIM}") == "true";'
lambda: 'return screen_settings::current.standby_enabled;'
then:
- lambda: |-
int dim_value = atoi("${DIM_BRIGHTNESS}");
int max_value = atoi("${MAX_BRIGHTNESS}");
if (std::string("${NIGHT_MODE}") == "true") {
auto now = id(homeassistant_time).now();
// Vóór de eerste tijd-sync (zie time: hierboven) is now()
// epoch 0 — dan geen nachtmodus-beslissing nemen op basis
// van een ongeldig uur, gewoon normale DIM_BRIGHTNESS.
if (now.is_valid()) {
int hour = now.hour;
int start_hour = atoi("${NIGHT_START_HOUR}");
int end_hour = atoi("${NIGHT_END_HOUR}");
bool is_night = false;
if (start_hour < end_hour) {
is_night = (hour >= start_hour && hour < end_hour);
} else {
is_night = (hour >= start_hour || hour < end_hour);
}
if (is_night) {
dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}");
}
}
}
// Rustig omlaag faden is prettig; omhoog moet juist snel.
id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform();
// Always mark as dimmed to stop the interval from re-firing
id(display_dimmed) = true;
// Only show the touch-blocking overlay when brightness actually changes
if (dim_value < max_value) {
lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
id(apply_screen_settings).execute();
if (screen_settings::current.home_on_standby) {
id(tile_page) = 0;
id(show_tile_page).execute();
}
# Terug naar het basisscherm bij standby. Bewust via de ESPHome
@@ -5515,7 +5506,9 @@ script:
if (runtime_tiles::enabled) {
runtime_tiles::render(id(lbl_room));
auto now = id(homeassistant_time).now();
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime("%H:%M"));
if (screen_settings::current.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M"));
return;
}
auto set_lbl = [](lv_obj_t* lbl, const std::string& s) {
+52 -59
View File
@@ -586,7 +586,7 @@ esphome:
min_version: 2026.6.2
project:
name: maxgramser.esp_screen
version: "0.1.0"
version: "0.1.2"
platformio_options:
build_flags:
- -DLV_USE_SNAPSHOT=1
@@ -607,8 +607,19 @@ esphome:
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
- lambda: |-
auto &settings = screen_settings::current;
settings.standby_enabled = std::string("${AUTO_DIM}") == "true";
settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}");
settings.brightness = atoi("${MAX_BRIGHTNESS}");
settings.standby_brightness = atoi("${DIM_BRIGHTNESS}");
settings.night_enabled = std::string("${NIGHT_MODE}") == "true";
settings.night_start = atoi("${NIGHT_START_HOUR}") * 60;
settings.night_end = atoi("${NIGHT_END_HOUR}") * 60;
settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}");
runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true";
if (!runtime_tiles::enabled) return;
runtime_tiles::load_settings();
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -682,7 +693,7 @@ esphome:
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
- light.turn_on:
id: back_light
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- script.execute: show_tile_page
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- if:
@@ -752,6 +763,12 @@ api:
- action: ui_state
then:
- lambda: |-
const auto &cfg = screen_settings::current;
ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u dimmed=%d",
cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness,
cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness,
cfg.show_clock, cfg.clock_24h, cfg.home_on_standby,
(unsigned)(millis() - id(last_touch_ms)), id(display_dimmed));
lv_area_t nav, grid;
lv_obj_update_layout(id(home_page)->obj);
lv_obj_get_coords(id(page_next), &nav);
@@ -779,9 +796,9 @@ interval:
- if:
condition:
lambda: |-
if (std::string("${AUTO_DIM}") != "true") return false;
if (!screen_settings::current.standby_enabled) return false;
if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false;
return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL);
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
then:
- script.execute: dim_display
@@ -792,33 +809,15 @@ time:
update_interval: 60s
on_time_sync:
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
on_time:
- seconds: 0
minutes: /1
then:
- script.execute: apply_screen_settings
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- seconds: 0
minutes: 0
hours: ${NIGHT_START_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
- seconds: 0
minutes: 0
hours: ${NIGHT_END_HOUR}
then:
- if:
condition:
lambda: 'return id(display_dimmed);'
then:
- script.execute: dim_display
globals:
- id: rendered_frames
type: uint32_t
@@ -1265,7 +1264,7 @@ text_sensor:
- platform: template
name: "Schermfirmware"
entity_category: diagnostic
lambda: 'return {"0.1.0"};'
lambda: 'return {"0.1.2"};'
update_interval: 60s
- platform: template
id: smartdisplay_action
@@ -4381,6 +4380,25 @@ script:
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
}
# Runtime changes do not count as touches and do not reset the idle timer.
- id: apply_screen_settings
mode: restart
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
id(display_dimmed) = false;
}
auto now = id(homeassistant_time).now();
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
id(back_light).turn_on().set_transition_length(300).set_brightness(level / 100.0f).perform();
if (id(display_dimmed)) lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
if (settings.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: wake_display
mode: restart
then:
@@ -4392,7 +4410,7 @@ script:
# Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag
# als je het scherm aanraakt en op licht staat te wachten.
transition_length: 80ms
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
@@ -4404,41 +4422,14 @@ script:
then:
- if:
condition:
lambda: 'return std::string("${AUTO_DIM}") == "true";'
lambda: 'return screen_settings::current.standby_enabled;'
then:
- lambda: |-
int dim_value = atoi("${DIM_BRIGHTNESS}");
int max_value = atoi("${MAX_BRIGHTNESS}");
if (std::string("${NIGHT_MODE}") == "true") {
auto now = id(homeassistant_time).now();
// Vóór de eerste tijd-sync (zie time: hierboven) is now()
// epoch 0 — dan geen nachtmodus-beslissing nemen op basis
// van een ongeldig uur, gewoon normale DIM_BRIGHTNESS.
if (now.is_valid()) {
int hour = now.hour;
int start_hour = atoi("${NIGHT_START_HOUR}");
int end_hour = atoi("${NIGHT_END_HOUR}");
bool is_night = false;
if (start_hour < end_hour) {
is_night = (hour >= start_hour && hour < end_hour);
} else {
is_night = (hour >= start_hour || hour < end_hour);
}
if (is_night) {
dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}");
}
}
}
// Rustig omlaag faden is prettig; omhoog moet juist snel.
id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform();
// Always mark as dimmed to stop the interval from re-firing
id(display_dimmed) = true;
// Only show the touch-blocking overlay when brightness actually changes
if (dim_value < max_value) {
lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
id(apply_screen_settings).execute();
if (screen_settings::current.home_on_standby) {
id(tile_page) = 0;
id(show_tile_page).execute();
}
# Terug naar het basisscherm bij standby. Bewust via de ESPHome
@@ -5327,7 +5318,9 @@ script:
if (runtime_tiles::enabled) {
runtime_tiles::render(id(lbl_room));
auto now = id(homeassistant_time).now();
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime("%H:%M"));
if (screen_settings::current.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M"));
return;
}
auto set_lbl = [](lv_obj_t* lbl, const std::string& s) {
+8
View File
@@ -1,3 +1,11 @@
# 0.1.2
- Per scherm standbyduur, normale/standby/nachthelderheid en nachturen instellen.
- Klok aan/uit, 12/24 uur en terug naar pagina 1 na standby.
- Instellingen direct doorsturen; firmware 0.1.2 bewaart ze in preferences.
- Bestaande tegels en sleutels blijven behouden. Eenmalig firmware bijwerken.
- De Guition-displaydriver is ongewijzigd; dit is geen oplossing voor paneelstrepen.
# 0.1.1
- Duidelijke foutmelding bij een ongeldige naam, te veel tegels of dubbele entiteiten.
+36 -1
View File
@@ -10,6 +10,38 @@ REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement'.split())
# Additive schema 1 extension. An absent object retains old firmware/YAML defaults.
SETTING_RULES = {
'standby_enabled': (True, None, None),
'standby_seconds': (600, 60, 86400),
'brightness': (100, 5, 100),
'standby_brightness': (20, 0, 100),
'night_enabled': (True, None, None),
'night_start': (1320, 0, 1439),
'night_end': (420, 0, 1439),
'night_brightness': (10, 0, 100),
'show_clock': (True, None, None),
'clock_24h': (True, None, None),
'home_on_standby': (False, None, None),
}
def validate_settings(data):
if not isinstance(data, dict) or set(data) - SETTING_RULES.keys():
raise ValueError('Onbekende scherminstellingen; vernieuw de beheerpagina.')
clean = {}
for key, (default, minimum, maximum) in SETTING_RULES.items():
value = data.get(key, default)
if minimum is None:
valid = type(value) is bool
else:
valid = type(value) is int and minimum <= value <= maximum
if not valid:
raise ValueError(f'Ongeldige waarde voor {key}.')
clean[key] = value
if max(clean['standby_brightness'], clean['night_brightness']) > clean['brightness']:
raise ValueError('Helderheid in standby en nacht mag niet hoger zijn dan normaal.')
return clean
def entity_id(value):
return isinstance(value, str) and len(value) <= 120 and re.fullmatch(r'[a-z0-9_]+\.[a-z0-9_]+', value) and value.split('.')[0] in DOMAINS
@@ -35,7 +67,10 @@ def validate_layout(data):
raise ValueError('Een tegelnaam mag maximaal 80 bytes bevatten.')
seen.add(tile['entity'])
clean.append({'entity': tile['entity'], 'name': name.strip()})
return {'title': title.strip(), 'tiles': clean}
result = {'title': title.strip(), 'tiles': clean}
if 'settings' in data:
result['settings'] = validate_settings(data['settings'])
return result
def state_message(index, tile, states):
state = states.get(tile['entity'], {})
+5
View File
@@ -130,6 +130,9 @@ class Manager:
if inbox not in {s['id'] for s in self.inventory()[0]}:
raise ValueError('Dit is geen gekoppeld ESP-scherm. Vernieuw het overzicht.')
layout = validate_layout(data)
# A still-open older UI may save tiles without the new optional settings.
if 'settings' not in layout and 'settings' in self.layouts.get(inbox, {}):
layout['settings'] = self.layouts[inbox]['settings'].copy()
known = {e['id'] for e in self.inventory()[1]}
if any(t['entity'] not in known for t in layout['tiles']):
raise ValueError('Een gekozen entiteit bestaat niet meer. Zoek de nieuwe entiteit op.')
@@ -149,6 +152,8 @@ class Manager:
async def sync_one(self, inbox, layout, force=False):
messages = [{'v': 1, 'op': 'layout', 'title': layout['title'], 'entities': [t['entity'] for t in layout['tiles']]}]
if 'settings' in layout:
messages[0]['settings'] = layout['settings']
messages += [state_message(i, tile, self.ha.states) for i, tile in enumerate(layout['tiles'])]
previous = self.sent.get(inbox, [])
force = force or not previous or messages[0] != previous[0]
+97
View File
@@ -65,6 +65,99 @@ function select(id) {
renderScreens();
renderTiles();
renderResults();
renderSettings();
}
const settingDefinitions = [
["standby_enabled", "Automatisch standby", "check", true],
["standby_seconds", "Standby na", "minutes", 600, 1, 1440],
["brightness", "Helderheid normaal", "range", 100, 5, 100],
["standby_brightness", "Helderheid in standby", "range", 20, 0, 100],
["night_enabled", "Nachtstand gebruiken", "check", true],
["night_brightness", "Helderheid in nachtstand", "range", 10, 0, 100],
["night_start", "Nachtstand vanaf", "time", 1320],
["night_end", "Nachtstand tot", "time", 420],
["show_clock", "Klok tonen", "check", true],
["clock_24h", "24-uursklok (uit = 12 uur)", "check", true],
["home_on_standby", "Na standby terug naar pagina 1", "check", false],
];
function renderSettings() {
const values = {
...Object.fromEntries(
settingDefinitions.map(([key, , , value]) => [key, value]),
),
...layout.settings,
};
const container = $("#settings-fields");
container.replaceChildren();
for (const [key, title, kind, , min, max] of settingDefinitions) {
const label = node(
"label",
undefined,
`setting ${kind === "check" ? "setting-check" : ""}`,
);
const caption = node("span", title),
input = node("input"),
output = node("output");
input.id = `setting-${key}`;
input.type =
kind === "check" ? "checkbox" : kind === "minutes" ? "number" : kind;
input.setAttribute("aria-label", title);
input.required = kind === "minutes" || kind === "time";
if (kind === "check") input.checked = values[key];
else if (kind === "time")
input.value = `${String(Math.floor(values[key] / 60)).padStart(2, "0")}:${String(values[key] % 60).padStart(2, "0")}`;
else {
input.min = min;
input.max = max;
input.step = 1;
input.value = kind === "minutes" ? values[key] / 60 : values[key];
}
const updateOutput = () =>
(output.textContent =
kind === "range"
? `${input.value}%`
: kind === "minutes"
? "minuten"
: "");
updateOutput();
input.oninput = () => {
if (!input.checkValidity() || input.value === "") return;
let value = kind === "check" ? input.checked : Number(input.value);
if (kind === "time") {
const [h, m] = input.value.split(":").map(Number);
value = h * 60 + m;
}
if (kind === "minutes") value *= 60;
layout.settings = { ...values, ...layout.settings, [key]: value };
// A lower normal brightness also lowers any higher standby levels.
if (key === "brightness")
for (const dim of ["standby_brightness", "night_brightness"]) {
layout.settings[dim] = Math.min(layout.settings[dim], value);
const other = $(`#setting-${dim}`);
other.value = layout.settings[dim];
other.parentElement.querySelector("output").textContent =
`${other.value}%`;
}
updateOutput();
markDirty();
};
label.append(caption, input, output);
container.append(label);
}
renderSettingsSupport();
}
function renderSettingsSupport() {
const version =
inventory.screens.find((s) => s.id === selected)?.firmware || "";
const parts = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(version);
const supported =
parts &&
(Number(parts[1]) > 0 ||
Number(parts[2]) > 1 ||
(Number(parts[2]) === 1 && Number(parts[3]) >= 2));
$("#settings-support").textContent = supported
? "Opslaan stuurt je instellingen direct naar dit scherm. Ze blijven ook na een herstart bewaard."
: "Eenmalig firmware 0.1.2 of nieuwer installeren via ESPHome. Je kunt de instellingen alvast bewaren; oudere firmware gebruikt ze nog niet.";
}
function renderScreens() {
$("#screens").replaceChildren();
@@ -220,6 +313,7 @@ async function refresh() {
: "Verbinding met Home Assistant herstellen…";
$("#connection").classList.toggle("online", inventory.connected);
renderScreens();
if (selected) renderSettingsSupport();
if (!selected && inventory.screens.length) select(inventory.screens[0].id);
} catch {
$("#connection").textContent =
@@ -228,6 +322,9 @@ async function refresh() {
}
$("#save").onclick = async () => {
if (busy) return;
for (const input of document.querySelectorAll("#settings-fields input")) {
if (!input.reportValidity()) return;
}
busy = true;
$("#save").disabled = true;
try {
+13
View File
@@ -86,6 +86,19 @@
<div id="results" aria-live="polite"></div>
</div>
</div>
<details class="screen-settings" open>
<summary>
Scherminstellingen
<span class="hint">Helderheid, rustmomenten en weergave</span>
</summary>
<p class="hint" id="settings-support"></p>
<div class="settings-grid" id="settings-fields"></div>
<p class="hint">
Nachturen gelden voor de helderheid tijdens standby. De eerste
aanraking maakt het scherm wakker; de volgende bedient je huis.
Gelijke begin- en eindtijd schakelen het nachtvenster uit.
</p>
</details>
<footer class="savebar">
<div>
<strong id="dirty">Alles opgeslagen</strong
+53
View File
@@ -563,3 +563,56 @@ textarea {
margin-left: 0;
}
}
.screen-settings {
margin: 28px 0 20px;
border-top: 1px solid var(--line);
padding-top: 20px;
}
.screen-settings summary {
cursor: pointer;
font-size: 18px;
font-weight: 700;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px 28px;
margin: 20px 0;
}
.setting {
padding: 14px;
border: 1px solid var(--line);
border-radius: 12px;
}
.setting output {
display: block;
color: var(--muted);
font-size: 13px;
}
.setting input[type="range"] {
padding: 0;
accent-color: var(--accent);
min-height: 32px;
}
.setting-check {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.setting-check input {
width: 22px;
height: 22px;
flex-shrink: 0;
margin: 0;
accent-color: var(--accent);
}
.setting-check output {
display: none;
}
@media (max-width: 640px) {
.settings-grid {
grid-template-columns: 1fr;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.1.1"
version: "0.1.2"
slug: esp_screen_manager
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
url: https://github.com/MaxGramser/homeassistant_espscreen
+42 -1
View File
@@ -8,7 +8,7 @@ import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'screen_manager/app'))
from core import discover, installation_yaml, packets, state_message, validate_layout
from core import discover, installation_yaml, packets, state_message, validate_layout, validate_settings
class ProtocolTests(unittest.TestCase):
def test_unicode_chunks_and_limits(self):
@@ -24,6 +24,22 @@ class ProtocolTests(unittest.TestCase):
with self.assertRaises(ValueError): validate_layout({'title':'Thuis','tiles':tiles})
self.assertEqual(validate_layout({'title':'Thuis','tiles':[]})['tiles'],[])
def test_settings_defaults_ranges_and_old_layout(self):
self.assertNotIn('settings', validate_layout({'title':'Thuis','tiles':[]}))
defaults = validate_settings({})
self.assertEqual(defaults['standby_seconds'], 600)
self.assertEqual(defaults['night_start'], 22*60)
for patch in [{'standby_seconds':True}, {'brightness':0}, {'night_start':1440},
{'show_clock':1}, {'standby_seconds':600.5}, {'new_unknown':1},
{'brightness':10,'standby_brightness':20}]:
with self.assertRaises(ValueError): validate_settings(patch)
for bad in [None, [], 'wrong']:
with self.assertRaises(ValueError): validate_settings(bad)
settings = validate_settings({'brightness':40,'standby_brightness':0,'night_brightness':0})
self.assertEqual(settings['brightness'],40)
message={'v':1,'op':'layout','title':'Thuis','entities':[], 'settings':settings}
self.assertTrue(all(len(p)<=255 for p in packets(message)))
def test_message_has_only_bounded_display_attributes(self):
state={'state':'on','attributes':{'friendly_name':'é'*90,'access_token':'private','hs_color':[float('nan'),300], 'brightness':float('inf')}}
msg=state_message(0,{'entity':'light.a','name':''},{'light.a':state})
@@ -87,6 +103,31 @@ class ManagerTests(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(ValueError): self.setup_manager(path)
self.assertIn('99',path.read_text(),'unknown schema must never be overwritten')
async def test_settings_survive_update_and_old_client_save(self):
with tempfile.TemporaryDirectory() as temp:
path=Path(temp)/'screens.json'; m=self.setup_manager(path)
legacy={'title':'Thuis','tiles':[{'entity':'light.a','name':'Mijn lamp'}]}
path.write_text(json.dumps({'version':1,'screens':{'text.screen':legacy}}))
m=self.setup_manager(path)
self.assertEqual(m.layouts['text.screen'],legacy)
settings=validate_settings({'brightness':55,'standby_seconds':1200})
m.save('text.screen',{**legacy,'settings':settings})
fresh=self.setup_manager(path)
self.assertEqual(fresh.layouts['text.screen']['settings'],settings)
fresh.save('text.screen',{**legacy,'title':'Andere titel'})
saved=fresh.layouts['text.screen']
self.assertEqual(saved['settings'],settings)
self.assertEqual(saved['tiles'],legacy['tiles'])
before=path.read_bytes()
with self.assertRaises(ValueError): fresh.save('text.screen',{**saved,'settings':{'brightness':0}})
self.assertEqual(path.read_bytes(),before)
await fresh.sync_one('text.screen',saved)
self.assertEqual(fresh.ha.messages[0][1]['settings'],settings)
await fresh.sync_one('text.screen',saved)
self.assertEqual(len(fresh.ha.messages),2)
await fresh.sync_one('text.screen',saved,True)
self.assertEqual(fresh.ha.messages[2][1]['settings'],settings)
async def test_reorder_aborts_old_batch(self):
with tempfile.TemporaryDirectory() as temp:
m=self.setup_manager(Path(temp)/'screens.json');layout={'title':'Thuis','tiles':[{'entity':'light.a','name':''}]}
+20
View File
@@ -0,0 +1,20 @@
#include "components/smart_display/screen_settings.h"
#include <cassert>
int main() {
using screen_settings::Settings;
Settings s;
assert(s.valid() && s.standby_seconds == 600);
assert(s.dim_level(-1) == 20); // No time sync must not accidentally select night.
assert(s.dim_level(1319) == 20 && s.dim_level(1320) == 10);
assert(s.dim_level(419) == 10 && s.dim_level(420) == 20);
s.night_start = 60; s.night_end = 120;
assert(!s.night(59) && s.night(60) && !s.night(120));
s.night_end = 60; assert(!s.night(60) && !s.night(0));
auto copy = s; assert(copy == s);
copy.clock_24h = 0; assert(!(copy == s));
s.brightness = 5; assert(!s.valid());
s.standby_brightness = s.night_brightness = 0; assert(s.valid());
s.standby_seconds = 86401; assert(!s.valid());
s.standby_seconds = 60; s.show_clock = 2; assert(!s.valid());
s.show_clock = 1; s.version = 2; assert(!s.valid());
}