设置一个公共 URL 来闪烁我的办公室灯光
Source: Dev.to
问题
我的树莓派位于家庭路由器后面。我不想进行端口转发或直接暴露 Home Assistant,但我想从互联网触发它。Droplet 解决了这个问题——它是面向公众的。问题是:如何安全地从 Droplet 连接到我的树莓派?
解决方案:Tailscale
Tailscale 创建设备之间的网状 VPN。在 Droplet 和 Pi 上都安装它,它们就可以使用私有 IP(如 100.x.x.x)相互通信——无需端口转发。
Internet → Droplet (public) → Tailscale → Pi (private) → Home Assistant
Claude Code 构建的内容
我使用了 Claude Code 来完成这项工作。我的关键洞察是,我可以直接给 Claude Code SSH 访问我的树莓派和 Droplet 的权限,让它处理其余的大部分工作。
它:
- SSH 登录我的树莓派并查询 Home Assistant,以获取我的灯光实体 ID
- 编写了一个 Bash 脚本,使灯光先闪红色,然后恢复之前的颜色
- 在树莓派和 Droplet 上都安装了 Tailscale
- 生成 SSH 密钥,以便 Droplet 能在树莓派上运行命令
- 创建了一个带令牌认证的 Flask Webhook
- 配置 nginx 来路由请求
- 创建 systemd 服务,使所有内容在重启后仍然有效
整个过程大约用了 20 分钟。大部分时间都在等待 apt 安装软件包。
架构
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
Source: …
闪光脚本
关键在于将灯光恢复到之前的状态。Home Assistant 的灯光可能处于不同的颜色模式,所以脚本在闪烁前会先保存当前状态:
# 保存当前状态
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')
# 闪红灯
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
# 恢复
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]}"
最初的版本只保存了亮度。当我告诉 Claude Code “灯光没有恢复到原来的状态” 时,它加入了对 xy_color 的处理。
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 peter@100.x.x.x "/home/peter/flash_lights.sh"'
subprocess.run(cmd, shell=True, timeout=15)
return jsonify({"status": "flashed", "user": tokens[token]["name"]})
令牌存储在一个 JSON 文件中:
{
"alice-token-123": {"name": "Alice", "created": "2026-01-05"},
"bob-token-456": {"name": "Bob", "created": "2026-01-05"}
}
每个人都有自己的令牌。通过删除对应条目来撤销访问权限。
接下来
既然管道已经搭建好,我可以:
- 不同来源使用不同颜色 – Slack 用蓝色,家庭短信用绿色,紧急情况用红色
- Slack 斜杠命令 – 为同事提供
/flash-peter - iOS 快捷指令 – 为我妻子提供一键按钮
- 速率限制 – 防止滥用
- 日志记录 – 谁在何时触发
如果你想构建类似的东西,需要的组件有:一台 Raspberry Pi(或运行 Home Assistant 的其他设备),两端都装有 Tailscale 的廉价 VPS,以及一些基础的 Python/Bash(在我的案例中,由 Claude 编写)。