Fix switch confirmation and details; retain compact slider icons
This commit is contained in:
@@ -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?
|
||||
|
||||
@@ -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<std::string, 4> 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; }
|
||||
|
||||
@@ -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<void(Tile &)> 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, 10> 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<JsonObject>();
|
||||
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<track.y1 && track.y2<=content.y2;
|
||||
}
|
||||
if(!lv_obj_has_flag(w.circle,LV_OBJ_FLAG_HIDDEN)){
|
||||
lv_area_t circle;lv_obj_get_coords(w.circle,&circle);
|
||||
bool mini=!lv_obj_has_flag(w.slider,LV_OBJ_FLAG_HIDDEN);
|
||||
fits=fits && circle.x1>=content.x1 && circle.x2<title.x1 && circle.y1>=content.y1;
|
||||
if(mini)fits=fits && circle.y2<track.y1;
|
||||
else fits=fits && circle.y2<=content.y2;
|
||||
if(!fits)ESP_LOGE("ui_test","Icon bounds slot=%u circle=%d,%d..%d,%d title_x=%d content=%d,%d..%d,%d",(unsigned)w.index,circle.x1,circle.y1,circle.x2,circle.y2,title.x1,content.x1,content.y1,content.x2,content.y2);
|
||||
if(mini || lv_obj_get_height(w.tile)<=80){
|
||||
int header_bottom=mini?track.y1-(lv_obj_get_height(w.tile)>80?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.index<model.count && model.tiles[w.index].background){
|
||||
bool palette_ok=lv_color_eq(lv_obj_get_style_bg_color(w.tile,LV_PART_MAIN),lv_color_hex(model.tiles[w.index].background)) &&
|
||||
@@ -603,6 +661,10 @@ inline void tick() {
|
||||
std::string status=waiting?(t.confirmed?"Bevestigd door Home Assistant":"Opdracht verstuurd..."):detail_state(t);
|
||||
if(detail_status)label(detail_status,status+(!waiting && !t.unit.empty()?" "+t.unit:""));
|
||||
if(detail_badge_status)label(detail_badge_status,status);
|
||||
if(detail_switch && !lv_obj_has_state(detail_switch,LV_STATE_PRESSED)){
|
||||
if(t.state=="on")lv_obj_add_state(detail_switch,LV_STATE_CHECKED);
|
||||
else lv_obj_remove_state(detail_switch,LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
if(!enabled)return;
|
||||
bool redraw=false;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Firmware 0.2.12 — switches
|
||||
|
||||
## Oorzaak en wijziging
|
||||
|
||||
ArduinoJson 7 leegt de uitvoerstring bij `serializeJson`. De oude code zette
|
||||
`state` eerst in die string en serialiseerde daarna de attributen eroverheen.
|
||||
Een simpele switch met gelijke attributen kreeg daardoor bij aan/uit dezelfde
|
||||
revision. De firmware bleef zes seconden op een bevestiging wachten, ook als
|
||||
de nieuwe status al ontvangen was.
|
||||
|
||||
Attributen worden nu apart geserialiseerd, waarna de status wordt toegevoegd.
|
||||
Switches/input_boolean hebben 150 ms minimale feedback in plaats van één seconde;
|
||||
zonder bevestiging blijft de bestaande begrensde wachttijd behouden. Overige
|
||||
kaarten houden hun minimale feedback. De opdracht wordt meteen verzonden en de
|
||||
tegel wordt direct ververst. Logs meten de tijd tot een gewijzigde HA-status;
|
||||
dit is een statusbevestiging, geen bewijs van een fysiek relaiscontact.
|
||||
|
||||
Lang indrukken opent een native LVGL-switch, zonder HA-opdracht bij openen.
|
||||
De bediening toont de door HA gemelde stand en blokkeert tijdens een lopende
|
||||
opdracht of onbeschikbaarheid. Uitgeschakelde switchiconen zijn grijs; eigen
|
||||
pastel tegelachtergronden blijven behouden. Beide borden gebruiken dezelfde code.
|
||||
|
||||
## Metingen en tests
|
||||
|
||||
- Voor de correctie: HA-events tijdens bediening door de eigenaar: printer-
|
||||
stroomschakelaar 56 ms van servicecall tot statuswijziging; de instelling
|
||||
voor stroomuitvalgeheugen 142–151 ms. De oude schermwachttijd is daarmee
|
||||
niet gelijk aan de verwerkingstijd van de HA-switch.
|
||||
- 51 Python-tests geslaagd.
|
||||
- Alle negen C++-testprogramma's geslaagd. De feedbacktest controleert een
|
||||
aan/uit-wijziging met identieke attributen, een onveranderde status, timeout,
|
||||
input_boolean en de millis-overloop.
|
||||
- Pakketgenerator gecontroleerd; protocol, opgeslagen tegels en preferences
|
||||
ongewijzigd. App 0.2.11 blijft bruikbaar; dit is een firmware-update.
|
||||
|
||||
## Controle op de schermen
|
||||
|
||||
- Beide runtimefirmwares gebouwd met ESPHome 2026.6.2 en via OTA geïnstalleerd
|
||||
op de vooraf geïdentificeerde Guition en CYD. Beide melden firmware 0.2.12.
|
||||
- Op beide apparaten: tien paginacontroles en vijftig overlaycycli geslaagd,
|
||||
inclusief bestaande controles voor tekst, sliders en pastelkleuren.
|
||||
- Guition: echte korte tikken geven voor de printer statusbevestiging op het
|
||||
scherm na 305 en 434 ms. De grote switch geeft 204 en 365 ms. De switch voor
|
||||
stroomuitvalgeheugen geeft 531 en 667 ms. Dit zijn volledige terugmeldtijden
|
||||
gemeten door de firmware, inclusief HA en de manager-synchronisatie.
|
||||
- Interne LVGL-schermopname gecontroleerd: naam, HA-stand en grote schakelaar
|
||||
zichtbaar. De eigenaar bevestigt snelle bediening, goed detailmenu en de
|
||||
grijze uitstand: “yes, allemaal top!”.
|
||||
- De actuele indelingen en instellingen voor beide schermen zijn na OTA exact
|
||||
gelijk aan de momentopname vóór deze update. Geen appdata of sleutels gewijzigd.
|
||||
|
||||
De fysieke bevestiging betreft Guition. CYD is gebouwd, geflasht en met de
|
||||
renderdiagnose getest; de nieuwe switchbediening is daar niet apart fysiek
|
||||
bevestigd. Er is geen langdurige stabiliteitsproef uitgevoerd voor deze patch.
|
||||
|
||||
## Aanvullende indeling
|
||||
|
||||
Mini-sliders behouden hun domeinicoon in een compacte cirkel naast titel en
|
||||
waarde. Daarvoor zijn afzonderlijke kleine MDI-fonts toegevoegd (18 px CYD,
|
||||
26 px Guition), zonder schaling via extra renderbuffers. CYD centreert zowel
|
||||
het tekstblok als de icooncirkel; met een slider gebeurt dat in de ruimte boven
|
||||
de slider. De geometriecontrole test nu ook icoonbegrenzing en centrering.
|
||||
|
||||
De eerste uitgebreide Guition-test vond te brede tekst bij wisselen van een
|
||||
watch/slider-tegel naar een gewone tegel in hetzelfde vak. LVGL gaf nog de oude
|
||||
x-positie terug. De berekening gebruikt nu de gevraagde nieuwe positie. Na die
|
||||
correctie slagen op beide apparaten opnieuw tien paginacontroles en vijftig
|
||||
overlaycycli, inclusief icoonbegrenzing, tekstbreedte, centrering en sliderafstand.
|
||||
Alle vier definitieve firmwareprofielen bouwen succesvol. Onder de zware
|
||||
Guition-renderproef zijn er nog waarschuwingen voor lange renderbewerkingen;
|
||||
die proef is geen bewijs van constante frame latency in alle situaties.
|
||||
+23
-1
@@ -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.10"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.12"
|
||||
DYNAMIC_TILES: "false"
|
||||
DASHBOARD_INBOX_INTERNAL: "true"
|
||||
DEVICE_NAME: "guition-wallbox"
|
||||
@@ -587,6 +587,7 @@ esphome:
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- -DLV_USE_SNAPSHOT=1
|
||||
- -DLV_USE_SWITCH=1
|
||||
includes:
|
||||
- cyd_ui.h
|
||||
- light_controls.h
|
||||
@@ -615,6 +616,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;
|
||||
@@ -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:
|
||||
|
||||
+25
-1
@@ -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:
|
||||
|
||||
+25
-1
@@ -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:
|
||||
- <esphome/components/smart_display/cyd_ui.h>
|
||||
- <esphome/components/smart_display/light_controls.h>
|
||||
@@ -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:
|
||||
|
||||
+23
-1
@@ -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:
|
||||
- <esphome/components/smart_display/cyd_ui.h>
|
||||
- <esphome/components/smart_display/light_controls.h>
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
#include "components/smart_display/runtime_model.h"
|
||||
#include <cassert>
|
||||
#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 <cassert>
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user