Replace light presets with shared hue, temperature and brightness sliders
This commit is contained in:
@@ -7,7 +7,7 @@ from aioesphomeapi import APIClient, LogLevel
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('action', choices=['touch_diagnostics', 'end_touch_diagnostics', 'ui_state'])
|
||||
parser.add_argument('action', choices=['touch_diagnostics', 'end_touch_diagnostics', 'ui_state', 'light_controls_preview'])
|
||||
parser.add_argument('--host', default='cyd-2432s028.local')
|
||||
parser.add_argument('--name', default='cyd-2432s028', help='Verwachte DEVICE_NAME')
|
||||
parser.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml')
|
||||
|
||||
@@ -24,6 +24,7 @@ async def main():
|
||||
failures = []
|
||||
passes = []
|
||||
frames = []
|
||||
slider_pass = []
|
||||
def log(msg):
|
||||
text = msg.message.decode(errors='replace') if isinstance(msg.message, bytes) else msg.message
|
||||
if any(tag in text for tag in ('ui_test', 'UI_TEST', '[health', 'took a long')):
|
||||
@@ -32,6 +33,7 @@ async def main():
|
||||
passes.append(text)
|
||||
match = re.search(r'frames=(\d+)', text)
|
||||
if match: frames.append(int(match[1]))
|
||||
if 'Light sliders: PASS' in text: slider_pass.append(text)
|
||||
if 'FAIL' in text: failures.append(text)
|
||||
if 'UI_TEST COMPLETE' in text: done.set()
|
||||
client.subscribe_logs(log, log_level=LogLevel.LOG_LEVEL_DEBUG, dump_config=False)
|
||||
@@ -39,6 +41,7 @@ async def main():
|
||||
await asyncio.wait_for(done.wait(), timeout=45)
|
||||
assert len(passes) == 10, f'Expected 10 page checks, got {len(passes)}'
|
||||
assert not failures, failures
|
||||
assert slider_pass, "Missing light slider event/capability check"
|
||||
assert len(frames) == 10 and frames[-1] - frames[0] >= 40, f'Render callbacks stalled: {frames}'
|
||||
print('PASS: 10 page checks and 50 overlay render cycles completed', flush=True)
|
||||
finally:
|
||||
|
||||
@@ -52,7 +52,11 @@ python -m unittest discover -s tests -p 'test_*.py'
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_cyd_ui.cpp -o /tmp/test_cyd_ui
|
||||
/tmp/test_cyd_ui
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_touch_filter.cpp -o /tmp/test_touch_filter
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls
|
||||
/tmp/test_light_controls
|
||||
/tmp/test_touch_filter
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls
|
||||
/tmp/test_light_controls
|
||||
python -m esphome config device.yaml
|
||||
python -m esphome compile device.yaml
|
||||
```
|
||||
@@ -79,3 +83,16 @@ Lokale profielbestanden veilig bewaard:
|
||||
Geef de eigenaar de drie lokale YAML-bestanden en de meetbestanden via een
|
||||
passend privé-kanaal. Gebruik voor een andere eigenaar de schone repository
|
||||
of starter-ZIP, zodat die eigen sleutels en eigen paneelkalibratie krijgt.
|
||||
|
||||
## Lichtkaart
|
||||
|
||||
- RGB+wit-lamp: drie schuiven; RGB-only: kleur en helderheid; wit-only:
|
||||
wittemperatuur en helderheid. Een gewone dimmer behoudt de helderheidskaart.
|
||||
- Sleep langzaam over de regenboog, laat los en controleer de fysieke lamp.
|
||||
Herhaal voor wittemperatuur en helderheid. Helderheid behoudt de kleurmodus.
|
||||
- Heropen de kaart en controleer de ontvangen waarden. Test twee lampen met
|
||||
verschillende temperatuurbereiken; er mag geen waarde van de andere lamp lekken.
|
||||
- Firmware-renderdiagnose bevat `Light sliders: PASS`: twintig preview-events
|
||||
per schuif, precies één commit bij loslaten, geen dubbele commit, geen commit
|
||||
na een afgebroken aanraking en vier capabilitycombinaties. Dit gebeurt met
|
||||
vervangen callbacks en bedient geen echte lampen.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Lichtregelaars — 12 september 2026
|
||||
|
||||
Gedeelde `light_controls.h` vervangt de kleurpresets in de CYD- en
|
||||
Guition-profielen. Hue, wittemperatuur en helderheid staan op één kaart;
|
||||
capabilities bepalen welke schuiven verschijnen.
|
||||
|
||||
## Uitgevoerd
|
||||
|
||||
- 23 Python-regressietests op de Guition-branch geslaagd.
|
||||
- C++-tests voor touchguard, touchfilter en nieuwe lichtwaardenparser geslaagd.
|
||||
- Beide persoonlijke profielen volledig gebouwd met ESPHome 2026.6.2.
|
||||
- Guition: RAM statisch 74.380 bytes; firmware 1.349.559 bytes.
|
||||
- CYD: RAM statisch 70.820 bytes; firmware 1.291.519 bytes (70,4% app-partitie).
|
||||
- Guition via OTA geflasht en apparaatidentiteit via versleutelde API bevestigd.
|
||||
- Op Guition `Light sliders: PASS`: twintig preview-events per regelaar,
|
||||
één commit bij loslaten, geen dubbele commit, geen commit na press-lost,
|
||||
vier capabilitycombinaties. Callbacks vervangen tijdens deze test: geen lampacties.
|
||||
- Tien pagina-/geometriecontroles en vijftig overlay-rendercycli geslaagd.
|
||||
Frames 6–69; vrije heap circa 6.514.320 bytes.
|
||||
- Veilige voorbeeldkaart uit LVGL als screenshot opgehaald en visueel gecontroleerd.
|
||||
Eerste screenshotpoging verloor de API-verbinding; herhaling met USB-logging
|
||||
voltooide alle 240 rijen zonder exception/backtrace in de capture.
|
||||
- Neutrale starterbundel met gedeelde header succesvol geëxporteerd.
|
||||
|
||||
De CYD-build vond een lokale PlatformIO-esptool-koppeling naar versie 4.9,
|
||||
terwijl de platformmetadata 5.3 vereiste. De lokale editable installatie is
|
||||
hersteld naar de al aanwezige 5.3-package; de volledige build slaagt daarna.
|
||||
Dit is een lokale toolchainreparatie, geen wijziging van bord/flashinstellingen.
|
||||
|
||||
## Grenzen
|
||||
|
||||
Een succesvolle build is geen fysieke CYD-test: alleen de Guition was aangesloten.
|
||||
Schuifevents in de firmwaretest zijn synthetisch; menselijke touchbediening en
|
||||
werkelijke HA-lampacties moeten apart worden gecontroleerd. De eigenaar vond de
|
||||
voorbeeldkaart er goed uitzien; de HA-koppeling ontbrak op dat moment nog.
|
||||
@@ -49,6 +49,44 @@ De voorbeeldconfig vult ook de overige velden in. Laat die staan tenzij je
|
||||
ze bewust gebruikt. `STATE_ENTITY` kan bij een scène/script een echte lamp of
|
||||
schakelaar volgen; een scène heeft zelf geen betrouwbare aan/uitstatus.
|
||||
|
||||
## Kleur, wittemperatuur en helderheid
|
||||
|
||||
Een RGB-/kleurenlamp met `LONGPRESS: "slider"` opent direct de lampkaart.
|
||||
Deze bevat maximaal drie horizontale schuiven:
|
||||
|
||||
- **Kleur:** een doorlopende regenboog (0–360° hue, 100% verzadiging).
|
||||
- **Wittemperatuur:** warm naar koel, binnen de minimum-/maximum-Kelvinwaarden
|
||||
die de lamp in Home Assistant meldt.
|
||||
- **Helderheid:** 1–100%; aan/uit blijft via de tegel beschikbaar.
|
||||
|
||||
De kleur- en witschuif verschijnen alleen als `supported_color_modes` deze
|
||||
ondersteunt. Een uitsluitend dimbare lamp houdt de gewone helderheidskaart.
|
||||
Er zijn geen extra tegelinstellingen nodig. Gebruik voor een lamp dezelfde
|
||||
`ENTITY` en `STATE_ENTITY`. De kleurkaart volgt `hs_color`, `color_temp_kelvin`,
|
||||
`min_color_temp_kelvin` en `max_color_temp_kelvin` van de bediende entiteit.
|
||||
Ontbreekt het temperatuurbereik, dan wacht de witschuif op HA-gegevens.
|
||||
|
||||
De kaart neemt bij openen de laatst ontvangen lampwaarden over. Slepen toont
|
||||
lokaal de gekozen waarde; pas loslaten verstuurt één opdracht. Kleur kiest de
|
||||
kleurmodus, wittemperatuur kiest de witmodus. Helderheid verandert de kleurmodus
|
||||
niet. De regenboog regelt hue; pastel/verzadiging is geen aparte regelaar.
|
||||
De hue-lijst wordt via ESPHome `data_template` naar HA gestuurd; zie de
|
||||
[officiële API-documentatie](https://esphome.io/components/api/#homeassistantaction-action).
|
||||
|
||||
De gedeelde implementatie staat in `light_controls.h`. Beide schermprofielen
|
||||
gebruiken deze; de Guition heeft grotere regelaars, de CYD een compacte kaart.
|
||||
`diagnostics/run_ui_test.py` controleert ook de schuifevents met vervangen
|
||||
callbacks: geen echte HA-acties. Voor een veilige 40-secondenvoorbeeldkaart:
|
||||
|
||||
```sh
|
||||
python diagnostics/control_ui.py light_controls_preview --host guition-wallbox.local --name guition-wallbox
|
||||
```
|
||||
|
||||
Gebruik voor CYD de eigen host en naam. Tijdens deze voorbeeldkaart worden
|
||||
lichtopdrachten onderdrukt en verdwijnt de kaart vanzelf. Test vervolgens de
|
||||
werkelijke lamp: lang indrukken, kleur kiezen, wit kiezen, helderheid aanpassen,
|
||||
sluiten en heropenen. Controleer de fysieke lamp en de waarden in HA.
|
||||
|
||||
## Voorbeelden
|
||||
|
||||
Wijzig deze sleutels in de bestaande map; plak niet nogmaals `substitutions:`.
|
||||
|
||||
+55
-457
@@ -774,6 +774,7 @@ external_components:
|
||||
esphome:
|
||||
includes:
|
||||
- cyd_ui.h
|
||||
- light_controls.h
|
||||
name: ${DEVICE_NAME}
|
||||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||||
on_boot:
|
||||
@@ -788,6 +789,13 @@ esphome:
|
||||
lv_obj_set_style_pad_column(id(vacuum_actions), 4, 0);
|
||||
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
|
||||
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
|
||||
- lambda: |-
|
||||
static const char *entities[] = {"${TILE1_ENTITY}", "${TILE2_ENTITY}", "${TILE3_ENTITY}", "${TILE4_ENTITY}", "${TILE5_ENTITY}", "${TILE6_ENTITY}", "${TILE7_ENTITY}", "${TILE9_ENTITY}"};
|
||||
light_controls::subscribe(entities, 8);
|
||||
light_controls::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}"));
|
||||
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); };
|
||||
- light.turn_on:
|
||||
id: back_light
|
||||
brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;"
|
||||
@@ -858,6 +866,9 @@ api:
|
||||
ESP_LOGI("ui_diag", "home=%d calibration=%d grid=%d,%d,%d,%d next=%d,%d,%d,%d",
|
||||
lv_screen_active() == id(home_page)->obj, lv_screen_active() == id(touch_cal_page)->obj,
|
||||
grid.x1, grid.y1, grid.x2, grid.y2, nav.x1, nav.y1, nav.x2, nav.y2);
|
||||
- action: light_controls_preview
|
||||
then:
|
||||
- script.execute: light_controls_preview
|
||||
- action: ui_self_test
|
||||
then:
|
||||
- script.execute: ui_self_test
|
||||
@@ -3536,7 +3547,6 @@ lvgl:
|
||||
scrollable: false
|
||||
on_click:
|
||||
- lvgl.widget.hide: color_detail_overlay
|
||||
- lvgl.widget.show: brightness_overlay
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
@@ -3547,6 +3557,9 @@ lvgl:
|
||||
|
||||
- label:
|
||||
id: color_detail_title
|
||||
width: 210
|
||||
long_mode: DOT
|
||||
text_align: CENTER
|
||||
align: TOP_MID
|
||||
y: 16
|
||||
text: "Light Color"
|
||||
@@ -3575,334 +3588,6 @@ lvgl:
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
- obj:
|
||||
id: color_mode_tabs
|
||||
align: TOP_MID
|
||||
y: 58
|
||||
width: 190
|
||||
height: 28
|
||||
radius: 14
|
||||
bg_color: 0x3C3C40
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
scrollable: false
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
widgets:
|
||||
- obj:
|
||||
id: color_tab_indicator
|
||||
x: 2
|
||||
y: 2
|
||||
width: 93
|
||||
height: 24
|
||||
radius: 12
|
||||
bg_color: 0x8E8E93
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: false
|
||||
scrollable: false
|
||||
- obj:
|
||||
x: 0
|
||||
y: 0
|
||||
width: 95
|
||||
height: 28
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: show_color_detail_tab
|
||||
tab: 0
|
||||
widgets:
|
||||
- label:
|
||||
id: color_tab_label
|
||||
align: CENTER
|
||||
text: "Color"
|
||||
text_font: sublabel_big
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
- obj:
|
||||
x: 95
|
||||
y: 0
|
||||
width: 95
|
||||
height: 28
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
on_click:
|
||||
- script.execute:
|
||||
id: show_color_detail_tab
|
||||
tab: 1
|
||||
widgets:
|
||||
- label:
|
||||
id: temp_tab_label
|
||||
align: CENTER
|
||||
text: "Temp."
|
||||
text_font: sublabel_big
|
||||
text_color: 0xFFFFFF
|
||||
clickable: false
|
||||
|
||||
- obj:
|
||||
id: color_palette_card
|
||||
align: TOP_MID
|
||||
y: 98
|
||||
width: 286
|
||||
height: 48
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
scrollable: false
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
widgets:
|
||||
- obj:
|
||||
id: color_preset_red
|
||||
x: 14
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0xFF3B30
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "red" }
|
||||
- obj:
|
||||
id: color_preset_orange
|
||||
x: 61
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0xFF9F0A
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "orange" }
|
||||
- obj:
|
||||
id: color_preset_yellow
|
||||
x: 108
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0xFFD60A
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "yellow" }
|
||||
- obj:
|
||||
id: color_preset_green
|
||||
x: 155
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0x30D158
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "green" }
|
||||
- obj:
|
||||
id: color_preset_blue
|
||||
x: 202
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0x0A84FF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "blue" }
|
||||
- obj:
|
||||
id: color_preset_purple
|
||||
x: 249
|
||||
y: 8
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 999
|
||||
bg_color: 0xBF5AF2
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_name, color: "purple" }
|
||||
|
||||
- obj:
|
||||
id: temp_palette_card
|
||||
align: TOP_MID
|
||||
y: 98
|
||||
width: 286
|
||||
height: 48
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
scrollable: false
|
||||
hidden: true
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
widgets:
|
||||
- obj:
|
||||
id: temp_preset_2700
|
||||
x: 24
|
||||
y: 8
|
||||
width: 62
|
||||
height: 30
|
||||
radius: 15
|
||||
bg_color: 0xFFD4A3
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_temperature, kelvin: 2700 }
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "2700K"
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A2A18
|
||||
clickable: false
|
||||
- obj:
|
||||
id: temp_preset_4000
|
||||
x: 112
|
||||
y: 8
|
||||
width: 62
|
||||
height: 30
|
||||
radius: 15
|
||||
bg_color: 0xFFE8C8
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_temperature, kelvin: 4000 }
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "4000K"
|
||||
text_font: sublabel_big
|
||||
text_color: 0x3A2A18
|
||||
clickable: false
|
||||
- obj:
|
||||
id: temp_preset_6500
|
||||
x: 200
|
||||
y: 8
|
||||
width: 62
|
||||
height: 30
|
||||
radius: 15
|
||||
bg_color: 0xEAF4FF
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
clickable: true
|
||||
scrollable: false
|
||||
scrollbar_mode: 'OFF'
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
on_click:
|
||||
- script.execute: { id: set_light_color_temperature, kelvin: 6500 }
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "6500K"
|
||||
text_font: sublabel_big
|
||||
text_color: 0x1E2A36
|
||||
clickable: false
|
||||
|
||||
- obj:
|
||||
id: color_brightness_card
|
||||
align: TOP_MID
|
||||
y: 165
|
||||
width: 188
|
||||
height: 46
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
scrollable: false
|
||||
pad_left: 0
|
||||
pad_right: 0
|
||||
pad_top: 0
|
||||
pad_bottom: 0
|
||||
widgets:
|
||||
- label:
|
||||
x: 13
|
||||
y: 6
|
||||
text: "Brightness"
|
||||
text_font: sublabel
|
||||
text_color: 0xD1D1D6
|
||||
hidden: true
|
||||
clickable: false
|
||||
- slider:
|
||||
id: color_brightness_slider
|
||||
x: 0
|
||||
y: 0
|
||||
width: 188
|
||||
height: 46
|
||||
min_value: 1
|
||||
max_value: 100
|
||||
value: 100
|
||||
bg_color: 0x939391
|
||||
bg_opa: COVER
|
||||
radius: 23
|
||||
border_width: 0
|
||||
indicator:
|
||||
bg_color: 0xFEC600
|
||||
bg_opa: COVER
|
||||
radius: 23
|
||||
knob:
|
||||
bg_opa: 0%
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 10
|
||||
on_value:
|
||||
- script.execute:
|
||||
id: color_brightness_preview
|
||||
value: !lambda "return x;"
|
||||
on_release:
|
||||
- script.execute:
|
||||
id: color_brightness_commit
|
||||
value: !lambda "return lv_slider_get_value(id(color_brightness_slider));"
|
||||
- label:
|
||||
id: color_brightness_value
|
||||
align: RIGHT_MID
|
||||
x: -13
|
||||
text: "100 %"
|
||||
text_font: sublabel_big
|
||||
text_color: 0xFFFFFF
|
||||
hidden: true
|
||||
clickable: false
|
||||
|
||||
# ---------- CLIMATE DETAIL OVERLAY ----------
|
||||
- obj:
|
||||
id: climate_detail_overlay
|
||||
@@ -4769,6 +4454,7 @@ script:
|
||||
mode: single
|
||||
then:
|
||||
- logger.log: "UI_TEST START: page/overlay render stress, no HA actions"
|
||||
- lambda: 'ESP_LOGI("ui_test", "Light sliders: %s", light_controls::self_test() ? "PASS" : "FAIL");'
|
||||
- lambda: 'id(calibration_active) = false;'
|
||||
- lvgl.page.show: home_page
|
||||
- script.execute: wake_display
|
||||
@@ -5093,129 +4779,36 @@ script:
|
||||
entity_id: !lambda "return id(active_entity);"
|
||||
hvac_mode: !lambda "return id(active_climate_mode);"
|
||||
|
||||
# Isolated 40-second preview; every light commit is suppressed in the helper.
|
||||
- id: light_controls_preview
|
||||
mode: single
|
||||
then:
|
||||
- script.execute: wake_display
|
||||
- lambda: |-
|
||||
light_controls::demo = true;
|
||||
light_controls::fallback.minimum = 2000;
|
||||
light_controls::fallback.maximum = 6500;
|
||||
light_controls::fallback.kelvin = 3000;
|
||||
light_controls::fallback.hue = 200;
|
||||
light_controls::open("diagnostic", true, true, 75);
|
||||
lv_label_set_text(id(color_detail_title), "Voorbeeld lamp");
|
||||
- lvgl.widget.hide: color_close_button
|
||||
- lvgl.widget.hide: color_done_button
|
||||
- lvgl.widget.show: color_detail_overlay
|
||||
- delay: 40s
|
||||
- lvgl.widget.hide: color_detail_overlay
|
||||
- lvgl.widget.show: color_close_button
|
||||
- lvgl.widget.show: color_done_button
|
||||
- lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};'
|
||||
|
||||
- id: open_color_detail
|
||||
mode: restart
|
||||
then:
|
||||
- lambda: |-
|
||||
lv_label_set_text(id(color_detail_title), id(active_title).c_str());
|
||||
|
||||
char buf[12];
|
||||
snprintf(buf, sizeof(buf), "%d %%", id(active_brightness_pct));
|
||||
lv_label_set_text(id(color_brightness_value), buf);
|
||||
lv_slider_set_value(id(color_brightness_slider), id(active_brightness_pct), LV_ANIM_OFF);
|
||||
|
||||
bool show_tabs = id(active_supports_color) && id(active_supports_temperature);
|
||||
if (show_tabs) {
|
||||
lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) {
|
||||
int palette_y = show_tabs ? 96 : 76;
|
||||
lv_obj_set_size(id(color_palette_card), 150, 86);
|
||||
lv_obj_set_y(id(color_palette_card), palette_y);
|
||||
lv_obj_set_pos(id(color_preset_red), 13, 4);
|
||||
lv_obj_set_pos(id(color_preset_orange), 60, 4);
|
||||
lv_obj_set_pos(id(color_preset_yellow), 107, 4);
|
||||
lv_obj_set_pos(id(color_preset_green), 13, 52);
|
||||
lv_obj_set_pos(id(color_preset_blue), 60, 52);
|
||||
lv_obj_set_pos(id(color_preset_purple), 107, 52);
|
||||
|
||||
lv_obj_set_size(id(temp_palette_card), 150, 126);
|
||||
lv_obj_set_y(id(temp_palette_card), palette_y);
|
||||
lv_obj_set_pos(id(temp_preset_2700), 44, 4);
|
||||
lv_obj_set_pos(id(temp_preset_4000), 44, 48);
|
||||
lv_obj_set_pos(id(temp_preset_6500), 44, 92);
|
||||
|
||||
lv_obj_set_y(id(color_brightness_card), 256);
|
||||
} else {
|
||||
int palette_y = show_tabs ? 98 : 78;
|
||||
lv_obj_set_size(id(color_palette_card), 286, 48);
|
||||
lv_obj_set_y(id(color_palette_card), palette_y);
|
||||
lv_obj_set_pos(id(color_preset_red), 14, 8);
|
||||
lv_obj_set_pos(id(color_preset_orange), 61, 8);
|
||||
lv_obj_set_pos(id(color_preset_yellow), 108, 8);
|
||||
lv_obj_set_pos(id(color_preset_green), 155, 8);
|
||||
lv_obj_set_pos(id(color_preset_blue), 202, 8);
|
||||
lv_obj_set_pos(id(color_preset_purple), 249, 8);
|
||||
|
||||
lv_obj_set_size(id(temp_palette_card), 286, 48);
|
||||
lv_obj_set_y(id(temp_palette_card), palette_y);
|
||||
lv_obj_set_pos(id(temp_preset_2700), 24, 8);
|
||||
lv_obj_set_pos(id(temp_preset_4000), 112, 8);
|
||||
lv_obj_set_pos(id(temp_preset_6500), 200, 8);
|
||||
|
||||
lv_obj_set_y(id(color_brightness_card), 165);
|
||||
}
|
||||
|
||||
id(active_color_detail_tab) = id(active_supports_color) ? 0 : 1;
|
||||
light_controls::open(id(active_entity), id(active_supports_color), id(active_supports_temperature), id(active_brightness_pct));
|
||||
- lvgl.widget.hide: brightness_overlay
|
||||
- lvgl.widget.show: color_detail_overlay
|
||||
- script.execute:
|
||||
id: show_color_detail_tab
|
||||
tab: !lambda "return id(active_color_detail_tab);"
|
||||
|
||||
- id: show_color_detail_tab
|
||||
mode: restart
|
||||
parameters:
|
||||
tab: int
|
||||
then:
|
||||
- lambda: |-
|
||||
if (tab == 0 && !id(active_supports_color) && id(active_supports_temperature)) {
|
||||
tab = 1;
|
||||
} else if (tab == 1 && !id(active_supports_temperature) && id(active_supports_color)) {
|
||||
tab = 0;
|
||||
}
|
||||
id(active_color_detail_tab) = tab;
|
||||
|
||||
bool show_tabs = id(active_supports_color) && id(active_supports_temperature);
|
||||
if (show_tabs) {
|
||||
lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
if (tab == 0 && id(active_supports_color)) {
|
||||
lv_obj_clear_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 2);
|
||||
} else if (tab == 1 && id(active_supports_temperature)) {
|
||||
lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 95);
|
||||
} else {
|
||||
lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
lv_obj_set_style_text_color(
|
||||
id(color_tab_label),
|
||||
id(active_supports_color) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93),
|
||||
0
|
||||
);
|
||||
lv_obj_set_style_text_color(
|
||||
id(temp_tab_label),
|
||||
id(active_supports_temperature) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93),
|
||||
0
|
||||
);
|
||||
if (tab == 0 && id(active_supports_color)) {
|
||||
lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xD1D1D6), 0);
|
||||
} else if (tab == 1 && id(active_supports_temperature)) {
|
||||
lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xD1D1D6), 0);
|
||||
lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xFFFFFF), 0);
|
||||
}
|
||||
|
||||
- id: color_brightness_preview
|
||||
parameters:
|
||||
value: int
|
||||
then:
|
||||
- lambda: |-
|
||||
id(active_brightness_pct) = value;
|
||||
char buf[12];
|
||||
snprintf(buf, sizeof(buf), "%d %%", value);
|
||||
lv_label_set_text(id(color_brightness_value), buf);
|
||||
|
||||
- id: color_brightness_commit
|
||||
parameters:
|
||||
@@ -5224,7 +4817,7 @@ script:
|
||||
- lambda: 'id(active_brightness_pct) = value;'
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";'
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true" && id(active_entity).rfind("light.", 0) == 0;'
|
||||
then:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
@@ -5232,37 +4825,36 @@ script:
|
||||
entity_id: !lambda "return id(active_entity);"
|
||||
brightness_pct: !lambda "return value;"
|
||||
|
||||
- id: set_light_color_name
|
||||
mode: queued
|
||||
- id: set_light_hue
|
||||
parameters:
|
||||
color: string
|
||||
value: int
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";'
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true" && id(active_supports_color) && id(active_entity).rfind("light.", 0) == 0;'
|
||||
then:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: !lambda "return id(active_entity);"
|
||||
color_name: !lambda "return color;"
|
||||
brightness_pct: !lambda "return id(active_brightness_pct);"
|
||||
data_template:
|
||||
hs_color: "{{ [hue | int, 100] }}"
|
||||
variables:
|
||||
hue: !lambda "return light_controls::clamp(value, 0, 360);"
|
||||
|
||||
- id: set_light_color_temperature
|
||||
mode: queued
|
||||
parameters:
|
||||
kelvin: int
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";'
|
||||
lambda: 'return std::string("${DIRECT_ACTIONS}") == "true" && id(active_supports_temperature) && id(active_entity).rfind("light.", 0) == 0 && light_controls::active->temperature_ready();'
|
||||
then:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: !lambda "return id(active_entity);"
|
||||
color_temp_kelvin: !lambda "return kelvin;"
|
||||
brightness_pct: !lambda "return id(active_brightness_pct);"
|
||||
color_temp_kelvin: !lambda "return light_controls::clamp(kelvin, light_controls::active->minimum, light_controls::active->maximum);"
|
||||
|
||||
- id: slider_preview
|
||||
parameters:
|
||||
@@ -5873,7 +5465,13 @@ script:
|
||||
condition:
|
||||
lambda: 'return id(active_control_kind) != "climate_temperature";'
|
||||
then:
|
||||
- lvgl.widget.show: brightness_overlay
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(active_supports_color) || id(active_supports_temperature);'
|
||||
then:
|
||||
- script.execute: open_color_detail
|
||||
else:
|
||||
- lvgl.widget.show: brightness_overlay
|
||||
|
||||
# UI refresh (values + time + styles) + render
|
||||
- id: ui_refresh
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace light_controls {
|
||||
inline int clamp(int value, int low, int high) { return std::max(low, std::min(high, value)); }
|
||||
// HA serializes list attributes as either JSON lists or Python tuples.
|
||||
inline bool first_number(const std::string &text, float &value) {
|
||||
const char *p = text.c_str();
|
||||
while (*p == ' ' || *p == '[' || *p == '(') ++p;
|
||||
char *end;
|
||||
float parsed = std::strtof(p, &end);
|
||||
if (end == p || !std::isfinite(parsed)) return false;
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
struct State {
|
||||
const char *entity = "";
|
||||
int hue = 0, kelvin = 3000, minimum = 0, maximum = 0;
|
||||
bool temperature_ready() const { return minimum >= 1000 && maximum <= 15000 && maximum > minimum; }
|
||||
void update(unsigned attribute, const std::string &text) {
|
||||
float number;
|
||||
if (!first_number(text, number)) return;
|
||||
if (attribute == 0 && number >= 0 && number <= 360) hue = clamp(std::lround(number), 0, 360);
|
||||
if (attribute == 1 && number >= 1000 && number <= 15000) kelvin = std::lround(number);
|
||||
if (attribute == 2) minimum = number >= 1000 && number <= 15000 ? std::lround(number) : 0;
|
||||
if (attribute == 3) maximum = number >= 1000 && number <= 15000 ? std::lround(number) : 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#ifndef LIGHT_CONTROLS_TEST
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "lvgl.h"
|
||||
#include <functional>
|
||||
namespace light_controls {
|
||||
inline State states[8];
|
||||
inline State fallback;
|
||||
inline State *active = &fallback;
|
||||
struct Row { lv_obj_t *box{}, *slider{}, *value{}; bool dirty = false; unsigned index{}; };
|
||||
inline Row rows[3];
|
||||
inline std::function<void(int)> commits[3];
|
||||
inline bool ready = false;
|
||||
inline bool demo = false;
|
||||
inline void state_updated(State *state);
|
||||
inline int top, spacing;
|
||||
inline void subscribe(const char *const *entities, size_t count) {
|
||||
static const char *attributes[] = {"hs_color", "color_temp_kelvin", "min_color_temp_kelvin", "max_color_temp_kelvin"};
|
||||
size_t slot = 0;
|
||||
for (size_t i = 0; i < count && slot < 8; ++i) {
|
||||
if (std::string(entities[i]).rfind("light.", 0) != 0) continue;
|
||||
bool duplicate = false;
|
||||
for (size_t j = 0; j < slot; ++j) if (std::string(states[j].entity) == entities[i]) duplicate = true;
|
||||
if (duplicate) continue;
|
||||
State *state = &states[slot++];
|
||||
state->entity = entities[i];
|
||||
for (unsigned a = 0; a < 4; ++a)
|
||||
esphome::api::global_api_server->subscribe_home_assistant_state(state->entity, attributes[a],
|
||||
[state, a](esphome::StringRef value) { state->update(a, std::string(value.c_str(), value.size())); state_updated(state); });
|
||||
}
|
||||
}
|
||||
inline void preview(Row &row) {
|
||||
int value = lv_slider_get_value(row.slider);
|
||||
if (row.index == 0) {
|
||||
lv_label_set_text_fmt(row.value, "%d°", value);
|
||||
lv_obj_set_style_bg_color(row.slider, lv_color_hsv_to_rgb(value % 360, 100, 100), LV_PART_KNOB);
|
||||
} else if (row.index == 1) {
|
||||
lv_label_set_text_fmt(row.value, "%d K", value);
|
||||
} else lv_label_set_text_fmt(row.value, "%d %%", value);
|
||||
}
|
||||
inline void event(lv_event_t *e) {
|
||||
auto &row = *static_cast<Row *>(lv_event_get_user_data(e));
|
||||
auto code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_VALUE_CHANGED) { row.dirty = true; preview(row); }
|
||||
if (code == LV_EVENT_PRESS_LOST) row.dirty = false;
|
||||
if (code == LV_EVENT_RELEASED && row.dirty) {
|
||||
row.dirty = false;
|
||||
int value = lv_slider_get_value(row.slider);
|
||||
if (row.index == 0) active->hue = value;
|
||||
if (row.index == 1) {
|
||||
if (!active->temperature_ready()) return;
|
||||
value = clamp(value, active->minimum, active->maximum);
|
||||
active->kelvin = value;
|
||||
}
|
||||
if (!demo && commits[row.index]) commits[row.index](value);
|
||||
}
|
||||
}
|
||||
inline lv_obj_t *plain(lv_obj_t *parent, int x, int y, int w, int h) {
|
||||
auto *obj = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(obj);
|
||||
lv_obj_remove_flag(obj, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE));
|
||||
lv_obj_set_pos(obj, x, y); lv_obj_set_size(obj, w, h);
|
||||
return obj;
|
||||
}
|
||||
inline void setup(lv_obj_t *parent, const lv_font_t *font, int width, int height) {
|
||||
if (ready) return;
|
||||
ready = true;
|
||||
bool large = width >= 480;
|
||||
top = large ? 94 : 55; spacing = large ? 110 : 58;
|
||||
int margin = large ? 24 : 16, w = width - 2 * margin;
|
||||
int track_x = 14, track_y = large ? 43 : 28, track_h = large ? 28 : 14;
|
||||
int track_w = w - 28;
|
||||
const char *names[] = {"Kleur", "Wittemperatuur", "Helderheid"};
|
||||
const uint32_t rainbow[] = {0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFF0000};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
auto &row = rows[i]; row.index = i;
|
||||
row.box = plain(parent, margin, top + i * spacing, w, spacing - 2);
|
||||
lv_obj_set_style_text_font(row.box, font, 0);
|
||||
lv_obj_set_style_text_color(row.box, lv_color_hex(0xEEEEF2), 0);
|
||||
auto *label = lv_label_create(row.box); lv_label_set_text(label, names[i]);
|
||||
lv_obj_set_pos(label, 2, 0);
|
||||
row.value = lv_label_create(row.box); lv_obj_align(row.value, LV_ALIGN_TOP_RIGHT, -2, 0);
|
||||
unsigned segments = i == 0 ? 6 : 1;
|
||||
for (unsigned n = 0; n < segments; ++n) {
|
||||
int start = n * track_w / segments, end = (n + 1) * track_w / segments;
|
||||
auto *stripe = plain(row.box, track_x + start, track_y, end - start, track_h);
|
||||
lv_obj_set_style_bg_opa(stripe, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(stripe, lv_color_hex(i == 0 ? rainbow[n] : i == 1 ? 0xFF9C32 : 0x40404A), 0);
|
||||
lv_obj_set_style_bg_grad_color(stripe, lv_color_hex(i == 0 ? rainbow[n + 1] : i == 1 ? 0xC6E6FF : 0xFFFFFF), 0);
|
||||
lv_obj_set_style_bg_grad_dir(stripe, LV_GRAD_DIR_HOR, 0);
|
||||
}
|
||||
row.slider = lv_slider_create(row.box);
|
||||
lv_obj_remove_style_all(row.slider);
|
||||
lv_obj_remove_flag(row.slider, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_pos(row.slider, track_x, track_y); lv_obj_set_size(row.slider, track_w, track_h);
|
||||
lv_obj_set_ext_click_area(row.slider, large ? 12 : 8);
|
||||
lv_obj_set_style_bg_opa(row.slider, LV_OPA_TRANSP, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(row.slider, LV_OPA_TRANSP, LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_opa(row.slider, LV_OPA_COVER, LV_PART_KNOB);
|
||||
lv_obj_set_style_bg_color(row.slider, lv_color_hex(0xFFFFFF), LV_PART_KNOB);
|
||||
lv_obj_set_style_radius(row.slider, LV_RADIUS_CIRCLE, LV_PART_KNOB);
|
||||
lv_obj_set_style_pad_all(row.slider, large ? 5 : 6, LV_PART_KNOB);
|
||||
lv_obj_set_style_border_width(row.slider, 2, LV_PART_KNOB);
|
||||
lv_obj_set_style_border_color(row.slider, lv_color_hex(0xFFFFFF), LV_PART_KNOB);
|
||||
lv_slider_set_range(row.slider, i == 0 ? 0 : i == 1 ? 2000 : 1, i == 0 ? 360 : i == 1 ? 6500 : 100);
|
||||
lv_obj_add_event_cb(row.slider, event, LV_EVENT_ALL, &row);
|
||||
preview(row);
|
||||
}
|
||||
}
|
||||
// Late capability attributes may arrive after opening the card. Enable the
|
||||
// waiting temperature slider without disturbing an in-progress gesture.
|
||||
inline void state_updated(State *state) {
|
||||
if (!ready || state != active || !state->temperature_ready()) return;
|
||||
auto &row = rows[1];
|
||||
if (!lv_obj_has_state(row.slider, LV_STATE_DISABLED)) return;
|
||||
lv_slider_set_range(row.slider, state->minimum, state->maximum);
|
||||
lv_slider_set_value(row.slider, state->kelvin, LV_ANIM_OFF);
|
||||
lv_obj_remove_state(row.slider, LV_STATE_DISABLED);
|
||||
preview(row); row.dirty = false;
|
||||
}
|
||||
inline void open(const std::string &entity, bool color, bool temperature, int brightness) {
|
||||
active = &fallback;
|
||||
for (auto &state : states) if (entity == state.entity) { active = &state; break; }
|
||||
int y = top;
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
auto &row = rows[i];
|
||||
bool visible = i == 0 ? color : i == 1 ? temperature : true;
|
||||
if (!visible) { lv_obj_add_flag(row.box, LV_OBJ_FLAG_HIDDEN); continue; }
|
||||
lv_obj_remove_flag(row.box, LV_OBJ_FLAG_HIDDEN); lv_obj_set_y(row.box, y); y += spacing;
|
||||
lv_obj_remove_state(row.slider, LV_STATE_DISABLED);
|
||||
if (i == 1 && !active->temperature_ready()) lv_obj_add_state(row.slider, LV_STATE_DISABLED);
|
||||
if (i == 1 && active->temperature_ready()) lv_slider_set_range(row.slider, active->minimum, active->maximum);
|
||||
lv_slider_set_value(row.slider, i == 0 ? active->hue : i == 1 ? active->kelvin : clamp(brightness, 1, 100), LV_ANIM_OFF);
|
||||
preview(row); row.dirty = false;
|
||||
if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, "Even wachten");
|
||||
}
|
||||
}
|
||||
inline bool self_test() {
|
||||
auto *previous = active;
|
||||
State saved_fallback = fallback;
|
||||
int saved_values[3]; bool saved_hidden[3];
|
||||
std::function<void(int)> saved[3];
|
||||
int calls[3] = {0, 0, 0};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
saved[i] = commits[i]; saved_values[i] = lv_slider_get_value(rows[i].slider);
|
||||
saved_hidden[i] = lv_obj_has_flag(rows[i].box, LV_OBJ_FLAG_HIDDEN);
|
||||
commits[i] = [&, i](int value) { if (value == (i == 0 ? 180 : i == 1 ? 4000 : 65)) ++calls[i]; };
|
||||
}
|
||||
fallback.minimum = 2200; fallback.maximum = 5000;
|
||||
open("diagnostic", true, true, 50);
|
||||
bool ok = true;
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
auto *slider = rows[i].slider;
|
||||
lv_slider_set_value(slider, i == 0 ? 180 : i == 1 ? 4000 : 65, LV_ANIM_OFF);
|
||||
for (int n = 0; n < 20; ++n) lv_obj_send_event(slider, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
ok &= calls[i] == 0;
|
||||
lv_obj_send_event(slider, LV_EVENT_RELEASED, nullptr);
|
||||
lv_obj_send_event(slider, LV_EVENT_RELEASED, nullptr);
|
||||
ok &= calls[i] == 1;
|
||||
lv_obj_send_event(slider, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
lv_obj_send_event(slider, LV_EVENT_PRESS_LOST, nullptr);
|
||||
lv_obj_send_event(slider, LV_EVENT_RELEASED, nullptr);
|
||||
ok &= calls[i] == 1;
|
||||
}
|
||||
for (unsigned mask = 0; mask < 4; ++mask) {
|
||||
open("diagnostic", mask & 1, mask & 2, 50);
|
||||
ok &= lv_obj_has_flag(rows[0].box, LV_OBJ_FLAG_HIDDEN) == !(mask & 1);
|
||||
ok &= lv_obj_has_flag(rows[1].box, LV_OBJ_FLAG_HIDDEN) == !(mask & 2);
|
||||
ok &= !lv_obj_has_flag(rows[2].box, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
fallback = saved_fallback;
|
||||
open(previous->entity, !saved_hidden[0], !saved_hidden[1], saved_values[2]);
|
||||
active = previous;
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
commits[i] = saved[i];
|
||||
lv_slider_set_value(rows[i].slider, saved_values[i], LV_ANIM_OFF);
|
||||
preview(rows[i]); rows[i].dirty = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#define LIGHT_CONTROLS_TEST
|
||||
#include "../light_controls.h"
|
||||
#include <cassert>
|
||||
int main() {
|
||||
using namespace light_controls;
|
||||
float value = -1;
|
||||
for (auto text : {"[120.5, 100]", "(120.5, 80.0)", "120.5"}) {
|
||||
assert(first_number(text, value)); assert(value == 120.5f);
|
||||
}
|
||||
for (auto text : {"None", "unknown", "unavailable", "[]", "nan", "inf", ""}) assert(!first_number(text, value));
|
||||
State a, b;
|
||||
a.update(0, "[239.6, 80]"); assert(a.hue == 240); assert(b.hue == 0);
|
||||
a.update(0, "None"); a.update(0, "[720, 100]"); assert(a.hue == 240);
|
||||
a.update(1, "2700"); a.update(1, "unavailable"); assert(a.kelvin == 2700);
|
||||
assert(!a.temperature_ready()); a.update(2, "2200"); assert(!a.temperature_ready());
|
||||
a.update(3, "5000"); assert(a.temperature_ready());
|
||||
assert(clamp(1800, a.minimum, a.maximum) == 2200);
|
||||
assert(clamp(6500, a.minimum, a.maximum) == 5000);
|
||||
a.update(3, "2000"); assert(!a.temperature_ready());
|
||||
a.update(3, "1e30"); assert(!a.temperature_ready());
|
||||
a.update(3, "5000"); assert(a.temperature_ready());
|
||||
assert(clamp(-1, 0, 360) == 0); assert(clamp(999, 0, 360) == 360);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FILES = ['.gitignore', 'README.md', 'AGENTS.md', 'requirements.txt', 'device.example.yaml',
|
||||
'calibration.example.yaml', 'secrets.yaml.example', 'cyd_ui.h', 'LICENSE',
|
||||
'calibration.example.yaml', 'secrets.yaml.example', 'cyd_ui.h', 'light_controls.h', 'LICENSE',
|
||||
'TILE_CONFIGURATION.md', 'CYD_STABILITY.md', 'TEST_RESULTS.md']
|
||||
FOLDERS = ['docs', 'tools', 'tests', 'components', 'diagnostics', 'fonts']
|
||||
SUFFIXES = {'.py', '.cpp', '.h', '.md', '.ttf', '.txt'}
|
||||
|
||||
Reference in New Issue
Block a user