← Back Home

Setting up a public URL that flashes my office lights

I've been running Home Assistant on a Raspberry Pi for over two years. It controls my Hue lights, Zigbee devices, the usual stuff. It's always been a local network thing.

As every vibecoder knows, you've got it working on https://localhost:3000 but that doesn't mean you can show your friends type of thing.

I had this little home network but I never wanted to expose it to the broader internet.

This past weekend, I spun up a $4/month DigitalOcean droplet to run ntfy, a self-hosted push notification service. The idea was to give various automations a way to send push notifications to my phone.

Then I realized: now I've got a droplet which can serve as my internet-facing endpoint. That means I can probably gateway to trigger things on my home network via HomeAssistant.

So, I now have a URL that anyone with a token can hit to flash my office lights red - pretty much an IRL ping that something needs my attention.

The Problem

My Pi is behind my home router. I don't want to port-forward or expose Home Assistant directly. But I want to trigger it from the internet.

The droplet solves this. It's public-facing. The question is: how do I get from the droplet to my Pi securely?

The Solution: Tailscale

Tailscale creates a mesh VPN between devices. Install it on both the droplet and the Pi, and they can talk to each other using private IPs (like 100.x.x.x) - no port forwarding needed.

Internet → Droplet (public) → Tailscale → Pi (private) → Home Assistant

What Claude Code Built

I used Claude Code to wire this up. My key insight was that I could simply give Claude Code SSH access to both my Pi and Droplet and let it handle a lot of the rest.

I described what I wanted, and it:

  1. SSHed into my Pi and queried Home Assistant to find my light entity IDs
  2. Wrote a bash script that flashes the lights red, then restores the previous color
  3. Installed Tailscale on both the Pi and droplet
  4. Generated SSH keys so the droplet can run commands on the Pi
  5. Created a Flask webhook with token-based auth
  6. Set up nginx to route requests
  7. Created systemd services so everything survives reboots

The whole thing took maybe 20 minutes. Most of that was waiting for apt to install packages.

The Architecture

Request: GET /flash-peter-office-lights?auth_token=xxx
                    ↓
            Cloudflare (HTTPS)
                    ↓
            DigitalOcean Droplet
            nginx → Flask (port 5000)
                    ↓
            Tailscale (100.x.x.x)
                    ↓
            Raspberry Pi
            SSH → flash_lights.sh
                    ↓
            Home Assistant API
                    ↓
            Lights flash red → restore

The Flash Script

The tricky part is restoring the lights to their previous state. Home Assistant lights can be in different color modes, so the script saves the current state before flashing:

# Save current state
STATE=$(curl -s -H "Authorization: Bearer $HA_TOKEN" \
  "http://localhost:8123/api/states/light.office")
WAS_ON=$(echo $STATE | jq -r '.state')
BRIGHTNESS=$(echo $STATE | jq -r '.attributes.brightness // 255')
XY_X=$(echo $STATE | jq -r '.attributes.xy_color[0] // empty')
XY_Y=$(echo $STATE | jq -r '.attributes.xy_color[1] // empty')

# Flash red
curl -s -X POST "http://localhost:8123/api/services/light/turn_on" \
  -H "Authorization: Bearer $HA_TOKEN" \
  -d '{"entity_id": "light.office", "rgb_color": [255, 0, 0], "brightness": 255}'

sleep 1

# Restore
curl -s -X POST "http://localhost:8123/api/services/light/turn_on" \
  -H "Authorization: Bearer $HA_TOKEN" \
  -d "{\"entity_id\": \"light.office\", \"brightness\": $BRIGHTNESS, \"xy_color\": [$XY_X, $XY_Y]}"

First version only saved brightness. When I told Claude Code "the lights aren't going back to where they were," it figured out the issue and added the xy_color handling.

The Webhook (Flask)

from flask import Flask, request, jsonify
import subprocess
import json

app = Flask(__name__)

def load_tokens():
    with open('/root/webhooks/tokens.json') as f:
        return json.load(f)

@app.route('/flash-peter-office-lights')
def flash():
    token = request.args.get('auth_token')
    if not token:
        return jsonify({"error": "Missing auth_token"}), 401

    tokens = load_tokens()
    if token not in tokens:
        return jsonify({"error": "Invalid token"}), 403

    # SSH to Pi via Tailscale and run the flash script
    cmd = 'ssh -i /root/.ssh/pi_key [email protected] "/home/peter/flash_lights.sh"'
    subprocess.run(cmd, shell=True, timeout=15)

    return jsonify({"status": "flashed", "user": tokens[token]["name"]})

Tokens live in a JSON file:

{
  "alice-token-123": {"name": "Alice", "created": "2026-01-05"},
  "bob-token-456": {"name": "Bob", "created": "2026-01-05"}
}

Each person gets their own token. Revoke access by deleting their entry.

What's Next

Now that the plumbing exists, I can:

If you want to build something similar, the pieces are: Raspberry Pi (or another device to run HomeAssistant), a cheap VPS with Tailscale on both ends, and some basic Python/bash (in my case, written by Claude).

Originally published on dev.to