diff --git a/AGENTS.md b/AGENTS.md index 0547682..0f9ea34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ De voorkeursroute voor nieuwe gebruikers is docs/EASY_SETUP.md: ESP Screen Manag plus remote ESPHome-pakketten. Geen token of blueprint nodig. Tegels staan in de permanente add-ondata; wifi/API/OTA blijven in de eigen ESPHome-YAML. Lees docs/RELEASING.md voordat je updates publiceert. Main distribueert beide borden. +Elke push naar GitHub is een release: verhoog dan ook altijd de add-onversie in +screen_manager/config.yaml (met CHANGELOG-regel), anders ziet HA geen update. Genereer packages met tools/generate_packages.py; bewerk ze niet handmatig. Runtime-modus ondersteunt alle kaarten op alle twintig posities. De positiebeperkingen hieronder gelden uitsluitend voor de oude handmatige profielen. diff --git a/README.md b/README.md index e99504a..f2cfe16 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,12 @@ De firmware en ingebouwde CLI zijn getest met **ESPHome 2026.6.2**. - **Pastel achtergronden per tegel:** kies rood voor een alles-uit-script, groen voor alles-aan, of een andere kleur. Titel en status blijven donker en leesbaar. De kleur verschijnt ook in het schermvoorbeeld; **Standaard** herstelt - de normale kleuren. Vereist firmware 0.2.10 of nieuwer. + de normale kleuren. Vereist firmware 0.2.10 of nieuwer. **Geen** laat de kaart + helemaal weg: de inhoud staat dan even groot direct op de schermachtergrond + (firmware 0.2.16). +- **Klok:** digitaal of analoog. De analoge klok heeft streepjes met 12, 3, 6 en 9 + en toont op een enkele tegel een kalenderblok (weekdag, dag, maand) naast de + wijzerplaat; dubbelbreed staat de digitale tijd met datum ernaast. - **Lampbediening:** helderheid, regenboogkleur en wittemperatuur volgens de mogelijkheden van de lamp. Open de detailbediening met een lange aanraking. - **Meer kaarten:** climate, vacuum, fan, cover, media player, sensoren, @@ -142,6 +147,11 @@ apparaatprofielen; een app verwijderen of flashgeheugen wissen is geen update. Zie [releasegeschiedenis](screen_manager/CHANGELOG.md) en [releases en protocolcompatibiliteit](docs/RELEASING.md). +**Voor wie zelf publiceert:** elke push naar GitHub is een release. Verhoog dan +altijd ook de add-onversie in `screen_manager/config.yaml` en zet de wijziging in +de CHANGELOG, anders biedt de HA-appwinkel geen update aan. Een wijziging aan het +scherm krijgt daarnaast een nieuwe `SCREEN_FIRMWARE_VERSION` in beide bordprofielen. + ## Handleidingen en hulp bij installatie - [Complete installatie vanuit ESP Screens](docs/EASY_SETUP.md) diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index f46edc9..0cac6aa 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -49,6 +49,7 @@ struct Tile { uint32_t timer_end = 0; float battery = NAN, volume = NAN; uint32_t supported = 0, background = 0; + bool transparent = false; // "Achtergrond: geen": card fill and border hidden, contents unchanged. std::string revision, pending_revision; uint32_t pending_since = 0; bool pending = false, confirmed = false, local_feedback = false; diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index f3de5db..00c46c4 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -161,7 +161,9 @@ inline std::string receive(const std::string &payload) { if(tile.pending && !tile.local_feedback && !was_confirmed && tile.confirmed) ESP_LOGI("runtime_action","HA state received entity=%s elapsed=%u ms",entity.c_str(),(unsigned)(esphome::millis()-tile.pending_since)); auto options = root["o"]; - tile.background = tile_palette::color(string(options["background"],16)); + std::string background=string(options["background"],16); + tile.background = tile_palette::color(background); + tile.transparent = tile_palette::transparent(background); tile.tap = string(options["tap"]); if (tile.tap.empty()) tile.tap="auto"; tile.display = string(options["display"]); if (tile.display.empty()) tile.display="standard"; tile.inline_control = string(options["inline"]); if (tile.inline_control.empty()) tile.inline_control="none"; @@ -543,11 +545,18 @@ inline std::string timer_text(const Tile &t) { if (t.state == "paused") return "Pauze " + countdown(duration_seconds(t.remaining)); return t.duration.empty() ? "Uit" : countdown(duration_seconds(t.duration)); } -inline std::string date_text(const esphome::ESPTime &now) { +inline std::string weekday_text(const esphome::ESPTime &now) { static const char *days[] = {"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"}; + return now.is_valid() && now.day_of_week >= 1 && now.day_of_week <= 7 ? days[now.day_of_week - 1] : ""; +} +inline std::string month_short(const esphome::ESPTime &now) { + static const char *months[] = {"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}; + return now.is_valid() && now.month >= 1 && now.month <= 12 ? months[now.month - 1] : ""; +} +inline std::string date_text(const esphome::ESPTime &now) { static const char *months[] = {"januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"}; if (!now.is_valid() || now.day_of_week < 1 || now.day_of_week > 7 || now.month < 1 || now.month > 12) return ""; - return std::string(days[now.day_of_week - 1]) + " " + std::to_string(now.day_of_month) + " " + months[now.month - 1]; + return weekday_text(now) + " " + std::to_string(now.day_of_month) + " " + months[now.month - 1]; } inline void event(lv_event_t *event) { auto &w = *static_cast(lv_event_get_user_data(event)); @@ -705,11 +714,14 @@ inline lv_obj_t *part_line(Widgets &w,unsigned i,lv_point_precise_t *points,unsi inline std::string time_text(esphome::ESPTime now) { return now.is_valid() ? now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M") : "--:--"; } -// Digital: big time over the date. Analog: dotted dial with hour and minute hands; -// a wide card adds the digital time next to the dial. +// Digital: big time over the date. Analog: index strokes (numerals at 12/3/6/9 on +// 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. inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int height) { bool analog=t.display=="analog"; - begin_extra(w,analog?"analog":"digital",width,height); + begin_extra(w,analog?(w.wide?"analog":"calendar"):"digital",width,height); auto now=now_time?now_time():esphome::ESPTime{}; const lv_font_t *big=clock_font?clock_font:watch_value_font?watch_value_font:w.value_font; const lv_font_t *small=lv_obj_get_style_text_font(w.title,LV_PART_MAIN); @@ -722,21 +734,53 @@ inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int heigh part_label(w,16,small,0,with_date?y+lv_font_get_line_height(big)+2:y,width,LV_TEXT_ALIGN_CENTER,with_date?date_text(now):""); return; } - int dial=std::min(height,width),cx=dial/2,cy=height/2,dot=large?4:2,radius=dial/2-dot; + // The dial keeps the same size with or without a card behind it. + int dial=std::min(height,width),cx=dial/2,cy=height/2,outer=dial/2-1,radius=dial/2-(large?4:2); for(int i=0;i<12;++i){ - float a=i*3.14159265f/6;int size=i%3==0?dot+(large?2:1):dot; - part_dot(w,i,cx+std::lround(radius*sinf(a))-size/2,cy-std::lround(radius*cosf(a))-size/2,size); + float a=i*3.14159265f/6;bool cardinal=i%3==0; + if(cardinal && large){ + // Numerals replace the four cardinal strokes; the box is one line high and wide. + int box=lv_font_get_line_height(w.value_font)+4,ring=outer-11; + part_label(w,i,w.value_font,cx+std::lround(ring*sinf(a))-box/2,cy-std::lround(ring*cosf(a))-box/2,box,LV_TEXT_ALIGN_CENTER,i==0?"12":std::to_string(i)); + continue; + } + int length=cardinal?(large?9:5):(large?5:3); + auto *p=w.points+4+2*i; + p[0]={(lv_value_precise_t)(cx+outer*sinf(a)),(lv_value_precise_t)(cy-outer*cosf(a))}; + p[1]={(lv_value_precise_t)(cx+(outer-length)*sinf(a)),(lv_value_precise_t)(cy-(outer-length)*cosf(a))}; + part_line(w,i,p,2,cardinal?(large?3:2):(large?2:1)); } float hour=((now.is_valid()?now.hour%12:0)+(now.is_valid()?now.minute:0)/60.0f)*3.14159265f/6, minute=(now.is_valid()?now.minute:0)*3.14159265f/30; w.points[0]={(lv_value_precise_t)cx,(lv_value_precise_t)cy};w.points[1]={(lv_value_precise_t)(cx+radius*0.52f*sinf(hour)),(lv_value_precise_t)(cy-radius*0.52f*cosf(hour))}; 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); + 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); part_label(w,15,big,x,y,width-x,LV_TEXT_ALIGN_CENTER,time_text(now)); part_label(w,16,small,x,with_date?y+lv_font_get_line_height(big)+2:y,width-x,LV_TEXT_ALIGN_CENTER,with_date?date_text(now):""); + return; } + int x=dial+(large?10:6),room=std::max(1,width-x); + if(!large){ + // Compact cards: "13 sep" in the large-value font beside the dial. + const lv_font_t *font=watch_value_font?watch_value_font:w.value_font; + part_label(w,16,font,x,std::max(0,int(height-lv_font_get_line_height(font))/2),room,LV_TEXT_ALIGN_CENTER,day+" "+month_short(now)); + return; + } + // Calendar block: weekday over a big day number with the short month beside it. + int top=std::max(0,int(height-lv_font_get_line_height(w.value_font)-lv_font_get_line_height(big))/2); + part_label(w,15,w.value_font,x,top,room,LV_TEXT_ALIGN_CENTER,weekday_text(now)); + std::string month=month_short(now); + lv_point_t day_size,month_size; + lv_text_get_size(&day_size,day.c_str(),big,0,0,LV_COORD_MAX,LV_TEXT_FLAG_EXPAND); + lv_text_get_size(&month_size,month.c_str(),small,0,0,LV_COORD_MAX,LV_TEXT_FLAG_EXPAND); + int gap=6,sx=x+std::max(0,int(room-(day_size.x+gap+month_size.x))/2),day_y=top+int(lv_font_get_line_height(w.value_font)); + part_label(w,16,big,sx,day_y,std::min(room,(int)day_size.x+2),LV_TEXT_ALIGN_LEFT,day); + // Both baselines line up: LVGL measures base_line from the bottom of the line box. + int month_y=day_y+(big->line_height-big->base_line)-(small->line_height-small->base_line); + part_label(w,17,small,sx+day_size.x+gap,std::max(0,month_y),std::max(1,int(x+room-(sx+day_size.x+gap))),LV_TEXT_ALIGN_LEFT,month); } // Current conditions on the left, five day columns on the right (wide cards only). inline void render_forecast(Widgets &w,const Tile &t,bool large,int width,int height) { @@ -938,7 +982,10 @@ inline void render(lv_obj_t *room) { lv_obj_set_style_bg_color(w.slider,lv_color_mix(color,lv_color_hex(dark_text?0xFFFFFF:0x263B50),30),LV_PART_MAIN); lv_obj_set_style_bg_color(w.tile, lv_color_hex(t.background ? t.background : (light_theme ? 0xFFFFFF : (on ? 0xF5F1E8 : 0x526C85))), 0); lv_obj_set_style_border_width(w.tile, 1, 0); - lv_obj_set_style_border_opa(w.tile, LV_OPA_COVER, 0); + // "Achtergrond: geen" hides only the card; geometry and padding stay identical, + // and the pressed flash still shows because it lives on the PRESSED state. + lv_obj_set_style_bg_opa(w.tile, t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER, 0); + lv_obj_set_style_border_opa(w.tile, t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER, 0); lv_obj_set_style_border_color(w.tile, t.background ? lv_color_mix(lv_color_hex(t.background),lv_color_hex(0x000000),220) : lv_color_hex(light_theme ? 0xDDDDDD : (on ? 0xF5F1E8 : 0x9CB3C8)), 0); lv_obj_set_style_bg_color(w.circle,circle_color,0); lv_obj_set_style_text_color(w.icon,icon_color,0); @@ -952,10 +999,10 @@ inline void render(lv_obj_t *room) { w.fill_color=color; for(unsigned i=0;i=2 && i%3==2 : w.extra_mode=="sunpath" ? i>=1 : i==16; + 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))lv_obj_set_style_text_color(p,muted?value_color:title_color,0); else if(w.extra_mode=="sunpath")continue; - else if(lv_obj_check_type(p,&lv_line_class))lv_obj_set_style_line_color(p,w.extra_mode=="graph"?color:i==13?icon_color:title_color,0); + else if(lv_obj_check_type(p,&lv_line_class))lv_obj_set_style_line_color(p,w.extra_mode=="graph"?color:i<12?value_color:i==13?icon_color:title_color,0); else lv_obj_set_style_bg_color(p,i==14?icon_color:value_color,0); } } @@ -1027,6 +1074,12 @@ inline bool check_tile_geometry() { if(!palette_ok)ESP_LOGE("ui_test","Tile palette FAIL slot=%u",(unsigned)w.index); fits=fits && palette_ok; } + if(w.index 10: diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 59ce93d..7f68525 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -298,6 +298,8 @@ function renderPreview() { const name = tile.name || entityName(tile.entity); const background=inventory.backgrounds?.[tile.options?.background]?.color; if(background)card.style.backgroundColor=background; + // "Geen": no card on the screen; the mockup keeps a dashed outline as drop target. + if (tile.options?.background === "none") card.classList.add("bare"); if (place.wide) card.classList.add("wide"); card.append(domainBadge(tile.entity), node("strong", name)); card.dataset.index = index; @@ -437,7 +439,7 @@ function renderTileSheet() { button.setAttribute("aria-pressed", String((tile.options?.background || "auto") === key)); const sample = node("span", undefined, "palette-sample"); if (choice.color) sample.style.backgroundColor = choice.color; - else sample.classList.add("palette-auto"); + else sample.classList.add(key === "none" ? "palette-none" : "palette-auto"); button.append(sample, node("span", choice.label)); button.onclick = () => { tile.options = { ...tile.options, background: key }; diff --git a/screen_manager/app/static/style.css b/screen_manager/app/static/style.css index d5c9aff..fd89b13 100644 --- a/screen_manager/app/static/style.css +++ b/screen_manager/app/static/style.css @@ -844,6 +844,8 @@ pre { .palette-choice:focus-visible { outline: 3px solid #1674ae; outline-offset: 2px; } .palette-sample { width: 36px; height: 28px; border-radius: 7px; border: 1px solid #0002; } .palette-auto { background: linear-gradient(135deg, #fff 50%, #e5e7eb 50%); } +.palette-none { background: linear-gradient(135deg, transparent 46%, #8b95a1 46%, #8b95a1 54%, transparent 54%), #e7e7e7; border-style: dashed; } +.preview-tile.bare { background: transparent; border: 1px dashed #c3c9d0; } .tile-palette small { color: #52616e; } /* Tile settings sheet and in-preview removal. */ diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index be1619b..e196143 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.14" +version: "0.2.15" slug: esp_screen_manager description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_tile_palette.cpp b/tests/test_tile_palette.cpp index 10e18d2..3540a41 100644 --- a/tests/test_tile_palette.cpp +++ b/tests/test_tile_palette.cpp @@ -6,4 +6,8 @@ int main() { assert(tile_palette::color("unknown-future-choice")==0); assert(tile_palette::color("red")!=0); assert(tile_palette::color("green")!=tile_palette::color("red")); + // "Geen" is not a colour: the card is hidden, and old firmware falls back to automatic. + assert(tile_palette::color("none")==0); + assert(tile_palette::transparent("none")); + assert(!tile_palette::transparent("auto") && !tile_palette::transparent("red") && !tile_palette::transparent("")); } diff --git a/tests/test_tile_palette.py b/tests/test_tile_palette.py index d47a00a..31e7840 100644 --- a/tests/test_tile_palette.py +++ b/tests/test_tile_palette.py @@ -34,6 +34,17 @@ class PaletteTests(unittest.IsolatedAsyncioTestCase): fresh.save('text.screen',{'title':'Nieuw','tiles':[{'entity':'light.a','options':{'background':'auto'}}]}) self.assertEqual(fresh.layouts['text.screen']['tiles'][0]['options']['background'],'auto') + def test_none_hides_the_card_and_needs_firmware_0216(self): + from core import min_firmware + header=(Path(__file__).resolve().parents[1]/'components/smart_display/tile_palette.h').read_text() + self.assertIn('name=="none"',header) + self.assertIsNone(TILE_BACKGROUNDS['none']['color']) + for entity in ('screen.clock','light.a'): + layout=validate_layout({'title':'Thuis','tiles':[{'entity':entity,'options':{'background':'none'}}]}) + self.assertEqual(layout['tiles'][0]['options']['background'],'none') + self.assertEqual(min_firmware(layout),(0,2,16)) + self.assertEqual(min_firmware({'tiles':[{'entity':'light.a','options':{'background':'red'}}]}),None) + def test_unapproved_colors_rejected(self): for value in ('#000000','url(test)','unknown',None,{},42): with self.assertRaises(ValueError):validate_layout({'title':'Thuis','tiles':[{'entity':'light.a','options':{'background':value}}]})