PRE-M8-T03: finalize WarmteLink hardware validation
frontend / frontend (push) Successful in 30s
pytest / test (push) Successful in 2m23s

This commit is contained in:
2026-08-22 19:29:54 +02:00
parent 2992bbb0ef
commit 8dbb59a3b7
4 changed files with 109 additions and 51 deletions
+2 -2
View File
@@ -9,8 +9,8 @@
- [`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 真机概念验证(bring-up 已完成正式 probe 待实现
- [`m8-warmtelink-energy.md`](./m8-warmtelink-energy.md) — WarmteLink P1 与多数据源 MeterPlanning 占位
- [`pre-m8-warmtelink-p1-poc.md`](./pre-m8-warmtelink-p1-poc.md) — WarmteLink P1 真机概念验证(已完成正式 CLI 长测与供暖变化均经物理表复核
- [`m8-warmtelink-energy.md`](./m8-warmtelink-energy.md) — WarmteLink P1 与多数据源 MeterPlanning 已解锁;架构仍开放
本文件定义**所有任务共用的格式与协作规则**,各个里程碑文档不再重复这些约定。
+23 -12
View File
@@ -1,8 +1,8 @@
# M8 — WarmteLink P1 与多数据源 MeterPlanning 占位)
> **状态:Planning 占位;尚未拆解任务卡,尚未锁定架构。** Pre-M8 人工 bring-up 已确认
> 两个累计量,但 M8 仍须等待 [Pre-M8 真机概念验证](./pre-m8-warmtelink-p1-poc.md)
> 的正式 probe、测试和长时间复验完成后再进入正式设计
> **状态:Planning 已解锁;尚未拆解任务卡,尚未锁定架构。** [Pre-M8 真机概念验证](./pre-m8-warmtelink-p1-poc.md)
> 已完成正式 probe、测试和 10 分钟复验;这只满足进入
> Planning 的证据门,不授权 schema、API、worker 或前端实现
## 1. 候选目标
@@ -20,11 +20,15 @@
- 当前 electricity Meter 与 `dsmr_reading` 之间没有显式 source FK/binding;电费计算通过代码约定直接查询 DSMR 电力寄存器。
- `Meter.commodity` 后端已为 `heating` 等品类预留,但“增加 commodity”本身不会自动获得相应数据源或解析能力。
- 当前 Devices UI/模型是 Modbus 专用,不能直接假设 WarmteLink 应复用 `modbus_device`
- 真机实测一个 WarmteLink serial source 同时输出两个累计 channelchannel 1 为生活热水
`m³`channel 2 为区域供暖 `GJ`;两者均已与物理表对照一致。
- 当前 P1 telegram 约每 10 秒一帧,没有瞬时流量、热功率、供水温度或回水温度字段。
- 当前线材/设备组合以 `115200 7N1` 才能稳定解析正文;header/CRC 仍不可验证,正式 ingestion
必须显式处理该数据质量状态
- 正式 CLI 复验中,一个 WarmteLink serial source 在 60/60 帧均输出两个累计 channelchannel 1
为生活热水 `m³`device type `006``5.900 m³`channel 2 为区域供暖 `GJ`device type
`012`、基线 `0.017 GJ`);同日与物理表复核一致。最终人工走查开启供暖后,channel 2 又从
`0.017 GJ` 增至 `0.018 GJ`,物理热量表同步显示 `0.018 GJ`
- 设备 timestamp 严格每 10 秒递进;frame 平均 256 bytes、范围 237–275,不能假定固定帧长
当前 P1 telegram 没有瞬时流量、热功率、供水温度或回水温度字段。
- 当前线材/设备组合以 `115200 7N1` 才能稳定解析正文。60/60 frame 缺少标准 `/`,故均为
`unverifiable`;即使部分 footer 恰为四位十六进制也不能验证 CRC。正式 ingestion 必须显式
处理该数据质量状态,不能将数值与表盘相符视为 CRC valid。
## 3. 下一轮 Planning 必须讨论的问题
@@ -41,11 +45,18 @@
## 4. Planning 入口条件
正式编写 M8 目标架构、数据模型和原子任务卡前,至少需要
进入 M8 Planning 的证据门已满足
- [x] Pre-M8 通过并留下脱敏字段清单。
- [x] 正式 probe 复验两个累计量、单位、channel、device type 与更新时间;equipment identifier
保持脱敏。
- [x] 记录 parser 适配结论和异常 framing/CRC 状态;10 分钟样本的稳定累计值不构成消费更新、
reset/wrap 或长期稳定性的证明。
- [x] 最终人工走查确认供暖消费时累计量按 `0.001 GJ` 更新并与物理表一致;精确更新延迟、
reset/wrap 和长期接纳策略仍留给 Planning。
正式编写 M8 目标架构、数据模型和原子任务卡前,仍需:
- Pre-M8 通过并留下脱敏字段清单。
- 用正式 probe 复验两个累计量及其单位、equipment id/channel 和更新时间。
- 确认原始 telegram 的长期稳定性、parser 适配方式和异常 CRC 的接纳策略。
- 重新走查现有 DSMR ingest、Meter epoch、Modbus device、expose/HA 和 Energy 前端边界。
- 与用户讨论并锁定 Meter ↔ source 的配置体验后,再决定 migration/API/UI 方案。
+65 -26
View File
@@ -1,8 +1,9 @@
# Pre-M8 — WarmteLink P1 真机概念验证
> **状态:进行中。** 2026-08-22 已完成临时脚本真机 bring-up,并确认两个可用累计量;
> 仓库内的正式 probe、解析测试和可重复验收流程尚未实现。完成下方 PRE-M8-T01T03
> 后,才把 Pre-M8 标记为完成并解除 M8 Planning 的入口限制。
> **状态:已完成。** 2026-08-22 已完成临时脚本真机 bring-up;随后仓库内正式 probe 以
> `115200 7N1` 连续运行 10 分钟,形成脱敏的可重复验收证据;最终人工走查开启供暖后又观察到
> 区域供暖累计量从 `0.017 GJ` 增至 `0.018 GJ`,并与物理表一致。Pre-M8 现已解除
> **M8 Planning** 的入口限制;M8 的架构和实现范围仍未锁定。
## 1. 目的
@@ -70,7 +71,8 @@ WarmteLink P1 → USB serial → 原始 telegram → 完整性状态 → 字段
### 3.3 Framing 与 CRC 异常
- 连续采集的 telegram 周期约为 10 秒;一次 8 帧盘点中每帧均为 256 字节。
- 临时 bring-up 的一次 8 帧盘点中每帧均为 256 字节;正式 10 分钟采样显示长度并不固定,
详见 §3.6。
- 正文结构稳定,版本、时间戳、两个 M-Bus channel、单位和值均可重复解析。
- 实测头部为 `)TU)2NWA-MYRSKY`,偶见 `)TU{2NWA-MYRSKY`;没有标准要求的 `/` 起始符。
- 帧尾存在 `!`,但其后的字符并非每帧都稳定为四位十六进制;即使恰好是四位,也无法从
@@ -111,6 +113,42 @@ M8 若要持久化这些读数,必须在 Planning 中单独锁定异常帧的
- 当前没有瞬时流量、温度或热功率;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:10``18: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 code`1-3:0.2.8``0-0:1.0.0``0-0:96.1.1`
`0-1:24.1.0``0-1:96.1.0``0-1:24.2.1``0-2:24.1.0``0-2:96.1.0``0-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
@@ -149,7 +187,7 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
### PRE-M8-T01 — 纯函数 telegram framing、CRC 与 OBIS parser
- **Status**: `todo`
- **Status**: `done`
- **Depends**: `none`
- **Context**: 先把串口 I/O 与解析分开,用脱敏 fixture 固定标准 DSMR 帧和本机异常帧行为。
@@ -176,11 +214,11 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
**Acceptance criteria**
- [ ] 标准 fixture 的 frame boundary 与 CRC 可验证为 `valid`
- [ ] 脱敏本机 fixture 被标为 `unverifiable`,但能按字段而非位置解析 `m³``GJ` channel。
- [ ] 任意 chunk boundary 和字段顺序不影响结果,未知字段原样保留。
- [ ] 累计量以 `Decimal` + 原单位返回。
- [ ] `pytest tests/test_p1_probe.py``pytest``ruff check .` 全绿。
- [x] 标准 fixture 的 frame boundary 与 CRC 可验证为 `valid`
- [x] 脱敏本机 fixture 被标为 `unverifiable`,但能按字段而非位置解析 `m³``GJ` channel。
- [x] 任意 chunk boundary 和字段顺序不影响结果,未知字段原样保留。
- [x] 累计量以 `Decimal` + 原单位返回。
- [x] `pytest tests/test_p1_probe.py``pytest``ruff check .` 全绿。
**Reviewer checklist**
@@ -190,7 +228,7 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
### PRE-M8-T02 — 只读 serial probe CLI
- **Status**: `todo`
- **Status**: `done`
- **Depends**: `PRE-M8-T01`
- **Context**: 在纯 parser 通过后增加最小 serial I/O,使真机验证可以从仓库稳定复现。
@@ -217,12 +255,12 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
**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 .` 全绿。
- [x] CLI 可用 `/dev/serial/by-id/...` 读取,且所有 framing 参数都可显式覆盖。
- [x] 默认参数准确反映本机 `115200 7N1`,帮助文本说明它是实测值而非 DSMR 标准默认。
- [x] raw output 保留原始 bytes;终端清楚区分 `valid``invalid``unverifiable`
- [x] permission/busy/disconnect 错误非零退出并给出可执行诊断,绝不建议常驻 root。
- [x] 依赖输入与两个生成 requirements 文件同步。
- [x] `pytest tests/test_p1_probe.py``pytest``ruff check .` 全绿。
**Reviewer checklist**
@@ -232,7 +270,7 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
### PRE-M8-T03 — 正式真机验收与 M8 交接
- **Status**: `todo`
- **Status**: `done`
- **Depends**: `PRE-M8-T02`
- **Context**: 用仓库内 probe 替代本轮临时脚本,形成可重复、脱敏且能支撑 M8 Planning 的证据。
@@ -257,11 +295,11 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
**Acceptance criteria**
- [ ] 仓库内 probe 在真机连续运行至少 10 分钟,无未处理异常退出。
- [ ] 脱敏摘要列出两个累计 channel、单位、精度、cadence 和完整性异常。
- [ ] `0.017 GJ``5.900 m³` 的基线或运行时新值与物理表再次对照。
- [ ] 明确没有从当前 P1 输出读取到瞬时流量、功率或温度。
- [ ] Pre-M8 状态与 roadmap/M8 入口同步;代码闸门保持全绿。
- [x] 仓库内 probe 在真机连续运行至少 10 分钟,无未处理异常退出。
- [x] 脱敏摘要列出两个累计 channel、单位、精度、cadence 和完整性异常。
- [x] `0.017 GJ``5.900 m³` 的基线或运行时新值与物理表再次对照。
- [x] 明确没有从当前 P1 输出读取到瞬时流量、功率或温度。
- [x] Pre-M8 状态与 roadmap/M8 入口同步;代码闸门保持全绿。
**Reviewer checklist**
@@ -277,10 +315,11 @@ parser 契约。是否引入 [`dsmr_parser`](https://github.com/ndokter/dsmr_par
- [x] GJ 和生活热水 m³ 均与物理表面板完全一致。
- [x] 已记录精度、约 10 秒 telegram cadence 和当前字段集合。
- [x] framing/CRC 失败已保留为显式异常,没有误报为校验通过。
- [ ] 仓库内 parser、fixtures、probe CLI 和自动化测试完成。
- [ ] 正式 probe 完成至少 10 分钟真机复验并产出脱敏摘要。
- [x] 仓库内 parser、fixtures、probe CLI 和自动化测试完成。
- [x] 正式 probe 完成至少 10 分钟真机复验并产出脱敏摘要。
- [x] 最终人工走查在供暖开启后观察到 `0.017 → 0.018 GJ`,并再次与物理表核对一致。
当前人工 bring-up 已足以锁定 M8 的两个累计量目标,但 Pre-M8 milestone 仍需完成 T01T03
Pre-M8 已完成并向 M8 解锁 Planning;它只交付下列真机事实,不锁定正式架构或实现任务
## 7. 向 M8 的交付物
+19 -11
View File
@@ -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 真机概念验证 | 人工 bring-up 已确认 `0.017 GJ``5.900 m³` 两个累计量;待实现正式只读 probe、解析测试和 10 分钟复验 |
| **M8** 📝 | WarmteLink P1 与多数据源 Meter | Pre-M8 正式 probe 完成后,规划一个 serial source 的两个累计 channel 与 Meter 的可配置关系 |
| **Pre-M8** | WarmteLink P1 真机概念验证 | 正式只读 CLI 长测通过;人工开启供暖后累计量 `0.017 → 0.018 GJ` 且与物理表一致,所有 frame 的 CRC 状态仍为 `unverifiable` |
| **M8** 📝 | WarmteLink P1 与多数据源 Meter | Pre-M8 已解锁 Planning;再讨论一个 serial source 的两个累计 channel 与 Meter 的可配置关系,尚未锁定架构或实施 |
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上;M4 是公网安全加固,在 M5 IoT 集成之前先堵住裸密码这个洞;M5 在安全基座就绪后再做 IoT 接入。
@@ -259,16 +259,22 @@ httpx / paho-mqtt / pyyaml / apscheduler 均为 M5 已有依赖,M6 复用,
---
## Pre-M8 — WarmteLink P1 真机概念验证(🛠️ bring-up 完成,probe 待实现
## Pre-M8 — WarmteLink P1 真机概念验证(✅ 已完成
### 目标
2026-08-22 的临时真机脚本已确认当前链路需要 `115200 7N1` 才能稳定解析正文;一个
WarmteLink source 暴露 channel 1 的生活热水累计量 `5.900 m³` 和 channel 2 的区域供暖
累计量 `0.017 GJ`,两者均与物理表完全一致。连续 8 帧没有流量、热功率或温度字段。
2026-08-22 的正式仓库 CLI 以 `115200 7N1` 连续运行 10 分钟,正常退出且没有 I/O error、未处理
异常或断连。一个 WarmteLink source 在 60/60 完整 frame 中暴露 channel 1 的生活热水累计量
`5.900 m³` 和 channel 2 的区域供暖累计量 `0.017 GJ`;二者再次与同日物理表一致。没有流量、
热功率或温度字段。
帧头缺少标准 `/`CRC 因而不可验证;这项异常必须由正式 probe 原样报告,不能伪装成校验
通过。下一步按 PRE-M8-T01T03 实现纯 parser、只读 serial CLI,并完成至少 10 分钟真机复验
所有完整 frame 都缺少标准 `/`CRC 因而`unverifiable`;这项异常由正式 probe 原样报告,
没有伪装成校验通过。frame 平均 256 bytes、范围 237275,设备 timestamp 严格每 10 秒递进
10 分钟内累计值无变化只能证明稳定基线,不能证明消费时更新频率或 reset/wrap 行为。
最终人工走查又在终端连续运行 probe 十几分钟并开启供暖;区域供暖累计量从 `0.017 GJ` 增至
`0.018 GJ`,物理热量表同步显示 `0.018 GJ`。这确认了实际消费时的累计递增与 `0.001 GJ`
精度,但不改变 CRC `unverifiable` 结论,也不证明精确更新延迟或 reset/wrap 行为。
本阶段不落库、不接 API/前端/HA,也不决定正式 Device/Source/Meter 关系。它只向 M8 提供脱敏的真机事实,避免在未知 firmware/字段语义上提前设计。
@@ -276,14 +282,16 @@ WarmteLink source 暴露 channel 1 的生活热水累计量 `5.900 m³` 和 chan
---
## M8 — WarmteLink P1 与多数据源 Meter(📝 Planning 占位
## M8 — WarmteLink P1 与多数据源 Meter(📝 Planning 已解锁
### 候选目标
在 Pre-M8 事实基础上,把 WarmteLink P1 已确认存在的区域供暖 GJ 和生活热水 m³ 接入
Energy 模块,并讨论 Meter 如何与实际数据源建立可配置关系。
当前不锁定数据库、API、后台 worker 或 UI 结构;特别是 DSMR MQTT source、P1 serial source、Device/Data Source 的定义和多 channel 映射,都留到下一轮 Planning 讨论后再拆原子任务。
Pre-M8 的证据门已满足,但当前不锁定数据库、API、后台 worker 或 UI 结构;特别是 DSMR MQTT
source、P1 serial source、Device/Data Source 的定义和多 channel 映射,都留到下一轮 Planning 讨论
并与用户锁定体验后再拆原子任务。
> Planning 占位与待决问题:[`docs/design/m8-warmtelink-energy.md`](./design/m8-warmtelink-energy.md)