<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Jason Cronje]]></title><description><![CDATA[Jason Cronje]]></description><link>https://blog.cronje.io</link><generator>RSS for Node</generator><lastBuildDate>Sat, 25 Apr 2026 11:14:33 GMT</lastBuildDate><atom:link href="https://blog.cronje.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[LED Strip Media Progress Bar ( Home Assistant / Apple TV / WLED)]]></title><description><![CDATA[Recently I installed an WLED strip on my media cabinet which sits below my TV. Initially, I set up a basic automation to power the LED strip on/off following the power state of my Apple TV. One day as I was pondering my next unnecessary and over-engi...]]></description><link>https://blog.cronje.io/led-strip-media-progress-bar-home-assistant-apple-tv-wled</link><guid isPermaLink="true">https://blog.cronje.io/led-strip-media-progress-bar-home-assistant-apple-tv-wled</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[Homelab]]></category><dc:creator><![CDATA[Jason Cronjé]]></dc:creator><pubDate>Sat, 15 Mar 2025 06:02:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742018917355/95e0bd82-6467-4bd9-ac79-0be1a12f4b91.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I installed an WLED strip on my media cabinet which sits below my TV. Initially, I set up a basic automation to power the LED strip on/off following the power state of my Apple TV. One day as I was pondering my next unnecessary and over-engineered automation project, I thought:</p>
<p><strong>What if I could use the LED strip as a media progress indicator bar for content playing on my Apple TV?</strong></p>
<p>After some quick research, it seems like other users have used WLED as a progress bar for other use cases, but nothing perfectly fit my goal.</p>
<h2 id="heading-goals-amp-limitations">Goals &amp; Limitations</h2>
<ul>
<li><p>The LED strip should update at a reasonable rate matching up with the content being played on my Apple TV.</p>
</li>
<li><p>A known limitation of the Home Assistant Apple TV integration is that the polling/update frequency is not that good.</p>
<ul>
<li>Our automation will need to calculate a precise progress value, assuming we may not always get timely/reliable updates from our Apple TV attributes.</li>
</ul>
</li>
<li><p>It seems that the Apple TV attributes in Home Assistant do not reflect application state, they only update when media is started/stopped (<a target="_blank" href="https://community.home-assistant.io/t/feature-request-apple-tv-app-id-attribute-update-on-select/508712">see this post</a>).</p>
<ul>
<li><p>If I exit a movie for example, the entity reflects a “paused” state and the attributes still reflect the movie that was exited. This does not change until new media is played.</p>
</li>
<li><p>Since I don’t want to stare at the progress bar when nothing is playing or after I exit content, I will treat the “paused” state (with a slight delay) as a trigger to go back to my default color.</p>
</li>
</ul>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Full disclaimer, I make no claims of being an expert at writing code or automations. As such, my talented French colleague by the name of Claude will be lending a heavy hand on this project. With that said, please feel free to tell Claude how you feel about their code.</div>
</div>

<h2 id="heading-components">Components</h2>
<ol>
<li><p><strong>Apple TV Integration</strong>: Built-in Home Assistant integration that monitors Apple TV state.</p>
<ol>
<li><p>The Apple TV integration provides several states that the automations monitor:</p>
<ul>
<li><p><code>playing</code>: Media is currently playing</p>
</li>
<li><p><code>paused</code>: Media is paused</p>
</li>
<li><p><code>idle</code>: Apple TV is on but not playing media</p>
</li>
<li><p><code>standby</code>: Apple TV is in sleep mode</p>
</li>
</ul>
</li>
<li><p>Additionally, the integration provides media attributes that we will use to calculate the current percentage of the progress bar:</p>
<ul>
<li><p><code>media_position</code>: Current position in the media (in seconds)</p>
</li>
<li><p><code>media_duration</code>: Total duration of the media (in seconds)</p>
</li>
<li><p><code>media_position_updated_at</code>: Timestamp when the position was last updated</p>
</li>
</ul>
</li>
</ol>
</li>
<li><p><strong>WLED REST API</strong>: Custom REST API endpoint for direct JSON control of WLED.</p>
</li>
</ol>
<h2 id="heading-wled-api">WLED API</h2>
<p>After a little bit of research, I decided that using the <a target="_blank" href="https://kno.wled.ge/interfaces/json-api/">WLED JSON API</a> would be the best approach for this project. This allows us to send a POST call along with a JSON body containing the instructions for the LED strip.</p>
<p>For example, my LED strip contains a total of 108 addressable LEDs. If I send this very basic POST request with the following JSON body, my entire LED strip turns orange. To test this you can use an API client like Postman, Scalar, etc.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"seg"</span>: {
    <span class="hljs-attr">"i"</span>: [<span class="hljs-number">0</span>, <span class="hljs-number">108</span>, <span class="hljs-string">"FFA500"</span>]
  }
}
</code></pre>
<p>Here are some additional examples of the JSON syntax:</p>
<ol>
<li><p><strong>Single Segment Control</strong> (used in the idle state):</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"on"</span>: <span class="hljs-literal">true</span>,
   <span class="hljs-attr">"bri"</span>: <span class="hljs-number">255</span>,
   <span class="hljs-attr">"seg"</span>: {
     <span class="hljs-attr">"i"</span>: [<span class="hljs-number">0</span>, <span class="hljs-number">108</span>, <span class="hljs-string">"FF8C00"</span>]
   }
 }
</code></pre>
</li>
<li><p><strong>Multi Segment Control</strong> (used in the progress bar state):</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"on"</span>: <span class="hljs-literal">true</span>,
   <span class="hljs-attr">"bri"</span>: <span class="hljs-number">255</span>,
   <span class="hljs-attr">"seg"</span>: {
     <span class="hljs-attr">"i"</span>: [<span class="hljs-number">0</span>, <span class="hljs-number">42</span>, <span class="hljs-string">"00FFFF"</span>, <span class="hljs-number">42</span>, <span class="hljs-number">108</span>, <span class="hljs-string">"FF0080"</span>]
   }
 }
</code></pre>
</li>
</ol>
<p>The <code>"i"</code> array in the WLED JSON format represents segments with their start position, end position, and color. The syntax is: <code>[start_position1, end_position1, "color1", start_position2, end_position2, "color2", ...]</code></p>
<h2 id="heading-home-assistant">Home Assistant</h2>
<h3 id="heading-configurationyaml">Configuration.yaml</h3>
<p>To apply this API control approach in Home Assistant, we added the following to our <code>configuration.yaml</code> file to define a REST command that will directly send JSON data to the WLED controller:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">rest_command:</span>
  <span class="hljs-attr">wled_progress:</span>
    <span class="hljs-attr">url:</span> <span class="hljs-string">http://123.456.789.0/json</span>
    <span class="hljs-attr">method:</span> <span class="hljs-string">POST</span>
    <span class="hljs-attr">content_type:</span> <span class="hljs-string">"application/json"</span>
    <span class="hljs-attr">payload:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ progress }}</span>"</span>
</code></pre>
<p>This command is used by both automations to control the WLED strip in different ways. As you can see, the payload has a placeholder variable "{{ progress }}". This will allow us to leverage the same rest_command in multiple different automations, as we can dynamically construct the JSON payload within our automation code.</p>
<h3 id="heading-automationsyaml">Automations.yaml</h3>
<h4 id="heading-1-apple-tv-power-and-paused-state">1. Apple TV Power and Paused State</h4>
<p>This consolidated automation handles both power on/off scenario and the paused scenario based on Apple TV state. I have chose the color orange (<code>"FF8C00"</code>) as my idle color but you can change this.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">'XXXXXXXXXXXXXXXX'</span>
  <span class="hljs-attr">alias:</span> <span class="hljs-string">Apple</span> <span class="hljs-string">TV</span> <span class="hljs-string">LED</span> <span class="hljs-string">Power</span> <span class="hljs-string">and</span> <span class="hljs-string">Color</span> <span class="hljs-string">Control</span>
  <span class="hljs-attr">description:</span> <span class="hljs-string">Controls</span> <span class="hljs-string">LED</span> <span class="hljs-string">strip</span> <span class="hljs-string">power</span> <span class="hljs-string">and</span> <span class="hljs-string">color</span> <span class="hljs-string">based</span> <span class="hljs-string">on</span> <span class="hljs-string">Apple</span> <span class="hljs-string">TV</span> <span class="hljs-string">state</span>
  <span class="hljs-attr">triggers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
    <span class="hljs-attr">trigger:</span> <span class="hljs-string">state</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">event:</span> <span class="hljs-string">start</span>
    <span class="hljs-attr">trigger:</span> <span class="hljs-string">homeassistant</span>
  <span class="hljs-attr">actions:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">variables:</span>
      <span class="hljs-attr">apple_tv_state:</span> <span class="hljs-string">'<span class="hljs-template-variable">{{ states(''media_player.apple_tv'') }}</span>'</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">choose:</span>
    <span class="hljs-comment"># When Apple TV is playing, we don't need to do anything as the progress bar automation handles it</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">conditions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
        <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
        <span class="hljs-attr">state:</span> <span class="hljs-string">playing</span>
      <span class="hljs-attr">sequence:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">delay:</span>
          <span class="hljs-attr">milliseconds:</span> <span class="hljs-number">100</span>
    <span class="hljs-comment"># When Apple TV is paused or idle, turn on LED and set to orange</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">conditions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
        <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">paused</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">idle</span>
      <span class="hljs-attr">sequence:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">target:</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.media_cabinet_led_strip</span>
        <span class="hljs-attr">data:</span> {}
        <span class="hljs-attr">action:</span> <span class="hljs-string">light.turn_on</span>
      <span class="hljs-comment"># Wait before applying orange color</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">delay:</span>
          <span class="hljs-attr">seconds:</span> <span class="hljs-number">3</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">data:</span>
          <span class="hljs-attr">progress:</span> <span class="hljs-string">"{% set json_data = {\n  \"on\": true,\n  \"bri\": 255,\n  \"seg\": {\n    \"i\": [0, 108, \"FF8C00\"]\n  }\n} %} <span class="hljs-template-variable">{{ json_data|tojson }}</span>\n"</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">rest_command.wled_progress</span>
    <span class="hljs-comment"># When Apple TV is off or standby, turn off the LED strip</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">conditions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
        <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">standby</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">'off'</span>
      <span class="hljs-attr">sequence:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">target:</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.media_cabinet_led_strip</span>
        <span class="hljs-attr">data:</span> {}
        <span class="hljs-attr">action:</span> <span class="hljs-string">light.turn_off</span>
      <span class="hljs-comment"># Also send command to ensure WLED is completely off</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">data:</span>
          <span class="hljs-attr">progress:</span> <span class="hljs-string">"{% set json_data = {\n  \"on\": false,\n  \"seg\": {\n    \"i\": [0, 0, \"000000\", 0, 108, \"000000\"]\n  }\n} %} <span class="hljs-template-variable">{{ json_data|tojson }}</span>\n"</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">rest_command.wled_progress</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">restart</span>
  <span class="hljs-attr">max_exceeded:</span> <span class="hljs-string">silent</span>
</code></pre>
<p><strong>Key aspects:</strong></p>
<ul>
<li><p>Consolidates both power control and orange color setting into a single automation</p>
<ul>
<li>Anywhere the orange color is mentioned (<code>"FF8C00"</code>) can be customized to a HEX color of your choosing</li>
</ul>
</li>
<li><p>Uses a state-based approach with three conditions:</p>
<ul>
<li><p>When playing: Does nothing (lets the progress bar automation handle it)</p>
</li>
<li><p>When paused/idle: Turns on LED and sets to orange after a 3-second delay</p>
</li>
<li><p>When off/standby: Turns off the LED strip completely</p>
</li>
</ul>
</li>
<li><p>Uses the restart mode to ensure only one instance runs at a time</p>
</li>
<li><p>Includes the HomeAssistant start trigger to set the correct state on system restart</p>
</li>
</ul>
<h4 id="heading-2-apple-tv-progress-bar-when-playing">2. Apple TV Progress Bar When Playing</h4>
<p>This automation creates a progress bar effect on the WLED strip while media is playing on the Apple TV:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">'XXXXXXXXXXXXXXXXXXXX'</span>
  <span class="hljs-attr">alias:</span> <span class="hljs-string">Apple</span> <span class="hljs-string">TV</span> <span class="hljs-string">Progress</span> <span class="hljs-string">Bar</span> <span class="hljs-string">When</span> <span class="hljs-string">Playing</span>
  <span class="hljs-attr">description:</span> <span class="hljs-string">Shows</span> <span class="hljs-string">progress</span> <span class="hljs-string">bar</span> <span class="hljs-string">on</span> <span class="hljs-string">WLED</span> <span class="hljs-string">when</span> <span class="hljs-string">Apple</span> <span class="hljs-string">TV</span> <span class="hljs-string">is</span> <span class="hljs-string">playing</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-comment"># Trigger to state</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">'playing'</span>
    <span class="hljs-comment"># Trigger for media_position attribute state</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
    <span class="hljs-attr">attribute:</span> <span class="hljs-string">media_position</span>
    <span class="hljs-comment"># Trigger every 1 second to ensure timely updates to LED strip</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">time_pattern</span>
    <span class="hljs-attr">seconds:</span> <span class="hljs-string">'/1'</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-comment"># Ensure Apple TV is in a playing state</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">media_player.apple_tv</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">'playing'</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
    <span class="hljs-attr">value_template:</span> <span class="hljs-string">'<span class="hljs-template-variable">{{ state_attr("media_player.apple_tv", "media_duration") is not none and state_attr("media_player.apple_tv", "media_duration") &gt; 0 }}</span>'</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-comment"># Format our JSON payload to send to our rest command</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">rest_command.wled_progress</span>
    <span class="hljs-attr">data:</span>
      <span class="hljs-attr">progress:</span> <span class="hljs-string">&gt;
        {% set last_position = state_attr('media_player.apple_tv', 'media_position')|float(0) %}
        {% set last_updated = state_attr('media_player.apple_tv', 'media_position_updated_at') %}
        {% set duration = state_attr('media_player.apple_tv', 'media_duration')|float(1) %}
        {% set position = last_position %}
        {% if last_updated is not none %}
          {% set time_diff = (now().timestamp() - as_timestamp(last_updated))|float(0) %}
          {% set position = last_position + time_diff %}
          {% if position &gt; duration %}
            {% set position = duration %}
          {% endif %}
        {% endif %}
        {% set progress_percent = position / duration if duration &gt; 0 else 0 %}
        {% set led_count = 108 %}
        {% set progress_leds = (progress_percent * led_count)|int %}
        {% set json_data = {
          "on": true,
          "bri": 255,
          "seg": {
            "i": [0, progress_leds, "00FFFF", progress_leds, 108, "FF0080"]
          }
        } %}
        {{ json_data|tojson }}
</span>  <span class="hljs-attr">mode:</span> <span class="hljs-string">restart</span>
  <span class="hljs-attr">max_exceeded:</span> <span class="hljs-string">silent</span>
</code></pre>
<p><strong>Key aspects:</strong></p>
<ul>
<li><p>Updates every second and on media position changes</p>
</li>
<li><p>Calculates the current playback position, accounting for time elapsed since last update</p>
</li>
<li><p>Calculates the percentage of playback completed and converts to LED positions</p>
</li>
<li><p>Creates a two-color effect: cyan (<code>"00FFFF"</code>) for completed portion and pink (<code>"FF0080"</code>) for remaining portion</p>
</li>
<li><p>Uses the WLED segment array notation to create a split-color effect</p>
</li>
</ul>
<h1 id="heading-future-enhancement-ideas">Future Enhancement Ideas</h1>
<p>Potential improvements to this setup:</p>
<ol>
<li><p>Add color customization through Home Assistant input_select entities</p>
</li>
<li><p>Implement brightness control based on time of day or ambient light sensors</p>
</li>
<li><p>Add additional visual effects for specific media types (movies, music, etc.)</p>
</li>
</ol>
<h1 id="heading-closing">Closing</h1>
<p>I hope you find this information useful! Please share your adaptations below and as always, feel free to ask any questions.</p>
]]></content:encoded></item><item><title><![CDATA[Hello World]]></title><description><![CDATA[This is my first blog post on my new headless Hashnode blog hosted with Vercel.]]></description><link>https://blog.cronje.io/hello-world</link><guid isPermaLink="true">https://blog.cronje.io/hello-world</guid><dc:creator><![CDATA[Jason Cronjé]]></dc:creator><pubDate>Tue, 08 Oct 2024 18:31:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728412525659/d7616f50-3935-478f-96b4-63ae7ecca9ce.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is my first blog post on my new headless Hashnode blog hosted with Vercel.</p>
]]></content:encoded></item></channel></rss>