diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index 2966248..c51da7d 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -156,6 +156,17 @@ struct Tile { return state == "on" || state == "cleaning" || state == "active" || (domain() == "person" && state == "home") || (domain() == "sun" && state == "above_horizon") || (domain() == "climate" && available() && state != "off"); } + // A slider shows the card's colour like Home Assistant's tile sliders: grey only while stateActive() + // (frontend src/common/entity/state_active.ts) calls the entity inactive, such as an off light or fan and a + // media player that is off or in standby. A number with a value is active there, and a closed cover's + // position slider keeps the cover's colour so it never looks disabled (hui-cover-position-card-feature.ts). + bool slider_active() const { + if (!available()) return false; + auto d = domain(); + if (d == "media_player") return state != "off" && state != "standby"; + if (d == "cover" || d == "number" || d == "input_number") return true; + return active(); + } }; // Slot position of a tile within the fixed two-column, three-row pages. struct Placement { uint8_t page = 0, slot = 0; }; diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index e998971..7e8c0bd 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -2287,7 +2287,7 @@ inline void style_panel(Widgets &w,const Tile &t,lv_color_t accent,lv_color_t te } if(w.pill){set_color(w.pill,LV_STYLE_BG_COLOR,key_bg);set_color(w.pill_value,LV_STYLE_TEXT_COLOR,text);} if(w.control_slider){ - bool on=fresh() && t.active(); + bool on=fresh() && t.slider_active(); lv_color_t fill=on?accent:lv_color_hex(0x9E9E9E); // The track is the fill colour at 20 % over the card, as in Home Assistant. set_color(w.control_slider,LV_STYLE_BG_COLOR,lv_color_mix(fill,card,51),LV_PART_MAIN); @@ -2462,8 +2462,10 @@ inline void render_slot(size_t slot) { lap(swipe_profile::CUSTOM); } bool on = fresh() && t.active(); + // Sliders colour more states than the card itself does, such as a playing media player (Tile::slider_active). + bool slider_on = fresh() && t.slider_active(); bool available=fresh() && t.available(); - int palette_state=(available?2:0)|(on?1:0); + int palette_state=(available?2:0)|(on?1:0)|(slider_on?4:0); if (w.cached_active == palette_state && !w.panel_dirty) { lap(swipe_profile::GEOMETRY); return; } w.cached_active = palette_state;w.panel_dirty=false; uint32_t accent = domain_accent(t); @@ -2474,10 +2476,10 @@ inline void render_slot(size_t slot) { // Darken the foreground slightly: very pale bulbs still need a visible icon. auto icon_color=available?lv_color_mix(color,lv_color_hex(0x333333),205):lv_color_hex(0x9E9E9E); // Off: a grey track without fill or handle, as in Home Assistant. - auto fill_color=on?color:lv_color_hex(0x9E9E9E); + auto fill_color=slider_on?color:lv_color_hex(0x9E9E9E); set_color(w.slider,LV_STYLE_BG_COLOR,fill_color,LV_PART_INDICATOR); set_color(w.slider,LV_STYLE_BG_COLOR,lv_color_mix(fill_color,lv_color_hex(0xFFFFFF),51),LV_PART_MAIN); - slider_bar(w.slider,slider_bar_shown(t,on)); + slider_bar(w.slider,slider_bar_shown(t,slider_on)); set_color(w.tile,LV_STYLE_BG_COLOR,lv_color_hex(t.background ? t.background : 0xFFFFFF)); set_number(w.tile,LV_STYLE_BORDER_WIDTH,1); // "Background: none" hides only the card; geometry and padding stay identical, diff --git a/docs/RELEASING.md b/docs/RELEASING.md index f83efeb..3820ccd 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -176,6 +176,19 @@ icons sit off-center in the browser); run `generate_packages.py` afterward. The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual profiles must come from the set. No changed preferences or keys. +### Compatibility 0.2.60 / firmware 0.2.52 + +`components/smart_display` and both board profiles; the app only raises `FIRMWARE_VERSION`. Storage, tile protocol, +preferences and keys are unchanged. + +- `Tile::slider_active()` (runtime_model.h) decides whether a slider shows the tile's colour, after Home Assistant's + `stateActive()` and its tile features: grey for an unavailable entity, a light or fan that is off and a media player + that is `off` or `standby`; a cover keeps its colour when closed (`hui-cover-position-card-feature.ts`) and a number + with a value is active. `style_panel` (sliders of direct controls) and `render_slot` (small sliders, with a third bit + in the palette cache key) use it. `Tile::active()` is unchanged: it still greys the circle of an off switch, person or + timer and gives the value overlay its `was_on`. +- Older firmware keeps the grey sliders; nothing else differs. + ### Compatibility 0.2.59 / firmware 0.2.51 App, both board profiles and `components/smart_display`. Storage, tile protocol, preferences and keys are diff --git a/docs/TEST_RESULTS_0260.md b/docs/TEST_RESULTS_0260.md new file mode 100644 index 0000000..a1b7a7f --- /dev/null +++ b/docs/TEST_RESULTS_0260.md @@ -0,0 +1,50 @@ +# Test results app 0.2.60 / firmware 0.2.52 (2026-09-17) + +Max saw grey sliders for a playing media player's volume and an open cover's position. Since 0.2.42 (a grey slider +end for a light that is off) a slider showed the tile's colour only while `Tile::active()` held, and that rule knows +`on`, `cleaning`, `active`, home, above the horizon and climate not off: never `playing` or `open`, and never a number. +Home Assistant colours tile sliders by `stateActive()` (frontend `src/common/entity/state_active.ts`, read on GitHub): +a media player is active unless off or standby, a cover unless closed, and its cover position feature keeps the open +colour for a closed cover so the slider does not look disabled. Every caller of `active()` was checked (the grey circle +of switches, people and timers, a light's own colour, the palette cache, both sliders, and `was_on` of the value +overlay in both profiles, which covers and media players no longer reach; the top bar and cards don't use it). +Widening `active()` would only have changed slider colours, but the closed cover needs a slider rule anyway, so the fix +is a separate `Tile::slider_active()` and `active()` stays as it was. See CHANGELOG 0.2.60 and docs/RELEASING.md. + +## Automated + +- C++ (`clang++ -std=c++17 -Wall -Wextra -Werror -I. -I components/smart_display`): 15/15 PASS, on HEAD plus only this + change in a separate worktree. `test_runtime_model` adds `test_slider_colours`: a light and a fan on and off; a media + player playing, paused, idle, buffering and on (coloured) against off, standby, unavailable and unknown (grey); a cover + open, opening, closing and closed (coloured) against unavailable; a number and an input_number with a value against + unknown; a tile that has received nothing. The test fails on the old rule (compiled against + `slider_active() { return active(); }`). +- Python (`.venv-portal`): 282 tests OK in that worktree, with the version bump. `generate_packages.py --check` green. + +## Builds (ESPHome 2026.6.2) + +ESP32 check profiles with the generated packages and local components. They were built from the shared working tree, +which also held other sessions' unfinished edits; the version bump alone was not built again. + +| Profile | RAM | Flash | +|---|---|---| +| CYD | 23.5% (77,060 B) | 77.8% (1,427,919 B) | +| Guition | 24.5% (80,436 B) | 18.8% (1,531,471 B) | + +RAM is the same as in 0.2.59. + +## Firmware on the Mac (host builds) + +Guition and CYD host builds with a demo home that runs the real manager, on three pages: a playing Sonos (volume), an +open blind (position) and a boiler setpoint (slider) on double-width tiles; a radio in standby, a closed blind and a +paused speaker; small sliders for an idle speaker, a TV that is off, an open blind, a number, a lamp that is on and a +hallway light that is off. Playing, paused and idle players show blue, blinds purple (the closed one as the short end +at 0 %), numbers teal and the lamp amber; the radio in standby and the TV stay grey, and the light that is off shows +its empty grey track as before. No compiler warnings from the smart_display headers. + +## Not tested / for the owner + +- Real hardware: nothing was flashed. After updating a screen, a playing speaker's volume slider and an open blind's + position slider are coloured, and the speaker's slider turns grey in standby. +- A media player that is off or in standby still shows its grey fill with the handle; Home Assistant hides the handle + there. Left as it is. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index a944e94..fe6f7d5 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -2,7 +2,7 @@ # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.51" + SCREEN_FIRMWARE_VERSION: "0.2.52" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index 3f8f9ab..f779ade 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -17,7 +17,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.51" + SCREEN_FIRMWARE_VERSION: "0.2.52" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. diff --git a/packages/cyd.yaml b/packages/cyd.yaml index e4da43f..d8f7f2e 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -18,7 +18,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.51" + SCREEN_FIRMWARE_VERSION: "0.2.52" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. diff --git a/packages/guition.yaml b/packages/guition.yaml index be85b42..f866a41 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -3,7 +3,7 @@ # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.51" + SCREEN_FIRMWARE_VERSION: "0.2.52" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 4c6a16d..f918df2 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.60 (firmware 0.2.52) + +Sliders keep their colour while a speaker plays or a blind is open. + +- **Coloured sliders for speakers, blinds and numbers.** Since 0.2.42 the volume slider of a playing speaker, the position slider of a blind or curtain and the slider of a number were grey, the colour of a light that is off. Sliders now follow Home Assistant's tile sliders: a speaker that plays, is paused or is idle shows its volume in blue, a cover its position in purple, also when it is closed, and a number its value in teal. A speaker that is off or in standby, and a light or fan that is off, stay grey. This goes for the sliders on double-width tiles and for the small sliders on single tiles. +- Needs firmware 0.2.52: press **Update** on the screen. + ## 0.2.59 (firmware 0.2.51) A history card you can read: axes, an hour, a day or a week, and the value under your finger. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 0554a88..652e94d 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -17,7 +17,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.51' +FIRMWARE_VERSION = '0.2.52' # 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. diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index 644edd9..755f4fc 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.59" +version: "0.2.60" slug: esp_screen_manager description: Choose tiles for your CYD and Guition touchscreen, without reflashing. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_runtime_model.cpp b/tests/test_runtime_model.cpp index 39aa8e3..9110f4a 100644 --- a/tests/test_runtime_model.cpp +++ b/tests/test_runtime_model.cpp @@ -132,3 +132,25 @@ static void test_extra() { assert(m.set_layout({"light.a"}, "Home", changed) && changed && !m.tiles[0].extra_ptr()); } struct RunTileExtra { RunTileExtra() { test_extra(); } } run_tile_extra; +// Sliders keep their colour the way Home Assistant's tile sliders do: grey for an off light or fan and a media +// player that is off or in standby, coloured for a player that plays, a cover open or closed and a number. +static void test_slider_colours() { + using namespace runtime_tiles; + Model m; bool changed = false; + assert(m.set_layout({"light.a", "fan.b", "media_player.c", "cover.d", "number.e", "input_number.f"}, "Home", changed)); + auto &light = m.tiles[0], &fan = m.tiles[1], &player = m.tiles[2], &cover = m.tiles[3], &number = m.tiles[4], &setpoint = m.tiles[5]; + player.state = "playing"; assert(!player.slider_active()); // nothing received yet + for (size_t i = 0; i < m.count; ++i) m.tiles[i].received = true; + light.state = "on"; assert(light.slider_active()); + light.state = "off"; assert(!light.slider_active()); + fan.state = "on"; assert(fan.slider_active()); + fan.state = "off"; assert(!fan.slider_active()); + for (const char *state : {"playing", "paused", "idle", "buffering", "on"}) { player.state = state; assert(player.slider_active()); } + for (const char *state : {"off", "standby", "unavailable", "unknown"}) { player.state = state; assert(!player.slider_active()); } + for (const char *state : {"open", "opening", "closing", "closed"}) { cover.state = state; assert(cover.slider_active()); } + cover.state = "unavailable"; assert(!cover.slider_active()); + number.state = "21.5"; assert(number.slider_active()); + number.state = "unknown"; assert(!number.slider_active()); + setpoint.state = "55.0"; assert(setpoint.slider_active()); +} +struct RunSliderColours { RunSliderColours() { test_slider_colours(); } } run_slider_colours;