Plex page: play/pause state and progress per stream

The EL512 Plex streams sensor now reads the Plex media players
(media_player.plex_*) every 5 s: who, title, playing/paused, position and
duration, falling back to the server sensor's list without them. The Plex
page shows a play/pause icon, position / duration and a thin progress bar
per stream (dotted while paused), counting on between updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-19 00:03:44 +02:00
1 parent 1e8a9a5958
commit 921a4c3aa6
2 files changed
+71 -7

No files matched your search

+41 -2
View File
@@ -62,6 +62,9 @@ globals:
- id: np_updated - id: np_updated
type: uint32_t type: uint32_t
initial_value: "0" 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. # Themed pages (Cyberpunk, KonoSuba, Steins;Gate): their fonts and the home data they show.
- id: cp_fonts - id: cp_fonts
type: el512::cyber::Fonts type: el512::cyber::Fonts
@@ -141,6 +144,8 @@ font:
size: 18 size: 18
glyphs: glyphs:
- "\U000F054B" # umbrella-outline - "\U000F054B" # umbrella-outline
- "\U000F040A" # play
- "\U000F03E4" # pause
# Cyberpunk pages # Cyberpunk pages
- file: "gfonts://Orbitron@700" - file: "gfonts://Orbitron@700"
id: f_cp_title id: f_cp_title
@@ -238,6 +243,8 @@ text_sensor:
id: plex_streams id: plex_streams
entity_id: sensor.el512_plex_streams entity_id: sensor.el512_plex_streams
attribute: streams attribute: streams
on_value:
- lambda: id(plex_updated) = millis();
- platform: homeassistant - platform: homeassistant
id: plex_update id: plex_update
entity_id: update.plex_media_server_mangavalkserver 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"); it.print(326, 140, id(f_m), TextAlign::CENTER, online ? "Nobody is watching" : "Server offline");
return; 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; int y = 50;
for (auto &s : streams) { for (auto &s : streams) {
if (y > 256 - 48) break; 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()); auto f = el512::split(s, '|');
it.print(152, y + 18, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), el512::field(s, 1), 352).c_str()); 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; y += 50;
} }
+30 -5
View File
@@ -112,18 +112,43 @@ template:
| map('string') | map('replace', '|', '/') | join('|') }} | map('string') | map('replace', '|', '/') | join('|') }}
{%- endif -%} {%- endif -%}
# Plex: the server sensor has one attribute per stream ("user - client": "title"). # Plex streams with play/pause state and position, from the Plex integration's
- sensor: # 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 - name: EL512 Plex streams
unique_id: el512_plex_streams unique_id: el512_plex_streams
icon: mdi:plex icon: mdi:plex
state: "{{ states('sensor.mangavalkserver') }}" state: "{{ states('sensor.mangavalkserver') }}"
attributes: attributes:
# "user - client|title;..." # "who|title|playing or paused|position s|duration s;..."
# (fallback: "user - client|title;...")
streams: >- streams: >-
{%- set s = states.sensor.mangavalkserver -%}
{%- set ns = namespace(out=[]) -%} {%- 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() {%- for k, v in s.attributes.items()
if k not in ['unit_of_measurement', 'friendly_name', 'icon', 'device_class', 'state_class'] -%} if k not in ['unit_of_measurement', 'friendly_name', 'icon', 'device_class', 'state_class'] -%}
{%- set ns.out = ns.out + [(k | replace('|', '/') | replace(';', ',')) ~ '|' ~ {%- set ns.out = ns.out + [(k | replace('|', '/') | replace(';', ',')) ~ '|' ~