# 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._normal_brightness` | `brightness` | | Dark mode (0.2.54+) | `switch._dark_mode` | `dark_mode` | | Auto standby | `switch._auto_standby` | `standby_enabled` | | Standby after | `number._standby_after` | `standby_seconds` | | Standby brightness | `number._standby_brightness` | `standby_brightness` | | Night mode | `switch._night_mode` | `night_enabled` | | Starts, Ends | `time._night_starts`, `time._night_ends` | `night_start`, `night_end` | | Night brightness | `number._night_brightness` | `night_brightness` | | Back to page 1, After | `switch._back_to_page_1`, `number._back_to_page_1_after` | `auto_home`, `auto_home_seconds` | | Also on standby | `switch._back_to_page_1_on_standby` | `home_on_standby` | | Swipe between pages | `switch._swipe_between_pages` | `swipe_pages` | | Page buttons (0.2.69+) | `switch._page_buttons` | `page_buttons` | | Rotation (Guition) | `select._rotation` | `rotation` | The 12 or 24-hour clock was a row and an entity of its own (`switch._24_hour_clock`) from firmware 0.2.49 to 0.2.75. Since app 0.2.90 and firmware 0.2.76 it is one setting for every screen, with the language and the number format: Settings → Language & region in ESP Screens, which sends `clock_24h` and `numbers` in the layout message. 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`). Dark mode came with firmware 0.2.54: a screen without its switch shows no Dark mode row in ESP Screens, and firmware that gets its settings with the layout never gets it at all. What Dark mode changes on the glass is in [docs/THEME.md](THEME.md). **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._open_settings`. The page is a menu of groups, each of which opens a page of its own: | Group | Rows | |---|---| | Brightness | Brightness, Dark mode, 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, Page buttons, 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 pager as the tile pages: a chevron in each half of the bar and a dot per page between them (firmware 0.2.69+). A 320x240 board shows five rows, a 480x480 board six; with the pager four and five. - **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 `packages/core.yaml`, which every board builds from, 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. A setting only some boards have goes into their board files instead, like the Rotation select of the Guition (docs/PROFILES.md). ### 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 `packages/core.yaml`), 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'`), as `dark_mode` and `page_buttons` are: such firmware cannot have it. - `web/src/store.ts`: one row in `SETTING_GROUPS`, with the label the screen uses; then build the editor (`cd web && npm test && npm run build`, AGENTS.md). - `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 --compile` builds the real firmware for the Mac and `render_settings.py ` 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.