ericboehs/aosmith-water-heater
GitHub: ericboehs/aosmith-water-heater
通过 CTA-2045 RS-485 接口对 A.O. Smith 热泵热水器进行本地监控和需求响应控制,经 MQTT 桥接至 Home Assistant,完全无需云账户。
Stars: 0 | Forks: 0
# A.O. Smith 热泵热水器 — 通过 CTA-2045 进行本地控制
通过 CTA-2045 "EcoPort" (RS-485) 对 A.O. Smith HPS10-50H45DV 热泵热水器进行本地监控和需求响应控制,并通过 MQTT 发布到 Home Assistant。无需云账户,不依赖 iCOMM。
制造商的云 API 经常宕机,对自动化毫无用处,而且其热水读数被量化为 0 / 50 / 100 %。水箱自身的端口报告相同数据,但精度达到十分之一个百分点,并且响应时间约为两秒。
## 硬件
| 部件 | 详情 |
|---|---|
| 水箱 | A.O. Smith HPS10-50H45DV (Voltex 混合动力) |
| 适配器 | Raspberry Pi Zero 2 W + Waveshare 隔离型 RS-485 HAT |
| 端口 | `/dev/serial0` → `ttyAMA0` (PL011), **19200 8N1** |
| 操作系统 | Raspberry Pi OS Lite (64 位), Debian 13 trixie, Python 3.13 |
在 `/boot/firmware/config.txt` 中,Pi 需要 `enable_uart=1`、`dtoverlay=miniuart-bt` 和 `core_freq=250`,这样真正的 PL011 才会连接到 GPIO 接脚上,而不是 mini-UART。
### ⚠️ 此接口内存在 240 V 电压
这是 CTA-2045 的 **AC 规格版本** —— 两根 120 V 火线与 RS-485 双绞线共用同一个接口,并且没有零线。它不是 EPRI 的 Arduino 模拟器中使用的低压 SPI 变体。在接触该接口之前,请务必阅读本节内容。
**在本设备上,排针的上下两行与规格图相反。** 顶行从左到右是引脚 7–12,底行从左到右是引脚 1–6。在相信任何线束或引脚图(包括本图)之前,请先用万用表进行验证。
| 行 | | | | | | |
|---|---|---|---|---|---|---|
| **顶行** | **Data+** (7) | 信号地 (8) | — | 大地 (10) | — | **120 V L1** (12) |
| **底行** | **Data−** (1) | — | — | — | **120 V L2** (5) | — |
只需三根线:**Data+ (7)、Data− (1)、信号地 (8)**。切勿接触引脚 5 和 12。
- **数据引脚与火线位于同一排的两端。** Data+ 是其所在行的第一个位置,而 120 V 火线是最后一个位置 —— 它们相隔四到五个位置,中间是空脚和地线。两根火线在位置上呈对角线分布,测量它们之间的电压为 240 V。
- **该设备上的接口防反插凸起已被打磨掉。** 因此,稍微插错位置只会碰到空脚或信号地,但**如果插反**,两行的端点就会互换,从而导致数据线导体接触到火线。请在接口和线束的 Data+ 角落涂上漆点作为标记,每次都检查方向。
- **使用隔离型收发器。** 数据引脚距离 240 V 仅几毫米,且信号地与大地相连。
- 健康的空闲状态:Data+ 3.232 V,Data− 1.592 V,1.640 V 差分电压 —— 带有故障安全偏置电阻的 3.3 V 收发器。
- **板卡未通电时 A+/B− 之间的电阻读数为 9.57 Ω 是正常的** —— 这是基板二极管加上 TVS 网络的结果,而不是短路。通电后,读数为 `OL`。
该接口只有 **一** 对差分线,因此总线在物理连线上就是半双工的,而不是在软件层面上。没有任何 UART 设置能改变这一点。
## 可用功能与局限性
读取:
- 热水百分比,精确到 0.1 %
- 运行状态(13 种状态 — 空闲/运行 × 正常/削减/增强,等)
- 储能容量和亏缺量,单位 Wh
- 寿命期累计能量
- 设定温度(实测为 150 °F)
- 型号和序列号
写入:
- 削减 (Shed)、结束削减 (End Shed)、增载 (Load Up)、严重高峰、电网紧急状态 — 均被接受并反映在运行状态中
本设备不支持,尽管规范中有定义:
- **设定温度写入** 返回“未实现”。
- **当前水温** 被拒绝:link NAK 原因 7,响应码 6。
- **运行模式** (混合动力 / 纯电 / 热泵 / 度假) 完全没有 CTA-2045 指令。只能通过前面板控制。
任何数据手册中均未提及的行为发现:
- **报告的功率是铭牌上的固定值,而非实测值。** 水箱在加热棒通电时准确报告 4500 W,在压缩机运行时准确报告 356 W,在数百次轮询中字节完全一致。请将其视为一种分类的“什么正在运行”指标。若要获取真实功率,请使用钳形电流表。
- **储能容量随运行模式而变化** — 纯电模式下为 7275 Wh,而混合动力模式下为 4250 Wh — 因此热水百分比必须通过*同一次*轮询中的容量和亏缺量来计算,绝不能使用缓存的容量值。
- **商品代码 7 表示亏缺量,而不是水位。** 它随着热水的消耗而上升。热水百分比 % = `100 × (1 − deficit / capacity)`。
- **削减 和 增载 在纯电模式下不起作用** — 没有压缩机可以调度。在混合动力模式下,增载 会提高目标温度并使压缩机持续运行,但绝不接通电阻加热棒,因此它能在不消耗更多电力的情况下提前储热。
- **水箱在最后一次通信状态消息后约 14 分钟会触发 SGD Error,这会清除所有活动事件。** 每 5 分钟发送一次心跳是强制性的,绝非可选。
## 协议说明
线路传输格式为 `msgType1, msgType2, length (2 bytes big endian), payload bytes, 2-byte checksum`。链路层的 ACK/NAK 是个例外:是不带长度和校验和的纯两字节消息 (`06 00`, `15 `)。
校验和是对以 `0xAA` 为种子、进行模 255 求和后计算的 Fletcher 校验:
```
sum1, sum2 = 0xAA, 0x00
for byte in data:
sum1 = (sum1 + byte) % 0xFF
sum2 = (sum2 + sum1) % 0xFF
first = 255 - ((sum1 + sum2) % 255)
second = 255 - ((sum1 + first) % 255)
```
商品响应包含两个操作码字节,一个前导字节,然后是 **13 字节的记录**:商品代码 (1) + 瞬时速率 (6, big endian) + 累积量 (6, big endian)。观测到的代码:`0x00` 电力,`0x06` 储能容量,`0x07` 储能亏缺量;`0x0a`/`0x0b` 对应 `0x06`/`0x07`。
事件持续时间编码为 `seconds = value² × 2`。零表示无限期,这段代码刻意从不发送此值 —— 一个永不过期的事件会比设置它的 daemon 存在得更久。
### 耗费了大量调试时间的三个问题
1. **总线为半双工。** 在水箱响应中途进行发送会与其发生冲突并破坏数据帧。响应是在静默状态下接收的,只有在获取完整帧后才会进行确认 —— 而且水箱根本不需要确认,因此默认关闭了确认 (ack)。
2. **必须独占端口。** 同一个 tty 上的第二个进程会悄悄窃取字节:驱动程序的 `TIOCGICOUNT` 显示接收到了数百个字节,而你自己的 `read()` 却什么也没返回。每一个症状看起来都像是协议错误。`SerialPort` 会采用 `flock`,因此失效的读取器会直接导致启动报错,而不是留下一个难以排查的谜团。
3. **水箱会重传它认为未被听到的响应。** 在发送下一个请求时仍在传输中的字节会被当作该请求的响应读取,连续的轮询会因此永久错位。`Link.request` 在每次交互后会清空 400 毫秒的缓冲。
最初尝试了 EPRI 的 `libcea2045`。它报告了一系列 `incomplete message received: 16/24/32` 错误,这看起来像是组帧错误 —— 但这些数字都是 8 的倍数,真正的原因是遗忘的第二个进程正在读取同一个 tty 并以 8 字节为单位切片窃取数据。**那不是该库的错**,这个发现被记录在此处而不是被悄悄忽略,因为这正是此总线极易引发的典型误判。
保留此实现是因为它是独立且易于调试的 —— 没有构建步骤,不需要 C++ 工具链,每个字节都有 trace hook,而且端口锁使得再次发生这种资源冲突故障成为不可能。
## 目录结构
```
mqtt/cta2045.py protocol: framing, checksum, parsing, Tank helpers
mqtt/cta2045_mqtt.py the daemon: poll → MQTT with HA discovery
systemd/cta2045-mqtt.service
tools/tank-cli.py manual status and demand-response control
tools/probe.py raw-wire diagnostic; prints every frame
homeassistant/cards.py Lovelace card definitions
homeassistant/patch_dashboard.py installs them via the websocket API
ble/ iCOMM Bluetooth LE protocol, recovered but blocked
```
## 蓝牙方面
CTA-2045 无法读取泄漏传感器,无法设置模式,也无法设置设定温度。水箱的 iCOMM 模块通过 BLE 暴露了这三者,并且其协议是通过反编译应用程序的 Hermes bundle 完全还原出来的 —— 包括组帧、CRC-8、会话握手以及寄存器映射。
但它不起作用。该模块在 ATT 层接受每一帧数据,却从不发回通知。`ble/` 目录中包含了相关脚本、寄存器表以及[关于所有被排除可能性的记录](ble/README.md),这样下次尝试就不必从零开始了。
## 安装
```
sudo apt install python3-paho-mqtt
mkdir -p ~/cta2045-mqtt
cp mqtt/cta2045.py mqtt/cta2045_mqtt.py ~/cta2045-mqtt/
# 凭据,root 拥有,模式 0600 — 永远不要放在 repo 中。
sudo install -m 0600 /dev/null /etc/cta2045-mqtt.env
sudo tee /etc/cta2045-mqtt.env >/dev/null <<'EOF'
MQTT_HOST=...
MQTT_USER=...
MQTT_PASSWORD=...
EOF
sudo cp systemd/cta2045-mqtt.service /etc/systemd/system/
sudo systemctl enable --now cta2045-mqtt
```
其他所有配置均由环境变量驱动:`CTA2045_PORT` (`/dev/serial0`)、`CTA2045_POLL_INTERVAL` (30 秒)、`CTA2045_HEARTBEAT_INTERVAL` (300 秒)、`CTA2045_EVENT_DURATION` (1800 秒)、`CTA2045_EVENT_REISSUE` (600 秒)、`MQTT_PORT`、`MQTT_TOPIC_BASE`、`MQTT_DISCOVERY_PREFIX`、`MQTT_NODE_ID`。
如果没有配置 `MQTT_PASSWORD`,daemon 将拒绝启动,而不会进行匿名连接。
## Home Assistant
通过 MQTT 发现,会作为一个单一设备生成十个实体:
| 实体 | 备注 |
|---|---|
| `sensor.*_operational_state` | 例如 Running Heightened |
| `sensor.*_hot_water` | % |
| `sensor.*_energy_deficit` | Wh |
| `sensor.*_storage_capacity` | Wh,取决于模式 |
| `sensor.*_nominal_power_draw` | 铭牌值,刻意**不设置** `device_class: power` |
| `sensor.*_lifetime_energy` | kWh,`total_increasing` |
| `sensor.*_setpoint` | °F,只读 |
| `binary_sensor.*_heating` | |
| `binary_sensor.*_sgd_error` | 水箱丢失了 RS-485 链接 |
| `select.*_demand_response` | 正常 / 削减 / 增载 / 严重高峰 / 电网紧急状态 |
该选择器 (`select`) 是唯一可写的控制项,因为它是水箱实际允许你更改的唯一内容。
指令是在轮询线程上应用的 —— 因为该链路是半双工且不可重入的 —— 但传入的指令会提前中断轮询休眠,因此在 Home Assistant 中的更改只需约一秒钟即可到达水箱。
在启动时,daemon 会读取运行状态并**接管 (adopt)** 任何已经在运行的事件,因此重启或重新启动系统不再会默默取消它。水箱报告的是效果而非指令,因此“削减 (Curtailed)”被作为“削减”接管,“增强”被作为“增载”接管。
## 手动控制
```
tools/tank-cli.py status
tools/tank-cli.py watch 30 20
tools/tank-cli.py loadup 45
tools/tank-cli.py normal
```
这些脚本会获取与 daemon 相同的锁,因此在服务运行期间执行它们会明显报错,而不是窃取其字节。请先停止服务。
## Wi-Fi
在 Pi Zero 2 W 上,请禁用 Broadcom 省电模式,否则在原本信号良好的连接上预计会出现约 20% 的丢包率和 300 毫秒的延迟峰值:
```
# /etc/NetworkManager/conf.d/wifi-powersave-off.conf
[connection]
wifi.powersave = 2
```
在原本信号良好的连接上(−50 dBm,信噪比 60/70,无内核错误)更改前后实测对比:丢包率 20% → 0%,平均延迟 77.8 毫秒 → 14.5 毫秒。
## 现有技术
- [epri-dev/CTA-2045-UCM-CPP-Library](https://github.com/epri-dev/CTA-2045-UCM-CPP-Library)
— 参考实现,也是这里的启动技术栈。
- [epri-dev/CTA-2045-Desktop-Simulator](https://github.com/epri-dev/CTA-2045-Desktop-Simulator)
— 中间态的响应码表位于 `Main_Form.vb` 中,且不存在于任何免费的 PDF 中。
- [EPRI/OpenADR mapping guide](https://www.bpa.gov/-/media/Aep/energy-efficiency/emerging-technologies/ET-Documents/epri-report_openadr-2045-guidelines-may-2019-000000003002008854.pdf)
— 运行状态枚举的来源。
CTA-2045-B 标准本身是收费的,这就是为什么这里的部分字段是通过观察到的行为来描述的,而不是引用标准文献。
## 注意事项
这里的所有内容都是针对**一台**水箱测试出来的 —— 一台运行设备版本为 2.14 的 HPS10-50H45DV。功能位图和未实现的指令因型号和固件而异,因此请将“不可用”列表视为针对本设备的测试结果,而不是对 A.O. Smith 产品的普遍评价。
水箱不是一种计量设备。若要获取真实功率,请使用钳形电流表。
## 许可证
MIT — 查看 [LICENSE](LICENSE)。
标签:CTA-2045, Home Assistant, RS-485, 智能家居, 本地控制, 热泵热水器, 物联网, 逆向工具