[{"content":"A working log of getting Hyperion-NG capture validated on old hardware. Splitter arrives tomorrow, LEDs not yet wired, but the capture pipeline is fully proven end to end.\nThe goal Build a Govee/Philips-Hue-Sync style TV backlight that mirrors on-screen colors to an LED strip behind the TV. Source-agnostic (works with anything plugged into HDMI, not just smart TV apps), running fully local, no cloud, no subscription.\nThe architecture Apple TV (4K) → HDCP-stripping HDMI splitter ─┬→ Samsung TV (4K passthrough) └→ Auvidea B101 (1080p) → Pi 3 (Hyperion-NG) → Ethernet → ESP32 (WLED) → SK6812 LED strip Three things matter about this architecture:\nThe splitter strips HDCP so the capture device sees a clean signal. Without this, modern sources (Apple TV, PS5, anything streaming) refuse to output to a non-compliant capture path. The splitter downscales 4K → 1080p on Output 2 because the B101 caps at 1080p. The TV still gets full 4K HDR on Output 1. Pi handles capture and processing, ESP32 handles LEDs. Splitting the work this way is more reliable than one device doing both. Hardware inventory What I had on hand:\nRaspberry Pi 3 Model B v1.2 Auvidea B101 HDMI-to-CSI capture board (TC358743 chip) CSI ribbon cable What I ordered:\nHBAVLINK 1x2 HDMI Splitter (auto-downscaling, HDCP 2.2/2.3 bypass) — the model that explicitly lists \u0026ldquo;Apple TV 4K, Elgato HD60s/x/Pro, Retrotink\u0026rdquo; compatibility on the listing Still need: SK6812 RGBW strip (60 LED/m, 5m), ESP32-WROOM-32, 5V 10A PSU, capacitors/resistor for clean wiring The Pi 3 + B101 combo is the reference Hyperion setup. The B101 plugs into the Pi\u0026rsquo;s CSI camera port via ribbon cable, which means capture goes through the GPU/ISP path instead of USB. That offloads enough work from the CPU that a Pi 3 can keep up with 1080p60 capture, which surprised me. I\u0026rsquo;d written off the Pi 3 initially.\nOS setup Flashed Raspberry Pi OS Lite (64-bit), Trixie release using Raspberry Pi Imager on a Mac.\nThe 64-bit version runs fine on the Pi 3 (BCM2837 is a 64-bit ARM Cortex-A53), and Hyperion has better-tuned 64-bit ARM builds. Lite, not Desktop, because this box is headless and you don\u0026rsquo;t want a GUI competing for resources.\nWhat didn\u0026rsquo;t work the first time First flash attempt: the Imager OS customization settings (hostname, SSH key, username) didn\u0026rsquo;t actually get written to the SD card. Booted into a vanilla install with no SSH, default user, default hostname. Cause was probably skipping or dismissing the \u0026ldquo;Apply OS customisation settings\u0026rdquo; dialog that pops up after clicking write.\nLesson: that dialog is mandatory for headless setup. If you miss it, you have to reflash or configure on the box directly with a keyboard.\nSecond flash: configured everything in the customization dialog upfront. Pi booted, joined WiFi, accepted SSH on first boot.\nDiagnosing the failed boot When the Pi only showed a red PWR LED (no green ACT activity), I ran through:\nRe-seat the SD card (friction fit, no click on Pi 3) Check power supply (Pi 3 needs a real 2.5A 5V source) Check ACT LED behavior on a working boot (rapid blinking during first ~30s, then sporadic) Plug in HDMI to a monitor as a fallback to see boot messages Re-flashing solved it. Most likely cause: SD card flash verification silently failed.\nInitial Pi configuration Once SSH was working, set hostname and updated:\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo reboot Then SSH\u0026rsquo;d back in as derek@hyperion.local.\nEnabling the B101 Edit /boot/firmware/config.txt (note: not /boot/config.txt — the path changed in Bookworm and stayed in Trixie):\ndtoverlay=tc358743 gpu_mem=128 The gpu_mem=128 is critical. Default 64MB is too tight on a Pi 3 for CSI capture buffers.\nAfter reboot, verified the chip was detected:\ndmesg | grep -i tc358743 Output:\n[ 0.041913] /soc/csi@7e801000: Fixed dependency cycle(s) with /soc/i2c0mux/i2c@1/tc358743@f [ 0.042020] /soc/i2c0mux/i2c@1/tc358743@f: Fixed dependency cycle(s) with /soc/csi@7e801000 [ 0.043604] /soc/csi@7e801000: Fixed dependency cycle(s) with /soc/i2c0mux/i2c@1/tc358743@f [ 0.045271] /soc/i2c0mux/i2c@1/tc358743@f: Fixed dependency cycle(s) with /soc/csi@7e801000 [ 12.091266] tc358743 10-000f: tc358743 found @ 0x1e (i2c-11-mux (chan_id 1)) The \u0026ldquo;Fixed dependency cycle(s)\u0026rdquo; messages are harmless boot-time noise. The line that matters: tc358743 found @ 0x1e. Driver bound.\nls /dev/video* Output: /dev/video0 exists alongside the Pi\u0026rsquo;s GPU encoder/decoder devices (video10–23, 31). Hyperion only cares about video0.\nEDID setup (where things got messy) The TC358743 needs an EDID telling source devices what resolutions it accepts. Without one, Apple TV (and most sources) won\u0026rsquo;t output anything.\nWhat didn\u0026rsquo;t work The Hyperion forum tutorials and Github gists all reference repos that 404 in 2026:\ngithub.com/hyperion-project/RPi-Tools — gone github.com/peterpan007/RPi-tc358743-EDID — gone github.com/hyperion-project/HyperBian/raw/master/edid/ — gone I tried a manual hex-dump approach (writing the EDID bytes to a file via xxd -r), but my hex-dump output didn\u0026rsquo;t pass v4l2-ctl\u0026rsquo;s validation:\n/home/derek/edid.bin contained an empty EDID, ignoring. 256 bytes on disk but invalid structure. Not worth debugging when there\u0026rsquo;s a better path.\nAlso hit some flag changes in newer v4l-utils (1.30+):\n--fix-edid-checksums no longer recognized The -d flag now needs to come before --set-edid The --set-edid argument now requires pad=0 to be specified What worked The cleanest path: v4l2-ctl ships with built-in EDID generators. No external file needed.\nv4l2-ctl -d /dev/v4l-subdev0 --set-edid=type=hdmi,pad=0 (Confirmed /dev/v4l-subdev0 was the right path via v4l2-ctl --list-devices — only one subdev existed and it was bound to the unicam/CSI device.)\nSilent output = success. The B101 now advertises 1080p capability to whatever source is connected.\nThe validation hack: HDMI loopback Splitter doesn\u0026rsquo;t arrive until tomorrow, but I wanted to validate the capture pipeline tonight. So I plugged the Pi\u0026rsquo;s own HDMI output back into the B101\u0026rsquo;s HDMI input.\nThis isn\u0026rsquo;t a \u0026ldquo;real\u0026rdquo; test (the Pi running headless Lite is just outputting a console framebuffer, no HDCP, no real content) but it does prove the capture path works end to end.\nv4l2-ctl --query-dv-timings -d /dev/video0 Output:\nActive width: 1920 Active height: 1080 Total width: 2200 Total height: 1125 Frame format: progressive Polarities: -vsync -hsync Pixelclock: 148500000 Hz (60.00 frames per second) Horizontal frontporch: 0 Horizontal sync: 280 Horizontal backporch: 0 Vertical frontporch: 0 Vertical sync: 45 Vertical backporch: 0 1920x1080p60, 148.5 MHz pixel clock. Standard 1080p60 timing. B101 capturing successfully.\nThen locked the timings and checked format:\nv4l2-ctl --set-dv-bt-timings query -d /dev/video0 v4l2-ctl --get-fmt-video -d /dev/video0 Output:\nBT timings set Format Video Capture: Width/Height : 1920/1080 Pixel Format : \u0026#39;BGR3\u0026#39; (24-bit BGR 8-8-8) Field : None Bytes per Line : 5760 Size Image : 6220800 Colorspace : sRGB Transfer Function : Default (maps to sRGB) YCbCr/HSV Encoding: Default (maps to ITU-R 601) Quantization : Default (maps to Full Range) This is the ideal format for Hyperion on a Pi 3:\nBGR3 24-bit = uncompressed RGB, no decode overhead 5760 bytes/line × 1080 = 6.2MB/frame = correct for raw RGB at 1080p sRGB, full range = correct for HDMI source No CPU cycles wasted on YUV → RGB conversion Additional source testing (the night got long) After the Pi loopback validated capture, I got curious about whether any non-HDCP source I had lying around could capture without the splitter. Three more tests, three different failure modes — all educational.\nChromecast 3rd gen (white pill) v4l2-ctl --query-dv-timings -d /dev/video0 Output:\nVIDIOC_QUERY_DV_TIMINGS: failed: Link has been severed Active width: 0 [...all zeros...] \u0026ldquo;Link has been severed\u0026rdquo; = the source negotiated and then refused. Classic HDCP rejection. The Chromecast detected the B101 isn\u0026rsquo;t HDCP-compliant and killed the link. This is the exact failure mode the HDCP-stripping splitter solves.\nThis is good news disguised as bad news. It\u0026rsquo;s a clean confirmation that HDCP is the only thing standing between the Apple TV and the B101. Hardware, driver, EDID, capture format — all fine. Just need the splitter.\nMacBook Air via USB-C → HDMI adapter Mirrored desktop, no protected content playing, B101 plugged in via USB-C dongle.\nSame Link has been severed error.\nmacOS appears to enforce HDCP even on a mirrored desktop in some configurations. Or the USB-C dongle is doing something weird with EDID negotiation. Either way, not a useful test source.\nHome Assistant OS box (mini PC) Different error this time:\nVIDIOC_QUERY_DV_TIMINGS: failed: No locks available \u0026ldquo;No locks available\u0026rdquo; means the B101 isn\u0026rsquo;t seeing a stable signal at all, vs. the Chromecast\u0026rsquo;s explicit handshake rejection.\nInvestigation:\n# SSH\u0026#39;d into HAOS console cat /sys/class/drm/*/status # Output: disconnected HAOS wasn\u0026rsquo;t driving the HDMI port at all. The kernel reported the DRM output as disconnected. Likely either headless boot config (display not detected at boot, port stays disabled) or HAOS just doesn\u0026rsquo;t bother outputting console video the way a regular Linux distro does.\nDifferent failure mode, unrelated to HDCP. Just a non-functional source.\nWhat the three failures teach Source Error Cause Splitter fixes? Pi loopback (none, worked) n/a n/a Chromecast Link severed HDCP rejection Yes Mac via USB-C Link severed HDCP rejection Yes HA box No locks available No HDMI output n/a (broken source) Two distinct error messages map to two distinct problem categories. Worth knowing for future debugging:\n\u0026ldquo;Link severed\u0026rdquo; = handshake started, rejected. HDCP, EDID conflict, or content protection. \u0026ldquo;No locks available\u0026rdquo; = no stable signal seen. Source not outputting, cable issue, or extreme resolution mismatch. What\u0026rsquo;s validated so far Pi 3 boots, on network, SSH working TC358743 driver loaded and bound /dev/video0 exists and captures EDID applied correctly via v4l2-ctl --set-edid=type=hdmi,pad=0 1080p60 timings detected and locked (via Pi loopback) Capture format is BGR3 (best case for Hyperion) HDCP confirmed as the blocker for real sources (justifies the splitter purchase retroactively) Trying to capture an actual frame (educational rabbit hole) After validation passed, I wanted to see a captured frame, not just confirm timings. Tried two paths.\nPath 1: Raw v4l2-ctl frame grab v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=BGR3 -d /dev/video0 v4l2-ctl --stream-mmap --stream-count=1 --stream-to=/tmp/frame.raw -d /dev/video0 Result: 0-byte file. The format set silently but streaming returned VIDIOC_STREAMON: Invalid argument.\nTried with explicit buffer count (--stream-mmap=3), same error.\nThe TC358743 driver advertises BGR3 as a supported format on this kernel but rejects it at STREAMON time. Underlying issue: the chip outputs UYVY natively and BGR3 conversion needs to happen in the ISP pipeline, which the unicam driver isn\u0026rsquo;t fully wiring up. Common quirk on this hardware.\nPath 2: Force UYVY format v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=UYVY v4l2-ctl --get-fmt-video -d /dev/video0 Format set successfully:\nPixel Format : \u0026#39;UYVY\u0026#39; (UYVY 4:2:2) Bytes per Line : 3840 Size Image : 4147200 Colorspace : SMPTE 170M This is the chip\u0026rsquo;s actual native format. But Hyperion overrides this on its own startup, so the manual format setting doesn\u0026rsquo;t persist into Hyperion\u0026rsquo;s grabber.\nInstalling Hyperion-NG The official install script URL is dead in 2026:\nbash \u0026lt;(curl -sL https://releases.hyperion-project.org/install) # Returns HTML 404, breaks bash with syntax error The current method is direct .deb install from GitHub releases. APT repo also broken for both Trixie and Bookworm (Release file not found).\nWhat worked:\ncurl -s https://api.github.com/repos/hyperion-project/hyperion.ng/releases/latest | grep \u0026#34;browser_download_url\u0026#34; Showed available builds for the latest release (2.2.1). For 64-bit Pi OS, grab the arm64 deb (not aarch64 — naming changed):\ncd /tmp wget https://github.com/hyperion-project/hyperion.ng/releases/download/2.2.1/Hyperion-2.2.1-Linux-arm64.deb sudo apt install ./Hyperion-2.2.1-Linux-arm64.deb -y Installs cleanly. Service auto-starts as hyperion@\u0026lt;username\u0026gt;. Web UI at port 8090.\nsudo systemctl status hyperion@derek # Active: active (running) Hyperion + B101 streaming issue Web UI auto-discovered the B101 as unicam-image in USB Capture settings. Configured:\nActivate: yes Device: unicam-image FPS: 30 Size decimation: 4 (default 8 was way too aggressive for 1080p input) Saved settings. Same VIDIOC_STREAMON error in Hyperion logs:\nhyperiond: VIDIOC_STREAMON failed: Invalid argument hyperiond: Throws error nr: VIDIOC_STREAMON error code 22, Invalid argument hyperiond: Throws error nr: VIDIOC_DQBUF error code 22, Invalid argument Pre-setting UYVY via v4l2-ctl before starting Hyperion didn\u0026rsquo;t help — Hyperion\u0026rsquo;s V4L2 grabber resets the format on open. There\u0026rsquo;s no exposed format selection in the web UI on this Hyperion version.\nThis is a known TC358743 + Hyperion compatibility issue that needs proper research:\nMay resolve with a real source signal (Apple TV via splitter) vs the Pi\u0026rsquo;s loopback HDMI which has known weird timing parameters May need a Hyperion config file edit to force the format May need a different Hyperion build or a kernel module workaround Stopping here for the day. The streaming format issue is the next debug target.\nCurrent state of the project What works Pi 3 booted, networked, SSH key-only auth B101 detected, driver bound, EDID applied /dev/video0 exists and reports correct timings (1080p60) Capture pipeline validated at the protocol level (timings, BT timings lock, format negotiation possible) Hyperion installed, running as systemd service, web UI accessible Hyperion auto-discovers the B101 as a capture device What doesn\u0026rsquo;t work yet VIDIOC_STREAMON fails for Hyperion\u0026rsquo;s V4L2 grabber on this Hyperion + TC358743 + Trixie combo Live preview not yet visible in Hyperion UI HDCP-protected sources (Apple TV, Chromecast) can\u0026rsquo;t connect directly — need splitter What\u0026rsquo;s pending Splitter arrival LED hardware order Resolution of the STREAMON format issue Then: WLED on ESP32, DDP config, LED layout calibration LED hardware order TV: Samsung QN65Q80D, 65\u0026quot;, outer dimensions 1446.5 × 829.3 mm, active panel ~1428 × 803 mm.\nLED counts at 60 LED/m (16.67mm pitch):\nTop: 86 Right: 48 Bottom: 86 Left: 48 Total: 268 LEDs The 5m reel (300 LEDs) gives ~32 LEDs of buffer for cuts and corner gaps. Power budget: 268 SK6812 RGBW LEDs at typical brightness pulls ~5-7A with 10-12A spikes during bright scenes.\nThe order Item Source Cost BTF-Lighting SK6812 RGBW Natural White, 5m, 60 LED/m, IP30 Amazon ~$45 HiLetgo ESP32-WROOM-32, 2-pack (USB-C) Amazon ~$15 ALITOVE 5V 15A 75W PSU (upgraded from 10A for headroom) Amazon ~$30 Barrel jack to screw terminal adapters, 5-pack Amazon ~$7 18 AWG silicone wire kit, red/black 25ft each Amazon ~$13 Electronic components kit (capacitors, resistors) Amazon ~$13 Total ~$123 Why these specific choices:\nSK6812 RGBW \u0026gt; WS2812B RGB — true white channel for bias lighting and skin tones, $10 more 60 LED/m \u0026gt; 30 LED/m — smoother color transitions, no visible gaps at typical TV viewing distance Natural white (4000K) — closer to typical TV color temp than warm or cool variants 15A PSU — 268 LEDs at edge of 10A capacity, 15A gives headroom without forcing brightness caps Power injection at both ends mandatory — voltage drop across 5m of strip causes far-end dimming and color shift; this is non-negotiable for \u0026gt;2m runs ESP32-WROOM-32 \u0026gt; ESP32-S2/S3/C3 — WLED most stable on original ESP32 What\u0026rsquo;s left when I\u0026rsquo;m back Plug in HBAVLINK splitter, validate Apple TV → splitter → B101 chain Debug the Hyperion VIDIOC_STREAMON format issue (may resolve with real source) Confirm live preview in Hyperion web UI Flash WLED to ESP32, configure for SK6812 RGBW + 268 LEDs Wire up strip with power injection at both ends, capacitor on PSU input, resistor on data line Configure Hyperion → DDP → ESP32 over Ethernet Calibrate LED layout (top: 86, right: 48, bottom: 86, left: 48) Color/gamma/saturation tuning Samsung Anynet+ + Apple TV \u0026ldquo;Control TVs and Receivers\u0026rdquo; for seamless power UX Lessons so far Documentation rot is real. Every Hyperion tutorial I found referenced repos and tools that have been moved, renamed, or deleted. The community has fragmented since the Hyperion-Project forum\u0026rsquo;s heyday. The official Hyperion-NG repo is still maintained, but supporting tooling (EDID files, install scripts on third-party mirrors, the APT repo) is mostly broken in 2026. The reliable path is GitHub Releases directly.\nv4l2-ctl is your friend, but only up to a point. When external EDID files don\u0026rsquo;t work, the built-in generators (type=hdmi) are reliable. When format negotiation fails, you can force formats. But Hyperion overrides v4l2-ctl settings on its own startup, so manual format setting doesn\u0026rsquo;t persist into the actual capture pipeline.\nThe Pi 3 + B101 combo is more capable than expected at the protocol level. The CSI capture path keeps the CPU free enough to read 1080p60 BGR3 frames via the driver. Whether Hyperion can actually grab those frames is a different question.\nValidate piece by piece. Capture pipeline is proven independently from Hyperion. When the splitter shows up, the only new variable is HDCP handshake and downscaling. Everything else is locked in. Same approach helped diagnose the streaming format issue — it\u0026rsquo;s not the hardware, not the driver, not the EDID, not the timings, it\u0026rsquo;s the format negotiation between Hyperion and the unicam driver.\nHDCP enforcement is real and granular. Different sources fail differently:\nChromecast 3rd gen: explicit handshake rejection macOS USB-C HDMI: same rejection (HDCP enforced even on mirrored desktop) Pi loopback: works fine (no HDCP enforced) HAOS: didn\u0026rsquo;t even output HDMI (unrelated failure mode) Knowing which error means what saves debugging time later.\nHardware loopbacks are useful but not real tests. The Pi 3 outputting to its own B101 input validated the capture path at the timing/format query level but might not be a clean enough source to validate streaming. Modern TVs and streaming devices output much cleaner HDMI signals than a Pi headless console.\nSetup commands reference For my own future reference, the commands that actually worked in order:\n# After Imager flash with customization, SSH in and update sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo reboot # Edit boot config sudo nano /boot/firmware/config.txt # Add at bottom: # dtoverlay=tc358743 # gpu_mem=128 sudo reboot # Verify driver bound dmesg | grep -i tc358743 ls /dev/video* ls /dev/v4l-subdev* v4l2-ctl --list-devices # Install missing tools sudo apt install v4l-utils -y # Set EDID via built-in generator (skip the external EDID file approach, all the URLs are dead) v4l2-ctl -d /dev/v4l-subdev0 --set-edid=type=hdmi,pad=0 # Validate capture (need a connected source) v4l2-ctl --query-dv-timings -d /dev/video0 v4l2-ctl --set-dv-bt-timings query -d /dev/video0 v4l2-ctl --get-fmt-video -d /dev/video0 # Install Hyperion (skip the dead install script, go direct to GitHub releases) cd /tmp curl -s https://api.github.com/repos/hyperion-project/hyperion.ng/releases/latest | grep \u0026#34;browser_download_url\u0026#34; # Pick arm64.deb URL from output wget https://github.com/hyperion-project/hyperion.ng/releases/download/2.2.1/Hyperion-2.2.1-Linux-arm64.deb sudo apt install ./Hyperion-2.2.1-Linux-arm64.deb -y # Verify service sudo systemctl status hyperion@$USER # Web UI: http://hyperion.local:8090 # Watch logs while debugging sudo journalctl -u hyperion@$USER -f # Force pixel format (workaround attempt for VIDIOC_STREAMON issues) v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=UYVY To be continued when the splitter arrives and after I\u0026rsquo;m back from travel. Next focus: cracking the VIDIOC_STREAMON format issue with a real source signal.\n","permalink":"https://derek.engineer/posts/diy-ambilight-pi3-b101-setup/","summary":"\u003cp\u003eA working log of getting Hyperion-NG capture validated on old hardware. Splitter arrives tomorrow, LEDs not yet wired, but the capture pipeline is fully proven end to end.\u003c/p\u003e\n\u003ch2 id=\"the-goal\"\u003eThe goal\u003c/h2\u003e\n\u003cp\u003eBuild a Govee/Philips-Hue-Sync style TV backlight that mirrors on-screen colors to an LED strip behind the TV. Source-agnostic (works with anything plugged into HDMI, not just smart TV apps), running fully local, no cloud, no subscription.\u003c/p\u003e\n\u003ch2 id=\"the-architecture\"\u003eThe architecture\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eApple TV (4K) → HDCP-stripping HDMI splitter ─┬→ Samsung TV (4K passthrough)\n                                               └→ Auvidea B101 (1080p) → \n                                                  Pi 3 (Hyperion-NG) → \n                                                  Ethernet → \n                                                  ESP32 (WLED) → \n                                                  SK6812 LED strip\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThree things matter about this architecture:\u003c/p\u003e","title":"Building a DIY Ambilight on a Raspberry Pi 3 with an Auvidea B101"},{"content":"The IKEA Varmblixt is a $30 RGB+white Zigbee pendant. It pairs directly to ZHA in Home Assistant, has smooth color transitions, and a wide enough color gamut to be useful as a status indicator. I turned it into an ambient display for my apartment — weather during the day, a sunset script at night, a cycling green light at 6:30am, and a solid red door alarm when I forget to close the front door.\nHardware IKEA Varmblixt ($30 at IKEA) — RGB+white, Zigbee Pairs over ZHA (no hub, no IKEA Dirigera needed) Entity: light.led_light0x07c2_varmblixt_table_wall_lamp Pair it the same as any ZHA device: hold the reset button until it blinks, then add device in the ZHA integration.\nScripts Three color scripts live in scripts.yaml. All use mode: restart so calling a new script immediately cancels the previous one without any cleanup logic.\ncotton_candy_sunset The default sunset mode. Slow amber drift, 45-second crossfades between two warm tones. Runs continuously until midnight.\ncotton_candy_sunset: alias: Cotton Candy Sunset description: Slow dreamy fade between sunset amber and warm magenta — default sunset mode sequence: - repeat: count: 500 sequence: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: rgb_color: [220, 80, 5] brightness_pct: 50 transition: 45 - delay: 00:00:10 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: rgb_color: [255, 130, 10] brightness_pct: 50 transition: 45 - delay: 00:00:10 mode: restart color_drift Six colors, 4-second transitions, 5-second holds. Lava lamp pacing. Good for background ambience when not using the sunset mode.\ncolor_drift: alias: Color Drift description: Slow hypnotic color cycle on the donut — lava lamp vibe sequence: - repeat: count: 500 sequence: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [255, 134, 8], brightness_pct: 50, transition: 4} - delay: 00:00:05 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [200, 0, 180], brightness_pct: 50, transition: 4} - delay: 00:00:05 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [0, 180, 180], brightness_pct: 50, transition: 4} - delay: 00:00:05 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [120, 0, 220], brightness_pct: 50, transition: 4} - delay: 00:00:05 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [255, 60, 0], brightness_pct: 50, transition: 4} - delay: 00:00:05 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [0, 210, 40], brightness_pct: 50, transition: 4} - delay: 00:00:05 mode: restart party_pulse Five colors, 1-second transitions, 1-second holds. For when you want it to be annoying on purpose.\nparty_pulse: alias: Party Pulse description: Fast energetic color flashes on the donut sequence: - repeat: count: 500 sequence: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [255, 0, 120], brightness_pct: 80, transition: 1} - delay: 00:00:01 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [50, 255, 0], brightness_pct: 80, transition: 1} - delay: 00:00:01 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [0, 220, 255], brightness_pct: 80, transition: 1} - delay: 00:00:01 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [255, 100, 0], brightness_pct: 80, transition: 1} - delay: 00:00:01 - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: {rgb_color: [160, 0, 255], brightness_pct: 80, transition: 1} - delay: 00:00:01 mode: restart Automations Weather + Sunset Triggers on sunrise, sunset, and every hour. After sunset: runs cotton_candy_sunset. During the day: sets a static color based on the PirateWeather condition — fog is a cool blue-white, sun is yellow, rain is blue, clouds are light blue.\n- id: varmblixt_sf_weather_sunset alias: \u0026#39;Varmblixt: SF Weather \u0026amp; Sunset\u0026#39; triggers: - trigger: sun event: sunset - trigger: sun event: sunrise - trigger: time_pattern hours: /1 actions: - choose: - conditions: - condition: sun after: sunset sequence: - action: script.turn_on target: entity_id: script.cotton_candy_sunset - conditions: - condition: sun after: sunrise before: sunset sequence: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: brightness_pct: 70 rgb_color: \u0026gt; {% set s = states(\u0026#34;weather.pirateweather\u0026#34;) %} {% if s == \u0026#34;fog\u0026#34; %}[200,200,255] {% elif s == \u0026#34;sunny\u0026#34; %}[255,200,0] {% elif s in [\u0026#34;rainy\u0026#34;,\u0026#34;pouring\u0026#34;] %}[0,100,255] {% elif s in [\u0026#34;cloudy\u0026#34;,\u0026#34;partlycloudy\u0026#34;] %}[150,200,255] {% else %}[255,150,50]{% endif %} mode: single Condition Color Sunny Yellow [255,200,0] Cloudy / Partly Cloudy Light blue [150,200,255] Rainy / Pouring Blue [0,100,255] Fog Blue-white [200,200,255] Other Warm orange [255,150,50] Cycling Green Light At 6:30am, checks PirateWeather. If temperature is 55–78°F, wind under 15mph, and no rain or fog, the lamp goes green. It\u0026rsquo;s a go/no-go signal before I check my phone. At 9am a second automation triggers the weather automation to revert it.\n- id: cycling_weather_green alias: Cycling Weather - Green Light triggers: - trigger: time at: 06:30:00 conditions: - condition: template value_template: \u0026gt; {% set temp = state_attr(\u0026#39;weather.pirateweather\u0026#39;, \u0026#39;temperature\u0026#39;) | float(0) %} {% set wind = state_attr(\u0026#39;weather.pirateweather\u0026#39;, \u0026#39;wind_speed\u0026#39;) | float(99) %} {% set cond = states(\u0026#39;weather.pirateweather\u0026#39;) %} {{ temp \u0026gt;= 55 and temp \u0026lt;= 78 and wind \u0026lt; 15 and cond not in [\u0026#39;rainy\u0026#39;, \u0026#39;pouring\u0026#39;, \u0026#39;fog\u0026#39;, \u0026#39;hail\u0026#39;, \u0026#39;snowy\u0026#39;, \u0026#39;snowy-rainy\u0026#39;, \u0026#39;lightning\u0026#39;, \u0026#39;lightning-rainy\u0026#39;, \u0026#39;exceptional\u0026#39;] }} actions: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: rgb_color: [0, 210, 40] brightness_pct: 50 mode: single - id: cycling_weather_revert alias: Cycling Weather - Revert at 9am triggers: - trigger: time at: 09:00:00 actions: - action: automation.trigger target: entity_id: automation.varmblixt_sf_weather_sunset mode: single Door Open Alert If the front door stays open for more than 10 seconds, the lamp goes solid red at 100% brightness — visible from anywhere in the apartment. When the door closes, it automatically reverts to the correct mode (sunset script or weather color depending on time of day).\n- id: door_open_light_flash alias: Alert - Door Open Light Red triggers: - trigger: state entity_id: binary_sensor.ikea_of_sweden_parasoll_door_window_sensor to: \u0026#39;on\u0026#39; for: seconds: 10 actions: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: rgb_color: [255, 0, 0] brightness_pct: 100 - wait_for_trigger: - trigger: state entity_id: binary_sensor.ikea_of_sweden_parasoll_door_window_sensor to: \u0026#39;off\u0026#39; timeout: hours: 1 continue_on_timeout: true - choose: - conditions: - condition: sun after: sunset sequence: - action: script.turn_on target: entity_id: script.cotton_candy_sunset - conditions: - condition: sun after: sunrise before: sunset sequence: - action: light.turn_on target: entity_id: light.led_light0x07c2_varmblixt_table_wall_lamp data: brightness_pct: 70 rgb_color: \u0026gt; {% set s = states(\u0026#34;weather.pirateweather\u0026#34;) %} {% if s == \u0026#34;fog\u0026#34; %}[200,200,255] {% elif s == \u0026#34;sunny\u0026#34; %}[255,200,0] {% elif s in [\u0026#34;rainy\u0026#34;,\u0026#34;pouring\u0026#34;] %}[0,100,255] {% elif s in [\u0026#34;cloudy\u0026#34;,\u0026#34;partlycloudy\u0026#34;] %}[150,200,255] {% else %}[255,150,50]{% endif %} mode: single Midnight Off Stops all running scripts and turns the lamp off at midnight. Without this, cotton_candy_sunset runs indefinitely on its 500-iteration loop.\n- id: \u0026#39;1758620017271\u0026#39; alias: Midnight triggers: - trigger: time at: 00:00:00 actions: - action: script.turn_off target: entity_id: - script.cotton_candy_sunset - script.color_drift - script.party_pulse - action: light.turn_off target: entity_id: - light.all_lights - light.led_light0x07c2_varmblixt_table_wall_lamp mode: single The explicit Varmblixt entity in the light.turn_off call is there because the lamp wasn\u0026rsquo;t reliably included in light.all_lights before a full HA restart — turning off all_lights alone didn\u0026rsquo;t always catch it.\nNotes PirateWeather is the weather integration used here. It\u0026rsquo;s a free alternative to the deprecated Dark Sky API and returns the same condition strings. Any weather integration that exposes a weather.* entity with the standard condition states will work — just swap the entity ID. The count: 500 in each script is a practical cap. At the slowest pace (cotton candy sunset: ~100 seconds per cycle) that\u0026rsquo;s about 14 hours, more than enough to cover any night. The midnight automation cleans it up before the loop ends anyway. mode: restart on scripts means calling script.turn_on on one while another is running kills the previous immediately. No extra cleanup needed. ","permalink":"https://derek.engineer/posts/ikea-varmblixt-home-assistant-automations/","summary":"\u003cp\u003eThe IKEA Varmblixt is a $30 RGB+white Zigbee pendant. It pairs directly to ZHA in Home Assistant, has smooth color transitions, and a wide enough color gamut to be useful as a status indicator. I turned it into an ambient display for my apartment — weather during the day, a sunset script at night, a cycling green light at 6:30am, and a solid red door alarm when I forget to close the front door.\u003c/p\u003e","title":"IKEA Varmblixt as a Home Assistant Status Light"},{"content":"This is a working log. The TRMNL X is ordered but hasn\u0026rsquo;t arrived yet. The server is running.\nWhat is TRMNL X TRMNL makes e-paper displays designed to sit on a desk and cycle through widgets — weather, calendar, news, whatever you build. The X model supports BYOS (Bring Your Own Server), which lets you point the device at your own backend instead of their cloud. That\u0026rsquo;s the only mode I\u0026rsquo;m interested in.\nThe Clarity Kit ($25 add-on) includes the Developer Edition firmware and a battery upgrade. The Developer Edition is what exposes the BYOS endpoint in the device settings.\nThe server I\u0026rsquo;m running this FastAPI BYOS server, a nearly-from-scratch rewrite of an earlier Flask implementation. It handles the firmware-facing API, plugin scheduling, image rendering, and a minimal web dashboard.\nArchitecture at a glance:\nTRMNL X firmware → GET /api/display → receives { image_url, filename, refresh_time } → fetches image_url → renders to e-ink panel The filename field changes on every refresh cycle so the ESP32 firmware knows the image is new and doesn\u0026rsquo;t skip the render. The server alternates between screen.bmp and screen1.bmp to force cache-busting without any coordination with the device.\nPlugin system Plugins live in trmnl_server/plugins/. Any class that inherits PluginBase with AUTO_REGISTER = True gets picked up automatically by the scheduler. Each plugin outputs two files — a 1-bit monochrome BMP (for legacy firmware) and a grayscale PNG (for newer firmware that supports it). The server handles dithering and grading; the plugin just generates an image.\nCurrent plugins in the repo:\nPlugin What it renders WeatherPlugin Braun-inspired minimalist weather card HNPlugin Top Hacker News headlines XKCDPlugin Latest xkcd strip ChartsPlugin Configurable chart rendering RandomImagePlugin Image from a local pool CalibrationPlugin Grayscale calibration target The scheduler runs each plugin on its own interval and writes assets to var/generated/. The firmware just polls /api/display and gets back whatever is current in the rotation.\nState persistence SQLite at var/db/trmnl.db tracks:\nDevice registration and per-device playlist state Plugin rotation position Battery voltage samples Config entries (overrides env vars, survives restarts) Request logs Config precedence: environment variables win over anything written via the /settings/* API, so SERVER_PORT in your shell always wins, but API-driven tweaks like display brightness or refresh intervals persist across restarts.\nRunning it cd ~/Code/trmnl source .venv/bin/activate python3 -m trmnl_server # or: make serve Default port is 4567. List active plugins:\npython -m trmnl_server --list-plugins Debug a single plugin without starting the full server:\npython -m trmnl_server --run-plugin WeatherPlugin --plugin-output /tmp Network setup The device will be on the local network and needs to reach the server by hostname. I\u0026rsquo;m not exposing this to the internet — it\u0026rsquo;s LAN-only.\nDNS via AdGuard Home AdGuard Home is already running as the local DNS server (replaces Pi-hole). Adding a custom rewrite took 30 seconds:\nSettings → DNS rewrites → Add rewrite\nDomain: trmnl.home Answer: 192.168.4.47 (the Mac mini\u0026rsquo;s static LAN IP) Now any device on the network that uses AdGuard as its DNS resolver can reach trmnl.home. The TRMNL X will use the router\u0026rsquo;s DNS, which points at AdGuard, so it\u0026rsquo;ll resolve correctly.\nCaddy reverse proxy The firmware expects HTTP (it avoids SSL by default to save battery). Caddy handles the reverse proxy from port 80 to the FastAPI server on 4567.\nCaddyfile:\nhttp://trmnl.home { reverse_proxy localhost:4567 } Start Caddy:\ncaddy start --config ~/Code/trmnl/Caddyfile That\u0026rsquo;s it. http://trmnl.home → localhost:4567. No TLS, no auth — this is a private LAN server.\nWhat\u0026rsquo;s next Device arrives → BYOS setup:\nPut TRMNL X in BYOS mode via device settings (Clarity Kit / Developer Edition firmware enables this) Point it at http://trmnl.home Verify /api/display is getting hit in the server logs Build a morning briefing plugin: calendar events + weather + any flagged emails The morning briefing plugin is the whole point. The e-ink panel will show today\u0026rsquo;s schedule and conditions at a glance without touching a phone.\n","permalink":"https://derek.engineer/posts/trmnl-x-byos-server-setup/","summary":"\u003cp\u003eThis is a working log. The TRMNL X is ordered but hasn\u0026rsquo;t arrived yet. The server is running.\u003c/p\u003e\n\u003ch2 id=\"what-is-trmnl-x\"\u003eWhat is TRMNL X\u003c/h2\u003e\n\u003cp\u003eTRMNL makes e-paper displays designed to sit on a desk and cycle through widgets — weather, calendar, news, whatever you build. The X model supports BYOS (Bring Your Own Server), which lets you point the device at your own backend instead of their cloud. That\u0026rsquo;s the only mode I\u0026rsquo;m interested in.\u003c/p\u003e","title":"TRMNL X BYOS: Self-Hosting the Server Before the Device Arrives"},{"content":"Electrical engineer. Intermediate Python developer. Building things.\nResume / personal site · derek@derekwelty.com\n","permalink":"https://derek.engineer/about/","summary":"\u003cp\u003eElectrical engineer. Intermediate Python developer. Building things.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://derekwelty.com\"\u003eResume / personal site\u003c/a\u003e · \u003ca href=\"mailto:derek@derekwelty.com\"\u003ederek@derekwelty.com\u003c/a\u003e\u003c/p\u003e","title":"About"}]