From 5b50e70472f3a9913af30aa5b6a792dc37850b64 Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Tue, 15 Sep 2026 13:34:18 +0200 Subject: [PATCH] Second hand on the analog clock, sliders like Home Assistant's, one top bar on every card (0.2.43) Firmware 0.2.37 for both boards: - The analog clock card gets a thin red second hand that moves every second while the screen is awake (runtime_tiles::screen_awake, set by the profiles from display_dimmed); in standby it hides and the clock keeps its once-a-minute redraw. - Card sliders follow HA's control slider: 12/42 and 8/42 corners, a 4 px handle an eighth of the height from the fill end, a stub of a third of the height at 0 or 1 %, the track in the fill colour at 20 %. An off light or fan shows only the empty grey track, also in the colour card's brightness row. A light's slider floors at 1 %. - Every overlay card shares one top bar: a round back arrow at the left (60/40 px), the name centred, and only the climate power as a right-hand action. The "done"/"ok" buttons of the colour and vacuum cards are gone; the brightness card gets its arrow. - The top bar reads "Connecting to Home Assistant..." / "Waiting for ESP Screens..." until the first layout, instead of "Choose tiles in HA". Checked with the host renderer for both boards and on the Guition over USB. Co-Authored-By: Claude Fable 5.1 --- components/smart_display/light_controls.h | 27 ++- components/smart_display/runtime_tiles.h | 80 ++++++-- components/smart_display/tile_icon_names.h | 1 + docs/RELEASING.md | 22 +++ guition-4848s040.yaml | 180 +++++++----------- home-like-2432s028.yaml | 174 +++++++---------- packages/cyd.yaml | 174 +++++++---------- packages/guition.yaml | 180 +++++++----------- screen_manager/CHANGELOG.md | 11 ++ screen_manager/app/core.py | 2 +- screen_manager/app/static/bar-roboto-400.woff | Bin 14120 -> 14120 bytes screen_manager/app/static/bar-roboto-500.woff | Bin 14240 -> 14240 bytes screen_manager/app/static/tile-icons.woff | Bin 15460 -> 15484 bytes screen_manager/app/tile_icons.py | 2 + screen_manager/config.yaml | 2 +- tests/test_header_bar.py | 13 ++ 16 files changed, 419 insertions(+), 449 deletions(-) diff --git a/components/smart_display/light_controls.h b/components/smart_display/light_controls.h index b5ecb9c..88e2995 100644 --- a/components/smart_display/light_controls.h +++ b/components/smart_display/light_controls.h @@ -40,7 +40,7 @@ namespace light_controls { inline State states[8]; inline State fallback; inline State *active = &fallback; -struct Row { lv_obj_t *box{}, *slider{}, *value{}; bool dirty = false; unsigned index{}; }; +struct Row { lv_obj_t *box{}, *slider{}, *value{}; bool dirty = false; unsigned index{}; bool off = false; }; inline Row rows[3]; inline std::function commits[3]; inline bool ready = false; @@ -81,15 +81,20 @@ inline void preview(Row &row) { lv_label_set_text_fmt(row.value, "%d K", value); lv_obj_set_style_bg_color(row.slider, kelvin_color(value, lv_slider_get_min_value(row.slider), lv_slider_get_max_value(row.slider)), LV_PART_KNOB); } else { - // The range reaches below 1 only to draw the round end that holds the handle at 1 %. + // The range reaches below 1 only to draw the short stub that holds the handle at 1 %. if (value < 1) { lv_slider_set_value(row.slider, 1, LV_ANIM_OFF); value = 1; } - lv_label_set_text_fmt(row.value, "%d %%", value); + // An off light shows only the track, as in Home Assistant, until the slider moves. + lv_obj_set_style_bg_opa(row.slider, row.off ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(row.slider, row.off ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_KNOB); + lv_obj_set_style_bg_color(row.slider, lv_color_hex(row.off ? 0xECECEC : 0xFDEFC3), LV_PART_MAIN); + if (row.off) lv_label_set_text(row.value, "Off"); + else lv_label_set_text_fmt(row.value, "%d %%", value); } } inline void event(lv_event_t *e) { auto &row = *static_cast(lv_event_get_user_data(e)); auto code = lv_event_get_code(e); - if (code == LV_EVENT_VALUE_CHANGED) { row.dirty = true; preview(row); } + if (code == LV_EVENT_VALUE_CHANGED) { row.dirty = true; row.off = false; preview(row); } if (code == LV_EVENT_PRESS_LOST) row.dirty = false; if (code == LV_EVENT_RELEASED && row.dirty) { row.dirty = false; @@ -187,15 +192,16 @@ inline void setup(lv_obj_t *parent, const lv_font_t *font, int width, int height lv_obj_set_style_outline_opa(row.slider, LV_OPA_20, LV_PART_KNOB); lv_slider_set_range(row.slider, i == 0 ? 0 : 2000, i == 0 ? 360 : 6500); } else { + // Track and fill corners, handle and shortest fill as in Home Assistant's 42 px control slider. lv_obj_set_pos(row.slider, track_x, track_y); lv_obj_set_size(row.slider, track_w, track_h); - lv_obj_set_style_radius(row.slider, LV_RADIUS_CIRCLE, LV_PART_MAIN); - lv_obj_set_style_radius(row.slider, LV_RADIUS_CIRCLE, LV_PART_INDICATOR); + lv_obj_set_style_radius(row.slider, track_h * 12 / 42, LV_PART_MAIN); + lv_obj_set_style_radius(row.slider, track_h * 8 / 42, LV_PART_INDICATOR); lv_obj_set_style_bg_opa(row.slider, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_bg_opa(row.slider, LV_OPA_COVER, LV_PART_INDICATOR); lv_obj_set_style_bg_color(row.slider, lv_color_hex(0xFDEFC3), LV_PART_MAIN); lv_obj_set_style_bg_color(row.slider, lv_color_hex(0xFFC107), LV_PART_INDICATOR); - // White handle inside the rounded end of the fill, as on the tiles (runtime_tiles::slider_handle). - int handle = large ? 6 : 4, back = track_h / 4 + handle / 2, half = track_h >> 1; + // White handle inside the end of the fill, as on the tiles (runtime_tiles::slider_handle). + int handle = large ? 4 : 3, back = std::max(1, track_h / 8) + handle / 2, half = track_h >> 1; lv_obj_set_style_bg_opa(row.slider, LV_OPA_COVER, LV_PART_KNOB); lv_obj_set_style_bg_color(row.slider, lv_color_hex(0xFFFFFF), LV_PART_KNOB); lv_obj_set_style_radius(row.slider, 2, LV_PART_KNOB); @@ -203,8 +209,8 @@ inline void setup(lv_obj_t *parent, const lv_font_t *font, int width, int height lv_obj_set_style_pad_right(row.slider, handle / 2 - back - (track_h - half), LV_PART_KNOB); lv_obj_set_style_pad_top(row.slider, -(track_h / 4), LV_PART_KNOB); lv_obj_set_style_pad_bottom(row.slider, -(track_h / 4), LV_PART_KNOB); - // 1 % still shows one round end: the range starts that far below 1. - int below = track_w > track_h ? 99 * track_h / (track_w - track_h) : 0; + // 1 % still shows a stub a third of the height wide: the range starts that far below 1. + int stub = std::max(track_h / 3, 2 * std::max(1, track_h / 8) + handle), below = track_w > stub ? 99 * stub / (track_w - stub) : 0; lv_slider_set_range(row.slider, 1 - below, 100); } lv_obj_add_event_cb(row.slider, event, LV_EVENT_ALL, &row); @@ -235,6 +241,7 @@ inline void open(const std::string &entity, bool color, bool temperature, int br if (i == 1 && !active->temperature_ready()) lv_obj_add_state(row.slider, LV_STATE_DISABLED); if (i == 1 && active->temperature_ready()) lv_slider_set_range(row.slider, active->minimum, active->maximum); lv_slider_set_value(row.slider, i == 0 ? active->hue : i == 1 ? active->kelvin : clamp(brightness, 1, 100), LV_ANIM_OFF); + row.off = i == 2 && brightness <= 0; preview(row); row.dirty = false; if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, "Waiting..."); } diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 85d354c..71877ec 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -42,6 +42,10 @@ inline lv_obj_t *time_label = nullptr; inline const lv_font_t *header_text_font = nullptr, *header_icon_font = nullptr; inline void render_header(); inline std::function now_time; +// True while the screen is in use: the profile reports standby (also the night level) as not awake. +// The analog clock's second hand runs only then; a standby screen stays on its minute redraws. +inline std::function screen_awake; +inline bool awake() { return !screen_awake || screen_awake(); } inline uint32_t now_epoch() { if (!now_time) return 0; auto t = now_time(); return t.is_valid() ? static_cast(t.timestamp) : 0; } inline void tick(); inline void refresh_tile(size_t index); @@ -101,6 +105,8 @@ struct Widgets { int title_x=0,title_y=0,value_x=0,value_y=0; const lv_font_t *value_font{}, *icon_font{}; // Wide cards span both columns; custom cards (clock, forecast, graph) draw into `extra`. bool wide=false; int base_width=0; lv_obj_t *extra{}; std::string extra_mode; std::array parts{}; lv_point_precise_t *points{}; + // Analog clock: centre and radius of the dial, so the second hand can move without a card redraw. + int hand_cx=0, hand_cy=0, hand_r=0, hand_width=1; // Soft area under a polyline (graph, sun path), painted by the extra container's draw event. const lv_point_precise_t *fill_points{}; unsigned fill_count=0; int fill_x=0, fill_y=0, fill_base=0; lv_color_t fill_color{}; lv_opa_t fill_opa=0; // Direct controls on a wide card: a panel at the right with pill keys, a -/+ pill, @@ -447,7 +453,8 @@ 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))); + // A light's slider stops at 1 %, as in Home Assistant; tapping the card turns it off. + if(d=="light")action("light.turn_on",t.entity,"brightness",std::to_string(std::max(3,(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)); @@ -601,10 +608,15 @@ inline void show_detail(unsigned index){ lv_obj_set_style_bg_color(detail_root,lv_color_hex(0xE7E7E7),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); + // The same top bar as the board's own cards: a round back arrow at the left, the name centred. + int bar=large?60:40,bar_x=large?16:10,bar_y=large?16:8; + auto *back=detail_button("",bar_x,bar_y,bar,bar,-1);lv_obj_set_style_radius(back,LV_RADIUS_CIRCLE,0);lv_obj_set_style_bg_color(back,lv_color_hex(0xEEEEEE),0); + auto *arrow=lv_obj_get_child(back,0);if(mini_icon_font)lv_obj_set_style_text_font(arrow,mini_icon_font,0);lv_label_set_text(arrow,"\U000F004D");lv_obj_set_size(arrow,LV_SIZE_CONTENT,LV_SIZE_CONTENT);lv_obj_center(arrow); + const lv_font_t *title_font=watch_font?watch_font:detail_font; + auto *heading=detail_label(detail_root,t.name,bar_x+bar+8,bar_y+(bar-lv_font_get_line_height(title_font))/2,width-2*(bar_x+bar+8)); + lv_obj_set_style_text_font(heading,title_font,0);lv_obj_set_height(heading,lv_font_get_line_height(title_font));lv_obj_set_style_text_align(heading,LV_TEXT_ALIGN_CENTER,0); 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); + detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?80:50,width-2*pad);lv_obj_set_style_text_align(detail_status,LV_TEXT_ALIGN_CENTER,0);lv_obj_set_style_text_color(detail_status,lv_color_hex(0x616161),0); auto d=t.domain(); if(t.is_switch()){ detail_label(detail_root,"Tap to toggle",pad,top,width-2*pad); @@ -851,19 +863,31 @@ inline void set_number(lv_obj_t *obj, lv_style_prop_t prop, int32_t number, lv_s lv_style_value_t value{}; value.num = number; lv_obj_set_local_style_prop(obj, prop, value, selector); } -// The white handle of a card slider sits inside the rounded end of the fill, like Home Assistant's -// slider. LVGL centres the knob on the end of the fill, so the knob is narrowed and moved back by a -// quarter of the height; the range starts below zero, so at 0 the fill is still one round end that -// holds the handle. Values below zero never leave the slider (see slider_event). +// Card sliders follow Home Assistant's control slider (a 42 px track there): corners of 12 and +// 8 px on track and fill, a white handle 4 px wide and half the height, an eighth of the height in +// from the end of the fill, and a fill that never gets shorter than a third of the height. LVGL +// centres the knob on the end of the fill, so the knob is narrowed and moved back; the range starts +// below zero, so at 0 the fill is still that short stub holding the handle. Values below zero never +// leave the slider (see slider_event). Small strips keep round ends: the corners would not show. +inline int slider_handle_width(int height) { return height > 20 ? 4 : 2; } +inline int slider_stub(int height) { return std::max(height / 3, 2 * std::max(1, height / 8) + slider_handle_width(height)); } inline void slider_handle(lv_obj_t *slider, int width, int height) { - int handle = height > 20 ? 6 : 4, inset = height / 4 + handle / 2, half = height >> 1; + int handle = slider_handle_width(height), inset = std::max(1, height / 8) + handle / 2, half = height >> 1; + set_number(slider, LV_STYLE_RADIUS, height > 20 ? height * 12 / 42 : LV_RADIUS_CIRCLE, LV_PART_MAIN); + set_number(slider, LV_STYLE_RADIUS, height > 20 ? height * 8 / 42 : LV_RADIUS_CIRCLE, LV_PART_INDICATOR); set_number(slider, LV_STYLE_PAD_LEFT, inset + handle / 2 - half, LV_PART_KNOB); set_number(slider, LV_STYLE_PAD_RIGHT, handle / 2 - inset - (height - half), LV_PART_KNOB); set_number(slider, LV_STYLE_PAD_TOP, -(height / 4), LV_PART_KNOB); set_number(slider, LV_STYLE_PAD_BOTTOM, -(height / 4), LV_PART_KNOB); - int below = width > height ? 1000 * height / (width - height) : 0; + int stub = slider_stub(height), below = width > stub ? 1000 * stub / (width - stub) : 0; if (lv_slider_get_min_value(slider) != -below || lv_slider_get_max_value(slider) != 1000) lv_slider_set_range(slider, -below, 1000); } +// An off light or fan shows only the grey track, as in Home Assistant: no fill, no handle. +inline void slider_bar(lv_obj_t *slider, bool shown) { + set_number(slider, LV_STYLE_BG_OPA, shown ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_INDICATOR); + set_number(slider, LV_STYLE_BG_OPA, shown ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_KNOB); +} +inline bool slider_bar_shown(const Tile &t, bool on) { auto d = t.domain(); return on || (d != "light" && d != "fan"); } inline void set_hidden(lv_obj_t *obj, bool hidden) { if (hidden) lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(obj, LV_OBJ_FLAG_HIDDEN); } // A card's size as its styles request it. LVGL's own getters only follow after a layout pass, and // that pass walks every object on the screen, so the cards are laid out without asking for one. @@ -899,7 +923,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(0xFFFFFF),LV_PART_KNOB);lv_obj_set_style_bg_opa(w.slider,LV_OPA_COVER,LV_PART_KNOB); slider_handle(w.slider,lv_obj_get_width(tile)-24,strip); lv_obj_set_style_border_width(w.slider,0,LV_PART_KNOB);lv_obj_set_style_shadow_width(w.slider,0,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,2,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_radius(w.slider,2,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_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); @@ -1028,7 +1052,14 @@ inline std::string time_text(esphome::ESPTime now) { // large cards) with hour and minute hands. A single card adds a calendar block // beside the dial (weekday, big day number, short month); a wide card adds the // digital time and the date instead. Parts: 0-11 marks, 12-13 hands, 14 centre, -// 15-17 text. +// 15-17 text, 18 the second hand (points 28-29), shown while the screen is awake. +inline void second_hand(Widgets &w,const esphome::ESPTime &now) { + float a=(now.is_valid()?now.second:0)*3.14159265f/30; + w.points[28]={(lv_value_precise_t)(w.hand_cx-w.hand_r*0.2f*sinf(a)),(lv_value_precise_t)(w.hand_cy+w.hand_r*0.2f*cosf(a))}; + w.points[29]={(lv_value_precise_t)(w.hand_cx+w.hand_r*0.92f*sinf(a)),(lv_value_precise_t)(w.hand_cy-w.hand_r*0.92f*cosf(a))}; + auto *p=part_line(w,18,w.points+28,2,w.hand_width); + set_hidden(p,!(awake() && now.is_valid())); +} inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int height) { bool analog=t.display=="analog"; begin_extra(w,analog?(w.wide?"analog":"calendar"):"digital",width,height); @@ -1065,6 +1096,11 @@ inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int heigh w.points[2]={(lv_value_precise_t)cx,(lv_value_precise_t)cy};w.points[3]={(lv_value_precise_t)(cx+radius*0.82f*sinf(minute)),(lv_value_precise_t)(cy-radius*0.82f*cosf(minute))}; part_line(w,12,w.points,2,large?5:3);part_line(w,13,w.points+2,2,large?3:2); int center=large?8:4;part_dot(w,14,cx-center/2,cy-center/2,center); + // A thin red second hand with a short tail, under the centre dot; tick() moves it every second. + w.hand_cx=cx;w.hand_cy=cy;w.hand_r=radius;w.hand_width=large?2:1; + bool new_hand=!w.parts[18]; + second_hand(w,now); + if(new_hand)lv_obj_move_to_index(w.parts[18],lv_obj_get_index(w.parts[14])); std::string day=now.is_valid()?std::to_string(now.day_of_month):"--"; if(w.wide){ int x=dial+(large?16:8),y=std::max(0,(height-text_h)/2); @@ -1257,7 +1293,6 @@ inline int layout_panel(Widgets &w,const Tile &t,bool large,int content_w,int co lv_obj_remove_flag(slider,LV_OBJ_FLAG_GESTURE_BUBBLE);lv_obj_remove_flag(slider,LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(slider,mode=="volume"?m.slider_w:m.pill_w,h); lv_obj_set_style_pad_all(slider,0,LV_PART_MAIN); - lv_obj_set_style_radius(slider,LV_RADIUS_CIRCLE,LV_PART_MAIN);lv_obj_set_style_radius(slider,LV_RADIUS_CIRCLE,LV_PART_INDICATOR); lv_obj_set_style_bg_opa(slider,LV_OPA_COVER,LV_PART_MAIN);lv_obj_set_style_bg_opa(slider,LV_OPA_COVER,LV_PART_INDICATOR); // The handle is a short white bar inside the fill, like Home Assistant's slider. lv_obj_set_style_radius(slider,2,LV_PART_KNOB);lv_obj_set_style_bg_color(slider,lv_color_hex(0xFFFFFF),LV_PART_KNOB);lv_obj_set_style_bg_opa(slider,LV_OPA_COVER,LV_PART_KNOB); @@ -1355,9 +1390,12 @@ inline void style_panel(Widgets &w,const Tile &t,lv_color_t accent,lv_color_t te } if(w.pill){set_color(w.pill,LV_STYLE_BG_COLOR,key_bg);set_color(w.pill_value,LV_STYLE_TEXT_COLOR,text);} if(w.control_slider){ - lv_color_t fill=fresh() && t.active()?accent:lv_color_hex(0x9E9E9E); - set_color(w.control_slider,LV_STYLE_BG_COLOR,lv_color_mix(fill,card,76),LV_PART_MAIN); + bool on=fresh() && t.active(); + lv_color_t fill=on?accent:lv_color_hex(0x9E9E9E); + // The track is the fill colour at 20 % over the card, as in Home Assistant. + set_color(w.control_slider,LV_STYLE_BG_COLOR,lv_color_mix(fill,card,51),LV_PART_MAIN); set_color(w.control_slider,LV_STYLE_BG_COLOR,fill,LV_PART_INDICATOR); + slider_bar(w.control_slider,slider_bar_shown(t,on)); } if(w.knob){set_color(w.keys[0],LV_STYLE_BG_COLOR,lv_color_hex(0xD7DADF));set_color(w.keys[0],LV_STYLE_BG_COLOR,lv_color_hex(0xC5C9CF),LV_STATE_PRESSED);} } @@ -1536,10 +1574,11 @@ inline void render_slot(size_t slot) { auto circle_color=available?lv_color_mix(color,lv_color_hex(0xFFFFFF),38):lv_color_hex(0xF0F0F0); // Darken the foreground slightly: very pale bulbs still need a visible icon. auto icon_color=available?lv_color_mix(color,lv_color_hex(0x333333),205):lv_color_hex(0x9E9E9E); - // Off: the round end that holds the handle is grey, so the slider reads as off too. + // Off: a grey track without fill or handle, as in Home Assistant. auto fill_color=on?color:lv_color_hex(0x9E9E9E); set_color(w.slider,LV_STYLE_BG_COLOR,fill_color,LV_PART_INDICATOR); - set_color(w.slider,LV_STYLE_BG_COLOR,lv_color_mix(fill_color,lv_color_hex(0xFFFFFF),30),LV_PART_MAIN); + set_color(w.slider,LV_STYLE_BG_COLOR,lv_color_mix(fill_color,lv_color_hex(0xFFFFFF),51),LV_PART_MAIN); + slider_bar(w.slider,slider_bar_shown(t,on)); set_color(w.tile,LV_STYLE_BG_COLOR,lv_color_hex(t.background ? t.background : 0xFFFFFF)); set_number(w.tile,LV_STYLE_BORDER_WIDTH,1); // "Background: none" hides only the card; geometry and padding stay identical, @@ -1563,7 +1602,7 @@ inline void render_slot(size_t slot) { bool muted=w.extra_mode=="forecast" ? i>=2 && i%3==2 : w.extra_mode=="sunpath" ? i>=1 : w.extra_mode=="calendar" ? i==15||i==17 : i==16; if(lv_obj_check_type(p,&lv_label_class))set_color(p,LV_STYLE_TEXT_COLOR,muted?value_color:title_color); else if(w.extra_mode=="sunpath")continue; - else if(lv_obj_check_type(p,&lv_line_class))set_color(p,LV_STYLE_LINE_COLOR,w.extra_mode=="graph"?color:i<12?value_color:i==13?icon_color:title_color); + else if(lv_obj_check_type(p,&lv_line_class))set_color(p,LV_STYLE_LINE_COLOR,w.extra_mode=="graph"?color:i==18?lv_color_hex(0xE53935):i<12?value_color:i==13?icon_color:title_color); else set_color(p,LV_STYLE_BG_COLOR,i==14?icon_color:value_color); } lap(swipe_profile::PALETTE); @@ -1586,10 +1625,11 @@ inline lv_timer_t *fill_timer=nullptr; inline bool fill_refreshed=false; inline uint32_t fill_step_ms=0; inline bool fill_cards(size_t cards); +// Before the first layout the screen is starting: HA connects, then ESP Screens sends the tiles. inline void render(lv_obj_t *room) { if (!enabled) return; room_label=room; swipe_profile::Lap lap; - label(room, !model.configured ? "Choose tiles in HA" : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title); + label(room, !model.configured ? (!ha_connected() ? "Connecting to Home Assistant..." : "Waiting for ESP Screens...") : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title); render_header(); lap(swipe_profile::HEADER); bool all=dirty_all || (!dirty_tiles && !dirty_header); @@ -2068,6 +2108,8 @@ inline void tick() { auto &w=widgets[slot];if(!w.tile || w.index>=model.count || lv_obj_has_flag(w.tile,LV_OBJ_FLAG_HIDDEN))continue; const auto &t=model.tiles[w.index]; if((t.builtin() && new_minute) || (t.domain()=="timer" && t.state=="active") || (t.domain()=="sun" && second%60==0))card(w.index); + // The second hand moves on its own: only its line is redrawn, and it hides during standby. + else if(w.parts[18] && w.points && t.builtin() && t.display=="analog")second_hand(w,now); } } if(redraw && refresh)refresh(); diff --git a/components/smart_display/tile_icon_names.h b/components/smart_display/tile_icon_names.h index af48f60..4892c25 100644 --- a/components/smart_display/tile_icon_names.h +++ b/components/smart_display/tile_icon_names.h @@ -175,6 +175,7 @@ inline constexpr Named NAMES[] = { {"chevron-left", 0xF0141}, {"chevron-right", 0xF0142}, {"close", 0xF0156}, + {"arrow-left", 0xF004D}, {"sun-snowflake-variant", 0xF1A79}, {"thermostat-auto", 0xF1B17}, {"weather-fog", 0xF0591}, diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ec1723f..3d83932 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -176,6 +176,28 @@ icons sit off-center in the browser); run `generate_packages.py` afterward. The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual profiles must come from the set. No changed preferences or keys. +### Compatibility 0.2.43 / firmware 0.2.37 + +Firmware only: storage version, tile protocol, preferences and keys are unchanged. + +- `runtime_tiles::screen_awake` is a hook both profiles set to `!id(display_dimmed)`; without it + the runtime counts as awake. `second_hand()` draws part 18 (points 28-29) of an analog clock + card and `tick()` moves it once a second while awake, hiding it otherwise; the minute redraw + stays as it was. +- `slider_handle()` now sets the track and fill radii (12/42 and 8/42 of the height, round ends + under 20 px), a handle of 4 px (2 px on small strips) an eighth of the height from the end, and a + shortest fill of a third of the height (`slider_stub`). `slider_bar()` hides fill and knob for + an off light or fan (`slider_bar_shown`); tracks are the fill colour at 20 % over the card. + `light_controls::Row::off` does the same for the colour card's brightness row. +- `commit_slider` floors a light's brightness at 3 (1 %); the card tap turns it off. +- Overlay top bar: the profiles' `*_close_button`, `climate_mode_overlay_x`, `vacuum_overlay_x` and + the new `overlay_back_button` are one round `arrow-left` (glyph `F004D`, added to + `tile_icons.FIXED`) button at the top left, 60/40 px (Guition/CYD) at (16,16)/(10,8); + `climate_power_button` is the same size at the top right. `color_done_button` and + `vacuum_overlay_ok` are gone; `show_detail()` draws the same bar for the runtime detail card. +- `render()` shows "Connecting to Home Assistant..." / "Waiting for ESP Screens..." until the + first layout arrives. + ### Compatibility 0.2.42 / firmware 0.2.36 Firmware only: storage version, tile protocol, preferences and keys are unchanged. What moved diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index de3ab02..6cae3ec 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.36" + SCREEN_FIRMWARE_VERSION: "0.2.37" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" @@ -641,6 +641,7 @@ esphome: runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); runtime_tiles::clock_font = id(clock_digits)->get_lv_font(); runtime_tiles::now_time = []() { return id(homeassistant_time).now(); }; + runtime_tiles::screen_awake = []() { return !id(display_dimmed); }; runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::control_font = id(sublabel_big)->get_lv_font(); // Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock @@ -1342,6 +1343,7 @@ font: - "\U000F0141" # chevron-left - "\U000F0142" # chevron-right - "\U000F0156" # close + - "\U000F004D" # arrow-left - "\U000F1A79" # sun-snowflake-variant - "\U000F1B17" # thermostat-auto - "\U000F0591" # weather-fog @@ -3678,10 +3680,33 @@ lvgl: clickable: false scrollable: false + - obj: + id: overlay_back_button + align: TOP_LEFT + x: 16 + y: 16 + width: 60 + height: 60 + radius: 999 + bg_color: 0xEEEEEE + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + on_click: + - script.execute: close_cards + widgets: + - label: + align: CENTER + text: "\U000F004D" + text_font: materialdesign_icons_mini + text_color: 0x1B1B1B + clickable: false + - label: id: overlay_title align: TOP_MID - y: 20 + y: 30 text: "Light" styles: style_room @@ -3992,45 +4017,10 @@ lvgl: - obj: id: color_close_button align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 - radius: 999 - bg_color: 0xEEEEEE - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - label: - id: color_detail_title - width: 310 - long_mode: DOT - text_align: CENTER - align: TOP_MID - y: 32 - text: "Light Color" - styles: style_room - - - obj: - id: color_done_button - align: TOP_RIGHT - x: -15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4042,11 +4032,22 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + - label: + id: color_detail_title + width: 310 + long_mode: DOT + text_align: CENTER + align: TOP_MID + y: 35 + text: "Light Color" + styles: style_room + + # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay @@ -4089,18 +4090,14 @@ lvgl: - obj: id: climate_close_button align: TOP_LEFT - x: 15 - y: 18 - # 44 instead of 36: slightly bigger tap target, grows away from the arc - # (top-left anchor stays put) instead of towards it. - width: 66 - height: 66 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% + border_width: 0 clickable: true scrollable: false on_click: @@ -4108,18 +4105,18 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false - obj: id: climate_power_button align: TOP_RIGHT - x: -15 - y: 18 - width: 66 - height: 66 + x: -16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4140,7 +4137,7 @@ lvgl: - label: id: climate_detail_title align: TOP_MID - y: 32 + y: 35 text: "Climate" styles: style_room @@ -4311,7 +4308,7 @@ lvgl: widgets: - label: align: TOP_MID - y: 32 + y: 35 text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B @@ -4684,16 +4681,14 @@ lvgl: - obj: id: climate_mode_overlay_x align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 - bg_color: 0xFFFFFF + bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xDDDDDD - border_opa: COVER + border_width: 0 clickable: true scrollable: false on_click: @@ -4701,8 +4696,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4727,10 +4722,9 @@ lvgl: - label: id: vacuum_title align: TOP_MID - y: 32 + y: 35 text: "Vacuum" - text_font: sublabel_big - text_color: 0x1B1B1B + styles: style_room clickable: false - obj: @@ -4948,38 +4942,13 @@ lvgl: - obj: id: vacuum_overlay_x align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - obj: - id: vacuum_overlay_ok - align: TOP_RIGHT - x: -15 - y: 18 - width: 54 - height: 54 - radius: 999 - bg_color: 0xD5EEFC - bg_opa: COVER border_width: 0 clickable: true scrollable: false @@ -4988,11 +4957,12 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + # ---------- DIM / WAKE OVERLAY ---------- - obj: id: dim_wake_overlay @@ -5691,12 +5661,10 @@ script: light_controls::open("diagnostic", true, true, 75); lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay - delay: 40s - lvgl.widget.hide: color_detail_overlay - lvgl.widget.show: color_close_button - - lvgl.widget.show: color_done_button - lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};' - id: open_color_detail diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index b31b2db..ad48422 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -57,7 +57,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.36" + SCREEN_FIRMWARE_VERSION: "0.2.37" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. @@ -823,6 +823,7 @@ esphome: runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); runtime_tiles::clock_font = id(clock_digits)->get_lv_font(); runtime_tiles::now_time = []() { return id(homeassistant_time).now(); }; + runtime_tiles::screen_awake = []() { return !id(display_dimmed); }; runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::control_font = id(sublabel_big)->get_lv_font(); // Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock @@ -1531,6 +1532,7 @@ font: - "\U000F0141" # chevron-left - "\U000F0142" # chevron-right - "\U000F0156" # close + - "\U000F004D" # arrow-left - "\U000F1A79" # sun-snowflake-variant - "\U000F1B17" # thermostat-auto - "\U000F0591" # weather-fog @@ -3797,24 +3799,47 @@ lvgl: clickable: false scrollable: false + - obj: + id: overlay_back_button + align: TOP_LEFT + x: 10 + y: 8 + width: 40 + height: 40 + radius: 999 + bg_color: 0xEEEEEE + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + on_click: + - script.execute: close_cards + widgets: + - label: + align: CENTER + text: "\U000F004D" + text_font: materialdesign_icons_mini + text_color: 0x1B1B1B + clickable: false + - label: id: overlay_title align: TOP_MID - y: 10 + y: 19 text: "Light" styles: style_room - label: id: overlay_value align: TOP_MID - y: 35 + y: 42 text: "100 %" styles: style_value_big - slider: id: overlay_slider align: CENTER - y: 18 + y: 24 width: ${OVERLAY_SLIDER_W} height: ${OVERLAY_SLIDER_H} min_value: 0 @@ -4112,44 +4137,9 @@ lvgl: id: color_close_button align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xEEEEEE - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - label: - id: color_detail_title - width: 210 - long_mode: DOT - text_align: CENTER - align: TOP_MID - y: 16 - text: "Light Color" - styles: style_room - - - obj: - id: color_done_button - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4161,11 +4151,22 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + - label: + id: color_detail_title + width: 210 + long_mode: DOT + text_align: CENTER + align: TOP_MID + y: 21 + text: "Light Color" + styles: style_room + + # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay @@ -4209,17 +4210,13 @@ lvgl: id: climate_close_button align: TOP_LEFT x: 10 - y: 9 - # 44 instead of 36: slightly bigger tap target, grows away from the arc - # (top-left anchor stays put) instead of towards it. - width: 44 - height: 44 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% + border_width: 0 clickable: true scrollable: false on_click: @@ -4227,8 +4224,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4236,9 +4233,9 @@ lvgl: id: climate_power_button align: TOP_RIGHT x: -10 - y: 9 - width: 44 - height: 44 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4259,7 +4256,7 @@ lvgl: - label: id: climate_detail_title align: TOP_MID - y: 16 + y: 21 text: "Climate" styles: style_room @@ -4430,7 +4427,7 @@ lvgl: widgets: - label: align: TOP_MID - y: 16 + y: 21 text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B @@ -4804,15 +4801,13 @@ lvgl: id: climate_mode_overlay_x align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 - bg_color: 0xFFFFFF + bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xDDDDDD - border_opa: COVER + border_width: 0 clickable: true scrollable: false on_click: @@ -4820,8 +4815,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4846,10 +4841,9 @@ lvgl: - label: id: vacuum_title align: TOP_MID - y: 16 + y: 21 text: "Vacuum" - text_font: sublabel_big - text_color: 0x1B1B1B + styles: style_room clickable: false - obj: @@ -5068,37 +5062,12 @@ lvgl: id: vacuum_overlay_x align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - obj: - id: vacuum_overlay_ok - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xD5EEFC - bg_opa: COVER border_width: 0 clickable: true scrollable: false @@ -5107,11 +5076,12 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + # ---------- DIM / WAKE OVERLAY ---------- - obj: id: dim_wake_overlay @@ -5786,12 +5756,10 @@ script: light_controls::open("diagnostic", true, true, 75); lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay - delay: 40s - lvgl.widget.hide: color_detail_overlay - lvgl.widget.show: color_close_button - - lvgl.widget.show: color_done_button - lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};' - id: open_color_detail diff --git a/packages/cyd.yaml b/packages/cyd.yaml index b602e39..aa204e7 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -58,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.36" + SCREEN_FIRMWARE_VERSION: "0.2.37" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. @@ -827,6 +827,7 @@ esphome: runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); runtime_tiles::clock_font = id(clock_digits)->get_lv_font(); runtime_tiles::now_time = []() { return id(homeassistant_time).now(); }; + runtime_tiles::screen_awake = []() { return !id(display_dimmed); }; runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::control_font = id(sublabel_big)->get_lv_font(); // Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock @@ -1520,6 +1521,7 @@ font: - "\U000F0141" # chevron-left - "\U000F0142" # chevron-right - "\U000F0156" # close + - "\U000F004D" # arrow-left - "\U000F1A79" # sun-snowflake-variant - "\U000F1B17" # thermostat-auto - "\U000F0591" # weather-fog @@ -3728,24 +3730,47 @@ lvgl: clickable: false scrollable: false + - obj: + id: overlay_back_button + align: TOP_LEFT + x: 10 + y: 8 + width: 40 + height: 40 + radius: 999 + bg_color: 0xEEEEEE + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + on_click: + - script.execute: close_cards + widgets: + - label: + align: CENTER + text: "\U000F004D" + text_font: materialdesign_icons_mini + text_color: 0x1B1B1B + clickable: false + - label: id: overlay_title align: TOP_MID - y: 10 + y: 19 text: "Light" styles: style_room - label: id: overlay_value align: TOP_MID - y: 35 + y: 42 text: "100 %" styles: style_value_big - slider: id: overlay_slider align: CENTER - y: 18 + y: 24 width: ${OVERLAY_SLIDER_W} height: ${OVERLAY_SLIDER_H} min_value: 0 @@ -4043,44 +4068,9 @@ lvgl: id: color_close_button align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xEEEEEE - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - label: - id: color_detail_title - width: 210 - long_mode: DOT - text_align: CENTER - align: TOP_MID - y: 16 - text: "Light Color" - styles: style_room - - - obj: - id: color_done_button - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4092,11 +4082,22 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + - label: + id: color_detail_title + width: 210 + long_mode: DOT + text_align: CENTER + align: TOP_MID + y: 21 + text: "Light Color" + styles: style_room + + # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay @@ -4140,17 +4141,13 @@ lvgl: id: climate_close_button align: TOP_LEFT x: 10 - y: 9 - # 44 instead of 36: slightly bigger tap target, grows away from the arc - # (top-left anchor stays put) instead of towards it. - width: 44 - height: 44 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% + border_width: 0 clickable: true scrollable: false on_click: @@ -4158,8 +4155,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4167,9 +4164,9 @@ lvgl: id: climate_power_button align: TOP_RIGHT x: -10 - y: 9 - width: 44 - height: 44 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4190,7 +4187,7 @@ lvgl: - label: id: climate_detail_title align: TOP_MID - y: 16 + y: 21 text: "Climate" styles: style_room @@ -4361,7 +4358,7 @@ lvgl: widgets: - label: align: TOP_MID - y: 16 + y: 21 text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B @@ -4735,15 +4732,13 @@ lvgl: id: climate_mode_overlay_x align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 - bg_color: 0xFFFFFF + bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xDDDDDD - border_opa: COVER + border_width: 0 clickable: true scrollable: false on_click: @@ -4751,8 +4746,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4777,10 +4772,9 @@ lvgl: - label: id: vacuum_title align: TOP_MID - y: 16 + y: 21 text: "Vacuum" - text_font: sublabel_big - text_color: 0x1B1B1B + styles: style_room clickable: false - obj: @@ -4999,37 +4993,12 @@ lvgl: id: vacuum_overlay_x align: TOP_LEFT x: 10 - y: 9 - width: 36 - height: 36 + y: 8 + width: 40 + height: 40 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - obj: - id: vacuum_overlay_ok - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xD5EEFC - bg_opa: COVER border_width: 0 clickable: true scrollable: false @@ -5038,11 +5007,12 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + # ---------- DIM / WAKE OVERLAY ---------- - obj: id: dim_wake_overlay @@ -5717,12 +5687,10 @@ script: light_controls::open("diagnostic", true, true, 75); lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay - delay: 40s - lvgl.widget.hide: color_detail_overlay - lvgl.widget.show: color_close_button - - lvgl.widget.show: color_done_button - lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};' - id: open_color_detail diff --git a/packages/guition.yaml b/packages/guition.yaml index 65eac4e..de7e4ef 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.36" + SCREEN_FIRMWARE_VERSION: "0.2.37" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" @@ -645,6 +645,7 @@ esphome: runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); runtime_tiles::clock_font = id(clock_digits)->get_lv_font(); runtime_tiles::now_time = []() { return id(homeassistant_time).now(); }; + runtime_tiles::screen_awake = []() { return !id(display_dimmed); }; runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::control_font = id(sublabel_big)->get_lv_font(); // Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock @@ -1331,6 +1332,7 @@ font: - "\U000F0141" # chevron-left - "\U000F0142" # chevron-right - "\U000F0156" # close + - "\U000F004D" # arrow-left - "\U000F1A79" # sun-snowflake-variant - "\U000F1B17" # thermostat-auto - "\U000F0591" # weather-fog @@ -3609,10 +3611,33 @@ lvgl: clickable: false scrollable: false + - obj: + id: overlay_back_button + align: TOP_LEFT + x: 16 + y: 16 + width: 60 + height: 60 + radius: 999 + bg_color: 0xEEEEEE + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + on_click: + - script.execute: close_cards + widgets: + - label: + align: CENTER + text: "\U000F004D" + text_font: materialdesign_icons_mini + text_color: 0x1B1B1B + clickable: false + - label: id: overlay_title align: TOP_MID - y: 20 + y: 30 text: "Light" styles: style_room @@ -3923,45 +3948,10 @@ lvgl: - obj: id: color_close_button align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 - radius: 999 - bg_color: 0xEEEEEE - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - label: - id: color_detail_title - width: 310 - long_mode: DOT - text_align: CENTER - align: TOP_MID - y: 32 - text: "Light Color" - styles: style_room - - - obj: - id: color_done_button - align: TOP_RIGHT - x: -15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -3973,11 +3963,22 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + - label: + id: color_detail_title + width: 310 + long_mode: DOT + text_align: CENTER + align: TOP_MID + y: 35 + text: "Light Color" + styles: style_room + + # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay @@ -4020,18 +4021,14 @@ lvgl: - obj: id: climate_close_button align: TOP_LEFT - x: 15 - y: 18 - # 44 instead of 36: slightly bigger tap target, grows away from the arc - # (top-left anchor stays put) instead of towards it. - width: 66 - height: 66 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% + border_width: 0 clickable: true scrollable: false on_click: @@ -4039,18 +4036,18 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false - obj: id: climate_power_button align: TOP_RIGHT - x: -15 - y: 18 - width: 66 - height: 66 + x: -16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER @@ -4071,7 +4068,7 @@ lvgl: - label: id: climate_detail_title align: TOP_MID - y: 32 + y: 35 text: "Climate" styles: style_room @@ -4242,7 +4239,7 @@ lvgl: widgets: - label: align: TOP_MID - y: 32 + y: 35 text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B @@ -4615,16 +4612,14 @@ lvgl: - obj: id: climate_mode_overlay_x align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 - bg_color: 0xFFFFFF + bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xDDDDDD - border_opa: COVER + border_width: 0 clickable: true scrollable: false on_click: @@ -4632,8 +4627,8 @@ lvgl: widgets: - label: align: CENTER - text: "x" - text_font: sublabel_big + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false @@ -4658,10 +4653,9 @@ lvgl: - label: id: vacuum_title align: TOP_MID - y: 32 + y: 35 text: "Vacuum" - text_font: sublabel_big - text_color: 0x1B1B1B + styles: style_room clickable: false - obj: @@ -4879,38 +4873,13 @@ lvgl: - obj: id: vacuum_overlay_x align: TOP_LEFT - x: 15 - y: 18 - width: 54 - height: 54 + x: 16 + y: 16 + width: 60 + height: 60 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - script.execute: close_cards - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0x1B1B1B - clickable: false - - - obj: - id: vacuum_overlay_ok - align: TOP_RIGHT - x: -15 - y: 18 - width: 54 - height: 54 - radius: 999 - bg_color: 0xD5EEFC - bg_opa: COVER border_width: 0 clickable: true scrollable: false @@ -4919,11 +4888,12 @@ lvgl: widgets: - label: align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons + text: "\U000F004D" + text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false + # ---------- DIM / WAKE OVERLAY ---------- - obj: id: dim_wake_overlay @@ -5622,12 +5592,10 @@ script: light_controls::open("diagnostic", true, true, 75); lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay - delay: 40s - lvgl.widget.hide: color_detail_overlay - lvgl.widget.show: color_close_button - - lvgl.widget.show: color_done_button - lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};' - id: open_color_detail diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 1d3fc99..7d9524a 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.2.43 (firmware 0.2.37) + +A second hand on the analog clock, sliders that look like Home Assistant's, and one top bar for every card. + +- **Second hand.** The analog clock card gets a thin red second hand with a short tail that moves every second while the screen is in use. During standby (also at the night level) it is hidden and the clock keeps its once-a-minute redraw, so a sleeping screen does no extra work. Only the hand's own line is redrawn, not the card. The small dial in the top bar stays as it was. +- **Sliders like Home Assistant.** Card sliders (a light's or fan's direct control, the Sonos volume, the brightness row of the colour card) now use the proportions of HA's control slider: softer corners on track and fill, a slimmer white handle a little in from the end of the fill, a track in the fill colour at 20 %, and a short stub instead of a full circle at 0 or 1 %. A light or fan that is off shows only the empty grey track, without fill or handle, exactly as HA does; the colour card's brightness row then reads Off until you move it. +- **A light's slider stops at 1 %.** As in HA, dragging a light's slider all the way down leaves it on at 1 %; tapping the card turns it off. +- **One top bar on every card.** Every card that opens over the tiles (brightness, colour, climate, vacuum, media, timer, switch, sensor history and the others) has the same bar: a round back arrow at the left, big enough for a finger (60 px on the Guition, 40 px on the CYD), and the name in the middle. Only a card with one overriding action keeps a round button at the right, such as the on/off of a climate device; the colour and vacuum cards lose their extra "done" buttons, which only closed the card. The brightness card, which had no button at all, gets the back arrow too. +- **A starting screen says so.** Until the first layout arrives the top bar reads "Connecting to Home Assistant..." and then "Waiting for ESP Screens...", instead of "Choose tiles in HA". +- Firmware 0.2.37 for both boards; the app offers the update. + ## 0.2.42 (firmware 0.2.36) Smoother page swipes and a new colour card for lights. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 47d697e..9d61e8e 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -17,7 +17,7 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] REPO = 'https://github.com/MaxGramser/homeassistant_espscreen' REFS = {'cyd': 'main', 'guition': 'main'} # Firmware shipped with this app release; screens below it get an update offer. -FIRMWARE_VERSION = '0.2.36' +FIRMWARE_VERSION = '0.2.37' ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split()) # Attributes whose boolean value the screen needs; every other bool stays behind. BOOL_ATTRS = frozenset(['is_volume_muted']) diff --git a/screen_manager/app/static/bar-roboto-400.woff b/screen_manager/app/static/bar-roboto-400.woff index 4daee46677987712eed4f186baba6c666d2e37bf..6fcdaf33fb25abcbf8bbb4078bdfbdcfff344949 100644 GIT binary patch delta 32 qcmV+*0N?+pZm4ds1pxz>Io^{90hI%$Cry)}0Tlzz+Oo5_0dO_VWek}B delta 32 qcmV+*0N?+pZm4ds1pxz=DJPQ%0hI%&NV}7u0Tlzy$@{ao0dO_NZ48+J diff --git a/screen_manager/app/static/bar-roboto-500.woff b/screen_manager/app/static/bar-roboto-500.woff index 8b40693337892fd2b065d8917c11a0c1a6a4e74a..e95315bd2e48ff48b3072150fb7467623b0a1726 100644 GIT binary patch delta 32 qcmV+*0N?+hZ=i3m1pxzmIqQ=M0hI%hYgLn=0Tlzz+Oo5_0q8Z(Qw=Nt delta 32 qcmV+*0N?+hZ=i3m1pxzlDK(P_0hI%jjKPzj0Tlzy$^5gp0q8ZxTMaA# diff --git a/screen_manager/app/static/tile-icons.woff b/screen_manager/app/static/tile-icons.woff index 1edf2a8418eb0766a1e82cea2eebfbbe2c3c1650..93ede61559353f182264b4d990807adc560a0d40 100644 GIT binary patch delta 13560 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 X<&Sx5&DY)rhJNh-KKVm<1;70t^3A#d diff --git a/screen_manager/app/tile_icons.py b/screen_manager/app/tile_icons.py index 3f84bf6..d0cf96c 100644 --- a/screen_manager/app/tile_icons.py +++ b/screen_manager/app/tile_icons.py @@ -205,6 +205,8 @@ FIXED = ( ('chevron-left', 'F0141'), ('chevron-right', 'F0142'), ('close', 'F0156'), + # Back button of every overlay (firmware 0.2.37+). + ('arrow-left', 'F004D'), ('sun-snowflake-variant', 'F1A79'), ('thermostat-auto', 'F1B17'), ('weather-fog', 'F0591'), diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index 1424be5..c0219ef 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.42" +version: "0.2.43" slug: esp_screen_manager description: Choose tiles for your CYD and Guition touchscreen, without reflashing. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_header_bar.py b/tests/test_header_bar.py index d322f35..fc84bdd 100644 --- a/tests/test_header_bar.py +++ b/tests/test_header_bar.py @@ -222,6 +222,19 @@ class ParityTests(unittest.TestCase): self.assertIn('render_header();', TILES.split('inline void render(lv_obj_t *room) {', 1)[1].split('\n', 5)[4]) self.assertLessEqual(HEADER_MIN_FIRMWARE, tuple(int(p) for p in FIRMWARE_VERSION.split('.'))) + def test_second_hand_runs_only_while_the_screen_is_awake(self): + for name in PROFILES: + text = (ROOT / name).read_text() + self.assertIn('runtime_tiles::screen_awake = []() { return !id(display_dimmed); };', text, name) + for name in ('packages/cyd.yaml', 'packages/guition.yaml'): + self.assertIn('runtime_tiles::screen_awake', (ROOT / name).read_text(), name) + self.assertIn('inline bool awake() { return !screen_awake || screen_awake(); }', TILES) + second_hand = TILES.split('inline void second_hand(', 1)[1].split('\n}', 1)[0] + self.assertIn('set_hidden(p,!(awake() && now.is_valid()));', second_hand) + self.assertIn('part_line(w,18,w.points+28,2,w.hand_width)', second_hand) + tick = TILES.split('inline void tick() {', 1)[1] + self.assertIn('t.builtin() && t.display=="analog")second_hand(w,now);', tick) + HAS_AIOHTTP = importlib.util.find_spec('aiohttp') is not None if HAS_AIOHTTP: from aiohttp.test_utils import TestClient, TestServer