Files
homeassistant_espscreen/docs/SETTINGS.md
T
Max Gramser b58f0cde25 Let the screen own its settings, answer its pings, and show them as cards (0.2.57)
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>
2026-09-16 15:50:41 +02:00

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.