hughobrien/breezyd

GitHub: hughobrien/breezyd

一个用 Go 编写的 Vents Twinfresh 系列通风机局域网控制工具,提供 CLI、守护进程、Web 仪表盘、HomeKit 网桥和 Prometheus 指标,无需云账号即可完整管理设备。

Stars: 0 | Forks: 0

# breezyd [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Go Reference](https://pkg.go.dev/badge/github.com/hughobrien/breezyd.svg)](https://pkg.go.dev/github.com/hughobrien/breezyd) [![Release](https://img.shields.io/github/v/release/hughobrien/breezyd)](https://github.com/hughobrien/breezyd/releases) 一个用于在局域网内控制 [Vents Twinfresh](https://ventilation-system.com/) Breezy 无管道热回收 通风机的 Go 库、守护进程和 CLI。它直接使用设备原生的 UDP/4000 协议进行通信 —— 无需云账号、MQTT broker、厂商应用或 Home Assistant 集成。仅限 LAN 使用。 CLI 可以独立工作 —— `breezy ` 会向 配置的设备打开 UDP 连接,完成操作后退出 —— 这也是全新安装后的默认行为。 当您需要轮询、缓存、JSON HTTP API、Prometheus `/metrics`、内嵌的 Web 仪表盘、通往 Apple Home 的 HomeKit 网桥,或者跨多个进程对同一设备的写入操作进行序列化时,可以添加可选的守护进程 (`breezyd`)。 ![breezy 仪表盘 — LAN 上的三个 Breezy 单元](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/d71ccd6b4f160026.png)

HomeKit bridge details in iOS Home — "breezyd" bridge with 3 accessories    Each Breezy as a separate AirPurifier accessory in the Apple Home rooms list

内嵌的 Web 仪表盘由守护进程在 `GET /` 路由提供;它使用 `templ` + datastar 进行服务端渲染,并通过 SSE 推送更新 —— 每个连接的浏览器都会在一个轮询周期内看到新状态,无需客户端轮询。它涵盖了电源/模式/速度/加热器/定时器,以及每个设备的 24 小时调度,并支持暗黑模式(通过 `prefers-color-scheme` 自动切换,或通过主题选择器手动切换)。详情请参见 [Web UI](#web-ui)。上面的截图是由 `just screenshot` 自动渲染的,并在设计更改时重新提交 —— README 始终展示当前状态。这两张 iPhone 截图展示了可选的 [HomeKit](#homekit) 网桥:每个配置的 Breezy 都会作为独立的 AirPurifier 配件出现在自动生成的 `breezyd` 网桥下。 ## 概览 ``` $ breezy ls NAME IP POWER MODE LAST POLL bedroom 192.168.1.152:4000 on supply 29s ago office 192.168.1.160:4000 on regeneration 29s ago playroom 192.168.1.148:4000 off extract 29s ago $ breezy playroom status # sensors + fans + service info $ breezy bedroom speed manual:30 # set bedroom fan to 30 % manual $ breezy office mode regeneration # heat-recovery mode $ breezy playroom faults # active fault codes (if any) ``` v1.0 发布了库、守护进程和 CLI;v1.1 增加了内嵌的 Web 仪表盘和可选的 NixOS-nginx 集成;v1.2 使 CLI 默认为独立运行(守护进程为按需启用);v1.3 增加了 HomeKit 网桥;v1.6 增加了全局设备密码继承以及一个 NixOS 模块,该模块能为主机上的每个用户自动检测守护进程。查看 `CHANGELOG.md` 获取各版本的详细信息。 包含的功能: - 传感器指标:湿度、eCO2、VOC、送风/排风/废气温度、 风扇 RPM、回收效率、滤网剩余时间、电机寿命、RTC 电池、故障代码。 - 控制:电源、气流模式(通风 / 再生 / 送风 / 排风)、 速度(预设 1-3 或手动 10-100 %)、加热器、夜间/涡轮增压 特殊模式定时器、滤网定时器重置、故障 重置、RTC 设置。 - 每个设备的快照和 Prometheus 指标。 - `breezy discover` 用于首次引导。 - 守护进程在 `GET /` 上提供的服务端渲染 Web 仪表盘(通过 SSE 使用 templ + datastar), 由同一个二进制文件提供服务;每 5 秒自动刷新;涵盖传感器、 风扇、服务信息以及四个高级控制项(电源 / 模式 / 速度 / 加热器)。支持暗黑模式(自动 + 手动覆盖)。 - 守护进程驱动的单设备调度:从仪表盘可折叠的 SCHEDULE 块中编辑一个简单的 `At | Action | Pct` 表格;守护进程会按计划触发写入操作,失败时有界限重试,并在持续失败时显示警告横幅。 - 可选的 HomeKit 网桥:每个 Breezy 会出现在 Apple Home 应用中, 包含电源、风扇速度、送风/排风开关以及完整的传感器 面板(RH、eCO2、VOC、四个温度)。 刻意未包含的功能:WiFi 重新配置、MQTT 网桥、Home Assistant 组件。参见 [已知局限](#known-limitations)。 ### 支持的设备 相同的硬件根据地区以不同的型号名称销售。所有 这些设备都报告单元类型 `0xB9 = 17` 并使用本项目 实现的协议: | 区域 | 产品名称 | |---------------|--------------------------------------------------------| | 欧洲 | Vents Twinfresh **Breezy 160**(亦称 Breezy Eco 160) | | 北美 | Vents Twinfresh **Elite 160 Pro**(无管道 HRV) | 厂商更小和更大的同系列产品(Breezy 200、Twinfresh Elite 200 Pro、 Breezy Eco 200)报告不同的单元类型字节(`20`、`22`、`24`),但使用 相同的线路协议;本项目应该能与它们兼容,尽管目前 仅在 160 型号上进行了测试。 ## 安装 选择与您的环境相匹配的路径。每种路径最终都能使用可工作的 `breezy ls`: - **NixOS 主机** → [NixOS](#nixos) — 4 步;守护进程 + CLI + 仪表盘,均由模块管理。 - **安装了 Nix 的 macOS 或非 NixOS Linux** → [随处运行的 Nix](#nix-anywhere) — `nix profile install` 会将二进制文件安装到 `$PATH`。 - **没有 Nix 的 Linux** → [Linux + systemd](#linux--systemd) — 下载预构建的二进制文件 + 可选的强化 systemd 单元。 ## NixOS 四个步骤:添加 flake 输入,发现设备,配置 模块,重新构建并使用它。 ### 1. 添加 flake 输入 + 模块导入 ``` { inputs.breezyd.url = "github:hughobrien/breezyd"; outputs = { self, nixpkgs, breezyd, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ breezyd.nixosModules.default ./breezyd.nix # the host-specific config from step 3 ]; }; }; } ``` ### 2. 发现您的设备 在配置之前,您需要每个单元的 16 字符设备 ID。 在模块配置好之前运行发现 —— `nix run` 不需要 预先安装任何东西: ``` nix run github:hughobrien/breezyd#breezy -- discover # 192.168.1.148 id=BREEZY00000000A0 type=17 (Breezy 160) # 192.168.1.152 id=BREEZY00000000A1 type=17 (Breezy 160) # 192.168.1.160 id=BREEZY00000000A2 type=17 (Breezy 160) ``` 如果您的设备使用非默认密码,请添加 `-p PASSWORD`(某些 固件尽管在规范中没有规定,但仍会丢弃密码不匹配的通配符请求)。 如果发现返回为空,但设备是可达的(Wi-Fi AP 隔离、不同的 VLAN,或主机防火墙阻止了 UDP/4000 是 常见原因),请将每个 IP 作为位置参数传递,以发送单播 通配符请求: ``` nix run github:hughobrien/breezyd#breezy -- discover -p huffpuff \ 192.168.1.148 192.168.1.152 192.168.1.160 ``` 记下 ID 和 IP —— 两者都将在下一步中使用。 ### 3. 配置模块 ``` # breezyd.nix { services.breezyd = { enable = true; settings = { # Fleet-wide protocol password. Used for the daemon's wildcard # discovery probes and inherited by any device that doesn't set # its own. daemon.password = "huffpuff"; # `ip` is optional — set it when broadcast is unreliable on your # LAN, and the daemon will skip discovery for that device. # Per-device `password` overrides `daemon.password`. devices.bedroom = { id = "BREEZY00000000A0"; ip = "192.168.1.148"; }; devices.office = { id = "BREEZY00000000A1"; ip = "192.168.1.152"; }; devices.playroom = { id = "BREEZY00000000A2"; ip = "192.168.1.160"; }; }; }; } ``` 内联的 `settings` 会渲染成位于 `/run/breezyd/breezyd.toml` 的 0600 权限 TOML 文件, 但您放在那里的任何内容最终都会在全局可读的 Nix 存储中暴露。对于真实的设备密码,请使用带有 sops-nix 或 agenix 的 `services.breezyd.configFile` 来指向一个由密码管理的文件。 ### 4. 重新构建并使用它 在 `nixos-rebuild switch` 之后,守护进程启动,`breezy` CLI 出现在每个用户的 PATH 中,模块还会写入一个小巧的 `/etc/breezy/config.toml`,其中仅包含 `[daemon].listen = "..."` —— 因此 CLI 会自动检测守护进程并与它通信,而无需任何人编写 `~/.config/breezy/config.toml`: ``` $ breezy ls NAME IP POWER MODE LAST POLL bedroom 192.168.1.152:4000 on supply 29s ago office 192.168.1.160:4000 on regeneration 29s ago playroom 192.168.1.148:4000 off extract 29s ago $ breezy playroom status # full snapshot $ breezy bedroom speed manual:30 $ breezy office mode regeneration ``` 如果某行在电源列显示 `?` / 在上次轮询列显示 `never`,则守护进程 尚未能连接到该设备。请检查日志: ``` journalctl -u breezyd -n 50 | grep -E 'discovery|no IP' ``` `discovery complete found=0` 意味着通配符探测没有得到任何 回复 —— 回到第 2 步并为每个设备添加 `ip = "..."`,这将 完全绕过发现阶段。 ### 模块的作用 创建一个 `breezyd` 系统用户,在 systemd 下通过强化配置运行守护进程( `NoNewPrivileges`、`ProtectSystem=strict`、`PrivateTmp`、 `MemoryDenyWriteExecute` 等),在 `network-online.target` 之后启动, 将 `breezy` CLI 添加到 `environment.systemPackages` 中,使其位于 每个用户的 PATH 中,并写入 `/etc/breezy/config.toml`(权限 0644, 仅包含 `[daemon].listen`)以便 CLI 自动检测守护进程。如果 您将监听器绑定到非回环地址,请设置 `services.breezyd.openFirewall = true`。 ### Prometheus(可选) ``` services.breezyd.prometheus.enable = true; # 可选调节项,显示默认值: # services.breezyd.prometheus.jobName = "breezyd"; # services.breezyd.prometheus.scrapeInterval = "30s"; ``` 仅当 `services.breezyd.enable` 和 `services.prometheus.enable` 都为 true 时,才向 `services.prometheus.scrapeConfigs` 中注入配置。 ### HomeKit(可选) ``` services.breezyd.homekit = { enable = true; port = 51827; # pin a fixed TCP port so the firewall hole is reachable }; services.breezyd.openFirewall = true; # opens HAP TCP + UDP/5353 for mDNS # 其他调节项,显示默认值: # services.breezyd.homekit.bridgeName = "breezyd"; # name shown during pairing # services.breezyd.homekit.stateDir = "/var/lib/breezyd/homekit"; ``` 每个配置的 Breezy 都会在 Apple Home 中显示为一个 HomeKit 配件。 该模块将 `[homekit]` 块附加到生成的配置中,并 管理位于 `/var/lib/breezyd` 下的状态目录。配对 PIN 会在首次启动时自动生成并打印在日志中;删除 状态目录即可重置。 `openFirewall` 默认为 `false`。当您将其与 `homekit.enable` 一起开启时,模块会打开 HAP TCP 端口(仅在 `homekit.port != 0` 时)以及 **用于 mDNS 的 UDP/5353** —— 后者 是让 iPhone 在 LAN 上发现网桥的关键。当 `port = 0`(默认值)时, 操作系统在启动时选择一个临时端口,防火墙无法 预先打开它,因此只要启用了主机防火墙,就应固定一个具体的 `port`。 如果您使用 `services.breezyd.configFile`(即您自己使用 sops-nix / agenix 管理 TOML), 启用 `homekit` 仍会调整 systemd 单元(状态目录、防火墙),但**不会**向您的文件中注入 `[homekit]` 块 —— 您需要自行添加。 ## 随处运行的 Nix 如果您安装了 Nix(NixOS、nix-darwin,或任何装有 Nix 包管理器的 Linux/macOS 主机),最快的安装方式是 `nix profile install`。CLI 和守护进程来自同一个 derivation,因此 两个二进制文件都会安装到 `$PATH`: ``` nix profile install github:hughobrien/breezyd breezy --version ``` flake 暴露的其他入口点: ``` # 无需安装直接运行二进制文件 — 单次调用速度较慢,因为 # `nix run` 每次都会重新检查 flake,但适用于一次性运行。 nix run github:hughobrien/breezyd # daemon nix run github:hughobrien/breezyd#breezy -- ls # CLI # 将独立二进制文件构建到 ./result/bin/ 中 nix build github:hughobrien/breezyd ./result/bin/breezyd --version # 进入包含 Go、gopls、goreleaser 等的 dev shell nix develop github:hughobrien/breezyd ``` 该 flake 暴露了三个包(`breezyd`、`breezy`、`default = breezyd`), 三个应用(`default`、`breezyd`、`breezy`),一个 `devShells.default`,以及一个 用于将守护进程作为 NixOS 服务运行的 `nixosModules.default`。 ### 1. 查找您的设备 ID `breezy discover` 在 UDP/4000 上广播通配符请求。每个接听到的 Breezy 都会以其 16 字符的设备 ID 和单元类型作为回应: ``` breezy discover # 192.168.1.148 id=BREEZY00000000A0 type=17 (Breezy 160) # 192.168.1.152 id=BREEZY00000000A1 type=17 (Breezy 160) # 192.168.1.160 id=BREEZY00000000A2 type=17 (Breezy 160) ``` 如果广播返回为空,但您可以 `ping` 通这些单元(Wi-Fi AP 隔离、mesh 跳数或不同的 VLAN 通常会丢弃广播,而 单播仍然有效),请将 IP 作为位置参数传递 —— CLI 将 向每个设备发送单播通配符请求: ``` breezy discover 192.168.1.148 192.168.1.152 192.168.1.160 ``` 如果结果*依然*为空,并且您已经将单元从出厂 默认密码修改过,请使用 `-p PASSWORD` 重试。厂商的规范指出通配符 发现是不需要认证的,但某些固件版本会静默丢弃 密码不匹配的请求: ``` breezy discover -p testpwd 192.168.1.148 192.168.1.152 192.168.1.160 ``` `-p` 也可以与广播一起使用(`breezy discover -p testpwd`),以防 您的网络正常,仅仅是密码的问题。 ### 2. 编写配置文件 以 `0600` 模式创建 `~/.config/breezy/config.toml`,为每个单元设置一个 `[devices.]` 块: ``` [devices.playroom] id = "BREEZY00000000A0" password = "testpwd" ip = "192.168.1.148" [devices.bedroom] id = "BREEZY00000000A1" password = "testpwd" ip = "192.168.1.152" [devices.office] id = "BREEZY00000000A2" password = "testpwd" ip = "192.168.1.160" ``` ``` mkdir -p ~/.config/breezy $EDITOR ~/.config/breezy/config.toml chmod 0600 ~/.config/breezy/config.toml ``` 0600 模式检查是强制执行的 —— 加载器否则将拒绝启动。 ### 3. 验证 ``` breezy ls # all configured devices, one line each breezy playroom status # full snapshot — sensors, fans, service info breezy bedroom speed manual:30 # set bedroom fan to 30 % manual breezy office mode regeneration # switch office to heat-recovery mode ``` `breezy --help` 是最权威的参考;请参阅 [CLI 概览](#cli-overview) 获取完整的动词列表。 ### 4. (可选)运行守护进程 如果您想要以下任何功能,请运行 `breezyd`: - **轮询 + 缓存** —— 每个设备的状态按可配置的 节拍刷新,从内存中提供数据。CLI 运行更快,且不需要读取时 设备都必须可达。 - **JSON HTTP API** 位于 `http://127.0.0.1:9876/v1/devices/...`。 - **Prometheus `/metrics`** 用于 Grafana 仪表盘 / 警报。 - **内嵌的 Web 仪表盘** —— 即本 README 顶部附近的截图。 - **HomeKit 网桥** —— 参见 [HomeKit](#homekit) 章节。 - **并发安全** 当多个进程针对同一设备编写 `breezy` 脚本时。独立运行的 CLI 之间互不协调;守护进程 通过互斥锁序列化每个设备的 UDP 通信。 在配置中添加 `[daemon]` 块并启动守护进程: ``` [daemon] listen = "127.0.0.1:9876" poll_interval = "30s" discovery = "on-start" # "on-start" | "off" | "periodic:" ``` ``` breezyd # logs to stderr; stop with SIGINT/SIGTERM ``` 当在配置中设置了 `[daemon].listen` 或 传递了 `--daemon URL` 时,CLI 会自动检测守护进程模式。使用 `--daemon http://...` 覆盖 可与远程守护进程通信,或者完全省略 `[daemon]` 以保持 独立运行模式。 如果 `breezyd` 启动时配置不存在,它会写入一个合理的 默认配置(其中 `[daemon]` 被注释掉,因此重新运行即可获得正常工作的 独立模式)并以“请编辑它”的消息退出。 如果您希望守护进程在 systemd 下开机自启,请参阅 [Linux + systemd](#linux--systemd) —— 该章节中的单元文件可以 原样工作,无论您是通过 `nix profile install` 还是从发布存档中获取二进制文件。 ## Linux + systemd 适用于没有 Nix 的 Ubuntu / Debian / Arch / Fedora 等系统。 ### 1. 下载二进制文件 为 Linux (amd64/arm64)、macOS (amd64/arm64) 和 Windows (amd64) 预构建的二进制文件发布在 [GitHub Releases 页面](https://github.com/hughobrien/breezyd/releases)。下载适合您平台的 压缩包,并将 `breezyd` 和 `breezy` 解压到 `$PATH` 上的某个位置: ``` # Linux amd64 示例 curl -sSL -o breezyd.tar.gz \ https://github.com/hughobrien/breezyd/releases/latest/download/breezyd_Linux_x86_64.tar.gz tar -xzf breezyd.tar.gz breezyd breezy sudo install -m 0755 breezyd breezy /usr/local/bin/ breezyd --version ``` ### 2. 查找您的设备 ID ``` breezy discover # 192.168.1.148 id=BREEZY00000000A0 type=17 (Breezy 160) # 192.168.1.152 id=BREEZY00000000A1 type=17 (Breezy 160) # 192.168.1.160 id=BREEZY00000000A2 type=17 (Breezy 160) ``` 如果广播结果为空,请将每个 IP 作为位置参数传递,并且如果您的设备使用非默认密码,请添加 `-p PASSWORD`: ``` breezy discover -p huffpuff 192.168.1.148 192.168.1.152 192.168.1.160 ``` ### 3. 独立使用 CLI 如果您只想要 CLI(不需要守护进程或服务),这就足够了: 以 0600 模式编写包含已发现 ID 的 `~/.config/breezy/config.toml`,然后运行 `breezy ls`。格式同 [随处运行的 Nix 独立配置](#2-write-the-config)。仅当您希望守护进程 在后台进行轮询时才跳到第 4 步。 ### 4. (可选)将 breezyd 作为系统服务运行 创建一个系统用户,将守护进程的配置放在 `/etc/breezyd/` 下, 并添加一个精简的 `/etc/breezy/config.toml` 以便用户获得自动检测功能: ``` # 创建守护进程的用户和配置目录。 sudo useradd --system --no-create-home --shell /usr/sbin/nologin breezyd sudo install -d -m 0750 -o breezyd -g breezyd /etc/breezyd # 写入守护进程的配置(包含密码)。权限模式为 0600。 sudo tee /etc/breezyd/breezyd.toml <<'EOF' >/dev/null [daemon] listen = "127.0.0.1:9876" poll_interval = "30s" discovery = "on-start" password = "huffpuff" [devices.bedroom] id = "BREEZY00000000A0" ip = "192.168.1.148" [devices.office] id = "BREEZY00000000A1" ip = "192.168.1.152" [devices.playroom] id = "BREEZY00000000A2" ip = "192.168.1.160" EOF sudo chown breezyd:breezyd /etc/breezyd/breezyd.toml sudo chmod 0600 /etc/breezyd/breezyd.toml # 写入 CLI 的系统回退配置(不含密码)。权限模式设为 0644,以便主机上的任何 # 用户都能读取;当不存在 # ~/.config/breezy/config.toml 时,CLI 会尝试读取此文件。 sudo install -d -m 0755 /etc/breezy sudo tee /etc/breezy/config.toml <<'EOF' >/dev/null [daemon] listen = "127.0.0.1:9876" EOF ``` 将以下内容保存为 `/etc/systemd/system/breezyd.service`(其 强化配置与 NixOS 模块的设置相一致): ``` [Unit] Description=Vents Twinfresh Breezy / Elite 160 Pro daemon Wants=network-online.target After=network-online.target [Service] ExecStart=/usr/local/bin/breezyd --config /etc/breezyd/breezyd.toml User=breezyd Group=breezyd Restart=on-failure RestartSec=5s # 安全加固 — breezyd 仅需要出站 UDP 和 HTTP 监听器。 NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true ProtectClock=true ProtectHostname=true ProtectProc=invisible # Go 的 net.Interfaces() 在 Linux 上需要 AF_NETLINK;若没有它, # HomeKit 网桥虽然运行,但不会在任何接口上进行广播(对 mDNS 无响应)。 RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true LockPersonality=true MemoryDenyWriteExecute=true SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged [Install] WantedBy=multi-user.target ``` 然后启用并启动: ``` sudo systemctl daemon-reload sudo systemctl enable --now breezyd journalctl -u breezyd -f # tail the log to confirm it's polling breezy ls # any user can talk to the daemon now ``` 如果您在配置中开启了 `[homekit]`,还需: - 将 `StateDirectory=breezyd` 添加到 `[Service]` 中,以便 systemd 为 HAP 服务器的配对状态创建 `/var/lib/breezyd/`。 - 在 `[homekit]` 中固定一个具体的 `port = N`(默认 `0` 是临时的,无法通过防火墙控制),然后打开主机防火墙: `ufw allow N/tcp`(或 `firewall-cmd --add-port=N/tcp`)。 - 打开 **用于 mDNS 的 UDP/5353** 以便 iPhone 能够发现网桥: `ufw allow 5353/udp`。如果不这样做,即使配对功能正常, 网桥也不会出现在添加配件列表中。 ## Web UI 守护进程在其 HTTP 监听器的根路径上提供服务端渲染的仪表盘(通过 SSE 使用 templ + datastar): ``` http://127.0.0.1:9876/ ``` 三列卡片(每个配置的设备一张)显示实时传感器 读数(湿度 / eCO₂ / VOC,每个都带有警报阈值;点击 数值会打开一个内联编辑器来调整阈值)、风扇 RPM 及 指令百分比、服务信息(滤网、电机寿命、RTC 电池、故障)、固件版本,以及电源、气流 模式、风扇速度(预设 1-3 或手动百分比滑块)、加热器和 夜间/涡轮增压 特殊模式定时器的控制项。当 固件设置超阈值标志时,传感器值会以红色显示。页面每 5 秒自动刷新;如果上次轮询已超过 90 秒,卡片会变为灰度显示。暗黑 模式自动跟随 `prefers-color-scheme`;点击标题旁边的主题图标可进行手动覆盖。 默认的 `[daemon].listen` 为 `127.0.0.1:9876`,这意味着 仪表盘只能从运行 `breezyd` 的主机访问。要从同一 LAN 上的手机或笔记本电脑使用它,请在 `~/.config/breezy/config.toml` 中更改监听器: ``` [daemon] listen = "0.0.0.0:9876" ``` (如果您不想绑定到所有接口,可以指定一个具体的 LAN IP。)更改配置后请重启 `breezyd`。 **安全隐患:** HTTP API 没有身份验证。绑定 到 LAN 可达的地址会将所有 `/v1/...` 端点暴露给同一网络中的任何人 —— 包括可以更改设备协议密码或 WiFi 凭据的原始 `POST /v1/devices//params/` 写入路径。缓解措施在于网络配置而非软件:将 设备(以及运行 `breezyd` 的主机)放在一个 IoT VLAN 中。请参阅 [安全](#security) 章节了解全貌。 ### 在 nginx 之后(NixOS) 如果您已经在运行 NixOS 和 `services.nginx`,在 LAN 上暴露 仪表盘的更简洁方式是使用模块的可选 nginx 集成:保持 `[daemon].listen = "127.0.0.1:9876"`(这样守护进程 本身保持仅绑定到回环地址,原始 API 从 LAN 不可达),并让 nginx 作为面向网络的服务: ``` services.nginx.enable = true; services.breezyd = { enable = true; nginx = { enable = true; virtualHost = "breezy.home.lan"; # basicAuthFile = "/run/secrets/breezy-htpasswd"; # sops-nix / agenix }; }; # 请自行定义 vhost — TLS、ACME、监听端口等。此模块 # 仅添加了带有 proxy_pass + basicAuthFile 的 location."/"。 services.nginx.virtualHosts."breezy.home.lan" = { forceSSL = true; enableACME = true; }; ``` 当仪表盘需要从运行 `breezyd` 的主机以外的 设备访问时,这是推荐的方案。结合 `basicAuthFile`,它为您提供了直接监听路径所缺乏的 传输层(如果您设置了 `forceSSL`,则为 TLS)和应用层(基本认证)的双重防护。守护进程的完整 `/v1/...` API 仍然位于 回环地址上,因此即使经过 nginx 认证后,受感染的 LAN 设备也无法访问原始的参数写入端点。 ## HomeKit 守护进程包含一个可选的 HomeKit 网桥。启用后,每个 配置的 Breezy 都会在 Apple Home 应用中作为一个配件出现, 包含电源、风扇速度、仅送风 / 仅排风 / 加热器 / 夜间 / 涡轮增压开关、完整的传感器面板(湿度、eCO2、VOC、四个 温度)、一个带有 iOS 原生“更换滤网”指示器的 滤网维护服务,以及一个用于 RTC 纽扣电池的电池服务(低电量警告约在 40% 时触发)。 通过添加以下内容到 `~/.config/breezy/config.toml` 启用它: ``` [homekit] enabled = true ``` 重启 `breezyd`。启动日志中将包含类似如下的行: ``` homekit: bridge ready name="breezyd" pin="123-45-678" state_dir="..." ``` 在 iPhone 上打开 Apple Home 应用 → 添加配件 → 手动 输入 PIN。所有配置的 Breezy 单元将一起出现;每个都有 其独立的磁贴。 **重置配对:** 删除状态目录(默认为 `~/.local/state/ breezyd/homekit`,在 NixOS 上为 `/var/lib/breezyd/homekit`)。 守护进程下次启动时会重新生成 PIN。 **可调参数**(均为可选): - `bridge_name`:配对期间显示的名称。默认为 `"breezyd"`。 - `port`:HAP 服务器的 TCP 端口。默认为 0(由操作系统分配)。 - `state_dir`:配对密钥和 PIN 存放的位置。 在 NixOS 上,网桥只需一个开关 —— 参见上方 NixOS 章节中的 [HomeKit(可选)](#homekit-optional)。 HomeKit 网桥始终使用守护进程路径 —— 写入操作通过 `pkg/breezy/ops` 进行,具有与 HTTP 处理程序相同的每个设备互斥锁序列化和风扇稳定窗口。独立运行 CLI 的并发注意事项 与此无关;HomeKit 网桥从不打开自己的 UDP socket。 ## Prometheus 守护进程以 Prometheus 展示格式暴露 `/metrics`。像抓取任何 其他目标一样抓取它: ``` # prometheus.yml scrape_configs: - job_name: breezy static_configs: - targets: ['localhost:9876'] ``` 每个指标都带有 `device=""` 和 `id="<16-char id>"` 标签。一些 有用的查询: ``` # 每个设备的室内温度 breezy_temperature_celsius{location="indoor"} # 任何超过其警报阈值的传感器(湿度 / co2 / voc) max by (device) (breezy_sensor_alert) > 0 # 逐间房的恢复效率 breezy_recovery_efficiency_pct # 滤网剩余时间(以天为单位) breezy_filter_remaining_seconds / 86400 # 过去 5 分钟内是否有设备变为不可达状态? time() - breezy_last_poll_timestamp > 300 ``` 当轮询器连接到设备时 `breezy_up{device="..."}` 为 `1`,否则为 `0`; 相应的 `breezy_last_poll_timestamp` 是上次成功读取的 Unix 时间戳。 在 NixOS 上,自动抓取集成只需一个开关 —— 参见 NixOS 章节中的 [Prometheus(可选)](#prometheus-optional)。 ## CLI 概览 `breezy --help` 是最权威的参考。其结构为“主语在谓语之前”, 因此按设备划分的命令读起来很自然: | 命令 | 功能 | | ------------------------------------ | -------------------------------------------- | | `breezy ls` | 每个已配置设备的单行表格摘要 | | `breezy discover [-p PWD] [ip...]` | LAN 广播(或向每个 IP 单播);`-p` 覆盖通配符发现密码 | | `breezy param` | 列出已知参数(id、类型、单位、容量;使用 `name` 搭配 `get`/`set`) | | `breezy playroom status` | 完整的结构化快照 | | `breezy bedroom on` / `off` | 电源 | | `breezy bedroom speed manual:30` | 将风扇设置为 30% 手动模式 | | `breezy bedroom speed 2` | 切换到预设 2 | | `breezy office mode regeneration` | 气流模式(ventilation / regeneration / supply / extract) | | `breezy office heater on` | 切换辅助加热器 | | `breezy bedroom timer night` | 启动夜间模式定时器(或 `turbo`/`off`) | | `breezy playroom faults` | 列出活动的故障代码 | | `breezy playroom firmware` | 固件版本 + 构建日期 | | `breezy playroom efficiency` | 回收效率百分比 % | | `breezy playroom rtc` | 显示设备时钟 | | `breezy playroom rtc set 2026-05-03T22:00:00-07:00` | 设置设备时钟 | | `breezy playroom reset-filter` | 清除滤网定时器 | | `breezy playroom reset-faults` | 清除活动的故障标志 | | `breezy playroom get humidity` | 按名称或十六进制读取原始参数 | | `breezy playroom set 0x25 1e` | 写入原始参数(十六进制) | CLI 的退出码为:`0` 成功,`1` 后端错误(守护进程模式下为 HTTP 封装消息,独立模式下为纯文本错误信息),`2` 本地用法错误。 ### 独立运行 vs 守护进程模式 CLI 默认为独立模式(直接通过 UDP 连接每个设备)。典型流程 在针对 Nix 用户的 [随处运行的 Nix](#nix-anywhere) 和针对其他用户的 [Linux + systemd](#linux--systemd) 中有详细说明;两者最终都以 在有无守护进程运行的情况下都能工作的 `breezy ls` 告终。 **并发注意事项:** 守护进程通过互斥锁序列化每个设备的 UDP 通信。独立运行的 CLI 进程之间互不协调 —— 两个在同一时刻针对同一设备调用的 `breezy` 可能会产生静默的校验和损坏。如果您针对同一设备并行编写脚本调用,请运行守护进程并在守护进程模式下使用 CLI。 ## 配置参考 `~/.config/breezy/config.toml` —— 模式 `0600`(当文件包含密码时由加载器强制要求)。守护进程和 CLI 都会读取此文件。CLI `[daemon].listen` 来决定是通过 HTTP 与守护进程通信还是直接通过 UDP 与每个设备通信,并在独立模式下读取每个 `[devices.]` 作为单播目标。 当主目录中不存在配置文件时,CLI 还会尝试将 `/etc/breezy/config.toml` 作为备选。 系统备选文件通常仅包含 `[daemon].listen = "..."`(没有密码),加载器允许 其权限为 0644,以便主机上的每个用户都可以读取。当 `services.breezyd.enable = true` 时,NixOS 模块会自动写入此文件。 带默认值的完整结构: ``` # 可选。如果没有此配置块,CLI 将以 standalone 模式运行(无 HTTP)。 [daemon] listen = "127.0.0.1:9876" # http listener; required when [daemon] present poll_interval = "30s" # default 30s discovery = "on-start" # "on-start" | "off" | "periodic:" password = "" # optional fleet-wide protocol password; used for # the daemon's discovery probes and inherited by # any [devices.] block that omits its own # 可选。默认关闭。参见 HomeKit 部分。 [homekit] enabled = false # bridge_name = "breezyd" # port = 0 # 0 = ephemeral # state_dir = "~/.local/state/breezyd/homekit" # 每个 Breezy 单元对应一个 [devices.] 配置块。Name = 用作 # CLI 的 的标签:"breezy playroom status"。 [devices.playroom] id = "BREEZY00000000A0" # 16-char device ID; from `breezy discover` password = "testpwd" # protocol password; falls back to [daemon].password if absent ip = "192.168.1.148" # required in standalone; optional in daemon mode ``` 如果您希望将配置保存在其他地方(例如 sops-nix / agenix),请使用 `--config /path/to/file` 为守护进程指定路径。只要文件包含密码,0600 模式检查仍然适用。CLI 的配置路径顺序固定为先 `~/.config/breezy/config.toml` 然后是 `/etc/breezy/config.toml`。 ## 安全 Breezy 固件会以明文形式通过 UDP/4000 向同一广播域中 知道 16 字符设备 ID 的任何客户端分发其自身的协议密码(参数 `0x7D`)、WiFi SSID (`0x95`) 和 WiFi 密码 (`0x96`)。发现过程本身是未经身份验证的 —— LAN 上的任何人都可以枚举每个 Breezy 单元并读取这些参数。 缓解措施在于网络配置而非软件:将这些单元放在无法访问家庭 LAN 其余部分的 IoT VLAN 中,并且只允许运行 `breezyd` 的主机访问该 VLAN。本项目没有在线路协议之上增加加密 —— 这不会改变威胁模型,因为设备固件本身就是以明文响应的。 位于 `GET /` 的 Web 仪表盘与 JSON API 使用相同的监听器。 如果您将 `[daemon].listen` 从默认的回环地址更改为 LAN 地址以便可以从手机访问仪表盘,您也将 把其余的 API —— 包括原始参数写入 —— 暴露给该网络上的任何人。同样的 VLAN 分段建议同样适用:将 运行 `breezyd` 的主机与设备一起放在 IoT VLAN 中,并从一台被临时授予该 VLAN 访问权限的工作站访问仪表盘,而不是将 `breezyd` 绑定到您受信任的 LAN。 ## 已知局限 这些是刻意的省略,而不是 bug。每一项都是设计选择;详见规范了解完整的理由。 - **无 WiFi 重新配置。** 从 CLI 更改 WiFi SSID/密码在技术上可行,但在操作上存在风险(一次错误的写入就会导致设备失联)。请使用厂商应用进行此操作。 - **无 MQTT 网桥。** HTTP API 和 Prometheus 指标面板已经覆盖了操作人员迄今为止的所有用例。状态缓存的架构设计使得未来可以在不重写核心的情况下添加网桥。 - **无 Home Assistant 集成。** 理由同上。任何想要 HA 集成的人都可以基于 `/v1/devices/` 构建 REST 传感器或抓取 `/metrics`。 ## 开发 想要参与开发 breezyd 本身?请从这里开始。 ### 从源代码构建 需要 Go 1.22+(在 1.26 上开发)以及 `templ` CLI。二进制文件本身没有其他系统依赖;竞争检测器配方(`just test-race`)需要一个可用的 C 工具链。 `nix develop` 提供了包括 `templ` 在内的所有先决条件。在 Nix 之外的环境: ``` go install github.com/a-h/templ/cmd/templ@v0.3.x ``` ``` just generate # run templ codegen (needed once after checkout or .templ edits) just build # generate + produces ./breezyd and ./breezy just check # vet + fast tests + templ-drift (pre-commit gate) just test-race # full race-detector run (the CI command) ``` `just test-race` 已经设置了 `CGO_ENABLED=1 CC=clang`,因此该配方在默认 `gcc` 缺少 TSan 运行时的开发主机上也能开箱即用。 ### 项目结构 ``` breezyd/ ├── pkg/breezy/ # protocol library (importable) │ ├── frame.go # FDFD/02 packet codec │ ├── client.go # UDP transport, retries, timeouts │ ├── params.go # parameter registry (id, type, R/W, units) │ ├── values.go # typed value codecs │ ├── discover.go # LAN broadcast │ └── fakedevice/ # in-process protocol-speaking fake for tests ├── cmd/breezyd/ # the daemon (HTTP + Prometheus + poller) │ └── ui/ # templ templates, datastar+dashboard vendor JS, style.css, view.go ├── cmd/breezy/ # the CLI (standalone UDP by default; daemon mode opt-in) ├── cmd/fakedevice/ # build-tagged fakedevice binary with admin HTTP (for Playwright) ├── internal/config/ # TOML config loader, shared by both ├── tools/ # Phase 0 Python probes (one-off, kept for reference) └── docs/superpowers/specs/ # design doc, parameter map, vendor PDF manual ``` ### 测试 ``` just test # unit tests (uses fakedevice) just test-race # same, with -race (the CI command) just lint # go vet + gofmt-drift check just check # lint + fast tests (pre-commit gate) just check-all # check + test-race + Playwright UI suite ``` UI 测试是位于 `tests/ui/` 下的端到端 Playwright 规范,它们会启动一个真实的 `breezyd` 进程,并将其指向 `cmd/fakedevice`(一个带有构建标签的 UDP 模拟设备,具有 HTTP 管理控制平面): ``` just test-ui-install # one-time: pnpm install + chromium download just test-ui # 82 tests (66 active + 16 fixme), ~20 s just screenshot # re-render tests/ui/screenshots/*.png ``` 使用原始的 `go` 命令运行单个 Go 包或测试: ``` go test ./pkg/breezy/... go test ./cmd/breezyd -run TestPoller_FanSettle ``` 针对真实硬件的实时集成测试同时受 `integration` 构建标签和 `BREEZY_INTEGRATION=1` 环境变量的门控,加上用于标识目标设备的三个环境变量。`just test-integration` 配方封装了所有这些内容: ``` just test-integration 192.168.1.148 BREEZY00000000A0 ``` 这些测试会对设备进行写入操作 —— 每个测试都会注册一个 `t.Cleanup` 以 恢复先前的值,因此重新运行后设备仍会保持其原始状态。 ### 深入文档的指南 - `docs/superpowers/specs/2026-05-03-twinfresh-cli-design.md` —— 完整的 v1 设计文档:协议决策、守护进程架构、错误语义、状态行格式等。 - `docs/superpowers/specs/2026-05-04-basic-ui-design.md` —— Web 仪表盘的设计文档,绑定地址的权衡,以及可选的 NixOS-nginx 反向代理集成。 - `docs/superpowers/specs/2026-05-04-discover-investigation.md` —— 导致 `breezy discover` 失败的两个原因(一个代码缺陷,已修复;以及 QEMU-NAT 环境限制,已记录),并提供了具体的下一步操作。 - `docs/superpowers/specs/2026-05-03-param-map.md` —— 设备暴露的每个参数 ID,包含类型、单位、观察到的值以及阶段 0 特性描述中的注释。 - `docs/superpowers/specs/breezy-manual-vendor.pdf` —— 厂商协议手册,是线路协议的权威参考。已在本地缓存以供离线阅读;官方副本由 Vents 发布在 。 - `docs/superpowers/specs/breezy-datasheet-vendor.pdf` —— 硬件数据手册。官方副本位于 。 ## 致谢 如果没有 **Ventilation Systems Ltd. (Vents)** 发布的协议文档,这个项目是不可能实现的。位于 的 Breezy / Breezy Eco 连接指导手册记录了本库实现的完整线路协议、数据包结构、功能码和参数表。阅读该手册确认了(并在某些地方纠正了)在此项目阶段 0 捕获的经验性逆向工程结果。感谢 Vents 公开发布这些文档。 在 `docs/superpowers/specs/` 下提供的手册和数据手册副本仅供方便使用,其版权归 © Vents 所有。请参阅上面的官方 URL 以获取最新版本。 ## 许可证 Copyright (C) 2026 Hugh O'Brien 本程序是自由软件:您可以根据自由软件基金会发布的 GNU 通用公共许可证的条款进行重新分发和/或修改,许可证版本为第 3 版或(由您选择的)任何更新版本(`SPDX-License-Identifier: GPL-3.0-or-later`)。 本程序的发布是希望它能有用,但不提供任何保证;甚至没有对适销性或特定用途适用性的默示保证。有关 GNU 通用公共许可证 v3 的全文,请参见 [LICENSE](LICENSE) 文件。 本项目未受 Ventilation Systems Ltd. 赞助或认可。 "Vents" 和 "Twinfresh" 是其各自所有者的商标。
标签:Apple HomeKit, CLI, EVTX分析, Go语言, HRV, IoT, Python安全, SSE, UDP通信, Web仪表盘, WiFi技术, 守护进程, 实时推送, 局域网协议, 局域网控制, 嵌入式Web, 开源硬件控制, 插件系统, 日志审计, 智能家居, 智能家电控制, 本地控制, 桥接, 热回收新风系统, 监控指标, 程序破解, 自定义请求头, 通风设备