Last active
July 12, 2026 05:32
-
-
Save cassidyjames/f067fdc31ccad560e8997630e25fad8b to your computer and use it in GitHub Desktop.
Now Playing color blueprint for Home Assistant
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| blueprint: | |
| name: Now Playing Color | |
| description: > | |
| Extracts the dominant color from a media player's album art whenever the | |
| track changes, boosts it to a minimum brightness so it stays legible on | |
| lights and displays, and stores the result as a hex string (e.g. "#e0803a") | |
| in an input_text helper. | |
| Consume the stored value in other automations, e.g.: | |
| rgb_color: >- | |
| {% set h = states('input_text.your_helper') | replace('#','') %} | |
| [{{ h[0:2]|int(base=16) }}, {{ h[2:4]|int(base=16) }}, {{ h[4:6]|int(base=16) }}] | |
| PREREQUISITES: | |
| 1. The `color_extractor` integration must be enabled. | |
| 2. The album art host must be allow-listed in configuration.yaml, e.g. | |
| homeassistant: | |
| allowlist_external_urls: | |
| - "https://yt3.googleusercontent.com/" | |
| 3. An input_text helper to store the color (max length >= 7). | |
| domain: automation | |
| input: | |
| media_player: | |
| name: Media player | |
| description: The player whose album art is sampled on each track change. | |
| selector: | |
| entity: | |
| filter: | |
| - domain: media_player | |
| color_store: | |
| name: Color storage helper | |
| description: > | |
| The input_text helper that receives the hex color. Its max length must | |
| be at least 7 to fit a value like "#e0803a". | |
| selector: | |
| entity: | |
| filter: | |
| - domain: input_text | |
| brightness_floor: | |
| name: Minimum brightness | |
| description: > | |
| Dark album art produces dim, hard-to-see colors. The extracted color is | |
| pushed up to at least this brightness (HSV value). 1.0 = always full | |
| brightness, 0.0 = never adjust. | |
| default: 0.85 | |
| selector: | |
| number: | |
| min: 0.0 | |
| max: 1.0 | |
| step: 0.05 | |
| mode: slider | |
| mode: queued | |
| max: 10 | |
| variables: | |
| media_player: !input media_player | |
| brightness_floor: !input brightness_floor | |
| triggers: | |
| - trigger: state | |
| entity_id: !input media_player | |
| attribute: media_title | |
| conditions: | |
| # Skip when there's no artwork to sample (idle player, stream without art). | |
| - condition: template | |
| value_template: "{{ state_attr(media_player, 'entity_picture') not in [none, ''] }}" | |
| actions: | |
| # 1. Pull the dominant color out of the album art. | |
| - action: color_extractor.get_color | |
| data: | |
| color_extract_url: "{{ state_attr(media_player, 'entity_picture') }}" | |
| response_variable: art_color | |
| continue_on_error: true | |
| # 2. Normalize to 0-1 fractions, falling back to white if extraction failed. | |
| - variables: | |
| r_frac: "{{ (art_color.color[0] if art_color is defined else 255) / 255 }}" | |
| g_frac: "{{ (art_color.color[1] if art_color is defined else 255) / 255 }}" | |
| b_frac: "{{ (art_color.color[2] if art_color is defined else 255) / 255 }}" | |
| # 3. RGB -> HSV. | |
| - variables: | |
| cmax: "{{ [r_frac, g_frac, b_frac] | max }}" | |
| cmin: "{{ [r_frac, g_frac, b_frac] | min }}" | |
| - variables: | |
| delta: "{{ cmax - cmin }}" | |
| - variables: | |
| hue: >- | |
| {%- if delta == 0 -%}0{%- elif cmax == r_frac -%}{{ (60 * (((g_frac - b_frac) / delta) % 6)) }}{%- elif cmax == g_frac -%}{{ (60 * (((b_frac - r_frac) / delta) + 2)) }}{%- else -%}{{ (60 * (((r_frac - g_frac) / delta) + 4)) }}{%- endif -%} | |
| sat: "{{ 0 if cmax == 0 else delta / cmax }}" | |
| # The brightness floor: never let value drop below the configured minimum. | |
| val: "{{ [cmax, brightness_floor | float] | max }}" | |
| # 4. HSV -> RGB, now with the boosted value. | |
| - variables: | |
| c: "{{ val | float * sat | float }}" | |
| m: "{{ val | float - (val | float * sat | float) }}" | |
| x: "{{ (val | float * sat | float) * (1 - ((((hue | float) / 60) % 2) - 1) | abs) }}" | |
| - variables: | |
| r1: >- | |
| {%- if hue|float < 60 -%}{{ c }}{%- elif hue|float < 120 -%}{{ x }}{%- elif hue|float < 180 -%}0{%- elif hue|float < 240 -%}0{%- elif hue|float < 300 -%}{{ x }}{%- else -%}{{ c }}{%- endif -%} | |
| g1: >- | |
| {%- if hue|float < 60 -%}{{ x }}{%- elif hue|float < 120 -%}{{ c }}{%- elif hue|float < 180 -%}{{ c }}{%- elif hue|float < 240 -%}{{ x }}{%- elif hue|float < 300 -%}0{%- else -%}0{%- endif -%} | |
| b1: >- | |
| {%- if hue|float < 60 -%}0{%- elif hue|float < 120 -%}0{%- elif hue|float < 180 -%}{{ x }}{%- elif hue|float < 240 -%}{{ c }}{%- elif hue|float < 300 -%}{{ c }}{%- else -%}{{ x }}{%- endif -%} | |
| - variables: | |
| final_r: "{{ (((r1 | float) + (m | float)) * 255) | round(0) | int }}" | |
| final_g: "{{ (((g1 | float) + (m | float)) * 255) | round(0) | int }}" | |
| final_b: "{{ (((b1 | float) + (m | float)) * 255) | round(0) | int }}" | |
| # 5. Store the hex color. | |
| - action: input_text.set_value | |
| target: | |
| entity_id: !input color_store | |
| data: | |
| value: "{{ '#%02x%02x%02x' | format(final_r, final_g, final_b) }}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment