diff --git a/esphome/el512-panel.yaml b/esphome/el512-panel.yaml index 35a1c89..1230ead 100644 --- a/esphome/el512-panel.yaml +++ b/esphome/el512-panel.yaml @@ -62,6 +62,9 @@ globals: - id: np_updated type: uint32_t initial_value: "0" + - id: plex_updated + type: uint32_t + initial_value: "0" # Themed pages (Cyberpunk, KonoSuba, Steins;Gate): their fonts and the home data they show. - id: cp_fonts type: el512::cyber::Fonts @@ -141,6 +144,8 @@ font: size: 18 glyphs: - "\U000F054B" # umbrella-outline + - "\U000F040A" # play + - "\U000F03E4" # pause # Cyberpunk pages - file: "gfonts://Orbitron@700" id: f_cp_title @@ -238,6 +243,8 @@ text_sensor: id: plex_streams entity_id: sensor.el512_plex_streams attribute: streams + on_value: + - lambda: id(plex_updated) = millis(); - platform: homeassistant id: plex_update entity_id: update.plex_media_server_mangavalkserver @@ -686,11 +693,43 @@ display: it.print(326, 140, id(f_m), TextAlign::CENTER, online ? "Nobody is watching" : "Server offline"); return; } + // Per stream: play/pause, who, time, title and a progress bar + // (who|title|state|pos|dur; the server-sensor fallback has no state). int y = 50; for (auto &s : streams) { if (y > 256 - 48) break; - it.print(152, y, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), el512::field(s, 0), 352).c_str()); - it.print(152, y + 18, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), el512::field(s, 1), 352).c_str()); + auto f = el512::split(s, '|'); + f.resize(5); + const bool playing = f[2] == "playing", paused = f[2] == "paused"; + const int dur = atoi(f[4].c_str()); + int pos = atoi(f[3].c_str()); + if (playing) pos += (int) ((millis() - id(plex_updated)) / 1000); + pos = std::min(pos, dur); + int x = 152; + if (playing || paused) { + it.print(150, y - 1, id(icons_s), TextAlign::TOP_LEFT, paused ? "\U000F03E4" : "\U000F040A"); + x = 172; + } + std::string when; + if (dur > 0) when = el512::mmss(pos) + " / " + el512::mmss(dur); + int tw = 0, tx, ty, th; + if (!when.empty()) { + it.get_text_bounds(0, 0, when.c_str(), id(f_s), TextAlign::TOP_LEFT, &tx, &ty, &tw, &th); + it.print(504, y, id(f_s), TextAlign::TOP_RIGHT, when.c_str()); + } + it.print(x, y, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), f[0], 504 - x - tw - 10).c_str()); + it.print(152, y + 17, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), f[1], 352).c_str()); + if (dur > 0) { + const int w = 352 * pos / dur; + if (paused) { + // Paused: hollow bar with a dotted fill. + it.rectangle(152, y + 42, 352, 5); + for (int px = 154; px < 152 + w; px += 2) it.draw_pixel_at(px, y + 44); + } else { + it.horizontal_line(152, y + 44, 352); + it.filled_rectangle(152, y + 42, w, 5); + } + } y += 50; } diff --git a/home_assistant/packages/el512_panel.yaml b/home_assistant/packages/el512_panel.yaml index 5d86338..66de913 100644 --- a/home_assistant/packages/el512_panel.yaml +++ b/home_assistant/packages/el512_panel.yaml @@ -112,18 +112,43 @@ template: | map('string') | map('replace', '|', '/') | join('|') }} {%- endif -%} - # Plex: the server sensor has one attribute per stream ("user - client": "title"). - - sensor: + # Plex streams with play/pause state and position, from the Plex integration's + # media players (media_player.plex_*); position advances every 5 s. Without + # any Plex players, falls back to the server sensor, which has one attribute + # per stream ("user - client": "title") but no state or position. + - triggers: + - trigger: time_pattern + seconds: "/5" + sensor: - name: EL512 Plex streams unique_id: el512_plex_streams icon: mdi:plex state: "{{ states('sensor.mangavalkserver') }}" attributes: - # "user - client|title;..." + # "who|title|playing or paused|position s|duration s;..." + # (fallback: "user - client|title;...") streams: >- - {%- set s = states.sensor.mangavalkserver -%} {%- set ns = namespace(out=[]) -%} - {%- if s -%} + {%- for p in states.media_player + if p.entity_id.startswith('media_player.plex_') and p.state in ['playing', 'paused'] -%} + {%- set a = p.attributes -%} + {%- set pos = a.media_position | default(0) -%} + {%- if p.state == 'playing' and a.media_position_updated_at is defined and a.media_position_updated_at -%} + {%- set pos = pos + (now() - as_datetime(a.media_position_updated_at)).total_seconds() -%} + {%- endif -%} + {%- set dur = a.media_duration | default(0) -%} + {%- set pos = [pos, dur] | min if dur else pos -%} + {%- set title = a.media_title | default('') -%} + {%- if a.media_series_title | default('') -%} + {%- set title = a.media_series_title ~ ' - ' ~ title -%} + {%- endif -%} + {%- set who = a.username | default(a.friendly_name | default(p.entity_id)) -%} + {%- set ns.out = ns.out + [[who, title, p.state, pos | int, dur | int] + | map('string') | map('replace', '|', '/') | map('replace', ';', ',') + | join('|')] -%} + {%- endfor -%} + {%- set s = states.sensor.mangavalkserver -%} + {%- if not ns.out and s -%} {%- for k, v in s.attributes.items() if k not in ['unit_of_measurement', 'friendly_name', 'icon', 'device_class', 'state_class'] -%} {%- set ns.out = ns.out + [(k | replace('|', '/') | replace(';', ',')) ~ '|' ~