Hello to all,
I would like to share with you a small project that i was looking to implement for some time now and that was to create an LED Seek tracker for my Plex media.
What is the WLED Seek Tracker:
In short: It is an easy way to visualize the progress of a media file playing on a plex server using WLED.
When a media file stars playing, the LEDs are turned on and as the file progresses in time, LEDs turn off to indicate the remaining time until the end. Here are some pictures to better understand what it does:
When the movie starts, the LED bar under the TV turns blue and it is full indicating that the movie is at the beginning.
Progressing further you can see that the bar is “reducing in size” indicating that the movie is at about half of it’s runtime. The first LED remains on to indicate the beginning of the bar when it’s dark.
Obviously at the end of the movie the majority of the LED bar is already off indicating that the movie ends.
What do you need in order to do this?
- Home Assistant (obviously)
- WLED on a nodeMCU
- An LED strip of individually addressable LEDs
- Optional: an electical channel to place the LEDs and a cover to diffuse the light (i have used paper)
Setup:
We need to setup a few things before we can even start building the automation that will bring this to life. Below i will outline the WLED set up, as well as the Home Assistant configuration set up:
WLED Setup
I will assume that you know how to flash the NodeMCU and you have a perfectly working LED strip. If yes, we now need to build a few segments. WLED is not perfect and has a few limitations. One of those that the segments do not survive a reboot of the NodeMCU. In order to overcome this, build your segments first and go to “Presets” save all the segments as a preset in the slot 16, and then go to Configuration → LED Preferences → Select: Apply preset 16 on boot:
Building the segments:
Here comes another limitation of WLED. It allows only up to 15 segments and for this reason, we need to now make a few calculations. In my setup, i have used an LED strip with 28 LEDs. As i mentioned before, the first and last LED are always staying on and they occupy their respective segments. The remaining 26 LEDs will need to be splited to 13 segments (as 15 is maximum). In my case, 26 LEDs / 13 Segments means 2 LEDs per segment but if you are using a bigger LED Strip you will need to have more LEDs per segment. Think this through, maybe draw it on a paper and make your calculations accordingly.
Ok, so now, building the segments is easy. Navigate to the Segments part of the WLED UI and start creating the segments. As i said, for me, the first segment is 1 LED then i have 13 Segments of 2 LEDs and the last is 1 LED as well giving in total 28 LEDs that i have available.
Building the Presets:
Although, WLED exposes each segment to Home assistant as a different light, i found it easier to build Presets in WLED so i can easily adjust if required. If you want to follow this advice, go to your segments, select the segments you want to control, set the brightness and the color of your choice (it doesn’t have to be one color) and save it as a preset. So for example, for the first part of the movie where the first 2 LEDs will turn off, i selected the segment with the LEDs from 1 to 3 and i turned the color to black to turn them off. Like this i continued until the end. Make sure to try it by selecting each preset one by one to verify that LEDs turn off in order. If everything is ok, that completes the WLED setup. Optionally, you can save your WLED configuration to a json file and store it somewhere safe.
Configuring Home Assistant
Now it’s the time to configure Home Assistant as we also need calculate the progress of the media file but also to split the media file in 13 equal parts which will be used in the automation. Below you can find the code that does just that. Please make sure to rename the entities of the media_player to your Plex Client entity:
# Calculate the time remaining on Plex Media
- platform: template
sensors:
plexremainingtime:
friendly_name: "Plex Time Remaining"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{ state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') - (state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_position') + as_timestamp(now()) - as_timestamp(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_position_updated_at'))) + ((as_timestamp((states.sensor.date_time_iso.state)) - as_timestamp((states.sensor.date_time_iso.state)))) }}
{% endif %}
# Split the Plex Media Duration to 13 equal parts
- platform: template
sensors:
moviesegment1:
friendly_name: "Movie Segment 1"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13) |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment2:
friendly_name: "Movie Segment 2"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*2 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment3:
friendly_name: "Movie Segment 3"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*3 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment4:
friendly_name: "Movie Segment 4"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*4 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment5:
friendly_name: "Movie Segment 5"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*5 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment6:
friendly_name: "Movie Segment 6"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*6 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment7:
friendly_name: "Movie Segment 7"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*7 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment8:
friendly_name: "Movie Segment 8"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*8 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment9:
friendly_name: "Movie Segment 9"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*9 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment10:
friendly_name: "Movie Segment 10"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*10 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment11:
friendly_name: "Movie Segment 11"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*11 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment12:
friendly_name: "Movie Segment 12"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*12 |float|round}}
{% endif %}
- platform: template
sensors:
moviesegment13:
friendly_name: "Movie Segment 13"
value_template: >-
{% if states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'unavailable' or states('media_player.plex_plex_for_android_tv_orange_tv_box') == 'idle' %}
0
{% else %}
{{(state_attr('media_player.plex_plex_for_android_tv_orange_tv_box', 'media_duration') / 13)*13 |float|round}}
{% endif %}
EDIT: #1: In order for the above to work, you need to have enabled the time_date platform in HA as well. Do so by adding the following to your configuration file if it’s not there:
- platform: time_date
display_options:
- 'time'
- 'date'
- 'date_time'
- 'date_time_utc'
- 'date_time_iso'
- 'time_date'
- 'time_utc'
In a few words, this is using the media duration attribute of the plex client entity to create sensors with time in seconds. The “moviesegment” parts of the code are sensors that equally split the media duration by 13 parts (remember, we have 13 segments set on WLED).
Optionally but recommended: Exclude the sensors from the recorder and history to avoid cluttering your database. If you want to do that, paste the following code as well on the configuration file:
recorder:
exclude:
entities:
- sensor.plexremainingtime
- sensor.moviesegment1
- sensor.moviesegment2
- sensor.moviesegment3
- sensor.moviesegment4
- sensor.moviesegment5
- sensor.moviesegment6
- sensor.moviesegment7
- sensor.moviesegment8
- sensor.moviesegment9
- sensor.moviesegment10
- sensor.moviesegment11
- sensor.moviesegment12
- sensor.moviesegment13
history:
exclude:
entities:
- sensor.plexremainingtime
- sensor.moviesegment1
- sensor.moviesegment2
- sensor.moviesegment3
- sensor.moviesegment4
- sensor.moviesegment5
- sensor.moviesegment6
- sensor.moviesegment7
- sensor.moviesegment8
- sensor.moviesegment9
- sensor.moviesegment10
- sensor.moviesegment11
- sensor.moviesegment12
- sensor.moviesegment13
After you paste the above to the configuration file, save and restart Home Assistant. It is now time to make an automation.
Tiding it all up with an automation:
In order to bring this to life, we need an automation that has 14 triggers. The first trigger activates the lights and when the “sensor.plexremainingtime” entity goes below the respective “sensor.moviesegment” value, it loads the corresponding preset to WLED. The last trigger, turns the LEDs off when the movie finishes:
alias: LED Movie Seeker
description: ""
trigger:
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment13
id: seg1
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment12
id: seg2
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment11
id: seg3
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment10
id: seg4
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment9
id: seg5
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment8
id: seg6
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment7
id: seg7
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment6
id: seg8
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment5
id: seg9
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment4
id: seg10
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment3
id: seg11
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment2
id: seg12
- platform: numeric_state
entity_id:
- sensor.plexremainingtime
below: sensor.moviesegment1
id: seg13
- platform: state
entity_id:
- media_player.plex_plex_for_android_tv_orange_tv_box
from: playing
to: idle
id: End
- platform: state
entity_id:
- media_player.plex_plex_for_android_tv_orange_tv_box
from: playing
to: unavailable
id: End2
condition: []
action:
- choose:
- conditions:
- condition: trigger
id:
- seg1
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 1
- conditions:
- condition: trigger
id:
- seg2
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 2
- conditions:
- condition: trigger
id:
- seg3
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 3
- conditions:
- condition: trigger
id:
- seg4
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 4
- conditions:
- condition: trigger
id:
- seg5
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 5
- conditions:
- condition: trigger
id:
- seg6
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 6
- conditions:
- condition: trigger
id:
- seg7
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 7
- conditions:
- condition: trigger
id:
- seg8
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 8
- conditions:
- condition: trigger
id:
- seg9
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 9
- conditions:
- condition: trigger
id:
- seg10
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 10
- conditions:
- condition: trigger
id:
- seg11
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 11
- conditions:
- condition: trigger
id:
- seg12
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 12
- conditions:
- condition: trigger
id:
- seg13
sequence:
- device_id: f738d00a3b908e09d37a63dfb2e7191f
domain: select
entity_id: 323d94f052c41329e2a0d0c7380d6b61
type: select_option
option: Segment 13
- conditions:
- condition: trigger
id:
- End
- End2
sequence:
- service: automation.turn_on
data: {}
target:
entity_id: automation.living_room_auto_lights
- service: automation.turn_off
data:
stop_actions: true
target:
entity_id: automation.led_movie_seeker
- service: light.turn_off
data: {}
target:
entity_id:
- light.movie_tracker_main
- light.movie_tracker
mode: restart
EDIT: #2 - I have updated the code to the configuration file with if statements as before it was throwing errors in the logs in case the Plex Media Player was unavailable or Idle. Now the code will fill the value 0 to the movie segments in case the player is either “Unavailable” or “Idle” avoiding a “NoneType” error in the logs.
Again make sure to rename any entities you have named differently or the Plex client entity in order for this to work.
If you have followed this far, you should now put a movie and test the whole thing. Hopefully, you will have a fully working WLED Seek Tracker.
If any questions arise, please share them below.
Thanks
MK