diff --git a/components/smart_display/cyd_ui.h b/components/smart_display/cyd_ui.h index 22cb30c..b56ea07 100644 --- a/components/smart_display/cyd_ui.h +++ b/components/smart_display/cyd_ui.h @@ -27,7 +27,12 @@ class TouchGuard { if (std::abs(x - start_x_) > 18 || std::abs(y - start_y_) > 18) moved_ = true; } bool accept(uint32_t now, int tile) { - if (accepted_ || moved_ || now - started_ < 60) return false; + return !moved_ && accept_slider(now, tile); + } + // Only for a slider which captured this contact and did not lose the press. + // Consume the gesture so its parent can never also turn it into a tile tap. + bool accept_slider(uint32_t now, int tile) { + if (accepted_ || now - started_ < 60) return false; if (has_previous_ && tile == previous_tile_ && now - previous_ < 600) return false; accepted_ = true; has_previous_ = true; diff --git a/components/smart_display/light_controls.h b/components/smart_display/light_controls.h index 9d78aad..72dc221 100644 --- a/components/smart_display/light_controls.h +++ b/components/smart_display/light_controls.h @@ -71,7 +71,7 @@ inline void preview(Row &row) { int value = lv_slider_get_value(row.slider); if (row.index == 0) { lv_label_set_text_fmt(row.value, "%d°", value); - lv_obj_set_style_bg_color(row.slider, lv_color_hsv_to_rgb(value % 360, 100, 100), LV_PART_KNOB); + lv_obj_set_style_bg_color(row.slider, lv_color_hex(0x111111), LV_PART_KNOB); } else if (row.index == 1) { lv_label_set_text_fmt(row.value, "%d K", value); } else lv_label_set_text_fmt(row.value, "%d %%", value); diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index c84a72a..643fa73 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -23,6 +23,7 @@ struct Tile { std::string entity, name, state, unit, modes, hvac_modes; std::array fan_speeds; unsigned fan_speed_count = 0; + std::string fan_speed; float brightness = NAN, percentage = NAN, position = NAN; float current = NAN, target = NAN, humidity = NAN, minimum = 7, maximum = 35, step = 0.5f; int hue = 0, kelvin = 3000, min_kelvin = 0, max_kelvin = 0; diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index e070af4..6ff7ec4 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -150,6 +150,7 @@ inline std::string receive(const std::string &payload) { if (std::isfinite(kelvin)) tile.kelvin = std::lround(std::clamp(kelvin, 1000.0f, 15000.0f)); tile.min_kelvin = std::clamp(number(a["min_color_temp_kelvin"], 0), 0.0f, 15000.0f); tile.max_kelvin = std::clamp(number(a["max_color_temp_kelvin"], 0), 0.0f, 15000.0f); + tile.fan_speed = string(a["fan_speed"], 48); tile.fan_speed_count = 0; if (a["fan_speed_list"].is()) for (JsonVariant speed : a["fan_speed_list"].as()) { if (tile.fan_speed_count == 4) break; @@ -193,6 +194,9 @@ namespace runtime_tiles { inline lv_obj_t *detail_root=nullptr; inline unsigned detail_index=0; inline const lv_font_t *detail_font=nullptr; +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_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); @@ -208,6 +212,9 @@ inline int slider_value(const Tile &t){ if(d=="number"||d=="input_number") {char *end;float state=strtof(t.state.c_str(),&end);if(end!=t.state.c_str() && t.maximum>t.minimum)value=(state-t.minimum)/(t.maximum-t.minimum);} return std::clamp((int)std::lround(value*1000),0,1000); } +inline lv_obj_t *captured_slider=nullptr; +inline bool slider_changed=false; +inline void slider_event(lv_event_t *e); inline void commit_slider(unsigned i,int raw){ if(i>=model.count || !fresh())return;auto &t=model.tiles[i];if(!t.available() || t.loading(esphome::millis()))return; float value=std::clamp(raw,0,1000)/1000.0f;auto d=t.domain(); @@ -220,10 +227,20 @@ inline void commit_slider(unsigned i,int raw){ value=std::clamp(t.minimum+std::round(value*(t.maximum-t.minimum)/t.step)*t.step,t.minimum,t.maximum); action(d+".set_value",t.entity,"value",std::to_string(value)); } } -inline void detail_button(const char *text,int x,int y,int width,int height,int command){ +inline void slider_event(lv_event_t *e){ + auto *slider=lv_event_get_target_obj(e);auto code=lv_event_get_code(e); + if(code==LV_EVENT_PRESSED){captured_slider=slider;slider_changed=false;} + 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; + if(changed && cyd::touch_guard.accept_slider(esphome::millis(),200+index))commit_slider(index,lv_slider_get_value(slider)); + } +} +inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height,int command){ auto *button=lv_obj_create(detail_root);lv_obj_remove_style_all(button);lv_obj_set_pos(button,x,y);lv_obj_set_size(button,width,height); lv_obj_set_style_bg_color(button,lv_color_hex(light_theme?(command==0?0x009FE3:0xD9E6F0):0x34495E),0);lv_obj_set_style_bg_opa(button,LV_OPA_COVER,0);lv_obj_set_style_radius(button,12,0);lv_obj_add_flag(button,LV_OBJ_FLAG_CLICKABLE); - auto *label=detail_label(button,text,6,0,width-12);lv_obj_center(label);if(command==0)lv_obj_set_style_text_color(label,lv_color_hex(0xFFFFFF),0);lv_obj_remove_flag(label,LV_OBJ_FLAG_CLICKABLE); + auto *label=detail_label(button,text,6,0,width-12);lv_obj_center(label);lv_obj_set_style_text_align(label,LV_TEXT_ALIGN_CENTER,0);if(command==0)lv_obj_set_style_text_color(label,lv_color_hex(0xFFFFFF),0);lv_obj_remove_flag(label,LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(button,[](lv_event_t *e){ int cmd=(intptr_t)lv_event_get_user_data(e);if(cmd==-1){hide_detail();return;} if(!fresh()||detail_index>=model.count || !cyd::touch_guard.accept(esphome::millis(),300+cmd))return; @@ -235,27 +252,69 @@ inline void detail_button(const char *text,int x,int y,int width,int height,int if(cmd==22)action("media_player.media_next_track",t.entity); if(cmd>=30 && cmd<38 && cmd-30<(int)t.option_count)action(t.domain()+".select_option",t.entity,"option",t.options[cmd-30]); },LV_EVENT_SHORT_CLICKED,(void*)(intptr_t)command); + lv_obj_set_style_bg_color(button,lv_color_hex(0x0075B0),LV_STATE_PRESSED); + lv_obj_set_style_transform_width(button,-2,LV_STATE_PRESSED);lv_obj_set_style_transform_height(button,-2,LV_STATE_PRESSED); + lv_obj_set_style_opa(button,LV_OPA_50,LV_STATE_DISABLED); + if(command>=0 && detail_action_count<16)detail_actions[detail_action_count++]=button; + return button; } 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_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);} - 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;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?0xEFEFEF: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; auto *heading=detail_label(detail_root,t.name,pad,large?24:12,width-80);if(watch_font){lv_obj_set_style_text_font(heading,watch_font,0);lv_obj_set_height(heading,lv_font_get_line_height(watch_font));} detail_button("X",width-58,8,48,40,-1); std::string state=t.state=="docked"?"In dock":t.state=="cleaning"?"Bezig met schoonmaken":t.state=="paused"?"Gepauzeerd":t.state; - detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?60:35,width-2*pad); + 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"){ - std::string summary=std::isfinite(t.battery)?"Accu "+std::to_string((int)t.battery)+"%":"Robotstofzuiger"; - detail_label(detail_root,summary,pad,top,width-2*pad);top+=large?42:25; - const char *names[]={"Start","Pauze","Naar dock","Zoeken"}; - for(int i=0;i<4;++i)detail_button(names[i],pad+(i%2)*(cw+gap),top+(i/2)*(bh+gap),cw,bh,i); - top+=2*(bh+gap); - for(unsigned i=0;iminimum?8+(chart_h-8)*(t.history[i]-minimum)/(maximum-minimum):chart_h/2; auto *bar=lv_obj_create(detail_root);lv_obj_remove_style_all(bar);lv_obj_set_pos(bar,pad+i*bar_w,chart_y+chart_h-h);lv_obj_set_size(bar,std::max(2,bar_w-2),h);lv_obj_set_style_bg_color(bar,lv_color_hex(0x16A5E6),0);lv_obj_set_style_bg_opa(bar,LV_OPA_COVER,0);} - detail_label(detail_root,"Eerder Nu",pad,height-24,width-2*pad); + detail_label(detail_root,std::to_string(t.history_hours)+" uur geleden",pad,height-24,(width-2*pad)/2); + auto *now=detail_label(detail_root,"Nu",width/2,height-24,width/2-pad);lv_obj_set_style_text_align(now,LV_TEXT_ALIGN_RIGHT,0); }else if(d=="select"||d=="input_select"){ for(unsigned i=0;i80?10:4);lv_obj_align(w.slider,LV_ALIGN_BOTTOM_MID,0,-1);lv_slider_set_range(w.slider,0,1000); - lv_obj_set_style_bg_color(w.slider,lv_color_hex(0x111111),LV_PART_KNOB);lv_obj_set_style_pad_all(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); + w.slider=lv_slider_create(tile);lv_obj_set_size(w.slider,lv_obj_get_width(tile)-24,lv_obj_get_height(tile)>80?28:10);lv_obj_align(w.slider,LV_ALIGN_BOTTOM_MID,0,-1);lv_slider_set_range(w.slider,0,1000); + 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_add_event_cb(w.slider,[](lv_event_t *e){auto &w=*static_cast(lv_event_get_user_data(e));if(!fresh() || !cyd::touch_guard.accept(esphome::millis(),200+w.index))return;commit_slider(w.index,lv_slider_get_value(w.slider));},LV_EVENT_RELEASED,&w); + 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]); } @@ -375,6 +436,9 @@ inline void render(lv_obj_t *room) { 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:14);lv_obj_add_flag(w.circle,LV_OBJ_FLAG_HIDDEN);lv_obj_set_x(w.title,0);lv_obj_set_x(w.value,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); bool on = fresh() && t.active(); @@ -392,6 +456,12 @@ inline void render(lv_obj_t *room) { } } inline void tick() { + if(detail_root && !lv_obj_has_flag(detail_root,LV_OBJ_FLAG_HIDDEN) && detail_index - diff --git a/packages/guition.yaml b/packages/guition.yaml index cf1bc89..8a7eaac 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -586,7 +586,7 @@ esphome: min_version: 2026.6.2 project: name: maxgramser.esp_screen - version: "0.2.0" + version: "0.2.1" platformio_options: build_flags: - -DLV_USE_SNAPSHOT=1 diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 88bfd97..f447279 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.1 + +- Algemene instellingen en Inspector direct bovenaan bereikbaar. +- Tegel selecteren: klikactie, grote waarde, kleine slider (ja/nee) en historieperiode. +- Inspectie per tegel met actuele HA-status en eigenschappen. +- Firmware: schuifgebaren correct verwerken, brede mini-slider zonder handvat, concrete historie-as. +- Nieuwe vacuumkaart met robotweergave, actieve zuigkracht en druk-/opdrachtfeedback. + # 0.2.0 - ESPHome CLI in de app: bestaande profielen controleren, bouwen en via USB/OTA installeren. diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index ff7ecb3..4255a1b 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -6,7 +6,8 @@ let inventory = { screens: [], entities: [] }, dirty = false, filter = "", dragIndex = -1, - busy = false; + busy = false, + selectedTile = null; const node = (tag, text, cls) => { const n = document.createElement(tag); if (text !== undefined) n.textContent = text; @@ -256,7 +257,22 @@ function renderTiles() { actions.append(b); } const options = node("details", undefined, "tile-options"); - options.append(node("summary", "Tegelinstellingen")); + options.append(node("summary", "Bediening & weergave instellen")); + options.open = selectedTile === tile.entity; + options.ontoggle = () => { + li.classList.toggle("selected-tile", options.open); + if (options.open) { + selectedTile = tile.entity; + document.querySelectorAll(".tile-options").forEach((other) => { + if (other !== options) other.open = false; + }); + } + }; + li.onclick = (e) => { + if (!e.target.closest("input, select, button, summary, .tile-options")) + options.open = !options.open; + }; + const controls = {}; const domain = tile.entity.split(".")[0]; const fields = [ [ @@ -295,10 +311,10 @@ function renderTiles() { ) fields.push([ "inline", - "Op de tegel", + "Kleine slider op deze tegel?", [ - ["none", "Geen extra bediening"], - ["slider", "Mini-schuif"], + ["none", "Nee"], + ["slider", "Ja, direct bedienen"], ], ]); if (domain === "sensor") @@ -328,6 +344,7 @@ function renderTiles() { : key === "history_hours" ? 24 : "none"); + controls[key] = input; input.onchange = () => { tile.options = { ...tile.options, @@ -337,11 +354,18 @@ function renderTiles() { tile.options.inline = "none"; if (key === "inline" && input.value === "slider") tile.options.display = "standard"; + for (const [field, control] of Object.entries(controls)) { + if (tile.options[field] !== undefined) + control.value = tile.options[field]; + } markDirty(); }; label.append(input); options.append(label); } + const inspectTile = node("button", "Inspecteer deze tegel", "quiet"); + inspectTile.onclick = () => inspect(tile.entity); + options.append(inspectTile); content.append(options); li.append(node("span", "⠿", "grip"), content, actions); $("#tiles").append(li); @@ -430,7 +454,7 @@ $("#save").onclick = async () => { } catch (e) { toast(e.message); } finally { - busy = false; + ((busy = false), (selectedTile = null)); $("#save").disabled = false; } }; @@ -625,17 +649,50 @@ $("#create-profile").onclick = async () => { toast(e.message); } }; -$("#inspect").onclick = async () => { +function openSection(id) { + const section = $(id); + if (section.tagName === "DETAILS") section.open = true; + section.scrollIntoView({ behavior: "smooth", block: "start" }); +} +$("#nav-tiles").onclick = () => openSection("#tile-section"); +$("#nav-settings").onclick = () => openSection("#general-settings"); +$("#nav-inspector").onclick = () => inspect(); +async function inspect(entity) { if (!selected) return; + openSection("#inspector-section"); + $("#inspection-summary").textContent = "Actuele HA-status ophalen..."; try { + const data = await ( + await api(`screens/${encodeURIComponent(selected)}/inspect`) + ).json(); + const tiles = entity + ? data.tiles.filter((t) => t.entity === entity) + : data.tiles; + $("#inspection-summary").replaceChildren(); + for (const tile of tiles) { + const card = node("article", undefined, "inspection-tile"); + card.append( + node("strong", tile.entity), + node("p", `Status: ${tile.state}`), + ); + const options = + layout.tiles.find((t) => t.entity === tile.entity)?.options || {}; + card.append( + node( + "small", + `Kleine slider: ${options.inline === "slider" ? "ja" : "nee"} · Weergave: ${options.display === "watch" ? "grote waarde" : "standaard"}`, + ), + ); + $("#inspection-summary").append(card); + } $("#inspection").textContent = JSON.stringify( - await ( - await api(`screens/${encodeURIComponent(selected)}/inspect`) - ).json(), + entity ? tiles[0] : data, null, 2, ); } catch (e) { + $("#inspection-summary").textContent = e.message; toast(e.message); } -}; +} +$("#inspect").onclick = () => inspect(); diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index cafc8e0..1d747f4 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -60,15 +60,22 @@ maxlength="60" placeholder="Bijvoorbeeld: Woonkamer" /> -
+ +

Je tegels

0 / 10

- Sleep om te ordenen of gebruik de pijltjes. Zes tegels per - pagina. + Klik op een tegel om bediening en weergave te kiezen. Sleep om + te ordenen. Zes tegels per pagina.

    @@ -87,7 +94,7 @@

    -
    +
    Scherminstellingen Helderheid, rustmomenten en weergave @@ -100,7 +107,7 @@ Gelijke begin- en eindtijd schakelen het nachtvenster uit.

    -
    +
    Inspector +
    Selecteer een scherm en lees de gegevens.