9.6 KiB
WarmteLink、数据源与热力计费运维手册
本手册说明如何在不改变既有 DSMR、Modbus 和 electricity 功能的前提下,部署只读的 WarmteLink P1 采集、绑定热量/生活热水 Meter、配置热力合同,并按需暴露给 Home Assistant。所有示例均使用占位符;不要把设备标识、GID、数据库路径、token 或真实合同金额提交到仓库。
安全边界与开始前备份
- 应用容器继续使用基础
docker-compose.yml中的非 rootuser: "1000:1000";基础文件和 dev 合并配置都不设置privileged或额外 capability。 - Docker device cgroup 必须以
rw映射,容器内固定为/dev/warmtelink:这是 pyserial 3.5 在 POSIX 上以O_RDWR打开串口所需的最小系统权限,并不表示业务可写。规则绝不包含m,且不授予 root、privileged或额外 capability。WarmteLink worker 仍只调用 serialread/close,绝不调用write或发送写命令。 - 不删除或覆盖
app_config、app.db、旧数据库、Docker volume 或既有 source。禁用/解绑/回滚配置不是删除历史的替代方式。 - 维护前停止写入窗口,使用宿主机的备份流程复制
./data/app.db到受保护的备份位置;确认备份可用后才运行 migration。不要把生产库复制到开发机或用于测试。
识别稳定串口并配置 Compose
在宿主机(不是容器)找出稳定 symlink;不要使用会在重启后变化的 /dev/ttyUSB* 名称:
ls -l /dev/serial/by-id/
stable_path=/dev/serial/by-id/<stable-by-id-name>
stat -c '%g %n' "$stable_path"
记录输出的数字 GID,而不是猜测 dialout 的数值。确认 path 指向预期的字符设备后,在部署机本地 .env(不提交)设置:
WARMTELINK_DEVICE_PATH=/dev/serial/by-id/<stable-by-id-name>
WARMTELINK_SERIAL_GID=<host-serial-gid>
production 使用基础 compose;local dev 使用 base 与 dev 合并文件。两种环境都会从本地 .env 读取这两个变量:
docker compose -f docker-compose.yml up -d
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
启动前可用相同文件组合运行 docker compose ... config。结果的 app 必须仍显示 user: "1000:1000",并只出现从 stable by-id path 到 /dev/warmtelink 的 rw device mapping 与 group_add GID;migration 不得有 device 或 group。不得出现 privileged、root user 或 m device permission。rw 仅满足 pyserial 的 POSIX O_RDWR 打开,不改变 worker 的只读业务行为。不得把设备路径或 GID 写入仓库的 .env.example 或文档。
同一物理串口在任意时刻只能有一个 owner。运行 Pre-M8 p1_probe.py 前,必须先停止 app(包括 dev stack),并在 probe 结束后再启动 app;不要让 probe 与 worker 同时打开该串口。
Migration 与 source 配置
先在维护窗口运行 migration;它只升级 schema,绝不删除历史表或配置:
docker compose -f docker-compose.yml run --rm migration
登录 Energy 页面,在 Sources 创建 warmtelink_serial source:
| 字段 | 值 |
|---|---|
| Path | /dev/warmtelink |
| Baud rate | 115200 |
| Data bits / parity / stop bits | 7 / N / 1 |
| Enabled | 先关闭,保存并检查配置后再开启 |
这些参数是固定 P1 profile。不要录入 telegram 或 equipment id;应用不会把原始 frame、设备身份或 serial 设置以外的协议标识保存到 source 配置中。开启后选择 Discover。成功发现后应有两个只读 channel:heating(GJ)和 hot water(m³);受 P1 CRC 限制,正常可接纳的质量可能显示为 unverifiable,这不是被错误提升为 valid。
在 Meters 分别创建或选择 heating 与 hot_water Meter,并各自选择与单位相符的 channel 创建 binding。source switch 只关闭旧 binding、在相同 Meter 上创建新 binding,不是换表;真正的物理换表才使用 Meter swap。跨 binding 或 Meter 边界的 15 分钟成本周期会明确标为 degraded,不能相减伪造成本。
运行检查与排障
启用 source 后,latest 通常约每 10 秒更新,持久化 history 最多每分钟一条。检查 source 状态、channel latest/quality 和绑定时间线,而不是从日志中寻找原始 telegram。
| 现象 | 安全排查 |
|---|---|
offline 或无法 open serial |
核对 stable symlink 是否仍存在、stat 的 GID 是否等于 WARMTELINK_SERIAL_GID,再用 docker compose ... config 检查 non-root rw(非 r、非 m)device rule;不要用 root/privileged 绕过权限。 |
| 无 channels / discover 超时 | 确认 source 启用、/dev/warmtelink path 和固定 115200 7N1;检查电缆供电后重试 Discover。 |
| 短暂拔线 | worker 标记 offline 并以退避重连;插回后应恢复。检查 history 的 (channel, recorded_at) 唯一性,不能手工补重复行。 |
| 成本 degraded | 查两端读数 freshness(120 秒)、quality、Meter epoch 与 binding;不要通过修改累计值清除 degraded。 |
若需要停采集,在 UI 禁用该 source,确认 worker 关闭串口后再维护电缆。删除有 channel、binding 或历史的 source 会被 API 拒绝;保留记录以保证审计和成本重算。
热力合同、成本与 Home Assistant
在 Contracts 选择 Thermal scope,创建 district_heating 合同及版本。费率由 operator 按合同人工录入,字段为 heating(EUR/GJ)、hot-water heating / water / tax(EUR/m³)与五个年固定费字段;仓库不含任何真实默认金额。thermal 和 electricity 各可有一个 active 合同,彼此不互斥。
成本页的 15 分钟 ledger 分开显示 heating 与 hot-water 三项 variable breakdown;fixed 费只在合同级 summary 按本地自然日计提一次,all-in = variable + fixed。用显式 recompute 来验证测试时间窗时,应手算并核对 Decimal 金额,保留原有 electricity 合同和数字不变。
在 Config 的 HA Expose 中只开启需要的 source、Meter 与 thermal entities。核对 unit、state class、availability、today reset 和换表后 identity;关闭 toggle 后应用会清理 retained discovery。不要把 source secret、设备 identity 或合同金额放进 HA entity 名称、日志或截图。
安全回滚
- 在 UI 禁用 WarmteLink source,确认状态离线且 worker 已停止;保留 channels、bindings、history、contracts 与成本账本。
- 停止 app 后,在 UI 保持 source 禁用;这不会删除
./data、数据库、配置或 volumes。需要恢复 WarmteLink 时,确认本地.env的 stable by-id/GID 后再重新启用 source。 - 确认 DSMR、Modbus、电价、既有 electricity 成本和前端正常。
- schema migration 不应以 production downgrade 回滚;只有经过验证的备份恢复流程才处理灾难恢复,且必须由 operator 在隔离维护窗口执行。
上线验收清单
- production 与 base+dev Compose 都从本地
.env获取 serial path/GID;app/migration 均为非 root,只有 app 有/dev/warmtelink:rw和 serial GID,绝无m、root 或 privileged。该rw仅为 pyserial 的O_RDWR打开,worker 业务仍只读,且没有真实设备/GID 被记录在仓库。 - 下列项目是交付后由用户在备份数据库、可回滚部署、真实 serial 设备和真实 HA 环境执行的人工验收;自动化技术验收不能替代这些观察,也不得把它们伪称为已执行。
用户人工验收记录(交付后填写)
本模板在交付后由用户填写;它不描述当前宿主环境,也不代表任何项目已通过。每项均填写日期、隔离部署 标识、§12 项号、预期观察、实际观察、脱敏日志/截图引用、回滚结果和结果状态。不得记录 stable device id、 GID、secret、数据库路径或合同金额;交付时所有项目默认均为“未执行”,不得填造真实环境结果。
| §12 项号 | 日期 | 隔离部署标识 | 预期观察 | 实际观察 | 脱敏日志/截图引用 | 回滚结果 | 结果状态 |
|---|---|---|---|---|---|---|---|
| 1. 默认 stack 回归 | 待填写 | 待填写 | DSMR、Modbus、电价/电费、前端均无回归 | 待填写 | 待填写 | 待填写 | 未执行 |
| 2. stable by-id 与权限 | 待填写 | 待填写 | 非 root、非 privileged、rw 无 m;pyserial 可打开,worker 无 write |
待填写 | 待填写 | 待填写 | 未执行 |
| 3. Discover 与隐私 | 待填写 | 待填写 | 两 channel/unit/quality;日志、DB、API、UI 均完成脱敏检查 | 待填写 | 待填写 | 待填写 | 未执行 |
| 4. history/拔插重连 | 待填写 | 待填写 | latest、分钟 history、拔插恢复且无重复记录 | 待填写 | 待填写 | 待填写 | 未执行 |
| 5. source switch / Meter swap | 待填写 | 待填写 | 两条时间线正确、边界 degraded、HA identity 按设计变化 | 待填写 | 待填写 | 待填写 | 未执行 |
| 6. thermal 合同/成本 | 待填写 | 待填写 | 脱敏测试费率手算一致;15 分钟与 01:05 fixed 正确 | 待填写 | 待填写 | 待填写 | 未执行 |
| 7. 双 active scope | 待填写 | 待填写 | electricity 数字对照一致;UI/成本/HA 不串 scope | 待填写 | 待填写 | 待填写 | 未执行 |
| 8. HA toggles | 待填写 | 待填写 | unit、state class、availability、today、identity、retained cleanup 正确 | 待填写 | 待填写 | 待填写 | 未执行 |
| 9. 重启与默认 compose 回滚 | 待填写 | 待填写 | 历史恢复;回默认 compose 后数据完整 | 待填写 | 待填写 | 待填写 | 未执行 |