Choose vacuum, vac & mop or mop on the vacuum card, and a climate card for the CYD (0.2.46)

App 0.2.46 / firmware 0.2.39:
- The vacuum's state message carries extras from its own device: the cleaning mode and water
  selects (found by translation key, then entity id ending, so localized ids work), the suction
  speeds to offer, the battery sensor (HA 2026.8 dropped battery_level), charging and the current
  room. Speeds and water levels a mode covers move into the mode row; Custom only while in use.
  The manager watches these entities and resends only the vacuum's message when one changes.
- Vacuum card redesign on both boards: robot with state, battery, charging bolt and room; Start
  (Pause, Resume) and Dock; then segmented rows for mode, suction and water, showing only the rows
  the chosen mode uses. Taps show at once and wait for Home Assistant; Locate is a round button in
  the Guition hero.
- CYD climate card without the arc: big setpoint between large -/+ keys (debounced, hold to
  repeat) and one row of mode keys in HA's order and colours, with a more key for fan and swing.
- Both profiles clear climate_edit_pending when the climate card is closed, instead of running
  ui_refresh every second while the flag stayed set.

Tested with the owner's Roborock S8 on HA 2026.9.1 (live data, OTA-flashed bench boards) and with
host renders of both boards; see docs/TEST_RESULTS_0246.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-15 15:22:28 +02:00
parent 4ff0d44312
commit aa2137f3f8
23 changed files with 1675 additions and 655 deletions
+441 -289
View File
@@ -57,7 +57,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.38"
SCREEN_FIRMWARE_VERSION: "0.2.39"
# 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.
@@ -886,6 +886,7 @@ esphome:
light_controls::fallback.maximum = t.max_kelvin;
id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode;
id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode;
id(active_climate_action) = t.hvac_action;
id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0,
t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t),
0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes,
@@ -898,22 +899,17 @@ esphome:
if (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000) return;
id(climate_edit_pending) = false;
id(active_climate_mode) = t.state;
id(active_climate_action) = t.hvac_action;
id(active_climate_supported_modes) = t.hvac_modes;
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(t.state));
id(climate_power_refresh).execute();
id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode;
id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode;
char buf[72];
if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current);
else snprintf(buf, sizeof(buf), "Now: --");
lv_label_set_text(id(climate_current_label), buf);
id(active_climate_current) = t.current;
id(active_climate_humidity) = std::isfinite(t.humidity) ? (int) lround(t.humidity) : -1;
if (std::isfinite(t.target)) {
int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10));
id(active_climate_target_x10) = target;
snprintf(buf, sizeof(buf), "%.1f°", target / 10.0f);
lv_label_set_text(id(climate_target_label), buf);
lv_arc_set_value(id(climate_arc), target);
id(active_climate_target_x10) = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10));
id(active_climate_target_known) = true;
}
id(climate_card_refresh).execute();
}
};
- lambda: |-
@@ -1107,7 +1103,16 @@ interval:
condition:
lambda: 'return id(climate_edit_pending) && !id(touch_down) && millis() - id(climate_edit_ms) >= 3000;'
then:
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
# The edit window is over: show what Home Assistant reports now. A closed card has
# nothing to show, and a flag left set would redraw every tile each second.
- lambda: |-
if (lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { id(climate_edit_pending) = false; return; }
if (!runtime_tiles::enabled) { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); return; }
// Runtime tiles: a state that arrived during the window was held back.
id(climate_edit_pending) = false;
if (runtime_tiles::detail_update && runtime_tiles::active_index >= 0 &&
runtime_tiles::active_index < (int) runtime_tiles::model.count)
runtime_tiles::detail_update(runtime_tiles::model.tiles[runtime_tiles::active_index]);
- if:
condition:
lambda: |-
@@ -1294,6 +1299,22 @@ globals:
type: std::string
restore_value: no
# Room temperature (NAN when unknown), what the device does (hvac_action, runtime tiles only),
# and whether Home Assistant reported a setpoint at all (heat_cool ranges do not).
- id: active_climate_current
type: float
restore_value: no
initial_value: 'NAN'
- id: active_climate_action
type: std::string
restore_value: no
- id: active_climate_target_known
type: bool
restore_value: no
initial_value: 'false'
font:
- file: "fonts/Roboto-500.ttf"
id: time_label
@@ -1328,6 +1349,13 @@ font:
bpp: 4
glyphs: [':','-','0','1','2','3','4','5','6','7','8','9']
# The climate card's setpoint, large enough to read across the room.
- file: "fonts/Roboto-400.ttf"
id: setpoint_digits
size: 40
bpp: 4
glyphs: ['-','.','°','0','1','2','3','4','5','6','7','8','9']
- file: "fonts/Roboto-700.ttf"
id: label
size: 11
@@ -1542,6 +1570,7 @@ font:
- "\U000F0596" # weather-pouring
- "\U000F067F" # weather-snowy-rainy
- "\U000F059B" # weather-sunset-down
- "\U000F01D8" # dots-horizontal
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons_mini
@@ -4168,13 +4197,17 @@ lvgl:
# ---------- CLIMATE DETAIL OVERLAY ----------
# Made for the small resistive panel: nothing to drag. A setpoint card with big -/+ keys
# (the number changes at once, one call goes out once the taps stop) and one row of mode
# keys like Home Assistant's climate card; fan and swing stay in the picker behind "more".
# A tap redraws only a label or a key, never a dial, so the SPI panel keeps up.
- obj:
id: climate_detail_overlay
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
bg_color: 0xF7F7F7
bg_color: 0xE7E7E7
bg_opa: COVER
hidden: true
scrollable: false
@@ -4185,27 +4218,6 @@ lvgl:
pad_bottom: 0
border_width: 0
widgets:
- obj:
id: climate_detail_bg
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
styles: style_bg_grad
scrollable: false
clickable: false
- obj:
id: climate_detail_scrim
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
bg_color: 0xFFFFFF
bg_opa: 62%
border_width: 0
clickable: false
scrollable: false
- obj:
id: climate_close_button
align: TOP_LEFT
@@ -4256,156 +4268,306 @@ lvgl:
- label:
id: climate_detail_title
align: TOP_MID
y: 21
y: 18
width: 210
long_mode: DOT
text_align: CENTER
text: "Climate"
styles: style_room
# What the device does (or its mode), the room temperature and humidity.
- label:
id: climate_current_label
align: TOP_MID
y: 42
text: "Now: --"
y: 54
width: 94%
long_mode: DOT
text_align: CENTER
text: "--"
text_font: sublabel_big
text_color: 0x1B1B1B
text_opa: 85%
text_color: 0x616161
clickable: false
- arc:
id: climate_arc
align: CENTER
y: 20
width: 184
height: 184
# Without this the arc claims an invisible extra
# click area outside the 184x184 box — near the close
# button top-left this creates overlap.
ext_click_area: 0
start_angle: 135
end_angle: 45
min_value: 50
max_value: 300
value: 200
adjustable: true
arc_width: 20
arc_rounded: true
arc_color: 0x4B4B4F
arc_opa: 80%
- obj:
id: climate_setpoint_card
align: TOP_MID
y: 80
width: 94%
height: 92
radius: 16
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: false
scrollable: false
widgets:
# Every clean tap counts (a quick triple tap is three steps); holding keeps stepping.
- obj:
id: climate_minus_button
align: LEFT_MID
x: 10
width: 72
height: 72
radius: 36
bg_color: 0xF1F1F1
bg_opa: COVER
border_width: 0
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xD9D9D9
disabled:
opa: 40%
on_short_click:
- if:
condition:
lambda: 'return cyd::touch_guard.accept_repeat(millis(), 60);'
then:
- script.execute: { id: climate_adjust_target, direction: -1, held: false }
on_long_press_repeat:
- script.execute: { id: climate_adjust_target, direction: -1, held: true }
widgets:
- label:
id: climate_minus_icon
align: CENTER
text: "\U000F0374"
text_font: materialdesign_icons
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_plus_button
align: RIGHT_MID
x: -10
width: 72
height: 72
radius: 36
bg_color: 0xF1F1F1
bg_opa: COVER
border_width: 0
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xD9D9D9
disabled:
opa: 40%
on_short_click:
- if:
condition:
lambda: 'return cyd::touch_guard.accept_repeat(millis(), 61);'
then:
- script.execute: { id: climate_adjust_target, direction: 1, held: false }
on_long_press_repeat:
- script.execute: { id: climate_adjust_target, direction: 1, held: true }
widgets:
- label:
id: climate_plus_icon
align: CENTER
text: "\U000F0415"
text_font: materialdesign_icons
text_color: 0x1B1B1B
clickable: false
- label:
id: climate_target_label
align: CENTER
y: -8
text: "--"
text_font: setpoint_digits
text_color: 0x1B1B1B
clickable: false
- label:
id: climate_set_label
align: CENTER
y: 26
text: "Target"
text_font: sublabel
text_color: 0x6B6B6B
clickable: false
# One key per mode the device supports (off is the power key); the one in use is
# filled in Home Assistant's colour for it. "More" opens the picker with fan and swing.
- obj:
id: climate_mode_keys
align: TOP_MID
y: 184
width: 94%
height: 46
bg_opa: TRANSP
border_width: 0
indicator:
arc_color: 0xFEC600
arc_opa: COVER
arc_width: 20
arc_rounded: true
knob:
bg_color: 0x111111
bg_opa: COVER
border_width: 0
shadow_width: 0
on_value:
- script.execute:
id: climate_target_preview
value: !lambda "return x;"
on_release:
- script.execute:
id: climate_target_commit
value: !lambda "return lv_arc_get_value(id(climate_arc));"
- label:
id: climate_set_label
align: CENTER
y: -16
text: "Target"
text_font: sublabel_big
text_color: 0x1B1B1B
text_opa: 68%
pad_all: 0
clickable: false
- label:
id: climate_target_label
align: CENTER
y: 20
text: "20.0°"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_minus_button
align: BOTTOM_LEFT
x: 38
y: -16
width: 48
height: 36
radius: 18
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 16%
clickable: true
scrollable: false
on_click:
- script.execute:
id: climate_adjust_target
direction: -1
layout:
type: FLEX
flex_flow: ROW
flex_align_main: CENTER
flex_align_cross: CENTER
pad_column: 6
widgets:
- label:
align: CENTER
text: "-"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_plus_button
align: BOTTOM_RIGHT
x: -38
y: -16
width: 48
height: 36
radius: 18
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 16%
clickable: true
scrollable: false
on_click:
- script.execute:
id: climate_adjust_target
direction: 1
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_mode_pill
align: BOTTOM_MID
y: -18
width: 126
height: 34
radius: 17
bg_color: 0xF0F0F0
bg_opa: 82%
border_width: 0
clickable: true
scrollable: false
on_click:
- script.execute: open_climate_mode_picker
widgets:
- label:
id: climate_mode_label
align: CENTER
text: "Auto"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmkey_auto
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "auto" }
widgets:
- label:
align: CENTER
text: "\U000F1B17"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_heat_cool
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "heat_cool" }
widgets:
- label:
align: CENTER
text: "\U000F1A79"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_heat
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "heat" }
widgets:
- label:
align: CENTER
text: "\U000F0238"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_cool
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "cool" }
widgets:
- label:
align: CENTER
text: "\U000F0717"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_dry
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "dry" }
widgets:
- label:
align: CENTER
text: "\U000F058E"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_fan_only
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: { id: set_climate_mode, mode: "fan_only" }
widgets:
- label:
align: CENTER
text: "\U000F0210"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
- obj:
id: cmkey_more
flex_grow: 1
height: 46
radius: 23
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0xEEEEEE
on_click:
- script.execute: open_climate_mode_picker
widgets:
- label:
align: CENTER
text: "\U000F01D8"
text_font: materialdesign_icons
text_color: 0x46525E
clickable: false
# ---------- CLIMATE MODE PICKER ----------
# Replaces cycling through modes. Cycling sends a set_hvac_mode on every
# tap, so to get from 'cool' to 'dry' a heat pump actually passes through
@@ -5490,55 +5652,106 @@ script:
id: smartdisplay_action
state: ""
- id: climate_target_preview
# A -/+ tap on the climate card: the number follows at once, the call waits for the last tap.
# Holding a key steps three times a second.
- id: climate_adjust_target
parameters:
value: int
direction: int
held: bool
then:
- lambda: |-
if (held && millis() - id(climate_edit_ms) < 300) return;
int step_x10 = std::max(5, id(active_climate_step_x10));
int next = cyd::quantize_temperature(id(active_climate_target_x10) + direction * step_x10,
id(active_climate_min_x10), id(active_climate_max_x10), step_x10);
id(climate_edit_ms) = millis();
id(climate_edit_pending) = true;
int min_x10 = id(active_climate_min_x10);
int max_x10 = id(active_climate_max_x10);
value = cyd::quantize_temperature(value, min_x10, max_x10,
id(active_climate_step_x10));
id(active_climate_target_x10) = value;
if (next == id(active_climate_target_x10) && id(active_climate_target_known)) return;
id(active_climate_target_x10) = next;
id(active_climate_target_known) = true;
id(climate_card_refresh).execute();
id(climate_target_send).execute(id(active_entity), next / 10.0f);
char buf[16];
snprintf(buf, sizeof(buf), "%.1f°", value / 10.0f);
lv_label_set_text(id(climate_target_label), buf);
lv_arc_set_value(id(climate_arc), value);
- id: climate_target_commit
# One set_temperature for a series of taps: every tap restarts the wait with the newest value.
# Entity and value travel along, so leaving the card right after a tap still sends it.
- id: climate_target_send
mode: restart
parameters:
value: int
entity: string
value: float
then:
- script.execute:
id: climate_target_preview
value: !lambda "return value;"
- delay: 700ms
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true";'
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && !entity.empty();'
then:
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: !lambda "return id(active_entity);"
temperature: !lambda "return id(active_climate_target_x10) / 10.0f;"
entity_id: !lambda "return entity;"
temperature: !lambda "return value;"
- id: climate_adjust_target
parameters:
direction: int
# Draws the climate card from the active_climate_* globals: status line, setpoint, -/+ and mode keys.
- id: climate_card_refresh
then:
- lambda: |-
int step_x10 = id(active_climate_step_x10);
if (step_x10 < 5) step_x10 = 5;
int next = id(active_climate_target_x10) + (direction * step_x10);
if (next < id(active_climate_min_x10)) next = id(active_climate_min_x10);
if (next > id(active_climate_max_x10)) next = id(active_climate_max_x10);
id(active_climate_target_x10) = next;
- script.execute:
id: climate_target_commit
value: !lambda "return id(active_climate_target_x10);"
auto lower = [](std::string v) {
for (char &c : v) { if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); }
return v;
};
std::string mode = lower(id(active_climate_mode));
bool off = mode == "off" || mode.empty() || mode == "unknown" || mode == "unavailable";
// What the device does (Heating), else its mode (Heat); then the room.
std::string status = off ? std::string("Off") : std::string(tile_controls::climate_action_text(lower(id(active_climate_action))));
if (status.empty()) status = tile_controls::climate_mode_text(mode);
char part[32];
if (std::isfinite(id(active_climate_current))) { snprintf(part, sizeof(part), " · Now %.1f°", id(active_climate_current)); status += part; }
if (id(active_climate_humidity) >= 0) { snprintf(part, sizeof(part), " · %d%%", id(active_climate_humidity)); status += part; }
lv_label_set_text(id(climate_current_label), status.c_str());
int target = id(active_climate_target_x10);
std::string value = id(active_climate_target_known)
? tile_controls::format_value(target / 10.0f, id(active_climate_step_x10) / 10.0f, "°") : std::string("--");
lv_label_set_text(id(climate_target_label), value.c_str());
lv_obj_set_style_text_color(id(climate_target_label), lv_color_hex(off ? 0x9E9E9E : 0x1B1B1B), LV_PART_MAIN);
auto enable = [](lv_obj_t *key, bool on) {
if (on) lv_obj_remove_state(key, LV_STATE_DISABLED); else lv_obj_add_state(key, LV_STATE_DISABLED);
};
enable(id(climate_minus_button), !id(active_climate_target_known) || target > id(active_climate_min_x10));
enable(id(climate_plus_button), !id(active_climate_target_known) || target < id(active_climate_max_x10));
// One key per supported mode, in Home Assistant's order. Fan and swing add "more"; with it
// five mode keys fit, and when a sixth mode is in use it takes the last place.
std::string supported = lower(id(active_climate_supported_modes));
auto has = [&](const char *m) {
return supported.find(std::string("\"") + m + "\"") != std::string::npos ||
supported.find(std::string("'") + m + "'") != std::string::npos;
};
struct Key { lv_obj_t *obj; const char *mode; };
const Key keys[] = {{id(cmkey_auto), "auto"}, {id(cmkey_heat_cool), "heat_cool"}, {id(cmkey_heat), "heat"},
{id(cmkey_cool), "cool"}, {id(cmkey_dry), "dry"}, {id(cmkey_fan_only), "fan_only"}};
bool more = !id(active_climate_fan_modes).empty() || !id(active_climate_swing_modes).empty();
const Key *visible[6];
unsigned count = 0, room = more ? 5 : 6;
for (const auto &k : keys) if (has(k.mode)) visible[count++] = &k;
if (count > room) {
for (unsigned i = room; i < count; ++i) if (mode == visible[i]->mode) visible[room - 1] = visible[i];
count = room;
}
for (const auto &k : keys) {
bool show = false;
for (unsigned i = 0; i < count; ++i) show = show || visible[i] == &k;
if (show) lv_obj_remove_flag(k.obj, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(k.obj, LV_OBJ_FLAG_HIDDEN);
bool selected = show && !off && mode == k.mode;
uint32_t color = tile_controls::mode_color(k.mode);
lv_obj_set_style_bg_color(k.obj, lv_color_hex(selected ? color : 0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_border_color(k.obj, lv_color_hex(selected ? color : 0xDDDDDD), LV_PART_MAIN);
lv_obj_set_style_text_color(lv_obj_get_child(k.obj, 0), lv_color_hex(selected ? 0xFFFFFF : 0x46525E), LV_PART_MAIN);
}
if (more) lv_obj_remove_flag(id(cmkey_more), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(cmkey_more), LV_OBJ_FLAG_HIDDEN);
if (count || more) lv_obj_remove_flag(id(climate_mode_keys), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(climate_mode_keys), LV_OBJ_FLAG_HIDDEN);
id(climate_power_refresh).execute();
# Opens the mode picker. Hides modes the entity doesn't support and marks
# the current one, so you see where you stand before you pick something.
@@ -5688,8 +5901,8 @@ script:
id(active_climate_power_on) = mode == "off" || mode.empty() || mode == "unknown";
id(climate_edit_ms) = millis();
id(climate_edit_pending) = true;
if (!id(active_climate_power_on)) { id(active_climate_mode) = "off"; lv_label_set_text(id(climate_mode_label), "Off"); }
id(climate_power_refresh).execute();
if (!id(active_climate_power_on)) id(active_climate_mode) = "off";
id(climate_card_refresh).execute();
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && id(active_climate_power_on);'
@@ -5728,9 +5941,10 @@ script:
id(climate_edit_ms) = millis();
id(climate_edit_pending) = true;
id(active_climate_mode) = mode;
// The action line would still say what the old mode did.
id(active_climate_action).clear();
id(active_climate_power_on) = mode != "off";
id(climate_power_refresh).execute();
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(mode));
id(climate_card_refresh).execute();
- lvgl.widget.hide: climate_mode_overlay
- if:
condition:
@@ -6285,6 +6499,8 @@ script:
}
id(active_climate_current_x10) = to_x10(climate_current_temperature, 0);
id(active_climate_current) = climate_current_temperature;
id(active_climate_target_known) = !isnan(climate_target_temperature);
int fallback_target = isnan(climate_current_temperature) ? 200 : id(active_climate_current_x10);
id(active_climate_target_x10) = to_x10(climate_target_temperature, fallback_target);
if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10);
@@ -6297,41 +6513,19 @@ script:
id(active_climate_supported_modes) = climate_supported_modes;
lv_label_set_text(id(climate_detail_title), title.c_str());
char current_buf[48];
if (isnan(climate_current_temperature)) {
snprintf(current_buf, sizeof(current_buf), "Now: --");
} else if (id(active_climate_humidity) >= 0) {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity));
} else {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature);
}
lv_label_set_text(id(climate_current_label), current_buf);
char target_buf[16];
snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f);
lv_label_set_text(id(climate_target_label), target_buf);
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(id(active_climate_mode)));
id(climate_power_refresh).execute();
lv_arc_set_range(id(climate_arc), id(active_climate_min_x10), id(active_climate_max_x10));
lv_arc_set_value(id(climate_arc), id(active_climate_target_x10));
// A portrait panel (240 x 320) keeps the same card, a little lower and with more air.
if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) {
lv_obj_set_size(id(climate_arc), 196, 196);
lv_obj_set_y(id(climate_arc), 8);
lv_obj_set_y(id(climate_current_label), 48);
lv_obj_set_y(id(climate_minus_button), -62);
lv_obj_set_y(id(climate_plus_button), -62);
lv_obj_set_y(id(climate_mode_pill), -18);
} else {
lv_obj_set_size(id(climate_arc), 176, 176);
lv_obj_set_y(id(climate_arc), 18);
lv_obj_set_y(id(climate_current_label), 40);
lv_obj_set_y(id(climate_minus_button), -14);
lv_obj_set_y(id(climate_plus_button), -14);
lv_obj_set_y(id(climate_mode_pill), -15);
lv_obj_set_y(id(climate_current_label), 60);
lv_obj_set_y(id(climate_setpoint_card), 96);
lv_obj_set_y(id(climate_mode_keys), 222);
for (lv_obj_t *key : {id(climate_minus_button), id(climate_plus_button)}) {
lv_obj_set_size(key, 56, 56);
lv_obj_set_style_radius(key, 28, LV_PART_MAIN);
}
lv_obj_set_x(id(climate_minus_button), 6);
lv_obj_set_x(id(climate_plus_button), -6);
}
id(climate_card_refresh).execute();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -6657,66 +6851,24 @@ script:
if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable")
set_lbl(id(t10_value), "Unavailable");
if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) {
auto label_for_climate_mode = [](std::string mode) {
for (char &c : mode) {
if (c >= 'A' && c <= 'Z') c = (char) (c - 'A' + 'a');
}
if (mode == "off") return std::string("Off");
if (mode == "heat") return std::string("Heat");
if (mode == "cool") return std::string("Cool");
if (mode == "heat_cool") return std::string("Heat/Cool");
if (mode == "auto") return std::string("Auto");
if (mode == "dry") return std::string("Dry");
if (mode == "fan_only") return std::string("Fan");
return mode.empty() ? std::string("Auto") : mode;
};
auto sync_open_climate = [&](const std::string& entity,
float current_temperature,
float target_temperature,
float humidity,
const std::string& climate_mode) {
if (id(active_entity) != entity) return;
auto to_x10 = [](float value, int fallback) -> int {
if (isnan(value)) return fallback;
return (int) lround(value * 10.0f);
};
id(active_climate_current_x10) = to_x10(current_temperature, id(active_climate_current_x10));
id(active_climate_current) = current_temperature;
id(active_climate_humidity) = isnan(humidity) ? -1 : (int) lround(humidity);
if (!climate_mode.empty() && !id(touch_down) &&
!(id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000)) {
id(active_climate_mode) = climate_mode;
lv_label_set_text(id(climate_mode_label), label_for_climate_mode(climate_mode).c_str());
}
const bool editing = id(touch_down) ||
(id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000);
if (!editing) id(climate_edit_pending) = false;
int target_x10 = editing ? id(active_climate_target_x10) :
to_x10(target_temperature, id(active_climate_target_x10));
if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10);
if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10);
int step_x10 = id(active_climate_step_x10);
if (step_x10 < 5) step_x10 = 5;
target_x10 = id(active_climate_min_x10) + (int) lround((target_x10 - id(active_climate_min_x10)) / (float) step_x10) * step_x10;
if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10);
if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10);
id(active_climate_target_x10) = target_x10;
char current_buf[48];
if (isnan(current_temperature)) {
snprintf(current_buf, sizeof(current_buf), "Now: --");
} else if (id(active_climate_humidity) >= 0) {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity));
} else {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature);
if (!climate_mode.empty() && !editing) id(active_climate_mode) = climate_mode;
if (!editing && !isnan(target_temperature)) {
id(active_climate_target_x10) = cyd::quantize_temperature((int) lround(target_temperature * 10.0f),
id(active_climate_min_x10), id(active_climate_max_x10), std::max(5, id(active_climate_step_x10)));
id(active_climate_target_known) = true;
}
lv_label_set_text(id(climate_current_label), current_buf);
char target_buf[16];
snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f);
lv_label_set_text(id(climate_target_label), target_buf);
lv_arc_set_value(id(climate_arc), id(active_climate_target_x10));
id(climate_card_refresh).execute();
};
if (std::string("${TILE1_TYPE}") == "climate") sync_open_climate("${TILE1_ENTITY}", id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state, std::string(id(tile1_climate_mode).state.c_str()));