# 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