Skip to content

PixelShober/PhilipsAmbilightToWLED

Repository files navigation

Philips Ambilight → WLED Sync

Python tools to grab Ambilight colors from a Philips TV (JointSpace API v6) and stream them live to WLED via UDP. Includes an interactive main.py that handles pairing once, stores config locally, and then just runs the sync.

Files

  • main.py – interactive launcher (recommended entrypoint).
  • tv2wled.py – core library/CLI (pair, discover, run).
  • pair_request.py / grant_pair.py – optional manual two-step pairing helpers.
  • requirements.txt – dependencies.
  • config.json / pair_state.jsonlocal runtime config; ignored by git.

Quick start (recommended)

  1. Install deps: pip install -r requirements.txt
  2. Run the interactive launcher:
    python main.py
    • Enter your TV IP when prompted; a PIN appears on the TV.
    • Enter the TV PIN when asked.
    • Enter your WLED IP/hostname (and optionally per-side LED counts).
  • config.json is written locally; it contains pairing creds and layout/WLED settings (git-ignored).
  1. On the next run, just call python main.py again. It will reuse config.json and start syncing. Stop with Ctrl+C.

Using the core CLI directly (tv2wled.py)

  • Discover TVs (best-effort SSDP):
    python tv2wled.py discover
  • Pair in one step and write config:
    python tv2wled.py pair --tv 192.168.1.50 --out config.json
  • Run sync (values fall back to config.json when flags omitted):
    python tv2wled.py run \
      --config config.json \
      --wled 192.168.1.80 \
      --fps 20 \
      --tv 192.168.1.50 \
      --top-leds 40 --right-leds 20 --bottom-leds 40 --left-leds 20 \
      --clockwise 1 --start-corner bottom_left --offset 0
    Omit --top/right/bottom/left-leds to auto-distribute using the TV topology and LED_COUNT env (default 120). Use --flip-* / --offset / --start-corner to match your strip orientation. Optional watchdog: --exit-after-no-data 30 (or set exit_after_no_data_s in config.json) to stop the sync when Ambilight data is unavailable.

Mapping sides onto a single strip (segments)

If your strip runs in one line and you want explicit index ranges per side (e.g., no bottom LEDs):

python tv2wled.py run --config config.json --wled 192.168.1.80 \
  --segment-left 20:50 --segment-top 50:80 --segment-right 80:100 \
  --bottom-leds 0
  • Ranges are start:end (end exclusive). Each side is resampled to fit the range.
  • --strip-length can be provided if you want a fixed output length; otherwise the max end is used.
  • Setting bottom_leds to 0 disables bottom-side sampling.
  • Advanced: partial side mapping with percentages: start:end[:pctStart[:pctEnd]]. Example: --segment-left 10:20:0:20 maps only the lower 20% of the left side into strip indices 10–19. You can repeat the same --segment-<side> flag to split a side across multiple ranges.

HTTP API (FastAPI, no auth)

If you want to switch between two configs ("normal" / "max") from Home Assistant, you can run the included FastAPI wrapper in Docker.
It starts/stops tv2wled.py run as a subprocess and exposes simple endpoints.

Docker quick start

  1. Create two config files next to this repo:
    • config_normal.json
    • config_max.json
  2. Start the container:
    docker compose up -d --build
  3. Switch modes via HTTP:
    • POST http://<host>:8000/mode/normal
    • POST http://<host>:8000/mode/max
    • POST http://<host>:8000/stop
    • GET http://<host>:8000/status
    • GET http://<host>:8000/logs?lines=200

Environment variables (optional):

  • TV2WLED_CONFIG_NORMAL (default /configs/normal.json)
  • TV2WLED_CONFIG_MAX (default /configs/max.json)
  • TV2WLED_DEFAULT_MODE (normal|max, default normal)
  • TV2WLED_AUTOSTART (1|0, default 1)
  • TV2WLED_EXTRA_ARGS (extra args appended to tv2wled.py run ...)
  • TV2WLED_EXIT_AFTER_NO_DATA_S (adds --exit-after-no-data N automatically)

Home Assistant example

rest_command:
  tv2wled_normal:
    url: "http://<host>:8000/mode/normal"
    method: POST
  tv2wled_max:
    url: "http://<host>:8000/mode/max"
    method: POST
  tv2wled_stop:
    url: "http://<host>:8000/stop"
    method: POST

sensor:
  - platform: rest
    name: tv2wled_status
    unique_id: tv2wled_status
    resource: "http://<host>:8000/status"
    value_template: "{{ value_json.running }}"
    json_attributes:
      - mode
      - pid
      - uptime_s
      - last_exit_code

### Home Assistant includes, unique_id, UI buttons (our setup)
- `configuration.yaml` (oder passende includes):
  ```yaml
  rest_command: !include rest_commands.yaml
  sensor: !include sensors.yaml
  binary_sensor: !include binary_sensors.yaml  # optional template helper
  switch: !include switches.yaml               # optional template switches
  • rest_commands.yaml (Mapping, kein leading -):
    tv2wled_normal:
      url: "http://192.168.178.166:8001/mode/normal"
      method: POST
    tv2wled_max:
      url: "http://192.168.178.166:8001/mode/max"
      method: POST
    tv2wled_stop:
      url: "http://192.168.178.166:8001/stop"
      method: POST
  • sensors.yaml (Liste, mit unique_id, sonst UI-Warnung):
    - platform: rest
      name: tv2wled_status
      unique_id: tv2wled_status
      resource: "http://192.168.178.166:8001/status"
      value_template: "{{ value_json.running }}"
      json_attributes:
        - mode
        - desired_mode
        - pid
        - uptime_s
        - last_exit_code
        - op_in_progress
        - op_error
        - log_lines
  • Optional binary_sensors.yaml:
    - platform: template
      sensors:
        tv2wled_running:
          friendly_name: "tv2wled running"
          unique_id: tv2wled_running
          value_template: "{{ states('sensor.tv2wled_status') in ['True','true','on','1'] }}"
          icon_template: >
            {% if is_state('binary_sensor.tv2wled_running','on') %}mdi:led-on{% else %}mdi:led-off{% endif %}
  • Smoother Switches (nutzen desired_mode, schicken stop zum Ausschalten, schneller Poll):
    • REST-Sensor mit k?rzerem Intervall:
      - platform: rest
        name: tv2wled_status
        unique_id: tv2wled_status
        resource: "http://192.168.178.166:8001/status"
        scan_interval: 2
        value_template: "{{ value_json.running }}"
        json_attributes:
          - mode
          - desired_mode
          - pid
          - uptime_s
          - last_exit_code
          - op_in_progress
          - op_error
          - log_lines
    • Template-Switches (switches.yaml), die sofort desired_mode zeigen, per stop ausschalten und nur verf?gbar sind, wenn das Ambilight-Licht an ist:
      - platform: template
        switches:
          tv2wled_mode_normal:
            friendly_name: "tv2wled Normal"
            unique_id: tv2wled_mode_normal
            availability: "{{ is_state('light.wohnzimmer_fernseher_ambilight','on') }}"
            value_template: >
              {% set d = state_attr('sensor.tv2wled_status','desired_mode') %}
              {% set m = state_attr('sensor.tv2wled_status','mode') %}
              {{ d == 'normal' or (d is none and m == 'normal') }}
            turn_on: { service: rest_command.tv2wled_normal }
            turn_off: { service: rest_command.tv2wled_stop }
          tv2wled_mode_max:
            friendly_name: "tv2wled Max"
            unique_id: tv2wled_mode_max
            availability: "{{ is_state('light.wohnzimmer_fernseher_ambilight','on') }}"
            value_template: >
              {% set d = state_attr('sensor.tv2wled_status','desired_mode') %}
              {% set m = state_attr('sensor.tv2wled_status','mode') %}
              {{ d == 'max' or (d is none and m == 'max') }}
            turn_on: { service: rest_command.tv2wled_max }
            turn_off: { service: rest_command.tv2wled_stop }
  • HA neu starten nach ?nderungen. Danach in der UI (Lovelace) Button-Karten anlegen (Dienst rest_command.tv2wled_normal|max|stop) und eine Entities-Karte mit sensor.tv2wled_status (optional binary_sensor.tv2wled_running).

Runbook: Proxmox Debian-LXC + Docker + Home Assistant (normal/max)

This is a "what we actually did" checklist so the setup can be reproduced later without extra context.

0) Proxmox notes (LXC)

  • Docker in an LXC container requires Proxmox CT features like nesting (and often keyctl). If Docker behaves oddly, consider running this in a small VM instead.

1) Folder layout (compose in parent, tv2wled in subfolder)

Example:

  • /opt/homelab/docker-compose.yml
  • /opt/homelab/tv2wled/ (this repo: Dockerfile, tv2wled.py, tv2wled_api.py, ...)
  • /opt/homelab/tv2wled/config_normal.json (secrets, git-ignored)
  • /opt/homelab/tv2wled/config_max.json (secrets, git-ignored)

Important:

  • The Docker build uses the build context. If docker-compose.yml is in the parent directory, set build.context: ./tv2wled so the Dockerfile and Python files are included.
  • Config files are mounted as volumes (read-only). They are not copied into the image (see .dockerignore).

2) docker-compose service snippet (build from ./tv2wled)

We hit Bind for 0.0.0.0:8000 failed: port is already allocated, so we mapped host port 8001 to container port 8000:

services:
  tv2wled:
    build:
      context: ./tv2wled
    container_name: homelab-tv2wled
    restart: unless-stopped
    ports:
      - "8001:8000"
    environment:
      TV2WLED_DEFAULT_MODE: normal
      TV2WLED_AUTOSTART: "1"
      TV2WLED_CONFIG_NORMAL: /configs/normal.json
      TV2WLED_CONFIG_MAX: /configs/max.json
      # optional watchdog without editing configs:
      # TV2WLED_EXIT_AFTER_NO_DATA_S: "30"
    volumes:
      - ./tv2wled/config_normal.json:/configs/normal.json:ro
      - ./tv2wled/config_max.json:/configs/max.json:ro

3) Install Docker inside the Debian-LXC

Commands used:

apt update
apt install -y ca-certificates curl gnupg

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker

docker --version
docker compose version

4) Build & start the container

From /opt/homelab (where docker-compose.yml is):

docker compose up -d --build tv2wled
docker compose ps tv2wled
docker compose logs --tail 200 tv2wled

Force a rebuild/recreate after changing files:

docker compose up -d --build --force-recreate tv2wled

5) DNS problem during build (pip install fails)

Symptom (during docker compose build):

  • Temporary failure in name resolution while running pip install ...

Host DNS looked fine (getent hosts pypi.org worked), but Docker build containers had DNS issues.

Diagnostics commands used:

cat /etc/resolv.conf
getent hosts pypi.org

docker run --rm python:3.11-slim python -c "import socket; print(socket.gethostbyname('pypi.org'))"
docker run --rm python:3.11-slim python -c "import urllib.request; print(urllib.request.urlopen('https://pypi.org/simple/requests/', timeout=10).status)"

Fix (set Docker daemon DNS) + restart Docker:

mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'JSON'
{
  "dns": ["1.1.1.1", "8.8.8.8"]
}
JSON

systemctl restart docker

If you also want local DNS names (e.g. fritz.box) inside containers, include your LAN DNS first:

cat > /etc/docker/daemon.json <<'JSON'
{
  "dns": ["192.168.178.166", "1.1.1.1", "8.8.8.8"]
}
JSON

systemctl restart docker

6) Port already allocated (8000)

Symptom:

  • Bind for 0.0.0.0:8000 failed: port is already allocated

Commands used to find the offender:

ss -ltnp | grep ':8000'
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep 8000

Fix:

  • Use another host port (we used 8001:8000), or stop the other service/container.

7) API endpoints to test (host 192.168.178.166:8001)

FastAPI (this project) returns JSON on /health. If you see plain OK, you are hitting another service.

Test commands:

curl -sS http://192.168.178.166:8001/health
curl -sS http://192.168.178.166:8001/status
curl -sS "http://192.168.178.166:8001/logs?lines=200"

curl -sS -X POST http://192.168.178.166:8001/mode/normal
curl -sS -X POST http://192.168.178.166:8001/mode/max
curl -sS -X POST http://192.168.178.166:8001/stop

Convenience GET aliases also exist:

curl -sS http://192.168.178.166:8001/mode/normal
curl -sS http://192.168.178.166:8001/mode/max
curl -sS http://192.168.178.166:8001/stop

8) Stop sync when Ambilight data is unavailable (watchdog)

Two options:

  1. Per config file: add a top-level field (same level as tv_ip):
    "exit_after_no_data_s": 30
  2. Or via Docker env without editing configs:
    • TV2WLED_EXIT_AFTER_NO_DATA_S: "30"

Internally this uses tv2wled.py run --exit-after-no-data 30 and exits the sync process after 30s without Ambilight data.

9) Quick local test (without Docker)

From repo root (needs Python 3.9+):

pip install -r requirements-api.txt
uvicorn tv2wled_api:app --host 0.0.0.0 --port 8000

Then hit http://localhost:8000/health, /status, /mode/normal.

10) What changed in this repo (summary)

  • Added tv2wled_api.py (FastAPI wrapper, no auth) to start/stop/switch tv2wled between two configs.
  • Added Dockerfile, docker-compose.yml, requirements-api.txt, .dockerignore.
  • Updated tv2wled.py to support --exit-after-no-data / exit_after_no_data_s.
  • Updated .gitignore to ignore config_*.json (normal/max configs contain secrets).

Optional manual two-step pairing

For debugging or manual flows (not needed when using main.py):

  1. Request PIN:
    python pair_request.py --tv 192.168.1.50
    This writes pair_state.json and shows a PIN on the TV.
  2. Grant using the PIN:
    python grant_pair.py --pin 1234
    This writes config.json. Then run python main.py.

Config notes

  • config.json holds TV credentials, layout, and WLED settings. It’s git-ignored.
  • Key fields:
    • tv_ip, base_url, device_id, auth_key
    • layout: top_leds/right_leds/bottom_leds/left_leds, flips, start_corner, clockwise, offset
    • wled: host, udp_port (default 21324), timeout_s, fps

Requirements

  • Python 3.9+ recommended
  • Philips TV with JointSpace enabled (API v6)
  • Network access between TV and host, and between host and WLED

About

Get Ambilight Data from your local TV and control your WLED in the room on base of your data

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published