Sync Your Tapo TP-Link L920-5 RGBIC LED Strip with PS5 Game Covers in Home Assistant

I built a Home Assistant automation that syncs a TP-Link Tapo L920-5 (5m RGBIC) LED strip with my PlayStation 5. Whenever the PS5 starts playing a game, Home Assistant grabs the game cover art (entity picture) and extracts a color palette from it, then pushes a short RGBIC color sequence to the LED strip. The result is a simple “ambilight-style” effect that matches the game you’re currently playing.

In this post I’ll share the full setup: required entities, the palette extraction step, and the Home Assistant automation/service call to run the color sequence. It’s lightweight, fully local on the HA side (apart from fetching the cover image), and you can tweak brightness, transition speed, and number of colors easily.

Requirements: You’ll need the PlayStation Network integration in Home Assistant to provide the “now playing” game cover (entity picture). My setup runs on Home Assistant in Docker, and I recommend Home Assistant 2025.12 or newer.

entity_id=image.psn_account_spielt_gerade
led: light.ledstripe01

Let’s start with Home Assistant REST API

Edit your configuration.yaml and add a REST Sensor:

    rest:                                          
      - resource: "http://<strong>IPORHOSTNAMEFROMDOCKERCONTAINER</strong>:8787/palette?entity_id=image.psn_account_spielt_gerade&k=6"
        scan_interval: 20                            
        sensor:                                               
          - name: psn_logo_palette           
            value_template: "{{ value_json.dominant_hex }}"
            json_attributes:                                                                                                                                        
              - sequence                                                                                                                                             
              - brightness                                                                                                                                           
              - transition                                                          
              - spread                       
              - directionCode-Sprache: HTML, XML (xml)

    Replace IPORHOSTNAMEFROMDOCKERCONTAINER with the IP address or hostname of your Docker container (see the next step).

    Create Docker Container

    First, we need a python script

    sudo mkdir /opt/palette-api/
    sudo touch /opt/palette-api/palette_api.py
    sudo vi /opt/palette-api/palette_api.py
    def extract_palette(img: Image.Image, k: int = 6):
        img = img.convert('RGB').resize((260, 260))
    
        # 1) Crop: 12% Rand weg (UI/Badges/Border reduzieren)
        w, h = img.size
        m = int(min(w, h) * 0.12)
        img = img.crop((m, m, w - m, h - m))
    
        # 2) Quantize -> Kandidaten
        q = img.quantize(colors=96, method=2)
        pal = q.getpalette()
        counts = q.getcolors() or []
        counts.sort(reverse=True, key=lambda x: x[0])
    
        candidates = []
        for cnt, idx in counts[:96]:
            rgb = (pal[idx*3], pal[idx*3+1], pal[idx*3+2])
            h, s, v = rgb_to_hsv_triplet(rgb)
    
            # 3) Filter: zu grau/zu dunkel/zu hell rauswerfen
            if s < 12:
                continue
            if v < 8 or v > 98:
                continue
    
            candidates.append((cnt, rgb, h, s, v))
    
        if not candidates:
            return [(0, 120, 255)] * k, 200  # fallback blau
    
        # 4) Auswahl: Hue-divers + RGB-divers (damit Blau/Rot/Highlights reinkommen)
        picked = []
        picked_hues = []
    
        def hue_dist(a, b):
            d = abs(a - b) % 360
            return min(d, 360 - d)
    
        for cnt, rgb, h, s, v in candidates:
            if any(hue_dist(h, ph) < 22 for ph in picked_hues):
                continue
            if any(sum((a-b)**2 for a, b in zip(rgb, prgb)) < 800 for prgb in picked):
                continue
            picked.append(rgb)
            picked_hues.append(h)
            if len(picked) >= k:
                break
    
        # Wenn wir noch nicht genug haben: ohne Hue-Regel auffuellen
        if len(picked) < k:
            for cnt, rgb, h, s, v in candidates:
                if any(sum((a-b)**2 for a, b in zip(rgb, prgb)) < 600 for prgb in picked):
                    continue
                picked.append(rgb)
                if len(picked) >= k:
                    break
    
        while len(picked) < k:
            picked.append(picked[-1])
    
        # dominanter Hue = haeufigste (nach Filter)
        dominant_hue = candidates[0][2]
        return picked, dominant_hue
    
    Code-Sprache: PHP (php)

    now create the Docker Container for example Portainer -> Create new stack (Web), Name: palette-api

    version: "3.8"
    
    services:
      palette-api:
        image: python:3.11
        container_name: palette-api
        restart: unless-stopped
    
        networks:
          <strong>IOT-600</strong>:
            ipv4_address: <strong>192.168.1.60</strong>
    
        environment:
          HA_BASE_URL: "http://<strong>IPFROMYOURHOMEASSISTANTSERVER</strong>:8123"
          HA_TOKEN: "<strong>PASTE_YOUR_TOKEN_HERE</strong>"
    
        volumes:
          - /opt/palette-api/palette_api.py:/app/palette_api.py:ro
    
        command: >
          bash -lc "
          pip install --no-cache-dir fastapi uvicorn pillow requests &&
          uvicorn palette_api:app --app-dir /app --host 0.0.0.0 --port 8787
          "
    
    networks:
      <strong>IOT-600</strong>:
        external: true
    Code-Sprache: HTML, XML (xml)

    To make this work, you need a Long-Lived Access Token from Home Assistant and the hostname/IP of your Home Assistant server. Then you paste both into the container environment variables:

    Get your Home Assistant Long-Lived Access Token

    • Open Home Assistant in your browser.
    • Click your profile (bottom-left in the sidebar) — or go to Settings → People → Users → (your user).
    • Scroll down to Long-Lived Access Tokens.
    • Click Create Token, give it a name (e.g., palette-api), and confirm.
    • Copy the token immediately (you won’t be able to see it again later).
    • Now paste it into:

    HA_TOKEN: „PASTE_YOUR_TOKEN_HERE“

    Set the Home Assistant base URL (Hostname or IP)

    HA_BASE_URL must point to the URL where the Docker container can reach Home Assistant:

    If Home Assistant is on the same machine/LAN, use its LAN IP or hostname, e.g.
    http://192.168.1.50:8123 or http://homeassistant.local:8123

    Paste it into:

    HA_BASE_URL: „http://192.168.1.50:8123“

    Notes / common pitfalls

    Don’t use localhost unless the container runs in the same network namespace as Home Assistant (usually it won’t).

    Keep the token secret (don’t post it in screenshots/logs).

    If you use HTTPS externally, you can also set https://… — but the key thing is that the container must be able to reach that address.

    Update the network configuration to fit your setup. I use a macvlan network (IOT-600) because the container needs a separate IP/subnet from the Docker host—specifically an isolated IoT VLAN/network in my environment. If you don’t need that, switch to your preferred Docker networking mode.

    Now we have the palette-api Python script, the Docker container, and the REST API sensor(s) set up in Home Assistant. Next, validate your configuration (configuration.yaml) and then restart Home Assistant to make sure everything is loaded correctly.

    Once Home Assistant is back up, the last missing piece is the automation that triggers the palette extraction and sends the resulting color sequence to your lights.

    Home Assistant Automation:

    alias: "PSN -> LEDStripe: Sequence + Off on End"
    description: ""
    triggers:
      - entity_id: sensor.psn_account_spielt_gerade
        trigger: state
    actions:
      - variables:
          led: light.ledstripe01
          game_title: "{{ states('sensor.psn_account_spielt_gerade') | trim }}"
          ended: >-
            {{ game_title in ['unknown','unavailable','', None, 'None', 'none',
            'idle', 'standby', 'offline'] }}
      - choose:
          - conditions:
              - condition: template
                value_template: "{{ ended }}"
            sequence:
              - target:
                  entity_id: "{{ led }}"
                action: light.turn_off
              - stop: Game ended -> LED off
      - delay: "00:00:02"
      - target:
          entity_id: sensor.psn_logo_palette
        action: homeassistant.update_entity
      - delay: "00:00:02"
      - variables:
          seq: >-
            {{ state_attr('sensor.psn_logo_palette', 'sequence') | default([], true)
            }}
          bri: >-
            {{ state_attr('sensor.psn_logo_palette', 'brightness') | default(80,
            true) | int }}
          trans: >-
            {{ state_attr('sensor.psn_logo_palette', 'transition') | default(6000,
            true) | int }}
          spread: >-
            {{ state_attr('sensor.psn_logo_palette', 'spread') | default(3, true) |
            int }}
          direction: >-
            {{ state_attr('sensor.psn_logo_palette', 'direction') | default(1, true)
            | int }}
      - condition: template
        value_template: "{{ seq is iterable and (seq | length) >= 6 }}"
      - target:
          entity_id: "{{ led }}"
        data:
          sequence: "{{ seq }}"
          segments: 0
          brightness: "{{ bri }}"
          repeat_times: 0
          transition: "{{ trans }}"
          spread: "{{ spread }}"
          direction: "{{ direction }}"
        action: tplink.sequence_effect
    mode: restart
    Code-Sprache: JavaScript (javascript)