WLED Seek Tracker

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:

image

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

1 Like

Hello to the community.

I would just like to mention the WLED Seek Tracker can be used for other purposes as well. I have been working on the project a bit more and this time, i used the existing setup from the first post to visualize distance from home. Let me explain further:

My job is 6km away from my house. I have built as well a zone for my work and even if my speakers announce when i leave from work, i wanted to have a visual indication as well as sometimes the message can be missed. Therefore, i am using the proximity sensor that will give the distance of my phone from the house to control the segments we build above. The logic is the following:

  • When i leave from work, turn on all the segments with red color and “breathe” effect.
  • When the proximity sensor value is below 4 (km) turn off the first 5 segments.
  • When the proximity sensor value is below 3 (km) turn off the second 5 segments.
  • When the proximity sensor value is below 2 (km) turn off the third 5 segments.
  • When the proximity sensor value is below 1 (km) turn on all the segments in green color, wait for 10 minutes and then turn them off.

For all the above, i have built separate presets in WLED UI which i call with numeric states triggers.

As you can see, the LED bar is now a fully functional visual distance tracker from home.

I hope this will give you some ideas but in general you can use it to visualize anything you need. Here are a few other examples that come to my mind:

  • Visualize your internet speed (using speedtest integration)
  • Visualize your PCs processor load
  • Visualize your energy consumption
  • Visualize the temperature in your house
  • Visualize the sun elevation

Basically anything that has some progress can be visualized.

Hope this gives you some inspiration!

With Kind Regards
MK

That’s a cool project. Your tv room is very nice too.

1 Like

Hi,

really cool project.

I want to do this on my own and i’m facing two problems:

  1. sensor.chromecastremainingtime is not updated. Splitting the movie time in parts works well, but i didn’t get a value vor sensor.chromecastremainingtime

This is what my sensor.yaml looks like:

- platform: template
  sensors:
    chromecastremainingtime:
        friendly_name: "Chromecast Time Remaining"
        value_template: "{{ state_attr('media_player.wohnzimmer', 'media_duration') - (state_attr('media_player.wohnzimmer', 'media_position') + as_timestamp(now()) - as_timestamp(state_attr('media_player.wohnzimmer', 'media_position_updated_at'))) + ((as_timestamp((states.sensor.date_time_iso.state)) - as_timestamp((states.sensor.date_time_iso.state)))) }}"
  1. Where did you get entity_id and device_id from the wled for the automatation?

thanks in advance.

Hi,

For the automation i didnt use state but device. If you choose in the actions Device you can select your WLED device and then you get an option to change your preset to whatever is the corresponding preset for the trigger. This basically assighs the device ID.

For the sensor can you please check it under developer options? I see that you are using a chromecast from the entity name. If you put it on your dashboard, does it display the progress?

Are you using plex or something else which you stream to the chromecast?

You mean the device attributes?

These are displayed for media_player.wohnzimmer.

But the sensor.chromecastremainingtime stays unavailable.

What does as_timestamp((states.sensor.date_time_iso.state)) do?

I can’t find this sensor in the developer options. Maybe this causes the problem?

I use jellyfin and netflix for streaming.

This is taking into consideration the time that is passing.

On plex, you have attributes: media duration and media position, but those alone are not enough to calculate the remaining time of the media file. We needed to implement the time that is passing in order to calculate correctly. Does Netflix media player have media duration and media position attributes?

Yes, I’ve checked all attributes you are using.
They are all available.

Maybe the wrong format?

Can you post the code from the configuration file please, just for me to understand what you are using to calculate…

EDIT: now it occured to me… It can be the format as you are stating. If you don’t have the following to your configuration please add:

- platform: time_date
  display_options:
    - 'time'
    - 'date'
    - 'date_time'
    - 'date_time_utc'
    - 'date_time_iso'
    - 'time_date'
    - 'time_utc'
    - 'beat'

This should help the states.sensor.date_time_iso.state to calculate the time!

I’ve wrote this here.

EDIT: I might that’s the problem.

This is not configured.

I’m currently not at home, but i will check this in about two hours

Thanks, i edited by post above. If that fixes the problem i will add it to the first post so everybody will copy it from there.

Hi, did this fix the issue?

Thanks

Hi,

Yes, that was the solution!

Thanks a lot for your help and sorry for the last response.

One another question: I’ve done some research regarded this because i want to increase the segments.

Here is written, that on a esp32 32 segements should be possible. I’ve tried this with a esp32 wroom 32. Adding the segments works, but creating the presets leads into an unstable wled.

What are your experiences in this regard?

Did you ever tried adressing each single led out of home assistant?

According to the Documation of WLED could this be a solution for a smoother progress bar.

I have tried with an ESP8266 so for me the segments are 15 max. And even so, sometimes the automation throws errors in the log. There is a post in Home Assistant community but there was no solution: Error using light services with WLED device - #3 by michaelkrtikos

I didnt try the per-segment individual control but if you want to give it try it looks like a good solution. In regards to the smoothness, it depends on your needs. For me, the strip of 28 leds in total is small under my tv and because the light is diffused, i wouldnt see much of a difference if one led turns off. I use it more or less as an indicator of how long the movie goes.

I did this automation mostly for my wife who usually when watching a movie is getting sleepy and she usually asks “how long till the end?” :slight_smile:

To be honest, i wasnt aware of the per-segment individual control but it bothers me that in the documentation its written that its not persistant. In practice it may be ok though…

Same as me :smile:

I have a canvas with a base cabinet.

And my led strip is 2 m with 288 LEDs.

Therefore it would be nice to get a smoother progress bar.

Wow, then yes, because if you have 288 LEDs you are turning off about 20 LEDs each chunck of the movie and that’s a lot.

I can’t get enough of it, I would like to resolve my seek tracker more finely.

I found out that you can control individual leds via the JSON API.

The command is as follows:

curl -X POST "http://WLED-IP/json" -d '{"seg":{"i":[1,[255,0,0]]}}' -H "Content-Type: application/json"

This command is switching the first led to on.

But I have no idea how I could implement this in HA.

WLED also has the “Percent” effect, you can use the intensity slider to specify the % of the segment that is illuminated