Let the on/off switch on a wide tile switch, instead of sending the opposite of what it shows (0.2.86)

A tap on the On/off switch of a wide or full-page tile moved the knob to on
and back, and the light stayed off. Since firmware 0.2.59 control_event drew
the new position first (Tile::optimistic writes it into state) and then asked
key_action for the action, which read that new position: an off light got
light.turn_off, an on light light.turn_on. A tap on the tile itself was never
affected.

tile_controls::press_key chooses the action first and then shows the new
position; control_event uses it. Unit tests cover a light, a fan, a sensor
and a cover key.

Checked with tools/check.sh (471 Python tests, 19/19 C++) and compiled for
both boards: CYD 1,677,504 B (91.4 %, +16 B), Guition 2,054,640 B (25.3 %).
On Max's Guition Wallbox (OTA from this branch) every tap on the switch sent
the right action and Home Assistant's logbook followed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-19 08:15:50 +02:00
parent 311fa2a7fd
commit 2d343a450f
8 changed files with 103 additions and 5 deletions
+1 -2
View File
@@ -2872,8 +2872,7 @@ inline void control_event(lv_event_t *e) {
return;
}
if(t.waiting(now))return;
if(command==tile_controls::TOGGLE)t.optimistic(t.state!="on");
auto a=tile_controls::key_action(t,command,w.key_args[n]);
auto a=tile_controls::press_key(t,command,w.key_args[n]);
if(a.valid())action(a.service,t.entity,a.key,a.value);
}
// A busy card is covered by a translucent white sheet with a small spinner until
+8
View File
@@ -368,6 +368,14 @@ inline Action key_action(const Tile &t, int command, const std::string &arg = ""
default: return {};
}
}
// A tap on a key: the action follows the stand the tile had, and a toggle then shows its new stand at once. The order
// matters, because Tile::optimistic writes the new stand into `state`: asked afterwards, key_action sent an off light
// light.turn_off, so the knob flipped on and back while the light stayed off (firmware 0.2.59 to 0.2.71).
inline Action press_key(Tile &t, int command, const std::string &arg = "") {
Action a = key_action(t, command, arg);
if (command == TOGGLE && a.valid()) t.optimistic(t.state != "on");
return a;
}
// ---- Vacuum card rows (firmware 0.2.39+) ----
// The value a row shows as chosen: the one just tapped while Home Assistant has not answered yet.
inline const std::string &shown_value(const Tile &t, const runtime_tiles::Choice &c, uint32_t now) {
+73
View File
@@ -0,0 +1,73 @@
# Test results app 0.2.86 / firmware 0.2.72 (2026-09-19)
Max reported that a tap on the on/off switch at the right of a wide tile went "from off to on to off in one click",
as if it registered a double click. A tap on the tile itself worked. The tile was `light.badkamer_3` on his Guition
Wallbox, a wide tile with the control *On/off switch*.
## Cause
No double tap: the switch sent the opposite of what it showed. `runtime_tiles::control_event` did two things in the
wrong order since firmware 0.2.59 (app 0.2.70, the "tap looks instant" round):
1. `Tile::optimistic(t.state != "on")` draws the new position at once. Since 0.2.59 it writes that position into
`Tile::state` (before, a separate `optimistic_on` field held it and `state` stayed as it was).
2. `tile_controls::key_action(t, TOGGLE)` then chose `turn_off` when `state` was "on", which it now always was for a
light that had been off.
So an off light got `light.turn_off`. Home Assistant answered "it worked" without a new state, the wait ended 800 ms
after that answer, `end_wait` undid the optimistic position, and the knob slid back to off. An on light got
`light.turn_on` the same way. The other three callers of `optimistic()` (the tile's own tap, the light card's switch,
a slider on an off light) choose their action before they call it, or don't read `state` afterwards.
Evidence before the fix:
- The two lines of `control_event`, run against the released headers (origin/main 311fa2a), print
`light off -> knob shows on, sends light.turn_off` and `light on -> knob shows off, sends light.turn_on`.
- Home Assistant's logbook for `light.badkamer_3` only had `light.toggle` state changes from the screen (taps on
the tile). A `turn_off` sent to a light that is off changes nothing, so it leaves no line in the logbook.
## What changed
- `tile_controls::press_key(Tile &, command, arg)`: chooses the action with `key_action` first, then shows a toggle's
new position with `optimistic()`, and only when there is an action to send.
- `runtime_tiles::control_event` calls `press_key` instead of the two lines.
- `tests/test_tile_controls.cpp`: an off light sends `light.turn_on` and shows on, and undoing that brings back off;
an on fan sends `fan.turn_off` and shows off; a toggle on a sensor sends nothing and leaves its state alone; a
cover's open key leaves the state alone.
## Automated
- `tools/check.sh`: Python 471 tests, C++ 19/19, packages, icons, Vitest, vue-tsc, the editor build and the bundle
in Git: all pass.
- `tools/check.sh --firmware --baseline 1677488` (ESPHome 2026.6.2): CYD 1,677,504 B = 91.4 % of the 1,835,008 B
slot, +16 B against 0.2.85 (tight band, well under the 8 KB rule); Guition 2,054,640 B = 25.3 %, the same as 0.2.85.
Built before the version bump, which only changes the version text (same length).
## On the screen
With Max's OK the fix went over the air to his Guition Wallbox (192.168.146.136). The YAML was the one his ESPHome
Device Builder holds for it, with the packages and components of this branch instead of GitHub. That build still says
0.2.71, so ESP Screens offers the released 0.2.72 afterwards. `device_info` read back the build's compile time
(2026-09-19 08:10:24).
Max then tapped the switch and the tile, and the screen's log and Home Assistant's logbook agree on every tap:
| Time | Where | Sent | Light after |
|---|---|---|---|
| 08:13:12.9 | switch (light on) | `light.turn_off` | off |
| 08:13:14.5 | switch | `light.turn_on` | on |
| 08:13:15.8 | switch | `light.turn_off` | off |
| 08:13:16.5 | tile | `light.toggle` | on |
| 08:13:17.1 | tile | `light.toggle` | off |
| 08:13:17.6 | switch | `light.turn_on` | on |
| 08:13:19.2 | switch | `light.turn_off` | off |
Home Assistant reported each new state 220-430 ms after the send. One tap at 08:13:18.1, 526 ms after the one
before, was dropped by the 600 ms same-control debounce (`TouchGuard::accept`), as before. Max: "werkt super!".
## Not tested
- The switch of a switch, an input boolean or a fan on real hardware: they go through the same `press_key` line
and the unit tests.
- The CYD: the fix is the same shared code. Its screen has no tile with a switch now, and it gets 0.2.72 through
**Update**.
+1 -1
View File
@@ -14,7 +14,7 @@
# fonts/materialdesignicons-webfont.ttf (Apache 2.0, github.com/Templarian/MaterialDesign-Webfont).
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.71"
SCREEN_FIRMWARE_VERSION: "0.2.72"
DASHBOARD_INBOX_INTERNAL: "false"
CALIBRATION_ON_BOOT: "false" # a bench build: start on the touch test page
ROOM_NAME: "Choose tiles in HA" # shown top left until ESP Screens sends the screen name
+7
View File
@@ -1,3 +1,10 @@
## 0.2.86 (firmware 0.2.72)
The on/off switch on a wide tile switches again.
- **The switch on a wide or full-page tile does what it shows.** Since firmware 0.2.59 (app 0.2.70), a tap on the *On/off switch* of a wide card sent the opposite of what it showed. The knob moved to on, but the screen asked Home Assistant to turn the light *off*. The light already was off, so nothing happened, and a moment later the knob slid back. On a light that was on, the same happened the other way round. The switch drew its new position before it chose the action, and then chose from that new position; it now chooses first. A tap on the tile itself was never affected. This fixes lights, switches, input booleans and fans. The switch is what a wide tile of these shows unless you choose another control, so most wide tiles of them had it.
- Needs firmware 0.2.72: press **Update** on the screen. Includes everything from 0.2.85. CYD firmware: 1,677,504 bytes, 91.4 % of the update slot (16 bytes more than 0.2.85).
## 0.2.85 (firmware 0.2.71)
Off looks off on every tile, in Home Assistant's colours.
+1 -1
View File
@@ -37,7 +37,7 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
# Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.71'
FIRMWARE_VERSION = '0.2.72'
# The Auto standby switch a screen offers Home Assistant automations.
AUTO_STANDBY_MIN_FIRMWARE = '0.2.41'
# The settings page the screen opens itself, and the screen.settings tile that opens it.
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.85"
version: "0.2.86"
slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen
+11
View File
@@ -128,6 +128,17 @@ int main() {
assert(key_action(make("light.lamp", "off"), TOGGLE).service == "light.turn_on");
assert(!key_action(make("sensor.x", "1"), TOGGLE).valid());
assert(!key_action(make("sensor.x", "1"), RUN).valid());
// A toggle key sends what its tap asks for and then shows the new stand; firmware 0.2.59 to 0.2.71 showed it first
// and so sent an off light light.turn_off.
Tile off_lamp = make("light.lamp", "off");
assert(press_key(off_lamp, TOGGLE).service == "light.turn_on" && off_lamp.state == "on" && off_lamp.optimistic_tap);
off_lamp.undo_optimistic(); assert(off_lamp.state == "off");
Tile on_fan = make("fan.ceiling", "on");
assert(press_key(on_fan, TOGGLE).service == "fan.turn_off" && on_fan.state == "off");
Tile odd = make("sensor.x", "1");
assert(!press_key(odd, TOGGLE).valid() && odd.state == "1" && !odd.optimistic_tap);
Tile shut = make("cover.shutter", "closed", 3);
assert(press_key(shut, COVER_OPEN).service == "cover.open_cover" && shut.state == "closed" && !shut.optimistic_tap);
// Panel kinds.
Tile lamp = make("light.lamp", "on"); lamp.controls = "brightness";