From c316d99fd77f779adb4482f5beb853d5ed6f8c77 Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Sun, 13 Sep 2026 00:47:49 +0200 Subject: [PATCH] Fix switch confirmation and details; retain compact slider icons --- README.md | 6 ++ components/smart_display/runtime_model.h | 6 +- components/smart_display/runtime_tiles.h | 100 ++++++++++++++++++----- docs/TEST_RESULTS_0212.md | 71 ++++++++++++++++ guition-4848s040.yaml | 24 +++++- home-like-2432s028.yaml | 26 +++++- packages/cyd.yaml | 26 +++++- packages/guition.yaml | 24 +++++- screen_manager/CHANGELOG.md | 8 ++ tests/test_action_feedback.cpp | 36 +++++++- 10 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 docs/TEST_RESULTS_0212.md diff --git a/README.md b/README.md index 1bd9326..40db564 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ De firmware en ingebouwde CLI zijn getest met **ESPHome 2026.6.2**. Functies hangen af van de mogelijkheden die Home Assistant voor een entiteit meldt. De app moet blijven draaien om de schermen van actuele gegevens te voorzien. +Vanaf firmware **0.2.12** toont lang indrukken op een switch een grote +schakelaar. Een korte tik schakelt direct; de uitstand krijgt een grijs icoon. +De feedback stopt zodra Home Assistant de gewijzigde stand meldt, met een +minimum van 150 ms voor switches. Tegels met een mini-slider behouden hun +icoon; op CYD staan het icoon en tekstblok verticaal gecentreerd. + ## Installeren vanuit Home Assistant ### Heb ik ESPHome nodig? diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index aa251f4..34bf316 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -19,6 +19,9 @@ inline bool valid_entity(const std::string &entity) { if (domain == allowed) return true; return false; } +inline std::string state_revision(const std::string &state, const std::string &attributes) { + return state + "\n" + attributes; +} struct Tile { std::string entity, name, state, unit, modes, hvac_modes; std::array fan_speeds; @@ -40,7 +43,8 @@ struct Tile { std::string revision, pending_revision; uint32_t pending_since = 0; bool pending = false, confirmed = false, local_feedback = false; - bool loading(uint32_t now) const { return pending && (now-pending_since < 1000 || (!confirmed && !local_feedback && now-pending_since < 6000)); } + bool is_switch() const { return domain()=="switch" || domain()=="input_boolean"; } + bool loading(uint32_t now) const { return pending && (now-pending_since < (is_switch()?150u:1000u) || (!confirmed && !local_feedback && now-pending_since < 6000)); } bool awaiting_action(uint32_t now) const { return loading(now) && !local_feedback; } void begin(uint32_t now, bool local=false) { pending=true; pending_since=now; confirmed=false; local_feedback=local; pending_revision=revision; } void observe(const std::string &next) { revision=next; if (pending && revision!=pending_revision) confirmed=true; } diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 2f3d7c2..e9ccea4 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -23,6 +23,7 @@ inline esphome::ESPPreferenceObject swipe_preference; inline Model model; inline std::string inbox; inline const lv_font_t *watch_font = nullptr; +inline const lv_font_t *mini_icon_font = nullptr; inline void tick(); inline void refresh_detail(unsigned index); inline int active_index = -1; @@ -60,7 +61,7 @@ inline bool parse_settings(JsonObject obj, screen_settings::Settings &s) { return s.valid(); } inline std::function detail, detail_update; -struct Widgets { lv_obj_t *tile{}, *title{}, *value{}, *circle{}, *icon{}; size_t index{}; int cached_active = -1; lv_obj_t *slider{}, *progress{}; int title_x=0,title_y=0,value_x=0,value_y=0; const lv_font_t *value_font{}; }; +struct Widgets { lv_obj_t *tile{}, *title{}, *value{}, *circle{}, *icon{}; size_t index{}; int cached_active = -1; lv_obj_t *slider{}, *progress{}; int title_x=0,title_y=0,value_x=0,value_y=0; const lv_font_t *value_font{}, *icon_font{}; }; inline std::array widgets; inline bool fresh() { return model.ready() && esphome::millis() - last_received < 95000; } inline float number(JsonVariant value, float fallback = NAN) { @@ -134,8 +135,13 @@ inline std::string receive(const std::string &payload) { if (!model.accepts(index, entity)) { result = "Fout: verouderde tegel"; return false; } Tile &tile = model.tiles[index]; auto a = root["a"].as(); - std::string revision = string(root["state"]); serializeJson(a, revision); + // ArduinoJson clears its destination string: serialize attributes first, then add state. + std::string attributes; serializeJson(a, attributes); + std::string revision = state_revision(string(root["state"]), attributes); + bool was_confirmed=tile.confirmed; tile.observe(revision); + if(tile.pending && !tile.local_feedback && !was_confirmed && tile.confirmed) + ESP_LOGI("runtime_action","HA state received entity=%s elapsed=%u ms",entity.c_str(),(unsigned)(esphome::millis()-tile.pending_since)); auto options = root["o"]; tile.background = tile_palette::color(string(options["background"],16)); tile.tap = string(options["tap"]); if (tile.tap.empty()) tile.tap="auto"; @@ -208,6 +214,8 @@ inline void action(const std::string &service, const std::string &entity, const if(!key.empty()) {esphome::api::HomeassistantServiceMap param;param.key=esphome::StringRef(key);param.value=esphome::StringRef(value);request.data.push_back(param);} for(auto &tile:model.tiles) if(tile.entity==entity)tile.begin(esphome::millis()); esphome::api::global_api_server->send_homeassistant_action(request); + ESP_LOGI("runtime_action","Sent service=%s entity=%s",service.c_str(),entity.c_str()); + if(refresh)refresh(); } inline void setting_event(const std::string &key, int value) { if(inbox.empty())return; @@ -227,12 +235,15 @@ inline lv_obj_t *detail_actions[16]{}; inline unsigned detail_action_count=0; inline lv_obj_t *detail_status=nullptr; inline lv_obj_t *detail_badge_status=nullptr; +inline lv_obj_t *detail_switch=nullptr; inline lv_obj_t *detail_label(lv_obj_t *parent,const std::string &text,int x,int y,int width) { auto *label=lv_label_create(parent);lv_label_set_text(label,text.c_str());lv_obj_set_pos(label,x,y);lv_obj_set_width(label,width); lv_obj_set_style_text_font(label,detail_font,0);lv_obj_set_style_text_color(label,lv_color_hex(light_theme?0x202020:0xFFFFFF),0); lv_label_set_long_mode(label,LV_LABEL_LONG_DOT);lv_obj_set_height(label,lv_font_get_line_height(detail_font));return label; } inline std::string detail_state(const Tile &t){ + if(t.state=="on")return "Aan"; + if(t.state=="off")return "Uit"; if(t.state=="docked")return "In dock"; if(t.state=="cleaning")return "Bezig met schoonmaken"; if(t.state=="paused")return "Gepauzeerd"; @@ -304,7 +315,7 @@ inline void show_detail(unsigned index){ if(index>=model.count)return;detail_index=index;auto &t=model.tiles[index]; if(!detail_font)detail_font=lv_obj_get_style_text_font(widgets[0].title,LV_PART_MAIN); if(!detail_root){detail_root=lv_obj_create(lv_screen_active());lv_obj_remove_style_all(detail_root);lv_obj_set_size(detail_root,lv_pct(100),lv_pct(100));lv_obj_remove_flag(detail_root,LV_OBJ_FLAG_SCROLLABLE);} - detail_action_count=0;detail_status=nullptr;detail_badge_status=nullptr;lv_obj_clean(detail_root);lv_obj_remove_flag(detail_root,LV_OBJ_FLAG_HIDDEN);lv_obj_move_foreground(detail_root); + detail_action_count=0;detail_status=nullptr;detail_badge_status=nullptr;detail_switch=nullptr;lv_obj_clean(detail_root);lv_obj_remove_flag(detail_root,LV_OBJ_FLAG_HIDDEN);lv_obj_move_foreground(detail_root); lv_obj_set_style_bg_color(detail_root,lv_color_hex(light_theme?0xE7E7E7:0x202A38),0);lv_obj_set_style_bg_opa(detail_root,LV_OPA_COVER,0); int width=lv_display_get_horizontal_resolution(lv_display_get_default()), height=lv_display_get_vertical_resolution(lv_display_get_default()); bool large=width>=480;int pad=large?20:10, top=large?100:62, gap=large?12:6,bh=large?58:34,cw=(width-pad*2-gap)/2; @@ -313,7 +324,28 @@ inline void show_detail(unsigned index){ std::string state=detail_state(t); detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?60:35,width-2*pad); auto d=t.domain(); - if(d=="vacuum"){ + if(t.is_switch()){ + detail_label(detail_root,"Tik om te schakelen",pad,top,width-2*pad); + detail_switch=lv_switch_create(detail_root); + lv_obj_set_size(detail_switch,large?240:140,large?112:64); + lv_obj_set_pos(detail_switch,(width-(large?240:140))/2,top+(large?65:30)); + lv_obj_set_style_bg_color(detail_switch,lv_color_hex(0xB0B0B0),LV_PART_MAIN); + lv_obj_set_style_bg_color(detail_switch,lv_color_hex(0xFFB900),LV_PART_INDICATOR|LV_STATE_CHECKED); + lv_obj_set_style_bg_color(detail_switch,lv_color_hex(0xFFFFFF),LV_PART_KNOB); + lv_obj_set_style_opa(detail_switch,LV_OPA_50,LV_STATE_DISABLED); + if(t.state=="on")lv_obj_add_state(detail_switch,LV_STATE_CHECKED); + lv_obj_add_event_cb(detail_switch,[](lv_event_t *e){ + if(detail_index>=model.count)return; + auto &tile=model.tiles[detail_index];auto *control=lv_event_get_target_obj(e); + bool allowed=fresh() && tile.available() && !tile.awaiting_action(esphome::millis()) && + cyd::touch_guard.accept(esphome::millis(),350); + bool requested_on=lv_obj_has_state(control,LV_STATE_CHECKED); + // Only HA's reported state is authoritative, including a refused/failed command. + if(tile.state=="on")lv_obj_add_state(control,LV_STATE_CHECKED);else lv_obj_remove_state(control,LV_STATE_CHECKED); + if(allowed)action(tile.domain()+(requested_on?".turn_on":".turn_off"),tile.entity); + },LV_EVENT_VALUE_CHANGED,nullptr); + detail_actions[detail_action_count++]=detail_switch; + }else if(d=="vacuum"){ // Native shapes keep the robot crisp without image buffers or extra layers. auto shape=[&](lv_obj_t *parent,int x,int y,int w,int h,uint32_t color,int radius){ auto *o=lv_obj_create(parent);lv_obj_remove_style_all(o);lv_obj_set_pos(o,x,y);lv_obj_set_size(o,w,h); @@ -422,7 +454,7 @@ inline void event(lv_event_t *event) { active_index = w.index; tile.begin(esphome::millis(),true); if (detail) detail(tile); - } else { active_index=w.index;tile.begin(esphome::millis(),true);show_detail(w.index); } + } else { active_index=w.index;show_detail(w.index); } return; } if (d == "light" || d == "switch" || d == "input_boolean" || d == "fan") action(d + ".toggle", tile.entity); @@ -432,9 +464,11 @@ inline void event(lv_event_t *event) { inline void bind(size_t index, lv_obj_t *tile, lv_obj_t *title, lv_obj_t *value, lv_obj_t *circle, lv_obj_t *icon) { // Compact cards need room for two text lines and a separate dimmer track. if(lv_obj_get_height(tile)<=80){lv_obj_set_style_pad_top(tile,4,0);lv_obj_set_style_pad_bottom(tile,4,0);} + lv_obj_set_style_border_width(tile,1,0); lv_obj_update_layout(tile); widgets[index] = {tile, title, value, circle, icon, index}; auto &w=widgets[index]; w.title_x=lv_obj_get_x(title);w.title_y=lv_obj_get_y(title);w.value_x=lv_obj_get_x(value);w.value_y=lv_obj_get_y(value);w.value_font=lv_obj_get_style_text_font(value,LV_PART_MAIN); + w.icon_font=lv_obj_get_style_text_font(icon,LV_PART_MAIN); // Fixed one-line boxes prevent wrapped names from overlapping the state on both boards. lv_obj_set_height(title,lv_font_get_line_height(lv_obj_get_style_text_font(title,LV_PART_MAIN))); lv_obj_set_height(value,lv_font_get_line_height(w.value_font)); @@ -505,22 +539,32 @@ inline void render(lv_obj_t *room) { lv_obj_set_width(w.progress,pending ? (esphome::millis()/120%5+1)*(lv_obj_get_width(w.tile)-24)/5 : 0); lv_obj_set_style_text_opa(w.icon,pending ? LV_OPA_40 : LV_OPA_COVER,0); bool watch=t.display=="watch"; - lv_obj_set_style_text_font(w.value,watch && watch_font ? watch_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))); - lv_obj_set_pos(w.value,watch?0:w.value_x,watch?(lv_obj_get_height(w.tile)>80?42:19):w.value_y); - if(watch)lv_obj_add_flag(w.circle,LV_OBJ_FLAG_HIDDEN);else lv_obj_remove_flag(w.circle,LV_OBJ_FLAG_HIDDEN); - lv_obj_set_x(w.title,watch?0:w.title_x); bool mini=t.inline_control=="slider" && !watch && t.available(); bool large_tile=lv_obj_get_height(w.tile)>80; - lv_obj_set_y(w.title,mini?0:w.title_y); - if(mini){lv_obj_set_y(w.value,large_tile?24:lv_obj_get_height(w.title)+1);lv_obj_add_flag(w.circle,LV_OBJ_FLAG_HIDDEN);lv_obj_set_x(w.title,0);lv_obj_set_x(w.value,0);} - // Labels default to content-sized widths. Explicit bounds are necessary for - // LVGL ellipsis, and must expand again when the icon is hidden for a slider. + lv_obj_set_style_text_font(w.value,watch && watch_font ? watch_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))); lv_obj_update_layout(w.tile); - int content_width=lv_obj_get_content_width(w.tile); - lv_obj_set_width(w.title,std::max(1,content_width-(int)lv_obj_get_x(w.title))); - lv_obj_set_width(w.value,std::max(1,content_width-(int)lv_obj_get_x(w.value))); - lv_obj_set_size(w.slider,content_width,large_tile?28:8); + int content_width=lv_obj_get_content_width(w.tile),content_height=lv_obj_get_content_height(w.tile); + 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; + int slider_height=large_tile?28:8; + int header_height=mini?content_height-slider_height-(large_tile?6:3):content_height; + int text_y=std::max(0,(header_height-text_height)/2); + int circle_size=mini?(large_tile?36:24):(large_tile?54:36); + lv_obj_set_size(w.circle,circle_size,circle_size); + lv_obj_set_style_text_font(w.icon,mini && mini_icon_font ? mini_icon_font : w.icon_font,0); + lv_obj_center(w.icon); + if(watch)lv_obj_add_flag(w.circle,LV_OBJ_FLAG_HIDDEN);else lv_obj_remove_flag(w.circle,LV_OBJ_FLAG_HIDDEN); + int text_x=watch?0:mini?circle_size+(large_tile?8:6):w.title_x; + lv_obj_set_pos(w.title,text_x,watch?0:(mini || !large_tile)?text_y:w.title_y); + lv_obj_set_pos(w.value,watch?0:mini?text_x:w.value_x, + watch?(large_tile?42:19):(mini || !large_tile)?text_y+title_height+line_gap:w.value_y); + lv_obj_set_pos(w.circle,0,(mini || !large_tile)?std::max(0,(header_height-circle_size)/2):12); + // Use the requested coordinates: LVGL getters still return the previous + // layout until its next pass when a slot changes from watch/slider to normal. + lv_obj_set_width(w.title,std::max(1,content_width-text_x)); + lv_obj_set_width(w.value,std::max(1,content_width-(watch?0:mini?text_x:w.value_x))); + lv_obj_set_size(w.slider,content_width,slider_height); lv_obj_align(w.slider,LV_ALIGN_BOTTOM_MID,0,0); if(mini){lv_obj_remove_flag(w.slider,LV_OBJ_FLAG_HIDDEN);if(!lv_obj_has_state(w.slider,LV_STATE_PRESSED))lv_slider_set_value(w.slider,slider_value(t),LV_ANIM_OFF);} else lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN); @@ -530,7 +574,7 @@ inline void render(lv_obj_t *room) { if (w.cached_active == palette_state) continue; w.cached_active = palette_state; uint32_t accent = domain_accent(t); - auto color=lv_color_hex(accent); + auto color=lv_color_hex(t.is_switch() && !on ? 0x9E9E9E : accent); if(t.domain()=="light" && on && t.has_hs_color) color=lv_color_hsv_to_rgb(t.hue%360,t.saturation,100); bool dark_text=light_theme || t.background!=0; @@ -565,6 +609,20 @@ inline bool check_tile_geometry() { lv_obj_get_coords(w.slider,&track); fits=fits && value.y2=content.x1 && circle.x2=content.y1; + if(mini)fits=fits && circle.y280?6:3)-1:content.y2; + int center_twice=content.y1+header_bottom; + fits=fits && std::abs(circle.y1+circle.y2-center_twice)<=2 && + std::abs(title.y1+value.y2-center_twice)<=2; + } + } if(!fits)ESP_LOGE("ui_test","Tile geometry FAIL slot=%u title_y=%d..%d value_y=%d..%d content_y=%d..%d",(unsigned)w.index,title.y1,title.y2,value.y1,value.y2,content.y1,content.y2); if(w.indexget_lv_font(); + runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; runtime_tiles::rotation_supported = true; @@ -1057,6 +1059,26 @@ font: - "${MDI_GLYPH_10}" - "\U000F012C" # check + - file: 'fonts/materialdesignicons-webfont.ttf' + id: materialdesign_icons_mini + size: 26 + glyphs: + - "\U000F0335" + - "\U000F0210" + - "\U000F111C" + - "\U000F029A" + - "${MDI_GLYPH_1}" + - "${MDI_GLYPH_2}" + - "${MDI_GLYPH_3}" + - "${MDI_GLYPH_4}" + - "${MDI_GLYPH_5}" + - "${MDI_GLYPH_6}" + - "${MDI_GLYPH_7}" + - "${MDI_GLYPH_8}" + - "${MDI_GLYPH_9}" + - "${MDI_GLYPH_10}" + - "\U000F012C" # check + # Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap # bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. debug: diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index 29b31ba..cb5b7db 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.10" + SCREEN_FIRMWARE_VERSION: "0.2.12" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" @@ -779,6 +779,9 @@ esphome: project: name: maxgramser.esp_screen version: "${SCREEN_FIRMWARE_VERSION}" + platformio_options: + build_flags: + - -DLV_USE_SWITCH=1 includes: - cyd_ui.h - light_controls.h @@ -807,6 +810,7 @@ esphome: settings.night_end = atoi("${NIGHT_END_HOUR}") * 60; settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); runtime_tiles::watch_font = id(headline)->get_lv_font(); + runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; runtime_tiles::load_settings(); @@ -1245,6 +1249,26 @@ font: - "${MDI_GLYPH_10}" - "\U000F012C" # check + - file: 'fonts/materialdesignicons-webfont.ttf' + id: materialdesign_icons_mini + size: 18 + glyphs: + - "\U000F0335" + - "\U000F0210" + - "\U000F111C" + - "\U000F029A" + - "${MDI_GLYPH_1}" + - "${MDI_GLYPH_2}" + - "${MDI_GLYPH_3}" + - "${MDI_GLYPH_4}" + - "${MDI_GLYPH_5}" + - "${MDI_GLYPH_6}" + - "${MDI_GLYPH_7}" + - "${MDI_GLYPH_8}" + - "${MDI_GLYPH_9}" + - "${MDI_GLYPH_10}" + - "\U000F012C" # check + # Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap # bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. debug: diff --git a/packages/cyd.yaml b/packages/cyd.yaml index 04c3b42..f089964 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -59,7 +59,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.10" + SCREEN_FIRMWARE_VERSION: "0.2.12" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" @@ -783,6 +783,9 @@ esphome: project: name: maxgramser.esp_screen version: "${SCREEN_FIRMWARE_VERSION}" + platformio_options: + build_flags: + - -DLV_USE_SWITCH=1 includes: - - @@ -811,6 +814,7 @@ esphome: settings.night_end = atoi("${NIGHT_END_HOUR}") * 60; settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); runtime_tiles::watch_font = id(headline)->get_lv_font(); + runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; runtime_tiles::load_settings(); @@ -1236,6 +1240,26 @@ font: - "${MDI_GLYPH_10}" - "\U000F012C" # check + - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf" + id: materialdesign_icons_mini + size: 18 + glyphs: + - "\U000F0335" + - "\U000F0210" + - "\U000F111C" + - "\U000F029A" + - "${MDI_GLYPH_1}" + - "${MDI_GLYPH_2}" + - "${MDI_GLYPH_3}" + - "${MDI_GLYPH_4}" + - "${MDI_GLYPH_5}" + - "${MDI_GLYPH_6}" + - "${MDI_GLYPH_7}" + - "${MDI_GLYPH_8}" + - "${MDI_GLYPH_9}" + - "${MDI_GLYPH_10}" + - "\U000F012C" # check + # Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap # bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. debug: diff --git a/packages/guition.yaml b/packages/guition.yaml index 37702eb..5d38639 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.10" + SCREEN_FIRMWARE_VERSION: "0.2.12" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" @@ -591,6 +591,7 @@ esphome: platformio_options: build_flags: - -DLV_USE_SNAPSHOT=1 + - -DLV_USE_SWITCH=1 includes: - - @@ -619,6 +620,7 @@ esphome: settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); runtime_tiles::light_theme = true; runtime_tiles::watch_font = id(headline)->get_lv_font(); + runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; runtime_tiles::rotation_supported = true; @@ -1048,6 +1050,26 @@ font: - "${MDI_GLYPH_10}" - "\U000F012C" # check + - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf" + id: materialdesign_icons_mini + size: 26 + glyphs: + - "\U000F0335" + - "\U000F0210" + - "\U000F111C" + - "\U000F029A" + - "${MDI_GLYPH_1}" + - "${MDI_GLYPH_2}" + - "${MDI_GLYPH_3}" + - "${MDI_GLYPH_4}" + - "${MDI_GLYPH_5}" + - "${MDI_GLYPH_6}" + - "${MDI_GLYPH_7}" + - "${MDI_GLYPH_8}" + - "${MDI_GLYPH_9}" + - "${MDI_GLYPH_10}" + - "\U000F012C" # check + # Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap # bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. debug: diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 6fce726..9aa4fda 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,11 @@ +## Firmware 0.2.12 (app 0.2.11 blijft bruikbaar) + +- Switches herkennen nu een aan/uit-terugmelding met ongewijzigde attributen. Daarmee verdwijnt de onterechte wachttijd van zes seconden. Na HA-bevestiging geldt voor switches slechts 150 ms minimale feedback. +- Uitgeschakelde switches hebben een grijs icoon en een grijze icoonachtergrond. Een zelfgekozen pastel tegelachtergrond blijft behouden. +- Mini-sliders behouden een compact domeinicoon naast titel en waarde. CYD centreert iconen en tekst verticaal; de renderdiagnose controleert centrering en vrije ruimte boven de slider. +- Lang indrukken op een switch of input_boolean opent een grote native LVGL-schakelaar met de echte HA-status, ook op CYD. Openen verstuurt geen opdracht. +- Installeer de nieuwe schermfirmware via ESP Screens; geen appupdate of gewijzigde configuratie nodig. + ## 0.2.11 - Compact raster voor de pastelkleuren in de tegeleditor; voorkomt dat algemene formulierstijlen het palet in een lange kolom zetten. Alleen de app verandert; schermfirmware 0.2.10 blijft actueel. diff --git a/tests/test_action_feedback.cpp b/tests/test_action_feedback.cpp index b1f622d..0b10274 100644 --- a/tests/test_action_feedback.cpp +++ b/tests/test_action_feedback.cpp @@ -1,4 +1,36 @@ #include "components/smart_display/runtime_model.h" -#include #include "components/smart_display/cyd_ui.h" -int main(){cyd::TouchGuard guard;guard.begin(0);guard.consume();assert(!guard.accept(100,1));assert(!guard.accept_slider(100,1));guard.begin(200);assert(guard.accept(300,1));runtime_tiles::Tile t;t.revision="off";t.begin(100);assert(t.loading(200));assert(t.awaiting_action(200));t.observe("off");assert(!t.confirmed);t.observe("on");assert(t.confirmed);assert(t.loading(999));assert(!t.loading(1100));t.begin(2000);t.observe("on");assert(t.loading(7999));assert(!t.loading(8000));assert(!t.awaiting_action(8000));t.begin(0xFFFFFFF0);assert(t.loading(20));t.begin(100,true);assert(t.loading(1099));assert(!t.awaiting_action(1099));assert(!t.loading(1100));} +#include + +int main() { + cyd::TouchGuard guard; + guard.begin(0); guard.consume(); + assert(!guard.accept(100,1)); assert(!guard.accept_slider(100,1)); + guard.begin(200); assert(guard.accept(300,1)); + + runtime_tiles::Tile t; + t.revision="off"; t.begin(100); + assert(t.loading(200)); assert(t.awaiting_action(200)); + t.observe("off"); assert(!t.confirmed); + t.observe("on"); assert(t.confirmed); + assert(t.loading(999)); assert(!t.loading(1100)); + t.begin(2000); t.observe("on"); + assert(t.loading(7999)); assert(!t.loading(8000)); assert(!t.awaiting_action(8000)); + t.begin(0xFFFFFFF0); assert(t.loading(20)); + t.begin(100,true); + assert(t.loading(1099)); assert(!t.awaiting_action(1099)); assert(!t.loading(1100)); + + // A plain switch changes state with identical attributes. This must confirm + // immediately instead of leaving the tile blocked for the six-second timeout. + runtime_tiles::Tile sw; + sw.entity="switch.printer"; + sw.revision=runtime_tiles::state_revision("off","{}"); sw.begin(100); + assert(sw.loading(249)); + sw.observe(runtime_tiles::state_revision("on","{}")); + assert(!sw.loading(250)); + sw.begin(300); sw.observe(runtime_tiles::state_revision("on","{}")); + assert(sw.loading(1000)); // Repeated identical data is not confirmation. + assert(!sw.loading(6300)); + sw.entity="input_boolean.test"; sw.begin(0xFFFFFFF0); sw.observe("off"); + assert(sw.loading(20)); assert(!sw.loading(200)); +}