diff --git a/components/smart_display/cyd_ui.h b/components/smart_display/cyd_ui.h index b56ea07..4ebd739 100644 --- a/components/smart_display/cyd_ui.h +++ b/components/smart_display/cyd_ui.h @@ -26,6 +26,7 @@ class TouchGuard { // A contact that jumps to another target must never become a tile tap. if (std::abs(x - start_x_) > 18 || std::abs(y - start_y_) > 18) moved_ = true; } + void consume() { accepted_ = true; moved_ = true; } bool accept(uint32_t now, int tile) { return !moved_ && accept_slider(now, tile); } diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index c6e0f07..92a8977 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -6,7 +6,7 @@ #include namespace runtime_tiles { -constexpr size_t MAX_TILES = 10; +constexpr size_t MAX_TILES = 20; inline bool valid_entity(const std::string &entity) { if (entity.size() > 120) return false; auto dot = entity.find('.'); diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index eaefee7..b73ec09 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -14,6 +14,8 @@ namespace runtime_tiles { inline bool enabled = false; inline bool light_theme = false; +inline bool swipe_pages = false; +inline esphome::ESPPreferenceObject swipe_preference; inline Model model; inline std::string inbox; inline const lv_font_t *watch_font = nullptr; @@ -25,6 +27,9 @@ inline std::function layout_changed, refresh, dismiss, settings_changed; inline esphome::ESPPreferenceObject settings_preference; inline void load_settings() { settings_preference = esphome::global_preferences->make_preference(0x53435231); + swipe_preference = esphome::global_preferences->make_preference(0x53575031); + uint32_t swipe_saved=0; + if(swipe_preference.load(&swipe_saved))swipe_pages=swipe_saved==1; screen_settings::Settings saved; if (settings_preference.load(&saved) && saved.valid()) screen_settings::current = saved; } @@ -47,7 +52,7 @@ inline bool parse_settings(JsonObject obj, screen_settings::Settings &s) { } 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{}; }; -inline std::array widgets; +inline std::array widgets; inline bool fresh() { return model.ready() && esphome::millis() - last_received < 95000; } inline float number(JsonVariant value, float fallback = NAN) { if (!value.is() && !value.is()) return fallback; @@ -88,9 +93,13 @@ inline std::string receive(const std::string &payload) { if (!entity.is() || entities.size() == MAX_TILES) return false; entities.push_back(entity.as()); } + if(!root["swipe_pages"].isNull() && !root["swipe_pages"].is())return false; inbox = string(root["inbox"], 160); bool changed = false; if (!model.set_layout(entities, string(root["title"], 96), changed)) return false; + if(root["swipe_pages"].is() && swipe_pages!=root["swipe_pages"].as()){ + swipe_pages=root["swipe_pages"].as();uint32_t saved=swipe_pages?1:0;swipe_preference.save(&saved); + } if (!(settings == screen_settings::current)) { screen_settings::current = settings; settings_preference.save(&settings); // ESPHome batches flash writes; no write on keepalive. @@ -160,7 +169,7 @@ inline std::string receive(const std::string &payload) { tile.fan_speeds[tile.fan_speed_count++] = string(speed, 48); } tile.received = true; - widgets[index].cached_active=-1; + for(auto &w:widgets)if(w.index==index)w.cached_active=-1; last_received = esphome::millis(); if (refresh) refresh(); if (active_index == static_cast(index) && detail_update) detail_update(tile); @@ -247,7 +256,9 @@ inline void slider_event(lv_event_t *e){ if(code==LV_EVENT_VALUE_CHANGED && captured_slider==slider)slider_changed=true; if(code==LV_EVENT_PRESS_LOST && captured_slider==slider){captured_slider=nullptr;slider_changed=false;} if(code==LV_EVENT_RELEASED && captured_slider==slider){ - unsigned index=(uintptr_t)lv_event_get_user_data(e);bool changed=slider_changed;captured_slider=nullptr;slider_changed=false; + unsigned index=(uintptr_t)lv_event_get_user_data(e); + for(auto &w:widgets)if(w.slider==slider){index=w.index;break;} + bool changed=slider_changed;captured_slider=nullptr;slider_changed=false; if(changed && cyd::touch_guard.accept_slider(esphome::millis(),200+index))commit_slider(index,lv_slider_get_value(slider)); } } @@ -274,7 +285,7 @@ inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height } 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[index].title,LV_PART_MAIN); + 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); 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); @@ -414,6 +425,7 @@ inline void bind(size_t index, lv_obj_t *tile, lv_obj_t *title, lv_obj_t *value, lv_obj_set_style_bg_color(w.slider,lv_color_hex(0x111111),LV_PART_KNOB);lv_obj_set_style_pad_hor(w.slider,lv_obj_get_height(tile)>80?-10:0,LV_PART_KNOB);lv_obj_set_style_pad_ver(w.slider,lv_obj_get_height(tile)>80?-5:1,LV_PART_KNOB);lv_obj_set_style_radius(w.slider,14,LV_PART_MAIN);lv_obj_set_style_radius(w.slider,14,LV_PART_INDICATOR);lv_obj_set_style_radius(w.slider,3,LV_PART_KNOB);lv_obj_set_style_bg_color(w.slider,lv_color_hex(0xFCE5B4),LV_PART_MAIN);lv_obj_set_style_bg_color(w.slider,lv_color_hex(0xFFB900),LV_PART_INDICATOR); lv_obj_set_style_opa(w.slider,LV_OPA_TRANSP,LV_PART_KNOB); lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(w.slider,LV_OBJ_FLAG_GESTURE_BUBBLE); lv_obj_add_event_cb(w.slider,slider_event,LV_EVENT_ALL,(void*)(uintptr_t)index); lv_obj_add_event_cb(tile, event, LV_EVENT_SHORT_CLICKED, &widgets[index]); lv_obj_add_event_cb(tile, event, LV_EVENT_LONG_PRESSED, &widgets[index]); @@ -452,8 +464,10 @@ inline uint32_t domain_accent(const Tile &t) { inline void render(lv_obj_t *room) { if (!enabled) return; label(room, !model.configured ? "Kies tegels in HA" : !model.ready() ? "Tegels laden..." : !fresh() ? "HA niet verbonden" : model.title); - for (size_t i = 0; i < model.count; ++i) { - const auto &t = model.tiles[i]; auto &w = widgets[i]; + for (size_t slot = 0; slot < 6; ++slot) { + auto &w=widgets[slot]; + if(!w.tile || w.index>=model.count)continue; + const auto &t = model.tiles[w.index]; label(w.title, t.name.empty() ? t.entity : t.name); label(w.icon, icon_for(t)); std::string value = t.state; @@ -508,6 +522,26 @@ inline void render(lv_obj_t *room) { lv_obj_set_style_text_color(w.value, lv_color_hex(light_theme ? 0x616161 : (on ? 0x46525E : 0xF0F4F8)), 0); } } + +inline void show_page(int &page, lv_obj_t *previous, lv_obj_t *next, lv_obj_t *number) { + int pages=std::max(1,(int(model.count)+5)/6); + page=std::clamp(page,0,pages-1); + for(size_t slot=0;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(); +} + inline void tick() { if(detail_root && !lv_obj_has_flag(detail_root,LV_OBJ_FLAG_HIDDEN) && detail_index=0.2.7. Bij oudere +firmware blijft een grotere indeling bewaard zonder ongeldig doorsturen. + +Controles tot nu toe: 46 Python-tests, acht C++-tests en alle vier ESPHome-builds +(Easy/manual, Guition/CYD) geslaagd. Test twintig entities/21 berichten, overflow, +firmwaregate, oude instellingen op de draad en contact consumeren geslaagd. +Fysieke swipeacceptatie en live HA-update volgen hieronder. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index 38b0b5e..3b640d2 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.5" + SCREEN_FIRMWARE_VERSION: "0.2.7" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "guition-wallbox" @@ -629,6 +629,17 @@ esphome: lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN); }; runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); }; + lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) { + if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) || + !id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return; + auto *input=lv_indev_active();if(!input)return; + auto direction=lv_indev_get_gesture_dir(input); + if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + cyd::touch_guard.consume(); + lv_indev_wait_release(input); + id(tile_page)+=direction==LV_DIR_LEFT?1:-1; + id(show_tile_page).execute(); + }, LV_EVENT_GESTURE, nullptr); runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); }; runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl)); runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl)); @@ -3100,7 +3111,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - - lambda: 'id(tile_page) = 0;' + - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - script.execute: show_tile_page widgets: - label: @@ -3134,7 +3145,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 12);' then: - - lambda: 'id(tile_page) = 1;' + - lambda: 'id(tile_page) += 1;' - script.execute: show_tile_page widgets: - label: @@ -4495,6 +4506,10 @@ script: - id: show_tile_page then: - lambda: |- + if(runtime_tiles::enabled){ + runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number)); + return; + } lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}}; diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index 210844b..bf71a47 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.5" + SCREEN_FIRMWARE_VERSION: "0.2.7" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" @@ -821,6 +821,17 @@ esphome: lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN); }; runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); }; + lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) { + if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) || + !id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return; + auto *input=lv_indev_active();if(!input)return; + auto direction=lv_indev_get_gesture_dir(input); + if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + cyd::touch_guard.consume(); + lv_indev_wait_release(input); + id(tile_page)+=direction==LV_DIR_LEFT?1:-1; + id(show_tile_page).execute(); + }, LV_EVENT_GESTURE, nullptr); runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); }; runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl)); runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl)); @@ -3244,7 +3255,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - - lambda: 'id(tile_page) = 0;' + - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - script.execute: show_tile_page widgets: - label: @@ -3278,7 +3289,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 12);' then: - - lambda: 'id(tile_page) = 1;' + - lambda: 'id(tile_page) += 1;' - script.execute: show_tile_page widgets: - label: @@ -4625,6 +4636,10 @@ script: - id: show_tile_page then: - lambda: |- + if(runtime_tiles::enabled){ + runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number)); + return; + } lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}}; diff --git a/packages/cyd.yaml b/packages/cyd.yaml index 9cdce9e..bd513eb 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -59,7 +59,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.5" + SCREEN_FIRMWARE_VERSION: "0.2.7" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" @@ -825,6 +825,17 @@ esphome: lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN); }; runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); }; + lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) { + if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) || + !id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return; + auto *input=lv_indev_active();if(!input)return; + auto direction=lv_indev_get_gesture_dir(input); + if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + cyd::touch_guard.consume(); + lv_indev_wait_release(input); + id(tile_page)+=direction==LV_DIR_LEFT?1:-1; + id(show_tile_page).execute(); + }, LV_EVENT_GESTURE, nullptr); runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); }; runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl)); runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl)); @@ -3177,7 +3188,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - - lambda: 'id(tile_page) = 0;' + - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - script.execute: show_tile_page widgets: - label: @@ -3211,7 +3222,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 12);' then: - - lambda: 'id(tile_page) = 1;' + - lambda: 'id(tile_page) += 1;' - script.execute: show_tile_page widgets: - label: @@ -4558,6 +4569,10 @@ script: - id: show_tile_page then: - lambda: |- + if(runtime_tiles::enabled){ + runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number)); + return; + } lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}}; diff --git a/packages/guition.yaml b/packages/guition.yaml index e5343c1..cdcf7dd 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.5" + SCREEN_FIRMWARE_VERSION: "0.2.7" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" @@ -633,6 +633,17 @@ esphome: lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN); }; runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); }; + lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) { + if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) || + !id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return; + auto *input=lv_indev_active();if(!input)return; + auto direction=lv_indev_get_gesture_dir(input); + if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + cyd::touch_guard.consume(); + lv_indev_wait_release(input); + id(tile_page)+=direction==LV_DIR_LEFT?1:-1; + id(show_tile_page).execute(); + }, LV_EVENT_GESTURE, nullptr); runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); }; runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl)); runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl)); @@ -3033,7 +3044,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - - lambda: 'id(tile_page) = 0;' + - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - script.execute: show_tile_page widgets: - label: @@ -3067,7 +3078,7 @@ lvgl: condition: lambda: 'return cyd::touch_guard.accept(millis(), 12);' then: - - lambda: 'id(tile_page) = 1;' + - lambda: 'id(tile_page) += 1;' - script.execute: show_tile_page widgets: - label: @@ -4428,6 +4439,10 @@ script: - id: show_tile_page then: - lambda: |- + if(runtime_tiles::enabled){ + runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number)); + return; + } lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}}; diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 427367c..0f6a06d 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.7 + +- Tot twintig tegels op vier vaste pagina’s; installeer eerst firmware 0.2.7. Zes tegelvakken worden hergebruikt om geheugen te sparen. +- Optionele LVGL-swipes tussen pagina’s via scherminstellingen. Sliders en detailmenu’s wisselen niet van pagina; na een swipe wordt de aanraking geconsumeerd. + ## 0.2.6 - Nieuw scherm hergebruikt bestaande ESPHome-wifi-secrets automatisch. De wizard vraagt alleen wifi als secrets.yaml nog niet bestaat. Ontbrekende wifi-sleutels in een bestaand bestand worden gemeld zonder het bestand te overschrijven. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 2735567..0e2ad1a 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -23,6 +23,7 @@ SETTING_RULES = { 'show_clock': (True, None, None), 'clock_24h': (True, None, None), 'home_on_standby': (False, None, None), + 'swipe_pages': (False, None, None), } def validate_settings(data): @@ -54,8 +55,8 @@ def validate_layout(data): title, tiles = data.get('title'), data.get('tiles') if not isinstance(title, str) or not title.strip() or len(title.encode()) > 96: raise ValueError('Geef het scherm een titel van maximaal 96 bytes.') - if not isinstance(tiles, list) or len(tiles) > 10: - raise ValueError('Kies maximaal 10 tegels.') + if not isinstance(tiles, list) or len(tiles) > 20: + raise ValueError('Kies maximaal 20 tegels.') clean, seen = [], set() for tile in tiles: if not isinstance(tile, dict) or not entity_id(tile.get('entity')): diff --git a/screen_manager/app/server.py b/screen_manager/app/server.py index a69579a..3ec8dac 100644 --- a/screen_manager/app/server.py +++ b/screen_manager/app/server.py @@ -165,13 +165,25 @@ class Manager: def inventory(self): return discover(self.ha.registry, self.ha.states, self.ha.devices, self.ha.areas) + def supports_twenty(self, inbox): + screen=next((s for s in self.inventory()[0] if s['id']==inbox), {}) + try: + version=tuple(int(part) for part in screen.get('firmware','').split('.')) + return len(version)==3 and version >= (0,2,7) + except (ValueError, TypeError): + return False + def save(self, inbox, data): if inbox not in {s['id'] for s in self.inventory()[0]}: raise ValueError('Dit is geen gekoppeld ESP-scherm. Vernieuw het overzicht.') layout = validate_layout(data) + if len(layout["tiles"])>10 and not self.supports_twenty(inbox): + raise ValueError("Installeer eerst schermfirmware 0.2.7 of nieuwer voor meer dan tien tegels.") # A still-open older UI may save tiles without the new optional settings. if 'settings' not in layout and 'settings' in self.layouts.get(inbox, {}): layout['settings'] = self.layouts[inbox]['settings'].copy() + if 'settings' in layout and 'swipe_pages' not in data.get('settings',{}): + layout['settings']['swipe_pages']=self.layouts.get(inbox,{}).get('settings',{}).get('swipe_pages',False) old_tiles = {t['entity']:t for t in self.layouts.get(inbox,{}).get('tiles',[])} for tile in layout['tiles']: if 'options' not in tile and 'options' in old_tiles.get(tile['entity'],{}): @@ -194,9 +206,13 @@ class Manager: self.ha.changed.set() async def sync_one(self, inbox, layout, force=False): + if len(layout["tiles"])>10 and not self.supports_twenty(inbox): + self.status[inbox]="Indeling bewaard; firmware 0.2.7+ nodig voor meer dan tien tegels" + return messages = [{'v': 1, 'op': 'layout', 'inbox': inbox, 'title': layout['title'], 'entities': [t['entity'] for t in layout['tiles']]}] if 'settings' in layout: - messages[0]['settings'] = layout['settings'] + messages[0]['settings'] = {k:v for k,v in layout['settings'].items() if k!='swipe_pages'} + messages[0]['swipe_pages'] = layout['settings'].get('swipe_pages',False) for i,tile in enumerate(layout['tiles']): message=state_message(i,tile,self.ha.states) if tile['entity'].startswith('sensor.') and hasattr(self.ha,'history'): diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 0591777..4fca4f2 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -81,6 +81,7 @@ const settingDefinitions = [ ["show_clock", "Klok tonen", "check", true], ["clock_24h", "24-uursklok (uit = 12 uur)", "check", true], ["home_on_standby", "Na standby terug naar pagina 1", "check", false], + ["swipe_pages", "Vegen tussen pagina’s (firmware 0.2.7+)", "check", false], ]; function renderSettings() { const values = { @@ -223,6 +224,12 @@ function domainBadge(id) { badge.style.background = background; return badge; } +function tileLimit() { + const version=inventory.screens.find(s=>s.id===selected)?.firmware || ""; + const match=/^(\d+)\.(\d+)\.(\d+)$/.exec(version); + if(!match)return 10; + return Number(match[1])>0 || Number(match[2])>2 || Number(match[2])===2 && Number(match[3])>=7 ? 20 : 10; +} function renderPreview() { const root = $("#layout-preview"); root.replaceChildren(); @@ -231,7 +238,7 @@ function renderPreview() { const frame = node("section", undefined, "screen-preview"); frame.append(node("small", `Pagina ${page + 1} · ${$("#title").value || "Thuis"}`, "preview-heading")); const grid = node("div", undefined, "preview-grid"); - for (let slot = page * 6; slot < Math.min(page * 6 + 6, 10); slot++) { + for (let slot = page * 6; slot < Math.min(page * 6 + 6, tileLimit()); slot++) { const tile = layout.tiles[slot]; const card = node("button", undefined, "preview-tile"); if (tile) { @@ -265,7 +272,7 @@ function renderPreview() { function renderTiles() { renderPreview(); $("#tiles").replaceChildren(); - $("#count").textContent = `${layout.tiles.length} / 10`; + $("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware voor 20":""}`; $("#no-tiles").hidden = layout.tiles.length > 0; layout.tiles.forEach((tile, i) => { if (i === 6) $("#tiles").append(node("li", "Pagina 2", "page-break")); @@ -472,7 +479,7 @@ function renderResults() { description, node("span", chosen.has(entity.id) ? "✓" : "+", "plus"), ); - b.disabled = chosen.has(entity.id) || layout.tiles.length >= 10; + b.disabled = chosen.has(entity.id) || layout.tiles.length >= tileLimit(); b.onclick = () => { layout.tiles.push({ entity: entity.id, name: "" }); selectedTile = entity.id; diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index c85dafc..8ba0cd5 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.6" +version: "0.2.7" slug: esp_screen_manager description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_action_feedback.cpp b/tests/test_action_feedback.cpp index 76a4a8b..b1f622d 100644 --- a/tests/test_action_feedback.cpp +++ b/tests/test_action_feedback.cpp @@ -1,3 +1,4 @@ #include "components/smart_display/runtime_model.h" #include -int main(){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 "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));} diff --git a/tests/test_portal.py b/tests/test_portal.py index 8112836..e168eeb 100644 --- a/tests/test_portal.py +++ b/tests/test_portal.py @@ -20,7 +20,7 @@ class ProtocolTests(unittest.TestCase): with self.assertRaises(ValueError): packets({'huge':'x'*4096}) def test_layout_rejects_duplicates_unsupported_and_overflow(self): - for tiles in [[{'entity':'lock.front'}],[{'entity':'light.a'}]*2,[{'entity':f'light.a{i}'} for i in range(11)],[{'entity':'light.a;restart'}]]: + for tiles in [[{'entity':'lock.front'}],[{'entity':'light.a'}]*2,[{'entity':f'light.a{i}'} for i in range(21)],[{'entity':'light.a;restart'}]]: with self.assertRaises(ValueError): validate_layout({'title':'Thuis','tiles':tiles}) self.assertEqual(validate_layout({'title':'Thuis','tiles':[]})['tiles'],[]) @@ -122,11 +122,11 @@ class ManagerTests(unittest.IsolatedAsyncioTestCase): with self.assertRaises(ValueError): fresh.save('text.screen',{**saved,'settings':{'brightness':0}}) self.assertEqual(path.read_bytes(),before) await fresh.sync_one('text.screen',saved) - self.assertEqual(fresh.ha.messages[0][1]['settings'],settings) + self.assertEqual(fresh.ha.messages[0][1]['settings'],{k:v for k,v in settings.items() if k!='swipe_pages'}) await fresh.sync_one('text.screen',saved) self.assertEqual(len(fresh.ha.messages),2) await fresh.sync_one('text.screen',saved,True) - self.assertEqual(fresh.ha.messages[2][1]['settings'],settings) + self.assertEqual(fresh.ha.messages[2][1]['settings'],{k:v for k,v in settings.items() if k!='swipe_pages'}) async def test_reorder_aborts_old_batch(self): with tempfile.TemporaryDirectory() as temp: diff --git a/tests/test_runtime_model.cpp b/tests/test_runtime_model.cpp index 649ac3c..2d710b5 100644 --- a/tests/test_runtime_model.cpp +++ b/tests/test_runtime_model.cpp @@ -23,6 +23,10 @@ int main() { assert(!m.accepts(0, "light.a")); assert(m.set_layout({}, "Leeg", changed)); assert(changed && m.ready() && m.count == 0); std::vector many; - for (int i = 0; i < 11; ++i) many.push_back("light.a" + std::to_string(i)); + for (int i = 0; i < 21; ++i) many.push_back("light.a" + std::to_string(i)); assert(!m.set_layout(many, "Te veel", changed)); assert(m.count == 0); + many.pop_back(); assert(m.set_layout(many,"Twintig",changed)); + assert(m.count==20 && m.accepts(19,"light.a19")); + for(size_t i=0;i