266 lines
9.3 KiB
YAML
266 lines
9.3 KiB
YAML
# 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
|