#pragma once #include "runtime_model.h" #include "screen_settings.h" #include "esphome/core/preferences.h" #include "cyd_ui.h" #include "light_controls.h" #include "esphome/components/json/json_util.h" #include "esphome/components/api/api_server.h" #include "esphome/core/hal.h" #include "lvgl.h" #include #include namespace runtime_tiles { inline bool enabled = false; inline bool light_theme = false; inline Model model; inline std::string inbox; inline const lv_font_t *watch_font = nullptr; inline void tick(); inline void refresh_detail(unsigned index); inline int active_index = -1; inline uint32_t last_received = 0; 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); screen_settings::Settings saved; if (settings_preference.load(&saved) && saved.valid()) screen_settings::current = saved; } inline bool parse_settings(JsonObject obj, screen_settings::Settings &s) { // Require the complete known schema; validate before touching any runtime state. if (obj.size() != 11) return false; const char *flags[] = {"standby_enabled", "night_enabled", "show_clock", "clock_24h", "home_on_standby"}; int32_t *flag_values[] = {&s.standby_enabled, &s.night_enabled, &s.show_clock, &s.clock_24h, &s.home_on_standby}; for (int i = 0; i < 5; ++i) { if (!obj[flags[i]].is()) return false; *flag_values[i] = obj[flags[i]].as(); } const char *numbers[] = {"standby_seconds", "brightness", "standby_brightness", "night_start", "night_end", "night_brightness"}; int32_t *values[] = {&s.standby_seconds, &s.brightness, &s.standby_brightness, &s.night_start, &s.night_end, &s.night_brightness}; for (int i = 0; i < 6; ++i) { if (!obj[numbers[i]].is() || obj[numbers[i]].is()) return false; *values[i] = obj[numbers[i]].as(); } 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{}; }; 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; float n = value.as(); return std::isfinite(n) ? n : fallback; } inline std::string string(JsonVariant value, size_t maximum = 160) { if (!value.is()) return {}; std::string s = value.as(); if (s.size() > maximum) { while (maximum > 0 && (static_cast(s[maximum]) & 0xC0) == 0x80) --maximum; s.resize(maximum); } return s; } inline std::string list(JsonVariant value) { if (!value.is()) return {}; std::string out; serializeJson(value, out); return out.size() <= 512 ? out : ""; } inline std::string receive(const std::string &payload) { if (!enabled) return "Gebruik het Easy Setup-profiel"; if (payload.size() > 4096) return "Fout: bericht te groot"; std::string result = "Fout: ongeldig bericht"; esphome::json::parse_json(payload, [&](JsonObject root) -> bool { if (root["v"].as() != 1) { result = "Fout: protocolversie"; return false; } auto op = string(root["op"]); if (op == "layout") { if (!root["entities"].is() || !root["title"].is()) return false; auto settings = screen_settings::current; if (!root["settings"].isNull() && (!root["settings"].is() || !parse_settings(root["settings"].as(), settings))) { result = "Fout: scherminstellingen"; return false; } std::vector entities; for (JsonVariant entity : root["entities"].as()) { if (!entity.is() || entities.size() == MAX_TILES) return false; entities.push_back(entity.as()); } inbox = string(root["inbox"], 160); bool changed = false; if (!model.set_layout(entities, string(root["title"], 96), changed)) return false; if (!(settings == screen_settings::current)) { screen_settings::current = settings; settings_preference.save(&settings); // ESPHome batches flash writes; no write on keepalive. if (settings_changed) settings_changed(); } if (changed) { active_index = -1; for (auto &w : widgets) w.cached_active = -1; if (dismiss) dismiss(); } last_received = esphome::millis(); if (layout_changed) layout_changed(); if (refresh) refresh(); result = "Indeling ontvangen"; return true; } if (op != "state" || !root["i"].is() || !root["a"].is()) return false; unsigned index = root["i"].as(); std::string entity = string(root["entity"], 120); 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); tile.observe(revision); auto options = root["o"]; tile.tap = string(options["tap"]); if (tile.tap.empty()) tile.tap="auto"; tile.display = string(options["display"]); if (tile.display.empty()) tile.display="standard"; tile.inline_control = string(options["inline"]); if (tile.inline_control.empty()) tile.inline_control="none"; tile.has_history=false; if (root["history"]["values"].is()) { tile.history.fill(NAN);unsigned j=0; for (JsonVariant value:root["history"]["values"].as()) { if(j==24) break; tile.history[j++]=number(value); } tile.has_history=j>0;tile.history_hours=std::clamp(root["history"]["hours"].as(),1u,24u); } tile.option_count=0; if (a["options"].is()) for(JsonVariant option:a["options"].as()) { if(tile.option_count==8)break;tile.options[tile.option_count++]=string(option,48); } tile.battery=number(a["battery_level"]);tile.volume=number(a["volume_level"]); tile.media_title=string(a["media_title"],80);tile.supported=a["supported_features"].as(); tile.name = string(root["name"], 80); tile.state = string(root["state"], 160); tile.unit = string(a["unit_of_measurement"], 20); tile.brightness = number(a["brightness"]); tile.percentage = number(a["percentage"]); tile.position = number(a["current_position"]); tile.current = number(a["current_temperature"]); tile.target = number(a["temperature"]); tile.humidity = number(a["current_humidity"]); tile.minimum = number(a["min_temp"], 7); tile.maximum = number(a["max_temp"], 35); tile.step = number(a["target_temp_step"], 0.5f); if(tile.domain()=="number" || tile.domain()=="input_number") { tile.minimum=number(a["min"],0);tile.maximum=number(a["max"],100);tile.step=number(a["step"],1); } if(tile.domain()=="weather") {tile.current=number(a["temperature"]);tile.unit=string(a["temperature_unit"],12);} tile.modes = list(a["supported_color_modes"]); tile.hvac_modes = list(a["hvac_modes"]); float hue = number(a["hs_color"][0]); if (std::isfinite(hue)) tile.hue = std::lround(std::clamp(hue, 0.0f, 360.0f)); float kelvin = number(a["color_temp_kelvin"]); 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; tile.fan_speeds[tile.fan_speed_count++] = string(speed, 48); } tile.received = true; widgets[index].cached_active=-1; last_received = esphome::millis(); if (refresh) refresh(); if (active_index == static_cast(index) && detail_update) detail_update(tile); refresh_detail(index); result = model.ready() ? "Gesynchroniseerd" : "Tegels laden"; return true; }); return result; } inline void action(const std::string &service, const std::string &entity, const std::string &key="", const std::string &value="") { if (!fresh() || !valid_entity(entity)) return; esphome::api::HomeassistantActionRequest request; request.service = esphome::StringRef(service); request.data.init(key.empty() ? 1 : 2); esphome::api::HomeassistantServiceMap entry; entry.key = esphome::StringRef("entity_id"); entry.value = esphome::StringRef(entity); request.data.push_back(entry); 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); } inline void setting_event(const std::string &key, int value) { if(inbox.empty())return; esphome::api::HomeassistantActionRequest request;request.service=esphome::StringRef("esphome.screen_setting");request.is_event=true; std::string number=std::to_string(value);request.data.init(3); const std::string keys[]={"inbox","key","value"},values[]={inbox,key,number}; for(int i=0;i<3;++i){esphome::api::HomeassistantServiceMap entry;entry.key=esphome::StringRef(keys[i]);entry.value=esphome::StringRef(values[i]);request.data.push_back(entry);} esphome::api::global_api_server->send_homeassistant_action(request); } } // namespace runtime_tiles // Small shared native-LVGL detail cards. No images, canvas buffers or free scrolling. 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); 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 void hide_detail(){if(detail_root)lv_obj_add_flag(detail_root,LV_OBJ_FLAG_HIDDEN);} inline int slider_value(const Tile &t){ auto d=t.domain();float value=0; if(d=="light")value=std::isfinite(t.brightness)?t.brightness/255:0; if(d=="fan")value=std::isfinite(t.percentage)?t.percentage/100:0; if(d=="cover")value=std::isfinite(t.position)?t.position/100:0; if(d=="media_player")value=std::isfinite(t.volume)?t.volume:0; 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(); if(d=="light")action("light.turn_on",t.entity,"brightness",std::to_string((int)std::lround(value*255))); if(d=="fan")action("fan.set_percentage",t.entity,"percentage",std::to_string((int)std::lround(value*100))); if(d=="cover")action("cover.set_cover_position",t.entity,"position",std::to_string((int)std::lround(value*100))); if(d=="media_player")action("media_player.volume_set",t.entity,"volume_level",std::to_string(value)); if(d=="number"||d=="input_number") { if(!std::isfinite(t.minimum)||!std::isfinite(t.maximum)||t.maximum<=t.minimum||t.step<=0)return; 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 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);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; auto &t=model.tiles[detail_index];if(!t.available()||t.loading(esphome::millis()))return; if(cmd<4){const char *services[]={"vacuum.start","vacuum.pause","vacuum.return_to_base","vacuum.locate"};action(services[cmd],t.entity);} if(cmd>=10 && cmd<14 && cmd-10<(int)t.fan_speed_count)action("vacuum.set_fan_speed",t.entity,"fan_speed",t.fan_speeds[cmd-10]); if(cmd==20)action("media_player.media_play_pause",t.entity); if(cmd==21)action("media_player.media_previous_track",t.entity); 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);} 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_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"){ // 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); lv_obj_set_style_radius(o,radius,0);lv_obj_set_style_bg_color(o,lv_color_hex(color),0);lv_obj_set_style_bg_opa(o,LV_OPA_COVER,0); lv_obj_remove_flag(o,LV_OBJ_FLAG_CLICKABLE);lv_obj_remove_flag(o,LV_OBJ_FLAG_SCROLLABLE);return o; }; uint32_t surface=light_theme?0xFFFFFF:0x2B3B4D, muted=light_theme?0xEAF5FC:0x344D63; if(large){ auto *hero=shape(detail_root,pad,100,width-2*pad,148,surface,24); shape(hero,18,14,120,120,muted,60); auto *robot=shape(hero,35,27,86,86,0xFFFFFF,43); lv_obj_set_style_border_width(robot,2,0);lv_obj_set_style_border_color(robot,lv_color_hex(0xCEDDE6),0); shape(robot,27,10,30,30,0xE3EBEF,15);shape(robot,35,18,14,14,0xA8BCC8,7); shape(robot,29,59,26,5,0x00A6ED,3); detail_label(hero,t.state=="cleaning"?"Aan het werk":"Klaar voor je huis",154,24,260); auto *badge=shape(hero,154,59,240,32,muted,16); auto *status=detail_label(badge,t.loading(esphome::millis())?"Opdracht verstuurd...":state,12,5,218); lv_obj_set_style_text_color(status,lv_color_hex(light_theme?0x087BA8:0xFFFFFF),0); detail_label(hero,std::isfinite(t.battery)?"Batterij "+std::to_string((int)t.battery)+"%":"Verbonden via Home Assistant",154,107,260); detail_button(t.state=="cleaning"?"Pauzeer schoonmaken":"Start schoonmaken",pad,260,width-2*pad,58,t.state=="cleaning"?1:0); detail_button("Terug naar dock",pad,330,cw,48,2); detail_button("Vind mijn robot",pad+cw+gap,330,cw,48,3); detail_label(detail_root,"Zuigkracht",pad,394,width-2*pad); top=424; }else{ auto *robot=shape(detail_root,pad,64,46,46,muted,23); shape(robot,16,8,14,14,0xA8BCC8,7);shape(robot,15,32,16,3,0x00A6ED,2); detail_label(detail_root,std::isfinite(t.battery)?"Batterij "+std::to_string((int)t.battery)+"%":"Robotstofzuiger",pad+58,66,width-2*pad-58); detail_label(detail_root,"Kies een actie",pad+58,87,width-2*pad-58); detail_button(t.state=="cleaning"?"Pauzeren":"Schoonmaken",pad,120,cw,38,t.state=="cleaning"?1:0); detail_button("Naar dock",pad+cw+gap,120,cw,38,2); detail_label(detail_root,"Zuigkracht",pad,168,width-2*pad);top=194; } if(!t.fan_speed_count)detail_label(detail_root,"Automatische zuigkracht",pad,top,width-2*pad); int count=std::max(1,(int)t.fan_speed_count),sw=(width-2*pad-gap*(count-1))/count; 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,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;i(lv_event_get_user_data(event)); if (!enabled || !fresh() || w.index >= model.count) return; auto code = lv_event_get_code(event); if (code != LV_EVENT_SHORT_CLICKED && code != LV_EVENT_LONG_PRESSED) return; if (!cyd::touch_guard.accept(esphome::millis(), 100 + w.index)) return; auto &tile = model.tiles[w.index]; auto d = tile.domain(); // Scenes/scripts often have timestamps or 'off'; unavailable devices never act. if (!tile.available() || tile.loading(esphome::millis()) || tile.tap=="none") return; bool open = code == LV_EVENT_LONG_PRESSED || d == "climate" || d == "vacuum" || d == "cover"; if(code==LV_EVENT_SHORT_CLICKED && tile.tap=="detail")open=true; if(code==LV_EVENT_SHORT_CLICKED && tile.tap=="toggle")open=false; if(d=="media_player" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("media_player.toggle",tile.entity);return;} if(d=="sensor" || d=="binary_sensor" || d=="weather" || d=="number" || d=="input_number" || d=="select" || d=="input_select" || d=="media_player" || d=="vacuum") { tile.begin(esphome::millis(),true); active_index=w.index; show_detail(w.index); return; } if (open) { if (d == "light" || d == "climate" || d == "vacuum" || d == "fan" || d == "cover") { 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); } return; } if (d == "light" || d == "switch" || d == "input_boolean" || d == "fan") action(d + ".toggle", tile.entity); if (d == "scene" || d == "script") action(d + ".turn_on", tile.entity); if (d == "button" || d == "input_button") action(d + ".press", tile.entity); } 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) { 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); // 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)); lv_label_set_long_mode(title,LV_LABEL_LONG_DOT);lv_label_set_long_mode(value,LV_LABEL_LONG_DOT); w.progress=lv_obj_create(tile);lv_obj_remove_style_all(w.progress);lv_obj_set_size(w.progress,0,3);lv_obj_align(w.progress,LV_ALIGN_BOTTOM_LEFT,0,0);lv_obj_set_style_bg_color(w.progress,lv_color_hex(0x00A6ED),0);lv_obj_set_style_bg_opa(w.progress,LV_OPA_COVER,0); 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,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]); } inline void label(lv_obj_t *obj, const std::string &text) { if (text != lv_label_get_text(obj)) lv_label_set_text(obj, text.c_str()); } 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]; label(w.title, t.name.empty() ? t.entity : t.name); label(w.icon, icon_for(t)); std::string value = t.state; if (!fresh() || !t.available()) value = "Niet beschikbaar"; else if (t.domain() == "light" && t.state == "on" && std::isfinite(t.brightness)) value = std::to_string(static_cast(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %"; else if (t.domain() == "climate" && std::isfinite(t.target)) { char b[32]; snprintf(b, sizeof(b), "%.1f°", t.target); value = b; } else if (value == "on") value = "Aan"; else if (value == "off") value = "Uit"; else if (value == "cleaning") value = "Bezig"; else if (value == "docked") value = "In dock"; else if (!t.unit.empty()) value += " " + t.unit; bool pending=t.loading(esphome::millis()); if(t.domain()=="weather" && std::isfinite(t.current)) {char b[32];snprintf(b,sizeof(b),"%.1f %s",t.current,t.unit.c_str());value=b;} if(t.domain()=="vacuum" && std::isfinite(t.battery))value += " / "+std::to_string((int)t.battery)+"%"; label(w.value, pending ? "Bezig..." : value); 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); lv_obj_set_width(w.value,watch?lv_obj_get_width(w.tile)-24:lv_obj_get_width(w.title)); 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(); if (w.cached_active == static_cast(on)) continue; w.cached_active = on; uint32_t accent = t.domain() == "light" ? 0xE4B23C : t.domain() == "climate" ? 0xF18750 : 0x548BD4; lv_obj_set_style_bg_color(w.tile, lv_color_hex(light_theme ? 0xFFFFFF : (on ? 0xF5F1E8 : 0x526C85)), 0); lv_obj_set_style_border_width(w.tile, 1, 0); lv_obj_set_style_border_opa(w.tile, LV_OPA_COVER, 0); lv_obj_set_style_border_color(w.tile, lv_color_hex(light_theme ? 0xDDDDDD : (on ? 0xF5F1E8 : 0x9CB3C8)), 0); lv_obj_set_style_bg_color(w.circle, lv_color_hex(light_theme ? (on ? (t.domain()=="light" ? 0xFFF1D2 : t.domain()=="climate" ? 0xFFE8D5 : 0xD5EEFC) : 0xF0F0F0) : (on ? accent : 0x263B50)), 0); lv_obj_set_style_text_color(w.icon, lv_color_hex(light_theme ? (on ? (t.domain()=="light" ? 0xFFB900 : t.domain()=="climate" ? 0xED8A3B : 0x009FE3) : 0x9E9E9E) : (on ? 0x172232 : 0xF3F5F7)), 0); lv_obj_set_style_text_color(w.title, lv_color_hex(light_theme ? 0x1B1B1B : (on ? 0x172232 : 0xF3F5F7)), 0); lv_obj_set_style_text_color(w.value, lv_color_hex(light_theme ? 0x616161 : (on ? 0x46525E : 0xF0F4F8)), 0); } } inline void tick() { if(detail_root && !lv_obj_has_flag(detail_root,LV_OBJ_FLAG_HIDDEN) && detail_index(active_index) >= model.count) return {}; auto &tile = model.tiles[active_index]; return index < tile.fan_speed_count ? tile.fan_speeds[index] : ""; } }