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

+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