b58f0cde25
A code check found that a setting changed on a screen could jump back. ESP Screen Manager kept its own copy and sent it with every full update: after a restart of the app or Home Assistant, every hour and after every save. A change it missed while it or Home Assistant restarted came back to the old value, a reported setting was lost when save() refused a layout with a removed entity, and every step of a held - or + on the screen resent the whole screen. Firmware 0.2.49 offers every setting as an entity over the existing preference records (no migration): Night mode, Night starts/ends, 24-hour clock, Back to page 1 and after how long, Back to page 1 on standby, Swipe between pages and, on the Guition, Rotation, next to the four numbers and Auto standby. The settings page and every entity write through settings_screen::set(), which clamps like the page and stores, applies and reports only a real change. screen_message answers a caller that asks with its status and layout revision (api.respond). The settings tile opens without Home Assistant. The app reads and changes the settings of such a screen through its entities and leaves them out of the layout message. Older firmware keeps them with the layout; a setting it reports is stored without the tile check and not sent back. Pings to screens that can answer learn at once whether a tile is missing and resend after 30 s instead of 120 s. Layout sensors come back after a Home Assistant restart, and weather tiles only ask for the forecasts the entity supports. The editor's settings form is now three cards with the rows of the screen's own page; a change applies at once, and an offline screen shows its values as unknown. Checked with 245 Python and 14 C++ tests, generate_packages --check, ESP32 builds of both profiles, host-build API tests on both boards, and a live run against Home Assistant 2026.9.1 with a temporary host device. Firmware 0.2.49. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
9.1 KiB
Markdown
164 lines
9.1 KiB
Markdown
# Settings: on the screen, in Home Assistant and in ESP Screens
|
|
|
|
Every screen setting can be changed in three places: the **settings page on the screen itself**
|
|
(firmware 0.2.44+), its **entity in Home Assistant** (firmware 0.2.49+), and the **Screen settings**
|
|
panel of the add-on editor. This page says who owns a value, what the page is made of, and exactly what
|
|
adding one more setting takes.
|
|
|
|
## Who owns a setting
|
|
|
|
**Firmware 0.2.49 and newer: the screen.** It keeps every value in its own preferences and offers each one
|
|
as an entity in the *config* category of its ESPHome device. The settings page, an automation and ESP Screens
|
|
all change a value through `settings_screen::set()`, so they never disagree, and nothing has to catch up
|
|
after ESP Screens or Home Assistant was away. ESP Screens reads the values from Home Assistant's states,
|
|
changes them with the entity's own action, and leaves them out of the layout message.
|
|
|
|
| Row on the screen | Entity | Key in ESP Screens |
|
|
|---|---|---|
|
|
| Brightness | `number.<screen>_normal_brightness` | `brightness` |
|
|
| Auto standby | `switch.<screen>_auto_standby` | `standby_enabled` |
|
|
| Standby after | `number.<screen>_standby_after` | `standby_seconds` |
|
|
| Standby brightness | `number.<screen>_standby_brightness` | `standby_brightness` |
|
|
| Night mode | `switch.<screen>_night_mode` | `night_enabled` |
|
|
| Starts, Ends | `time.<screen>_night_starts`, `time.<screen>_night_ends` | `night_start`, `night_end` |
|
|
| Night brightness | `number.<screen>_night_brightness` | `night_brightness` |
|
|
| Clock | `switch.<screen>_24_hour_clock` | `clock_24h` |
|
|
| Back to page 1, After | `switch.<screen>_back_to_page_1`, `number.<screen>_back_to_page_1_after` | `auto_home`, `auto_home_seconds` |
|
|
| Also on standby | `switch.<screen>_back_to_page_1_on_standby` | `home_on_standby` |
|
|
| Swipe between pages | `switch.<screen>_swipe_between_pages` | `swipe_pages` |
|
|
| Rotation (Guition) | `select.<screen>_rotation` | `rotation` |
|
|
|
|
The first four entities and Auto standby existed before 0.2.49; ESP Screens recognizes a screen that owns
|
|
its settings by one of the others (`OWNED_SETTINGS_MARKERS` in `core.py`).
|
|
|
|
**Older firmware: ESP Screens.** The values travel in the layout message: `settings`, the frozen block of
|
|
eleven keys, with `swipe_pages`, `auto_home`, `auto_home_seconds` and `rotation` as keys of their own. A
|
|
change on the screen comes back as an `esphome.screen_setting` event, which ESP Screens stores with the
|
|
layout without sending it back.
|
|
|
|
## What the user sees
|
|
|
|
Holding the top bar of the overview for about one and a half seconds opens the page; a blue line grows
|
|
along the top edge while you hold, and letting go before it finishes cancels. A screen can also carry a
|
|
`screen.settings` tile, which opens the page with or without Home Assistant, and Home Assistant can open it
|
|
with `esphome.<screen>_open_settings`.
|
|
|
|
The page is a menu of groups, each of which opens a page of its own:
|
|
|
|
| Group | Rows |
|
|
|---|---|
|
|
| Brightness | Brightness, Auto standby, Standby after, Standby brightness |
|
|
| Night | Night mode, Starts, Ends, Night brightness |
|
|
| Screen | Clock, Back to page 1, After, Also on standby, Swipe between pages, Rotation (boards that turn) |
|
|
| This screen | Screen, Address, Firmware, Home Assistant, Restart |
|
|
|
|
Every change is stored on the screen, applied at once and published on its entity, so Home Assistant and
|
|
ESP Screens show it within a second. The editor's **Screen settings** panel has the first three groups as
|
|
cards with the same rows: a switch for a toggle, `-` and `+` that repeat while held, chips for the clock
|
|
and the rotation. A change there applies at once, without Save. An offline screen shows its values as
|
|
unknown and takes no changes until it is back.
|
|
|
|
## The rules the page follows
|
|
|
|
- **Two levels, never three.** A group page is the deepest place a setting can live.
|
|
- **No free scrolling.** A group that does not fit gets the same `< Previous / Next >` pager as the tile
|
|
pages. A 320x240 board shows five rows, a 480x480 board six.
|
|
- **A row is a control, not a form.** Toggles flip on tap, numbers and times have `-` and `+`, a choice
|
|
cycles through its options in a chip on the right.
|
|
- **A row that depends on a switch above it is greyed out, not hidden**, so the page never jumps around.
|
|
- **Only settings that belong to this piece of glass.** Tiles, the top bar and pages belong to the editor,
|
|
which has a mouse and a keyboard; the screen gets what you want to change while standing in front of it.
|
|
|
|
## Adding a setting
|
|
|
|
The example below adds "Beep on touch" (`beep`), a switch. Every firmware that has it also owns its
|
|
settings, so a new setting never travels in the layout message.
|
|
|
|
### 1. Where the value lives
|
|
|
|
- `screen_settings::Settings` in `components/smart_display/screen_settings.h` is the **frozen** block of
|
|
eleven values that ESP Screens sends older firmware as `settings`. Its format is version 1 and changing it
|
|
needs a preference migration *and* firmware-version gating in the add-on, because older firmware refuses a
|
|
`settings` object that does not have exactly its own keys. Don't.
|
|
- Anything new is a plain value in `settings_screen.h` next to `swipe_pages`, `rotation` and `auto_home`,
|
|
with a preference record of its own: load it in `runtime_tiles::load_settings()` and write it in
|
|
`persist_settings()`.
|
|
|
|
```cpp
|
|
// settings_screen.h, next to the others
|
|
inline int32_t swipe_pages = 0, rotation = 0, auto_home = 1, auto_home_seconds = 120, beep = 0;
|
|
```
|
|
|
|
### 2. The one write path, and the row
|
|
|
|
`settings_screen::set(key, value)` is where every writer ends up. Give the key a branch that clamps it the
|
|
way the page steps it, and add the value to the before/after comparison at the bottom, so a value the screen
|
|
already has is not stored, applied or reported again:
|
|
|
|
```cpp
|
|
else if (key == "beep") reported = beep = flag(value);
|
|
```
|
|
|
|
Then one line in the table of the group it belongs to:
|
|
|
|
```cpp
|
|
toggle("Beep on touch", [] { return beep; }, [](int32_t value) { set("beep", value); }),
|
|
```
|
|
|
|
`set()` calls `changed(key, value)`, which stores, applies and reports in one go; `key` must be the add-on's
|
|
key exactly. The row kinds are `toggle`, `number` (fixed step, optional unit), `duration` (seconds, steps
|
|
grow with the value), `moment` (minutes since midnight, quarters by tap and hours while held), `choice`,
|
|
`info` and `action`. A row can carry `shown` (leave it out on boards that lack the hardware) and `enabled`
|
|
(grey while the switch it depends on is off).
|
|
|
|
Watch the count: a group of more than five rows gets a pager on a 320x240 board. Six is the maximum
|
|
that still fits a 480x480 board in one go.
|
|
|
|
### 3. The entity
|
|
|
|
In both board profiles, with the same name as the row:
|
|
|
|
```yaml
|
|
switch:
|
|
- platform: template
|
|
id: setting_beep
|
|
name: "Beep on touch"
|
|
entity_category: config
|
|
restore_mode: DISABLED
|
|
lambda: 'return settings_screen::beep != 0;'
|
|
turn_on_action:
|
|
- lambda: 'settings_screen::set("beep", 1);'
|
|
turn_off_action:
|
|
- lambda: 'settings_screen::set("beep", 0);'
|
|
```
|
|
|
|
The screen's preferences hold the value, so the entity reads it in a lambda instead of keeping a copy
|
|
(`restore_value` cannot be combined with a lambda). A switch publishes its own changes. A number, time or
|
|
select takes `update_interval: never` and one line in `apply_screen_settings`, which publishes it when it
|
|
differs; that script runs after every change and on every Home Assistant connection (the time sync), so the
|
|
entity has a value before Home Assistant reads the states. Then run `python3 tools/generate_packages.py`.
|
|
|
|
### 4. The add-on
|
|
|
|
- `screen_manager/app/core.py`: one line in `SETTING_RULES` (`'beep': (False, None, None)`) for validation,
|
|
one in `SETTING_ENTITIES` (`'beep': ('switch', 'Beep on touch')`, the entity name exactly as in the
|
|
profiles), and the key in `SETTINGS_BESIDE_BLOCK` so it never enters the frozen block.
|
|
- `screen_manager/app/server.py`: `settings_view` already leaves a key out for a screen whose device has no
|
|
entity for it. Leave it out for a screen that does not own its settings too (`owner` `'layout'`), like
|
|
`auto_home` below firmware 0.2.44: such firmware cannot have it.
|
|
- `screen_manager/app/static/app.js`: one row in `SETTING_GROUPS`, with the label the screen uses.
|
|
- `screen_manager/app/claude_skill.py`: a row in the table of screen entities.
|
|
|
|
### 5. Tests and proof
|
|
|
|
- `tests/test_settings_screen.cpp` (`clang++ -std=c++17 -Wall -Wextra -Werror -I.`) walks every page and
|
|
every row, and checks `set()`: add the key to its table of clamps.
|
|
- `tests/test_screen_owned_settings.py` checks the entities in both profiles, `SETTING_ENTITIES`, the editor
|
|
rows (labels, steps and the duration ladder against `settings_screen.h`) and the add-on's calls;
|
|
`tests/test_settings_view.py` and `tests/test_layout.py` cover the rest of the add-on side.
|
|
- Render it before believing it: `.esphome/readme-render/host_build.py <board> --compile` builds the real
|
|
firmware for the Mac and `render_settings.py <board>` drives it over the API and saves PNGs of every
|
|
page, including the hold gesture, the time picker and the settings tile. The host build takes its time
|
|
from the Mac instead of Home Assistant, so without a connection-time sync its numbers, times and select
|
|
get their first value at the next whole minute.
|