First commit

This commit is contained in:
iosullivan committed 2026-07-19 16:49:55 +08:00
commit 61c37de884
171 files changed
+9201

No files matched your search

+2932
View File
File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
# Modular screens (beta)
Goal: pick which carousel screens are compiled in, straight from the YAML - comment a
line in `packages:` and the screen is gone from the firmware (and the flash it used).
This is a **beta** layout on the `beta` branch. It needs to be compiled on a device
(there is no local validation here). Until it is verified, `main` keeps the single-file
config.
## How you choose screens
You keep one thin file locally (`guition-va.yaml`, repo root) that pulls the core and the
screens from GitHub as a remote package. Pick screens in its `files:` list:
```yaml
packages:
core:
url: https://github.com/MichalZaniewicz/esphome-guition-jc3636k718c-va
ref: beta
files:
- base/core.yaml # always on (clock + controls + settings)
- base/screens/timer.yaml
- base/screens/cool-cars.yaml
- base/screens/space-wars.yaml # comment out to drop Space Wars
refresh: 0s
```
Remove/comment a `base/screens/*.yaml` line -> that screen's page, scripts, globals and
game tick are not compiled, and its carousel slot disappears. Nothing else to edit.
## How it stays decoupled (the contract)
ESPHome merges package lists (globals/scripts/interval/lvgl.pages/esphome.on_boot all
concatenate), so each screen file is self-contained and the core never names a screen's
symbols. The glue is a few plain globals in the core:
- `g_base` (int) - id of the current carousel screen. Fixed ids: 0=clock, 1=player,
2=timer, 3=cars, 4=space (an id never changes, even if a screen is absent).
- `g_order[12]`, `g_order_n` - carousel order, built at boot. Core seeds clock+player;
each screen package appends its id in an `esphome.on_boot` step (priority sets the
position). Swipe left/right just steps through `g_order` and wraps.
- `g_nav_req` (bool) + `g_nav_anim` (int: 0=right 1=left 2=top 3=bottom) - core sets
these on a screen change; each screen package owns a small handler that, when
`g_base == <my id>`, runs its own `lvgl.page.show` with the matching animation and
clears `g_nav_req`. Core only `page.show`s its own (clock/player).
- `g_knob_capture` (bool) + `g_knob_delta` (int) - when a screen wants the knob (game
running, timer idle), it sets `g_knob_capture=true`; the core knob handler then just
accumulates +/-1 into `g_knob_delta` instead of changing volume. The screen reads and
zeroes `g_knob_delta` in its own tick. Core never names a game variable.
- Per-screen **settings registry** (`g_wgrp_*` + `g_wopt_*`) - a screen declares its own
Settings -> Widgets group and options in `on_boot`: append a group name to
`g_wgrp_labels`, then append each option (label + kind 0=toggle / 1=action + a `bool*`
to its own global) to `g_wopt_*`. The core renders them generically (modes 30 options /
31 confirm). The option globals live in the screen package, not the core.
- `g_factory_reset` (bool, persists a reboot) - Settings -> System -> Factory reset sets
it; each game package clears its own score globals when it sees the flag in its tick.
So a screen package contributes: its `globals`, its `script`s, its game-tick `interval`,
its `lvgl.pages` entry, an `on_boot` step to register its carousel id (and any Settings
group/options), and one nav handler. It reads/writes only the shared core globals above.
## Status
- [x] core refactor: data-driven carousel (`g_order`), nav via `g_nav_req`, knob via
`g_knob_capture`/`g_knob_delta`.
- [x] extract Space Wars -> `base/screens/space-wars.yaml`
- [x] extract Cool Cars -> `base/screens/cool-cars.yaml`
- [x] extract Player -> `base/screens/player.yaml` (volume + overlay stay in core)
- [x] timer carousel-screen toggle -> `base/screens/timer.yaml` (voice timers/alarm/badge stay in core)
- [x] new Weather screen -> `base/screens/weather.yaml` (radial 7-day dial, knob highlights the day;
forecast from the HA helper `base/screens/weather.ha-helper.yaml`)
- [x] new Thermostat screen -> `base/screens/thermostat.yaml` (climate.* dial; knob sets the target with a
debounced commit, tap toggles on/off, accent colour by heating/cooling/idle)
- [x] settings ownership: home-screen widget toggles moved to a "Home" submenu (core); "Display"
keeps only global brightness/night/screen-off
- [x] carousel order via the `screen_order` substitution; screens register `g_present`,
absent ones are skipped (clock kept as a safety fallback)
- [x] self-contained screen settings: each screen declares its Widgets group + options
(toggles/actions) and owns the backing globals; the core renders them generically
(`g_wgrp_*` / `g_wopt_*`, modes 30/31). Game scores + reset live in the game packages.
- [x] verified on device (compiles; screens toggle from `files:`, order from `screen_order`,
settings list adapts)
- [x] merged to `main` (stable); `beta` is the active dev branch
## Adding a new screen (recipe)
A screen package adds: its `globals` / `script`s / game-tick `interval` / `lvgl.pages`
entry; an `esphome.on_boot` step that sets `id(g_present)[<id>] = true`; a 50ms nav
handler that shows its page when `g_nav_req && g_base == <id>`. To give it a carousel
slot, add its name to the core `name -> id` map in the order builder and to `screen_order`.
To add Settings, register a Widgets group + options in `on_boot` (see `demo.yaml` /
`weather.yaml` as templates): claim `int g = id(g_wgrp_n)++;`, append the group name to
`id(g_wgrp_labels)`, then for each option append to `g_wopt_*` (group `g`, a label, kind
0=toggle / 1=action, and `&id(your_global)`). Declare those globals in your own file.
+565
View File
@@ -0,0 +1,565 @@
# Cool Cars - optional carousel screen (base id 3). Lane racer.
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
# Scores (g_top / g_best) + the "Reset scores" settings action live here - fully self-contained.
esphome:
on_boot:
- priority: 670
then:
- lambda: 'id(g_present)[3] = true;' # register after clock/player/timer
# Settings (Settings -> Widgets -> Cool Cars -> Reset scores) + factory-reset handling.
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Cool Cars";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_cars_reset);
globals:
# --- scores (persistent) + reset flag, owned by this package ---
- id: g_best
type: int
restore_value: yes
initial_value: '0'
- id: g_top
type: int[10]
restore_value: yes
- id: g_cars_reset # Widgets "Reset scores" sets this; the watcher below clears the scores
type: bool
restore_value: no
initial_value: 'false'
# --- "Car" game (page_game screen, g_base==3) ---
- id: g_game_state # 0=idle 1=playing 2=game over
type: int
restore_value: no
initial_value: '0'
- id: g_car_lane # car lane 0/1/2
type: int
restore_value: no
initial_value: '1'
- id: g_score
type: int
restore_value: no
initial_value: '0'
- id: g_gspeed # obstacle speed (px/tick)
type: int
restore_value: no
initial_value: '7'
- id: g_lives # lives (3) - game v2
type: int
restore_value: no
initial_value: '3'
- id: g_inv # invulnerability frames after a hit (anti double-hit)
type: int
restore_value: no
initial_value: '0'
- id: g_o1_type # car obstacle: 0=cop 1=muscle 2=sedan
type: int
restore_value: no
initial_value: '0'
- id: g_o1_lane
type: int
restore_value: no
initial_value: '0'
- id: g_o1_x # X position (horizontal; they move left)
type: int
restore_value: no
initial_value: '200'
- id: g_o2_type # obstacle: 0=cop 1=muscle 2=sedan 3=cone (all = crash)
type: int
restore_value: no
initial_value: '0'
- id: g_o2_lane
type: int
restore_value: no
initial_value: '2'
- id: g_o2_x
type: int
restore_value: no
initial_value: '320'
- id: g_o3_type # 3rd obstacle (more = harder)
type: int
restore_value: no
initial_value: '1'
- id: g_o3_lane
type: int
restore_value: no
initial_value: '1'
- id: g_o3_x
type: int
restore_value: no
initial_value: '460'
- id: g_cn_lane # coin
type: int
restore_value: no
initial_value: '1'
- id: g_cn_x
type: int
restore_value: no
initial_value: '440'
- id: g_cn_on
type: bool
restore_value: no
initial_value: 'true'
- id: g_dash_off # offset of the dashed lane lines (0..359)
type: int
restore_value: no
initial_value: '0'
script:
# --- "Car" game ---
- id: car_render # car lane (Y) + score + lives (hearts) + message per state
then:
- lvgl.widget.update: { id: car, y: !lambda 'return (id(g_car_lane) - 1) * 55;' }
- lvgl.label.update:
id: lbl_game_score
text: !lambda 'char b[10]; snprintf(b, sizeof(b), "%d", id(g_score)); return std::string(b);'
- lvgl.label.update:
id: lbl_lives
text: !lambda |-
std::string h; for (int i = 0; i < id(g_lives); i++) h += "󰋑"; return h;
# Shows the correct game screen per state (0=menu 1=playing 2=game over 3=how-to 4=scores)
- id: game_screen
then:
# knob steers only during play + game over -> capture it there (block volume), free it in menu/how-to/scores
- lambda: 'id(g_knob_capture) = (id(g_game_state) == 1 || id(g_game_state) == 2);'
# 1) hide everything
- lvgl.widget.hide: road_bg
- lvgl.widget.hide: logo_bg
- lvgl.widget.hide: car
- lvgl.widget.hide: obs1
- lvgl.widget.hide: obs2
- lvgl.widget.hide: obs3
- lvgl.widget.hide: coin
- lvgl.widget.hide: dl0
- lvgl.widget.hide: dl1
- lvgl.widget.hide: db0
- lvgl.widget.hide: db1
- lvgl.widget.hide: lbl_lives
- lvgl.widget.hide: lbl_game_score
- lvgl.widget.hide: hit_flash
- lvgl.widget.hide: btn_start
- lvgl.widget.hide: btn_howto
- lvgl.widget.hide: btn_scores
- lvgl.widget.hide: lbl_over
- lvgl.widget.hide: lbl_rec
- lvgl.widget.hide: btn_replay
- lvgl.widget.hide: btn_exit
- lvgl.widget.hide: lbl_howto_t
- lvgl.widget.hide: lbl_howto1
- lvgl.widget.hide: lbl_howto2
- lvgl.widget.hide: lbl_howto_b
- lvgl.widget.hide: img_howcoin
- lvgl.widget.hide: lbl_howto_l
- lvgl.widget.hide: lbl_howheart
- lvgl.widget.hide: badge1
- lvgl.widget.hide: badge2
- lvgl.widget.hide: badge3
- lvgl.widget.hide: badge4
- lvgl.widget.hide: lbl_scores_t
- lvgl.widget.hide: lbl_scores1
- lvgl.widget.hide: lbl_scores2
- lvgl.widget.hide: btn_back
# 2) show per state
- if: # MENU
condition: { lambda: 'return id(g_game_state) == 0;' }
then:
- lvgl.widget.show: logo_bg
- lvgl.widget.show: btn_start
- lvgl.widget.show: btn_howto
- lvgl.widget.show: btn_scores
- if: # GAME
condition: { lambda: 'return id(g_game_state) == 1;' }
then:
- lvgl.widget.show: road_bg
- lvgl.widget.show: car
- lvgl.widget.show: obs1
- lvgl.widget.show: obs2
- lvgl.widget.show: obs3
- lvgl.widget.show: coin
- lvgl.widget.show: dl0
- lvgl.widget.show: dl1
- lvgl.widget.show: db0
- lvgl.widget.show: db1
- lvgl.widget.show: lbl_lives
- lvgl.widget.show: lbl_game_score
- if: # GAME OVER
condition: { lambda: 'return id(g_game_state) == 2;' }
then:
- lvgl.widget.show: logo_bg
- lvgl.widget.show: lbl_over
- lvgl.widget.show: lbl_rec
- lvgl.widget.show: btn_replay
- lvgl.widget.show: btn_exit
- lvgl.label.update: { id: lbl_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_best)); return std::string(b);' }
- if: # HOW TO PLAY
condition: { lambda: 'return id(g_game_state) == 3;' }
then:
- lvgl.widget.show: lbl_howto_t
- lvgl.widget.show: lbl_howto1
- lvgl.widget.show: lbl_howto2
- lvgl.widget.show: lbl_howto_b
- lvgl.widget.show: img_howcoin
- lvgl.widget.show: lbl_howto_l
- lvgl.widget.show: lbl_howheart
- lvgl.widget.show: badge1
- lvgl.widget.show: badge2
- lvgl.widget.show: badge3
- lvgl.widget.show: badge4
- lvgl.widget.show: btn_back
- if: # SCORES (top 10)
condition: { lambda: 'return id(g_game_state) == 4;' }
then:
- lvgl.widget.show: lbl_scores_t
- lvgl.widget.show: lbl_scores1
- lvgl.widget.show: lbl_scores2
- lvgl.widget.show: btn_back
- lvgl.label.update:
id: lbl_scores1
text: !lambda |-
std::string s; char b[24];
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
- lvgl.label.update:
id: lbl_scores2
text: !lambda |-
std::string s; char b[24];
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
- id: score_render # score with "$" (refreshed on change, not every tick)
then:
- lvgl.label.update:
id: lbl_game_score
text: !lambda 'char b[10]; snprintf(b, sizeof(b), "%d", id(g_score)); return std::string(b);'
- id: obs1_spawn # obstacle: 0=cop 1=muscle 2=sedan 3=cone
then:
- lambda: |-
id(g_o1_type) = esp_random() % 4;
id(g_o1_lane) = esp_random() % 3;
id(g_o1_x) = 200 + (int)(esp_random() % 120);
- if: { condition: { lambda: 'return id(g_o1_type)==0;' }, then: [lvgl.image.update: { id: obs1, src: img_cop }] }
- if: { condition: { lambda: 'return id(g_o1_type)==1;' }, then: [lvgl.image.update: { id: obs1, src: img_mus }] }
- if: { condition: { lambda: 'return id(g_o1_type)==2;' }, then: [lvgl.image.update: { id: obs1, src: img_sed }] }
- if: { condition: { lambda: 'return id(g_o1_type)==3;' }, then: [lvgl.image.update: { id: obs1, src: img_cone }] }
- lvgl.widget.update: { id: obs1, x: !lambda 'return id(g_o1_x);', y: !lambda 'return (id(g_o1_lane)-1)*55;' }
- id: obs2_spawn
then:
- lambda: |-
id(g_o2_type) = esp_random() % 4;
id(g_o2_lane) = esp_random() % 3;
id(g_o2_x) = 260 + (int)(esp_random() % 140);
- if: { condition: { lambda: 'return id(g_o2_type)==0;' }, then: [lvgl.image.update: { id: obs2, src: img_cop }] }
- if: { condition: { lambda: 'return id(g_o2_type)==1;' }, then: [lvgl.image.update: { id: obs2, src: img_mus }] }
- if: { condition: { lambda: 'return id(g_o2_type)==2;' }, then: [lvgl.image.update: { id: obs2, src: img_sed }] }
- if: { condition: { lambda: 'return id(g_o2_type)==3;' }, then: [lvgl.image.update: { id: obs2, src: img_cone }] }
- lvgl.widget.update: { id: obs2, x: !lambda 'return id(g_o2_x);', y: !lambda 'return (id(g_o2_lane)-1)*55;' }
- id: obs3_spawn
then:
- lambda: |-
id(g_o3_type) = esp_random() % 4;
id(g_o3_lane) = esp_random() % 3;
id(g_o3_x) = 340 + (int)(esp_random() % 160);
- if: { condition: { lambda: 'return id(g_o3_type)==0;' }, then: [lvgl.image.update: { id: obs3, src: img_cop }] }
- if: { condition: { lambda: 'return id(g_o3_type)==1;' }, then: [lvgl.image.update: { id: obs3, src: img_mus }] }
- if: { condition: { lambda: 'return id(g_o3_type)==2;' }, then: [lvgl.image.update: { id: obs3, src: img_sed }] }
- if: { condition: { lambda: 'return id(g_o3_type)==3;' }, then: [lvgl.image.update: { id: obs3, src: img_cone }] }
- lvgl.widget.update: { id: obs3, x: !lambda 'return id(g_o3_x);', y: !lambda 'return (id(g_o3_lane)-1)*55;' }
- id: coin_spawn
then:
- lambda: |-
id(g_cn_lane) = esp_random() % 3;
id(g_cn_x) = 220 + (int)(esp_random() % 200);
id(g_cn_on) = true;
- lvgl.widget.show: coin
- lvgl.widget.update: { id: coin, x: !lambda 'return id(g_cn_x);', y: !lambda 'return (id(g_cn_lane)-1)*55;' }
- id: game_start
then:
- lambda: |-
id(g_game_state) = 1; id(g_score) = 0; id(g_gspeed) = 7;
id(g_car_lane) = 1; id(g_lives) = 3; id(g_inv) = 0;
id(g_knob_capture) = true; id(g_knob_delta) = 0;
- lvgl.widget.update: { id: car, x: -120 }
- script.execute: obs1_spawn
- script.execute: obs2_spawn
- script.execute: obs3_spawn
- script.execute: coin_spawn
- script.execute: score_render
- script.execute: game_screen
- script.execute: car_render
- id: lose_life # losing a life: red flash + invulnerability (blinking), 0 lives -> game over
then:
- lambda: 'id(g_lives)--; id(g_inv) = 22;' # ~1.1s invulnerability (scaled for 50ms)
- lvgl.widget.show: hit_flash
- if:
condition: { lambda: 'return id(g_lives) <= 0;' }
then: [ script.execute: game_crash ]
else: [ script.execute: car_render ]
- id: game_crash # GAME OVER (0 lives) - save score + insert into top 10
then:
- lambda: |-
id(g_game_state) = 2; // game_screen keeps the knob captured on game over (no volume)
int s = id(g_score);
if (s > id(g_best)) id(g_best) = s;
for (int i = 0; i < 10; i++) {
if (s > id(g_top)[i]) {
for (int j = 9; j > i; j--) id(g_top)[j] = id(g_top)[j-1];
id(g_top)[i] = s;
break;
}
}
- script.execute: game_screen
interval:
# Reset scores when the Widgets action fired (g_cars_reset). Idempotent + cheap.
- interval: 250ms
then:
- if:
condition: { lambda: 'return id(g_cars_reset) || id(g_factory_reset);' }
then:
- lambda: 'for (int i = 0; i < 10; i++) id(g_top)[i] = 0; id(g_best) = 0; id(g_cars_reset) = false;'
# nav: show this screen when it is selected in the carousel
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==3;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_game_state) = 0;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_game, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_game, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_game, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_game, animation: move_bottom, time: 250ms }] }
- script.execute: game_screen
# "Car" game (driving right) - tick only on the game screen during play.
# 50ms (20fps) so motion is smooth; speeds are scaled down to keep the same px/sec pace.
- interval: 50ms
then:
- if:
condition:
lambda: 'return id(g_base) == 3 && id(g_game_state) == 1;'
then:
- if:
condition: { lambda: 'return id(g_knob_capture) && id(g_knob_delta) != 0;' }
then:
- lambda: 'id(g_car_lane) += id(g_knob_delta); if (id(g_car_lane) < 0) id(g_car_lane) = 0; if (id(g_car_lane) > 2) id(g_car_lane) = 2; id(g_knob_delta) = 0;'
- script.execute: car_render
- lambda: |-
id(g_o1_x) -= id(g_gspeed);
id(g_o2_x) -= id(g_gspeed);
id(g_o3_x) -= id(g_gspeed);
id(g_cn_x) -= id(g_gspeed);
if (id(g_inv) > 0) id(g_inv)--;
int sp = 7 + id(g_score) / 11; if (sp > 16) sp = 16; id(g_gspeed) = sp; // ramps up (scaled for 50ms)
id(g_dash_off) = (id(g_dash_off) + id(g_gspeed)) % 360;
# Recycle obstacles past the left edge (+ a point for dodging, score updated only on change)
- if:
condition: { lambda: 'return id(g_o1_x) < -200;' }
then: [ lambda: 'id(g_score)++;', script.execute: obs1_spawn, script.execute: score_render ]
- if:
condition: { lambda: 'return id(g_o2_x) < -200;' }
then: [ lambda: 'id(g_score)++;', script.execute: obs2_spawn, script.execute: score_render ]
- if:
condition: { lambda: 'return id(g_o3_x) < -200;' }
then: [ lambda: 'id(g_score)++;', script.execute: obs3_spawn, script.execute: score_render ]
- if:
condition: { lambda: 'return id(g_cn_x) < -200;' }
then: [ script.execute: coin_spawn ]
# Collisions (cars + cone = lose a life)
- if:
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o1_x) - (-120)) < 30 && id(g_o1_lane) == id(g_car_lane);' }
then: [ script.execute: lose_life ]
- if:
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o2_x) - (-120)) < 30 && id(g_o2_lane) == id(g_car_lane);' }
then: [ script.execute: lose_life ]
- if:
condition: { lambda: 'return id(g_inv) <= 0 && abs(id(g_o3_x) - (-120)) < 30 && id(g_o3_lane) == id(g_car_lane);' }
then: [ script.execute: lose_life ]
# Coin pickup
- if:
condition: { lambda: 'return id(g_cn_on) && abs(id(g_cn_x) - (-120)) < 28 && id(g_cn_lane) == id(g_car_lane);' }
then:
- lambda: 'id(g_score) += 5; id(g_cn_on) = false;'
- lvgl.widget.hide: coin
- script.execute: coin_spawn
- script.execute: score_render
# Hit effect: red flash fades after a moment, car blinks during invulnerability
- if:
condition: { lambda: 'return id(g_inv) > 0;' }
then:
- lvgl.widget.update: { id: car, hidden: !lambda 'return (id(g_inv) % 4) >= 2;' }
- lvgl.widget.update: { id: hit_flash, hidden: !lambda 'return id(g_inv) < 16;' }
# Move obstacles/coin/dashes in ONE lambda (lv_obj_align) instead of 8 widget.update
# actions - far less per-tick overhead (the pattern that keeps Snake/Knobuss smooth).
- lambda: |-
lv_obj_align(id(obs1), LV_ALIGN_CENTER, id(g_o1_x), (id(g_o1_lane) - 1) * 55);
lv_obj_align(id(obs2), LV_ALIGN_CENTER, id(g_o2_x), (id(g_o2_lane) - 1) * 55);
lv_obj_align(id(obs3), LV_ALIGN_CENTER, id(g_o3_x), (id(g_o3_lane) - 1) * 55);
lv_obj_align(id(coin), LV_ALIGN_CENTER, id(g_cn_x), (id(g_cn_lane) - 1) * 55);
int d0 = ((0 - id(g_dash_off)) % 360 + 360) % 360 - 180;
int d1 = ((180 - id(g_dash_off)) % 360 + 360) % 360 - 180;
lv_obj_align(id(dl0), LV_ALIGN_CENTER, d0, -28);
lv_obj_align(id(dl1), LV_ALIGN_CENTER, d1, -28);
lv_obj_align(id(db0), LV_ALIGN_CENTER, d0, 27);
lv_obj_align(id(db1), LV_ALIGN_CENTER, d1, 27);
image:
- id: img_road2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/road2.png"
type: RGB565
- id: img_p
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/pcar2.png"
type: RGB565
transparency: alpha_channel
- id: img_cop
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/ocop2.png"
type: RGB565
transparency: alpha_channel
- id: img_mus
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/omus2.png"
type: RGB565
transparency: alpha_channel
- id: img_sed
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/osed2.png"
type: RGB565
transparency: alpha_channel
- id: img_coin
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/coin.png"
type: RGB565
transparency: alpha_channel
- id: img_cone
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/cone.png"
type: RGB565
transparency: alpha_channel
- id: img_logo
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/cool-cars/logo.png"
type: RGB565
lvgl:
pages:
# --- "Car" GAME page (after the player): 3 lanes, car at the bottom, obstacles from the top ---
- id: page_game
bg_color: 0x0D1323
scrollbar_mode: "off"
scrollable: false
widgets:
- image: { id: road_bg, align: CENTER, x: 0, y: 0, src: img_road2, hidden: true } # road (only in-game)
# Dashed lane lines - moving (scrolled in the interval = sense of driving)
- obj: { id: dl0, align: CENTER, x: 0, y: -28, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
- obj: { id: dl1, align: CENTER, x: 0, y: -28, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
- obj: { id: db0, align: CENTER, x: 0, y: 27, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
- obj: { id: db1, align: CENTER, x: 0, y: 27, width: 26, height: 4, radius: 0, border_width: 0, bg_color: 0xECD25A, hidden: true }
- image: { id: coin, align: CENTER, x: 440, y: 0, src: img_coin, hidden: true }
- image: { id: obs1, align: CENTER, x: 200, y: -55, src: img_cop, hidden: true }
- image: { id: obs2, align: CENTER, x: 320, y: 55, src: img_mus, hidden: true }
- image: { id: obs3, align: CENTER, x: 460, y: 0, src: img_sed, hidden: true }
- image: { id: car, align: CENTER, x: -120, y: 0, src: img_p, hidden: true }
- label: { id: lbl_lives, align: CENTER, x: -70, y: -108, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
- label: { id: lbl_game_score, align: CENTER, x: 70, y: -108, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
- obj: { id: hit_flash, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, border_width: 0, bg_color: 0xFF2A2A, bg_opa: 45%, hidden: true }
# --- Logo background (menu) ---
- image: { id: logo_bg, align: CENTER, x: 0, y: 0, src: img_logo, hidden: true }
# --- MENU (state 0): Start / How to play? / Scores (under the logo) ---
- button:
id: btn_start
align: CENTER
y: -8
width: 150
height: 44
radius: 22
bg_color: 0x2FE6CF
bg_opa: COVER
border_width: 0
on_click: [ script.execute: game_start ]
widgets:
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x06201C }
- button:
id: btn_howto
align: CENTER
y: 42
width: 150
height: 44
radius: 22
bg_color: 0x2A2E55
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_game_state) = 3;', script.execute: game_screen ]
widgets:
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xFFFFFF }
- button:
id: btn_scores
align: CENTER
y: 92
width: 150
height: 44
radius: 22
bg_color: 0x2A2E55
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_game_state) = 4;', script.execute: game_screen ]
widgets:
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xFFFFFF }
# --- GAME OVER (state 2): score + Play + Exit ---
- label: { id: lbl_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xFFFFFF, hidden: true }
- label: { id: lbl_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x9AA6B2, hidden: true }
- button:
id: btn_replay
align: CENTER
x: -66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x2FE6CF
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ script.execute: game_start ]
widgets:
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x06201C }
- button:
id: btn_exit
align: CENTER
x: 66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_game_state) = 0;', script.execute: game_screen ]
widgets:
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
# --- HOW TO PLAY? (state 3) - navy background, logo accents ---
- label: { id: lbl_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xE94FB0, hidden: true }
- obj: { id: badge1, align: CENTER, x: -90, y: -46, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge2, align: CENTER, x: -90, y: -10, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge3, align: CENTER, x: -90, y: 26, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge4, align: CENTER, x: -90, y: 62, width: 24, height: 24, radius: 12, bg_color: 0xE94FB0, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
- label: { id: lbl_howto1, align_to: { id: badge1, align: OUT_RIGHT_MID, x: 8 }, text: "Use the knob", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_howto2, align_to: { id: badge2, align: OUT_RIGHT_MID, x: 8 }, text: "Dodge obstacles", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_howto_b, align_to: { id: badge3, align: OUT_RIGHT_MID, x: 8 }, text: "Collect bonuses", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- image: { id: img_howcoin, align_to: { id: lbl_howto_b, align: OUT_RIGHT_MID, x: 6 }, src: img_coin, hidden: true }
- label: { id: lbl_howto_l, align_to: { id: badge4, align: OUT_RIGHT_MID, x: 8 }, text: "You have 3 lives", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_howheart, align_to: { id: lbl_howto_l, align: OUT_RIGHT_MID, x: 6 }, text: "󰋑󰋑󰋑", text_font: icons18, text_color: 0xE24B4A, hidden: true }
# --- SCORES (state 4): top 10 in two columns ---
- label: { id: lbl_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xE94FB0, hidden: true }
- label: { id: lbl_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
# --- Back (state 3/4 -> menu) ---
- button:
id: btn_back
align: CENTER
y: 120
width: 130
height: 42
radius: 21
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_game_state) = 0;', script.execute: game_screen ]
widgets:
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
+147
View File
@@ -0,0 +1,147 @@
# ============================================================================
# DEMO / EXAMPLE SCREEN (base id 6, carousel name "demo")
# ----------------------------------------------------------------------------
# A minimal, heavily-commented screen that shows how a screen package is built:
# * it registers itself in the carousel (g_present),
# * it shows its own LVGL page,
# * it reacts to a tap (toggles the background black <-> white, text inverts),
# * it declares its OWN settings toggles (Settings -> Widgets -> Demo):
# g_demo_border -> draw a border
# g_demo_led -> drive the LED ring (green on white bg, red on black bg)
# The globals live in THIS file and are registered into the core's generic settings
# registry (g_wgrp_* / g_wopt_*) in on_boot - see how below as the template.
# * it drives the LED ring directly (a simple demo - it bypasses the ring
# reaction-priority system in the core; that is fine for an example).
#
# Everything a screen needs lives in THIS file (globals/scripts/interval/lvgl page +
# its settings registration). Remove the screen by dropping this file from the package
# "files:" list - nothing else to edit, anywhere.
# ============================================================================
esphome:
on_boot:
# Register this screen so the carousel knows it exists. The core builds the
# carousel order from `screen_order`, including only screens that set g_present.
- priority: 655
then:
- lambda: 'id(g_present)[6] = true;'
# Register this screen's settings (Settings -> Widgets -> Demo) in the generic registry.
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Demo";
int o;
o = id(g_wopt_n)++; id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Border"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_demo_border);
o = id(g_wopt_n)++; id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Demo LED"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_demo_led);
globals:
- id: g_demo_bg # background: false = black, true = white (runtime state)
type: bool
restore_value: no
initial_value: 'false'
# Demo settings (declared here, registered above; the core menu reaches them by pointer).
- id: g_demo_border # draw a border (Settings -> Widgets -> Demo)
type: bool
restore_value: yes
initial_value: 'false'
- id: g_demo_led # drive the LED ring (Settings -> Widgets -> Demo)
type: bool
restore_value: yes
initial_value: 'false'
# Tap de-duplication: the core bumps g_tap_seq on every normal-mode tap; we
# remember the last value we handled so one tap = one toggle.
- id: g_demo_lasttap
type: int
restore_value: no
initial_value: '0'
# Tracks whether WE currently own the LED ring, so we can release it on leave.
# Persistent so that after a reboot (the ring component restores its last colour) the
# 80ms loop below sees "we owned it", and since we boot onto the home screen it turns
# the ring back off instead of leaving the demo colour stuck on.
- id: g_demo_ring_on
type: bool
restore_value: yes
initial_value: 'false'
script:
# Redraws the screen from the three state flags (g_demo_bg / border / led).
- id: demo_render
then:
# background + text colour (text is the inverse of the background)
- if:
condition: { lambda: 'return id(g_demo_bg);' } # white background
then:
- lvgl.widget.update: { id: demo_bg, bg_color: 0xFFFFFF, border_color: 0x000000 }
- lvgl.label.update: { id: demo_lbl, text_color: 0x000000 }
else: # black background
- lvgl.widget.update: { id: demo_bg, bg_color: 0x000000, border_color: 0xFFFFFF }
- lvgl.label.update: { id: demo_lbl, text_color: 0xFFFFFF }
# border on/off (width is an int, so a lambda is fine here)
- lvgl.widget.update: { id: demo_bg, border_width: !lambda 'return id(g_demo_border) ? 6 : 0;' }
# LED ring: green when the background is white, red when it is black
- if:
condition: { lambda: 'return id(g_demo_led);' }
then:
- if:
condition: { lambda: 'return id(g_demo_bg);' }
then: [ light.turn_on: { id: ring_light, red: 0%, green: 100%, blue: 0%, effect: "None" } ]
else: [ light.turn_on: { id: ring_light, red: 100%, green: 0%, blue: 0%, effect: "None" } ]
- lambda: 'id(g_demo_ring_on) = true;'
else:
# LED toggle is off: release the ring if we were driving it
- if:
condition: { lambda: 'return id(g_demo_ring_on);' }
then:
- light.turn_off: ring_light
- lambda: 'id(g_demo_ring_on) = false;'
interval:
# Carousel navigation: when this screen becomes selected, show its page.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==6;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_demo_lasttap) = id(g_tap_seq);' # sync tap counter so arriving never counts as a tap
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_demo, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_demo, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_demo, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_demo, animation: move_bottom, time: 250ms }] }
- script.execute: demo_render
# React to a tap while this screen is on top; release the LED when we leave.
- interval: 80ms
then:
- if:
condition: { lambda: 'return id(g_base)==6 && id(g_tap_seq) != id(g_demo_lasttap);' }
then:
- lambda: 'id(g_demo_lasttap) = id(g_tap_seq); id(g_demo_bg) = !id(g_demo_bg);' # one tap = flip background
- script.execute: demo_render
- if:
condition: { lambda: 'return id(g_base) != 6 && id(g_demo_ring_on);' }
then:
- lambda: 'id(g_demo_ring_on) = false;'
lvgl:
pages:
- id: page_demo
bg_color: 0x000000
widgets:
# A full circle that fills the round screen - this is the background we recolour.
# radius 180 on a 360x360 box makes it a circle, so the optional border is a clean ring.
- obj:
id: demo_bg
align: CENTER
width: 360
height: 360
radius: 180
bg_color: 0x000000
bg_opa: COVER
border_width: 0
border_color: 0xFFFFFF
- label:
id: demo_lbl
align: CENTER
text: "TAP ME!"
text_font: montserrat_34
text_color: 0xFFFFFF
+711
View File
@@ -0,0 +1,711 @@
# Knobuss - optional carousel screen (base id 10). A Gyruss-style tube shooter: your ship orbits the rim, you
# auto-fire inward, foes spiral out from the centre. Turn the knob to aim, blast them before they
# reach you. Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
# Scores (g_gy_top / g_gy_best) + the "Reset scores" settings action live here - fully self-contained.
#
# Perf: ship + bullet pool (6) + enemy pool (10) are all positioned each tick from float arrays in
# ONE lambda (lv_obj_set_pos over a pooled lv_obj_t* array - LVGL can't index a widget by a runtime
# var), so per-tick cost is constant and the action chain stays shallow. Sprites have a fixed src
# (enemies alternate enemy/enemy2 by slot), so there are no per-tick image swaps.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_present)[10] = true;'
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Knobuss";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_gy_reset);
globals:
- id: g_gy_best
type: int
restore_value: yes
initial_value: '0'
- id: g_gy_top
type: int[10]
restore_value: yes
- id: g_gy_reset
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
type: int
restore_value: no
initial_value: '0'
- id: g_gy_score
type: int
restore_value: no
initial_value: '0'
- id: g_gy_lives
type: int
restore_value: no
initial_value: '3'
- id: g_gy_ship_ang # ship orbit angle (radians)
type: float
restore_value: no
initial_value: '0.0'
- id: g_gy_dead
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_fire_clk
type: int
restore_value: no
initial_value: '0'
- id: g_gy_spawn_clk
type: int
restore_value: no
initial_value: '0'
- id: g_gy_hud_dirty # score/lives changed this tick -> refresh the HUD labels
type: bool
restore_value: no
initial_value: 'false'
# bullet pool (fly inward along a fixed radial)
- id: g_gy_br # radius (px from centre)
type: float[6]
restore_value: no
- id: g_gy_ba # angle (radians)
type: float[6]
restore_value: no
- id: g_gy_bon
type: bool[6]
restore_value: no
# enemy pool (spiral outward from the centre)
- id: g_gy_er
type: float[10]
restore_value: no
- id: g_gy_ea
type: float[10]
restore_value: no
- id: g_gy_eon
type: bool[10]
restore_value: no
# starfield (depth illusion): dots rush outward from the centre, faster as they near the rim
- id: g_gy_sr # radius (px from centre)
type: float[10]
restore_value: no
- id: g_gy_sa # angle (radians)
type: float[10]
restore_value: no
# explosion pool (expanding shockwave ring of dots) - fired when a foe reaches the rim
- id: g_gy_exx # centre X (relative to screen centre, px)
type: float[2]
restore_value: no
- id: g_gy_exy # centre Y
type: float[2]
restore_value: no
- id: g_gy_exage # -1 = inactive, else explosion frame 0..7
type: int[2]
restore_value: no
- id: g_gy_shipframe # current ship rotation frame (0..15), -1 forces a refresh
type: int
restore_value: no
initial_value: '-1'
- id: g_gy_shipdirty # ship rotation bucket changed this tick
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_exact # an explosion is active or expired this tick (refresh its sprite)
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_tick # frame counter (throttles the starfield + ship-sprite refresh)
type: int
restore_value: no
initial_value: '0'
script:
# position every active widget from the float arrays (constant cost, one shallow lambda)
- id: gy_render
then:
- lambda: |-
const float R = 148.0f;
// starfield (background depth) - small dots rushing outward
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
for (int k = 0; k < 6; k++) {
float r = id(g_gy_sr)[k], a = id(g_gy_sa)[k];
lv_obj_set_pos(SF[k], 180 + (int) lroundf(cosf(a) * r) - 1, 180 + (int) lroundf(sinf(a) * r) - 1);
}
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
for (int k = 0; k < 6; k++) {
if (id(g_gy_bon)[k]) {
float r = id(g_gy_br)[k], a = id(g_gy_ba)[k];
lv_obj_set_pos(BB[k], 180 + (int) lroundf(cosf(a) * r) - 6, 180 + (int) lroundf(sinf(a) * r) - 6);
} else lv_obj_set_pos(BB[k], 400, 400);
}
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
for (int k = 0; k < 10; k++) {
if (id(g_gy_eon)[k]) {
float r = id(g_gy_er)[k], a = id(g_gy_ea)[k];
lv_obj_set_pos(EE[k], 180 + (int) lroundf(cosf(a) * r) - 13, 180 + (int) lroundf(sinf(a) * r) - 13);
} else lv_obj_set_pos(EE[k], 400, 400);
}
float sa = id(g_gy_ship_ang);
lv_obj_set_pos(id(gy_ship), 180 + (int) lroundf(cosf(sa) * R) - 18, 180 + (int) lroundf(sinf(sa) * R) - 18);
// explosions (32px sprites) - positioned while alive, parked otherwise
lv_obj_t *XX[2] = { id(gy_x0), id(gy_x1) };
for (int s = 0; s < 2; s++) {
if (id(g_gy_exage)[s] >= 0) lv_obj_set_pos(XX[s], 180 + (int) lroundf(id(g_gy_exx)[s]) - 16, 180 + (int) lroundf(id(g_gy_exy)[s]) - 16);
else lv_obj_set_pos(XX[s], 400, 400);
}
# set the ship sprite to its current rotation frame (called only when the bucket changes)
- id: gy_shipspr
then:
- if: { condition: { lambda: 'return id(g_gy_shipframe)==0;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship0 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==1;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship1 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==2;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship2 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==3;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship3 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==4;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship4 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==5;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship5 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==6;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship6 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==7;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship7 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==8;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship8 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==9;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship9 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==10;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship10 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==11;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship11 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==12;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship12 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==13;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship13 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==14;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship14 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==15;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship15 } ] }
# advance/hide the explosion sprites by their frame (called only while a burst is live)
- id: gy_explspr
then:
- if:
condition: { lambda: 'return id(g_gy_exage)[0] < 0;' }
then: [ lvgl.widget.hide: gy_x0 ]
else:
- lvgl.widget.show: gy_x0
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==0;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl0 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==1;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl1 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==2;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl2 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==3;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl3 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==4;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl4 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==5;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl5 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==6;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl6 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]>=7;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl7 } ] }
- if:
condition: { lambda: 'return id(g_gy_exage)[1] < 0;' }
then: [ lvgl.widget.hide: gy_x1 ]
else:
- lvgl.widget.show: gy_x1
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==0;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl0 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==1;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl1 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==2;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl2 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==3;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl3 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==4;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl4 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==5;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl5 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==6;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl6 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]>=7;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl7 } ] }
- id: gy_hud
then:
- lvgl.label.update: { id: lbl_gy_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_gy_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_gy_lives, text: !lambda 'char b[16]; snprintf(b,sizeof(b),"LIVES %d",id(g_gy_lives)<0?0:id(g_gy_lives)); return std::string(b);' }
# park every gameplay widget off-screen
- id: gy_clear
then:
- lambda: |-
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
for (int k = 0; k < 6; k++) lv_obj_set_pos(BB[k], 400, 400);
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
for (int k = 0; k < 10; k++) lv_obj_set_pos(EE[k], 400, 400);
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
for (int k = 0; k < 6; k++) lv_obj_set_pos(SF[k], 400, 400);
lv_obj_set_pos(id(gy_ship), 400, 400);
- lvgl.widget.hide: gy_x0
- lvgl.widget.hide: gy_x1
- id: gy_start
then:
- lambda: |-
id(g_gy_state) = 1; id(g_gy_score) = 0; id(g_gy_lives) = 3; id(g_gy_dead) = false;
id(g_gy_ship_ang) = -1.5708f; // start at the top
id(g_gy_fire_clk) = 0; id(g_gy_spawn_clk) = 0;
for (int k = 0; k < 6; k++) id(g_gy_bon)[k] = false;
for (int k = 0; k < 10; k++) id(g_gy_eon)[k] = false;
for (int k = 0; k < 6; k++) { id(g_gy_sr)[k] = 6.0f + (esp_random() % 168); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
for (int s = 0; s < 2; s++) id(g_gy_exage)[s] = -1;
id(g_gy_shipframe) = -1; // force a ship-sprite refresh on the first tick
id(g_knob_capture) = true; id(g_knob_delta) = 0;
- script.execute: gy_screen
- script.execute: gy_hud
- id: gy_over
then:
- lambda: |-
id(g_gy_state) = 2;
int s = id(g_gy_score);
if (s > id(g_gy_best)) id(g_gy_best) = s;
for (int i = 0; i < 10; i++) {
if (s > id(g_gy_top)[i]) {
for (int j = 9; j > i; j--) id(g_gy_top)[j] = id(g_gy_top)[j-1];
id(g_gy_top)[i] = s;
break;
}
}
- script.execute: gy_screen
# screen state + the single knob-capture authority (captured only in play + game over)
- id: gy_screen
then:
- lambda: 'id(g_knob_capture) = (id(g_gy_state) == 1 || id(g_gy_state) == 2);'
- script.execute: gy_clear
- lvgl.widget.hide: gy_logo
- lvgl.widget.hide: gy_ring
- lvgl.widget.hide: gy_ring2
- lvgl.widget.hide: gy_ring3
- lvgl.widget.hide: gy_core
- lvgl.widget.hide: lbl_gy_score
- lvgl.widget.hide: lbl_gy_lives
- lvgl.widget.hide: lbl_gy_title
- lvgl.widget.hide: lbl_gy_sub
- lvgl.widget.hide: btn_gy_start
- lvgl.widget.hide: btn_gy_howto
- lvgl.widget.hide: btn_gy_scores
- lvgl.widget.hide: lbl_gy_over
- lvgl.widget.hide: lbl_gy_rec
- lvgl.widget.hide: btn_gy_replay
- lvgl.widget.hide: btn_gy_exit
- lvgl.widget.hide: lbl_gy_howto_t
- lvgl.widget.hide: gy_hb1
- lvgl.widget.hide: gy_hb2
- lvgl.widget.hide: gy_hb3
- lvgl.widget.hide: gy_hb4
- lvgl.widget.hide: lbl_gy_howto1
- lvgl.widget.hide: lbl_gy_howto2
- lvgl.widget.hide: lbl_gy_howto3
- lvgl.widget.hide: lbl_gy_howto4
- lvgl.widget.hide: gy_life_icon
- lvgl.widget.hide: lbl_gy_scores_t
- lvgl.widget.hide: lbl_gy_scores1
- lvgl.widget.hide: lbl_gy_scores2
- lvgl.widget.hide: btn_gy_back
- if: # MENU
condition: { lambda: 'return id(g_gy_state) == 0;' }
then:
- lvgl.widget.show: gy_logo
- lvgl.widget.show: btn_gy_start
- lvgl.widget.show: btn_gy_howto
- lvgl.widget.show: btn_gy_scores
- if: # PLAY
condition: { lambda: 'return id(g_gy_state) == 1;' }
then:
- lvgl.widget.show: gy_ring
- lvgl.widget.show: gy_ring2
- lvgl.widget.show: gy_ring3
- lvgl.widget.show: gy_core
- lvgl.widget.show: lbl_gy_score
- lvgl.widget.show: lbl_gy_lives
- script.execute: gy_render
- if: # GAME OVER
condition: { lambda: 'return id(g_gy_state) == 2;' }
then:
- lvgl.widget.show: gy_logo
- lvgl.widget.show: lbl_gy_over
- lvgl.widget.show: lbl_gy_rec
- lvgl.widget.show: btn_gy_replay
- lvgl.widget.show: btn_gy_exit
- lvgl.label.update: { id: lbl_gy_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_gy_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_gy_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_gy_best)); return std::string(b);' }
- if: # HOW TO PLAY
condition: { lambda: 'return id(g_gy_state) == 3;' }
then:
- lvgl.widget.show: lbl_gy_howto_t
- lvgl.widget.show: gy_hb1
- lvgl.widget.show: gy_hb2
- lvgl.widget.show: gy_hb3
- lvgl.widget.show: gy_hb4
- lvgl.widget.show: lbl_gy_howto1
- lvgl.widget.show: lbl_gy_howto2
- lvgl.widget.show: lbl_gy_howto3
- lvgl.widget.show: lbl_gy_howto4
- lvgl.widget.show: gy_life_icon
- lvgl.widget.show: btn_gy_back
- if: # SCORES (top 10)
condition: { lambda: 'return id(g_gy_state) == 4;' }
then:
- lvgl.widget.show: lbl_gy_scores_t
- lvgl.widget.show: lbl_gy_scores1
- lvgl.widget.show: lbl_gy_scores2
- lvgl.widget.show: btn_gy_back
- lvgl.label.update:
id: lbl_gy_scores1
text: !lambda |-
std::string s; char b[24];
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
- lvgl.label.update:
id: lbl_gy_scores2
text: !lambda |-
std::string s; char b[24];
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
interval:
- interval: 250ms
then:
- if:
condition: { lambda: 'return id(g_gy_reset) || id(g_factory_reset);' }
then:
- lambda: 'for (int i = 0; i < 10; i++) id(g_gy_top)[i] = 0; id(g_gy_best) = 0; id(g_gy_reset) = false;'
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==10;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_gy_state) = 0; id(g_knob_delta) = 0;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_bottom, time: 250ms }] }
- script.execute: gy_screen
# Game tick: aim (knob) + auto-fire + spiral the foes + collisions; all positioning in gy_render.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_base) == 10 && id(g_gy_state) == 1;' }
then:
- lambda: |-
id(g_gy_hud_dirty) = false; id(g_gy_shipdirty) = false;
id(g_gy_tick)++;
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
id(g_gy_ship_ang) += kd * 0.30f; // aim sensitivity: ~17 deg per knob detent
// auto-fire a bullet down the ship's radial
if (++id(g_gy_fire_clk) >= 5) {
id(g_gy_fire_clk) = 0;
for (int k = 0; k < 6; k++) if (!id(g_gy_bon)[k]) { id(g_gy_bon)[k] = true; id(g_gy_br)[k] = 138.0f; id(g_gy_ba)[k] = id(g_gy_ship_ang); break; }
}
for (int k = 0; k < 6; k++) if (id(g_gy_bon)[k]) { id(g_gy_br)[k] -= 9.0f; if (id(g_gy_br)[k] < 12.0f) id(g_gy_bon)[k] = false; }
// spawn foes from the core (faster as the score climbs)
int sw = 24 - id(g_gy_score) / 200; if (sw < 9) sw = 9;
if (++id(g_gy_spawn_clk) >= sw) {
id(g_gy_spawn_clk) = 0;
for (int k = 0; k < 10; k++) if (!id(g_gy_eon)[k]) {
float a = (esp_random() % 62832) / 10000.0f;
id(g_gy_eon)[k] = true; id(g_gy_er)[k] = 32.0f; id(g_gy_ea)[k] = a; break; // start at the core's rim (not under it) to avoid central overdraw
}
}
// age existing explosions first (8-frame sprite); refresh sprite while any is live/expiring
id(g_gy_exact) = false;
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] >= 0) {
id(g_gy_exact) = true;
if (++id(g_gy_exage)[s] > 7) id(g_gy_exage)[s] = -1;
}
// foes spiral outward; ANY foe that reaches the rim costs a life and explodes there
float esp = 1.5f + id(g_gy_score) / 1500.0f; if (esp > 3.2f) esp = 3.2f;
for (int k = 0; k < 10; k++) if (id(g_gy_eon)[k]) {
id(g_gy_er)[k] += esp; id(g_gy_ea)[k] += 0.05f;
if (id(g_gy_er)[k] >= 146.0f) {
id(g_gy_eon)[k] = false;
id(g_gy_lives)--; id(g_gy_hud_dirty) = true; if (id(g_gy_lives) <= 0) id(g_gy_dead) = true;
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] < 0) { id(g_gy_exx)[s] = cosf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exy)[s] = sinf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exage)[s] = 0; id(g_gy_exact) = true; break; }
}
}
// depth starfield (throttled to every 2nd tick): rush outward, respawn near the centre
if ((id(g_gy_tick) & 1) == 0) for (int k = 0; k < 6; k++) {
id(g_gy_sr)[k] += id(g_gy_sr)[k] * 0.10f + 2.0f;
if (id(g_gy_sr)[k] > 176.0f) { id(g_gy_sr)[k] = 4.0f + (esp_random() % 14); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
}
// ship rotation frame (every 2nd tick, on the ODD ticks so it doesn't share a tick with
// the starfield move above - spreads the two throttled costs; position stays 50ms)
if ((id(g_gy_tick) & 1) == 1) {
int bk = (int) lroundf(id(g_gy_ship_ang) / 0.3926991f); bk %= 16; if (bk < 0) bk += 16;
id(g_gy_shipdirty) = (bk != id(g_gy_shipframe));
if (id(g_gy_shipdirty)) id(g_gy_shipframe) = bk;
}
// bullet vs foe (polar proximity)
for (int b = 0; b < 6; b++) if (id(g_gy_bon)[b]) for (int e = 0; e < 10; e++) if (id(g_gy_eon)[e]) {
float dr = id(g_gy_br)[b] - id(g_gy_er)[e]; if (dr < 0) dr = -dr;
if (dr < 13.0f) {
float da = id(g_gy_ba)[b] - id(g_gy_ea)[e]; da = atan2f(sinf(da), cosf(da));
if (fabsf(da) < 0.28f) { id(g_gy_bon)[b] = false; id(g_gy_eon)[e] = false; id(g_gy_score) += 10; id(g_gy_hud_dirty) = true; break; }
}
}
- if: { condition: { lambda: 'return id(g_gy_shipdirty);' }, then: [ script.execute: gy_shipspr ] }
- if: { condition: { lambda: 'return id(g_gy_exact);' }, then: [ script.execute: gy_explspr ] }
- script.execute: gy_render
- if:
condition: { lambda: 'return id(g_gy_dead);' }
then:
- script.execute: gy_over
else:
- if:
condition: { lambda: 'return id(g_gy_hud_dirty);' }
then:
- script.execute: gy_hud
# Sprites are pulled from the repo /main/ raw URLs at compile time.
# Third-party art (see assets/sprites/knobuss/CREDITS.txt).
image:
- id: img_gy_ship0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship4.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship5.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship6.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship7.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship8
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship8.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship9
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship9.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship10
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship10.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship11
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship11.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship12
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship12.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship13
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship13.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship14
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship14.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship15
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship15.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl4.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl5.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl6.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl7.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_bullet
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/bullet.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_core
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/core.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_logo
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/knobuss-logo.png"
type: RGB565
- id: img_gy_life
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/life.png"
type: RGB565
transparency: alpha_channel
lvgl:
pages:
- id: page_gyruss
bg_color: 0x070611
scrollbar_mode: "off"
scrollable: false
widgets:
# Knobuss title screen (full-screen menu backdrop, behind the buttons) - hidden in play
- image: { id: gy_logo, align: CENTER, x: 0, y: 0, src: img_gy_logo, hidden: true }
# starfield (declared first = drawn behind everything) - parked until play
- obj: { id: gy_s0, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s1, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s2, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s3, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s4, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s5, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
# concentric guide rings (static) suggest a tube receding to the core - free depth cue
- obj: { id: gy_ring3, align: CENTER, x: 0, y: 0, width: 112, height: 112, radius: 56, bg_opa: TRANSP, border_width: 1, border_color: 0x1A2742, hidden: true }
- obj: { id: gy_ring2, align: CENTER, x: 0, y: 0, width: 200, height: 200, radius: 100, bg_opa: TRANSP, border_width: 1, border_color: 0x1E2C4A, hidden: true }
- obj: { id: gy_ring, align: CENTER, x: 0, y: 0, width: 300, height: 300, radius: 150, bg_opa: TRANSP, border_width: 2, border_color: 0x2A3C66, hidden: true }
- image: { id: gy_core, align: CENTER, x: 0, y: 0, src: img_gy_core, hidden: true }
# foes: 4 glowing-orb colours cycled across the 10 slots (fixed src - no runtime swap)
- image: { id: gy_e0, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e1, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_e2, x: 400, y: 400, src: img_gy_enemy2 }
- image: { id: gy_e3, x: 400, y: 400, src: img_gy_enemy3 }
- image: { id: gy_e4, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e5, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_e6, x: 400, y: 400, src: img_gy_enemy2 }
- image: { id: gy_e7, x: 400, y: 400, src: img_gy_enemy3 }
- image: { id: gy_e8, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e9, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_b0, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b1, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b2, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b3, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b4, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b5, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_ship, x: 400, y: 400, src: img_gy_ship0 }
# explosion sprites (8-frame), parked + hidden until a foe hits the rim
- image: { id: gy_x0, x: 400, y: 400, src: img_gy_expl0, hidden: true }
- image: { id: gy_x1, x: 400, y: 400, src: img_gy_expl0, hidden: true }
- label: { id: lbl_gy_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: 0xCFE4FF, hidden: true }
- label: { id: lbl_gy_lives, align: CENTER, x: 0, y: 150, text: "LIVES 3", text_font: font_artist, text_color: 0xFF7A6A, hidden: true }
- label: { id: lbl_gy_title, align: CENTER, x: 0, y: -86, text: "KNOBUSS", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- label: { id: lbl_gy_sub, align: CENTER, x: 0, y: -52, text: "tube shooter", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
- button:
id: btn_gy_start
align: CENTER
y: -8
width: 150
height: 44
radius: 22
bg_color: 0x39C6E6
bg_opa: COVER
border_width: 0
on_click: [ script.execute: gy_start ]
widgets:
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x06131A }
- button:
id: btn_gy_howto
align: CENTER
y: 40
width: 150
height: 44
radius: 22
bg_color: 0x1C2A44
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_gy_state) = 3;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xC4D2F0 }
- button:
id: btn_gy_scores
align: CENTER
y: 88
width: 150
height: 44
radius: 22
bg_color: 0x1C2A44
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_gy_state) = 4;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xC4D2F0 }
- label: { id: lbl_gy_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xCFE4FF, hidden: true }
- label: { id: lbl_gy_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
- button:
id: btn_gy_replay
align: CENTER
x: -66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x39C6E6
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ script.execute: gy_start ]
widgets:
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x06131A }
- button:
id: btn_gy_exit
align: CENTER
x: 66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x2A2E3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
- label: { id: lbl_gy_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- obj: { id: gy_hb1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
- label: { id: lbl_gy_howto1, align_to: { id: gy_hb1, align: OUT_RIGHT_MID, x: 8 }, text: "Turn knob to aim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto2, align_to: { id: gy_hb2, align: OUT_RIGHT_MID, x: 8 }, text: "You auto-fire inward", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto3, align_to: { id: gy_hb3, align: OUT_RIGHT_MID, x: 8 }, text: "Blast foes before the rim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto4, align_to: { id: gy_hb4, align: OUT_RIGHT_MID, x: 8 }, text: "You have three lives", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- image: { id: gy_life_icon, align_to: { id: lbl_gy_howto4, align: OUT_RIGHT_MID, x: 8 }, src: img_gy_life, hidden: true }
- label: { id: lbl_gy_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- label: { id: lbl_gy_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
- button:
id: btn_gy_back
align: CENTER
y: 120
width: 130
height: 42
radius: 21
bg_color: 0x2A2E3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
+265
View File
@@ -0,0 +1,265 @@
# Player - optional carousel screen (base id 1). Full-screen album art + transport.
# Remove it: drop this file from your config's package "files:" list.
# Volume control + the volume overlay stay in the core; this is only the now-playing SCREEN.
esphome:
on_boot:
- priority: 690
then:
- lambda: 'id(g_present)[1] = true;' # register after clock
globals:
- id: g_last_pic
type: std::string
restore_value: no
- id: g_pos # position in the track (s) - progress bar
type: int
restore_value: no
initial_value: '0'
- id: g_dur # track length (s)
type: int
restore_value: no
initial_value: '0'
# Required by online_image (fetching album art over HTTP)
http_request:
timeout: 10s
verify_ssl: false
# Album art fetched dynamically from HA (entity_picture is a signed URL)
online_image:
- id: album_art
url: "${ha_base_url}/"
format: JPEG
type: RGB565
resize: 360x360
# Album art FIX: after download you must EXPLICITLY refresh the LVGL widget (it won't redraw itself)
on_download_finished:
- lvgl.image.update:
id: ui_album
src: album_art
on_error:
- logger.log: "Failed to fetch album art"
sensor:
- platform: homeassistant
id: media_pos
entity_id: ${player_entity}
attribute: media_position
on_value:
- lambda: 'id(g_pos) = (int) x;'
- platform: homeassistant
id: media_dur
entity_id: ${player_entity}
attribute: media_duration
on_value:
- lambda: 'id(g_dur) = (int) x;'
text_sensor:
- platform: homeassistant
id: media_title
entity_id: ${player_entity}
attribute: media_title
on_value:
- lvgl.label.update:
id: lbl_title
text: !lambda 'return x.empty() ? std::string("-") : x;'
- platform: homeassistant
id: media_artist
entity_id: ${player_entity}
attribute: media_artist
on_value:
- lvgl.label.update:
id: lbl_artist
text: !lambda 'return x;'
- platform: homeassistant
id: media_pic
entity_id: ${player_entity}
attribute: entity_picture
on_value:
- lambda: |-
if (x.empty()) return;
std::string url = x.rfind("http", 0) == 0 ? x : std::string("${ha_base_url}") + x;
if (url == id(g_last_pic)) return; // same album art -> don't reload (less flicker)
id(g_last_pic) = url;
id(album_art)->set_url(url.c_str());
id(album_art)->update();
# Player state -> only "playing" shows the player; pause/stop = player disappears (clock)
- platform: homeassistant
id: media_state
entity_id: ${player_entity}
on_value:
- lvgl.label.update: # icon on the button: pause when playing, play when not
id: lbl_play
text: !lambda 'return id(media_state).state == "playing" ? std::string("") : std::string("");'
- if:
condition:
lambda: 'return !id(g_va_active) && !id(g_boot) && id(g_mode) == 0 && (id(g_base) == 0 || id(g_base) == 1);' # auto-switch clock<->player only when we're on the clock/player (and not during boot splash)
then:
# Only switch on an ACTUAL transition: home -> player when playback starts,
# player -> home when it stops. (Avoids re-navigating on every media-state event,
# incl. the state HA pushes at boot, which used to flip the home to Classic.)
- if:
condition: { lambda: 'return x == "playing" && id(g_base) == 0;' }
then:
- lvgl.page.show: { id: page_player, animation: move_left, time: 250ms }
- lambda: 'id(g_base) = 1;'
- if:
condition: { lambda: 'return x != "playing" && id(g_base) == 1;' }
then:
# back to the home base, respecting the active watchface (Classic shows
# page_home directly; other faces self-show via g_wf_req) - don't hardcode Classic
- lambda: 'id(g_base) = 0; id(g_nav_anim) = 1;'
- if:
condition: { lambda: 'return id(g_watchface) == 0;' }
then:
- lvgl.page.show: { id: page_home, animation: move_right, time: 250ms }
else:
- lambda: 'id(g_wf_req) = true;'
interval:
# nav: show this screen when it is selected in the carousel
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==1;' }
then:
- lambda: 'id(g_nav_req) = false;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_player, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_player, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_player, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_player, animation: move_bottom, time: 250ms }] }
# track progress (position / time / progress bar)
- interval: 1s
then:
# Track progress: tick the position while playing + update time and bar
- if:
condition:
lambda: 'return id(media_state).state == "playing" && id(g_dur) > 0 && id(g_pos) < id(g_dur);'
then:
- lambda: 'id(g_pos)++;'
- lvgl.label.update:
id: lbl_time
text: !lambda |-
int p = id(g_pos), d = id(g_dur);
if (d <= 0) return std::string("");
char b[20]; snprintf(b, sizeof(b), "%d:%02d | %d:%02d", p/60, p%60, d/60, d%60);
return std::string(b);
- lvgl.bar.update:
id: prog_bar
value: !lambda 'return id(g_dur) > 0 ? (int)(1000L * id(g_pos) / id(g_dur)) : 0;'
lvgl:
pages:
# --- Player page: full-screen album art + scrim + title/artist + volume arc ---
- id: page_player
bg_color: 0x000000
widgets:
# Album art covering the whole round screen (360x360; the bezel hides the corners)
- image:
id: ui_album
align: CENTER
src: album_art
# Dim the whole thing under text and buttons (readability)
- obj:
align: CENTER
width: 360
height: 360
radius: 0
border_width: 0
bg_color: 0x000000
bg_opa: 42%
# Title (scroll) + artist (clip) - in the upper part
- label:
id: lbl_title
align: CENTER
y: -72
width: 250
long_mode: scroll_circular
text_align: CENTER
text_font: font_med
text_color: 0xFFFFFF
text: "-"
- label:
id: lbl_artist
align: CENTER
y: -46
width: 230
long_mode: clip
text_align: CENTER
text_font: font_artist
text_color: 0x9FC7BE
text: ""
# Transport: prev / play-pause / next (touch buttons)
- button:
id: btn_prev
align: CENTER
x: -84
y: 6
width: 50
height: 50
radius: 25
bg_color: 0x223230
bg_opa: COVER
border_width: 0
on_click:
- homeassistant.action:
action: media_player.media_previous_track
data: { entity_id: "${player_entity}" }
widgets:
- label: { align: CENTER, text: "", text_font: montserrat_22, text_color: 0xCFEAE4 }
- button:
id: btn_play
align: CENTER
x: 0
y: 6
width: 74
height: 74
radius: 37
bg_color: 0x2FE6CF
bg_opa: COVER
border_width: 0
on_click:
- homeassistant.action:
action: media_player.media_play_pause
data: { entity_id: "${player_entity}" }
widgets:
- label: { id: lbl_play, align: CENTER, text: "", text_font: montserrat_28, text_color: 0x06201C }
- button:
id: btn_next
align: CENTER
x: 84
y: 6
width: 50
height: 50
radius: 25
bg_color: 0x223230
bg_opa: COVER
border_width: 0
on_click:
- homeassistant.action:
action: media_player.media_next_track
data: { entity_id: "${player_entity}" }
widgets:
- label: { align: CENTER, text: "", text_font: montserrat_22, text_color: 0xCFEAE4 }
# Time + progress bar
- label:
id: lbl_time
align: CENTER
y: 64
text: ""
text_font: font_artist
text_color: 0xCFEAE4
- bar:
id: prog_bar
align: CENTER
y: 90
width: 210
height: 5
min_value: 0
max_value: 1000
value: 0
border_width: 0
indicator:
bg_color: 0x2FE6CF
+244
View File
@@ -0,0 +1,244 @@
# Sensors / glance - optional carousel screen (base id 8).
# Shows one configurable Home Assistant sensor at a time, big and readable; turn the KNOB to
# cycle between them. Pulls straight from HA (no helper). Works for 1 sensor (no cycling) up to
# 6 (knob scrolls, a "n/total" indicator appears). Any entity works - the state is shown as text
# and the unit comes from the entity's unit_of_measurement attribute.
#
# Configure slots with the glanceN_entity / glanceN_name / glanceN_icon substitutions. Leave a
# slot at its default (a non-existent entity) and it's skipped automatically. Icon names use the
# same set as the control tiles; leave empty for no icon.
#
# Remove it: drop this file from the package "files:" list.
esphome:
on_boot:
- priority: 655
then:
- lambda: |-
id(g_present)[8] = true; // register the sensors screen
// resolve icon names -> glyphs (same set as the control tiles); "" -> no icon
auto ic = [](const std::string &s) -> std::string {
if (s == "bulb" || s == "light") return "\U000f0335";
if (s == "lamp") return "\U000f06b5";
if (s == "led" || s == "strip") return "\U000f07d6";
if (s == "power") return "\U000f0425";
if (s == "plug") return "\U000f06a5";
if (s == "switch") return "\U000f0521";
if (s == "fan") return "\U000f0210";
if (s == "ac" || s == "aircon") return "\U000f001b";
if (s == "heater" || s == "radiator") return "\U000f0438";
if (s == "thermometer" || s == "temp") return "\U000f050f";
if (s == "lock") return "\U000f033e";
if (s == "unlock") return "\U000f033f";
if (s == "door") return "\U000f081a";
if (s == "blinds") return "\U000f00ac";
if (s == "tv") return "\U000f0502";
if (s == "speaker") return "\U000f04c3";
if (s == "bell") return "\U000f009a";
if (s == "wifi") return "\U000f05a9";
if (s == "home") return "\U000f02dc";
if (s == "flash") return "\U000f0241";
if (s == "air" || s == "pm" || s == "airquality") return "\U000f0d44";
if (s == "plant" || s == "sprout" || s == "moisture") return "\U000f0e66";
if (s == "plex") return "\U000f06ba";
if (s == "server") return "\U000f048b";
if (s == "shield" || s == "alarm" || s == "security") return "\U000f068a";
return s;
};
id(g_gl_glyph)[0] = ic("${glance1_icon}"); id(g_gl_name)[0] = "${glance1_name}";
id(g_gl_glyph)[1] = ic("${glance2_icon}"); id(g_gl_name)[1] = "${glance2_name}";
id(g_gl_glyph)[2] = ic("${glance3_icon}"); id(g_gl_name)[2] = "${glance3_name}";
id(g_gl_glyph)[3] = ic("${glance4_icon}"); id(g_gl_name)[3] = "${glance4_name}";
id(g_gl_glyph)[4] = ic("${glance5_icon}"); id(g_gl_name)[4] = "${glance5_name}";
id(g_gl_glyph)[5] = ic("${glance6_icon}"); id(g_gl_name)[5] = "${glance6_name}";
globals:
- id: g_gl_val
type: std::string[6]
restore_value: no
- id: g_gl_unit
type: std::string[6]
restore_value: no
- id: g_gl_name
type: std::string[6]
restore_value: no
- id: g_gl_glyph
type: std::string[6]
restore_value: no
- id: g_gl_present
type: bool[6]
restore_value: no
- id: g_gl_sel
type: int
restore_value: no
initial_value: '0'
script:
- id: gl_render
then:
- lambda: |-
int first = -1;
for (int i = 0; i < 6; i++) if (id(g_gl_present)[i]) { first = i; break; }
if (first >= 0 && !id(g_gl_present)[id(g_gl_sel)]) id(g_gl_sel) = first;
- if:
condition: { lambda: 'int c=0; for(int i=0;i<6;i++) if(id(g_gl_present)[i]) c++; return c==0;' }
then:
- lvgl.widget.show: gl_empty
- lvgl.widget.hide: gl_icon
- lvgl.widget.hide: gl_value
- lvgl.widget.hide: gl_name
- lvgl.widget.hide: gl_pos
else:
- lvgl.widget.hide: gl_empty
- lvgl.widget.show: gl_value
- lvgl.widget.show: gl_name
- lvgl.label.update: { id: gl_icon, text: !lambda 'return id(g_gl_glyph)[id(g_gl_sel)];' }
- lvgl.widget.update: { id: gl_icon, hidden: !lambda 'return id(g_gl_glyph)[id(g_gl_sel)].empty();' }
- lvgl.label.update:
id: gl_value
text: !lambda |-
std::string v = id(g_gl_val)[id(g_gl_sel)];
if (v.empty() || v == "unknown" || v == "unavailable") return std::string("--");
std::string u = id(g_gl_unit)[id(g_gl_sel)];
if (!u.empty()) v += " " + u;
// replace symbols the value font lacks (micro sign, superscripts) so units don't tofu
auto rep = [&](const std::string &a, const std::string &b) {
size_t p; while ((p = v.find(a)) != std::string::npos) v.replace(p, a.size(), b);
};
rep("\xC2\xB5", "u"); // µ micro sign (U+00B5) -> u
rep("\xCE\xBC", "u"); // μ Greek mu (U+03BC, what HA often uses) -> u
rep("\xC2\xB2", "2"); // ² -> 2
rep("\xC2\xB3", "3"); // ³ -> 3
rep("\xC2\xB9", "1"); // ¹ -> 1
return v;
- lvgl.label.update: { id: gl_name, text: !lambda 'return id(g_gl_name)[id(g_gl_sel)];' }
- lvgl.widget.update: { id: gl_pos, hidden: !lambda 'int c=0; for(int i=0;i<6;i++) if(id(g_gl_present)[i]) c++; return c<=1;' }
- lvgl.label.update:
id: gl_pos
text: !lambda |-
std::string d; // slider dots: active = bigger (circle-medium), rest = circle-small
for (int i=0;i<6;i++) {
if (!id(g_gl_present)[i]) continue;
d += (i == id(g_gl_sel)) ? "\U000f09de" : "\U000f09df";
}
return d;
# per-sensor accent (by slot): tints the icon + dots so each glance has its identity
- if: { condition: { lambda: 'return id(g_gl_sel)==0;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x35C759 }, lvgl.label.update: { id: gl_pos, text_color: 0x35C759 } ] }
- if: { condition: { lambda: 'return id(g_gl_sel)==1;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x3FA9D6 }, lvgl.label.update: { id: gl_pos, text_color: 0x3FA9D6 } ] }
- if: { condition: { lambda: 'return id(g_gl_sel)==2;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xF5B83D }, lvgl.label.update: { id: gl_pos, text_color: 0xF5B83D } ] }
- if: { condition: { lambda: 'return id(g_gl_sel)==3;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xA66BFF }, lvgl.label.update: { id: gl_pos, text_color: 0xA66BFF } ] }
- if: { condition: { lambda: 'return id(g_gl_sel)==4;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0xFF6B60 }, lvgl.label.update: { id: gl_pos, text_color: 0xFF6B60 } ] }
- if: { condition: { lambda: 'return id(g_gl_sel)==5;' }, then: [ lvgl.label.update: { id: gl_icon, text_color: 0x2FE6CF }, lvgl.label.update: { id: gl_pos, text_color: 0x2FE6CF } ] }
text_sensor:
- platform: homeassistant
id: gl1_v
entity_id: ${glance1_entity}
on_value: [ lambda: 'id(g_gl_val)[0]=x; id(g_gl_present)[0]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl1_u
entity_id: ${glance1_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[0]=x;', script.execute: gl_render ]
- platform: homeassistant
id: gl2_v
entity_id: ${glance2_entity}
on_value: [ lambda: 'id(g_gl_val)[1]=x; id(g_gl_present)[1]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl2_u
entity_id: ${glance2_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[1]=x;', script.execute: gl_render ]
- platform: homeassistant
id: gl3_v
entity_id: ${glance3_entity}
on_value: [ lambda: 'id(g_gl_val)[2]=x; id(g_gl_present)[2]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl3_u
entity_id: ${glance3_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[2]=x;', script.execute: gl_render ]
- platform: homeassistant
id: gl4_v
entity_id: ${glance4_entity}
on_value: [ lambda: 'id(g_gl_val)[3]=x; id(g_gl_present)[3]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl4_u
entity_id: ${glance4_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[3]=x;', script.execute: gl_render ]
- platform: homeassistant
id: gl5_v
entity_id: ${glance5_entity}
on_value: [ lambda: 'id(g_gl_val)[4]=x; id(g_gl_present)[4]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl5_u
entity_id: ${glance5_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[4]=x;', script.execute: gl_render ]
- platform: homeassistant
id: gl6_v
entity_id: ${glance6_entity}
on_value: [ lambda: 'id(g_gl_val)[5]=x; id(g_gl_present)[5]=true;', script.execute: gl_render ]
- platform: homeassistant
id: gl6_u
entity_id: ${glance6_entity}
attribute: unit_of_measurement
on_value: [ lambda: 'id(g_gl_unit)[5]=x;', script.execute: gl_render ]
interval:
# Carousel nav: when selected, show the page and take the knob (only if >1 sensor).
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==8;' }
then:
- lambda: 'id(g_nav_req) = false;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_sensors, animation: move_bottom, time: 250ms }] }
- script.execute: gl_render
# Knob cycles between present sensors (capture only when there's more than one).
- interval: 120ms
then:
- if:
condition: { lambda: 'return id(g_base)==8;' }
then:
- lambda: |-
int c=0; for (int i=0;i<6;i++) if (id(g_gl_present)[i]) c++;
id(g_knob_capture) = (c > 1);
- if:
condition: { lambda: 'return id(g_knob_delta) != 0;' }
then:
- lambda: |-
int d = id(g_knob_delta); id(g_knob_delta) = 0;
int cnt=0; for (int i=0;i<6;i++) if (id(g_gl_present)[i]) cnt++;
if (cnt > 0) {
int step = (d > 0) ? 1 : -1; int n = (d > 0) ? d : -d;
for (int k=0;k<n;k++) { do { id(g_gl_sel) = (id(g_gl_sel)+step+6)%6; } while (!id(g_gl_present)[id(g_gl_sel)]); }
}
- script.execute: gl_render
lvgl:
pages:
- id: page_sensors
bg_color: 0x000000
widgets:
- obj: # faint outer ring for depth (matches the other screens)
id: gl_ring_out
align: CENTER
width: 340
height: 340
radius: 170
bg_opa: TRANSP
border_width: 2
border_color: 0x1A1F2A
border_opa: 70%
clickable: false
scrollable: false
- label: { id: gl_icon, align: CENTER, y: -70, text: "", text_font: icons34, text_color: 0xBFD2DA }
- label: { id: gl_value, align: CENTER, y: -6, width: 320, text_align: CENTER, long_mode: clip, text: "--", text_font: montserrat_34, text_color: 0xFFFFFF }
- label: { id: gl_name, align: CENTER, y: 40, width: 300, text_align: CENTER, long_mode: clip, text: "", text_font: font_med, text_color: 0x9FC7BE }
- label: { id: gl_pos, align: CENTER, y: 88, text: "", text_font: icons18, text_color: 0x6F9A92, hidden: true }
- label: { id: gl_empty, align: CENTER, text: "Set glance sensors", text_font: font_med, text_color: 0x466761, hidden: true }
+704
View File
@@ -0,0 +1,704 @@
# Snake - optional carousel screen (base id 9). Smooth-steering 360-degree snake steered by the knob.
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
# Scores (g_sn_top / g_sn_best) + the "Reset scores" settings action live here - fully self-contained.
#
# GENERATED by scripts/gen_snake.py - edit the generator, not the blocks. Body = a trail of dots
# (ring buffer) rendered from a lambda (lv_obj_set_pos), so per-tick cost is constant + shallow.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_present)[9] = true;'
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Snake";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_sn_reset);
globals:
- id: g_sn_best
type: int
restore_value: yes
initial_value: '0'
- id: g_sn_top
type: int[10]
restore_value: yes
- id: g_sn_reset
type: bool
restore_value: no
initial_value: 'false'
- id: g_sn_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
type: int
restore_value: no
initial_value: '0'
- id: g_sn_len # body dots in the snake
type: int
restore_value: no
initial_value: '5'
- id: g_sn_score
type: int
restore_value: no
initial_value: '0'
- id: g_sn_tx # body trail dots X (ring buffer; centre coords px)
type: float[100]
restore_value: no
- id: g_sn_ty
type: float[100]
restore_value: no
- id: g_sn_head # ring index of the newest dot
type: int
restore_value: no
initial_value: '0'
- id: g_sn_hx # smooth head position (leads the newest dot)
type: float
restore_value: no
initial_value: '0.0'
- id: g_sn_hy
type: float
restore_value: no
initial_value: '0.0'
- id: g_sn_ang # heading (radians)
type: float
restore_value: no
initial_value: '0.0'
- id: g_sn_grow # pending growth (appends that won't drop the tail)
type: int
restore_value: no
initial_value: '0'
- id: g_sn_dead
type: bool
restore_value: no
initial_value: 'false'
- id: g_sn_add # ring slot to show/position this tick (-1 none)
type: int
restore_value: no
initial_value: '-1'
- id: g_sn_del # ring slot to park off-screen this tick (-1 none)
type: int
restore_value: no
initial_value: '-1'
- id: g_sn_fx
type: float[3]
restore_value: no
- id: g_sn_fy
type: float[3]
restore_value: no
- id: g_sn_fi
type: int[3]
restore_value: no
- id: g_sn_fon
type: bool[3]
restore_value: no
- id: g_sn_fclk
type: int
restore_value: no
initial_value: '0'
- id: g_sn_newfruit
type: int
restore_value: no
initial_value: '-1'
- id: g_sn_skx
type: float[2]
restore_value: no
- id: g_sn_sky
type: float[2]
restore_value: no
- id: g_sn_skon
type: bool[2]
restore_value: no
- id: g_sn_skage
type: int[2]
restore_value: no
- id: g_sn_skclk
type: int
restore_value: no
initial_value: '0'
- id: g_sn_newskull
type: int
restore_value: no
initial_value: '-1'
script:
- id: sn_fspr0
then:
- lvgl.widget.update: { id: sn_fruit0, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[0]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[0]);' }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit0 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit1 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit2 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit3 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit4 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit5 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit6 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit7 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit8 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit9 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit10 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[0] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit0, src: img_sn_fruit11 } ] }
- id: sn_fspr1
then:
- lvgl.widget.update: { id: sn_fruit1, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[1]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[1]);' }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit0 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit1 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit2 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit3 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit4 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit5 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit6 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit7 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit8 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit9 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit10 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[1] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit1, src: img_sn_fruit11 } ] }
- id: sn_fspr2
then:
- lvgl.widget.update: { id: sn_fruit2, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_fx)[2]);', y: !lambda 'return (int) lroundf(id(g_sn_fy)[2]);' }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 0;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit0 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 1;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit1 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 2;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit2 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 3;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit3 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 4;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit4 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 5;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit5 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 6;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit6 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 7;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit7 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 8;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit8 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 9;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit9 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 10;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit10 } ] }
- if: { condition: { lambda: 'return id(g_sn_fi)[2] == 11;' }, then: [ lvgl.image.update: { id: sn_fruit2, src: img_sn_fruit11 } ] }
- id: sn_hud
then:
- lvgl.label.update: { id: lbl_sn_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sn_score)); return std::string(b);' }
- id: sn_items
then:
- lvgl.widget.update: { id: sn_fruit0, hidden: !lambda 'return !id(g_sn_fon)[0];' }
- lvgl.widget.update: { id: sn_fruit1, hidden: !lambda 'return !id(g_sn_fon)[1];' }
- lvgl.widget.update: { id: sn_fruit2, hidden: !lambda 'return !id(g_sn_fon)[2];' }
- lvgl.widget.update: { id: sn_skull0, hidden: !lambda 'return !id(g_sn_skon)[0];' }
- lvgl.widget.update: { id: sn_skull1, hidden: !lambda 'return !id(g_sn_skon)[1];' }
# full body redraw - used at start / when (re)entering the play screen (one lambda, not per tick)
- id: sn_drawfull
then:
- lambda: |-
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
for (int k = 0; k < 100; k++) {
int d = (id(g_sn_head) - k + 400) % 100;
if (d < id(g_sn_len)) lv_obj_set_pos(SS[k], 180 + (int) lroundf(id(g_sn_tx)[k]) - 4, 180 + (int) lroundf(id(g_sn_ty)[k]) - 4);
else lv_obj_set_pos(SS[k], 400, 400);
}
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - 4, 180 + (int) lroundf(id(g_sn_hy)) - 4);
- script.execute: sn_items
# hide every gameplay widget (park body off-screen + hide fruit/skulls)
- id: sn_clear
then:
- lambda: |-
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
for (int k = 0; k < 100; k++) lv_obj_set_pos(SS[k], 400, 400);
lv_obj_set_pos(id(sn_head), 400, 400);
- lvgl.widget.hide: sn_fruit0
- lvgl.widget.hide: sn_fruit1
- lvgl.widget.hide: sn_fruit2
- lvgl.widget.hide: sn_skull0
- lvgl.widget.hide: sn_skull1
- id: sn_start
then:
- lambda: |-
id(g_sn_state) = 1; id(g_sn_score) = 0; id(g_sn_dead) = false;
id(g_sn_ang) = -1.5708f; id(g_sn_grow) = 0; // heading up
id(g_sn_len) = 5; id(g_sn_head) = 4;
for (int i = 0; i < 100; i++) { id(g_sn_tx)[i] = 0.0f; id(g_sn_ty)[i] = 0.0f; }
for (int k = 0; k < 5; k++) { id(g_sn_tx)[k] = 0.0f; id(g_sn_ty)[k] = (4 - k) * 8.0f; }
id(g_sn_hx) = 0.0f; id(g_sn_hy) = 0.0f;
for (int s = 0; s < 3; s++) id(g_sn_fon)[s] = false;
for (int s = 0; s < 2; s++) { id(g_sn_skon)[s] = false; id(g_sn_skage)[s] = 0; }
id(g_sn_fclk) = 0; id(g_sn_skclk) = 0;
id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1; id(g_sn_add) = -1; id(g_sn_del) = -1;
id(g_knob_capture) = true; id(g_knob_delta) = 0;
float a = (esp_random() % 62832) / 10000.0f; float rad = 60.0f + (esp_random() % 80);
id(g_sn_fx)[0] = cosf(a)*rad; id(g_sn_fy)[0] = sinf(a)*rad;
id(g_sn_fi)[0] = esp_random() % 12; id(g_sn_fon)[0] = true;
- script.execute: sn_fspr0
- script.execute: sn_screen
- script.execute: sn_drawfull
- script.execute: sn_hud
- id: sn_over
then:
- lambda: |-
id(g_sn_state) = 2;
int s = id(g_sn_score);
if (s > id(g_sn_best)) id(g_sn_best) = s;
for (int i = 0; i < 10; i++) {
if (s > id(g_sn_top)[i]) {
for (int j = 9; j > i; j--) id(g_sn_top)[j] = id(g_sn_top)[j-1];
id(g_sn_top)[i] = s;
break;
}
}
- script.execute: sn_screen
# screen state + the single knob-capture authority (captured only in play + game over)
- id: sn_screen
then:
- lambda: 'id(g_knob_capture) = (id(g_sn_state) == 1 || id(g_sn_state) == 2);'
- lvgl.widget.hide: sn_ring
- script.execute: sn_clear
- lvgl.widget.hide: lbl_sn_score
- lvgl.widget.hide: sn_logo
- lvgl.widget.hide: btn_sn_start
- lvgl.widget.hide: btn_sn_howto
- lvgl.widget.hide: btn_sn_scores
- lvgl.widget.hide: lbl_sn_over
- lvgl.widget.hide: lbl_sn_rec
- lvgl.widget.hide: btn_sn_replay
- lvgl.widget.hide: btn_sn_exit
- lvgl.widget.hide: lbl_sn_howto_t
- lvgl.widget.hide: sn_b1
- lvgl.widget.hide: sn_b2
- lvgl.widget.hide: sn_b3
- lvgl.widget.hide: sn_b4
- lvgl.widget.hide: lbl_sn_howto1
- lvgl.widget.hide: lbl_sn_howto2
- lvgl.widget.hide: lbl_sn_howto3
- lvgl.widget.hide: lbl_sn_howto4
- lvgl.widget.hide: lbl_sn_scores_t
- lvgl.widget.hide: lbl_sn_scores1
- lvgl.widget.hide: lbl_sn_scores2
- lvgl.widget.hide: btn_sn_back
- if: # MENU
condition: { lambda: 'return id(g_sn_state) == 0;' }
then:
- lvgl.widget.show: sn_logo
- lvgl.widget.show: btn_sn_start
- lvgl.widget.show: btn_sn_howto
- lvgl.widget.show: btn_sn_scores
- if: # PLAY
condition: { lambda: 'return id(g_sn_state) == 1;' }
then:
- lvgl.widget.show: sn_ring
- lvgl.widget.show: lbl_sn_score
- script.execute: sn_drawfull
- if: # GAME OVER
condition: { lambda: 'return id(g_sn_state) == 2;' }
then:
- lvgl.widget.show: sn_logo
- lvgl.widget.show: lbl_sn_over
- lvgl.widget.show: lbl_sn_rec
- lvgl.widget.show: btn_sn_replay
- lvgl.widget.show: btn_sn_exit
- lvgl.label.update: { id: lbl_sn_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_sn_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_sn_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_sn_best)); return std::string(b);' }
- if: # HOW TO PLAY
condition: { lambda: 'return id(g_sn_state) == 3;' }
then:
- lvgl.widget.show: lbl_sn_howto_t
- lvgl.widget.show: sn_b1
- lvgl.widget.show: sn_b2
- lvgl.widget.show: sn_b3
- lvgl.widget.show: sn_b4
- lvgl.widget.show: lbl_sn_howto1
- lvgl.widget.show: lbl_sn_howto2
- lvgl.widget.show: lbl_sn_howto3
- lvgl.widget.show: lbl_sn_howto4
- lvgl.widget.show: btn_sn_back
- if: # SCORES (top 10)
condition: { lambda: 'return id(g_sn_state) == 4;' }
then:
- lvgl.widget.show: lbl_sn_scores_t
- lvgl.widget.show: lbl_sn_scores1
- lvgl.widget.show: lbl_sn_scores2
- lvgl.widget.show: btn_sn_back
- lvgl.label.update:
id: lbl_sn_scores1
text: !lambda |-
std::string s; char b[24];
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_sn_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
- lvgl.label.update:
id: lbl_sn_scores2
text: !lambda |-
std::string s; char b[24];
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_sn_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
interval:
- interval: 250ms
then:
- if:
condition: { lambda: 'return id(g_sn_reset) || id(g_factory_reset);' }
then:
- lambda: 'for (int i = 0; i < 10; i++) id(g_sn_top)[i] = 0; id(g_sn_best) = 0; id(g_sn_reset) = false;'
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==9;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_sn_state) = 0; id(g_knob_delta) = 0;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_snake, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_snake, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_snake, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_snake, animation: move_bottom, time: 250ms }] }
- script.execute: sn_screen
# Game tick: steer + glide head; lay/drop one trail dot when the head advanced SEG_DIST.
# All body/head positioning is done IN this lambda (lv_obj_set_pos) to keep the action chain shallow.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_base) == 9 && id(g_sn_state) == 1;' }
then:
- lambda: |-
id(g_sn_add) = -1; id(g_sn_del) = -1; id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1;
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
id(g_sn_ang) += kd * 0.26f;
float spd = 3.2f + id(g_sn_score) / 400.0f; if (spd > 5.5f) spd = 5.5f;
float hx = id(g_sn_hx) + cosf(id(g_sn_ang)) * spd;
float hy = id(g_sn_hy) + sinf(id(g_sn_ang)) * spd;
if (hx*hx + hy*hy > 168.0f * 168.0f) { id(g_sn_dead) = true; } // hit the ring
else {
id(g_sn_hx) = hx; id(g_sn_hy) = hy;
int len = id(g_sn_len), head = id(g_sn_head);
for (int i = 6; i < len; i++) {
int sl = (head - i + 400) % 100;
float dx = hx - id(g_sn_tx)[sl], dy = hy - id(g_sn_ty)[sl];
if (dx*dx + dy*dy < 8.0f * 8.0f) { id(g_sn_dead) = true; break; }
}
for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) {
float dx = hx - id(g_sn_skx)[s], dy = hy - id(g_sn_sky)[s];
if (dx*dx + dy*dy < 13.0f * 13.0f) { id(g_sn_dead) = true; }
}
for (int s = 0; s < 3 && !id(g_sn_dead); s++) if (id(g_sn_fon)[s]) {
float dx = hx - id(g_sn_fx)[s], dy = hy - id(g_sn_fy)[s];
if (dx*dx + dy*dy < 12.0f * 12.0f) { id(g_sn_fon)[s] = false; id(g_sn_score) += 10; id(g_sn_grow) += 3; }
}
if (!id(g_sn_dead)) {
float dx = hx - id(g_sn_tx)[head], dy = hy - id(g_sn_ty)[head];
if (dx*dx + dy*dy >= 8.0f * 8.0f) {
int nh = (head + 1) % 100;
id(g_sn_tx)[nh] = hx; id(g_sn_ty)[nh] = hy;
id(g_sn_head) = nh; id(g_sn_add) = nh;
if (id(g_sn_grow) > 0 && id(g_sn_len) < 99) { id(g_sn_grow)--; id(g_sn_len)++; id(g_sn_del) = -1; } // cap at POOL-1: the ring needs one free slot or add/drop collide
else { if (id(g_sn_grow) > 0) id(g_sn_grow)--; id(g_sn_del) = (head - id(g_sn_len) + 1 + 400) % 100; }
}
}
}
if (!id(g_sn_dead)) {
int fc = 0; for (int s = 0; s < 3; s++) if (id(g_sn_fon)[s]) fc++;
id(g_sn_fclk)++;
int wait = (fc <= 1) ? 45 : 90;
if (fc < 3 && id(g_sn_fclk) >= wait) {
id(g_sn_fclk) = 0;
int sl = -1; for (int s = 0; s < 3; s++) if (!id(g_sn_fon)[s]) { sl = s; break; }
if (sl >= 0) {
float a = (esp_random() % 62832) / 10000.0f; float rad = 25.0f + (esp_random() % 140);
id(g_sn_fx)[sl] = cosf(a)*rad; id(g_sn_fy)[sl] = sinf(a)*rad;
id(g_sn_fi)[sl] = esp_random() % 12; id(g_sn_fon)[sl] = true; id(g_sn_newfruit) = sl;
}
}
int sc = 0; for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) sc++;
id(g_sn_skclk)++;
if (sc < 2 && id(g_sn_skclk) >= 200) {
id(g_sn_skclk) = 0;
int sl = -1; for (int s = 0; s < 2; s++) if (!id(g_sn_skon)[s]) { sl = s; break; }
if (sl >= 0) {
float a = (esp_random() % 62832) / 10000.0f; float rad = 30.0f + (esp_random() % 130);
id(g_sn_skx)[sl] = cosf(a)*rad; id(g_sn_sky)[sl] = sinf(a)*rad;
id(g_sn_skon)[sl] = true; id(g_sn_skage)[sl] = 0; id(g_sn_newskull) = sl;
}
}
for (int s = 0; s < 2; s++) if (id(g_sn_skon)[s]) { id(g_sn_skage)[s]++; if (id(g_sn_skage)[s] >= 220) id(g_sn_skon)[s] = false; }
// render (constant cost): head every tick + the one added/dropped dot
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - 4, 180 + (int) lroundf(id(g_sn_hy)) - 4);
if (id(g_sn_add) >= 0 || id(g_sn_del) >= 0) {
lv_obj_t *SS[100] = { id(sn_seg0), id(sn_seg1), id(sn_seg2), id(sn_seg3), id(sn_seg4), id(sn_seg5), id(sn_seg6), id(sn_seg7), id(sn_seg8), id(sn_seg9), id(sn_seg10), id(sn_seg11), id(sn_seg12), id(sn_seg13), id(sn_seg14), id(sn_seg15), id(sn_seg16), id(sn_seg17), id(sn_seg18), id(sn_seg19), id(sn_seg20), id(sn_seg21), id(sn_seg22), id(sn_seg23), id(sn_seg24), id(sn_seg25), id(sn_seg26), id(sn_seg27), id(sn_seg28), id(sn_seg29), id(sn_seg30), id(sn_seg31), id(sn_seg32), id(sn_seg33), id(sn_seg34), id(sn_seg35), id(sn_seg36), id(sn_seg37), id(sn_seg38), id(sn_seg39), id(sn_seg40), id(sn_seg41), id(sn_seg42), id(sn_seg43), id(sn_seg44), id(sn_seg45), id(sn_seg46), id(sn_seg47), id(sn_seg48), id(sn_seg49), id(sn_seg50), id(sn_seg51), id(sn_seg52), id(sn_seg53), id(sn_seg54), id(sn_seg55), id(sn_seg56), id(sn_seg57), id(sn_seg58), id(sn_seg59), id(sn_seg60), id(sn_seg61), id(sn_seg62), id(sn_seg63), id(sn_seg64), id(sn_seg65), id(sn_seg66), id(sn_seg67), id(sn_seg68), id(sn_seg69), id(sn_seg70), id(sn_seg71), id(sn_seg72), id(sn_seg73), id(sn_seg74), id(sn_seg75), id(sn_seg76), id(sn_seg77), id(sn_seg78), id(sn_seg79), id(sn_seg80), id(sn_seg81), id(sn_seg82), id(sn_seg83), id(sn_seg84), id(sn_seg85), id(sn_seg86), id(sn_seg87), id(sn_seg88), id(sn_seg89), id(sn_seg90), id(sn_seg91), id(sn_seg92), id(sn_seg93), id(sn_seg94), id(sn_seg95), id(sn_seg96), id(sn_seg97), id(sn_seg98), id(sn_seg99) };
if (id(g_sn_add) >= 0) lv_obj_set_pos(SS[id(g_sn_add)], 180 + (int) lroundf(id(g_sn_tx)[id(g_sn_add)]) - 4, 180 + (int) lroundf(id(g_sn_ty)[id(g_sn_add)]) - 4);
if (id(g_sn_del) >= 0) lv_obj_set_pos(SS[id(g_sn_del)], 400, 400);
}
}
- if:
condition: { lambda: 'return id(g_sn_dead);' }
then:
- script.execute: sn_over
else:
- if:
condition: { lambda: 'return id(g_sn_newfruit) >= 0;' }
then:
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 0;' }, then: [ script.execute: sn_fspr0 ] }
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 1;' }, then: [ script.execute: sn_fspr1 ] }
- if: { condition: { lambda: 'return id(g_sn_newfruit) == 2;' }, then: [ script.execute: sn_fspr2 ] }
- if:
condition: { lambda: 'return id(g_sn_newskull) >= 0;' }
then:
- if: { condition: { lambda: 'return id(g_sn_newskull) == 0;' }, then: [ lvgl.widget.update: { id: sn_skull0, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_skx)[0]);', y: !lambda 'return (int) lroundf(id(g_sn_sky)[0]);' } ] }
- if: { condition: { lambda: 'return id(g_sn_newskull) == 1;' }, then: [ lvgl.widget.update: { id: sn_skull1, hidden: false, x: !lambda 'return (int) lroundf(id(g_sn_skx)[1]);', y: !lambda 'return (int) lroundf(id(g_sn_sky)[1]);' } ] }
- script.execute: sn_items
- script.execute: sn_hud
image:
- id: img_sn_logo
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/snake-logo.png"
type: RGB565
- id: img_sn_fruit0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit0.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit1.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit2.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit3.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit4.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit5.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit6.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit7.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit8
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit8.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit9
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit9.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit10
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit10.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_fruit11
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/fruit11.png"
type: RGB565
transparency: alpha_channel
- id: img_sn_skull
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/skull.png"
type: RGB565
transparency: alpha_channel
lvgl:
pages:
- id: page_snake
bg_color: 0x0D1E15
scrollbar_mode: "off"
scrollable: false
widgets:
- obj: { id: sn_ring, align: CENTER, x: 0, y: 0, width: 344, height: 344, radius: 172, bg_opa: TRANSP, border_width: 4, border_color: 0x3A8C4E, hidden: true }
- image: { id: sn_fruit0, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
- image: { id: sn_fruit1, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
- image: { id: sn_fruit2, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }
- image: { id: sn_skull0, align: CENTER, x: 0, y: 0, src: img_sn_skull, hidden: true }
- image: { id: sn_skull1, align: CENTER, x: 0, y: 0, src: img_sn_skull, hidden: true }
- obj: { id: sn_seg0, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg1, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg2, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg3, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg4, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg5, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg6, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg7, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg8, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg9, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg10, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg11, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg12, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg13, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg14, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg15, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg16, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg17, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg18, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg19, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg20, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg21, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg22, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg23, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg24, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg25, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg26, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg27, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg28, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg29, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg30, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg31, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg32, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg33, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg34, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg35, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg36, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg37, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg38, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg39, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg40, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg41, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg42, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg43, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg44, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg45, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg46, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg47, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg48, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg49, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg50, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg51, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg52, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg53, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg54, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg55, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg56, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg57, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg58, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg59, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg60, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg61, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg62, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg63, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg64, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg65, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg66, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg67, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg68, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg69, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg70, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg71, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg72, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg73, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg74, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg75, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg76, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg77, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg78, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg79, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg80, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg81, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg82, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg83, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg84, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg85, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg86, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg87, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg88, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg89, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg90, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg91, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg92, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg93, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg94, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg95, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg96, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg97, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg98, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_seg99, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0x52A049, bg_opa: COVER }
- obj: { id: sn_head, x: 400, y: 400, width: 9, height: 9, radius: 4, border_width: 0, bg_color: 0xB8E86A, bg_opa: COVER }
- label: { id: lbl_sn_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
- image: { id: sn_logo, align: CENTER, x: 0, y: 0, src: img_sn_logo, hidden: true }
- button:
id: btn_sn_start
align: CENTER
y: 4
width: 150
height: 44
radius: 22
bg_color: 0xA6D85F
bg_opa: COVER
border_width: 0
on_click: [ script.execute: sn_start ]
widgets:
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x12241A }
- button:
id: btn_sn_howto
align: CENTER
y: 54
width: 150
height: 44
radius: 22
bg_color: 0x244A30
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_sn_state) = 3;', script.execute: sn_screen ]
widgets:
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xCFE6C4 }
- button:
id: btn_sn_scores
align: CENTER
y: 104
width: 150
height: 44
radius: 22
bg_color: 0x244A30
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_sn_state) = 4;', script.execute: sn_screen ]
widgets:
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xCFE6C4 }
- label: { id: lbl_sn_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0x12212D, hidden: true }
- label: { id: lbl_sn_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x12212D, hidden: true }
- button:
id: btn_sn_replay
align: CENTER
x: -66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0xA6D85F
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ script.execute: sn_start ]
widgets:
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x12241A }
- button:
id: btn_sn_exit
align: CENTER
x: 66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
widgets:
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
- label: { id: lbl_sn_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xA6D85F, hidden: true }
- obj: { id: sn_b1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: sn_b2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: sn_b3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: 0x6FB23F, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: sn_b4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0xE05A4A, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "!", text_font: font_artist, text_color: 0xFFFFFF } ] }
- label: { id: lbl_sn_howto1, align_to: { id: sn_b1, align: OUT_RIGHT_MID, x: 8 }, text: "Turn knob to steer", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
- label: { id: lbl_sn_howto2, align_to: { id: sn_b2, align: OUT_RIGHT_MID, x: 8 }, text: "Eat fruit to grow", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
- label: { id: lbl_sn_howto3, align_to: { id: sn_b3, align: OUT_RIGHT_MID, x: 8 }, text: "Avoid the ring & tail", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
- label: { id: lbl_sn_howto4, align_to: { id: sn_b4, align: OUT_RIGHT_MID, x: 8 }, text: "Skulls kill - avoid!", text_font: font_med, text_color: 0xC6DCC0, hidden: true }
- label: { id: lbl_sn_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xA6D85F, hidden: true }
- label: { id: lbl_sn_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC6DCC0, hidden: true }
- label: { id: lbl_sn_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC6DCC0, hidden: true }
- button:
id: btn_sn_back
align: CENTER
y: 120
width: 130
height: 42
radius: 21
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
widgets:
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }
+498
View File
@@ -0,0 +1,498 @@
# Space Wars - optional carousel screen (base id 4). Vertical shooter.
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
# Scores (g_sw_top / g_sw_best) + the "Reset scores" settings action live here - fully self-contained.
esphome:
on_boot:
- priority: 660
then:
- lambda: 'id(g_present)[4] = true;' # register after clock/player/timer/cars
# Settings (Settings -> Widgets -> Space Wars -> Reset scores) + factory-reset handling.
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Space Wars";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_sw_reset);
globals:
# --- scores (persistent) + reset flag, owned by this package ---
- id: g_sw_best
type: int
restore_value: yes
initial_value: '0'
- id: g_sw_top
type: int[10]
restore_value: yes
- id: g_sw_reset # Widgets "Reset scores" sets this; the watcher below clears the scores
type: bool
restore_value: no
initial_value: 'false'
# --- Game 2 "Space Wars" (page_game2, g_base==4) ---
- id: g_sw_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
type: int
restore_value: no
initial_value: '0'
- id: g_sw_px # player ship X
type: int
restore_value: no
initial_value: '0'
- id: g_sw_lives
type: int
restore_value: no
initial_value: '3'
- id: g_sw_inv
type: int
restore_value: no
initial_value: '0'
- id: g_sw_score
type: int
restore_value: no
initial_value: '0'
- id: g_fire # auto-fire cadence counter
type: int
restore_value: no
initial_value: '0'
- id: g_tcount # time counter (points for time survived)
type: int
restore_value: no
initial_value: '0'
- id: g_sw_crash
type: bool
restore_value: no
initial_value: 'false'
- id: g_b_x # player bullets (2): X
type: int[2]
restore_value: no
- id: g_b_y
type: int[2]
restore_value: no
- id: g_b_on
type: bool[2]
restore_value: no
- id: g_en_x # enemies (3)
type: int[3]
restore_value: no
- id: g_en_y
type: int[3]
restore_value: no
- id: g_en_on
type: bool[3]
restore_value: no
- id: g_eb_x # enemy bullets (2)
type: int[2]
restore_value: no
- id: g_eb_y
type: int[2]
restore_value: no
- id: g_eb_on
type: bool[2]
restore_value: no
- id: g_exp_f # explosion: countdown frame counter (0=inactive), ticks in the 70ms loop
type: int
restore_value: no
initial_value: '0'
- id: g_exp_x # explosion: position (from the destroyed enemy)
type: int
restore_value: no
initial_value: '0'
- id: g_exp_y
type: int
restore_value: no
initial_value: '0'
- id: g_exp_new # flag: start the animation this tick
type: bool
restore_value: no
initial_value: 'false'
- id: g_exp_hide # flag: hide the explosion this tick
type: bool
restore_value: no
initial_value: 'false'
script:
# ===== SPACE WARS (game 2) =====
- id: sw_render # ship X + score + lives
then:
- lvgl.widget.update: { id: sw_ship, x: !lambda 'return id(g_sw_px);' }
- lvgl.label.update: { id: lbl_sw_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sw_score)); return std::string(b);' }
- lvgl.label.update:
id: lbl_sw_lives
text: !lambda |-
std::string h; for (int i=0;i<id(g_sw_lives);i++) h += "󰋑"; return h;
- id: sw_start
then:
- lambda: |-
id(g_sw_state)=1; id(g_sw_score)=0; id(g_sw_lives)=3; id(g_sw_inv)=0;
id(g_sw_px)=0; id(g_fire)=0; id(g_tcount)=0;
id(g_knob_capture)=true; id(g_knob_delta)=0;
id(g_exp_f)=0; id(g_exp_new)=false; id(g_exp_hide)=false;
for (int i=0;i<2;i++){ id(g_b_on)[i]=false; id(g_eb_on)[i]=false; }
for (int i=0;i<3;i++){ id(g_en_on)[i]=true; id(g_en_x)[i]=(int)(esp_random()%280)-140; id(g_en_y)[i]=-160 - i*70; }
- script.execute: sw_screen
- script.execute: sw_render
- id: sw_lose
then:
- lambda: 'id(g_sw_lives)--; id(g_sw_inv)=16;'
- lvgl.widget.show: sw_flash
- if:
condition: { lambda: 'return id(g_sw_lives) <= 0;' }
then: [ script.execute: sw_over ]
else: [ script.execute: sw_render ]
- id: sw_over
then:
- lambda: |-
id(g_sw_state)=2; // sw_screen keeps the knob captured on game over (no volume)
int s=id(g_sw_score);
if (s>id(g_sw_best)) id(g_sw_best)=s;
for (int i=0;i<10;i++){ if(s>id(g_sw_top)[i]){ for(int j=9;j>i;j--) id(g_sw_top)[j]=id(g_sw_top)[j-1]; id(g_sw_top)[i]=s; break; } }
// reset game state so the end of the current tick restores nothing (red flash / enemies)
id(g_sw_inv)=0;
for(int i=0;i<3;i++) id(g_en_on)[i]=false;
for(int i=0;i<2;i++){ id(g_b_on)[i]=false; id(g_eb_on)[i]=false; }
id(g_exp_f)=0; id(g_exp_new)=false; id(g_exp_hide)=false;
- script.execute: sw_screen
- id: sw_screen # 0=menu 1=playing 2=game over 3=how-to 4=scores
then:
# knob steers only during play + game over -> capture it there (block volume), free it in menu/how-to/scores
- lambda: 'id(g_knob_capture) = (id(g_sw_state) == 1 || id(g_sw_state) == 2);'
- lvgl.widget.hide: sw_bg
- lvgl.widget.hide: sw_logo
- lvgl.widget.hide: sw_ship
- lvgl.widget.hide: sw_exp
- lvgl.widget.hide: b0
- lvgl.widget.hide: b1
- lvgl.widget.hide: eb0
- lvgl.widget.hide: eb1
- lvgl.widget.hide: en0
- lvgl.widget.hide: en1
- lvgl.widget.hide: en2
- lvgl.widget.hide: lbl_sw_lives
- lvgl.widget.hide: lbl_sw_score
- lvgl.widget.hide: sw_flash
- lvgl.widget.hide: btn_sw_start
- lvgl.widget.hide: btn_sw_howto
- lvgl.widget.hide: btn_sw_scores
- lvgl.widget.hide: lbl_sw_over
- lvgl.widget.hide: lbl_sw_rec
- lvgl.widget.hide: btn_sw_replay
- lvgl.widget.hide: btn_sw_exit
- lvgl.widget.hide: lbl_sw_ht_t
- lvgl.widget.hide: badge_h1
- lvgl.widget.hide: badge_h2
- lvgl.widget.hide: badge_h3
- lvgl.widget.hide: badge_h4
- lvgl.widget.hide: lbl_sw_ht1
- lvgl.widget.hide: lbl_sw_ht2
- lvgl.widget.hide: lbl_sw_ht3
- lvgl.widget.hide: lbl_sw_ht4
- lvgl.widget.hide: lbl_sw_hheart
- lvgl.widget.hide: lbl_sw_st
- lvgl.widget.hide: lbl_sw_s1
- lvgl.widget.hide: lbl_sw_s2
- lvgl.widget.hide: btn_sw_back
- if: # MENU
condition: { lambda: 'return id(g_sw_state)==0;' }
then: [ lvgl.widget.show: sw_logo, lvgl.widget.show: btn_sw_start, lvgl.widget.show: btn_sw_howto, lvgl.widget.show: btn_sw_scores ]
- if: # GAME
condition: { lambda: 'return id(g_sw_state)==1;' }
then:
- lvgl.widget.show: sw_bg
- lvgl.widget.show: sw_ship
- lvgl.widget.show: en0
- lvgl.widget.show: en1
- lvgl.widget.show: en2
- lvgl.widget.show: lbl_sw_lives
- lvgl.widget.show: lbl_sw_score
- if: # GAME OVER
condition: { lambda: 'return id(g_sw_state)==2;' }
then:
- lvgl.widget.show: sw_logo
- lvgl.widget.show: lbl_sw_over
- lvgl.widget.show: lbl_sw_rec
- lvgl.widget.show: btn_sw_replay
- lvgl.widget.show: btn_sw_exit
- lvgl.label.update: { id: lbl_sw_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_sw_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_sw_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_sw_best)); return std::string(b);' }
- if: # HOW TO PLAY
condition: { lambda: 'return id(g_sw_state)==3;' }
then:
- lvgl.widget.show: sw_bg
- lvgl.widget.show: lbl_sw_ht_t
- lvgl.widget.show: badge_h1
- lvgl.widget.show: badge_h2
- lvgl.widget.show: badge_h3
- lvgl.widget.show: badge_h4
- lvgl.widget.show: lbl_sw_ht1
- lvgl.widget.show: lbl_sw_ht2
- lvgl.widget.show: lbl_sw_ht3
- lvgl.widget.show: lbl_sw_ht4
- lvgl.widget.show: lbl_sw_hheart
- lvgl.widget.show: btn_sw_back
- if: # SCORES
condition: { lambda: 'return id(g_sw_state)==4;' }
then:
- lvgl.widget.show: sw_bg
- lvgl.widget.show: lbl_sw_st
- lvgl.widget.show: lbl_sw_s1
- lvgl.widget.show: lbl_sw_s2
- lvgl.widget.show: btn_sw_back
- lvgl.label.update: { id: lbl_sw_s1, text: !lambda 'std::string s; char b[24]; for(int i=0;i<5;i++){ if(i) s+="\n"; if(id(g_sw_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sw_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s+=b; } return s;' }
- lvgl.label.update: { id: lbl_sw_s2, text: !lambda 'std::string s; char b[24]; for(int i=5;i<10;i++){ if(i>5) s+="\n"; if(id(g_sw_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sw_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s+=b; } return s;' }
interval:
# Reset scores when the Widgets action fired (g_sw_reset). Idempotent + cheap.
- interval: 250ms
then:
- if:
condition: { lambda: 'return id(g_sw_reset) || id(g_factory_reset);' }
then:
- lambda: 'for (int i = 0; i < 10; i++) id(g_sw_top)[i] = 0; id(g_sw_best) = 0; id(g_sw_reset) = false;'
# nav: show this screen when it is selected in the carousel
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==4;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_sw_state) = 0;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_game2, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_game2, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_game2, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_game2, animation: move_bottom, time: 250ms }] }
- script.execute: sw_screen
# Space Wars - tick only on the Space screen during play
- interval: 70ms
then:
- if:
condition: { lambda: 'return id(g_base) == 4 && id(g_sw_state) == 1;' }
then:
- lambda: |-
id(g_sw_px) += id(g_knob_delta) * 18; if (id(g_sw_px) < -150) id(g_sw_px) = -150; if (id(g_sw_px) > 150) id(g_sw_px) = 150; id(g_knob_delta) = 0;
int px = id(g_sw_px);
if (id(g_sw_inv) > 0) id(g_sw_inv)--;
if (id(g_exp_f) > 0) { id(g_exp_f)--; if (id(g_exp_f)==0) id(g_exp_hide)=true; }
id(g_tcount)++; if (id(g_tcount) % 10 == 0) id(g_sw_score)++;
// auto-fire every 5 ticks into a free slot
id(g_fire)++;
if (id(g_fire) >= 5) { id(g_fire)=0; for(int i=0;i<2;i++){ if(!id(g_b_on)[i]){ id(g_b_on)[i]=true; id(g_b_x)[i]=px; id(g_b_y)[i]=100; break; } } }
// player bullets go up
for (int i=0;i<2;i++){ if(id(g_b_on)[i]){ id(g_b_y)[i]-=24; if(id(g_b_y)[i]<-185) id(g_b_on)[i]=false; } }
// enemies go down; destroyed/escaped -> respawn at the top
for (int e=0;e<3;e++){
if(id(g_en_on)[e]){ id(g_en_y)[e]+=6; if(id(g_en_y)[e]>188){ id(g_en_y)[e]=-160; id(g_en_x)[e]=(int)(esp_random()%280)-140; } }
else { id(g_en_on)[e]=true; id(g_en_y)[e]=-160-(int)(esp_random()%140); id(g_en_x)[e]=(int)(esp_random()%280)-140; }
}
// enemies shoot
if ((int)(esp_random()%100) < 9) { int k=(int)(esp_random()%3); if(id(g_en_on)[k]) for(int i=0;i<2;i++){ if(!id(g_eb_on)[i]){ id(g_eb_on)[i]=true; id(g_eb_x)[i]=id(g_en_x)[k]; id(g_eb_y)[i]=id(g_en_y)[k]+26; break; } } }
// enemy bullets go down
for (int i=0;i<2;i++){ if(id(g_eb_on)[i]){ id(g_eb_y)[i]+=13; if(id(g_eb_y)[i]>188) id(g_eb_on)[i]=false; } }
// player bullet x enemy
for (int b=0;b<2;b++){ if(id(g_b_on)[b]) for(int e=0;e<3;e++){ if(id(g_en_on)[e] && abs(id(g_b_x)[b]-id(g_en_x)[e])<22 && abs(id(g_b_y)[b]-id(g_en_y)[e])<26){ id(g_en_on)[e]=false; id(g_b_on)[b]=false; id(g_sw_score)+=10; id(g_exp_x)=id(g_en_x)[e]; id(g_exp_y)=id(g_en_y)[e]; id(g_exp_f)=6; id(g_exp_new)=true; break; } } }
// enemy / enemy bullet x player
id(g_sw_crash)=false;
if (id(g_sw_inv)<=0){
for(int e=0;e<3;e++){ if(id(g_en_on)[e] && abs(id(g_en_x)[e]-px)<26 && abs(id(g_en_y)[e]-130)<32){ id(g_en_on)[e]=false; id(g_sw_crash)=true; break; } }
for(int i=0;i<2;i++){ if(id(g_eb_on)[i] && abs(id(g_eb_x)[i]-px)<18 && abs(id(g_eb_y)[i]-130)<26){ id(g_eb_on)[i]=false; id(g_sw_crash)=true; break; } }
}
- lvgl.widget.update: { id: sw_ship, x: !lambda 'return id(g_sw_px);' }
- if: { condition: { lambda: 'return id(g_sw_crash);' }, then: [ script.execute: sw_lose ] }
# explosion after destroying an enemy: set position, show, run the animation once
- if:
condition: { lambda: 'return id(g_exp_new);' }
then:
- lvgl.widget.update: { id: sw_exp, x: !lambda 'return id(g_exp_x);', y: !lambda 'return id(g_exp_y);', hidden: false }
- lvgl.animimg.start: sw_exp
- lambda: 'id(g_exp_new)=false;'
- if:
condition: { lambda: 'return id(g_exp_hide);' }
then:
- lvgl.animimg.stop: sw_exp
- lvgl.widget.hide: sw_exp
- lambda: 'id(g_exp_hide)=false;'
# positions + visibility
- lvgl.widget.update: { id: b0, x: !lambda 'return id(g_b_x)[0];', y: !lambda 'return id(g_b_y)[0];', hidden: !lambda 'return !id(g_b_on)[0];' }
- lvgl.widget.update: { id: b1, x: !lambda 'return id(g_b_x)[1];', y: !lambda 'return id(g_b_y)[1];', hidden: !lambda 'return !id(g_b_on)[1];' }
- lvgl.widget.update: { id: eb0, x: !lambda 'return id(g_eb_x)[0];', y: !lambda 'return id(g_eb_y)[0];', hidden: !lambda 'return !id(g_eb_on)[0];' }
- lvgl.widget.update: { id: eb1, x: !lambda 'return id(g_eb_x)[1];', y: !lambda 'return id(g_eb_y)[1];', hidden: !lambda 'return !id(g_eb_on)[1];' }
- lvgl.widget.update: { id: en0, x: !lambda 'return id(g_en_x)[0];', y: !lambda 'return id(g_en_y)[0];', hidden: !lambda 'return !id(g_en_on)[0];' }
- lvgl.widget.update: { id: en1, x: !lambda 'return id(g_en_x)[1];', y: !lambda 'return id(g_en_y)[1];', hidden: !lambda 'return !id(g_en_on)[1];' }
- lvgl.widget.update: { id: en2, x: !lambda 'return id(g_en_x)[2];', y: !lambda 'return id(g_en_y)[2];', hidden: !lambda 'return !id(g_en_on)[2];' }
- lvgl.label.update: { id: lbl_sw_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sw_score)); return std::string(b);' }
- if:
condition: { lambda: 'return id(g_sw_inv) > 0;' }
then:
- lvgl.widget.update: { id: sw_ship, hidden: !lambda 'return (id(g_sw_inv) % 4) >= 2;' }
- lvgl.widget.update: { id: sw_flash, hidden: !lambda 'return id(g_sw_inv) < 12;' }
image:
# Space Wars
- id: img_spacebg
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/spacebg.png"
type: RGB565
- id: img_logo2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/logo2.png"
type: RGB565
- id: img_sp
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/player.png"
type: RGB565
transparency: alpha_channel
- id: img_se1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/enemy1.png"
type: RGB565
transparency: alpha_channel
- id: img_se2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/enemy2.png"
type: RGB565
transparency: alpha_channel
- id: img_exp0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp0.png"
type: RGB565
transparency: alpha_channel
- id: img_exp1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp1.png"
type: RGB565
transparency: alpha_channel
- id: img_exp2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp2.png"
type: RGB565
transparency: alpha_channel
- id: img_exp3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp3.png"
type: RGB565
transparency: alpha_channel
- id: img_exp4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp4.png"
type: RGB565
transparency: alpha_channel
- id: img_exp5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/space-wars/exp5.png"
type: RGB565
transparency: alpha_channel
lvgl:
pages:
# --- SPACE WARS page (game 2): vertical shooter ---
- id: page_game2
bg_color: 0x140A20
scrollbar_mode: "off"
scrollable: false
widgets:
- image: { id: sw_bg, align: CENTER, x: 0, y: 0, src: img_spacebg, hidden: true }
- image: { id: sw_logo, align: CENTER, x: 0, y: 0, src: img_logo2, hidden: true }
- obj: { id: b0, align: CENTER, x: 0, y: 0, width: 4, height: 14, radius: 2, border_width: 0, bg_color: 0xFFE45A, hidden: true }
- obj: { id: b1, align: CENTER, x: 0, y: 0, width: 4, height: 14, radius: 2, border_width: 0, bg_color: 0xFFE45A, hidden: true }
- obj: { id: eb0, align: CENTER, x: 0, y: 0, width: 5, height: 14, radius: 2, border_width: 0, bg_color: 0xFF5050, hidden: true }
- obj: { id: eb1, align: CENTER, x: 0, y: 0, width: 5, height: 14, radius: 2, border_width: 0, bg_color: 0xFF5050, hidden: true }
- image: { id: en0, align: CENTER, x: -60, y: -150, src: img_se1, hidden: true }
- image: { id: en1, align: CENTER, x: 60, y: -150, src: img_se1, hidden: true }
- image: { id: en2, align: CENTER, x: 0, y: -150, src: img_se2, hidden: true }
- image: { id: sw_ship, align: CENTER, x: 0, y: 130, src: img_sp, hidden: true }
- animimg:
id: sw_exp
align: CENTER
x: 0
y: 0
src: [img_exp0, img_exp1, img_exp2, img_exp3, img_exp4, img_exp5]
duration: 420ms
repeat_count: 1
auto_start: false
hidden: true
- label: { id: lbl_sw_lives, align: CENTER, x: -54, y: -132, text: "", text_font: icons18, text_color: 0xE24B4A, hidden: true }
- label: { id: lbl_sw_score, align: CENTER, x: 54, y: -132, text: "0", text_font: font_med, text_color: 0xCFEAE4, hidden: true }
- obj: { id: sw_flash, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, border_width: 0, bg_color: 0xFF2A2A, bg_opa: 45%, hidden: true }
# MENU
- button:
id: btn_sw_start
align: CENTER
y: 4
width: 150
height: 44
radius: 22
bg_color: 0xFF7A4D
bg_opa: COVER
border_width: 0
on_click: [ script.execute: sw_start ]
widgets: [ label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x2A0E06 } ]
- button:
id: btn_sw_howto
align: CENTER
y: 54
width: 150
height: 44
radius: 22
bg_color: 0x3A2150
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_sw_state)=3;', script.execute: sw_screen ]
widgets: [ label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xFFFFFF } ]
- button:
id: btn_sw_scores
align: CENTER
y: 104
width: 150
height: 44
radius: 22
bg_color: 0x3A2150
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_sw_state)=4;', script.execute: sw_screen ]
widgets: [ label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xFFFFFF } ]
# GAME OVER
- label: { id: lbl_sw_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xFFFFFF, hidden: true }
- label: { id: lbl_sw_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x9AA6B2, hidden: true }
- button:
id: btn_sw_replay
align: CENTER
x: -66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0xFF7A4D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ script.execute: sw_start ]
widgets: [ label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x2A0E06 } ]
- button:
id: btn_sw_exit
align: CENTER
x: 66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_sw_state)=0;', script.execute: sw_screen ]
widgets: [ label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF } ]
# HOW TO PLAY
- label: { id: lbl_sw_ht_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0xFF7A4D, hidden: true }
- obj: { id: badge_h1, align: CENTER, x: -90, y: -46, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge_h2, align: CENTER, x: -90, y: -10, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge_h3, align: CENTER, x: -90, y: 26, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: badge_h4, align: CENTER, x: -90, y: 62, width: 24, height: 24, radius: 12, bg_color: 0xFF7A4D, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
- label: { id: lbl_sw_ht1, align_to: { id: badge_h1, align: OUT_RIGHT_MID, x: 8 }, text: "Use the knob", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_sw_ht2, align_to: { id: badge_h2, align: OUT_RIGHT_MID, x: 8 }, text: "Dodge bullets", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_sw_ht3, align_to: { id: badge_h3, align: OUT_RIGHT_MID, x: 8 }, text: "Destroy enemies", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_sw_ht4, align_to: { id: badge_h4, align: OUT_RIGHT_MID, x: 8 }, text: "You have 3 lives", text_font: font_med, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_sw_hheart, align_to: { id: lbl_sw_ht4, align: OUT_RIGHT_MID, x: 6 }, text: "󰋑󰋑󰋑", text_font: icons18, text_color: 0xE24B4A, hidden: true }
# SCORES
- label: { id: lbl_sw_st, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0xFF7A4D, hidden: true }
- label: { id: lbl_sw_s1, align: CENTER, x: -50, y: -6, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
- label: { id: lbl_sw_s2, align: CENTER, x: 50, y: -6, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC2CDDA, hidden: true }
- button:
id: btn_sw_back
align: CENTER
y: 120
width: 130
height: 42
radius: 21
bg_color: 0x3A3A3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_sw_state)=0;', script.execute: sw_screen ]
widgets: [ label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF } ]
+298
View File
@@ -0,0 +1,298 @@
# Thermostat - optional carousel screen (base id 7).
# A round dial for a Home Assistant climate.* entity (${climate_entity}):
# * big centre = the target temperature; turn the KNOB to change it (by the entity's
# target_temp_step). The new value is sent to HA ~0.6 s after you stop turning.
# * "now ..." = the current room temperature; "RH ..%" = humidity (if the entity has it).
# * top label + the target colour show the action: heating (orange) / cooling (blue) /
# idle (teal) / off (grey). Bottom chip = the HVAC mode.
# * TAP toggles the climate on/off.
#
# Set ${climate_entity} in your config. Remove this screen by dropping the file from the
# package "files:" list.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_present)[7] = true;' # register the thermostat screen
globals:
- id: g_th_cur # current temperature (-127 = no reading yet)
type: float
restore_value: no
initial_value: '-127.0'
- id: g_th_target # target temperature (synced from HA; edited locally by the knob)
type: float
restore_value: no
initial_value: '20.0'
- id: g_th_action # hvac_action: heating / cooling / idle / off (may be empty)
type: std::string
restore_value: no
- id: g_th_mode # hvac mode (the entity state): off / heat / cool / auto / heat_cool
type: std::string
restore_value: no
- id: g_th_hum # current humidity % (-1 = none)
type: int
restore_value: no
initial_value: '-1'
- id: g_th_min
type: float
restore_value: no
initial_value: '7.0'
- id: g_th_max
type: float
restore_value: no
initial_value: '35.0'
- id: g_th_step
type: float
restore_value: no
initial_value: '0.5'
- id: g_th_pending # true = local edit not yet sent; blocks HA from overwriting the target
type: bool
restore_value: no
initial_value: 'false'
- id: g_th_lasttap # last g_tap_seq we handled (one tap = one on/off toggle)
type: int
restore_value: no
initial_value: '0'
script:
# Sends the edited target to HA, but only ~0.6 s after the last knob turn (mode: restart).
- id: th_commit
mode: restart
then:
- delay: 600ms
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: ${climate_entity}
temperature: !lambda 'char b[8]; snprintf(b, sizeof(b), "%.1f", id(g_th_target)); return std::string(b);'
- lambda: 'id(g_th_pending) = false;'
# Redraws the dial from the state globals.
- id: th_render
then:
- lvgl.label.update:
id: th_target
text: !lambda 'char b[12]; snprintf(b, sizeof(b), "%.1f°", id(g_th_target)); return std::string(b);'
- lvgl.label.update:
id: th_cur
text: !lambda |-
if (id(g_th_cur) < -100) return std::string("now --");
char b[16]; snprintf(b, sizeof(b), "now %.1f°", id(g_th_cur)); return std::string(b);
- lvgl.widget.update: { id: th_hum, hidden: !lambda 'return id(g_th_hum) < 0;' }
- lvgl.label.update:
id: th_hum
text: !lambda |-
if (id(g_th_hum) < 0) return std::string("");
char b[12]; snprintf(b, sizeof(b), "RH %d%%", id(g_th_hum)); return std::string(b);
- lvgl.label.update:
id: th_mode
text: !lambda |-
std::string m = id(g_th_mode);
if (m == "heat_cool") return std::string("Auto");
if (m.empty()) return std::string("-");
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
- lvgl.arc.update:
id: th_arc
value: !lambda |-
float r = id(g_th_max) - id(g_th_min);
if (r <= 0) return 0.0f;
float p = 100.0f * (id(g_th_target) - id(g_th_min)) / r;
if (p < 0) p = 0; if (p > 100) p = 100;
return p;
- lvgl.label.update:
id: th_action
text: !lambda |-
std::string m = id(g_th_mode), a = id(g_th_action);
if (m == "off" || m.empty()) return std::string("OFF");
if (a.find("heat") != std::string::npos || (a.empty() && m == "heat")) return std::string("HEATING");
if (a.find("cool") != std::string::npos || (a.empty() && m == "cool")) return std::string("COOLING");
if (a == "idle") return std::string("IDLE");
if (!a.empty()) { if (a[0] >= 'a' && a[0] <= 'z') a[0] -= 32; return a; }
if (m[0] >= 'a' && m[0] <= 'z') m[0] -= 32; return m;
# accent colour for the action label + target by heat/cool/off/idle
- if:
condition:
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("heat") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "heat"));'
then:
- lvgl.label.update: { id: th_action, text_color: 0xFF6B30 }
- lvgl.label.update: { id: th_target, text_color: 0xFF6B30 }
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0xFF6B30 } }
else:
- if:
condition:
lambda: 'return id(g_th_mode) != "off" && !id(g_th_mode).empty() && (id(g_th_action).find("cool") != std::string::npos || (id(g_th_action).empty() && id(g_th_mode) == "cool"));'
then:
- lvgl.label.update: { id: th_action, text_color: 0x3FA9D6 }
- lvgl.label.update: { id: th_target, text_color: 0x3FA9D6 }
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x3FA9D6 } }
else:
- if:
condition: { lambda: 'return id(g_th_mode) == "off" || id(g_th_mode).empty();' }
then:
- lvgl.label.update: { id: th_action, text_color: 0x6F9A92 }
- lvgl.label.update: { id: th_target, text_color: 0x6F9A92 }
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x44506A } }
else:
- lvgl.label.update: { id: th_action, text_color: 0x9FC7BE }
- lvgl.label.update: { id: th_target, text_color: 0xFFFFFF }
- lvgl.arc.update: { id: th_arc, indicator: { arc_color: 0x2FE6CF } }
- lambda: |- # position the target head-dot on the gauge
float r = id(g_th_max) - id(g_th_min);
float p = (r > 0) ? 100.0f * (id(g_th_target) - id(g_th_min)) / r : 0.0f;
if (p < 0) p = 0; if (p > 100) p = 100;
float a = (135.0f + p / 100.0f * 270.0f) * 0.01745329f;
int x = 180 + (int) lroundf(144.0f * cosf(a)) - 9;
int y = 180 + (int) lroundf(144.0f * sinf(a)) - 9;
lv_obj_set_pos(id(th_dot), x, y);
sensor:
- platform: homeassistant
id: th_cur_s
entity_id: ${climate_entity}
attribute: current_temperature
on_value:
- lambda: 'id(g_th_cur) = x;'
- script.execute: th_render
- platform: homeassistant
id: th_tgt_s
entity_id: ${climate_entity}
attribute: temperature
on_value:
- lambda: 'if (!id(g_th_pending)) id(g_th_target) = x;' # don't clobber a value being edited
- script.execute: th_render
- platform: homeassistant
id: th_hum_s
entity_id: ${climate_entity}
attribute: current_humidity
on_value:
- lambda: 'id(g_th_hum) = (int) roundf(x);'
- script.execute: th_render
- platform: homeassistant
id: th_min_s
entity_id: ${climate_entity}
attribute: min_temp
on_value: [ lambda: 'id(g_th_min) = x;' ]
- platform: homeassistant
id: th_max_s
entity_id: ${climate_entity}
attribute: max_temp
on_value: [ lambda: 'id(g_th_max) = x;' ]
- platform: homeassistant
id: th_step_s
entity_id: ${climate_entity}
attribute: target_temp_step
on_value: [ lambda: 'if (x > 0) id(g_th_step) = x;' ]
text_sensor:
- platform: homeassistant
id: th_mode_s
entity_id: ${climate_entity} # the entity state IS the hvac mode
on_value:
- lambda: 'id(g_th_mode) = x;'
- script.execute: th_render
- platform: homeassistant
id: th_action_s
entity_id: ${climate_entity}
attribute: hvac_action
on_value:
- lambda: 'id(g_th_action) = x;'
- script.execute: th_render
interval:
# Carousel nav: when selected, show the page and take the knob.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==7;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_knob_capture) = true; id(g_th_lasttap) = id(g_tap_seq);'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_thermostat, animation: move_bottom, time: 250ms }] }
- script.execute: th_render
# Knob (adjust target, debounced commit) + tap (on/off) while this screen is on top.
- interval: 120ms
then:
- if:
condition: { lambda: 'return id(g_base)==7 && id(g_knob_delta) != 0;' }
then:
- lambda: |-
id(g_th_target) += id(g_knob_delta) * id(g_th_step);
id(g_knob_delta) = 0;
if (id(g_th_target) < id(g_th_min)) id(g_th_target) = id(g_th_min);
if (id(g_th_target) > id(g_th_max)) id(g_th_target) = id(g_th_max);
id(g_th_pending) = true;
- script.execute: th_commit
- script.execute: th_render
- if:
condition: { lambda: 'return id(g_base)==7 && id(g_tap_seq) != id(g_th_lasttap);' }
then:
- lambda: 'id(g_th_lasttap) = id(g_tap_seq);'
- if:
condition: { lambda: 'return id(g_th_mode) == "off";' }
then:
- homeassistant.action:
action: climate.turn_on
data:
entity_id: ${climate_entity}
else:
- homeassistant.action:
action: climate.turn_off
data:
entity_id: ${climate_entity}
lvgl:
pages:
- id: page_thermostat
bg_color: 0x0B1020
widgets:
- obj: # faint outer ring for depth
id: th_ring_out
align: CENTER
width: 332
height: 332
radius: 166
bg_opa: TRANSP
border_width: 2
border_color: 0x1B2740
border_opa: 70%
clickable: false
scrollable: false
- arc:
id: th_arc
align: CENTER
width: 300
height: 300
arc_width: 12
adjustable: false
min_value: 0
max_value: 100
value: 50
start_angle: 135
end_angle: 45
arc_color: 0x21304A
indicator:
arc_color: 0x2FE6CF
arc_width: 12
- obj: # head-dot: white bead marking the target on the gauge (positioned in th_render)
id: th_dot
x: 0
y: 0
width: 18
height: 18
radius: 9
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 0
shadow_color: 0xFFFFFF
shadow_width: 10
shadow_opa: 55%
clickable: false
scrollable: false
- label: { id: th_action, align: CENTER, y: -86, text: "", text_font: font_artist, text_color: 0x9FC7BE }
- label: { id: th_target, align: CENTER, y: -14, text: "--", text_font: montserrat_48, text_color: 0xFFFFFF }
- label: { id: th_cur, align: CENTER, y: 30, text: "now --", text_font: font_med, text_color: 0x9FC7BE }
- label: { id: th_hum, align: CENTER, y: 58, text: "", text_font: font_artist, text_color: 0x3FA9D6, hidden: true }
- label: { id: th_mode, align: CENTER, y: 92, text: "", text_font: font_artist, text_color: 0xCFEAE4 }
+17
View File
@@ -0,0 +1,17 @@
# Timer carousel-screen toggle.
#
# Unlike the other screens, the timer cannot be a full standalone package: its logic is
# wired into core components (the voice_assistant on_timer_* triggers, the shared alarm
# overlay, timer_render, the home-screen badge, the knob). So the timer FEATURE always
# lives in base/core.yaml. This tiny package only adds the timer SCREEN to the swipe
# carousel (base id 2).
#
# Remove this file from your config's package "files:" list to hide the timer screen from
# the carousel. Voice timers ("set a 5 minute timer"), the alarm and the home-screen
# countdown badge keep working - you just can't swipe to the on-device timer screen.
esphome:
on_boot:
- priority: 680
then:
- lambda: 'id(g_present)[2] = true;' # add the timer screen to the carousel
+37
View File
@@ -0,0 +1,37 @@
# Home Assistant helper for the device's Weather screen (base/screens/weather.yaml).
#
# HA no longer exposes a weather entity's multi-day forecast as an attribute, so this
# template calls weather.get_forecasts and publishes a single compact sensor the device
# reads. State format: "condition,high,low,DayName|..." (one segment per day, up to 7).
#
# 1. Change weather.forecast_dom below to your weather entity (3 places).
# 2. Put this in HA configuration.yaml (or a packages/ file), then restart HA / reload
# template entities. It creates sensor.va_weekly_forecast.
# 3. On the device, ${forecast_entity} defaults to sensor.va_weekly_forecast - override
# the substitution if you name it differently.
template:
- trigger:
- platform: homeassistant
event: start
- platform: time_pattern
minutes: "/30"
action:
- service: weather.get_forecasts
target:
entity_id: weather.forecast_dom
data:
type: daily
response_variable: fc
sensor:
- name: "VA weekly forecast"
unique_id: va_weekly_forecast
state: >
{% set ns = namespace(parts=[]) %}
{% for d in fc['weather.forecast_dom'].forecast[:7] %}
{% set hi = (d.temperature | round(0)) if d.temperature is not none else 0 %}
{% set lo = (d.templow | round(0)) if (d.templow is defined and d.templow is not none) else hi %}
{% set nm = as_timestamp(d.datetime) | timestamp_custom('%a', true) %}
{% set ns.parts = ns.parts + [d.condition ~ ',' ~ hi ~ ',' ~ lo ~ ',' ~ nm] %}
{% endfor %}
{{ ns.parts | join('|') }}
+541
View File
@@ -0,0 +1,541 @@
# Weather - carousel screen (base id 5). GENERATED by scripts/gen_weather.py.
# A dial of forecast days around the rim (knob scrolls), animated condition icon + colour-coded
# temperature in the centre, a glow that slides to the selected day. Forecast comes from a Home
# Assistant helper sensor (see base/screens/weather.ha-helper.yaml). Remove: drop from `files:`.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_present)[5] = true;'
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Weather";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Fahrenheit"; id(g_wopt_kind)[o] = 0; id(g_wopt_ptr)[o] = &id(g_wx_fahrenheit);
globals:
# --- forecast data ---
- id: g_wx_fahrenheit
type: bool
restore_value: yes
initial_value: 'false'
- id: g_wx_now
type: int
restore_value: no
initial_value: '-127'
- id: g_wx_state_cur
type: std::string
restore_value: no
- id: g_wx_cond
type: std::string[7]
restore_value: no
- id: g_wx_icon
type: std::string[7]
restore_value: no
- id: g_wx_name
type: std::string[7]
restore_value: no
- id: g_wx_hi
type: int[7]
restore_value: no
- id: g_wx_lo
type: int[7]
restore_value: no
- id: g_wx_n
type: int
restore_value: no
initial_value: '0'
- id: g_wx_day
type: int
restore_value: no
initial_value: '0'
# --- render cache ---
- id: g_wx_sel
type: std::string
restore_value: no
- id: g_wx_tc
type: int
restore_value: no
initial_value: '-127'
- id: g_wx_lastcond
type: std::string
restore_value: no
- id: g_wx_ikey
type: std::string
restore_value: no
- id: g_wx_gpos
type: float
restore_value: no
initial_value: '0.0'
font:
- file: "https://github.com/Templarian/MaterialDesign-Webfont/raw/v7.4.47/fonts/materialdesignicons-webfont.ttf"
id: icons_wx
size: 30
bpp: 4
glyphs: ["󰖙", "󰖔", "󰖕", "󰖑", "󰖝", "󰖓", "󰖖", "󰖗", "󰖘", "󰖐"]
image:
- id: img_wx_sun0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_sun7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/sun7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_partly7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/partly7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_moon7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/moon7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_cloud7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/cloud7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_rain7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/rain7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_storm7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/storm7.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow0.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow1.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow2.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow3.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow4.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow5.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow6.png"
type: RGB565
transparency: alpha_channel
- id: img_wx_snow7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/weather/snow7.png"
type: RGB565
transparency: alpha_channel
script:
- id: wx_render
then:
- lambda: |-
int d = id(g_wx_day);
bool has = (id(g_wx_n) > 0 && d < id(g_wx_n));
id(g_wx_sel) = has ? id(g_wx_cond)[d] : id(g_wx_state_cur);
id(g_wx_tc) = has ? id(g_wx_hi)[d] : id(g_wx_now);
- lvgl.label.update:
id: wx_day
text: !lambda |-
int d = id(g_wx_day);
if (id(g_wx_n) <= 0 || d >= id(g_wx_n)) return std::string("TODAY");
std::string s = id(g_wx_name)[d];
if (s == "Mon") return std::string("MONDAY");
if (s == "Tue") return std::string("TUESDAY");
if (s == "Wed") return std::string("WEDNESDAY");
if (s == "Thu") return std::string("THURSDAY");
if (s == "Fri") return std::string("FRIDAY");
if (s == "Sat") return std::string("SATURDAY");
if (s == "Sun") return std::string("SUNDAY");
return s;
- lvgl.label.update:
id: wx_htemp
text: !lambda |-
int d = id(g_wx_day);
bool f = id(g_wx_fahrenheit);
char u = f ? 'F' : 'C';
if (id(g_wx_n) <= 0 || d >= id(g_wx_n)) {
if (id(g_wx_now) > -100) { int n = id(g_wx_now); if (f) n = n*9/5+32; char b[12]; snprintf(b,sizeof(b),"%d°%c",n,u); return std::string(b); }
return std::string("--");
}
int hi = id(g_wx_hi)[d], lo = id(g_wx_lo)[d];
if (f) { hi = hi*9/5+32; lo = lo*9/5+32; }
char b[24]; snprintf(b,sizeof(b),"%d° / %d°",hi,lo); return std::string(b);
- lvgl.label.update:
id: wx_cond
text: !lambda |-
if (id(g_wx_n) <= 0) return std::string("Add the HA forecast helper");
std::string s = id(g_wx_sel);
if (s == "sunny") return std::string("Sunny");
if (s == "clear-night") return std::string("Clear");
if (s == "partlycloudy") return std::string("Partly Cloudy");
if (s == "cloudy") return std::string("Cloudy");
if (s == "fog") return std::string("Fog");
if (s == "rainy") return std::string("Rainy");
if (s == "pouring") return std::string("Heavy Rain");
if (s == "lightning" || s == "lightning-rainy") return std::string("Storm");
if (s == "snowy" || s == "snowy-rainy") return std::string("Snow");
if (s == "hail") return std::string("Hail");
if (s == "windy" || s == "windy-variant") return std::string("Windy");
if (s == "exceptional") return std::string("Severe");
return s;
- if:
condition: { lambda: 'return id(g_wx_sel) != id(g_wx_lastcond);' }
then:
- lambda: |-
id(g_wx_lastcond) = id(g_wx_sel);
std::string s = id(g_wx_sel), k;
if (s == "sunny") k = "sun";
else if (s == "clear-night") k = "moon";
else if (s == "partlycloudy") k = "partly";
else if (s == "rainy" || s == "pouring") k = "rain";
else if (s == "lightning" || s == "lightning-rainy") k = "storm";
else if (s == "snowy" || s == "snowy-rainy" || s == "hail") k = "snow";
else k = "cloud";
id(g_wx_ikey) = k;
- lvgl.animimg.stop: wx_a_sun
- lvgl.animimg.stop: wx_a_partly
- lvgl.animimg.stop: wx_a_moon
- lvgl.animimg.stop: wx_a_cloud
- lvgl.animimg.stop: wx_a_rain
- lvgl.animimg.stop: wx_a_storm
- lvgl.animimg.stop: wx_a_snow
- lvgl.widget.hide: wx_a_sun
- lvgl.widget.hide: wx_a_partly
- lvgl.widget.hide: wx_a_moon
- lvgl.widget.hide: wx_a_cloud
- lvgl.widget.hide: wx_a_rain
- lvgl.widget.hide: wx_a_storm
- lvgl.widget.hide: wx_a_snow
- if: { condition: { lambda: 'return id(g_wx_ikey) == "sun";' }, then: [ lvgl.widget.show: wx_a_sun, lvgl.animimg.start: wx_a_sun ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "partly";' }, then: [ lvgl.widget.show: wx_a_partly, lvgl.animimg.start: wx_a_partly ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "moon";' }, then: [ lvgl.widget.show: wx_a_moon, lvgl.animimg.start: wx_a_moon ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "cloud";' }, then: [ lvgl.widget.show: wx_a_cloud, lvgl.animimg.start: wx_a_cloud ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "rain";' }, then: [ lvgl.widget.show: wx_a_rain, lvgl.animimg.start: wx_a_rain ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "storm";' }, then: [ lvgl.widget.show: wx_a_storm, lvgl.animimg.start: wx_a_storm ] }
- if: { condition: { lambda: 'return id(g_wx_ikey) == "snow";' }, then: [ lvgl.widget.show: wx_a_snow, lvgl.animimg.start: wx_a_snow ] }
- lvgl.widget.update: { id: wx_glow, hidden: !lambda 'return id(g_wx_n) <= 0;' }
- lvgl.widget.update: { id: wx_d0, hidden: !lambda 'return 0 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(0 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(0 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d0, text: !lambda 'return id(g_wx_n) > 0 ? id(g_wx_icon)[0] : std::string("");' }
- lvgl.widget.update: { id: wx_d1, hidden: !lambda 'return 1 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(1 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(1 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d1, text: !lambda 'return id(g_wx_n) > 1 ? id(g_wx_icon)[1] : std::string("");' }
- lvgl.widget.update: { id: wx_d2, hidden: !lambda 'return 2 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(2 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(2 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d2, text: !lambda 'return id(g_wx_n) > 2 ? id(g_wx_icon)[2] : std::string("");' }
- lvgl.widget.update: { id: wx_d3, hidden: !lambda 'return 3 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(3 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(3 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d3, text: !lambda 'return id(g_wx_n) > 3 ? id(g_wx_icon)[3] : std::string("");' }
- lvgl.widget.update: { id: wx_d4, hidden: !lambda 'return 4 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(4 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(4 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d4, text: !lambda 'return id(g_wx_n) > 4 ? id(g_wx_icon)[4] : std::string("");' }
- lvgl.widget.update: { id: wx_d5, hidden: !lambda 'return 5 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(5 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(5 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d5, text: !lambda 'return id(g_wx_n) > 5 ? id(g_wx_icon)[5] : std::string("");' }
- lvgl.widget.update: { id: wx_d6, hidden: !lambda 'return 6 >= id(g_wx_n);', x: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(140.0f * sinf(6 * 6.2831853f / n));', y: !lambda 'int n=id(g_wx_n); if(n<1)n=1; return (int) lroundf(-140.0f * cosf(6 * 6.2831853f / n));' }
- lvgl.label.update: { id: wx_d6, text: !lambda 'return id(g_wx_n) > 6 ? id(g_wx_icon)[6] : std::string("");' }
- lvgl.label.update: { id: wx_htemp, text_color: 0xFFFFFF }
- if: { condition: { lambda: 'return id(g_wx_tc) > -100 && id(g_wx_tc) <= 0;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x7FB2FF } ] }
- if: { condition: { lambda: 'return id(g_wx_tc) > 0 && id(g_wx_tc) < 10;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x6FD0E8 } ] }
- if: { condition: { lambda: 'return id(g_wx_tc) >= 10 && id(g_wx_tc) < 18;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0x7FE6B0 } ] }
- if: { condition: { lambda: 'return id(g_wx_tc) >= 18 && id(g_wx_tc) < 25;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0xFFD061 } ] }
- if: { condition: { lambda: 'return id(g_wx_tc) >= 25;' }, then: [ lvgl.label.update: { id: wx_htemp, text_color: 0xFF7A4D } ] }
sensor:
- platform: homeassistant
id: wx_now_temp
entity_id: ${weather_entity}
attribute: temperature
on_value:
- lambda: 'id(g_wx_now) = (int) roundf(x);'
- script.execute: wx_render
text_sensor:
- platform: homeassistant
id: wx_now_state
entity_id: ${weather_entity}
on_value:
- lambda: 'id(g_wx_state_cur) = x;'
- script.execute: wx_render
- platform: homeassistant
id: wx_week
entity_id: ${forecast_entity}
on_value:
- lambda: |-
auto glyph = [](const std::string &s) -> std::string {
if (s == "sunny") return std::string("󰖙");
if (s == "clear-night") return std::string("󰖔");
if (s == "partlycloudy") return std::string("󰖕");
if (s == "fog") return std::string("󰖑");
if (s == "windy" || s == "windy-variant") return std::string("󰖝");
if (s == "lightning" || s == "lightning-rainy") return std::string("󰖓");
if (s == "pouring") return std::string("󰖖");
if (s == "rainy") return std::string("󰖗");
if (s == "snowy" || s == "snowy-rainy" || s == "hail") return std::string("󰖘");
return std::string("󰖐");
};
std::string s = x;
for (int i = 0; i < 7; i++) { id(g_wx_icon)[i] = ""; id(g_wx_cond)[i] = ""; }
int n = 0; size_t pos = 0;
while (n < 7) {
size_t bar = s.find('|', pos);
std::string seg = (bar == std::string::npos) ? s.substr(pos) : s.substr(pos, bar - pos);
size_t c1 = seg.find(',');
if (c1 != std::string::npos) {
size_t c2 = seg.find(',', c1 + 1);
size_t c3 = (c2 != std::string::npos) ? seg.find(',', c2 + 1) : std::string::npos;
if (c2 != std::string::npos && c3 != std::string::npos) {
std::string cond = seg.substr(0, c1);
id(g_wx_cond)[n] = cond;
id(g_wx_icon)[n] = glyph(cond);
id(g_wx_hi)[n] = atoi(seg.substr(c1 + 1, c2 - c1 - 1).c_str());
id(g_wx_lo)[n] = atoi(seg.substr(c2 + 1, c3 - c2 - 1).c_str());
id(g_wx_name)[n] = seg.substr(c3 + 1);
n++;
}
}
if (bar == std::string::npos) break;
pos = bar + 1;
}
id(g_wx_n) = n;
if (id(g_wx_day) >= n) id(g_wx_day) = 0;
- script.execute: wx_render
interval:
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==5;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_knob_capture) = true; id(g_wx_day) = 0; id(g_wx_gpos) = 0.0f; id(g_wx_lastcond) = "";'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_weather, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_weather, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_weather, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_weather, animation: move_bottom, time: 250ms }] }
- script.execute: wx_render
- interval: 120ms
then:
- if:
condition: { lambda: 'return id(g_base)==5 && id(g_knob_delta) != 0;' }
then:
- lambda: |-
id(g_wx_day) += id(g_knob_delta); id(g_knob_delta) = 0;
int mx = (id(g_wx_n) > 0) ? id(g_wx_n) - 1 : 0;
if (id(g_wx_day) < 0) id(g_wx_day) = 0;
if (id(g_wx_day) > mx) id(g_wx_day) = mx;
- script.execute: wx_render
- interval: 30ms
then:
- if:
condition: { lambda: 'return id(g_base)==5;' }
then:
- lambda: |-
float p = id(g_wx_gpos), tgt = (float) id(g_wx_day);
if (fabsf(tgt - p) > 0.001f) { // only while actually sliding
int n = id(g_wx_n); if (n < 1) n = 1;
p += (tgt - p) * 0.35f;
if (fabsf(tgt - p) < 0.02f) p = tgt;
id(g_wx_gpos) = p;
float rx = 140.0f * sinf(p * 6.2831853f / n);
float ry = -140.0f * cosf(p * 6.2831853f / n);
lv_obj_set_pos(id(wx_glow), 180 + (int) lroundf(rx) - 28, 180 + (int) lroundf(ry) - 28);
}
lvgl:
pages:
- id: page_weather
bg_color: 0x0B1020
widgets:
- obj: { id: wx_bg, align: CENTER, x: 0, y: 0, width: 360, height: 360, radius: 0, bg_color: 0x1C2838, bg_opa: COVER, border_width: 0 }
- obj: { id: wx_ring_out, align: CENTER, width: 348, height: 348, radius: 174, bg_opa: TRANSP, border_width: 2, border_color: 0x33455C, border_opa: 55%, clickable: false, scrollable: false }
- obj: { id: wx_glow, x: 152, y: 12, width: 56, height: 56, radius: 28, bg_color: 0xCAD6E8, bg_opa: 30%, border_width: 0, hidden: true }
- label: { id: wx_d0, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d1, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d2, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d3, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d4, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d5, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- label: { id: wx_d6, align: CENTER, x: 0, y: 0, text: "", text_font: icons_wx, text_color: 0xDCE6F2, hidden: true }
- animimg: { id: wx_a_sun, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_sun0, img_wx_sun1, img_wx_sun2, img_wx_sun3, img_wx_sun4, img_wx_sun5, img_wx_sun6, img_wx_sun7 ] }
- animimg: { id: wx_a_partly, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_partly0, img_wx_partly1, img_wx_partly2, img_wx_partly3, img_wx_partly4, img_wx_partly5, img_wx_partly6, img_wx_partly7 ] }
- animimg: { id: wx_a_moon, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_moon0, img_wx_moon1, img_wx_moon2, img_wx_moon3, img_wx_moon4, img_wx_moon5, img_wx_moon6, img_wx_moon7 ] }
- animimg: { id: wx_a_cloud, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_cloud0, img_wx_cloud1, img_wx_cloud2, img_wx_cloud3, img_wx_cloud4, img_wx_cloud5, img_wx_cloud6, img_wx_cloud7 ] }
- animimg: { id: wx_a_rain, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_rain0, img_wx_rain1, img_wx_rain2, img_wx_rain3, img_wx_rain4, img_wx_rain5, img_wx_rain6, img_wx_rain7 ] }
- animimg: { id: wx_a_storm, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_storm0, img_wx_storm1, img_wx_storm2, img_wx_storm3, img_wx_storm4, img_wx_storm5, img_wx_storm6, img_wx_storm7 ] }
- animimg: { id: wx_a_snow, align: CENTER, x: 0, y: -52, duration: 1400ms, repeat_count: 65535, auto_start: false, hidden: true, src: [ img_wx_snow0, img_wx_snow1, img_wx_snow2, img_wx_snow3, img_wx_snow4, img_wx_snow5, img_wx_snow6, img_wx_snow7 ] }
- label: { id: wx_day, align: CENTER, x: 0, y: -10, text: "", text_font: font_artist, text_color: 0x9AA6B2 }
- label: { id: wx_htemp, align: CENTER, x: 0, y: 24, text: "--", text_font: montserrat_34, text_color: 0xFFFFFF }
- label: { id: wx_cond, align: CENTER, x: 0, y: 62, text: "", text_font: font_med, text_color: 0xCFE0F2 }
+118
View File
@@ -0,0 +1,118 @@
# ═════════════════════════════════════════════════════════════════════════════
# DEMO WATCHFACE - a minimal, heavily-commented template. Copy this file to
# base/watchfaces/<your-name>.yaml, change the 4 spots marked >>> EDIT <<< and
# you have your own home-screen look.
#
# How watchfaces work (same idea as the carousel screens):
# - The home base (the clock area) can show one of several "faces".
# - "Classic" is built into core and is always the fallback (face id 0).
# - Every OTHER face is an optional file like this one, switched on by adding it
# to the `files:` list in your thin config, and chosen at runtime in
# Settings > Home > Watchface (turn the knob to preview, tap to keep).
# - Core can't reference a page that might not be compiled in, so each face
# SHOWS ITSELF: core just raises a flag (g_wf_req) and the active face's own
# interval calls lvgl.page.show. Same trick the screens use (g_nav_req).
#
# Everything you can read from core (no extra entities needed):
# id(ha_time).now() - current time (ESPTime) -> strftime("%H"), etc.
# id(wx_temp).state - outdoor temperature (float) + .has_state()
# id(room_temp).state - room temperature (float)
# id(room_hum).state - room humidity (float)
# id(g_nf_glyph) - weather icon glyph (std::string, MDI in font icons34)
# id(g_nf_cond) - weather condition text, UPPERCASE
# id(g_batt) / id(g_charging) - battery % (int, -1 = unknown) / charging (bool)
# id(g_show_weather) / id(g_show_climate) - the Display toggles (bool)
# Fonts already provided by core: montserrat_18/48, font_med, font_artist,
# font_sel, icons18, icons34. Define your own under `font:` if you want more.
# ═════════════════════════════════════════════════════════════════════════════
# ── 1. REGISTER the face at boot ──────────────────────────────────────────────
# Pick a UNIQUE id (0 = Classic, 1 = Neon are taken; use 2, 3, ...). The name is
# what shows in the picker. Priority 655 runs after core but before the boot
# finalize that validates the saved choice.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_wf_present)[2] = true; id(g_wf_name)[2] = "Demo";' # >>> EDIT <<< id + name
# ── 2. (optional) your own fonts ──────────────────────────────────────────────
# This demo reuses core's fonts, so nothing here. Add a `font:` block if needed.
# ── 3. THE PAGE ───────────────────────────────────────────────────────────────
# A full-screen page. Give it a unique id (page_home_<name>) and unique widget ids.
lvgl:
pages:
- id: page_home_demo # >>> EDIT <<< unique page id
bg_color: 0x000000
widgets:
- label: # big centered clock
id: dwf_time
align: CENTER
y: -16
text: "00:00"
text_font: montserrat_48
text_color: 0xFFFFFF
- label: # small date under it
id: dwf_date
align: CENTER
y: 30
text: ""
text_font: font_med
text_color: 0x6f9a92
# ── 4. INTERVALS: self-show + refresh ─────────────────────────────────────────
interval:
# Self-show: core raises g_wf_req when it wants the home base shown. If we are
# the active face, show OUR page (and render once so it isn't blank), then clear
# the flag. Keep the id check (== 2) matching the one you registered above.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 2;' } # >>> EDIT <<< id
then:
- lambda: 'id(g_wf_req) = false;'
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_demo, animation: move_bottom, time: 250ms }] }
- script.execute: dwf_render
# Refresh once a second while we are the active home face.
- interval: 1s
then:
- if:
condition: { lambda: 'return id(g_watchface) == 2 && id(g_base) == 0;' } # >>> EDIT <<< id
then: [ script.execute: dwf_render ]
# ── 5. RENDER: fill your widgets from core's data ─────────────────────────────
script:
- id: dwf_render # >>> EDIT <<< unique script id
then:
# Time - HH:MM (24h). Use "%I:%M %p" for 12-hour.
- lvgl.label.update:
id: dwf_time
text: !lambda 'return id(ha_time).now().is_valid() ? id(ha_time).now().strftime("%H:%M") : std::string("00:00");'
# Date - e.g. "Mon 19 Jun"
- lvgl.label.update:
id: dwf_date
text: !lambda |-
auto t = id(ha_time).now();
if (!t.is_valid()) return std::string("");
static const char* d[8] = {"","Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
return day + t.strftime(" %d %b");
# ── Want more? Uncomment and add widgets above, then fill them here. ──
# Example: show room temperature (respecting the "Show climate" toggle)
# - lvgl.label.update:
# id: dwf_temp
# text: !lambda |-
# if (!id(g_show_climate) || !id(room_temp).has_state()) return std::string("");
# char b[12]; snprintf(b, sizeof(b), "%.1f\xC2\xB0", id(room_temp).state); return std::string(b);
# Example: weather icon (font icons34) + outdoor temp
# - lvgl.label.update: { id: dwf_wx, text: !lambda 'return id(g_show_weather) ? id(g_nf_glyph) : std::string("");' }
# ── 6. TURN IT ON ─────────────────────────────────────────────────────────────
# Add this file to the `files:` list in your thin config:
# - base/watchfaces/demo.yaml
# then rebuild and pick "Demo" in Settings > Home > Watchface.
+191
View File
@@ -0,0 +1,191 @@
# ═════════════════════════════════════════════════════════════════════════════
# MINECRAFT watchface (optional, id 3). A blocky scene with a day/night cycle:
# sky + textured grass/dirt ground, the sun by day / moon + stars by night, pixel
# flowers on the grass, a blocky pixel font for the time (classic drop shadow),
# outdoor temp, and a Minecraft heart bar (HUD) for the battery.
#
# PERFORMANCE: the per-second tick is just a tiny lambda; the heavy redraws (the big
# pixel digits, the day/night swap) only run when the minute actually changes or the
# day/night state flips - so it stays light. No animated clouds.
#
# Assets (font + sprites) are fetched from this repo's /main/ raw GitHub URLs at compile time.
# Watchface contract: see base/watchfaces/demo.yaml.
# ═════════════════════════════════════════════════════════════════════════════
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_wf_present)[3] = true; id(g_wf_name)[3] = "Minecraft";'
globals:
- { id: g_mc_last_min, type: int, restore_value: no, initial_value: '-1' }
- { id: g_mc_last_night, type: int, restore_value: no, initial_value: '-1' }
- { id: g_mc_upd_min, type: bool, restore_value: no, initial_value: 'false' }
- { id: g_mc_upd_night, type: bool, restore_value: no, initial_value: 'false' }
font:
- file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/fonts/pixel-font.ttf
id: mc_font_big
size: 78
glyphs: "0123456789:"
- file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/fonts/pixel-font.ttf
id: mc_font_sm
size: 26
glyphs: " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" # date only (uppercased) - no GF_Latin_Core, avoids missing-glyph warnings
image:
- { id: img_mc_ground, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/ground.png, type: RGB565 }
- { id: img_mc_sun, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/sun.png, type: RGB565, transparency: alpha_channel }
- { id: img_mc_moon, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/moon.png, type: RGB565, transparency: alpha_channel }
- { id: img_mc_flower, file: https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/minecraft/flower.png, type: RGB565, transparency: alpha_channel }
lvgl:
pages:
- id: page_home_mc
bg_color: 0x000000
widgets:
# sky (recoloured day/night) - full screen, behind everything
- obj: { id: mc_sky, x: 0, y: 0, width: 360, height: 360, bg_color: 0x5A9BD4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false }
# stars (night only)
- obj: { id: mc_star1, x: 44, y: 64, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
- obj: { id: mc_star2, x: 120, y: 42, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
- obj: { id: mc_star3, x: 250, y: 48, width: 6, height: 6, bg_color: 0xEAECF5, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
- obj: { id: mc_star4, x: 70, y: 116, width: 5, height: 5, bg_color: 0xC9D0E4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
- obj: { id: mc_star5, x: 300, y: 120, width: 5, height: 5, bg_color: 0xC9D0E4, bg_opa: COVER, radius: 0, border_width: 0, clickable: false, scrollable: false, hidden: true }
# textured ground (flush to the bottom edge)
- image: { id: mc_ground, src: img_mc_ground, align: BOTTOM_MID, y: 0 }
# pixel flowers standing on the grass
- image: { id: mc_flower1, src: img_mc_flower, x: 56, y: 200 }
- image: { id: mc_flower2, src: img_mc_flower, x: 282, y: 206 }
# sun by day / moon by night (top-right)
- image: { id: mc_sun, src: img_mc_sun, align: TOP_RIGHT, x: -38, y: 38 }
- image: { id: mc_moon, src: img_mc_moon, align: TOP_RIGHT, x: -38, y: 38, hidden: true }
- label: { id: mc_sun_temp, align: TOP_RIGHT, x: -52, y: 100, text: "--°", text_font: font_artist, text_color: 0x16314A }
# time: dark shadow behind + white in front (the Minecraft look)
- label: { id: mc_time_sh, align: CENTER, x: 3, y: -41, text: "00:00", text_font: mc_font_big, text_color: 0x1A2A40 }
- label: { id: mc_time, align: CENTER, x: 0, y: -44, text: "00:00", text_font: mc_font_big, text_color: 0xFFFFFF }
- label: { id: mc_date, align: CENTER, x: 0, y: 14, text: "", text_font: mc_font_sm, text_color: 0xEAF2FB }
# battery as a Minecraft heart bar (HUD), low: 5 dark "empty" slots + red fill on top
- label: { id: mc_hearts_bg, align: BOTTOM_LEFT, x: 128, y: -16, text: "󰋑󰋑󰋑󰋑󰋑", text_font: icons18, text_color: 0x3A1614 }
- label: { id: mc_hearts, align: BOTTOM_LEFT, x: 128, y: -16, text: "", text_font: icons18, text_color: 0xE2453F }
# timer badge (active timer - minutes left)
- label:
id: mc_timer
align: TOP_MID
y: 6
text: ""
text_font: montserrat_18
text_color: 0xFFFFFF
bg_color: 0xFF3B30
bg_opa: COVER
radius: 12
pad_left: 11
pad_right: 11
pad_top: 4
pad_bottom: 4
hidden: true
interval:
- interval: 50ms # self-show when core requests the home base
then:
- if:
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 3;' }
then:
- lambda: 'id(g_wf_req) = false; id(g_mc_last_min) = -1; id(g_mc_last_night) = -1;' # force a full redraw on entry
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_mc, animation: move_bottom, time: 250ms }] }
- script.execute: mc_render
- interval: 1s # cheap tick - only redraws on a real change
then:
- if:
condition: { lambda: 'return id(g_watchface) == 3 && id(g_base) == 0;' }
then: [ script.execute: mc_render ]
script:
- id: mc_render
then:
# Decide what actually changed (so the second-by-second tick is nearly free)
- lambda: |-
id(g_mc_upd_min) = false; id(g_mc_upd_night) = false;
auto t = id(ha_time).now();
if (!t.is_valid()) return;
int mn = t.hour * 60 + t.minute;
if (mn != id(g_mc_last_min)) { id(g_mc_last_min) = mn; id(g_mc_upd_min) = true; }
int night = (t.hour < 6 || t.hour >= 20) ? 1 : 0;
if (night != id(g_mc_last_night)) { id(g_mc_last_night) = night; id(g_mc_upd_night) = true; }
# Minute changed -> redraw time / date / temp / hearts
- if:
condition: { lambda: 'return id(g_mc_upd_min);' }
then:
- lvgl.label.update: { id: mc_time, text: !lambda 'return id(ha_time).now().strftime("%H:%M");' }
- lvgl.label.update: { id: mc_time_sh, text: !lambda 'return id(ha_time).now().strftime("%H:%M");' }
- lvgl.label.update:
id: mc_date
text: !lambda |-
auto t = id(ha_time).now();
static const char* d[8] = {"","SUN","MON","TUE","WED","THU","FRI","SAT"};
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
std::string s = day + t.strftime(" %d %b");
for (auto &c : s) { if (c >= 'a' && c <= 'z') c -= 32; }
return s;
- if:
condition: { lambda: 'return id(g_show_weather);' }
then:
- lvgl.widget.show: mc_sun_temp
- lvgl.label.update:
id: mc_sun_temp
text: !lambda |-
if (!id(wx_temp).has_state()) return std::string("--°");
char b[10]; snprintf(b, sizeof(b), "%d°", (int) roundf(id(wx_temp).state)); return std::string(b);
else: [ lvgl.widget.hide: mc_sun_temp ]
- lvgl.label.update:
id: mc_hearts
text: !lambda |-
int p = id(g_batt);
int n = (p < 0) ? 5 : (p + 10) / 20; if (n < 0) n = 0; if (n > 5) n = 5;
std::string s; for (int i = 0; i < n; i++) s += "󰋑";
return s;
# Day/night flipped -> swap sky tint, sun<->moon, stars, temp colour
- if:
condition: { lambda: 'return id(g_mc_upd_night);' }
then:
- if:
condition: { lambda: 'return id(g_mc_last_night) == 1;' }
then:
- lvgl.widget.update: { id: mc_sky, bg_color: 0x101A33 }
- lvgl.widget.hide: mc_sun
- lvgl.widget.show: mc_moon
- lvgl.widget.show: mc_star1
- lvgl.widget.show: mc_star2
- lvgl.widget.show: mc_star3
- lvgl.widget.show: mc_star4
- lvgl.widget.show: mc_star5
- lvgl.label.update: { id: mc_sun_temp, text_color: 0xC8D2E2 }
else:
- lvgl.widget.update: { id: mc_sky, bg_color: 0x5A9BD4 }
- lvgl.widget.show: mc_sun
- lvgl.widget.hide: mc_moon
- lvgl.widget.hide: mc_star1
- lvgl.widget.hide: mc_star2
- lvgl.widget.hide: mc_star3
- lvgl.widget.hide: mc_star4
- lvgl.widget.hide: mc_star5
- lvgl.label.update: { id: mc_sun_temp, text_color: 0x16314A }
# Timer badge (active timer - minutes left), kept responsive (outside the minute gate)
- if:
condition: { lambda: 'return id(g_timer_active);' }
then:
- lvgl.widget.show: mc_timer
- lvgl.label.update:
id: mc_timer
text: !lambda |-
int s = id(g_timer_secs); if (s < 0) s = 0;
int m = (s + 59) / 60;
char b[24]; snprintf(b, sizeof(b), " %d min", m);
return std::string(b);
else:
- lvgl.widget.hide: mc_timer
+245
View File
@@ -0,0 +1,245 @@
# ─────────────────────────────────────────────────────────────────────────────
# NEON watchface (optional home face). Add to the thin config's files: list to
# enable; the picker (Settings > Home > Watchface) then offers it. Watchface id 1.
#
# A full-screen home page with huge two-tone digits, neon rim rings, weather +
# room/battery stats. It reuses core's clock/weather/room/battery sensors and the
# g_show_weather / g_show_climate display toggles - no extra entities needed.
#
# Contract (mirrors the screen carousel): self-registers in the watchface registry
# at boot, renders itself while active, and self-shows its page when core requests
# the home base (core can't reference an optional page id, hence the g_wf_req flag).
# ─────────────────────────────────────────────────────────────────────────────
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_wf_present)[1] = true; id(g_wf_name)[1] = "Neon";'
globals:
- { id: g_nf_last_min, type: int, restore_value: no, initial_value: '-1' } # perf: only redraw on minute change
- { id: g_nf_upd_min, type: bool, restore_value: no, initial_value: 'false' }
font:
# Huge bold digits - only 0-9 needed, so it stays tiny in flash
- file: { type: gfonts, family: "Roboto", weight: 900 }
id: font_neon
size: 108
glyphs: "0123456789"
lvgl:
pages:
- id: page_home_neon
bg_color: 0x000000
widgets:
- obj: # outer neon ring at the rim
id: nf_ring_out
align: CENTER
width: 352
height: 352
radius: 176
bg_opa: TRANSP
border_width: 4
border_color: 0xFF2E88
border_opa: 60%
clickable: false
scrollable: false
- obj: # second neon ring, also hugging the rim (interior left free for content)
id: nf_ring_in
align: CENTER
width: 320
height: 320
radius: 160
bg_opa: TRANSP
border_width: 4
border_color: 0x9A5BFF
border_opa: 55%
clickable: false
scrollable: false
- label: # weather icon
id: nf_wx_ic
align: CENTER
x: -34
y: -103
text: "󰖕"
text_font: icons34
text_color: 0xBFD2DA
- label: # outdoor temperature
id: nf_wx_temp
align: CENTER
x: 26
y: -106
text: "--°"
text_font: font_sel
text_color: 0xFFFFFF
- label: # condition text (UPPERCASE)
id: nf_wx_cond
align: CENTER
y: -76
text: ""
text_font: font_artist
text_color: 0x8A93A0
- label: # HOURS
id: nf_hh
align: CENTER
x: -70
y: -4
text: "00"
text_font: font_neon
text_color: 0xFF2E88
- label: # MINUTES
id: nf_mm
align: CENTER
x: 70
y: -4
text: "00"
text_font: font_neon
text_color: 0x9A5BFF
- label: # date
id: nf_date
align: CENTER
y: 70
text: ""
text_font: font_med
text_color: 0x6f9a92
- label: # room temperature icon
id: nf_ct_ic
align: CENTER
x: -60
y: 99
text: "󰔏"
text_font: icons18
text_color: 0xFF8A5C
- label:
id: nf_ct_val
align: CENTER
x: -32
y: 99
text: "--"
text_font: font_artist
text_color: 0x9AA3B0
- label: # room humidity icon
id: nf_hum_ic
align: CENTER
x: 16
y: 99
text: "󰖎"
text_font: icons18
text_color: 0x3FA9D6
- label:
id: nf_hum_val
align: CENTER
x: 42
y: 99
text: "--"
text_font: font_artist
text_color: 0x9AA3B0
- label: # battery (glyph + %), mirrored from core's lbl_batt (same LVGL symbols)
id: nf_bt
align: CENTER
y: 126
text: ""
text_font: montserrat_18
text_color: 0x6f9a92
- label: # timer badge (active timer - mins left), mirrors the classic face
id: nf_timer
align: TOP_MID
y: 22
text: ""
text_font: montserrat_18
text_color: 0xFFFFFF
bg_color: 0xFF3B30
bg_opa: COVER
radius: 14
pad_left: 12
pad_right: 12
pad_top: 5
pad_bottom: 5
hidden: true
interval:
# Self-show: core sets g_wf_req when it wants the home base shown; we own page_home_neon.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_wf_req) && id(g_watchface) == 1;' }
then:
- lambda: 'id(g_wf_req) = false; id(g_nf_last_min) = -1;' # force a full redraw on entry
# slide in matching the carousel direction (like Classic / the screens) - no laggy fade
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_home_neon, animation: move_bottom, time: 250ms }] }
- script.execute: neon_render
# Refresh while this is the active home face
- interval: 1s
then:
- if:
condition: { lambda: 'return id(g_watchface) == 1 && id(g_base) == 0;' }
then: [ script.execute: neon_render ]
script:
- id: neon_render
then:
# Cheap tick: figure out if the minute actually changed (heavy redraws only then)
- lambda: |-
id(g_nf_upd_min) = false;
auto t = id(ha_time).now();
if (!t.is_valid()) return;
int mn = t.hour * 60 + t.minute;
if (mn != id(g_nf_last_min)) { id(g_nf_last_min) = mn; id(g_nf_upd_min) = true; }
- if:
condition: { lambda: 'return id(g_nf_upd_min);' }
then:
- lvgl.label.update: { id: nf_hh, text: !lambda 'return id(ha_time).now().strftime("%H");' }
- lvgl.label.update: { id: nf_mm, text: !lambda 'return id(ha_time).now().strftime("%M");' }
- lvgl.label.update: { id: nf_wx_ic, text: !lambda 'return id(g_nf_glyph).empty() ? std::string("󰖕") : id(g_nf_glyph);' }
- lvgl.label.update: { id: nf_wx_cond, text: !lambda 'return id(g_nf_cond);' }
- lvgl.label.update:
id: nf_wx_temp
text: !lambda |-
if (!id(wx_temp).has_state()) return std::string("--°");
char b[10]; snprintf(b, sizeof(b), "%d°", (int) roundf(id(wx_temp).state)); return std::string(b);
- lvgl.label.update:
id: nf_date
text: !lambda |-
auto t = id(ha_time).now();
static const char* d[8] = {"","Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
std::string day = (t.day_of_week >= 1 && t.day_of_week <= 7) ? std::string(d[t.day_of_week]) : std::string("");
return t.strftime("%d %b") + " " + day;
- lvgl.label.update:
id: nf_ct_val
text: !lambda 'if (!id(room_temp).has_state()) return std::string("--"); char b[12]; snprintf(b, sizeof(b), "%.1f°", id(room_temp).state); return std::string(b);'
- lvgl.label.update:
id: nf_hum_val
text: !lambda 'if (!id(room_hum).has_state()) return std::string("--"); char b[8]; snprintf(b, sizeof(b), "%d%%", (int) roundf(id(room_hum).state)); return std::string(b);'
# Battery: mirror core's lbl_batt text + colour
- lambda: 'lv_label_set_text(id(nf_bt), lv_label_get_text(id(lbl_batt)));'
- if:
condition: { lambda: 'return id(g_charging);' }
then: [ lvgl.label.update: { id: nf_bt, text_color: 0x35c759 } ]
else: [ lvgl.label.update: { id: nf_bt, text_color: 0x6f9a92 } ]
# Weather / climate visibility obey the Display toggles
- if:
condition: { lambda: 'return id(g_show_weather);' }
then: [ lvgl.widget.show: nf_wx_ic, lvgl.widget.show: nf_wx_temp, lvgl.widget.show: nf_wx_cond ]
else: [ lvgl.widget.hide: nf_wx_ic, lvgl.widget.hide: nf_wx_temp, lvgl.widget.hide: nf_wx_cond ]
- if:
condition: { lambda: 'return id(g_show_climate);' }
then: [ lvgl.widget.show: nf_ct_ic, lvgl.widget.show: nf_ct_val, lvgl.widget.show: nf_hum_ic, lvgl.widget.show: nf_hum_val ]
else: [ lvgl.widget.hide: nf_ct_ic, lvgl.widget.hide: nf_ct_val, lvgl.widget.hide: nf_hum_ic, lvgl.widget.hide: nf_hum_val ]
# Timer badge - kept responsive (cheap) so it appears/updates promptly
- if:
condition: { lambda: 'return id(g_timer_active);' }
then:
- lvgl.widget.show: nf_timer
- lvgl.label.update:
id: nf_timer
text: !lambda |-
int s = id(g_timer_secs); if (s < 0) s = 0;
int m = (s + 59) / 60;
char b[24]; snprintf(b, sizeof(b), " %d min", m);
return std::string(b);
else:
- lvgl.widget.hide: nf_timer