From c37dfacfc79368e4908242c52bd6ae661e716a08 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sat, 22 Aug 2026 17:23:41 +0200 Subject: [PATCH] PRE-M8: record WarmteLink findings and implementation plan --- docs/design/README.md | 2 +- docs/design/m8-warmtelink-energy.md | 20 +- docs/design/pre-m8-warmtelink-p1-poc.md | 310 ++++++++++++++++++------ docs/roadmap.md | 18 +- 4 files changed, 266 insertions(+), 84 deletions(-) diff --git a/docs/design/README.md b/docs/design/README.md index 0541e84..fb05b9f 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -9,7 +9,7 @@ - [`m5-iot-energy.md`](./m5-iot-energy.md) — IoT 集成与能耗采集(Modbus/Energy + MQTT/HA Discovery + 前端侧边栏) - [`m6-tibber-dynamic-energy.md`](./m6-tibber-dynamic-energy.md) — 通用电价层 + DSMR 实时电表接入 + 实时买卖电费计算 + HA Energy 反哺 - [`m7-meter-epochs-archival.md`](./m7-meter-epochs-archival.md) — 电表生命周期 / 换表归档(Meter epochs) -- [`pre-m8-warmtelink-p1-poc.md`](./pre-m8-warmtelink-p1-poc.md) — WarmteLink P1 真机概念验证(等待线到货) +- [`pre-m8-warmtelink-p1-poc.md`](./pre-m8-warmtelink-p1-poc.md) — WarmteLink P1 真机概念验证(bring-up 已完成,正式 probe 待实现) - [`m8-warmtelink-energy.md`](./m8-warmtelink-energy.md) — WarmteLink P1 与多数据源 Meter(Planning 占位) 本文件定义**所有任务共用的格式与协作规则**,各个里程碑文档不再重复这些约定。 diff --git a/docs/design/m8-warmtelink-energy.md b/docs/design/m8-warmtelink-energy.md index e6305c9..9133d1f 100644 --- a/docs/design/m8-warmtelink-energy.md +++ b/docs/design/m8-warmtelink-energy.md @@ -1,13 +1,15 @@ # M8 — WarmteLink P1 与多数据源 Meter(Planning 占位) -> **状态:Planning 占位;尚未拆解任务卡,尚未锁定架构。** M8 必须等待 [Pre-M8 真机概念验证](./pre-m8-warmtelink-p1-poc.md)完成后再进入正式设计。 +> **状态:Planning 占位;尚未拆解任务卡,尚未锁定架构。** Pre-M8 人工 bring-up 已确认 +> 两个累计量,但 M8 仍须等待 [Pre-M8 真机概念验证](./pre-m8-warmtelink-p1-poc.md) +> 的正式 probe、测试和长时间复验完成后再进入正式设计。 ## 1. 候选目标 把 Vattenfall WarmteLink 的 P1 数据接入现有 Energy 模块,至少支持: - 区域供暖累计热量(GJ)。 -- 真机 telegram 确认存在时的生活热水累计量(预计为 m³,最终以实测为准)。 +- 生活热水累计量(真机已确认为 m³)。 - 历史读数、当前状态以及按需暴露给 Home Assistant。 - 与现有 Meter epoch/换表归档语义兼容。 @@ -18,6 +20,11 @@ - 当前 electricity Meter 与 `dsmr_reading` 之间没有显式 source FK/binding;电费计算通过代码约定直接查询 DSMR 电力寄存器。 - `Meter.commodity` 后端已为 `heating` 等品类预留,但“增加 commodity”本身不会自动获得相应数据源或解析能力。 - 当前 Devices UI/模型是 Modbus 专用,不能直接假设 WarmteLink 应复用 `modbus_device`。 +- 真机实测一个 WarmteLink serial source 同时输出两个累计 channel:channel 1 为生活热水 + `m³`,channel 2 为区域供暖 `GJ`;两者均已与物理表对照一致。 +- 当前 P1 telegram 约每 10 秒一帧,没有瞬时流量、热功率、供水温度或回水温度字段。 +- 当前线材/设备组合以 `115200 7N1` 才能稳定解析正文;header/CRC 仍不可验证,正式 ingestion + 必须显式处理该数据质量状态。 ## 3. 下一轮 Planning 必须讨论的问题 @@ -28,7 +35,7 @@ 3. “Device”与“Data Source”是否为同一概念;前端 Devices 是否需要改名或分组。 4. 一个 P1 source 暴露多个 measurement channel 时,如何映射到一个或多个 Meter。 5. 直接 P1 读数是否使用独立存储,还是将现有 `dsmr_reading` 泛化;如何保证多 source 去重和隔离。 -6. heating GJ 与可选 hot-water m³ 的 commodity、单位、累计/换表语义。 +6. heating GJ 与 hot-water m³ 的 commodity、单位、累计/换表语义。 7. M8 是否只做采集与展示;区域供暖合同、价格和成本计算是否留到后续里程碑。 8. 串口 worker 的重连、停止、配置热更新、Docker device mapping 与权限边界。 @@ -37,14 +44,15 @@ 正式编写 M8 目标架构、数据模型和原子任务卡前,至少需要: - Pre-M8 通过并留下脱敏字段清单。 -- 确认实际存在几个累计量及其单位、equipment id/channel 和更新时间。 -- 确认原始 telegram 的稳定性与 parser 适配方式。 +- 用正式 probe 复验两个累计量及其单位、equipment id/channel 和更新时间。 +- 确认原始 telegram 的长期稳定性、parser 适配方式和异常 CRC 的接纳策略。 - 重新走查现有 DSMR ingest、Meter epoch、Modbus device、expose/HA 和 Energy 前端边界。 - 与用户讨论并锁定 Meter ↔ source 的配置体验后,再决定 migration/API/UI 方案。 ## 5. 当前明确不做 - 本占位不创建 implementation task,不授权 schema/API/frontend 变更。 -- 不假设生活热水 m³ 一定可读,也不承诺可拆分“空间供暖 GJ”和“生活热水 GJ”。 +- 不承诺当前 P1 未提供的瞬时流量、热功率或温度,也不承诺可拆分“空间供暖 GJ”和 + “生活热水 GJ”。 - 不提前把 WarmteLink 塞进 `modbus_device` 或现有 `dsmr_reading`。 - 不在缺少真机证据时设计通用 telemetry framework。 diff --git a/docs/design/pre-m8-warmtelink-p1-poc.md b/docs/design/pre-m8-warmtelink-p1-poc.md index 4fc64c2..952d134 100644 --- a/docs/design/pre-m8-warmtelink-p1-poc.md +++ b/docs/design/pre-m8-warmtelink-p1-poc.md @@ -1,24 +1,27 @@ # Pre-M8 — WarmteLink P1 真机概念验证 -> **状态:等待 USB→P1 线到货后执行。** 本文只定义验证边界与证据要求;当前仓库尚未实现下文所示的 probe 命令。 +> **状态:进行中。** 2026-08-22 已完成临时脚本真机 bring-up,并确认两个可用累计量; +> 仓库内的正式 probe、解析测试和可重复验收流程尚未实现。完成下方 PRE-M8-T01~T03 +> 后,才把 Pre-M8 标记为完成并解除 M8 Planning 的入口限制。 ## 1. 目的 -在进入 M8 正式设计和实现前,先用新家的 Vattenfall WarmteLink 做一次只读真机验证,回答以下问题: +在进入 M8 正式设计和实现前,用新家的 Vattenfall WarmteLink 建立一条只读 P1 验证链,回答: -1. 当前 USB→P1 线、主机串口权限和 WarmteLink P1 端口能否稳定输出完整 telegram。 -2. telegram 的 framing、CRC、时间戳、OBIS/M-Bus channel 和单位能否被 parser 正确识别。 -3. 实际能够读取哪些累计量:区域供暖热量(GJ)、生活热水体积(m³)或其它字段。 -4. 读数的精度、更新频率和累计语义,是否与热力表/水表面板上的数字一致。 +1. USB→P1 线、主机串口权限和 WarmteLink P1 端口能否稳定输出可解析数据。 +2. 真机使用什么串口参数,telegram 的 framing、CRC、时间戳和 OBIS/M-Bus channel 有何特征。 +3. 实际能够读取哪些累计量,以及单位、精度、更新频率和累计语义。 +4. P1 值是否与热量表/生活热水表面板一致。 -Pre-M8 是 M8 的证据门:在真机字段和语义确认前,不决定数据库结构、Meter 数据源绑定、后台采集服务或前端布局。 +Pre-M8 是 M8 的证据门:它只交付真机事实和可重复 probe,不决定数据库结构、Meter 数据源 +绑定、后台采集服务或前端布局。 ## 2. 执行边界 本阶段只建立下面这条最短链路: ```text -WarmteLink P1 → USB serial → 完整 telegram → CRC 校验 → 字段解析 → 终端输出 +WarmteLink P1 → USB serial → 原始 telegram → 完整性状态 → 字段解析 → 终端输出 ``` 明确不做: @@ -28,99 +31,264 @@ WarmteLink P1 → USB serial → 完整 telegram → CRC 校验 → 字段解析 - 不发布 MQTT / Home Assistant Discovery。 - 不修改现有 DSMR Reader MQTT、电费计算或 Meter 逻辑。 - 不在本阶段决定 WarmteLink 应落在哪个正式 Device/Source 模型中。 +- 不写串口、不修改 FTDI EEPROM;probe 必须严格只读。 -## 3. 预期操作方式 +## 3. 2026-08-22 真机事实 -线到货后,在 workspace 的 virtual environment 中实现并运行一个只读 probe。命令形态暂定为: +### 3.1 USB、权限与线材 + +- 新 USB→P1 线是 FTDI FT232R,稳定路径形态为 + `/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_-if00-port0`;正式配置不得依赖 + `/dev/ttyUSB1`。 +- 运行用户加入 `dialout` 后可以直接读取 tty,无需 root,也不应把容器作为硬件 bring-up + 的中间层。 +- 已用另一根已知正常的 DSMR 线在电表上读到连续、CRC 正确的 DSMR telegram,证明宿主机 + 串口读取方法本身可用。 +- 曾临时清除新 FTDI 线 EEPROM 的 `INVERT_RXD` 做对照;输出发生变化但仍不可解析,随后已将 + EEPROM 逐字节恢复为原厂镜像并回读校验。正式方案不得依赖 EEPROM 修改。 + +### 3.2 串口参数矩阵 + +真机不是按最初假设的 `115200 8N1` 得到可读正文;当前线材与 WarmteLink 的实测最佳组合为: + +```text +115200 baud, 7 data bits, no parity, 1 stop bit(7N1) +``` + +| 参数 | 实测结果 | +| --- | --- | +| `115200 7N1` | 正文稳定可读,可枚举 9 个 OBIS 字段 | +| `115200 7N2` | 同样可读;没有理由增加停止位,正式默认仍用 `7N1` | +| `115200 8N1/8E1/8O1` | 乱码,无有效 OBIS/CRC | +| `115200 7E1/7O1` | 乱码,无有效 OBIS/CRC | +| `120000–3000000`,分别用 `8N1`、`7N1` | 无完整报文;`120000` 仅残留少量可辨识文本 | +| XON/XOFF 开/关 | 不改变 `7N1` 的字段解析结果 | + +[DSMR 5.0.2 P1 Companion Standard](https://www.netbeheernederland.nl/sites/default/files/2024-02/dsmr_5.0.2_p1_companion_standard.pdf) +规定 `115200 8N1`。因此 `7N1` 是当前设备/线材组合的实测事实,不应被文档或代码包装成 +标准 DSMR framing;CLI 必须允许显式覆盖数据位、校验位和停止位。 + +### 3.3 Framing 与 CRC 异常 + +- 连续采集的 telegram 周期约为 10 秒;一次 8 帧盘点中每帧均为 256 字节。 +- 正文结构稳定,版本、时间戳、两个 M-Bus channel、单位和值均可重复解析。 +- 实测头部为 `)TU)2NWA-MYRSKY`,偶见 `)TU{2NWA-MYRSKY`;没有标准要求的 `/` 起始符。 +- 帧尾存在 `!`,但其后的字符并非每帧都稳定为四位十六进制;即使恰好是四位,也无法从 + 缺失的 `/` 起点完成标准 DSMR CRC16 验证。 + +因此正式 probe 必须把完整性明确表示为 `valid`、`invalid` 或 `unverifiable`,保留原始字节并 +输出原因。它可以在 `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` | `` | WarmteLink/gateway equipment identifier | +| `0-1:24.1.0` | `(006)` | channel 1,M-Bus device type `0x06`,生活热水 | +| `0-1:96.1.0` | `` | channel 1 equipment identifier;样本制造商可解码为 `KAM` | +| `0-1:24.2.1` | `()(5.900*m3)` | 生活热水累计体积,输出到 `0.001 m³` 小数位 | +| `0-2:24.1.0` | `(012)` | channel 2,M-Bus device type `0x0C`,热量表 | +| `0-2:96.1.0` | `` | channel 2 equipment identifier;样本制造商可解码为 `KAM` | +| `0-2:24.2.1` | `()(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](https://oms-group.org/wp-content/uploads/2024/05/OMS-Spec_Vol2_AnnexA_F121.pdf)。 + +### 3.5 对 M8 已经成立的事实 + +- 一个 WarmteLink serial source 同时暴露两个独立累计 measurement channel。 +- channel 1 是生活热水 `m³`;channel 2 是区域供暖 `GJ`,不能按论坛样例固定 channel。 +- 两个累计量都与物理表一致,可进入 M8 的 Meter/source 映射讨论。 +- 当前没有瞬时流量、温度或热功率;M8 只承诺累计量采集与展示。 +- 串口 framing 和 CRC 异常尚未消失,必须作为正式 ingestion 的显式质量状态处理。 + +## 4. 正式 probe 的预期操作方式 + +实现后,在 workspace virtual environment 中运行只读 probe: ```bash source .venv/bin/activate python -m scripts.p1_probe \ --device /dev/serial/by-id/ \ + --baudrate 115200 \ + --bytesize 7 \ + --parity N \ + --stopbits 1 \ --duration 600 \ --show-changes \ - --raw-output /tmp/warmtelink-p1-telegram.txt + --raw-output /tmp/warmtelink-p1-telegram.bin ``` -最终参数名可在实现 probe 时调整,但应保留这些能力: +参数名可在实现时小幅调整,但必须保留这些能力: -- 使用稳定的 `/dev/serial/by-id/...` 路径,而不是依赖可能变化的 `/dev/ttyUSB0`。 -- 连续读取多帧,而不是只看一帧偶然样本。 -- 同时显示完整帧/CRC 结果、原始 OBIS 字段和解析后的值/单位。 -- 枚举 telegram 中出现的所有 M-Bus channel、device type、equipment id、capture timestamp、value 和 unit,不依赖固定字段顺序。 -- 可只显示发生变化的字段,便于观察更新频率。 -- 原始 telegram 默认只写到 `/tmp`;未经脱敏不提交到 Git。 +- 设备路径由用户显式传入,文档推荐 `/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 常驻运行。 -### 3.1 分两步 bring-up:Bash 冒烟验证 → Python probe +Home Assistant Community 的 +[最小 WarmteLink Bash 读取方法](https://community.home-assistant.io/t/solved-dsmr-add-warmtelink-as-data-source/485255/2) +仍可作为快速可见性检查,但论坛样例的 header、channel 和 device type 与本机均不同,不能作为 +parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_parser) 也由 T01 的实测 +兼容性决定;若它要求标准 `/...!CRC` framing,则应保留小型专用 parser,而不是绕过其校验。 -Home Assistant Community 的一份 WarmteLink 实例提供了一个适合作为硬件 -bring-up 起点的[最小 Bash 读取方法](https://community.home-assistant.io/t/solved-dsmr-add-warmtelink-as-data-source/485255/2): -先把串口设为 115200 baud,逐行读取设备,并从带 `GJ` 的行中取出累计值。该帖展示的 -telegram 样例还给出了以下**候选事实**: +## 5. 实现任务 -- 设备头为 `/NWA-WARMTELINK`,版本字段为 `1-3:0.2.8(50)`。 -- M-Bus channel 1 的 device type 样例为 `004`。 -- 累计热量样例位于 `0-1:24.2.1()(*GJ)`。 -- telegram 以 `!` 加四位 CRC 结束。 +### PRE-M8-T01 — 纯函数 telegram framing、CRC 与 OBIS parser -这些是其他用户在 2022 年记录的单机样本,只用于提出假设,不能替代本机 firmware、线材和 -实际 telegram 的验证。当前 Home Assistant 的 -[DSMR 文档](https://www.home-assistant.io/integrations/dsmr/)确认其 DSMR 集成支持 DSMR v5 与 -M-Bus subdevice;该集成底层使用 -[`dsmr_parser`](https://github.com/ndokter/dsmr_parser)。实现 probe 时可把它作为候选解析基线 -进行对照,但是否引入为本项目正式依赖留到 M8 Planning 决定。 +- **Status**: `todo` +- **Depends**: `none` +- **Context**: 先把串口 I/O 与解析分开,用脱敏 fixture 固定标准 DSMR 帧和本机异常帧行为。 -线到货后的执行顺序调整为: +**Files** -1. **Bash 冒烟验证**:用稳定的 `/dev/serial/by-id/...` 路径配置串口并短时读取;先保留完整 - 原始字节流,再确认是否能看到 `/NWA-WARMTELINK`、帧尾和带 `GJ` 的行。论坛脚本中的 - `GJ` 文本提取只能用作快速可见性检查,不能算解析或验收通过。 -2. **Python probe**:在已确认物理链路工作的前提下,实现上面的 `scripts.p1_probe`,完成 - 完整 framing、CRC、全部字段枚举、结构化解析、连续多帧变化观察和人工面板对照。 +- `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` -不复制论坛脚本的 MQTT 发布步骤:Pre-M8 仍只输出到终端和 `/tmp`,MQTT / Home Assistant -集成属于 M8 设计范围。 +**Steps** -## 4. 人工对照 +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 + 写单元测试。 -probe 运行期间,人工从热力表和相关水表面板记录同一时间附近的显示值,并与终端结果对照: +**Out of scope / 不要碰** -| 检查项 | 需要记录 | -| --- | --- | -| 区域供暖 | 面板累计值、P1 值、单位、两者时间差 | -| 生活热水 | 面板累计值、P1 是否存在对应字段、单位、两者时间差 | -| 更新时间 | 连续 telegram 中数值变化的间隔 | -| 累计语义 | 数值是否单调累计,是否出现每日归零或其它重置 | +- 不打开真实 serial device,不增加依赖,不写数据库/API/MQTT。 +- 不因本机正文可读而伪造 `/` header 或把 CRC 状态升级为 valid。 -允许 P1 capture time 与按表时间之间存在合理延迟;不能只凭数值接近就认定字段含义,必须同时核对单位、channel/device type 和时间戳。 +**Acceptance criteria** -## 5. 通过条件 +- [ ] 标准 fixture 的 frame boundary 与 CRC 可验证为 `valid`。 +- [ ] 脱敏本机 fixture 被标为 `unverifiable`,但能按字段而非位置解析 `m³` 和 `GJ` channel。 +- [ ] 任意 chunk boundary 和字段顺序不影响结果,未知字段原样保留。 +- [ ] 累计量以 `Decimal` + 原单位返回。 +- [ ] `pytest tests/test_p1_probe.py`、`pytest`、`ruff check .` 全绿。 -Pre-M8 完成需留下以下证据: +**Reviewer checklist** -- [ ] 连续收到可识别为 WarmteLink 的完整 telegram。 -- [ ] CRC 校验通过;若失败,已区分串口/线材问题与 parser 问题。 -- [ ] parser 不依赖字段固定顺序,并列出全部实际 channel/OBIS 字段。 -- [ ] 找到 GJ 累计值并与热力表面板对照,误差可由显示精度或 capture 延迟解释。 -- [ ] 明确实际 telegram 是否包含独立的生活热水 m³ 累计量;若包含,已与水表面板对照。 -- [ ] 记录数值精度、telegram 频率、字段更新频率和累计/重置行为。 -- [ ] 形成一份脱敏结果摘要,足以支持下一轮 M8 Planning。 +- CRC 覆盖范围必须严格从 `/` 到 `!`(包含二者),不得对缺失字节做猜测性修补。 +- fixture 必须脱敏且保留足以复现 framing 异常的字节结构。 +- parser 不得硬编码 channel 1=GJ 或 channel 2=m³。 -如果只能确认 GJ、没有独立生活热水 m³,这也是有效结论,不视为 Pre-M8 失败。 +### PRE-M8-T02 — 只读 serial probe CLI -## 6. 失败分类 +- **Status**: `todo` +- **Depends**: `PRE-M8-T01` +- **Context**: 在纯 parser 通过后增加最小 serial I/O,使真机验证可以从仓库稳定复现。 -- 完全无数据:优先检查 USB 识别、串口权限、P1 request line、线材方向/供电。 -- 输出乱码或不成帧:优先检查串口参数、信号反相和线材兼容性。 -- 原始帧完整但解析失败:保存脱敏样本,调整 parser/字段映射。 -- 解析成功但面板对不上:检查 capture timestamp、累计语义、单位和 WarmteLink firmware 差异。 +**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;终端清楚区分 `valid`、`invalid`、`unverifiable`。 +- [ ] permission/busy/disconnect 错误非零退出并给出可执行诊断,绝不建议常驻 root。 +- [ ] 依赖输入与两个生成 requirements 文件同步。 +- [ ] `pytest tests/test_p1_probe.py`、`pytest`、`ruff check .` 全绿。 + +**Reviewer checklist** + +- 确认所有 serial write path 均不存在。 +- 确认测试完全 mock 硬件、没有 CI timing flake,退出路径总会关闭文件描述符。 +- 确认 requirements 是生成结果而非仅手改 lock file。 + +### PRE-M8-T03 — 正式真机验收与 M8 交接 + +- **Status**: `todo` +- **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 GJ`、`5.900 m³` 的基线或运行时新值与物理表再次对照。 +- [ ] 明确没有从当前 P1 输出读取到瞬时流量、功率或温度。 +- [ ] Pre-M8 状态与 roadmap/M8 入口同步;代码闸门保持全绿。 + +**Reviewer checklist** + +- 证据必须来自正式 CLI,不得只复述本轮临时脚本结果。 +- 任何 equipment id、FTDI serial 和未脱敏 raw capture 都不得进入 Git。 +- CRC/framing 风险必须原样交给 M8,不能用“数值看起来正确”替代完整性判断。 + +## 6. 当前通过条件 + +- [x] USB、tty 权限和稳定 `/dev/serial/by-id/...` 路径已验证。 +- [x] 串口参数矩阵已完成,实测正文可读参数为 `115200 7N1`。 +- [x] 已枚举全部实际 channel/OBIS 字段,并确认没有轮换出现的额外测量量。 +- [x] GJ 和生活热水 m³ 均与物理表面板完全一致。 +- [x] 已记录精度、约 10 秒 telegram cadence 和当前字段集合。 +- [x] framing/CRC 失败已保留为显式异常,没有误报为校验通过。 +- [ ] 仓库内 parser、fixtures、probe CLI 和自动化测试完成。 +- [ ] 正式 probe 完成至少 10 分钟真机复验并产出脱敏摘要。 + +当前人工 bring-up 已足以锁定 M8 的两个累计量目标,但 Pre-M8 milestone 仍需完成 T01~T03。 ## 7. 向 M8 的交付物 -Pre-M8 只向 M8 交付事实,不交付正式架构: +Pre-M8 完成后只向 M8 交付事实,不交付正式架构: -- 已脱敏的 telegram 结构与字段清单。 -- GJ / 可选 m³ 的实际 channel、OBIS、单位、精度和更新时间。 -- 串口参数、稳定设备路径与部署权限要求。 -- parser 适配结论以及需要保留的异常样本。 -- 对“一个来源包含几个可用计量通道”的实测结论。 +- 脱敏 telegram 结构、fixture 和全部字段清单。 +- GJ 与 m³ 的实际 channel、device type、单位、精度和更新时间。 +- 串口参数、稳定设备路径形态与 `dialout` 权限要求。 +- parser 适配结论和 `unverifiable` framing/CRC 异常样本。 +- “一个 serial source 包含两个独立累计计量 channel”的实测结论。 +- 当前 P1 不提供瞬时流量、热功率或温度的明确边界。 diff --git a/docs/roadmap.md b/docs/roadmap.md index 251200d..320bec2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,7 +2,7 @@ 本文档记录 `home-automation` 在 `v1.0.3` 之后的下一阶段规划。这一阶段不是小修补,而是几次较大的结构性改动:单库化、前端重写、以及远期的移动端试水。 -> 每个里程碑的设计与**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)、M4 [`m4-login-hardening.md`](./design/m4-login-hardening.md)、M5 [`m5-iot-energy.md`](./design/m5-iot-energy.md)、M6 [`m6-tibber-dynamic-energy.md`](./design/m6-tibber-dynamic-energy.md)、M7 [`m7-meter-epochs-archival.md`](./design/m7-meter-epochs-archival.md)、Pre-M8 [`pre-m8-warmtelink-p1-poc.md`](./design/pre-m8-warmtelink-p1-poc.md)、M8 [`m8-warmtelink-energy.md`](./design/m8-warmtelink-energy.md)。Pre-M8/M8 当前仍是验证与 Planning 占位,尚无可派发的实现任务卡。 +> 每个里程碑的设计与**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)、M4 [`m4-login-hardening.md`](./design/m4-login-hardening.md)、M5 [`m5-iot-energy.md`](./design/m5-iot-energy.md)、M6 [`m6-tibber-dynamic-energy.md`](./design/m6-tibber-dynamic-energy.md)、M7 [`m7-meter-epochs-archival.md`](./design/m7-meter-epochs-archival.md)、Pre-M8 [`pre-m8-warmtelink-p1-poc.md`](./design/pre-m8-warmtelink-p1-poc.md)、M8 [`m8-warmtelink-energy.md`](./design/m8-warmtelink-energy.md)。Pre-M8 已有可执行任务卡;M8 仍是 Planning 占位。 ## 当前基线(v1.0.3) @@ -40,8 +40,8 @@ | **M5** ✅ | IoT / 能耗采集 | 通用 Modbus 采集(YAML profile + JSON readings)+ MQTT/HA Discovery + 前端侧边栏 + Energy 视图 | | **M6** ✅ | 通用电价层 + DSMR 接入 + 实时电费计算 | 通用电价层(manual/tibber profile + 合同版本)+ DSMR 实时电表接入 + 每 15min 寄存器差×价计量电费(不可变快照)+ 日/月/年汇总 + 反哺 HA Energy + 前端合同/价格/费用视图 | | **M7** ✅ | 电表生命周期 / 换表归档 | 引入 Meter epoch,计费永不跨表算 delta,跨表/无表/异常 delta 一律降级,累计按当前表归零,追溯换表可重算,Meter CRUD API + 前端管理 UI | -| **Pre-M8** ⏳ | WarmteLink P1 真机概念验证 | USB→P1 到货后用 workspace venv 只读采集实际 telegram,校验 CRC/解析,并与热力表和水表面板对照 | -| **M8** 📝 | WarmteLink P1 与多数据源 Meter | 等 Pre-M8 后规划直接 P1 采集、区域供暖读数及 Meter 与数据源的可配置关系;当前仅占位 | +| **Pre-M8** 🛠️ | WarmteLink P1 真机概念验证 | 人工 bring-up 已确认 `0.017 GJ` 与 `5.900 m³` 两个累计量;待实现正式只读 probe、解析测试和 10 分钟复验 | +| **M8** 📝 | WarmteLink P1 与多数据源 Meter | 等 Pre-M8 正式 probe 完成后,规划一个 serial source 的两个累计 channel 与 Meter 的可配置关系 | | **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 | 排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上;M4 是公网安全加固,在 M5 IoT 集成之前先堵住裸密码这个洞;M5 在安全基座就绪后再做 IoT 接入。 @@ -259,11 +259,16 @@ httpx / paho-mqtt / pyyaml / apscheduler 均为 M5 已有依赖,M6 复用, --- -## Pre-M8 — WarmteLink P1 真机概念验证(⏳ 等待硬件) +## Pre-M8 — WarmteLink P1 真机概念验证(🛠️ bring-up 完成,probe 待实现) ### 目标 -USB→P1 线到货后,先在 workspace virtual environment 中运行只读 probe,直接采集 Vattenfall WarmteLink 的实际 telegram。验证完整帧、CRC、OBIS/M-Bus channel、单位、精度和更新频率,并把解析出的 GJ 与热力表面板、可选 m³ 与水表面板进行人工对照。 +2026-08-22 的临时真机脚本已确认当前链路需要 `115200 7N1` 才能稳定解析正文;一个 +WarmteLink source 暴露 channel 1 的生活热水累计量 `5.900 m³` 和 channel 2 的区域供暖 +累计量 `0.017 GJ`,两者均与物理表完全一致。连续 8 帧没有流量、热功率或温度字段。 + +帧头缺少标准 `/`,CRC 因而不可验证;这项异常必须由正式 probe 原样报告,不能伪装成校验 +通过。下一步按 PRE-M8-T01~T03 实现纯 parser、只读 serial CLI,并完成至少 10 分钟真机复验。 本阶段不落库、不接 API/前端/HA,也不决定正式 Device/Source/Meter 关系。它只向 M8 提供脱敏的真机事实,避免在未知 firmware/字段语义上提前设计。 @@ -275,7 +280,8 @@ USB→P1 线到货后,先在 workspace virtual environment 中运行只读 pro ### 候选目标 -在 Pre-M8 事实基础上,把 WarmteLink P1 的区域供暖 GJ、以及真机确认存在时的生活热水 m³ 接入 Energy 模块,并讨论 Meter 如何与实际数据源建立可配置关系。 +在 Pre-M8 事实基础上,把 WarmteLink P1 已确认存在的区域供暖 GJ 和生活热水 m³ 接入 +Energy 模块,并讨论 Meter 如何与实际数据源建立可配置关系。 当前不锁定数据库、API、后台 worker 或 UI 结构;特别是 DSMR MQTT source、P1 serial source、Device/Data Source 的定义和多 channel 映射,都留到下一轮 Planning 讨论后再拆原子任务。