Add a card for blinds and curtains, and fix what Studio 1 showed after 0.2.57 (0.2.58)

A tap on a cover now opens a card like Home Assistant's own, in the style
of the climate and vacuum cards: a tall position slider on which the blind
hangs from the top, a tilt slider over slats for venetian blinds, and open,
stop and close keys. The key of the direction the cover moves is filled; a
key that cannot move it further is disabled and stays out of the keys the
card's tick enables. Sliders show their value while dragged and send it on
release. The card offers only what supported_features allows. A
battery-powered blind shows its battery at the top right: the app sends
the device's battery sensor as the extra the vacuum card uses, and adds
current_tilt_position to the attributes.

Checking Studio 1 after its update to firmware 0.2.49 found that a setting
number equal to its initial state 0 (night brightness 0 %) was never
published, so Home Assistant kept it unknown and ESP Screens could not
change it; apply_screen_settings now publishes a number without a state.
The app also took "unavailable" for the device name during the restart
and wrote sensor.esp_screens_unavailable; diagnostics ignore unknown and
unavailable. The editor redraws its tiles, top bar and entity list when
the live stream opened a screen before the first full inventory (a busy
add-on), and loads app.js and style.css by content stamp after Safari kept
an old script.

Checked with 254 Python and 14 C++ tests, generate_packages --check, ESP32
builds of both profiles, and 20 API checks of the card on host builds of
both boards. Firmware 0.2.50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-16 16:41:10 +02:00
parent b58f0cde25
commit d6e5b35dcf
24 changed files with 696 additions and 69 deletions
+13 -8
View File
@@ -18,7 +18,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.49"
SCREEN_FIRMWARE_VERSION: "0.2.50"
# 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.
@@ -499,8 +499,9 @@ api:
runtime_tiles::active_index = index;
auto &tile = runtime_tiles::model.tiles[index];
auto domain = tile.domain();
// Lights, fans, covers and climate open the board's own card, like a long press does.
if ((domain == "light" || domain == "fan" || domain == "cover" || domain == "climate") && runtime_tiles::detail) {
// Lights, fans and climate open the board's own card, like a long press does; covers (firmware 0.2.50+)
// open the runtime card, as a tap does.
if ((domain == "light" || domain == "fan" || domain == "climate") && runtime_tiles::detail) {
tile.begin(millis(), true);
runtime_tiles::detail(tile);
return;
@@ -3691,11 +3692,15 @@ script:
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
// The setting numbers and times only publish a changed value (HA keeps identical states anyway); the
// switches publish their own changes. An open settings page shows a change from Home Assistant too.
if (id(setting_brightness).state != settings.brightness) id(setting_brightness).publish_state(settings.brightness);
if (id(setting_standby_brightness).state != settings.standby_brightness) id(setting_standby_brightness).publish_state(settings.standby_brightness);
if (id(setting_night_brightness).state != settings.night_brightness) id(setting_night_brightness).publish_state(settings.night_brightness);
if (id(setting_standby_seconds).state != settings.standby_seconds) id(setting_standby_seconds).publish_state(settings.standby_seconds);
if (id(setting_auto_home_seconds).state != settings_screen::auto_home_seconds) id(setting_auto_home_seconds).publish_state(settings_screen::auto_home_seconds);
// A number without a state publishes too: a night brightness of 0 equals the number's initial state and
// stayed "unknown" in Home Assistant before firmware 0.2.50.
for (auto [entity, value] : {std::make_pair(id(setting_brightness), settings.brightness),
std::make_pair(id(setting_standby_brightness), settings.standby_brightness),
std::make_pair(id(setting_night_brightness), settings.night_brightness),
std::make_pair(id(setting_standby_seconds), settings.standby_seconds),
std::make_pair(id(setting_auto_home_seconds), settings_screen::auto_home_seconds)}) {
if (!entity->has_state() || entity->state != value) entity->publish_state(value);
}
for (auto [entity, minutes] : {std::make_pair(id(setting_night_start), settings.night_start), std::make_pair(id(setting_night_end), settings.night_end)}) {
if (!entity->has_state() || entity->hour * 60 + entity->minute != minutes) entity->update();
}
+13 -8
View File
@@ -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.49"
SCREEN_FIRMWARE_VERSION: "0.2.50"
# 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"
@@ -382,8 +382,9 @@ api:
runtime_tiles::active_index = index;
auto &tile = runtime_tiles::model.tiles[index];
auto domain = tile.domain();
// Lights, fans, covers and climate open the board's own card, like a long press does.
if ((domain == "light" || domain == "fan" || domain == "cover" || domain == "climate") && runtime_tiles::detail) {
// Lights, fans and climate open the board's own card, like a long press does; covers (firmware 0.2.50+)
// open the runtime card, as a tap does.
if ((domain == "light" || domain == "fan" || domain == "climate") && runtime_tiles::detail) {
tile.begin(millis(), true);
runtime_tiles::detail(tile);
return;
@@ -3685,11 +3686,15 @@ script:
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
// The setting numbers and times only publish a changed value (HA keeps identical states anyway); the
// switches publish their own changes. An open settings page shows a change from Home Assistant too.
if (id(setting_brightness).state != settings.brightness) id(setting_brightness).publish_state(settings.brightness);
if (id(setting_standby_brightness).state != settings.standby_brightness) id(setting_standby_brightness).publish_state(settings.standby_brightness);
if (id(setting_night_brightness).state != settings.night_brightness) id(setting_night_brightness).publish_state(settings.night_brightness);
if (id(setting_standby_seconds).state != settings.standby_seconds) id(setting_standby_seconds).publish_state(settings.standby_seconds);
if (id(setting_auto_home_seconds).state != settings_screen::auto_home_seconds) id(setting_auto_home_seconds).publish_state(settings_screen::auto_home_seconds);
// A number without a state publishes too: a night brightness of 0 equals the number's initial state and
// stayed "unknown" in Home Assistant before firmware 0.2.50.
for (auto [entity, value] : {std::make_pair(id(setting_brightness), settings.brightness),
std::make_pair(id(setting_standby_brightness), settings.standby_brightness),
std::make_pair(id(setting_night_brightness), settings.night_brightness),
std::make_pair(id(setting_standby_seconds), settings.standby_seconds),
std::make_pair(id(setting_auto_home_seconds), settings_screen::auto_home_seconds)}) {
if (!entity->has_state() || entity->state != value) entity->publish_state(value);
}
for (auto [entity, minutes] : {std::make_pair(id(setting_night_start), settings.night_start), std::make_pair(id(setting_night_end), settings.night_end)}) {
if (!entity->has_state() || entity->hour * 60 + entity->minute != minutes) entity->update();
}