Calendar analog clock with ticks and numerals; tiles without background
Firmware 0.2.16: the analog clock uses index strokes with 12/3/6/9 numerals on large cards and shows a calendar block (weekday, big day, short month) beside the dial on single tiles; wide cards keep the digital time and date. New palette key 'none' hides only the card fill and border: geometry, padding and the pressed flash stay identical. Skeleton frames and the render self-test honour the transparency. App 0.2.15: 'Geen' in the background palette with a dashed mockup tile, min_firmware 0.2.16 for such layouts, demo layout with the bare calendar clock (--wide for the double-width clock). README and AGENTS note that every push is a release and bumps the add-on version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Widgets *>(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<w.parts.size();++i){
|
||||
auto *p=w.parts[i];if(!p)continue;
|
||||
bool muted=w.extra_mode=="forecast" ? 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<model.count){
|
||||
bool bare=model.tiles[w.index].transparent;
|
||||
bool opa_ok=(lv_obj_get_style_bg_opa(w.tile,LV_PART_MAIN)==LV_OPA_TRANSP)==bare && (lv_obj_get_style_border_opa(w.tile,LV_PART_MAIN)==LV_OPA_TRANSP)==bare;
|
||||
if(!opa_ok)ESP_LOGE("ui_test","Tile background FAIL slot=%u transparent=%d",(unsigned)w.index,bare);
|
||||
fits=fits && opa_ok;
|
||||
}
|
||||
ok=ok && fits;
|
||||
}
|
||||
return ok;
|
||||
@@ -1078,6 +1131,10 @@ inline void skeleton_page(int page) {
|
||||
auto &w=widgets[slot];if(!w.tile || lv_obj_has_flag(w.tile,LV_OBJ_FLAG_HIDDEN))continue;
|
||||
for(auto *o:{w.title,w.value,w.circle,w.unit,w.slider,w.extra})if(o)lv_obj_add_flag(o,LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_width(w.progress,0);
|
||||
// A card-less tile shows no skeleton frame either.
|
||||
bool bare=w.index<model.count && model.tiles[w.index].transparent;
|
||||
lv_obj_set_style_bg_opa(w.tile,bare?LV_OPA_TRANSP:LV_OPA_COVER,0);
|
||||
lv_obj_set_style_border_opa(w.tile,bare?LV_OPA_TRANSP:LV_OPA_COVER,0);
|
||||
lv_obj_set_style_bg_color(w.tile,lv_color_hex(bg),0);
|
||||
lv_obj_set_style_border_color(w.tile,lv_color_hex(border),0);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,6 @@ inline uint32_t color(const std::string &name) {
|
||||
if(name=="gray")return 0xE5E7EB;
|
||||
return 0; // Automatic/default, including unknown future palette names.
|
||||
}
|
||||
// "Geen": no card behind the tile contents. Older firmware treats it as automatic.
|
||||
inline bool transparent(const std::string &name) { return name=="none"; }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from core import extras, packets, state_message, validate_layout # noqa: E402
|
||||
|
||||
def demo_layout():
|
||||
return validate_layout({'title': 'Demo kaarten', 'tiles': [
|
||||
{'entity': 'screen.clock', 'name': '', 'options': {'display': 'analog', 'size': 'wide'}},
|
||||
{'entity': 'screen.clock', 'name': '', 'options': {'display': 'analog', 'background': 'none'}},
|
||||
{'entity': 'weather.demo', 'name': 'Buiten', 'options': {'display': 'forecast'}},
|
||||
{'entity': 'sensor.demo_temperatuur', 'name': 'Woonkamer', 'options': {'display': 'graph'}},
|
||||
{'entity': 'person.demo', 'name': 'Max', 'options': {}},
|
||||
@@ -47,11 +47,13 @@ def demo_forecast(now):
|
||||
return [{'datetime': (now + timedelta(days=i)).isoformat(), 'condition': conditions[i], 'temperature': 21 - i, 'templow': 11 + i}
|
||||
for i in range(6)]
|
||||
|
||||
def messages(inbox, rotate=0, digital=False):
|
||||
def messages(inbox, rotate=0, digital=False, wide=False):
|
||||
now = datetime.now(timezone.utc)
|
||||
layout, states = demo_layout(), demo_states(now)
|
||||
if digital:
|
||||
layout['tiles'][0]['options'] = {'display': 'digital'}
|
||||
elif wide:
|
||||
layout['tiles'][0]['options'] = {'display': 'analog', 'size': 'wide'}
|
||||
layout['tiles'] = layout['tiles'][rotate:] + layout['tiles'][:rotate]
|
||||
out = [{'v': 1, 'op': 'layout', 'inbox': inbox, 'title': layout['title'], 'entities': [t['entity'] for t in layout['tiles']]}]
|
||||
for i, tile in enumerate(layout['tiles']):
|
||||
@@ -68,7 +70,8 @@ async def main():
|
||||
p.add_argument('--name', required=True, help='Verwachte DEVICE_NAME')
|
||||
p.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml')
|
||||
p.add_argument('--rotate', type=int, default=0, help='Zet tegel N vooraan, zodat latere pagina\'s op pagina 1 komen')
|
||||
p.add_argument('--digital', action='store_true', help='Digitale klok op een enkele tegel in plaats van analoog dubbelbreed')
|
||||
p.add_argument('--digital', action='store_true', help='Digitale klok op een enkele tegel in plaats van de analoge kalenderkaart zonder achtergrond')
|
||||
p.add_argument('--wide', action='store_true', help='Analoge klok dubbelbreed (wijzerplaat met digitale tijd en datum)')
|
||||
args = p.parse_args()
|
||||
client = APIClient(args.host, 6053, noise_psk=yaml.safe_load(args.secrets.read_text())['api_encryption_key'],
|
||||
client_info='Demo layout', expected_name=args.name)
|
||||
@@ -83,7 +86,7 @@ async def main():
|
||||
replies.append(update.state)
|
||||
client.subscribe_states(state)
|
||||
started = time.monotonic()
|
||||
for message in messages(inbox.object_id if hasattr(inbox, 'object_id') else 'text.inbox', args.rotate % 9, args.digital):
|
||||
for message in messages(inbox.object_id if hasattr(inbox, 'object_id') else 'text.inbox', args.rotate % 9, args.digital, args.wide):
|
||||
for packet in packets(message):
|
||||
client.text_command(inbox.key, packet)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
+2
-1
@@ -103,7 +103,8 @@ klikgedrag, een mini-slider, een grote waarde, een grafiek (sensoren), een
|
||||
weersvoorspelling (weer) of de breedte **Dubbelbreed**. De ingebouwde **Klok**
|
||||
staat bovenaan de kiezer; zon, kookwekkers en personen vind je via de filters. Kies bij
|
||||
**Pastel achtergrond** een eigen kleur met donkere tekst; **Standaard** herstelt
|
||||
de normale weergave. Hiervoor is firmware 0.2.10+ nodig. Een lege
|
||||
de normale weergave en **Geen** laat de kaart weg, zodat de inhoud even groot
|
||||
direct op de schermachtergrond staat (firmware 0.2.16+). Hiervoor is firmware 0.2.10+ nodig. Een lege
|
||||
tegel brengt je naar de zoekbalk; toevoegen vult de eerstvolgende vrije positie.
|
||||
Het voorbeeld toont de indeling, geen live sensorwaarden.
|
||||
Klik **Opslaan & naar scherm** om je wijzigingen door te sturen.
|
||||
|
||||
@@ -128,6 +128,18 @@ Een oude apprelease van vóór 0.2.10 kent dit optieveld niet. Bij terugrollen v
|
||||
de app herstel je daarom de bijbehorende gegevensback-up; wis of negeer geen
|
||||
onbekende opties om een oud opslagbestand toch te openen.
|
||||
|
||||
### Compatibiliteit app 0.2.15 / firmware 0.2.16
|
||||
|
||||
Opslagversie en tegelprotocol blijven 1. `tiles[].options.background` krijgt de
|
||||
extra paletnaam `none` ("Geen"): geen kaart achter de tegelinhoud. Oudere
|
||||
firmware kent de naam niet en valt terug op de normale kaart; omdat dat voor de
|
||||
gebruiker onzichtbaar misgaat, stuurt de manager zo'n indeling pas na firmware
|
||||
0.2.16 (`min_firmware`) en bewaart haar ondertussen. De firmware verbergt
|
||||
uitsluitend vulling en rand (`bg_opa`/`border_opa`); maten, padding en de
|
||||
pressed-feedback op de PRESSED-state blijven gelijk. De analoge klok wijzigt
|
||||
alleen de tekening (streepjes, cijfers, kalenderblok op enkele tegels); er is
|
||||
geen nieuw veld voor nodig. Geen gewijzigde preferences of sleutels.
|
||||
|
||||
### Compatibiliteit 0.2.12 / firmware 0.2.14
|
||||
|
||||
Opslagversie en tegelprotocol blijven 1. Additief: `tiles[].options.size`
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# App 0.2.15 / firmware 0.2.16 — kalenderklok en tegels zonder achtergrond
|
||||
|
||||
## Wijzigingen
|
||||
|
||||
- **Analoge klok**: index-streepjes (`lv_line`, parts 0–11) met de cijfers 12,
|
||||
3, 6 en 9 als labels op de vier hoofdposities van grote kaarten; de CYD
|
||||
houdt twaalf streepjes. Op een enkele tegel staat naast de wijzerplaat een
|
||||
kalenderblok: weekdag (tegelsubfont), grote dag (`clock_digits`) met de
|
||||
korte maand (`label`) op dezelfde basislijn — LVGL's `base_line` wordt
|
||||
daarvoor gebruikt. De CYD toont "13 sep" in het grote-waardefont. De
|
||||
dubbelbrede kaart houdt de digitale tijd en datum naast de wijzerplaat.
|
||||
Nieuwe extra-modus `calendar` (enkel) naast `analog` (dubbelbreed), zodat
|
||||
de parts bij een breedtewissel opnieuw worden opgebouwd.
|
||||
- **Achtergrond: Geen** (`background: none`): de firmware zet `bg_opa` en
|
||||
`border_opa` van de tegel op transparant; padding, maten en positie blijven
|
||||
identiek aan een kaart, zoals de eigenaar vroeg ("niet groter maken zonder
|
||||
achtergrond"). De pressed-flits blijft werken (PRESSED-state). Het skeleton
|
||||
bij een paginawissel toont voor zo'n tegel geen kader. `check_tile_geometry`
|
||||
controleert nu ook of de opaciteit klopt met de tegeloptie.
|
||||
- **App**: paletsleutel `none` ("Geen") met een doorgestreept staal; de mockup
|
||||
toont zo'n tegel als stippellijn; `min_firmware` eist 0.2.16 voor indelingen
|
||||
met deze optie. Demo-indeling (`diagnostics/send_layout.py`) gebruikt nu de
|
||||
enkele analoge klok zonder achtergrond; `--wide` geeft de dubbelbrede klok.
|
||||
|
||||
## Tests
|
||||
|
||||
- 54 Python-tests (met nieuwe paletnaam-test) en negen C++-testprogramma's
|
||||
geslaagd; `tools/generate_packages.py --check` verifieert beide pakketten.
|
||||
- Guition-build geslaagd (RAM 31,0 %, flash 17,4 %); CYD-build geslaagd
|
||||
(RAM 30,0 %, flash 73,0 %). Eerste build faalde op `std::max(int, long)`
|
||||
rond `lv_font_get_line_height`; opgelost met expliciete int-casts.
|
||||
- Poorten vooraf vastgesteld met `esptool chip_id`: `usbserial-210` = ESP32-S3
|
||||
(Guition), `usbserial-130` = ESP32 (CYD). Beide via USB geflasht; het
|
||||
uploadlog toont `firmware.bin` uit de juiste buildmap.
|
||||
- Guition, demo-indeling: acht `page_check=PASS count=9` (kalenderklok zonder
|
||||
achtergrond, forecast, grafieken, zonnebaan), daarna één FAIL op het moment
|
||||
dat de manager de echte indeling terugzette (slot 5 wisselde van zonnebaan
|
||||
naar een gewone tegel binnen dezelfde frame; count ging naar 5). Tweede run
|
||||
op de echte indeling: 10/10 PASS (`count=5`, heap ~6,46 MB). Snapshot
|
||||
`diagnostics/guition-0216-demo-p1.png` toont de echte indeling met de
|
||||
kalenderklok (streepjes, cijfers, "zondag 13 sep").
|
||||
- CYD, demo-indeling: 10/10 PASS (zes met `count=9`, daarna `count=7` na
|
||||
herstel van de echte indeling; ~40–53 kB vrij heap).
|
||||
- De eigenaar bekeek de Guition na de flash: "ziet er top uit". De CYD is nog
|
||||
niet visueel beoordeeld; geen langdurige stabiliteitsproef uitgevoerd.
|
||||
@@ -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.15"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.16"
|
||||
DYNAMIC_TILES: "false"
|
||||
DASHBOARD_INBOX_INTERNAL: "true"
|
||||
DEVICE_NAME: "guition-wallbox"
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.15"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.16"
|
||||
DYNAMIC_TILES: "false"
|
||||
DASHBOARD_INBOX_INTERNAL: "true"
|
||||
DEVICE_NAME: "cyd-new"
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@
|
||||
############################################################
|
||||
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.15"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.16"
|
||||
DYNAMIC_TILES: "true"
|
||||
DASHBOARD_INBOX_INTERNAL: "false"
|
||||
DEVICE_NAME: "cyd-new"
|
||||
|
||||
@@ -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.15"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.16"
|
||||
DYNAMIC_TILES: "true"
|
||||
DASHBOARD_INBOX_INTERNAL: "false"
|
||||
DEVICE_NAME: "guition-new"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
## 0.2.15 (firmware 0.2.16)
|
||||
|
||||
- **Achtergrond: Geen** als extra keuze in het tegelpalet: de kaart en de rand vallen weg en de tegelinhoud staat direct op de schermachtergrond, in dezelfde maat en op dezelfde plek als mét kaart. Werkt voor elke tegel; de mockup toont zo'n tegel met een stippellijn. Vereist schermfirmware 0.2.16; de app bewaart de indeling en meldt het als het scherm ouder is.
|
||||
- **Analoge klok**: streepjes als index met de cijfers 12, 3, 6 en 9 (de CYD houdt alleen streepjes). Op een enkele tegel staat naast de wijzerplaat een kalenderblok — weekdag, grote dag, maand (CYD: "13 sep"). Dubbelbreed blijft de digitale tijd met datum naast de wijzerplaat.
|
||||
|
||||
## 0.2.14 (firmware 0.2.15 blijft actueel)
|
||||
|
||||
- Tegelinstellingen openen in een paneel bóven de schermmockup (op mobiel een sheet onderaan): naam, weergave, breedte, tikgedrag, slider, geschiedenis en kleur als knoppen, direct zichtbaar in de mockup erachter. Geen springende pagina meer.
|
||||
|
||||
@@ -18,6 +18,8 @@ ATTRS = frozenset('brightness percentage current_position current_temperature te
|
||||
|
||||
TILE_BACKGROUNDS = {
|
||||
'auto': {'label': 'Standaard', 'color': None},
|
||||
# No card behind the tile: contents keep their size and place on the screen background.
|
||||
'none': {'label': 'Geen', 'color': None},
|
||||
'red': {'label': 'Rood', 'color': '#FADADD'},
|
||||
'orange': {'label': 'Oranje', 'color': '#FFE1C6'},
|
||||
'yellow': {'label': 'Geel', 'color': '#FFF0C2'},
|
||||
@@ -76,6 +78,8 @@ def entity_id(value):
|
||||
|
||||
def min_firmware(layout):
|
||||
"""Oldest firmware that still accepts this layout; None when any version works."""
|
||||
if any(t.get('options', {}).get('background') == 'none' for t in layout['tiles']):
|
||||
return (0, 2, 16)
|
||||
if any(t['entity'].split('.')[0] in NEW_DOMAINS for t in layout['tiles']):
|
||||
return (0, 2, 14)
|
||||
if len(layout['tiles']) > 10:
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
|
||||
@@ -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}}]})
|
||||
|
||||
Reference in New Issue
Block a user