Files
home-automation/docs/design/pre-m8-warmtelink-p1-poc.md
tliu93 8dbb59a3b7
frontend / frontend (push) Successful in 30s
pytest / test (push) Successful in 2m23s
PRE-M8-T03: finalize WarmteLink hardware validation
2026-08-22 19:29:54 +02:00

17 KiB
Raw Permalink Blame History

Pre-M8 — WarmteLink P1 真机概念验证

状态:已完成。 2026-08-22 已完成临时脚本真机 bring-up;随后仓库内正式 probe 以 115200 7N1 连续运行 10 分钟,形成脱敏的可重复验收证据;最终人工走查开启供暖后又观察到 区域供暖累计量从 0.017 GJ 增至 0.018 GJ,并与物理表一致。Pre-M8 现已解除 M8 Planning 的入口限制;M8 的架构和实现范围仍未锁定。

1. 目的

在进入 M8 正式设计和实现前,用新家的 Vattenfall WarmteLink 建立一条只读 P1 验证链,回答:

  1. USB→P1 线、主机串口权限和 WarmteLink P1 端口能否稳定输出可解析数据。
  2. 真机使用什么串口参数,telegram 的 framing、CRC、时间戳和 OBIS/M-Bus channel 有何特征。
  3. 实际能够读取哪些累计量,以及单位、精度、更新频率和累计语义。
  4. P1 值是否与热量表/生活热水表面板一致。

Pre-M8 是 M8 的证据门:它只交付真机事实和可重复 probe,不决定数据库结构、Meter 数据源 绑定、后台采集服务或前端布局。

2. 执行边界

本阶段只建立下面这条最短链路:

WarmteLink P1 → USB serial → 原始 telegram → 完整性状态 → 字段解析 → 终端输出

明确不做:

  • 不写入 app.db,不新增 Alembic migration。
  • 不新增 FastAPI API、后台常驻 worker、配置页面或 Energy 前端。
  • 不发布 MQTT / Home Assistant Discovery。
  • 不修改现有 DSMR Reader MQTT、电费计算或 Meter 逻辑。
  • 不在本阶段决定 WarmteLink 应落在哪个正式 Device/Source 模型中。
  • 不写串口、不修改 FTDI EEPROM;probe 必须严格只读。

3. 2026-08-22 真机事实

3.1 USB、权限与线材

  • 新 USB→P1 线是 FTDI FT232R,稳定路径形态为 /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_<redacted>-if00-port0;正式配置不得依赖 /dev/ttyUSB1
  • 运行用户加入 dialout 后可以直接读取 tty,无需 root,也不应把容器作为硬件 bring-up 的中间层。
  • 已用另一根已知正常的 DSMR 线在电表上读到连续、CRC 正确的 DSMR telegram,证明宿主机 串口读取方法本身可用。
  • 曾临时清除新 FTDI 线 EEPROM 的 INVERT_RXD 做对照;输出发生变化但仍不可解析,随后已将 EEPROM 逐字节恢复为原厂镜像并回读校验。正式方案不得依赖 EEPROM 修改。

3.2 串口参数矩阵

真机不是按最初假设的 115200 8N1 得到可读正文;当前线材与 WarmteLink 的实测最佳组合为:

115200 baud, 7 data bits, no parity, 1 stop bit7N1
参数 实测结果
115200 7N1 正文稳定可读,可枚举 9 个 OBIS 字段
115200 7N2 同样可读;没有理由增加停止位,正式默认仍用 7N1
115200 8N1/8E1/8O1 乱码,无有效 OBIS/CRC
115200 7E1/7O1 乱码,无有效 OBIS/CRC
1200003000000,分别用 8N17N1 无完整报文;120000 仅残留少量可辨识文本
XON/XOFF 开/关 不改变 7N1 的字段解析结果

DSMR 5.0.2 P1 Companion Standard 规定 115200 8N1。因此 7N1 是当前设备/线材组合的实测事实,不应被文档或代码包装成 标准 DSMR framing;CLI 必须允许显式覆盖数据位、校验位和停止位。

3.3 Framing 与 CRC 异常

  • 临时 bring-up 的一次 8 帧盘点中每帧均为 256 字节;正式 10 分钟采样显示长度并不固定, 详见 §3.6。
  • 正文结构稳定,版本、时间戳、两个 M-Bus channel、单位和值均可重复解析。
  • 实测头部为 )TU)2NWA-MYRSKY,偶见 )TU{2NWA-MYRSKY;没有标准要求的 / 起始符。
  • 帧尾存在 !,但其后的字符并非每帧都稳定为四位十六进制;即使恰好是四位,也无法从 缺失的 / 起点完成标准 DSMR CRC16 验证。

因此正式 probe 必须把完整性明确表示为 validinvalidunverifiable,保留原始字节并 输出原因。它可以在 unverifiable 状态下枚举字段用于 PoC,但绝不能把该帧报告为 CRC 已通过。 M8 若要持久化这些读数,必须在 Planning 中单独锁定异常帧的接纳、重复确认和告警策略。

3.4 实际字段清单

连续 8 帧只出现以下 9 个字段;除 capture timestamp 外,字段集合和值均稳定:

OBIS 实测结构/值 结论
1-3:0.2.8 (50) DSMR P1 输出版本 5.0
0-0:1.0.0 (YYMMDDhhmmssX) telegram/capture timestamp,每 10 秒变化
0-0:96.1.1 <redacted> WarmteLink/gateway equipment identifier
0-1:24.1.0 (006) channel 1M-Bus device type 0x06,生活热水
0-1:96.1.0 <redacted> channel 1 equipment identifier;样本制造商可解码为 KAM
0-1:24.2.1 (<timestamp>)(5.900*m3) 生活热水累计体积,输出到 0.001 m³ 小数位
0-2:24.1.0 (012) channel 2M-Bus device type 0x0C,热量表
0-2:96.1.0 <redacted> channel 2 equipment identifier;样本制造商可解码为 KAM
0-2:24.2.1 (<timestamp>)(0.017*GJ) 区域供暖累计热量,输出到 0.001 GJ 小数位

人工面板在同一时间显示 5.900 m³0.017 GJ,与 P1 值完全一致;用户已确认这两个累计量 足以作为后续 Home Assistant Energy 展示的数据基础。

没有发现瞬时流量、当前热功率、供水温度或回水温度字段。完整 OMS/M-Bus 模型允许这些可选 量,但 WarmteLink 当前 P1 telegram 没有导出它们;Pre-M8 和 M8 不得假设它们可用。参考 OMS Specification Vol. 2 Annex A

3.5 对 M8 已经成立的事实

  • 一个 WarmteLink serial source 同时暴露两个独立累计 measurement channel。
  • channel 1 是生活热水 channel 2 是区域供暖 GJ,不能按论坛样例固定 channel。
  • 两个累计量都与物理表一致,可进入 M8 的 Meter/source 映射讨论。
  • 当前没有瞬时流量、温度或热功率;M8 只承诺累计量采集与展示。
  • 串口 framing 和 CRC 异常尚未消失,必须作为正式 ingestion 的显式质量状态处理。

3.6 正式 CLI 10 分钟复验(脱敏)

2026-08-22,仓库内 python -m scripts.p1_probe 在真实 /dev/serial/by-id/<redacted> 上以 115200 7N1 运行 --duration 600 --show-changes,原始 bytes 仅写入 /tmp。CLI 正常以 exit code 0 结束;期间没有 I/O error、未处理异常或断连。

  • 读取到 60 个完整 frame、15,362 raw bytes;最后 2 bytes 是下一帧的不完整残片,60 个完整帧 合计 15,360 bytes。
  • 设备 timestamp 从 18:16:1018:26:00,严格每 10 秒递进。CLI 处理的 59 个相邻间隔中, 53 个为 10.0 s、1 个为 9.0 s、3 个为 0.0 s、2 个为 20.0 s,均值 9.81 s0/20 s 配对来自 serial chunk 的批量交付,不代表设备 cadence 改变。
  • 完整 frame 长度分布为 237 bytes × 2、239 × 1、254 × 12、256 × 31、258 × 11、275 × 3 平均 256 bytes、范围 237–275。不能再把临时样本的「固定 256 bytes」视为帧格式契约;变长与 非标准 footer/分块边界一致。
  • 60/60 的完整性均为 unverifiable,因为 60/60 缺少标准 / 起始符。footer 长度为 4 × 41、 6 × 16、23 × 3;仅 16/60 的 footer 恰为四位十六进制,但仍不能在缺失 / 时完成标准 CRC16 验证。没有任何 frame 被报告为 CRC valid。
  • 60/60 帧均解析到同一组 9 个 OBIS code1-3:0.2.80-0:1.0.00-0:96.1.10-1:24.1.00-1:96.1.00-1:24.2.10-2:24.1.00-2:96.1.00-2:24.2.1。 equipment identifier 已脱敏,不进入 Git。
  • channel 1 的 device type 是 006,累计值稳定为 5.900 m³channel 2 的 device type 是 012,累计值稳定为 0.017 GJ。同日人工表盘复核也显示 5.900 m³ / 0.017 GJ,正式 CLI 因而复现了该基线。
  • 60 帧中未见瞬时流量、热功率、供水温度或回水温度字段。10 分钟内累计值未变化只能证明这段 时间的累计值稳定;它不能证明发生消费时的更新频率,也不能证明 reset 或 wrap 行为。

3.7 最终人工走查:供暖累计量变化

正式长测交付后,用户又在终端直接运行只读 probe 十几分钟并开启供暖。channel 2 的区域供暖 累计量在本次运行中从 0.017 GJ 增至 0.018 GJ,同一时刻物理热量表也显示 0.018 GJ; 因此可以确认当前 P1 输出会在实际供暖消费下更新累计量,且 0.001 GJ 的变化与物理表一致。

这次人工走查没有改变完整性结论:telegram 仍缺少标准 /,数值与物理表一致不能替代 CRC 验证。走查也没有覆盖 reset、wrap 或精确更新延迟;这些仍须由 M8 的接纳、重复确认和告警策略 处理,而不能从一次累计量递增外推。

4. 正式 probe 的预期操作方式

实现后,在 workspace virtual environment 中运行只读 probe

source .venv/bin/activate
python -m scripts.p1_probe \
  --device /dev/serial/by-id/<usb-p1-device> \
  --baudrate 115200 \
  --bytesize 7 \
  --parity N \
  --stopbits 1 \
  --duration 600 \
  --show-changes \
  --raw-output /tmp/warmtelink-p1-telegram.bin

参数名可在实现时小幅调整,但必须保留这些能力:

  • 设备路径由用户显式传入,文档推荐 /dev/serial/by-id/...
  • 串口默认采用本机实测 115200 7N1,同时允许显式覆盖 framing。
  • 连续读取多帧,输出 telegram cadence、帧长度和读取/重连错误。
  • 同时显示完整性/CRC 状态、原始 OBIS 字段和解析后的 channel、设备类型、值与单位。
  • parser 不依赖字段固定顺序,也不把 GJ/m³ 固定到 channel 1 或 2。
  • --show-changes 可只显示发生变化的字段,原始捕获可写入用户指定的 /tmp 路径。
  • 捕获文件默认按原始 bytes 保存;未经脱敏不得提交到 Git。
  • permission denied 时给出 dialout 指引;不得建议以 root 常驻运行。

Home Assistant Community 的 最小 WarmteLink Bash 读取方法 仍可作为快速可见性检查,但论坛样例的 header、channel 和 device type 与本机均不同,不能作为 parser 契约。是否引入 dsmr_parser 也由 T01 的实测 兼容性决定;若它要求标准 /...!CRC framing,则应保留小型专用 parser,而不是绕过其校验。

5. 实现任务

PRE-M8-T01 — 纯函数 telegram framing、CRC 与 OBIS parser

  • Status: done
  • Depends: none
  • Context: 先把串口 I/O 与解析分开,用脱敏 fixture 固定标准 DSMR 帧和本机异常帧行为。

Files

  • create scripts/p1_probe.py
  • create tests/fixtures/dsmr_p1_valid.txt
  • create tests/fixtures/warmtelink_p1_7n1.txt
  • create tests/test_p1_probe.py

Steps

  1. 实现不依赖串口的增量 framing、DSMR CRC16 和通用 OBIS 行解析函数。
  2. 用数据结构表达原始 header/footer、完整性状态、timestamp、channel、device type、equipment id、 value 和 unit;数值使用 Decimal,不使用二进制浮点保存累计量。
  3. 加入一份完全脱敏的本机结构 fixture,并另造一份 CRC 正确的标准 DSMR fixture。
  4. 对字段重排、分块输入、缺失 /、非十六进制 footer、CRC mismatch、未知字段和两个 channel 写单元测试。

Out of scope / 不要碰

  • 不打开真实 serial device,不增加依赖,不写数据库/API/MQTT。
  • 不因本机正文可读而伪造 / header 或把 CRC 状态升级为 valid。

Acceptance criteria

  • 标准 fixture 的 frame boundary 与 CRC 可验证为 valid
  • 脱敏本机 fixture 被标为 unverifiable,但能按字段而非位置解析 GJ channel。
  • 任意 chunk boundary 和字段顺序不影响结果,未知字段原样保留。
  • 累计量以 Decimal + 原单位返回。
  • pytest tests/test_p1_probe.pypytestruff check . 全绿。

Reviewer checklist

  • CRC 覆盖范围必须严格从 /!(包含二者),不得对缺失字节做猜测性修补。
  • fixture 必须脱敏且保留足以复现 framing 异常的字节结构。
  • parser 不得硬编码 channel 1=GJ 或 channel 2=m³。

PRE-M8-T02 — 只读 serial probe CLI

  • Status: done
  • Depends: PRE-M8-T01
  • Context: 在纯 parser 通过后增加最小 serial I/O,使真机验证可以从仓库稳定复现。

Files

  • modify requirements.in
  • modify requirements.txt
  • modify dev-requirements.txt
  • modify scripts/p1_probe.py
  • modify tests/test_p1_probe.py

Steps

  1. 增加受约束的 pyserial runtime 依赖并用仓库既有 pip-compile 流程同步生成 requirements。
  2. 实现 --device、framing 参数、--duration--show-changes--raw-output
  3. 默认使用 115200 7N1;串口只读,禁止 write、EEPROM 或自动修改设备配置。
  4. 输出每帧完整性状态、全部字段、值变化、cadence 和错误;SIGINT/超时后关闭串口并正常退出。
  5. 用 fake serial/chunk stream 测试 CLI,不要求 CI 存在 /dev/ttyUSB*

Out of scope / 不要碰

  • 不做 daemon、自动重连 worker、Docker device mapping、数据库、API、MQTT 或 HA Discovery。
  • 不内置本机 FTDI 序列号,不自动扫描或改写任意 USB 设备。

Acceptance criteria

  • CLI 可用 /dev/serial/by-id/... 读取,且所有 framing 参数都可显式覆盖。
  • 默认参数准确反映本机 115200 7N1,帮助文本说明它是实测值而非 DSMR 标准默认。
  • raw output 保留原始 bytes;终端清楚区分 validinvalidunverifiable
  • permission/busy/disconnect 错误非零退出并给出可执行诊断,绝不建议常驻 root。
  • 依赖输入与两个生成 requirements 文件同步。
  • pytest tests/test_p1_probe.pypytestruff check . 全绿。

Reviewer checklist

  • 确认所有 serial write path 均不存在。
  • 确认测试完全 mock 硬件、没有 CI timing flake,退出路径总会关闭文件描述符。
  • 确认 requirements 是生成结果而非仅手改 lock file。

PRE-M8-T03 — 正式真机验收与 M8 交接

  • Status: done
  • Depends: PRE-M8-T02
  • Context: 用仓库内 probe 替代本轮临时脚本,形成可重复、脱敏且能支撑 M8 Planning 的证据。

Files

  • modify docs/design/pre-m8-warmtelink-p1-poc.md
  • modify docs/design/m8-warmtelink-energy.md
  • modify docs/design/README.md
  • modify docs/roadmap.md

Steps

  1. 在真实 /dev/serial/by-id/... 上运行 probe 至少 10 分钟,并保留原始捕获在 /tmp
  2. 汇总帧数、cadence、长度、完整性状态、全部字段和读数变化;不得提交原始设备标识。
  3. 再次与物理表对照 GJ 和 m³,并记录累计/重置行为中本次能证实和不能证实的部分。
  4. 更新本节事实、通过条件和 M8 入口;只有证据齐全后才把 Pre-M8 标记为完成。

Out of scope / 不要碰

  • 不为完成 checklist 而修补原始字节或放宽 CRC 结果。
  • 不进入 M8 schema/API/worker/frontend 实现。

Acceptance criteria

  • 仓库内 probe 在真机连续运行至少 10 分钟,无未处理异常退出。
  • 脱敏摘要列出两个累计 channel、单位、精度、cadence 和完整性异常。
  • 0.017 GJ5.900 m³ 的基线或运行时新值与物理表再次对照。
  • 明确没有从当前 P1 输出读取到瞬时流量、功率或温度。
  • Pre-M8 状态与 roadmap/M8 入口同步;代码闸门保持全绿。

Reviewer checklist

  • 证据必须来自正式 CLI,不得只复述本轮临时脚本结果。
  • 任何 equipment id、FTDI serial 和未脱敏 raw capture 都不得进入 Git。
  • CRC/framing 风险必须原样交给 M8,不能用“数值看起来正确”替代完整性判断。

6. 当前通过条件

  • USB、tty 权限和稳定 /dev/serial/by-id/... 路径已验证。
  • 串口参数矩阵已完成,实测正文可读参数为 115200 7N1
  • 已枚举全部实际 channel/OBIS 字段,并确认没有轮换出现的额外测量量。
  • GJ 和生活热水 m³ 均与物理表面板完全一致。
  • 已记录精度、约 10 秒 telegram cadence 和当前字段集合。
  • framing/CRC 失败已保留为显式异常,没有误报为校验通过。
  • 仓库内 parser、fixtures、probe CLI 和自动化测试完成。
  • 正式 probe 完成至少 10 分钟真机复验并产出脱敏摘要。
  • 最终人工走查在供暖开启后观察到 0.017 → 0.018 GJ,并再次与物理表核对一致。

Pre-M8 已完成并向 M8 解锁 Planning;它只交付下列真机事实,不锁定正式架构或实现任务。

7. 向 M8 的交付物

Pre-M8 完成后只向 M8 交付事实,不交付正式架构:

  • 脱敏 telegram 结构、fixture 和全部字段清单。
  • GJ 与 m³ 的实际 channel、device type、单位、精度和更新时间。
  • 串口参数、稳定设备路径形态与 dialout 权限要求。
  • parser 适配结论和 unverifiable framing/CRC 异常样本。
  • “一个 serial source 包含两个独立累计计量 channel”的实测结论。
  • 当前 P1 不提供瞬时流量、热功率或温度的明确边界。