From 4af92e1b4c10af5aae0c0823883a6fb80b2cc3bd Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Sun, 13 Sep 2026 09:09:37 +0200 Subject: [PATCH] Smooth graphs, sun path card, skeleton page switches and editor drag & drop Firmware 0.2.15: trend graphs use a Catmull-Rom curve with a soft fill drawn by the card's draw event; sun.sun gets a wide sun-path card with horizon, arc and the sun at its current position; page switches show the new page's card frames immediately and fill them from a one-shot timer; the navigation is one full-width bar without a divider; the CYD adopts the Guition light palette in styles and overlays. App 0.2.13: pointer-based drag & drop (mouse and touch) to reorder tiles in the mockup and to drop entities from the list, sticky save bar, a new header with explained actions and a three-step guide, clearer firmware dialog. New sun, weather and clock tiles start wide in their best view. Co-Authored-By: Claude Fable 5.1 --- README.md | 7 +- components/smart_display/runtime_tiles.h | 159 ++++++++++++++-- diagnostics/send_layout.py | 2 +- docs/TEST_RESULTS_0215.md | 55 ++++++ guition-4848s040.yaml | 52 ++++-- home-like-2432s028.yaml | 223 ++++++++++++----------- packages/cyd.yaml | 223 ++++++++++++----------- packages/guition.yaml | 52 ++++-- screen_manager/CHANGELOG.md | 10 + screen_manager/app/core.py | 8 +- screen_manager/app/static/app.js | 143 +++++++++++---- screen_manager/app/static/index.html | 77 ++++++-- screen_manager/app/static/style.css | 114 +++++++++++- screen_manager/config.yaml | 2 +- tests/test_extended.py | 1 + 15 files changed, 807 insertions(+), 321 deletions(-) create mode 100644 docs/TEST_RESULTS_0215.md diff --git a/README.md b/README.md index f2d92c7..e99504a 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,10 @@ De firmware en ingebouwde CLI zijn getest met **ESPHome 2026.6.2**. - **Speciale kaarten (firmware 0.2.14+):** een **klok** (digitaal of analoog) als ingebouwde tegel, een **weersvoorspelling** met vijf dagen op een dubbelbrede kaart, een **grafiek** van de sensorgeschiedenis in de tegel, - **zonsopgang/-ondergang** (`sun.sun`), een **kookwekker** (`timer.*`, tikken - start of pauzeert) en **aanwezigheid** (`person.*`). Kies ze in de kiezer als - elke andere tegel; **Dubbelbreed** is een optie voor iedere tegel. + een **zonnebaan** (`sun.sun`: horizon met de zon tussen opkomst en + ondergang), een **kookwekker** (`timer.*`, tikken start of pauzeert) en + **aanwezigheid** (`person.*`). Kies ze in de kiezer als elke andere tegel of + sleep ze direct in de schermmockup; **Dubbelbreed** is een optie voor iedere tegel. - **Scherminstellingen:** standby-tijd, normale en gedimde helderheid, nachturen, klok, terug naar de hoofdpagina en optioneel swipen tussen pagina's. - **Guition-rotatie:** 0°, 90°, 180° of 270°, direct vanuit de beheerpagina. diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 87d55a5..f3de5db 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -28,6 +28,7 @@ inline const lv_font_t *mini_icon_font = nullptr; inline const lv_font_t *watch_value_font = nullptr, *watch_icon_font = nullptr; // Big digits for the clock card; the board profile sets it with the local time source. inline const lv_font_t *clock_font = nullptr; +inline lv_obj_t *room_label = nullptr; // remembered by render() so page switches can render synchronously inline std::function now_time; inline uint32_t now_epoch() { if (!now_time) return 0; auto t = now_time(); return t.is_valid() ? static_cast(t.timestamp) : 0; } inline void tick(); @@ -75,7 +76,10 @@ struct Widgets { int title_x=0,title_y=0,value_x=0,value_y=0; const lv_font_t *value_font{}, *icon_font{}; // Wide cards span both columns; custom cards (clock, forecast, graph) draw into `extra`. bool wide=false; int base_width=0; lv_obj_t *extra{}; std::string extra_mode; std::array parts{}; lv_point_precise_t *points{}; + // Soft area under a polyline (graph, sun path), painted by the extra container's draw event. + const lv_point_precise_t *fill_points{}; unsigned fill_count=0; int fill_x=0, fill_y=0, fill_base=0; lv_color_t fill_color{}; lv_opa_t fill_opa=0; }; +constexpr unsigned POINT_BUFFER = 128; inline std::array widgets; inline bool fresh() { return model.ready() && esphome::millis() - last_received < 95000; } inline float number(JsonVariant value, float fallback = NAN) { @@ -640,13 +644,47 @@ inline uint32_t domain_accent(const Tile &t) { inline void end_extra(Widgets &w) { if(!w.extra)return; lv_obj_add_flag(w.extra,LV_OBJ_FLAG_HIDDEN); + w.fill_points=nullptr;w.fill_count=0; if(!w.extra_mode.empty()){lv_obj_clean(w.extra);w.parts.fill(nullptr);w.extra_mode.clear();delete[] w.points;w.points=nullptr;} } +// Two triangles per segment between the polyline and its baseline. No canvas +// buffer is needed, so the CYD can afford it as well. +inline void extra_draw(lv_event_t *e) { + auto &w=*static_cast(lv_event_get_user_data(e)); + if(!w.fill_points || w.fill_count<2 || !w.fill_opa)return; + auto *layer=lv_event_get_layer(e);lv_area_t area;lv_obj_get_coords(w.extra,&area); + lv_draw_triangle_dsc_t dsc;lv_draw_triangle_dsc_init(&dsc);dsc.color=w.fill_color;dsc.opa=w.fill_opa; + const lv_value_precise_t ox=area.x1+w.fill_x, oy=area.y1+w.fill_y, base=oy+w.fill_base; + for(unsigned i=0;i+1minimum?(t.history[i]-minimum)/(maximum-minimum):0.5f; - w.points[n++]={(lv_value_precise_t)(stroke/2+i*(width-stroke-1)/23),(lv_value_precise_t)(height-stroke/2-1-level*(height-stroke-2))}; + raw[n++]={(lv_value_precise_t)(stroke/2+i*(width-stroke-1)/23),(lv_value_precise_t)(height-stroke/2-1-level*(height-stroke-1-top))}; } - if(n==1){w.points[1]=w.points[0];w.points[1].x=(lv_value_precise_t)(width-1);n=2;} - part_line(w,0,w.points,n,stroke,x,y); + if(n==1){raw[1]=raw[0];raw[1].x=(lv_value_precise_t)(width-1);n=2;} + unsigned count=smooth(raw,n,w.points,POINT_BUFFER,width,height); + part_line(w,0,w.points,count,stroke,x,y); + w.fill_points=w.points;w.fill_count=count;w.fill_x=x;w.fill_y=y;w.fill_base=height;w.fill_opa=LV_OPA_20; +} +// Sun path: horizon, an arc from sunrise to sunset and the sun at the current +// position (or below the horizon at night). Wide cards only. +inline int minutes_of(const std::string &clock) { + unsigned h=0,m=0;return sscanf(clock.c_str(),"%u:%u",&h,&m)==2 && h<24 && m<60 ? int(h*60+m) : -1; +} +inline void render_sunpath(Widgets &w,const Tile &t,bool large,int width,int height) { + begin_extra(w,"sunpath",width,height); + const lv_font_t *title_font=lv_obj_get_style_text_font(w.title,LV_PART_MAIN); + int title_h=lv_font_get_line_height(title_font),text_h=lv_font_get_line_height(w.value_font); + int horizon=height-text_h-(large?4:2),top=title_h+(large?4:2),x0=large?14:8,x1=width-x0; + part_label(w,0,title_font,0,0,width,LV_TEXT_ALIGN_LEFT,t.name.empty()?"Zon":t.name); + part_label(w,1,w.value_font,0,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_LEFT,"op "+t.sunrise); + part_label(w,2,w.value_font,width/2,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_RIGHT,"onder "+t.sunset); + auto now=now_time?now_time():esphome::ESPTime{}; + int rise=minutes_of(t.sunrise),set=minutes_of(t.sunset),minute=now.is_valid()?now.hour*60+now.minute:-1; + bool day=t.state=="above_horizon";float fraction=0.5f; + if(rise>=0 && set>=0 && minute>=0){ + if(day){int span=(set-rise+1440)%1440;if(!span)span=1;fraction=std::clamp(float((minute-rise+1440)%1440)/span,0.0f,1.0f);} + else{int span=(rise-set+1440)%1440;if(!span)span=1;fraction=std::clamp(float((minute-set+1440)%1440)/span,0.0f,1.0f);} + } + const unsigned segments=40;float amplitude=day?float(horizon-top):float(height-text_h-horizon-(large?2:1)); + auto *arc=w.points,*travelled=w.points+segments+1,*line=w.points+2*segments+3; + for(unsigned i=0;i<=segments;++i){ + float a=3.14159265f*i/segments; + arc[i]={(lv_value_precise_t)(x0+(x1-x0)*float(i)/segments),(lv_value_precise_t)(day?horizon-sinf(a)*amplitude:horizon+sinf(a)*amplitude)}; + } + unsigned filled=std::min(segments,unsigned(fraction*segments)); + for(unsigned i=0;i<=filled;++i)travelled[i]=arc[i]; + float sa=3.14159265f*fraction; + lv_point_precise_t sun={(lv_value_precise_t)(x0+(x1-x0)*fraction),(lv_value_precise_t)(day?horizon-sinf(sa)*amplitude:horizon+sinf(sa)*amplitude)}; + travelled[filled+1]=sun; + line[0]={(lv_value_precise_t)0,(lv_value_precise_t)horizon};line[1]={(lv_value_precise_t)(width-1),(lv_value_precise_t)horizon}; + bool dark_text=light_theme || t.background!=0; + uint32_t path=dark_text?0xCFD8DC:0x6B8299, accent=day?0xFF9800:0x5C6BC0, disc=day?0xFFB300:0xB0BEC5; + lv_obj_set_style_line_color(part_line(w,3,line,2,2),lv_color_hex(path),0); + lv_obj_set_style_line_color(part_line(w,4,arc,segments+1,large?2:1),lv_color_hex(path),0); + lv_obj_set_style_line_color(part_line(w,5,travelled,filled+2,large?4:3),lv_color_hex(accent),0); + int size=large?18:10,glow=size+(large?12:6); + auto *halo=part_dot(w,6,int(sun.x)-glow/2,int(sun.y)-glow/2,glow);lv_obj_set_style_bg_color(halo,lv_color_hex(disc),0);lv_obj_set_style_bg_opa(halo,LV_OPA_30,0); + lv_obj_set_style_bg_color(part_dot(w,7,int(sun.x)-size/2,int(sun.y)-size/2,size),lv_color_hex(disc),0); + if(day){w.fill_points=travelled;w.fill_count=filled+2;w.fill_x=0;w.fill_y=0;w.fill_base=horizon;w.fill_color=lv_color_hex(0xFFB300);w.fill_opa=LV_OPA_20;} } inline void render(lv_obj_t *room) { if (!enabled) return; + room_label=room; label(room, !model.configured ? "Kies tegels in HA" : !model.ready() ? "Tegels laden..." : !fresh() ? "HA niet verbonden" : model.title); for (size_t slot = 0; slot < 6; ++slot) { auto &w=widgets[slot]; @@ -777,8 +860,9 @@ inline void render(lv_obj_t *room) { bool large_tile=lv_obj_get_height(w.tile)>80; // Cards that replace the name/status layout entirely. bool clock=t.builtin(), forecast=d=="weather" && t.display=="forecast" && w.wide && t.forecast_count>0 && fresh() && t.available(); + bool sunpath=d=="sun" && t.display=="sunpath" && w.wide && !t.sunrise.empty() && !t.sunset.empty() && fresh() && t.available(); bool graph=d=="sensor" && t.display=="graph" && t.has_history && !clock; - bool custom=clock||forecast; + bool custom=clock||forecast||sunpath; if(!large_tile){lv_obj_set_style_pad_top(w.tile,watch||custom||graph?2:4,0);lv_obj_set_style_pad_bottom(w.tile,watch||custom||graph?2:4,0);} lv_obj_set_style_text_font(w.value,watch && watch_value_font ? watch_value_font : w.value_font,0); lv_obj_set_height(w.value,lv_font_get_line_height(lv_obj_get_style_text_font(w.value,LV_PART_MAIN))); @@ -787,7 +871,9 @@ inline void render(lv_obj_t *room) { for(auto *o:{w.title,w.value,w.circle,w.unit}){if(custom)lv_obj_add_flag(o,LV_OBJ_FLAG_HIDDEN);else lv_obj_remove_flag(o,LV_OBJ_FLAG_HIDDEN);} if(custom){ lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN); - if(clock)render_clock(w,t,large_tile,content_width,content_height);else render_forecast(w,t,large_tile,content_width,content_height); + if(clock)render_clock(w,t,large_tile,content_width,content_height); + else if(sunpath)render_sunpath(w,t,large_tile,content_width,content_height); + else render_forecast(w,t,large_tile,content_width,content_height); }else{ int title_height=lv_obj_get_height(w.title),value_height=lv_obj_get_height(w.value); int line_gap=large_tile?2:1,text_height=title_height+line_gap+value_height; @@ -862,9 +948,13 @@ inline void render(lv_obj_t *room) { lv_obj_set_style_text_color(w.title, title_color, 0); lv_obj_set_style_text_color(w.value, value_color, 0); // Custom parts follow the card palette: text like the title, lines/dots in the accent. + // The sun path sets its own colours on every render. + w.fill_color=color; for(unsigned i=0;i=2 && i%3==2 ? value_color : i==16 ? value_color : title_color,0); + bool muted=w.extra_mode=="forecast" ? i>=2 && i%3==2 : w.extra_mode=="sunpath" ? i>=1 : i==16; + if(lv_obj_check_type(p,&lv_label_class))lv_obj_set_style_text_color(p,muted?value_color:title_color,0); + else if(w.extra_mode=="sunpath")continue; else if(lv_obj_check_type(p,&lv_line_class))lv_obj_set_style_line_color(p,w.extra_mode=="graph"?color:i==13?icon_color:title_color,0); else lv_obj_set_style_bg_color(p,i==14?icon_color:value_color,0); } @@ -946,7 +1036,15 @@ inline unsigned page_count() { std::array placement; return pack(model.tiles,model.count,placement); } -inline void show_page(int &page, lv_obj_t *previous, lv_obj_t *next, lv_obj_t *number) { +// Page switches feel immediate: the new page's card frames (right widths, no +// contents, light skeleton style) appear in the very next frame, and a one-shot +// LVGL timer fills them in right after. Keepalives and re-packing on the same +// page apply directly. Nothing is allocated, so the CYD stays comfortable. +inline lv_obj_t *nav_prev=nullptr,*nav_next=nullptr,*nav_number=nullptr; +inline int applied_page=-1,target_page=0; +inline lv_timer_t *page_timer=nullptr; +// Slot assignment plus card widths and visibility for a page; contents are untouched. +inline int place_page(int page) { std::array placement; int pages=pack(model.tiles,model.count,placement); page=std::clamp(page,0,pages-1); @@ -958,14 +1056,41 @@ inline void show_page(int &page, lv_obj_t *previous, lv_obj_t *next, lv_obj_t *n if(slot1)lv_obj_remove_flag(control,LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(control,LV_OBJ_FLAG_HIDDEN); } - if(page==0)lv_obj_add_state(previous,LV_STATE_DISABLED);else lv_obj_remove_state(previous,LV_STATE_DISABLED); - if(page==pages-1)lv_obj_add_state(next,LV_STATE_DISABLED);else lv_obj_remove_state(next,LV_STATE_DISABLED); - std::string caption=std::to_string(page+1)+" / "+std::to_string(pages);lv_label_set_text(number,caption.c_str()); - if(refresh)refresh(); + if(page==0)lv_obj_add_state(nav_prev,LV_STATE_DISABLED);else lv_obj_remove_state(nav_prev,LV_STATE_DISABLED); + if(page==pages-1)lv_obj_add_state(nav_next,LV_STATE_DISABLED);else lv_obj_remove_state(nav_next,LV_STATE_DISABLED); + for(auto *control:{nav_prev,nav_next})if(lv_obj_get_child_count(control)) + lv_obj_set_style_text_opa(lv_obj_get_child(control,0),lv_obj_has_state(control,LV_STATE_DISABLED)?LV_OPA_30:LV_OPA_COVER,0); + std::string caption=std::to_string(page+1)+" / "+std::to_string(pages);lv_label_set_text(nav_number,caption.c_str()); + return page; +} +inline void apply_page(int page) { + applied_page=place_page(page); + if(room_label)render(room_label);else if(refresh)refresh(); +} +inline void skeleton_page(int page) { + place_page(page); + uint32_t bg=light_theme?0xF4F5F7:0x4A6076, border=light_theme?0xE3E5E8:0x6F879C; + for(size_t slot=0;slot=model.count || lv_obj_has_flag(w.tile,LV_OBJ_FLAG_HIDDEN))continue; const auto &t=model.tiles[w.index]; - if(t.builtin() || (t.domain()=="timer" && t.state=="active"))redraw=true; + if(t.builtin() || (t.domain()=="timer" && t.state=="active") || (t.domain()=="sun" && second%60==0))redraw=true; } } if(redraw && refresh)refresh(); diff --git a/diagnostics/send_layout.py b/diagnostics/send_layout.py index a6d659d..3d53aa4 100644 --- a/diagnostics/send_layout.py +++ b/diagnostics/send_layout.py @@ -23,7 +23,7 @@ def demo_layout(): {'entity': 'sensor.demo_temperatuur', 'name': 'Woonkamer', 'options': {'display': 'graph'}}, {'entity': 'person.demo', 'name': 'Max', 'options': {}}, {'entity': 'timer.demo', 'name': 'Eieren', 'options': {}}, - {'entity': 'sun.sun', 'name': 'Zon', 'options': {}}, + {'entity': 'sun.sun', 'name': 'Zon', 'options': {'display': 'sunpath'}}, {'entity': 'sensor.demo_energie', 'name': 'Verbruik vandaag', 'options': {'display': 'graph', 'size': 'wide'}}, {'entity': 'light.demo', 'name': 'Tafellamp', 'options': {'display': 'watch', 'size': 'wide'}}, {'entity': 'weather.demo_standaard', 'name': 'Weer nu', 'options': {}}, diff --git a/docs/TEST_RESULTS_0215.md b/docs/TEST_RESULTS_0215.md new file mode 100644 index 0000000..9f89cc8 --- /dev/null +++ b/docs/TEST_RESULTS_0215.md @@ -0,0 +1,55 @@ +# App 0.2.13 / firmware 0.2.15 — zonnebaan, gladde grafiek, editor-UX + +## Wijzigingen + +- **Grafiek**: Catmull-Rom-curve door dezelfde 24 historiepunten (vier + tussenpunten per segment) met een zachte vulling eronder. De vulling wordt + door een `LV_EVENT_DRAW_MAIN`-tekenevent van de kaartcontainer als + driehoeken getekend; er is geen canvasbuffer nodig, dus ook de CYD kan het. +- **Zonnebaan** (`sun.sun`, `display: sunpath`, altijd dubbelbreed): horizon, + boog van opkomst naar ondergang, de zon op de huidige positie met een gloed + en een vulling van het afgelegde deel; ’s nachts loopt de zon onder de + horizon in indigo. De positie wordt uit de HH:MM-tijden van de manager en + de lokale tijd berekend en ververst elke minuut. +- **Navigatie**: één onderbalk over de volle breedte met een doorlopende + bovenlijn; linkerhelft = vorige, rechterhelft = volgende, paginanummer in + het midden. Een uitgeschakelde helft dimt zijn tekst. +- **Paginawissel**: `show_page` zet in hetzelfde frame de kaartkaders van de + nieuwe pagina neer (juiste breedtes via `pack`, inhoud verborgen, lichte + skeletonstijl) en een eenmalige LVGL-timer van 60 ms vult daarna de inhoud + synchroon in. Een eerdere fade-variant voelde traag en is vervangen. + Keepalives en herpakken op dezelfde pagina passen direct toe. +- **CYD-kleuren**: het Guition-schema is overgenomen — vlakke lichtgrijze + achtergrond (0xE7E7E7), witte kaarten met 0xDDDDDD-rand, donkere tekst, + lichtblauwe iconcirkels — inclusief alle bedieningsoverlays (75 kleurwaarden + één-op-één gelijk aan de Guition) en `light_theme` voor de runtimekaarten. +- **Navigatie** zonder scheidingslijn: alleen de teksten "< Vorige", + "Volgende >" en het paginanummer. +- **Editor**: pointer-gebaseerd slepen (muis en touch met kort vasthouden) + om tegels in de mockup te ordenen en entiteiten uit de lijst op een plek + te laten vallen, met auto-scroll bij de schermranden; sticky opslaan-balk; + nieuwe kop met drie uitgelegde acties en een stappenuitleg; verbeterde + firmwaredialoog. Zon-, weer- en kloktegels starten dubbelbreed in hun + mooiste weergave. + +## Tests + +- 55 Python-tests en negen C++-testprogramma's geslaagd; `--check` van de + pakketgenerator; beide Easy Setup-profielen gebouwd (CYD-flash 72,9 %). +- Editor in de browser met een gemockte inventory gecontroleerd: kop, stappen, + sticky opslaan-balk, dubbelbrede mockup. Slepen getest met gesimuleerde + pointer-events: tegel 4 → positie 1 en een entiteit uit de lijst → positie 2 + komen op de juiste plek; een sleep telt niet als klik. +- Beide borden via USB geflasht; `device_info` meldt 0.2.15. Guition-snapshots + van de demo-indeling: zonnebaan met zon op de boog en tijden, gladde + gevulde grafiek enkel- en dubbelbreed, nieuwe onderbalk. Demo-renderdiagnose + op de Guition 10/10 geslaagd (`count=9`). +- De eigenaar bevestigde het uiterlijk op de Guition, meldde tussenframes bij + paginawissels (foto met een achtergebleven sparkline) en vond de fade te + traag; daarop de skeletonwissel. Renderdiagnose op de echte indelingen van + beide borden na de laatste flash: zie hieronder. +- Laatste flash (Guition 08:59:18, CYD 09:03:32, beide 0.2.15): renderdiagnose + op de echte indelingen geslaagd — Guition 10/10 (`count=6`, heap ~6,46 MB), + CYD 10/10 (`count=7`, ~52,5 kB vrij). Beide handmatige profielen compileren. + De skeletonwissel en de CYD-kleuren zijn door de eigenaar visueel te + beoordelen; er is geen langdurige stabiliteitsproef uitgevoerd. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index d4ace4e..3ff5f2c 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -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.14" + SCREEN_FIRMWARE_VERSION: "0.2.15" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "guition-wallbox" @@ -3227,19 +3227,22 @@ lvgl: was_on: false # ---------- VALUE SLIDER OVERLAY ---------- + # ---------- PAGE NAVIGATION: one full-width bar ---------- - obj: id: page_prev align: BOTTOM_LEFT - x: 14 - y: -4 - width: 150 + x: 0 + y: 0 + width: 200 height: 44 pad_all: 0 - radius: 12 - bg_color: 0xEEEEEE - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3249,31 +3252,39 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "< Vorige" - text_font: sublabel + align: LEFT_MID + x: 24 + text: "< Vorige" + text_font: label text_color: 0x1B1B1B clickable: false - label: id: page_number align: BOTTOM_MID - y: -18 + y: 0 + width: 80 + height: 44 + pad_top: 12 + border_width: 0 text: "1 / 2" + text_align: CENTER text_font: sublabel text_color: 0x1B1B1B - obj: id: page_next align: BOTTOM_RIGHT - x: -14 - y: -4 - width: 150 + x: 0 + y: 0 + width: 200 height: 44 pad_all: 0 - radius: 12 - bg_color: 0xEEEEEE - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3283,9 +3294,10 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "Volgende >" - text_font: sublabel + align: RIGHT_MID + x: -24 + text: "Volgende >" + text_font: label text_color: 0x1B1B1B clickable: false diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index b2cdc84..c9dc16e 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.14" + SCREEN_FIRMWARE_VERSION: "0.2.15" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" @@ -643,8 +643,8 @@ substitutions: OVERLAY_SLIDER_W: "96" OVERLAY_SLIDER_H: "160" OVERLAY_ICON_Y: "74" - BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition + BG_BOTTOM_COLOR: "0xE7E7E7" TILE_ICON_X: "0" TILE_ICON_Y: "0" TILE_TITLE_X: "48" @@ -809,6 +809,7 @@ esphome: settings.night_start = atoi("${NIGHT_START_HOUR}") * 60; settings.night_end = atoi("${NIGHT_END_HOUR}") * 60; settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); + runtime_tiles::light_theme = true; runtime_tiles::watch_font = id(headline)->get_lv_font(); runtime_tiles::watch_value_font = id(watch_value)->get_lv_font(); runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); @@ -898,7 +899,7 @@ esphome: - lambda: |- static const char *entities[] = {"${TILE1_ENTITY}", "${TILE2_ENTITY}", "${TILE3_ENTITY}", "${TILE4_ENTITY}", "${TILE5_ENTITY}", "${TILE6_ENTITY}", "${TILE7_ENTITY}", "${TILE9_ENTITY}"}; if (!runtime_tiles::enabled) 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::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}"), true); 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); }; @@ -2059,7 +2060,9 @@ lvgl: bg_color: 0xFFFFFF bg_opa: COVER radius: 18 - border_width: 0 + border_width: 1 + border_color: 0xDDDDDD + border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 @@ -2068,10 +2071,12 @@ lvgl: shadow_opa: 40% - id: style_tile_disabled - bg_color: 0x939391 + bg_color: 0xFFFFFF bg_opa: COVER radius: 18 - border_width: 0 + border_width: 1 + border_color: 0xDDDDDD + border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 @@ -2080,25 +2085,25 @@ lvgl: shadow_opa: 25% - id: style_icon_circle - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER radius: 999 border_width: 0 - id: style_icon_circle_disabled - bg_color: 0x7B7B6F + bg_color: 0xEEEEEE bg_opa: COVER radius: 999 border_width: 0 - id: style_room text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_time text_font: time_label - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_title @@ -2113,17 +2118,17 @@ lvgl: - id: style_value_big text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 80% - id: style_title_disabled text_font: label - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_value_disabled text_font: sublabel - text_color: 0xD9D9D9 + text_color: 0x1B1B1B text_opa: 100% pages: @@ -3364,19 +3369,22 @@ lvgl: was_on: false # ---------- VALUE SLIDER OVERLAY ---------- + # ---------- PAGE NAVIGATION: one full-width bar ---------- - obj: id: page_prev align: BOTTOM_LEFT - x: 9 - y: -2 - width: 100 + x: 0 + y: 0 + width: 130 height: 32 pad_all: 0 - radius: 8 - bg_color: 0x3C3C40 - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3386,31 +3394,39 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "< Vorige" - text_font: sublabel - text_color: 0xFFFFFF + align: LEFT_MID + x: 12 + text: "< Vorige" + text_font: label + text_color: 0x1B1B1B clickable: false - label: id: page_number align: BOTTOM_MID - y: -9 + y: 0 + width: 60 + height: 32 + pad_top: 9 + border_width: 0 text: "1 / 2" + text_align: CENTER text_font: sublabel - text_color: 0xFFFFFF + text_color: 0x1B1B1B - obj: id: page_next align: BOTTOM_RIGHT - x: -9 - y: -2 - width: 100 + x: 0 + y: 0 + width: 130 height: 32 pad_all: 0 - radius: 8 - bg_color: 0x3C3C40 - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3420,10 +3436,11 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "Volgende >" - text_font: sublabel - text_color: 0xFFFFFF + align: RIGHT_MID + x: -12 + text: "Volgende >" + text_font: label + text_color: 0x1B1B1B clickable: false - obj: @@ -3460,7 +3477,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 18% border_width: 0 clickable: false @@ -3491,7 +3508,7 @@ lvgl: value: 100 adv_hittest: false - bg_color: 0x939391 + bg_color: 0xFFFFFF bg_opa: 100% radius: 28 # Enige widget met ZOWEL radius ALS een kind (overlay_icon). @@ -3504,7 +3521,7 @@ lvgl: border_width: 0 indicator: - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: 100% radius: 28 @@ -3536,7 +3553,7 @@ lvgl: align: CENTER y: ${OVERLAY_ICON_Y} text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B text: "${TILE1_ICON}" clickable: false @@ -3745,7 +3762,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x171719 + bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false @@ -3771,7 +3788,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 72% border_width: 0 clickable: false @@ -3785,7 +3802,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3799,7 +3816,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -3832,7 +3849,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- CLIMATE DETAIL OVERLAY ---------- @@ -3842,7 +3859,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x171719 + bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false @@ -3868,7 +3885,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 62% border_width: 0 clickable: false @@ -3884,7 +3901,7 @@ lvgl: width: 44 height: 44 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3898,7 +3915,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -3909,7 +3926,7 @@ lvgl: width: 44 height: 44 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -3921,7 +3938,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -3937,7 +3954,7 @@ lvgl: y: 42 text: "Current: --" text_font: sublabel_big - text_color: 0xD1E8FF + text_color: 0x1B1B1B text_opa: 85% clickable: false @@ -3988,7 +4005,7 @@ lvgl: y: -16 text: "SET TO" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 68% clickable: false @@ -3998,7 +4015,7 @@ lvgl: y: 20 text: "20.0°" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4009,7 +4026,7 @@ lvgl: width: 48 height: 36 radius: 18 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4025,7 +4042,7 @@ lvgl: align: CENTER text: "-" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4036,7 +4053,7 @@ lvgl: width: 48 height: 36 radius: 18 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4052,7 +4069,7 @@ lvgl: align: CENTER text: "+" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4062,7 +4079,7 @@ lvgl: width: 126 height: 34 radius: 17 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 82% border_width: 0 clickable: true @@ -4075,7 +4092,7 @@ lvgl: align: CENTER text: "Auto" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- CLIMATE MODE PICKER ---------- # Vervangt het doorcyclen van modi. Cyclen stuurt bij elke tik een @@ -4089,7 +4106,7 @@ lvgl: width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 90% border_width: 0 clickable: true @@ -4101,7 +4118,7 @@ lvgl: y: 16 text: "Mode" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: @@ -4128,7 +4145,7 @@ lvgl: width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4141,14 +4158,14 @@ lvgl: align: CENTER text: "Off" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4161,14 +4178,14 @@ lvgl: align: CENTER text: "Heat" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_cool width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4181,14 +4198,14 @@ lvgl: align: CENTER text: "Cool" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat_cool width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4201,14 +4218,14 @@ lvgl: align: CENTER text: "Heat/Cool" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_auto width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4221,14 +4238,14 @@ lvgl: align: CENTER text: "Auto" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_dry width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4241,14 +4258,14 @@ lvgl: align: CENTER text: "Dry" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_fan_only width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4261,7 +4278,7 @@ lvgl: align: CENTER text: "Fan" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4272,7 +4289,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4286,7 +4303,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4297,7 +4314,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -4309,7 +4326,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- VACUUM CONTROL ---------- @@ -4322,7 +4339,7 @@ lvgl: width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 90% border_width: 0 clickable: true @@ -4335,7 +4352,7 @@ lvgl: y: 16 text: "Vacuum" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4360,7 +4377,7 @@ lvgl: width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4373,14 +4390,14 @@ lvgl: align: CENTER text: "Start" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_pause width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4393,14 +4410,14 @@ lvgl: align: CENTER text: "Pause" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_return_to_base width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4413,14 +4430,14 @@ lvgl: align: CENTER text: "Dock" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_stop width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4433,7 +4450,7 @@ lvgl: align: CENTER text: "Stop" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -4441,7 +4458,7 @@ lvgl: y: 138 text: "Suction" text_font: sublabel - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4466,7 +4483,7 @@ lvgl: width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4481,14 +4498,14 @@ lvgl: align: CENTER text: "Quiet" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_balanced width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4503,14 +4520,14 @@ lvgl: align: CENTER text: "Balanced" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_turbo width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4525,14 +4542,14 @@ lvgl: align: CENTER text: "Turbo" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_max width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4547,7 +4564,7 @@ lvgl: align: CENTER text: "Max" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4558,7 +4575,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4572,7 +4589,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4583,7 +4600,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -4595,7 +4612,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- DIM / WAKE OVERLAY ---------- diff --git a/packages/cyd.yaml b/packages/cyd.yaml index 03141b7..01c7c29 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -59,7 +59,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.14" + SCREEN_FIRMWARE_VERSION: "0.2.15" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" @@ -644,8 +644,8 @@ substitutions: OVERLAY_SLIDER_W: "96" OVERLAY_SLIDER_H: "160" OVERLAY_ICON_Y: "74" - BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition + BG_BOTTOM_COLOR: "0xE7E7E7" TILE_ICON_X: "0" TILE_ICON_Y: "0" TILE_TITLE_X: "48" @@ -813,6 +813,7 @@ esphome: settings.night_start = atoi("${NIGHT_START_HOUR}") * 60; settings.night_end = atoi("${NIGHT_END_HOUR}") * 60; settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); + runtime_tiles::light_theme = true; runtime_tiles::watch_font = id(headline)->get_lv_font(); runtime_tiles::watch_value_font = id(watch_value)->get_lv_font(); runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); @@ -902,7 +903,7 @@ esphome: - lambda: |- static const char *entities[] = {"${TILE1_ENTITY}", "${TILE2_ENTITY}", "${TILE3_ENTITY}", "${TILE4_ENTITY}", "${TILE5_ENTITY}", "${TILE6_ENTITY}", "${TILE7_ENTITY}", "${TILE9_ENTITY}"}; if (!runtime_tiles::enabled) 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::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}"), true); 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); }; @@ -1992,7 +1993,9 @@ lvgl: bg_color: 0xFFFFFF bg_opa: COVER radius: 18 - border_width: 0 + border_width: 1 + border_color: 0xDDDDDD + border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 @@ -2001,10 +2004,12 @@ lvgl: shadow_opa: 40% - id: style_tile_disabled - bg_color: 0x939391 + bg_color: 0xFFFFFF bg_opa: COVER radius: 18 - border_width: 0 + border_width: 1 + border_color: 0xDDDDDD + border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 @@ -2013,25 +2018,25 @@ lvgl: shadow_opa: 25% - id: style_icon_circle - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER radius: 999 border_width: 0 - id: style_icon_circle_disabled - bg_color: 0x7B7B6F + bg_color: 0xEEEEEE bg_opa: COVER radius: 999 border_width: 0 - id: style_room text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_time text_font: time_label - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_title @@ -2046,17 +2051,17 @@ lvgl: - id: style_value_big text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 80% - id: style_title_disabled text_font: label - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 100% - id: style_value_disabled text_font: sublabel - text_color: 0xD9D9D9 + text_color: 0x1B1B1B text_opa: 100% pages: @@ -3297,19 +3302,22 @@ lvgl: was_on: false # ---------- VALUE SLIDER OVERLAY ---------- + # ---------- PAGE NAVIGATION: one full-width bar ---------- - obj: id: page_prev align: BOTTOM_LEFT - x: 9 - y: -2 - width: 100 + x: 0 + y: 0 + width: 130 height: 32 pad_all: 0 - radius: 8 - bg_color: 0x3C3C40 - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3319,31 +3327,39 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "< Vorige" - text_font: sublabel - text_color: 0xFFFFFF + align: LEFT_MID + x: 12 + text: "< Vorige" + text_font: label + text_color: 0x1B1B1B clickable: false - label: id: page_number align: BOTTOM_MID - y: -9 + y: 0 + width: 60 + height: 32 + pad_top: 9 + border_width: 0 text: "1 / 2" + text_align: CENTER text_font: sublabel - text_color: 0xFFFFFF + text_color: 0x1B1B1B - obj: id: page_next align: BOTTOM_RIGHT - x: -9 - y: -2 - width: 100 + x: 0 + y: 0 + width: 130 height: 32 pad_all: 0 - radius: 8 - bg_color: 0x3C3C40 - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3353,10 +3369,11 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "Volgende >" - text_font: sublabel - text_color: 0xFFFFFF + align: RIGHT_MID + x: -12 + text: "Volgende >" + text_font: label + text_color: 0x1B1B1B clickable: false - obj: @@ -3393,7 +3410,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 18% border_width: 0 clickable: false @@ -3424,7 +3441,7 @@ lvgl: value: 100 adv_hittest: false - bg_color: 0x939391 + bg_color: 0xFFFFFF bg_opa: 100% radius: 28 # Enige widget met ZOWEL radius ALS een kind (overlay_icon). @@ -3437,7 +3454,7 @@ lvgl: border_width: 0 indicator: - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: 100% radius: 28 @@ -3469,7 +3486,7 @@ lvgl: align: CENTER y: ${OVERLAY_ICON_Y} text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B text: "${TILE1_ICON}" clickable: false @@ -3678,7 +3695,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x171719 + bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false @@ -3704,7 +3721,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 72% border_width: 0 clickable: false @@ -3718,7 +3735,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3732,7 +3749,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -3765,7 +3782,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- CLIMATE DETAIL OVERLAY ---------- @@ -3775,7 +3792,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x171719 + bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false @@ -3801,7 +3818,7 @@ lvgl: y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 62% border_width: 0 clickable: false @@ -3817,7 +3834,7 @@ lvgl: width: 44 height: 44 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3831,7 +3848,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -3842,7 +3859,7 @@ lvgl: width: 44 height: 44 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -3854,7 +3871,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -3870,7 +3887,7 @@ lvgl: y: 42 text: "Current: --" text_font: sublabel_big - text_color: 0xD1E8FF + text_color: 0x1B1B1B text_opa: 85% clickable: false @@ -3921,7 +3938,7 @@ lvgl: y: -16 text: "SET TO" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B text_opa: 68% clickable: false @@ -3931,7 +3948,7 @@ lvgl: y: 20 text: "20.0°" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -3942,7 +3959,7 @@ lvgl: width: 48 height: 36 radius: 18 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3958,7 +3975,7 @@ lvgl: align: CENTER text: "-" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -3969,7 +3986,7 @@ lvgl: width: 48 height: 36 radius: 18 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -3985,7 +4002,7 @@ lvgl: align: CENTER text: "+" text_font: headline - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -3995,7 +4012,7 @@ lvgl: width: 126 height: 34 radius: 17 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 82% border_width: 0 clickable: true @@ -4008,7 +4025,7 @@ lvgl: align: CENTER text: "Auto" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- CLIMATE MODE PICKER ---------- # Vervangt het doorcyclen van modi. Cyclen stuurt bij elke tik een @@ -4022,7 +4039,7 @@ lvgl: width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 90% border_width: 0 clickable: true @@ -4034,7 +4051,7 @@ lvgl: y: 16 text: "Mode" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: @@ -4061,7 +4078,7 @@ lvgl: width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4074,14 +4091,14 @@ lvgl: align: CENTER text: "Off" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4094,14 +4111,14 @@ lvgl: align: CENTER text: "Heat" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_cool width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4114,14 +4131,14 @@ lvgl: align: CENTER text: "Cool" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat_cool width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4134,14 +4151,14 @@ lvgl: align: CENTER text: "Heat/Cool" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_auto width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4154,14 +4171,14 @@ lvgl: align: CENTER text: "Auto" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_dry width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4174,14 +4191,14 @@ lvgl: align: CENTER text: "Dry" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: cmode_fan_only width: 132 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4194,7 +4211,7 @@ lvgl: align: CENTER text: "Fan" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4205,7 +4222,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4219,7 +4236,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4230,7 +4247,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -4242,7 +4259,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- VACUUM CONTROL ---------- @@ -4255,7 +4272,7 @@ lvgl: width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true - bg_color: 0x000000 + bg_color: 0xFFFFFF bg_opa: 90% border_width: 0 clickable: true @@ -4268,7 +4285,7 @@ lvgl: y: 16 text: "Vacuum" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4293,7 +4310,7 @@ lvgl: width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4306,14 +4323,14 @@ lvgl: align: CENTER text: "Start" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_pause width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4326,14 +4343,14 @@ lvgl: align: CENTER text: "Pause" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_return_to_base width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4346,14 +4363,14 @@ lvgl: align: CENTER text: "Dock" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vac_stop width: 72 height: 38 radius: 19 - bg_color: 0x2C2C30 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4366,7 +4383,7 @@ lvgl: align: CENTER text: "Stop" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - label: @@ -4374,7 +4391,7 @@ lvgl: y: 138 text: "Suction" text_font: sublabel - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4399,7 +4416,7 @@ lvgl: width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4414,14 +4431,14 @@ lvgl: align: CENTER text: "Quiet" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_balanced width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4436,14 +4453,14 @@ lvgl: align: CENTER text: "Balanced" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_turbo width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4458,14 +4475,14 @@ lvgl: align: CENTER text: "Turbo" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: id: vacspd_max width: 72 height: 32 radius: 19 - bg_color: 0x1F1F23 + bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true @@ -4480,7 +4497,7 @@ lvgl: align: CENTER text: "Max" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4491,7 +4508,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0x3C3C40 + bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF @@ -4505,7 +4522,7 @@ lvgl: align: CENTER text: "x" text_font: sublabel_big - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false - obj: @@ -4516,7 +4533,7 @@ lvgl: width: 36 height: 36 radius: 999 - bg_color: 0xFEC600 + bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true @@ -4528,7 +4545,7 @@ lvgl: align: CENTER text: "\U000F012C" text_font: materialdesign_icons - text_color: 0xFFFFFF + text_color: 0x1B1B1B clickable: false # ---------- DIM / WAKE OVERLAY ---------- diff --git a/packages/guition.yaml b/packages/guition.yaml index ca54fb3..34c46a2 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -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.14" + SCREEN_FIRMWARE_VERSION: "0.2.15" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" @@ -3160,19 +3160,22 @@ lvgl: was_on: false # ---------- VALUE SLIDER OVERLAY ---------- + # ---------- PAGE NAVIGATION: one full-width bar ---------- - obj: id: page_prev align: BOTTOM_LEFT - x: 14 - y: -4 - width: 150 + x: 0 + y: 0 + width: 200 height: 44 pad_all: 0 - radius: 12 - bg_color: 0xEEEEEE - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3182,31 +3185,39 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "< Vorige" - text_font: sublabel + align: LEFT_MID + x: 24 + text: "< Vorige" + text_font: label text_color: 0x1B1B1B clickable: false - label: id: page_number align: BOTTOM_MID - y: -18 + y: 0 + width: 80 + height: 44 + pad_top: 12 + border_width: 0 text: "1 / 2" + text_align: CENTER text_font: sublabel text_color: 0x1B1B1B - obj: id: page_next align: BOTTOM_RIGHT - x: -14 - y: -4 - width: 150 + x: 0 + y: 0 + width: 200 height: 44 pad_all: 0 - radius: 12 - bg_color: 0xEEEEEE - bg_opa: COVER + radius: 0 + bg_opa: TRANSP border_width: 0 scrollable: false + pressed: + bg_color: 0xE3E3E3 + bg_opa: COVER on_short_click: - if: condition: @@ -3216,9 +3227,10 @@ lvgl: - script.execute: show_tile_page widgets: - label: - align: CENTER - text: "Volgende >" - text_font: sublabel + align: RIGHT_MID + x: -24 + text: "Volgende >" + text_font: label text_color: 0x1B1B1B clickable: false diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index df03e4b..90799c3 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.2.13 (firmware 0.2.15) + +- Editor: sleep tegels in de schermmockup om te ordenen en sleep entiteiten rechtstreeks uit de lijst naar een plek in de mockup — met muis én touch (even vasthouden). De opslaan-balk blijft altijd in beeld. +- Nieuwe kop met duidelijke acties (**Nieuw scherm**, **Firmware & USB**, **Uitleg**) en een korte uitleg in drie stappen; de firmwaredialoog legt profiel, doel en knoppen uit. +- **Zonnebaan**: `sun.sun` toont dubbelbreed een horizon met de zon op zijn huidige positie tussen opkomst en ondergang (’s nachts onder de horizon), met beide tijden. Nieuwe zon-, weer- en kloktegels starten meteen dubbelbreed in hun mooiste weergave. +- Grafiek: vloeiende curve door dezelfde 24 punten met een zachte vulling eronder, zoals de HA-trendkaart. +- Scherm: de paginering is één geïntegreerde onderbalk — links tikken is vorige, rechts is volgende, het paginanummer staat in het midden. +- Scherm: bij een paginawissel verschijnen direct de kaartkaders van de nieuwe pagina (skeleton) en vult de inhoud een fractie later; geen oude waarden meer in nieuwe kaders. +- CYD: hetzelfde lichte kleurenschema als de Guition — lichtgrijze achtergrond, witte kaarten met een fijne rand, donkere tekst en lichtblauwe accenten, ook in de bedieningskaarten. + ## 0.2.12 (firmware 0.2.14) - Nieuwe kaarten in de kiezer: **Klok** (digitaal of analoog, ingebouwd), **weersvoorspelling** met vijf dagen op een dubbelbrede weerkaart, **grafiek** van de sensorgeschiedenis in de tegel, **zon** (`sun.sun`, opkomst en ondergang in je eigen tijdzone), **kookwekker** (`timer.*`, live aftellen; tikken start of pauzeert, lang indrukken annuleert) en **aanwezigheid** (`person.*`). diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index eac384a..691b6de 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -30,7 +30,9 @@ TILE_BACKGROUNDS = { } # Display modes per domain; everything else offers standard and watch (large value). -DISPLAYS = {'weather': ('standard', 'watch', 'forecast'), 'sensor': ('standard', 'watch', 'graph'), 'screen': ('digital', 'analog')} +DISPLAYS = {'weather': ('standard', 'watch', 'forecast'), 'sensor': ('standard', 'watch', 'graph'), 'screen': ('digital', 'analog'), 'sun': ('standard', 'watch', 'sunpath')} +# Displays that only work on a double-width card. +WIDE_ONLY = ('forecast', 'sunpath') # Additive schema 1 extension. An absent object retains old firmware/YAML defaults. SETTING_RULES = { @@ -114,8 +116,8 @@ def validate_layout(data): for key, allowed in choices.items(): if key in options and options[key] not in allowed: raise ValueError('Ongeldige tegelinstelling: ' + key) - # The five-day strip only fits a double-width card. - if options.get('display') == 'forecast': + # The five-day strip and the sun path only fit a double-width card. + if options.get('display') in WIDE_ONLY: options = {**options, 'size': 'wide'} if options.get('tap') == 'toggle' and domain not in {'light','switch','input_boolean','fan','media_player'}: raise ValueError('Deze entiteit ondersteunt geen aan/uit-actie.') diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 2bf3a07..ca8d0b7 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -5,7 +5,6 @@ let inventory = { screens: [], entities: [] }, layout = null, dirty = false, filter = "", - dragIndex = -1, busy = false, selectedTile = null; const node = (tag, text, cls) => { @@ -225,7 +224,15 @@ const domains = { timer: ["Kookwekker", "⏱", "#008577", "#def3ed"], person: ["Persoon", "☺", "#2f7d32", "#e1f2e2"], }; -const displayNames = { standard: "standaard", watch: "grote waarde", forecast: "weersvoorspelling", graph: "grafiek", digital: "digitale klok", analog: "analoge klok" }; +const displayNames = { standard: "standaard", watch: "grote waarde", forecast: "weersvoorspelling", graph: "grafiek", digital: "digitale klok", analog: "analoge klok", sunpath: "zonnebaan" }; +// New tiles start with the card that shows the entity best. +function defaultOptions(id) { + const domain = id.split(".")[0]; + if (domain === "sun") return { options: { display: "sunpath", size: "wide" } }; + if (domain === "weather") return { options: { display: "forecast", size: "wide" } }; + if (domain === "screen") return { options: { display: "digital", size: "wide" } }; + return {}; +} // Same packing as the firmware: wide tiles start in the left column and take a whole row. function packTiles(tiles) { let position = 0; @@ -283,11 +290,8 @@ function renderPreview() { if(background)card.style.backgroundColor=background; if (place.wide) card.classList.add("wide"); card.append(domainBadge(tile.entity), node("strong", name)); - card.draggable = true; - card.ondragstart = (e) => { dragIndex = index; e.dataTransfer.setData("text/plain", String(index)); }; - card.ondragend = () => { dragIndex = -1; }; - card.ondragover = (e) => { if (dragIndex >= 0) e.preventDefault(); }; - card.ondrop = (e) => { e.preventDefault(); if (dragIndex >= 0) move(dragIndex, index); }; + card.dataset.index = index; + enableDrag(card, { kind: "tile", index }); card.classList.toggle("chosen", selectedTile === tile.entity); card.setAttribute("aria-label", `Tegel ${index + 1}: ${name}, instellen`); card.onclick = () => { @@ -322,27 +326,6 @@ function renderTiles() { if (i > 0 && placement[i].page !== placement[i - 1].page) $("#tiles").append(node("li", `Pagina ${placement[i].page + 1}`, "page-break")); const li = node("li", undefined, "tile"); li.classList.toggle("selected-tile", selectedTile === tile.entity); - li.draggable = true; - li.ondragstart = (e) => { - if (e.target.closest("input, select, button")) { - e.preventDefault(); - return; - } - dragIndex = i; - e.dataTransfer.setData("text/plain", String(i)); - li.classList.add("dragging"); - }; - li.ondragend = () => { - dragIndex = -1; - li.classList.remove("dragging"); - }; - li.ondragover = (e) => { - if (dragIndex >= 0) e.preventDefault(); - }; - li.ondrop = (e) => { - e.preventDefault(); - if (dragIndex >= 0) move(dragIndex, i); - }; const content = node("div"), input = node("input"); input.value = tile.name; @@ -423,6 +406,7 @@ function renderTiles() { : [["standard", "Naam en status"], ["watch", "Grote waarde"]]; if (domain === "weather") displays.push(["forecast", "Weersvoorspelling (dubbelbreed)"]); if (domain === "sensor") displays.push(["graph", "Grafiek van de geschiedenis"]); + if (domain === "sun") displays.push(["sunpath", "Zonnebaan (dubbelbreed)"]); const fields = [ [ "tap", @@ -498,7 +482,7 @@ function renderTiles() { }; if (key === "display" && input.value === "watch") tile.options.inline = "none"; - if (key === "display" && input.value === "forecast") + if (key === "display" && ["forecast", "sunpath"].includes(input.value)) tile.options.size = "wide"; if (key === "inline" && input.value === "slider") tile.options.display = "standard"; @@ -522,6 +506,97 @@ function renderTiles() { $("#tiles").append(li); }); } +function addTile(id, at) { + if (layout.tiles.some((t) => t.entity === id) || layout.tiles.length >= tileLimit()) return; + layout.tiles.splice(Math.min(at, layout.tiles.length), 0, { entity: id, name: "", ...defaultOptions(id) }); + selectedTile = id; + markDirty(); + renderTiles(); + renderResults(); +} +// Pointer-based drag & drop: works with mouse and touch, from the picker into +// the mockup and between tiles. Touch starts after a short hold so the page +// still scrolls; a finished drag never doubles as a click. +const drag = { active: false, source: null, element: null, ghost: null, target: null, timer: 0, start: null, offset: null, pointerId: null, suppressUntil: 0, last: null, scroller: 0 }; +function enableDrag(element, source) { + element.addEventListener("pointerdown", (e) => { + if (e.button !== 0 || element.disabled) return; + Object.assign(drag, { source, element, start: { x: e.clientX, y: e.clientY }, pointerId: e.pointerId, active: false }); + clearTimeout(drag.timer); + if (e.pointerType === "touch") drag.timer = setTimeout(() => beginDrag(e), 260); + }); + element.addEventListener("pointermove", (e) => { + if (!drag.start || drag.element !== element) return; + const distance = Math.hypot(e.clientX - drag.start.x, e.clientY - drag.start.y); + if (!drag.active) { + if (e.pointerType === "touch") { if (distance > 10) { clearTimeout(drag.timer); drag.start = null; } return; } + if (distance < 6) return; + beginDrag(e); + } + moveDrag(e); + }); + const finish = (e) => { + if (drag.element !== element) return; + clearTimeout(drag.timer); + if (drag.active) endDrag(e.type === "pointerup"); + drag.start = null; + }; + element.addEventListener("pointerup", finish); + element.addEventListener("pointercancel", finish); +} +function beginDrag(e) { + if (drag.active || !drag.start) return; + drag.active = true; + try { drag.element.setPointerCapture(drag.pointerId); } catch {} + const rect = drag.element.getBoundingClientRect(); + const ghost = drag.element.cloneNode(true); + ghost.classList.add("drag-ghost"); + ghost.style.width = `${rect.width}px`; + drag.offset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; + document.body.append(ghost); + drag.ghost = ghost; + document.body.classList.add("dragging"); + document.addEventListener("touchmove", blockScroll, { passive: false }); + // Near the viewport edges the page scrolls along, so the mockup can be reached on small screens. + drag.scroller = setInterval(() => { + if (!drag.last) return; + const step = drag.last.y < 70 ? -12 : drag.last.y > innerHeight - 70 ? 12 : 0; + if (step) { window.scrollBy(0, step); setTarget(document.elementFromPoint(drag.last.x, drag.last.y)?.closest(".preview-tile, .preview-gap, .preview-grid") || null); } + }, 16); + moveDrag(e); +} +function blockScroll(e) { if (drag.active) e.preventDefault(); } +function moveDrag(e) { + if (!drag.ghost) return; + drag.last = { x: e.clientX, y: e.clientY }; + drag.ghost.style.transform = `translate(${e.clientX - drag.offset.x}px, ${e.clientY - drag.offset.y}px)`; + const under = document.elementFromPoint(e.clientX, e.clientY); + setTarget(under?.closest(".preview-tile, .preview-gap, .preview-grid") || null); +} +function setTarget(target) { + if (drag.target === target) return; + drag.target?.classList.remove("drop-target"); + drag.target = target; + drag.target?.classList.add("drop-target"); +} +function endDrag(drop) { + const { target, source } = drag; + setTarget(null); + drag.ghost?.remove(); + drag.ghost = null; + drag.active = false; + drag.suppressUntil = Date.now() + 400; + clearInterval(drag.scroller); + drag.last = null; + document.body.classList.remove("dragging"); + document.removeEventListener("touchmove", blockScroll); + try { drag.element.releasePointerCapture(drag.pointerId); } catch {} + if (!drop || !target) return; + const to = target.dataset.index !== undefined ? Number(target.dataset.index) : layout.tiles.length; + if (source.kind === "tile") { if (to !== source.index) move(source.index, to); } + else addTile(source.id, to); +} +window.addEventListener("click", (e) => { if (Date.now() < drag.suppressUntil) { e.stopPropagation(); e.preventDefault(); } }, true); function renderResults() { if (!layout) return; const query = $("#search").value.toLocaleLowerCase(); @@ -550,13 +625,8 @@ function renderResults() { node("span", chosen.has(entity.id) ? "✓" : "+", "plus"), ); b.disabled = chosen.has(entity.id) || layout.tiles.length >= tileLimit(); - b.onclick = () => { - layout.tiles.push({ entity: entity.id, name: "" }); - selectedTile = entity.id; - markDirty(); - renderTiles(); - renderResults(); - }; + b.onclick = () => addTile(entity.id, layout.tiles.length); + if (!b.disabled) enableDrag(b, { kind: "entity", id: entity.id }); $("#results").append(b); } if (!matches.length) @@ -845,6 +915,7 @@ function openSection(id) { section.scrollIntoView({ behavior: "smooth", block: "start" }); } $("#nav-tiles").onclick = () => openSection("#tile-section"); +$("#open-help").onclick = () => openSection("#help"); $("#nav-settings").onclick = () => openSection("#general-settings"); $("#nav-inspector").onclick = () => inspect(); async function inspect(entity) { diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index 4d05fd2..e28fa7d 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -12,11 +12,29 @@
▦
- ESP ScreensJe huis. Jouw bediening. + ESP ScreensTegels voor je CYD en Guition, zonder opnieuw flashen
- - +
@@ -27,6 +45,26 @@
Verbinden… +
    +
  1. + 1 · KoppelenKies Nieuw scherm voor de installatie-YAML met eigen + sleutels. Eén keer via USB flashen, daarna gaat alles draadloos. +
  2. +
  3. + 2 · Tegels kiezenSleep entiteiten uit de lijst rechts in de schermmockup, of tik + op +. Klik op een tegel voor weergave, breedte en kleur. +
  4. +
  5. + 3 · OpslaanMet Opslaan & naar scherm staat het direct op je scherm. + Nieuwe schermfuncties installeer je via Firmware & USB. +
  6. +
@@ -271,13 +318,19 @@
-

Firmware & USB

- +
+

FIRMWARE & USB

+

Schermfirmware bouwen en installeren

+
+

- De ingebouwde ESPHome CLI gebruikt je eigen YAML en secrets. Tegels - blijven in ESP Screens staan. Op de Raspberry kan de eerste build enkele - minuten duren. + De ingebouwde ESPHome CLI gebruikt je eigen profiel (YAML met sleutels) + en secrets. Kies het profiel van het scherm, dan een USB-poort (scherm + aan de Home Assistant-machine) of Wifi / OTA met de hostnaam. + Controleren valideert alleen, Alleen bouwen compileert, + Bouwen & installeren zet de firmware op het scherm. Je tegels + blijven in ESP Screens staan. De eerste build duurt enkele minuten.