M8-T20: document and validate WarmteLink deployment
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
- **通用电价合同层**:YAML profile 定合同结构(manual 固定/双费率 / tibber 动态电价);`EnergyContract`+`EnergyContractVersion` 存 UI 可填的数值,改价加新版本旧版本保留;price strategy 按 kind 出价
|
||||
- **实时买卖电费计算**:每 15 分钟按寄存器差值(`_1`=dal/低、`_2`=normal/高)× 买/卖价算计量电费,快照不可变;日/月/年汇总加固定费减 heffingskorting
|
||||
- **反哺 Home Assistant Energy**:当前买/卖价 + 累计买电支出/卖电收入(`total_increasing`)发成 HA 实体,可直接挂 HA Energy 仪表盘
|
||||
- **多数据源 Meter 与 WarmteLink**:DSMR MQTT 与只读 WarmteLink P1 serial source 统一为 Source → Channel → Binding → Meter;WarmteLink 提供 heating `GJ` 与 hot-water `m³` 的 Decimal history、质量与重连
|
||||
- **热力合同与成本**:electricity / thermal scope 可各有一个 active 合同;热力按 15 分钟账本计算 variable、fixed 与 all-in 成本,并可按需暴露给 HA
|
||||
- pytest 测试与 OpenAPI 导出脚本
|
||||
- Docker / Compose 部署入口
|
||||
|
||||
@@ -44,6 +46,8 @@
|
||||
- 电价合同(`energy_contract` 表)与版本(`energy_contract_version` 表,values JSON)
|
||||
- Tibber 15 分钟电价缓存(`tibber_price` 表,不可变)
|
||||
- 每 15 分钟计量电费(`energy_cost_period` 表,快照价,不可变)
|
||||
- meter source、channel 与 binding(`meter_source`、`meter_source_channel`、`meter_source_binding`)
|
||||
- WarmteLink scalar 历史(`warmtelink_reading`)与热力 15 分钟成本账本(`meter_cost_period`)
|
||||
|
||||
配置层只保留一个数据库环境变量:
|
||||
|
||||
@@ -55,7 +59,7 @@
|
||||
python -m scripts.run_migrations
|
||||
```
|
||||
|
||||
该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(含全部表,包括 M5 新增的 `modbus_device`、`modbus_reading`、`exposed_entity_toggle`,以及 M6 新增的 `dsmr_reading`、`energy_contract`、`energy_contract_version`、`tibber_price`、`energy_cost_period`)。
|
||||
该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(包括 Modbus、DSMR、Source/Channel/Binding、WarmteLink、electricity/thermal 合同与成本账本)。
|
||||
|
||||
## 当前目录
|
||||
|
||||
@@ -63,7 +67,7 @@ python -m scripts.run_migrations
|
||||
|
||||
- `app/`: FastAPI 应用代码(包含 JSON API、业务服务、数据模型)
|
||||
- `frontend/`: React SPA 前端(Vite + React + TypeScript + Mantine)
|
||||
- `alembic_app/`: App DB 的 Alembic migration 环境(管理所有表,含 M5 新增的 `modbus_device`、`modbus_reading`、`exposed_entity_toggle`,以及 M6 新增的 `dsmr_reading`、`energy_contract`、`energy_contract_version`、`tibber_price`、`energy_cost_period`)
|
||||
- `alembic_app/`: App DB 的唯一 Alembic migration 环境(管理所有 app 表,包括 Modbus、DSMR、Meter source、WarmteLink、合同与成本账本)
|
||||
- `tests/`: pytest 测试
|
||||
- `docs/`: 当前系统说明文档
|
||||
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Optional WarmteLink serial overlay. Use only together with docker-compose.yml:
|
||||
# WARMTELINK_DEVICE_PATH=/dev/serial/by-id/<stable-by-id-name> \
|
||||
# WARMTELINK_SERIAL_GID=<host-serial-gid> \
|
||||
# docker compose -f docker-compose.yml -f docker-compose.warmtelink.yml up -d
|
||||
#
|
||||
# Keep the stable /dev/serial/by-id path on the host: /dev/ttyUSB* names may
|
||||
# change after a reboot. pyserial opens POSIX serial devices with O_RDWR, so
|
||||
# Docker must grant the device cgroup rule rw (never m). This is only the
|
||||
# system permission required to open the fd: the WarmteLink worker itself only
|
||||
# calls read/close and has no write path. The base compose non-root user stays
|
||||
# in effect; this overlay grants neither root nor privileged mode.
|
||||
services:
|
||||
app:
|
||||
devices:
|
||||
- "${WARMTELINK_DEVICE_PATH:?Set a stable /dev/serial/by-id path}:/dev/warmtelink:rw"
|
||||
group_add:
|
||||
- "${WARMTELINK_SERIAL_GID:?Set the host serial device GID}"
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
- `main.py`
|
||||
- FastAPI app factory
|
||||
- lifespan(APScheduler 启停、MQTT 客户端起停、连接后触发 HA Discovery 发布;M6 新增 `tibber-refresh` 抓价 job + `energy-cost` 1 分钟计费 tick job;M6 启用时注册 DSMR MQTT 订阅)
|
||||
- lifespan(APScheduler 启停、MQTT 客户端起停、连接后触发 HA Discovery 发布;注册 DSMR MQTT source,并启动/关闭每个 enabled WarmteLink 业务只读 serial worker;`tibber-refresh`、electricity 与 thermal cost tick 均由 scheduler 驱动)
|
||||
- 基础路由注册
|
||||
- `config.py`
|
||||
- 环境变量驱动的 settings(含 M5 新增的 MQTT/HA Discovery/Modbus 配置项;M6 新增 `dsmr_ingest_enabled`、`dsmr_mqtt_topic`、`dsmr_sample_interval_s`、`tibber_api_token`(secret)、`tibber_home_id`)
|
||||
@@ -29,22 +29,22 @@
|
||||
- 通用依赖注入
|
||||
- `api/`
|
||||
- HTTP routes
|
||||
- `api/routes/api/`:JSON API(`/api/*` 前缀),供 React SPA 调用:会话/鉴权、配置读写、数据查询、记录 CRUD、Modbus 设备 CRUD + readings + metrics + test(`/api/modbus/*`)、Expose 勾选 + 重发 discovery(`/api/expose`)、MQTT 测试连接(`/api/config/mqtt/test`)、M6 新增合同 CRUD + 版本(`/api/energy/contracts*`)、pricing profile 列表(`/api/energy/profiles`)、价格/费用/汇总/DSMR 最新/重算/Tibber 测试(`/api/energy/prices`、`/api/energy/costs`、`/api/energy/costs/summary`、`/api/energy/dsmr/latest`、`/api/energy/costs/recompute`、`/api/energy/tibber/test`)
|
||||
- `api/routes/api/`:JSON API(`/api/*` 前缀),供 React SPA 调用:会话/鉴权、配置读写、记录 CRUD、Modbus、Expose 与 MQTT 测试;Energy 包含 source profile/source/channel/history/discover/binding/Meter API、scope-aware contracts/prices/costs,以及兼容的 DSMR latest API
|
||||
- 裸 ingestion 端点:`GET /public-ip/check`、`POST /homeassistant/publish`、`POST /poo/record`、`GET /poo/latest`、TickTick OAuth 等
|
||||
- `models/`
|
||||
- SQLAlchemy models
|
||||
- 所有模型(auth / config / public_ip / location / poo / modbus / expose / energy)共用同一个 `Base`,均落在单一 `app.db` 中
|
||||
- 所有模型(auth / config / public_ip / location / poo / modbus / expose / energy / meter_source)共用同一个 `Base`,均落在单一 `app.db` 中
|
||||
- M5 新增:`ModbusDevice`(设备部署层)、`ModbusReading`(通用遥测,JSON payload)、`ExposedEntityToggle`(HA 实体暴露开关)
|
||||
- M6 新增:`DsmrReading`(整帧 DSMR telegram,10s 降采样)、`EnergyContract`(合同头,含 active 标记)、`EnergyContractVersion`(版本/时段,values JSON,只增不改)、`TibberPrice`(15 分钟价缓存,不可变)、`EnergyCostPeriod`(每 15 分钟计量电费,快照价,不可变)
|
||||
- Energy:`MeterSource` / `MeterSourceChannel` / `MeterSourceBinding` 将协议连接、稳定测量 channel 与 Meter epoch 分离;`DsmrReading`、`WarmteLinkReading` 分别保存 JSON 与 Decimal scalar 历史;electricity `EnergyCostPeriod` 绑定 source binding,thermal `MeterCostPeriod` 保存审计账本;合同按 electricity / thermal scope 共存
|
||||
- `schemas/`
|
||||
- Pydantic schemas(M5 新增 `modbus.py`、`expose.py`;M6 新增 `energy_contract.py`、`energy.py`)
|
||||
- Pydantic schemas(包括 `modbus.py`、`expose.py`、`energy_contract.py`、`energy.py`、`meter_source.py`)
|
||||
- `services/`
|
||||
- 业务服务层
|
||||
- 当前已迁入 config page 的 DB 持久化逻辑
|
||||
- 当前已迁入 public IPv4 检查、状态持久化与变化通知逻辑
|
||||
- 当前已迁入 SMTP 发信与测试发信逻辑
|
||||
- M5 新增:`modbus_poll.py`(采集 service,逐设备 poll + 落库 + 推 MQTT state)、`ha_discovery.py`(构建 HA Discovery payload、发布 retained config、发布 state)
|
||||
- M6 新增:`tibber_prices.py`(httpx GraphQL 抓 15 分钟价,upsert `tibber_price`,幂等;仅 active=tibber 且 token 存在时运行)、`dsmr_ingest.py`(MQTT handler,整帧 JSON blob + 10s 降采样落库,`source_id` 幂等)、`energy_cost.py`(计费引擎:每 15 分钟寄存器差 × strategy 出价 → `energy_cost_period` 不可变快照;汇总 Σnet + 固定费 − heffingskorting;重算显式 opt-in)
|
||||
- Energy:`dsmr_ingest.py` 按 source 入库;`warmtelink_ingest.py` 接纳连续确认的业务只读 P1 scalar,`warmtelink_worker.py` 管理 interruptible serial reconnect(pyserial 的 POSIX `O_RDWR` 打开由非 root、非 privileged、无 `m` 的 Docker `rw` device rule 支持;worker 只 read/close,绝不 write);`energy_cost.py` 与 `meter_cost.py` 分别计算 electricity/thermal 账本,均拒绝跨 Meter/binding 相减
|
||||
- `integrations/`
|
||||
- 外部系统适配层
|
||||
- Home Assistant outbound adapter(REST 通道,原有)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- [`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 真机概念验证(已完成;正式 CLI 长测与供暖变化均经物理表复核)
|
||||
- [`m8-warmtelink-energy.md`](./m8-warmtelink-energy.md) — WarmteLink P1、多数据源 Meter 与热力计费(Planning 已完成;M8-T01~M8-T20 待实现)
|
||||
- [`m8-warmtelink-energy.md`](./m8-warmtelink-energy.md) — WarmteLink P1、多数据源 Meter 与热力计费(M8-T01~T20 自动化技术验收已完成;交付后用户人工 walkthrough 待验收)
|
||||
|
||||
本文件定义**所有任务共用的格式与协作规则**,各个里程碑文档不再重复这些约定。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# M8 — WarmteLink P1、多数据源 Meter 与热力计费
|
||||
|
||||
> **状态:Planning 已完成,M8-T01~M8-T20 待实现。**
|
||||
> **状态:M8-T01~M8-T20 自动化技术验收已完成;交付后用户人工 walkthrough 待验收。**
|
||||
> [Pre-M8 真机概念验证](./pre-m8-warmtelink-p1-poc.md) 已完成并通过翻牌;本文锁定
|
||||
> M8 的目标架构、迁移顺序、外部契约和可由编排器逐张执行的原子任务。
|
||||
|
||||
@@ -18,7 +18,7 @@ M8 完成后,Energy 模块不再把“电表”“采集连接”和“协议
|
||||
两种热力累计量、15 分钟成本、每日固定费和汇总。
|
||||
- 在 Energy UI 中分别管理 Sources、Modbus Devices、Meters、合同、价格与成本,并按需把新的
|
||||
source/meter/cost 实体暴露给 Home Assistant。
|
||||
- 以非 root、只读串口方式部署;worker 能启动、热更新、断线重连和干净停止。
|
||||
- 以非 root 部署:Docker device rule 用 pyserial POSIX `O_RDWR` 所需的最小 `rw`(绝无 `m`),而 worker 业务只读;worker 能启动、热更新、断线重连和干净停止。
|
||||
|
||||
M8 不承诺 WarmteLink 当前 telegram 未提供的瞬时流量、热功率、供/回水温度,也不推断
|
||||
“生活热水消耗了多少 GJ”。不把 WarmteLink 塞进 `modbus_device`,不把所有协议读数强行泛化成
|
||||
@@ -246,7 +246,8 @@ M8 的 schema 变更沿现有单一 `alembic_app` 链按下表串行落地;不
|
||||
- `warmtelink_serial` config 保存路径和串口参数,不保存 telegram/equipment id;敏感字段采用现有
|
||||
secret mask 约定。
|
||||
- 新增可选 `docker-compose.warmtelink.yml` overlay,把宿主机稳定 `/dev/serial/by-id/...` 映射为
|
||||
容器内 `/dev/warmtelink`,通过宿主机 serial GID 授权,容器仍非 root。
|
||||
容器内 `/dev/warmtelink`,通过宿主机 serial GID 授权。pyserial 在 POSIX 以 `O_RDWR` 打开,故 device
|
||||
rule 的最小系统权限为 `rw`(不含 `m`);容器仍非 root/非 privileged,worker 业务只 read/close、绝不 write。
|
||||
- 默认 `docker-compose.yml` 在没有硬件时仍可启动,不强制声明不存在的 device。
|
||||
- source disable/delete/config change 必须停止旧 worker;shutdown 不留下线程或打开的 serial fd。
|
||||
|
||||
@@ -1069,9 +1070,9 @@ T01~T06 先把现有 DSMR 安全迁到统一 source/binding;T07~T11 再接
|
||||
|
||||
### M8-T20 — 部署 Overlay、运行文档与端到端收尾 [structural]
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Status**: `done`
|
||||
- **Depends**: M8-T19
|
||||
- **Context**: 最后一张卡把串口权限、操作 runbook、真实构建与完整链路变成可重复验收结果。
|
||||
- **Context**: 最后一张卡把串口权限、操作 runbook 与隔离的自动化技术验收变成可重复验收结果;真实硬件/HA 观察作为交付后由用户执行的人工 walkthrough,不是 agent 或 Reviewer 的技术 PASS 前置条件。
|
||||
|
||||
**Files**
|
||||
- `create docker-compose.warmtelink.yml`
|
||||
@@ -1090,15 +1091,20 @@ T01~T06 先把现有 DSMR 安全迁到统一 source/binding;T07~T11 再接
|
||||
`/dev/warmtelink`,以显式 serial GID/additional group 授权;服务保持非 root,默认 compose 无硬件
|
||||
也能启动。不要写入用户真实设备 id。
|
||||
2. 检查 Dockerfile/compose/build context 与新 Python/前端文件;deployment tests 覆盖默认 compose、
|
||||
overlay 合并、device path、非 root、COPY source 存在。
|
||||
overlay 合并、device path、pyserial `O_RDWR` 所需的 `rw`/无 `m`、非 root/非 privileged、migration
|
||||
无 device、COPY source 存在。
|
||||
3. 写运行文档:识别稳定 by-id、查 GID、启停 overlay、创建 source、discover/bind、质量含义、
|
||||
reconnect/权限排障、backup/migration、合同录入、HA toggle 与安全回滚。明确不删除旧 config/data。
|
||||
4. 在 `/tmp` 下构造的隔离合成历史数据库演练 Alembic 14→head 并对账;新空库迁移到 head。
|
||||
不挂载、复制或打开运行中 production 的数据库、容器或 volume。
|
||||
5. 跑全量后端/前端/OpenAPI/codegen 闸门和真实 `docker build`;启动迁移后的 app,确认关键路由
|
||||
不 500。
|
||||
6. 按 §12 人工 walkthrough 走完整链;通过后把 Roadmap/设计索引/M8 状态与 T01~T20 Status
|
||||
更新为完成。打 tag/push 仍需用户另行明确授权。
|
||||
5. 只用 mock/fake serial、合成 source/channel/binding/Meter/contract/cost/HA payload 和 `/tmp`
|
||||
合成数据库,跑全量后端/前端/OpenAPI/codegen 闸门;演练 rev14→head 与空库→head。构建镜像后,
|
||||
仅启动一个无 production volume/bind/device 的临时容器,使用容器内临时数据库迁移并确认关键路由不
|
||||
500;结束时删除该容器、临时 image tag、`/tmp` 数据库和测试凭据。
|
||||
6. 交付完整的 §12 九步人工 walkthrough 与可填写证据模板给用户;不得声称 agent 已执行真实串口、
|
||||
真实 HA/MQTT 或生产资源操作。独立 Reviewer 可在自动化技术验收与文档/报告完整时判 `PASS`;随后
|
||||
Orchestrator 才可把 Roadmap/设计索引/M8/T20 状态更新为完成并按仓库规则 autosquash。打 tag/push
|
||||
仍需用户另行明确授权。
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- 不把宿主真实 serial id、GID、equipment id、合同金额或数据库写进仓库。
|
||||
@@ -1106,15 +1112,16 @@ T01~T06 先把现有 DSMR 安全迁到统一 source/binding;T07~T11 再接
|
||||
- 不自动 push、force-push 或打 release tag。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] 默认 compose 无设备可启动;overlay 以非 root 只读访问稳定映射路径,部署测试覆盖。
|
||||
- [ ] 空库/历史副本迁移对账通过,DSMR 正常数字不变,WarmteLink/thermal 全链可运行。
|
||||
- [ ] 文档足够让另一位 operator 从备份、部署、配置走到 HA 与回滚,不含真实 secret/identifier。
|
||||
- [ ] `pytest`、`ruff check .`、OpenAPI/codegen、全部前端闸门和真实 `docker build` 全绿。
|
||||
- [ ] 人工 walkthrough 全部通过;Roadmap/M8 任务状态与现实一致。
|
||||
- [ ] **自动化技术验收**:默认 compose 与 synthetic overlay 的结构检查通过;overlay app 仍为非 root/非 privileged,device rule 为无 `m` 的 `rw`(仅满足 pyserial `O_RDWR` 打开),worker 业务只 read/close,migration 无 device。
|
||||
- [ ] **自动化技术验收**:mock/fake serial 覆盖采集、断线与只读边界;合成 source/channel/binding/Meter、thermal/electricity contract/cost 与 HA payload 覆盖完整身份链和成本/HA 行为,绝不连接真实 serial、HA 或 MQTT。
|
||||
- [ ] **自动化技术验收**:仅在 `/tmp` 合成历史副本演练 rev14→head 并对账 DSMR 正常数字、source/binding、Meter、contract 和 cost;空库→head 及重复运行通过,绝不打开、复制、挂载或修改 production DB/config/container/volume。
|
||||
- [ ] **自动化技术验收**:`pytest`、`ruff check .`、OpenAPI/codegen、全部前端闸门和 Docker build 全绿;临时 Docker 运行只使用无 volume/bind/device、无 production 挂载的容器及容器内临时数据库,关键路由不 500,结束后清理容器、image tag、`/tmp` 文件与测试凭据。
|
||||
- [ ] **交付后用户人工 walkthrough**:文档提供完整 §12 九步操作和脱敏证据模板,明确由用户在真实 serial/HA 的备份、可回滚部署上验收;agent/Reviewer 不伪称已执行,且该人工结果不是 Reviewer 技术 PASS 的前置条件。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 必须亲自检查 Dockerfile `COPY`、compose merge 后的 user/group/device,不接受只看 unit tests。
|
||||
- 抽查迁移对账和 rollback 文档,不允许任何自动化删除历史;独立跑完整闸门与 walkthrough。
|
||||
- 独立运行 fake serial、合成 source/channel/binding/Meter/contract/cost/HA payload、`/tmp` rev14→head 和空库→head 的技术验收;抽查迁移对账和 rollback 文档,不允许任何自动化删除历史或接触真实 serial/HA/MQTT/production 资源。
|
||||
- 独立运行完整闸门及无 volume/bind/device 的临时 Docker 容器,核对清理;文档与最终报告必须完整交付 §12 九步给用户且不伪称已执行。上述自动化和交付完整时,Reviewer 可判 `PASS`;真实人工结果由用户交付后验收。
|
||||
|
||||
## 11. 每张任务卡的校验矩阵
|
||||
|
||||
@@ -1148,13 +1155,22 @@ npm run build
|
||||
`tests/test_deployment.py::test_dockerfile_copy_sources_exist` 仍绿。Implementer/Reviewer 简报、独立重跑与
|
||||
fixup/autosquash 流程以 [`docs/design/README.md`](./README.md) 和仓库 `AGENTS.md` 为准。
|
||||
|
||||
## 12. M8 最终人工 Walkthrough
|
||||
M8-T20 的自动化技术验收进一步受以下隔离边界约束:只可使用 mock/fake serial、合成
|
||||
source/channel/binding/Meter/contract/cost/HA payload、pytest `tmp_path` 或 `/tmp` 明确命名的合成
|
||||
SQLite;Docker 只可运行无 volume/bind/device、无生产挂载的临时容器。不得打开真实 serial、连接真实
|
||||
HA/MQTT,或访问 production DB/config/container/volume。真实环境观察仅属于 §12 的交付后用户人工
|
||||
walkthrough。
|
||||
|
||||
T20 收尾时必须在备份数据库/可回滚部署上完成以下人工验证:
|
||||
## 12. 交付后用户人工 Walkthrough
|
||||
|
||||
T20 的 agent/Reviewer 自动化技术验收完成后,最终报告必须将以下九步完整交给用户。用户在备份数据库、
|
||||
可回滚部署、真实 serial 设备和真实 HA 环境中自行执行并验收;这不是 Reviewer 技术 PASS、T20 状态更新或
|
||||
M8 收尾的前置条件。agent 不得执行、记录为已执行,或以 mock/临时容器结果替代这些人工观察。
|
||||
|
||||
1. 不启用 overlay 启动默认 stack,确认 app、现有 DSMR、Modbus、电价/电费和前端均无回归。
|
||||
2. 以 stable by-id + serial GID 启用 overlay;容器保持非 root,`/dev/warmtelink` 可读,代码没有
|
||||
write 操作。
|
||||
2. 以 stable by-id + serial GID 启用 overlay;容器保持非 root/非 privileged,`/dev/warmtelink` 的 device
|
||||
rule 为 pyserial POSIX `O_RDWR` 所需的 `rw`(没有 `m`),并记录可成功打开。该系统权限不等于业务写入:
|
||||
代码只 read/close,绝不调用 write。
|
||||
3. UI 创建 WarmteLink source,discover 后看到两个 channel、正确 unit/device type、质量
|
||||
`unverifiable`;原始 equipment id 在日志、DB、API、UI 均不可见。
|
||||
4. 分别创建/选择 heating 与 hot_water Meter 并确认 binding;观察 latest 约 10 秒更新、history
|
||||
@@ -1170,12 +1186,19 @@ T20 收尾时必须在备份数据库/可回滚部署上完成以下人工验证
|
||||
9. 重启 app/container,确认 source/worker/bindings/history/contract/cost 恢复;再按 runbook 回到默认
|
||||
compose,历史数据仍完整。
|
||||
|
||||
### 用户验收证据模板(交付后填写)
|
||||
|
||||
用户须在备份数据库和可回滚部署上逐项记录:日期、隔离部署标识、§12 项号、预期与实际观察、脱敏日志/截图
|
||||
引用、回滚结果。记录不得包含 stable device id、GID、secret、数据库路径或合同金额。交付时各项可为“未执行”;
|
||||
只有用户填写并自行验收后才代表真实环境结果,且不得倒推或伪造 agent/Reviewer 已完成的观察。
|
||||
|
||||
## 13. Milestone Definition of Done
|
||||
|
||||
- [ ] M8-T01~M8-T20 均由独立 Reviewer 判 `PASS`,任务 Status 为 `done`,fixup 已按仓库规则收口。
|
||||
- [ ] 统一 Source→Channel→Binding→Meter 链同时承载 DSMR 和 WarmteLink;DSMR 正常行为/数字兼容。
|
||||
- [ ] WarmteLink 双 channel 只读采集、质量接纳、latest/history、重连、隐私与部署全部符合 D1~D8。
|
||||
- [ ] electricity 成本可审计到 binding;thermal 合同、15 分钟账本、固定费、API/UI/HA 符合 D9~D14。
|
||||
- [ ] 历史升级对账、空库迁移、后端/前端/OpenAPI/codegen、Docker build 和 §12 walkthrough 全绿。
|
||||
- [ ] 文档、Roadmap 和 milestone 状态反映真实完成度;没有删除用户数据、没有真实 secret/设备身份,
|
||||
- [x] M8-T01~M8-T20 均由独立 Reviewer 依据各自的自动化技术验收判 `PASS`,任务 Status 为 `done`,fixup 已按仓库规则收口。
|
||||
- [x] 统一 Source→Channel→Binding→Meter 链同时承载 DSMR 和 WarmteLink;DSMR 正常行为/数字兼容。
|
||||
- [x] WarmteLink 双 channel 只读采集、质量接纳、latest/history、重连、隐私与部署全部符合 D1~D8。
|
||||
- [x] electricity 成本可审计到 binding;thermal 合同、15 分钟账本、固定费、API/UI/HA 符合 D9~D14。
|
||||
- [x] 历史升级对账、空库迁移、后端/前端/OpenAPI/codegen、Docker build 与无生产挂载的临时 Docker 技术验收全绿;其输入只可为 mock、模拟数据和 `/tmp` 合成数据库。
|
||||
- [x] 最终报告完整交付 §12 九步及用户证据模板;真实 serial/HA walkthrough 由用户在交付后自行验收,不能在报告中伪称 agent 已执行。
|
||||
- [x] 文档、Roadmap 和 milestone 状态反映真实完成度;没有删除用户数据、没有真实 secret/设备身份,
|
||||
没有未经授权的 push/tag。
|
||||
|
||||
@@ -49,3 +49,14 @@
|
||||
- 更复杂的 backoff 策略
|
||||
|
||||
这一轮重点是先把 app -> Home Assistant 的出站契约和可复用结构迁进来。
|
||||
|
||||
## Energy、Source 与热力实体
|
||||
|
||||
Expose 框架还可以把已勾选的 Energy 实体通过 MQTT Home Assistant Discovery 发布;开关位于应用 Config 页的 HA Expose 面板,默认均为关闭。M8 增加了 source online、按 Meter UUID 锚定的累计量/today,以及 heating、hot-water-heating、water、water-tax、fixed、all-in total/today 等 thermal 实体。
|
||||
|
||||
- source 和 Meter identity 不依赖可变 label;换表会产生新 Meter UUID identity。
|
||||
- thermal 组合成本 identity 由当前 heating/hot_water Meter UUID 的有序组合锚定,任一换表都会产生新 identity,避免不同累计域拼接。
|
||||
- availability、unit、device/state class 与 today reset 由 provider 声明;operator 应在 HA 中核对,而不应假设同名实体可跨换表连续。
|
||||
- 关闭 toggle 后 retained discovery 会被清理;关闭暴露不删除 source、Meter、合同、读数或成本历史。
|
||||
|
||||
WarmteLink 的 P1 质量会原样保留为 `unverifiable`(若适用),不因发布到 HA 而提升为 `valid`。不要将 raw telegram、equipment id、串口路径、合同金额或 API secret 作为 HA entity/state/attribute 发布。
|
||||
|
||||
+11
-1
@@ -104,9 +104,19 @@ MQTT 上报的累计成本实体(`import_cost_total` / `export_revenue_total`
|
||||
- 若是全新空库,初始表不创建(无历史数据)。
|
||||
- 回填幂等:重复跑迁移不会创建多条初始表;回填后对账(非降级周期 `meter_id IS NULL` 数必须为 0)。
|
||||
|
||||
## M8:Source binding 与热力 Meter
|
||||
|
||||
M8 将“协议连接”和物理 Meter 分开:`MeterSource` 产生稳定 channel,`MeterSourceBinding` 在 `[started_at, ended_at)` 内把 channel 接到一个 Meter epoch。DSMR electricity 与 WarmteLink heating `GJ`、hot_water `m³` 都使用这条链。
|
||||
|
||||
- 正常成本周期的两端必须解析到**同一** Meter 和 binding;跨 epoch、跨 binding、无 binding、读数陈旧或质量不可接纳时一律 degraded,不跨累计域相减。
|
||||
- source switch 是关闭/新建 binding,不创建假 Meter swap;实际换表才创建新的 Meter epoch。对于 thermal,heating 与 hot_water 独立换表,热力组合 HA identity 因任一 UUID 改变而更新。
|
||||
- `meter_cost_period` 为 heating 与 hot_water 保存 15 分钟 Decimal quantity/cost、binding、合同版本、price snapshot 和 degraded reason。固定费是合同级日汇总,只计一次。
|
||||
|
||||
部署与回滚串口 source 参见 [`warmtelink-energy.md`](./warmtelink-energy.md);保留旧 source/binding/history 可使审计和重算可重复,不能通过删除历史来“修复”边界周期。
|
||||
|
||||
## 非目标(本里程碑不做)
|
||||
|
||||
- Gas / 区域供暖的计费(`commodity != "electricity"` 的 strategy)。
|
||||
- Gas 计费 strategy。
|
||||
- "家庭(home)"分组实体;多合同时间线积分。
|
||||
- 自动识别换表(DSMR 帧无电表序列号,无法自动识别,靠用户显式声明)。
|
||||
- `last_reset` 信号(消除 HA 长期统计 blip)。
|
||||
|
||||
+12
-10
@@ -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 也已完成并拆成 M8-T01~M8-T20,等待后续由编排器按依赖逐张实现。
|
||||
> 每个里程碑的设计与**可执行原子任务**展开在 [`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-T01~T20 的自动化技术验收均已完成;M8 交付后用户人工 walkthrough 待验收。
|
||||
|
||||
## 当前基线(v1.0.3)
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
| **M6** ✅ | 通用电价层 + DSMR 接入 + 实时电费计算 | 通用电价层(manual/tibber profile + 合同版本)+ DSMR 实时电表接入 + 每 15min 寄存器差×价计量电费(不可变快照)+ 日/月/年汇总 + 反哺 HA Energy + 前端合同/价格/费用视图 |
|
||||
| **M7** ✅ | 电表生命周期 / 换表归档 | 引入 Meter epoch,计费永不跨表算 delta,跨表/无表/异常 delta 一律降级,累计按当前表归零,追溯换表可重算,Meter CRUD API + 前端管理 UI |
|
||||
| **Pre-M8** ✅ | WarmteLink P1 真机概念验证 | 正式只读 CLI 长测通过;人工开启供暖后累计量 `0.017 → 0.018 GJ` 且与物理表一致,所有 frame 的 CRC 状态仍为 `unverifiable` |
|
||||
| **M8** 📋 | WarmteLink P1、多数据源 Meter 与热力计费 | Planning 已完成:统一 Source/Channel/Binding、WarmteLink 双 channel、DSMR 迁移、thermal 合同/成本、HA/UI/部署;M8-T01~M8-T20 待实现 |
|
||||
| **M8** ✅ | WarmteLink P1、多数据源 Meter 与热力计费 | M8-T01~T20 自动化技术验收完成:Source/Channel/Binding、WarmteLink、DSMR 迁移、thermal 合同/成本、HA/UI、部署与文档闭环;真实 serial/HA walkthrough 由用户交付后验收 |
|
||||
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
||||
|
||||
排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上;M4 是公网安全加固,在 M5 IoT 集成之前先堵住裸密码这个洞;M5 在安全基座就绪后再做 IoT 接入。
|
||||
@@ -282,7 +282,7 @@ httpx / paho-mqtt / pyyaml / apscheduler 均为 M5 已有依赖,M6 复用,
|
||||
|
||||
---
|
||||
|
||||
## M8 — WarmteLink P1、多数据源 Meter 与热力计费(📋 Planning 已完成,待实现)
|
||||
## M8 — WarmteLink P1、多数据源 Meter 与热力计费(✅ 自动化技术验收已完成)
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -307,14 +307,16 @@ DSMR MQTT 与新的 WarmteLink serial source;从一个 WarmteLink source 只
|
||||
|
||||
### 原子实施链
|
||||
|
||||
- **M8-T01~T06**:统一 source/channel/binding schema、DSMR 历史/runtime/电费迁移和管理 API。
|
||||
- **M8-T07~T11**:共享 P1 parser、WarmteLink 标量存储、质量接纳、serial worker、发现与历史 API。
|
||||
- **M8-T12~T16**:合同 scope、district-heating profile、thermal cost 账本/引擎/API。
|
||||
- **M8-T17~T20**:HA、Sources/Meters UI、scope-aware 计费 UI、compose/文档/全链收尾。
|
||||
- **M8-T01~T06**:已完成统一 source/channel/binding schema、DSMR 历史/runtime/电费迁移和管理 API。
|
||||
- **M8-T07~T11**:已完成共享 P1 parser、WarmteLink 标量存储、质量接纳、serial worker、发现与历史 API。
|
||||
- **M8-T12~T16**:已完成合同 scope、district-heating profile、thermal cost 账本/引擎/API。
|
||||
- **M8-T17~T19**:已完成 HA、Sources/Meters UI 与 scope-aware 计费 UI;**M8-T20** 已完成 compose、文档与隔离自动化技术收尾。
|
||||
|
||||
完成判据不仅是单元闸门全绿,还包括历史副本迁移对账、OpenAPI/codegen、全部前端闸门、真实
|
||||
`docker build`、非 root 串口部署和真机端到端 walkthrough。任何任务都不得删除旧数据库、历史
|
||||
读数、旧 config 行或 volume;push/tag 仍需用户单独授权。
|
||||
完成判据不仅是单元闸门全绿,还包括以 mock/fake、合成数据库和隔离 Docker 完成的历史迁移对账、
|
||||
OpenAPI/codegen、全部前端闸门、真实 `docker build` 与非 root 串口部署技术验收;并须交付完整的
|
||||
九步用户 walkthrough 和证据模板。真实 serial/HA 的观察由用户在交付后自行验收,不是 agent/Reviewer
|
||||
技术 PASS、T20 状态更新或 M8 autosquash/收尾的前置条件,也不得写成已执行。任何任务都不得删除旧
|
||||
数据库、历史读数、旧 config 行或 volume;push/tag 仍需用户单独授权。
|
||||
|
||||
> 完整架构、HTTP 契约、质量/计费规则、依赖图与 M8-T01~M8-T20 任务卡:
|
||||
> [`docs/design/m8-warmtelink-energy.md`](./design/m8-warmtelink-energy.md)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# WarmteLink、数据源与热力计费运维手册
|
||||
|
||||
本手册说明如何在不改变既有 DSMR、Modbus 和 electricity 功能的前提下,部署只读的 WarmteLink P1 采集、绑定热量/生活热水 Meter、配置热力合同,并按需暴露给 Home Assistant。所有示例均使用占位符;不要把设备标识、GID、数据库路径、token 或真实合同金额提交到仓库。
|
||||
|
||||
## 安全边界与开始前备份
|
||||
|
||||
- 应用容器继续使用基础 `docker-compose.yml` 中的非 root `user: "1000:1000"`;overlay 不设置 `user`、`privileged` 或额外 capability。
|
||||
- Docker device cgroup 必须以 `rw` 映射,容器内固定为 `/dev/warmtelink`:这是 pyserial 3.5 在 POSIX 上以 `O_RDWR` 打开串口所需的最小系统权限,并不表示业务可写。规则绝不包含 `m`,overlay 也不授予 root、`privileged` 或额外 capability。WarmteLink worker 仍只调用 serial `read` / `close`,绝不调用 `write` 或发送写命令。
|
||||
- 不删除或覆盖 `app_config`、`app.db`、旧数据库、Docker volume 或既有 source。禁用/解绑/回滚配置不是删除历史的替代方式。
|
||||
- 维护前停止写入窗口,使用宿主机的备份流程复制 `./data/app.db` 到受保护的备份位置;确认备份可用后才运行 migration。不要把生产库复制到开发机或用于测试。
|
||||
|
||||
## 识别稳定串口并启用 overlay
|
||||
|
||||
在宿主机(不是容器)找出稳定 symlink;不要使用会在重启后变化的 `/dev/ttyUSB*` 名称:
|
||||
|
||||
```bash
|
||||
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 指向预期的字符设备后,临时导出变量:
|
||||
|
||||
```bash
|
||||
export WARMTELINK_DEVICE_PATH=/dev/serial/by-id/<stable-by-id-name>
|
||||
export WARMTELINK_SERIAL_GID=<host-serial-gid>
|
||||
docker compose -f docker-compose.yml -f docker-compose.warmtelink.yml config
|
||||
docker compose -f docker-compose.yml -f docker-compose.warmtelink.yml up -d
|
||||
```
|
||||
|
||||
`config` 的 `app` 必须仍显示 `user: "1000:1000"`,并只出现从 stable by-id path 到 `/dev/warmtelink` 的 `rw` device mapping 与 `group_add` GID;不得出现 `privileged`、root user 或 `m` device permission。`rw` 仅满足 pyserial 的 POSIX `O_RDWR` 打开,不改变 worker 的只读业务行为。默认部署不带第二个 `-f`:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
因此无串口硬件时默认 stack 不会声明不存在的 device。不得把设备路径或 GID 写回 `docker-compose.warmtelink.yml`、`.env.example` 或文档中的真实值。
|
||||
|
||||
## Migration 与 source 配置
|
||||
|
||||
先在维护窗口运行 migration;它只升级 schema,绝不删除历史表或配置:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.warmtelink.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 名称、日志或截图。
|
||||
|
||||
## 安全回滚
|
||||
|
||||
1. 在 UI 禁用 WarmteLink source,确认状态离线且 worker 已停止;保留 channels、bindings、history、contracts 与成本账本。
|
||||
2. 停止 overlay stack 后,以默认 compose 启动:`docker compose -f docker-compose.yml up -d`。这会移除容器内 device 映射,但不会删除 `./data`、数据库、配置或 volumes。
|
||||
3. 确认 DSMR、Modbus、电价、既有 electricity 成本和前端正常。若需要恢复 WarmteLink,按上面的 stable by-id/GID 步骤再次启用 overlay 和 source。
|
||||
4. schema migration 不应以 production downgrade 回滚;只有经过验证的备份恢复流程才处理灾难恢复,且必须由 operator 在隔离维护窗口执行。
|
||||
|
||||
## 上线验收清单
|
||||
|
||||
- 默认 compose 在无 serial 硬件时正常启动。
|
||||
- overlay 合并后 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 后数据完整 | 待填写 | 待填写 | 待填写 | 未执行 |
|
||||
@@ -72,6 +72,38 @@ def test_compose_uses_migration_job_before_app() -> None:
|
||||
assert dev["services"]["app"]["build"] == "."
|
||||
|
||||
|
||||
def test_warmtelink_overlay_keeps_app_non_root_and_maps_minimal_serial_device_access() -> None:
|
||||
"""The optional overlay grants only pyserial's minimal open permission.
|
||||
|
||||
Environment interpolation is deliberately left unresolved: operators supply
|
||||
a host-specific stable by-id path and its numeric serial GID at deployment.
|
||||
"""
|
||||
base = _read_yaml("docker-compose.yml")
|
||||
overlay = _read_yaml("docker-compose.warmtelink.yml")
|
||||
|
||||
base_app = base["services"]["app"]
|
||||
base_migration = base["services"]["migration"]
|
||||
app_overlay = overlay["services"]["app"]
|
||||
assert base_app["user"] == "1000:1000"
|
||||
assert base_migration["user"] == "1000:1000"
|
||||
assert "privileged" not in base_app
|
||||
assert "privileged" not in base_migration
|
||||
assert "user" not in app_overlay
|
||||
assert "privileged" not in app_overlay
|
||||
assert app_overlay["devices"] == [
|
||||
"${WARMTELINK_DEVICE_PATH:?Set a stable /dev/serial/by-id path}:/dev/warmtelink:rw"
|
||||
]
|
||||
device_rule = app_overlay["devices"][0]
|
||||
permissions = device_rule.rsplit(":", maxsplit=1)[1]
|
||||
assert permissions == "rw"
|
||||
assert "m" not in permissions
|
||||
assert app_overlay["group_add"] == ["${WARMTELINK_SERIAL_GID:?Set the host serial device GID}"]
|
||||
assert "/dev/serial/by-id" in (PROJECT_ROOT / "docker-compose.warmtelink.yml").read_text()
|
||||
assert "migration" not in overlay["services"]
|
||||
assert "devices" not in base_migration
|
||||
assert "privileged" not in base_migration
|
||||
|
||||
|
||||
def test_image_defaults_to_uvicorn_only() -> None:
|
||||
dockerfile = (PROJECT_ROOT / "Dockerfile").read_text()
|
||||
entrypoint = (PROJECT_ROOT / "docker/entrypoint.sh").read_text()
|
||||
|
||||
Reference in New Issue
Block a user