设置一个公共 URL 来闪烁我的办公室灯光

发布: (2026年1月6日 GMT+8 02:38)
5 min read
原文: Dev.to

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 编写)。

Back to Blog

相关文章

阅读更多 »

Rapg:基于 TUI 的密钥管理器

我们都有这种经历。你加入一个新项目,首先听到的就是:“在 Slack 的置顶消息里查找 .env 文件”。或者你有多个 .env …

技术是赋能者,而非救世主

为什么思考的清晰度比你使用的工具更重要。Technology 常被视为一种魔法开关——只要打开,它就能让一切改善。新的 software,...

踏入 agentic coding

使用 Copilot Agent 的经验 我主要使用 GitHub Copilot 进行 inline edits 和 PR reviews,让我的大脑完成大部分思考。最近我决定 t...