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 4daee46..6fcdaf3 100644 Binary files a/screen_manager/app/static/bar-roboto-400.woff and b/screen_manager/app/static/bar-roboto-400.woff differ diff --git a/screen_manager/app/static/bar-roboto-500.woff b/screen_manager/app/static/bar-roboto-500.woff index 8b40693..e95315b 100644 Binary files a/screen_manager/app/static/bar-roboto-500.woff and b/screen_manager/app/static/bar-roboto-500.woff differ diff --git a/screen_manager/app/static/tile-icons.woff b/screen_manager/app/static/tile-icons.woff index 1edf2a8..93ede61 100644 Binary files a/screen_manager/app/static/tile-icons.woff and b/screen_manager/app/static/tile-icons.woff differ 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