Bring the big climate card to the Guition, with fan and swing on their own card (0.2.47)
App 0.2.47 / firmware 0.2.40: - guition-4848s040.yaml gets the CYD's climate card: the setpoint between large -/+ keys (debounced, hold to repeat) and one row of mode keys in HA's order and colours; the arc, scrim, mode pill and climate_number font are gone. - Fan and swing modes sit right on the card as segmented rows behind their icons, drawn by the new climate_card.h (async rebuild, taps through the touch guard and a profile hook). The card is laid out per case from one table: no, one or two settings rows, with or without mode keys. - Both boards hide a mode key row that would hold a single mode and centre the setpoint instead. - Glyph arrow-oscillating (Home Assistant's swing icon) joins the icon set. Verified on the Guition over OTA with the owner's air conditioner and with host renders of both boards; see docs/TEST_RESULTS_0247.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,8 @@ The firmware and built-in CLI are tested with **ESPHome 2026.6.2**.
|
||||
per tile; **None** keeps the regular card.
|
||||
- **Weather card** with current weather, the coming hours and days including chance of
|
||||
rain or millimeters; **climate card** with an on/off button, mode, fan, and swing settings.
|
||||
On the CYD the climate card has big − / + keys and one row of mode keys instead of a dial.
|
||||
The target temperature sits big between − / + keys with one row of mode keys below; the
|
||||
Guition shows fan and swing right away on a card of their own, the CYD behind ···.
|
||||
- **Vacuum card** with the state, battery and charging, start and dock, and how the robot
|
||||
cleans: **vacuum, vacuum and mop, or mop only** for robots that offer a cleaning mode in
|
||||
Home Assistant (such as Roborock), then suction and water. Only the rows the chosen mode
|
||||
@@ -100,7 +101,7 @@ The firmware and built-in CLI are tested with **ESPHome 2026.6.2**.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/guition-weather.png" width="32%" alt="Weather card: current weather, the coming hours and the coming days with chance of rain">
|
||||
<img src="docs/images/guition-climate.png" width="32%" alt="Climate card: target temperature dial, current temperature and humidity, and the mode">
|
||||
<img src="docs/images/guition-climate.png" width="32%" alt="Climate card: the target temperature between big minus and plus keys, the mode keys, and fan and swing choices">
|
||||
<img src="docs/images/guition-light.png" width="32%" alt="Light control: color, color temperature and brightness">
|
||||
</p>
|
||||
<p align="center">
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#pragma once
|
||||
// Fan and swing modes on the Guition's climate card (firmware 0.2.40+): one white card below the mode
|
||||
// keys with a segmented row per setting, each behind its icon, where the CYD keeps them behind "more".
|
||||
// The board profile owns the card and its scripts; this draws the rows into the card and reports a tap
|
||||
// as the setting ('f' fan, 's' swing) and the index in the entity's own list.
|
||||
#include "cyd_ui.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "lvgl.h"
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace climate_card {
|
||||
constexpr unsigned MAX_CHOICES = 6;
|
||||
struct Row { char kind = 0; const char *icon = ""; std::string modes, current; };
|
||||
struct Look {
|
||||
lv_obj_t *card = nullptr;
|
||||
const lv_font_t *font = nullptr, *icon_font = nullptr;
|
||||
int row_h = 44, gap = 10, edge = 14, icon_w = 42;
|
||||
};
|
||||
inline Look look;
|
||||
inline std::function<void(char, int)> choose;
|
||||
inline Row rows[2];
|
||||
inline int row_count = 0;
|
||||
inline bool queued = false;
|
||||
|
||||
// Home Assistant's raw mode names read as words: "fan_only" -> "Fan only", "high" -> "High".
|
||||
inline std::string pretty(std::string raw) {
|
||||
for (char &c : raw) if (c == '_') c = ' ';
|
||||
if (!raw.empty() && raw[0] >= 'a' && raw[0] <= 'z') raw[0] = static_cast<char>(raw[0] - 'a' + 'A');
|
||||
return raw;
|
||||
}
|
||||
inline unsigned choices(const std::string &modes) {
|
||||
unsigned n = 0;
|
||||
while (n < MAX_CHOICES && !cyd::list_item(modes, n).empty()) ++n;
|
||||
return n;
|
||||
}
|
||||
// Height of the card for `count` rows, so the profile can place the rest of the card before it is drawn.
|
||||
inline int height(int count) { return count ? 2 * look.edge + count * look.row_h + (count - 1) * look.gap : 0; }
|
||||
inline int card_width() {
|
||||
// The card is 94 % wide, like the setpoint card above it.
|
||||
return lv_display_get_horizontal_resolution(lv_display_get_default()) * 94 / 100;
|
||||
}
|
||||
inline int text_width(const std::string &text, const lv_font_t *font) {
|
||||
lv_point_t size;
|
||||
lv_text_get_size(&size, text.c_str(), font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
|
||||
return size.x;
|
||||
}
|
||||
inline lv_obj_t *shape(lv_obj_t *parent, int x, int y, int w, int h, uint32_t color, int radius) {
|
||||
auto *o = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(o);
|
||||
lv_obj_set_pos(o, x, y);
|
||||
lv_obj_set_size(o, w, h);
|
||||
lv_obj_set_style_radius(o, radius, 0);
|
||||
lv_obj_set_style_bg_color(o, lv_color_hex(color), 0);
|
||||
lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0);
|
||||
lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE);
|
||||
return o;
|
||||
}
|
||||
inline void tapped(lv_event_t *e) {
|
||||
const auto code = static_cast<unsigned>(reinterpret_cast<uintptr_t>(lv_event_get_user_data(e)));
|
||||
const char kind = static_cast<char>(code >> 8);
|
||||
const int index = static_cast<int>(code & 0xFF);
|
||||
if (!cyd::touch_guard.accept(esphome::millis(), 500 + static_cast<int>(code & 0xFFF))) {
|
||||
ESP_LOGI("touch", "tap on climate %c row ignored: %s", kind, cyd::touch_guard.reason().c_str());
|
||||
return;
|
||||
}
|
||||
if (choose) choose(kind, index);
|
||||
}
|
||||
// Draws the rows: an icon, then one rounded track with every choice as wide as its word plus an equal
|
||||
// share of the room left; the chosen one is filled in the cards' blue.
|
||||
inline void build() {
|
||||
queued = false;
|
||||
auto *card = look.card;
|
||||
if (!card || !look.font || !look.icon_font) return;
|
||||
lv_obj_clean(card);
|
||||
if (!row_count) { lv_obj_add_flag(card, LV_OBJ_FLAG_HIDDEN); return; }
|
||||
lv_obj_set_height(card, height(row_count));
|
||||
lv_obj_remove_flag(card, LV_OBJ_FLAG_HIDDEN);
|
||||
const int width = card_width(), track_x = look.edge + look.icon_w, track_w = width - 2 * look.edge - look.icon_w;
|
||||
const int inset = std::max(3, look.row_h / 12), room = track_w - 2 * inset, icon_h = lv_font_get_line_height(look.icon_font);
|
||||
int y = look.edge;
|
||||
for (int r = 0; r < row_count; ++r) {
|
||||
const Row &row = rows[r];
|
||||
auto *icon = lv_label_create(card);
|
||||
lv_label_set_text(icon, row.icon);
|
||||
lv_obj_set_style_text_font(icon, look.icon_font, 0);
|
||||
lv_obj_set_style_text_color(icon, lv_color_hex(0x6B6B6B), 0);
|
||||
lv_obj_set_pos(icon, look.edge, y + (look.row_h - icon_h) / 2);
|
||||
shape(card, track_x, y, track_w, look.row_h, 0xF1F1F1, look.row_h / 2);
|
||||
const unsigned n = choices(row.modes);
|
||||
std::string labels[MAX_CHOICES];
|
||||
int widths[MAX_CHOICES], words = 0;
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
labels[i] = pretty(cyd::list_item(row.modes, i));
|
||||
widths[i] = text_width(labels[i], look.font);
|
||||
words += widths[i];
|
||||
}
|
||||
const int share = n ? (room - words) / static_cast<int>(n) : 0;
|
||||
int sx = track_x + inset;
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
const int sw = i + 1 == n ? track_x + track_w - inset - sx : (words <= room ? widths[i] + share : room / static_cast<int>(n));
|
||||
const bool selected = cyd::list_item(row.modes, i) == row.current;
|
||||
const int sh = look.row_h - 2 * inset;
|
||||
auto *segment = shape(card, sx, y + inset, sw, sh, 0x009FE3, sh / 2);
|
||||
lv_obj_add_flag(segment, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_bg_opa(segment, selected ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_bg_opa(segment, LV_OPA_COVER, LV_STATE_PRESSED);
|
||||
lv_obj_set_style_bg_color(segment, lv_color_hex(selected ? 0x0075B0 : 0xD5EEFC), LV_STATE_PRESSED);
|
||||
auto *label = lv_label_create(segment);
|
||||
lv_label_set_text(label, labels[i].c_str());
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_style_text_font(label, look.font, 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(selected ? 0xFFFFFF : 0x1B1B1B), 0);
|
||||
lv_obj_set_width(label, std::min(sw - 6, widths[i] + 2));
|
||||
lv_obj_remove_flag(label, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_center(label);
|
||||
const uintptr_t code = (static_cast<uintptr_t>(static_cast<unsigned char>(row.kind)) << 8) | i;
|
||||
lv_obj_add_event_cb(segment, tapped, LV_EVENT_SHORT_CLICKED, reinterpret_cast<void *>(code));
|
||||
sx += sw;
|
||||
}
|
||||
y += look.row_h + look.gap;
|
||||
}
|
||||
}
|
||||
// Called from the card's refresh, also while a row's own tap is still being handled: the rows are drawn
|
||||
// once LVGL has finished that event, never under the finger that is lifting.
|
||||
inline void show(const Row *list, int count) {
|
||||
row_count = std::clamp(count, 0, 2);
|
||||
for (int i = 0; i < row_count; ++i) rows[i] = list[i];
|
||||
if (queued) return;
|
||||
queued = true;
|
||||
lv_async_call([](void *) { build(); }, nullptr);
|
||||
}
|
||||
} // namespace climate_card
|
||||
@@ -186,6 +186,7 @@ inline constexpr Named NAMES[] = {
|
||||
{"weather-snowy-rainy", 0xF067F},
|
||||
{"weather-sunset-down", 0xF059B},
|
||||
{"dots-horizontal", 0xF01D8},
|
||||
{"arrow-oscillating", 0xF1C91},
|
||||
};
|
||||
inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);
|
||||
// Codepoint of a name the fonts carry; 0 for any other name.
|
||||
|
||||
@@ -176,6 +176,25 @@ 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.47 / firmware 0.2.40
|
||||
|
||||
Firmware and board profiles; the app only raises `FIRMWARE_VERSION`. Storage, protocol, preferences and
|
||||
keys are unchanged.
|
||||
|
||||
- guition-4848s040.yaml gets the CYD's climate card (0.2.46): the arc, `climate_detail_bg`/`_scrim`,
|
||||
`climate_mode_pill` and the `climate_number` font are gone; `setpoint_digits` is Roboto 400 at 64 px.
|
||||
The scripts `climate_adjust_target`, `climate_target_send` and `climate_card_refresh` match the CYD,
|
||||
except that the Guition's refresh hides the ··· key and lays the card out per case (no, one or two
|
||||
settings rows, with or without mode keys) from one table, applied only when that case changes.
|
||||
- New `components/smart_display/climate_card.h`: `climate_card::show(rows, count)` draws the fan
|
||||
('f') and swing ('s') rows into `climate_settings_card` through `lv_async_call`, so a row can redraw
|
||||
itself from its own tap; taps go through `cyd::touch_guard` and the profile's `climate_card::choose`
|
||||
hook, which runs `set_climate_fan_mode` / `set_climate_swing_mode` with the index in the entity's list
|
||||
(`cyd::list_item`). At most six choices per row. Both scripts now refresh the card at once.
|
||||
- Both profiles hide the mode key row when a device offers at most one mode besides off (and no ···),
|
||||
and centre the setpoint card instead.
|
||||
- Glyph `arrow-oscillating` (F1C91), Home Assistant's swing icon, joins `tile_icons.FIXED`.
|
||||
|
||||
### Compatibility 0.2.46 / firmware 0.2.39
|
||||
|
||||
Storage version, tile protocol (`v: 1`), preferences and keys are unchanged. Additive wire fields only.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Test results app 0.2.47 / firmware 0.2.40 (2026-09-15)
|
||||
|
||||
The new climate card on the Guition with fan and swing on a card of their own, and no single mode
|
||||
key on either board. See CHANGELOG 0.2.47 and RELEASING "Compatibility 0.2.47".
|
||||
|
||||
## Automated
|
||||
|
||||
- Python (`.venv-portal`): 170 tests OK (the icon tests cover the new `arrow-oscillating` glyph).
|
||||
- C++ (`clang++ -std=c++17 -Wall -Wextra -Werror -I . -I components/smart_display`): 13/13 PASS.
|
||||
`climate_card.h` draws LVGL objects and is covered by the host renders below, not by a unit test.
|
||||
- `generate_packages.py --check` green, `generate_icons.py --check` verified (150 pickable icons,
|
||||
180 glyphs).
|
||||
- ESPHome 2026.6.2 compile SUCCESS, one after another: `easy-guition-device.yaml` (RAM 32.9%, flash
|
||||
18.9%), `easy-cyd-device.yaml` (RAM 32.0%, flash 78.5%), `guition-device.yaml` (RAM 33.0%, flash 20.0%)
|
||||
and `device.yaml` (RAM 32.0%, flash 83.3%). Packages carry `SCREEN_FIRMWARE_VERSION` 0.2.40.
|
||||
|
||||
## Bench boards (OTA)
|
||||
|
||||
- Both Easy Setup builds uploaded over the network; `device_info` reports project 0.2.40, compiled
|
||||
2026-09-15 15:42:18 (Guition) and 15:44:03 (CYD), matching the build logs. ESP Screens reports both
|
||||
screens Synced on 0.2.40.
|
||||
- `preview_runtime_card` opened the climate card of the owner's `climate.airco_remeha_diva_ac` on
|
||||
the Guition with live data from app 0.2.46. The capture (`capture_ui.py`) shows Off with a grey 19°,
|
||||
the five mode keys (heat/cool, heat, cool, dry, fan only), Fan with Low chosen and Swing with Off
|
||||
chosen. The vacuum card of 0.2.46 is unchanged.
|
||||
|
||||
## Renders (ESPHome host + SDL, both boards, this release's code)
|
||||
|
||||
- Guition climate card in four cases: heating with three modes and a fan row, an air conditioner with
|
||||
five modes, fan and swing (the owner's attributes), the same with two swing modes, and a heat-only
|
||||
thermostat without fan or swing (no mode keys, setpoint centred). Nothing runs off the screen; the
|
||||
card with two rows ends 10 px above the bottom edge.
|
||||
- CYD climate card: unchanged apart from the heat-only thermostat, which now shows the centred setpoint
|
||||
without a mode key.
|
||||
- `docs/images/guition-climate.png` comes from the air conditioner render.
|
||||
|
||||
## Not tested / for the owner
|
||||
|
||||
- The Guition climate card by hand: − / + taps and holding, mode keys, fan and swing choices with the
|
||||
real air conditioner (tapping sends real commands).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 30 KiB |
+493
-292
@@ -1,7 +1,7 @@
|
||||
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
|
||||
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.39"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.40"
|
||||
# 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"
|
||||
@@ -694,6 +694,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,
|
||||
@@ -706,22 +707,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: |-
|
||||
@@ -731,6 +727,13 @@ esphome:
|
||||
light_controls::commits[0] = [](int value) { id(set_light_hue).execute(value); };
|
||||
light_controls::commits[1] = [](int value) { id(set_light_color_temperature).execute(value); };
|
||||
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
|
||||
climate_card::look.card = id(climate_settings_card);
|
||||
climate_card::look.font = id(label)->get_lv_font();
|
||||
climate_card::look.icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||||
climate_card::choose = [](char kind, int index) {
|
||||
if (kind == 'f') id(set_climate_fan_mode).execute(index);
|
||||
else id(set_climate_swing_mode).execute(index);
|
||||
};
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
@@ -1109,12 +1112,29 @@ 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-700.ttf"
|
||||
id: climate_number
|
||||
size: 46
|
||||
# The climate card's setpoint, large enough to read across the room.
|
||||
- file: "fonts/Roboto-400.ttf"
|
||||
id: setpoint_digits
|
||||
size: 64
|
||||
bpp: 4
|
||||
glyphs: "0123456789.-° "
|
||||
glyphs: ['-','.','°','0','1','2','3','4','5','6','7','8','9']
|
||||
- file: "fonts/Roboto-500.ttf"
|
||||
id: time_label
|
||||
size: 27
|
||||
@@ -1363,6 +1383,7 @@ font:
|
||||
- "\U000F067F" # weather-snowy-rainy
|
||||
- "\U000F059B" # weather-sunset-down
|
||||
- "\U000F01D8" # dots-horizontal
|
||||
- "\U000F1C91" # arrow-oscillating
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: materialdesign_icons_mini
|
||||
@@ -4059,13 +4080,17 @@ lvgl:
|
||||
|
||||
|
||||
# ---------- CLIMATE DETAIL OVERLAY ----------
|
||||
# The CYD's climate card at the Guition's size: 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.
|
||||
- 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
|
||||
@@ -4076,27 +4101,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
|
||||
@@ -4147,156 +4151,322 @@ lvgl:
|
||||
- label:
|
||||
id: climate_detail_title
|
||||
align: TOP_MID
|
||||
y: 35
|
||||
y: 30
|
||||
width: 300
|
||||
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: 84
|
||||
text: "Now: --"
|
||||
y: 92
|
||||
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: 40
|
||||
width: 276
|
||||
height: 276
|
||||
# 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: 30
|
||||
arc_rounded: true
|
||||
arc_color: 0xE5E5E5
|
||||
arc_opa: 80%
|
||||
- obj:
|
||||
id: climate_setpoint_card
|
||||
align: TOP_MID
|
||||
y: 136
|
||||
width: 94%
|
||||
height: 200
|
||||
radius: 24
|
||||
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: 18
|
||||
width: 116
|
||||
height: 116
|
||||
radius: 58
|
||||
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: -18
|
||||
width: 116
|
||||
height: 116
|
||||
radius: 58
|
||||
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: -14
|
||||
text: "--"
|
||||
text_font: setpoint_digits
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: climate_set_label
|
||||
align: CENTER
|
||||
y: 46
|
||||
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: 364
|
||||
width: 94%
|
||||
height: 76
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
indicator:
|
||||
arc_color: 0x00A6ED
|
||||
arc_opa: COVER
|
||||
arc_width: 30
|
||||
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: -32
|
||||
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: 40
|
||||
text: "20.0°"
|
||||
text_font: climate_number
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
|
||||
scrollable: false
|
||||
layout:
|
||||
type: FLEX
|
||||
flex_flow: ROW
|
||||
flex_align_main: CENTER
|
||||
flex_align_cross: CENTER
|
||||
pad_column: 10
|
||||
widgets:
|
||||
- obj:
|
||||
id: cmkey_auto
|
||||
flex_grow: 1
|
||||
height: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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
|
||||
# Fan and swing, each a row of choices behind its icon (climate_card.h fills the card).
|
||||
- obj:
|
||||
id: climate_minus_button
|
||||
align: BOTTOM_LEFT
|
||||
x: 57
|
||||
y: -32
|
||||
width: 72
|
||||
height: 72
|
||||
radius: 22
|
||||
bg_color: 0xEEEEEE
|
||||
id: climate_settings_card
|
||||
align: TOP_MID
|
||||
y: 340
|
||||
width: 94%
|
||||
height: 116
|
||||
radius: 24
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
border_width: 1
|
||||
border_color: 0xFFFFFF
|
||||
border_opa: 16%
|
||||
clickable: true
|
||||
border_color: 0xDDDDDD
|
||||
pad_all: 0
|
||||
hidden: true
|
||||
clickable: false
|
||||
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_plus_button
|
||||
align: BOTTOM_RIGHT
|
||||
x: -57
|
||||
y: -32
|
||||
width: 72
|
||||
height: 72
|
||||
radius: 22
|
||||
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: -36
|
||||
width: 189
|
||||
height: 68
|
||||
radius: 26
|
||||
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
|
||||
# ---------- 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
|
||||
@@ -5405,55 +5575,157 @@ 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"}};
|
||||
// The Guition shows fan and swing on their own card below, so no "more" key.
|
||||
bool more = false;
|
||||
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);
|
||||
// One mode is no choice: the power key already turns it on and off.
|
||||
const bool show_keys = count > 1 || more;
|
||||
if (show_keys) 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);
|
||||
|
||||
// Fan and swing rows, in the entity's own names and order.
|
||||
climate_card::Row settings[2];
|
||||
int rows = 0;
|
||||
if (!cyd::list_item(id(active_climate_fan_modes), 0).empty())
|
||||
settings[rows++] = {'f', "\U000F0210", id(active_climate_fan_modes), id(active_climate_fan_mode)};
|
||||
if (!cyd::list_item(id(active_climate_swing_modes), 0).empty())
|
||||
settings[rows++] = {'s', "\U000F1C91", id(active_climate_swing_modes), id(active_climate_swing_mode)};
|
||||
// One layout per card: the setpoint gives room to the mode keys and the settings below it,
|
||||
// and a card with nothing below keeps the setpoint centred.
|
||||
static int laid_out = -1;
|
||||
if (laid_out != rows * 2 + show_keys) {
|
||||
laid_out = rows * 2 + show_keys;
|
||||
struct Layout { int status, card_y, card_h, key, number_y, caption_y, modes_y, modes_h, settings_y, edge, row_h, gap; };
|
||||
static const Layout layouts[6] = {
|
||||
// no mode keys: 0, 1, 2 rows
|
||||
{92, 180, 200, 116, -14, 46, 0, 0, 0, 14, 48, 10},
|
||||
{92, 140, 200, 116, -14, 46, 0, 0, 364, 14, 48, 10},
|
||||
{90, 130, 180, 108, -12, 42, 0, 0, 330, 14, 46, 10},
|
||||
// mode keys: 0, 1, 2 rows
|
||||
{92, 136, 200, 116, -14, 46, 364, 76, 0, 14, 48, 10},
|
||||
{90, 126, 170, 108, -12, 42, 312, 68, 394, 12, 44, 8},
|
||||
{86, 118, 140, 96, -10, 36, 270, 60, 342, 12, 42, 8},
|
||||
};
|
||||
const Layout &l = layouts[(show_keys ? 3 : 0) + rows];
|
||||
lv_obj_set_y(id(climate_current_label), l.status);
|
||||
lv_obj_set_y(id(climate_setpoint_card), l.card_y);
|
||||
lv_obj_set_height(id(climate_setpoint_card), l.card_h);
|
||||
for (lv_obj_t *button : {id(climate_minus_button), id(climate_plus_button)}) {
|
||||
lv_obj_set_size(button, l.key, l.key);
|
||||
lv_obj_set_style_radius(button, l.key / 2, LV_PART_MAIN);
|
||||
}
|
||||
lv_obj_set_y(id(climate_target_label), l.number_y);
|
||||
lv_obj_set_y(id(climate_set_label), l.caption_y);
|
||||
if (show_keys) {
|
||||
lv_obj_set_y(id(climate_mode_keys), l.modes_y);
|
||||
lv_obj_set_height(id(climate_mode_keys), l.modes_h);
|
||||
for (lv_obj_t *mode_key : {id(cmkey_auto), id(cmkey_heat_cool), id(cmkey_heat), id(cmkey_cool), id(cmkey_dry), id(cmkey_fan_only), id(cmkey_more)}) {
|
||||
lv_obj_set_height(mode_key, l.modes_h);
|
||||
lv_obj_set_style_radius(mode_key, l.modes_h / 2, LV_PART_MAIN);
|
||||
}
|
||||
}
|
||||
lv_obj_set_y(id(climate_settings_card), l.settings_y);
|
||||
climate_card::look.edge = l.edge;
|
||||
climate_card::look.row_h = l.row_h;
|
||||
climate_card::look.gap = l.gap;
|
||||
}
|
||||
climate_card::show(settings, rows);
|
||||
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.
|
||||
@@ -5563,6 +5835,7 @@ script:
|
||||
id(active_climate_fan_mode) = cyd::list_item(id(active_climate_fan_modes), index);
|
||||
id(climate_edit_ms) = millis();
|
||||
id(climate_edit_pending) = true;
|
||||
id(climate_card_refresh).execute();
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- if:
|
||||
condition:
|
||||
@@ -5582,6 +5855,7 @@ script:
|
||||
id(active_climate_swing_mode) = cyd::list_item(id(active_climate_swing_modes), index);
|
||||
id(climate_edit_ms) = millis();
|
||||
id(climate_edit_pending) = true;
|
||||
id(climate_card_refresh).execute();
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- if:
|
||||
condition:
|
||||
@@ -5603,8 +5877,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);'
|
||||
@@ -5643,9 +5917,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:
|
||||
@@ -6200,6 +6475,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);
|
||||
@@ -6212,41 +6489,7 @@ 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));
|
||||
|
||||
if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) {
|
||||
lv_obj_set_size(id(climate_arc), 294, 294);
|
||||
lv_obj_set_y(id(climate_arc), 16);
|
||||
lv_obj_set_y(id(climate_current_label), 96);
|
||||
lv_obj_set_y(id(climate_minus_button), -124);
|
||||
lv_obj_set_y(id(climate_plus_button), -124);
|
||||
lv_obj_set_y(id(climate_mode_pill), -36);
|
||||
} else {
|
||||
lv_obj_set_size(id(climate_arc), 264, 264);
|
||||
lv_obj_set_y(id(climate_arc), 36);
|
||||
lv_obj_set_y(id(climate_current_label), 80);
|
||||
lv_obj_set_y(id(climate_minus_button), -28);
|
||||
lv_obj_set_y(id(climate_plus_button), -28);
|
||||
lv_obj_set_y(id(climate_mode_pill), -30);
|
||||
}
|
||||
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);
|
||||
@@ -6572,66 +6815,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()));
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.39"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.40"
|
||||
# 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.
|
||||
@@ -1571,6 +1571,7 @@ font:
|
||||
- "\U000F067F" # weather-snowy-rainy
|
||||
- "\U000F059B" # weather-sunset-down
|
||||
- "\U000F01D8" # dots-horizontal
|
||||
- "\U000F1C91" # arrow-oscillating
|
||||
|
||||
- file: 'fonts/materialdesignicons-webfont.ttf'
|
||||
id: materialdesign_icons_mini
|
||||
@@ -5749,8 +5750,13 @@ script:
|
||||
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);
|
||||
// One mode is no choice: the power key already turns it on and off. Without keys the
|
||||
// setpoint card moves to the middle of the room below the status line.
|
||||
const bool show_keys = count > 1 || more;
|
||||
if (show_keys) 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);
|
||||
const bool landscape = atoi("${DISPLAY_W}") >= atoi("${DISPLAY_H}");
|
||||
lv_obj_set_y(id(climate_setpoint_card), show_keys ? (landscape ? 80 : 96) : (landscape ? 108 : 140));
|
||||
id(climate_power_refresh).execute();
|
||||
|
||||
# Opens the mode picker. Hides modes the entity doesn't support and marks
|
||||
|
||||
+8
-2
@@ -58,7 +58,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.39"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.40"
|
||||
# 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.
|
||||
@@ -1560,6 +1560,7 @@ font:
|
||||
- "\U000F067F" # weather-snowy-rainy
|
||||
- "\U000F059B" # weather-sunset-down
|
||||
- "\U000F01D8" # dots-horizontal
|
||||
- "\U000F1C91" # arrow-oscillating
|
||||
|
||||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||||
id: materialdesign_icons_mini
|
||||
@@ -5680,8 +5681,13 @@ script:
|
||||
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);
|
||||
// One mode is no choice: the power key already turns it on and off. Without keys the
|
||||
// setpoint card moves to the middle of the room below the status line.
|
||||
const bool show_keys = count > 1 || more;
|
||||
if (show_keys) 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);
|
||||
const bool landscape = atoi("${DISPLAY_W}") >= atoi("${DISPLAY_H}");
|
||||
lv_obj_set_y(id(climate_setpoint_card), show_keys ? (landscape ? 80 : 96) : (landscape ? 108 : 140));
|
||||
id(climate_power_refresh).execute();
|
||||
|
||||
# Opens the mode picker. Hides modes the entity doesn't support and marks
|
||||
|
||||
+493
-292
@@ -2,7 +2,7 @@
|
||||
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
|
||||
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.39"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.40"
|
||||
# 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"
|
||||
@@ -698,6 +698,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,
|
||||
@@ -710,22 +711,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: |-
|
||||
@@ -735,6 +731,13 @@ esphome:
|
||||
light_controls::commits[0] = [](int value) { id(set_light_hue).execute(value); };
|
||||
light_controls::commits[1] = [](int value) { id(set_light_color_temperature).execute(value); };
|
||||
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
|
||||
climate_card::look.card = id(climate_settings_card);
|
||||
climate_card::look.font = id(label)->get_lv_font();
|
||||
climate_card::look.icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||||
climate_card::choose = [](char kind, int index) {
|
||||
if (kind == 'f') id(set_climate_fan_mode).execute(index);
|
||||
else id(set_climate_swing_mode).execute(index);
|
||||
};
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||||
@@ -1098,12 +1101,29 @@ 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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-700.ttf"
|
||||
id: climate_number
|
||||
size: 46
|
||||
# The climate card's setpoint, large enough to read across the room.
|
||||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf"
|
||||
id: setpoint_digits
|
||||
size: 64
|
||||
bpp: 4
|
||||
glyphs: "0123456789.-° "
|
||||
glyphs: ['-','.','°','0','1','2','3','4','5','6','7','8','9']
|
||||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||||
id: time_label
|
||||
size: 27
|
||||
@@ -1352,6 +1372,7 @@ font:
|
||||
- "\U000F067F" # weather-snowy-rainy
|
||||
- "\U000F059B" # weather-sunset-down
|
||||
- "\U000F01D8" # dots-horizontal
|
||||
- "\U000F1C91" # arrow-oscillating
|
||||
|
||||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||||
id: materialdesign_icons_mini
|
||||
@@ -3990,13 +4011,17 @@ lvgl:
|
||||
|
||||
|
||||
# ---------- CLIMATE DETAIL OVERLAY ----------
|
||||
# The CYD's climate card at the Guition's size: 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.
|
||||
- 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
|
||||
@@ -4007,27 +4032,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
|
||||
@@ -4078,156 +4082,322 @@ lvgl:
|
||||
- label:
|
||||
id: climate_detail_title
|
||||
align: TOP_MID
|
||||
y: 35
|
||||
y: 30
|
||||
width: 300
|
||||
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: 84
|
||||
text: "Now: --"
|
||||
y: 92
|
||||
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: 40
|
||||
width: 276
|
||||
height: 276
|
||||
# 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: 30
|
||||
arc_rounded: true
|
||||
arc_color: 0xE5E5E5
|
||||
arc_opa: 80%
|
||||
- obj:
|
||||
id: climate_setpoint_card
|
||||
align: TOP_MID
|
||||
y: 136
|
||||
width: 94%
|
||||
height: 200
|
||||
radius: 24
|
||||
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: 18
|
||||
width: 116
|
||||
height: 116
|
||||
radius: 58
|
||||
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: -18
|
||||
width: 116
|
||||
height: 116
|
||||
radius: 58
|
||||
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: -14
|
||||
text: "--"
|
||||
text_font: setpoint_digits
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
- label:
|
||||
id: climate_set_label
|
||||
align: CENTER
|
||||
y: 46
|
||||
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: 364
|
||||
width: 94%
|
||||
height: 76
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
indicator:
|
||||
arc_color: 0x00A6ED
|
||||
arc_opa: COVER
|
||||
arc_width: 30
|
||||
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: -32
|
||||
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: 40
|
||||
text: "20.0°"
|
||||
text_font: climate_number
|
||||
text_color: 0x1B1B1B
|
||||
clickable: false
|
||||
|
||||
scrollable: false
|
||||
layout:
|
||||
type: FLEX
|
||||
flex_flow: ROW
|
||||
flex_align_main: CENTER
|
||||
flex_align_cross: CENTER
|
||||
pad_column: 10
|
||||
widgets:
|
||||
- obj:
|
||||
id: cmkey_auto
|
||||
flex_grow: 1
|
||||
height: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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: 76
|
||||
radius: 38
|
||||
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
|
||||
# Fan and swing, each a row of choices behind its icon (climate_card.h fills the card).
|
||||
- obj:
|
||||
id: climate_minus_button
|
||||
align: BOTTOM_LEFT
|
||||
x: 57
|
||||
y: -32
|
||||
width: 72
|
||||
height: 72
|
||||
radius: 22
|
||||
bg_color: 0xEEEEEE
|
||||
id: climate_settings_card
|
||||
align: TOP_MID
|
||||
y: 340
|
||||
width: 94%
|
||||
height: 116
|
||||
radius: 24
|
||||
bg_color: 0xFFFFFF
|
||||
bg_opa: COVER
|
||||
border_width: 1
|
||||
border_color: 0xFFFFFF
|
||||
border_opa: 16%
|
||||
clickable: true
|
||||
border_color: 0xDDDDDD
|
||||
pad_all: 0
|
||||
hidden: true
|
||||
clickable: false
|
||||
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_plus_button
|
||||
align: BOTTOM_RIGHT
|
||||
x: -57
|
||||
y: -32
|
||||
width: 72
|
||||
height: 72
|
||||
radius: 22
|
||||
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: -36
|
||||
width: 189
|
||||
height: 68
|
||||
radius: 26
|
||||
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
|
||||
# ---------- 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
|
||||
@@ -5336,55 +5506,157 @@ 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"}};
|
||||
// The Guition shows fan and swing on their own card below, so no "more" key.
|
||||
bool more = false;
|
||||
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);
|
||||
// One mode is no choice: the power key already turns it on and off.
|
||||
const bool show_keys = count > 1 || more;
|
||||
if (show_keys) 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);
|
||||
|
||||
// Fan and swing rows, in the entity's own names and order.
|
||||
climate_card::Row settings[2];
|
||||
int rows = 0;
|
||||
if (!cyd::list_item(id(active_climate_fan_modes), 0).empty())
|
||||
settings[rows++] = {'f', "\U000F0210", id(active_climate_fan_modes), id(active_climate_fan_mode)};
|
||||
if (!cyd::list_item(id(active_climate_swing_modes), 0).empty())
|
||||
settings[rows++] = {'s', "\U000F1C91", id(active_climate_swing_modes), id(active_climate_swing_mode)};
|
||||
// One layout per card: the setpoint gives room to the mode keys and the settings below it,
|
||||
// and a card with nothing below keeps the setpoint centred.
|
||||
static int laid_out = -1;
|
||||
if (laid_out != rows * 2 + show_keys) {
|
||||
laid_out = rows * 2 + show_keys;
|
||||
struct Layout { int status, card_y, card_h, key, number_y, caption_y, modes_y, modes_h, settings_y, edge, row_h, gap; };
|
||||
static const Layout layouts[6] = {
|
||||
// no mode keys: 0, 1, 2 rows
|
||||
{92, 180, 200, 116, -14, 46, 0, 0, 0, 14, 48, 10},
|
||||
{92, 140, 200, 116, -14, 46, 0, 0, 364, 14, 48, 10},
|
||||
{90, 130, 180, 108, -12, 42, 0, 0, 330, 14, 46, 10},
|
||||
// mode keys: 0, 1, 2 rows
|
||||
{92, 136, 200, 116, -14, 46, 364, 76, 0, 14, 48, 10},
|
||||
{90, 126, 170, 108, -12, 42, 312, 68, 394, 12, 44, 8},
|
||||
{86, 118, 140, 96, -10, 36, 270, 60, 342, 12, 42, 8},
|
||||
};
|
||||
const Layout &l = layouts[(show_keys ? 3 : 0) + rows];
|
||||
lv_obj_set_y(id(climate_current_label), l.status);
|
||||
lv_obj_set_y(id(climate_setpoint_card), l.card_y);
|
||||
lv_obj_set_height(id(climate_setpoint_card), l.card_h);
|
||||
for (lv_obj_t *button : {id(climate_minus_button), id(climate_plus_button)}) {
|
||||
lv_obj_set_size(button, l.key, l.key);
|
||||
lv_obj_set_style_radius(button, l.key / 2, LV_PART_MAIN);
|
||||
}
|
||||
lv_obj_set_y(id(climate_target_label), l.number_y);
|
||||
lv_obj_set_y(id(climate_set_label), l.caption_y);
|
||||
if (show_keys) {
|
||||
lv_obj_set_y(id(climate_mode_keys), l.modes_y);
|
||||
lv_obj_set_height(id(climate_mode_keys), l.modes_h);
|
||||
for (lv_obj_t *mode_key : {id(cmkey_auto), id(cmkey_heat_cool), id(cmkey_heat), id(cmkey_cool), id(cmkey_dry), id(cmkey_fan_only), id(cmkey_more)}) {
|
||||
lv_obj_set_height(mode_key, l.modes_h);
|
||||
lv_obj_set_style_radius(mode_key, l.modes_h / 2, LV_PART_MAIN);
|
||||
}
|
||||
}
|
||||
lv_obj_set_y(id(climate_settings_card), l.settings_y);
|
||||
climate_card::look.edge = l.edge;
|
||||
climate_card::look.row_h = l.row_h;
|
||||
climate_card::look.gap = l.gap;
|
||||
}
|
||||
climate_card::show(settings, rows);
|
||||
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.
|
||||
@@ -5494,6 +5766,7 @@ script:
|
||||
id(active_climate_fan_mode) = cyd::list_item(id(active_climate_fan_modes), index);
|
||||
id(climate_edit_ms) = millis();
|
||||
id(climate_edit_pending) = true;
|
||||
id(climate_card_refresh).execute();
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- if:
|
||||
condition:
|
||||
@@ -5513,6 +5786,7 @@ script:
|
||||
id(active_climate_swing_mode) = cyd::list_item(id(active_climate_swing_modes), index);
|
||||
id(climate_edit_ms) = millis();
|
||||
id(climate_edit_pending) = true;
|
||||
id(climate_card_refresh).execute();
|
||||
- lvgl.widget.hide: climate_mode_overlay
|
||||
- if:
|
||||
condition:
|
||||
@@ -5534,8 +5808,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);'
|
||||
@@ -5574,9 +5848,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:
|
||||
@@ -6131,6 +6406,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);
|
||||
@@ -6143,41 +6420,7 @@ 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));
|
||||
|
||||
if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) {
|
||||
lv_obj_set_size(id(climate_arc), 294, 294);
|
||||
lv_obj_set_y(id(climate_arc), 16);
|
||||
lv_obj_set_y(id(climate_current_label), 96);
|
||||
lv_obj_set_y(id(climate_minus_button), -124);
|
||||
lv_obj_set_y(id(climate_plus_button), -124);
|
||||
lv_obj_set_y(id(climate_mode_pill), -36);
|
||||
} else {
|
||||
lv_obj_set_size(id(climate_arc), 264, 264);
|
||||
lv_obj_set_y(id(climate_arc), 36);
|
||||
lv_obj_set_y(id(climate_current_label), 80);
|
||||
lv_obj_set_y(id(climate_minus_button), -28);
|
||||
lv_obj_set_y(id(climate_plus_button), -28);
|
||||
lv_obj_set_y(id(climate_mode_pill), -30);
|
||||
}
|
||||
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);
|
||||
@@ -6503,66 +6746,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()));
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
## 0.2.47 (firmware 0.2.40)
|
||||
|
||||
The new climate card comes to the Guition, with fan and swing right on the card.
|
||||
|
||||
- **Climate card on the Guition.** The dial is gone here too: the target temperature sits big between two large − / + keys, the number follows every tap and one call goes out when you stop tapping, and holding a key keeps stepping. One row of keys picks the mode in Home Assistant's order and colours.
|
||||
- **Fan and swing at a glance.** Below the mode keys a white card shows the fan speeds and the swing modes as rows of choices, each behind its icon, like the cleaning settings on the vacuum card. Tap a choice and it shows at once. The card makes room for whatever the device has: with fan and swing everything moves a little closer together, and a thermostat without them keeps a larger setpoint.
|
||||
- **One mode is no choice.** A thermostat that can only heat no longer shows a single mode key; its setpoint sits in the middle of the card, on both boards. The power key still turns it on and off.
|
||||
- Firmware 0.2.40 for both boards; the app offers the update. The CYD keeps fan and swing behind ···.
|
||||
|
||||
## 0.2.46 (firmware 0.2.39)
|
||||
|
||||
Choose between vacuuming, mopping or both on the vacuum card, and a climate card made for the small CYD.
|
||||
|
||||
@@ -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.39'
|
||||
FIRMWARE_VERSION = '0.2.40'
|
||||
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split())
|
||||
# Attributes whose boolean value the screen needs; every other bool stays behind.
|
||||
BOOL_ATTRS = frozenset(['is_volume_muted'])
|
||||
|
||||
Binary file not shown.
@@ -218,6 +218,8 @@ FIXED = (
|
||||
('weather-sunset-down', 'F059B'),
|
||||
# "More" key on the CYD climate card: fan and swing (firmware 0.2.39+).
|
||||
('dots-horizontal', 'F01D8'),
|
||||
# Swing row on the Guition climate card, Home Assistant's icon for swing modes (firmware 0.2.40+).
|
||||
('arrow-oscillating', 'F1C91'),
|
||||
)
|
||||
|
||||
# Pickable icons by name, and every glyph the firmware fonts contain.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ESP Screen Manager
|
||||
version: "0.2.46"
|
||||
version: "0.2.47"
|
||||
slug: esp_screen_manager
|
||||
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
|
||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||
|
||||
Reference in New Issue
Block a user