6.6 KiB
Meter Epochs(电表生命周期 / 换表归档)
本文档说明 Meter epoch 概念、换表声明流程、计费隔离行为、累计量归零、追溯重算,以及与 HA 累计 blip 的已知行为。
为什么需要 Meter epoch
计费引擎采用寄存器差值(delta)模型:每 15 分钟成本 =(周期末读数 − 周期初读数)× 单价。荷兰 2G 智能电表退网后,电网公司会把表换成 4G 表(同址换表);搬家继承新表也是同类场景。
问题在于:新表的寄存器基数与旧表无关,若跨表算 delta,会产出负成本或巨额假成本。Meter epoch 让引擎知道"某时刻起属于哪一块物理表",从而永不跨表算 delta。
核心概念
一条 meter 记录 = 一块物理电表的一段安装期 [started_at, ended_at):
started_at:表安装/继承/搬家的起点(可在过去,追溯声明)。ended_at:null = 当前 active;非 null = 已退役表的结束时刻。label:人读标签,便于识别(如 "旧 2G 表 @ Dorpsstraat 1")。reason:initial(初始建档)/meter_swap(换表)/home_move(搬家)/other。commodity:默认electricity;为未来 gas/heating 预留,每 commodity 至多一个 active 表。
换表 = 关闭旧 active 表(ended_at = T)+ 新建 started_at = T 的表。系统无法自动识别换表(DSMR 帧无电表序列号),靠用户显式声明才能隔离。
计费隔离行为
每个 15 分钟计费周期,引擎先查该周期两端(t0/t1)分别属于哪块表:
| 情况 | 结果 |
|---|---|
| 同一块表(正常) | 在表窗口内取读数、算 delta、正常计费 |
| 无表覆盖(未声明) | 降级(成本置 0,degraded=true) |
| 跨表边界(t0 ≠ t1 的表) | 降级(丢这一个周期,可接受;换表常伴随长时间无数据) |
| delta < 0 或 > 100 kWh/15min | 降级(兜底异常,如 DSMR 回绕/毛刺,与换表无关也防住) |
忘了声明换表怎么办:搬家必有的数据空档(staleness)+ delta 护栏保证不会产出垃圾成本,只是新数据暂混在旧表 epoch、累计未归零。事后补声明 + 追溯重算即可纠正(见下方"追溯换表")。
换表声明流程
通过前端(Energy 页 → Meters tab)
- 在 Energy 页切到 Meters tab,查看当前电表时间线。
- 点击"声明换表 / 继承新表",填写:
- Label:新表的人读标签(如 "4G 新表 2026")
- 安装日期:新表的
started_at(可选择历史日期追溯) - Reason:
meter_swap/home_move/other
- 保存后,旧表自动关闭(
ended_at = 填写日期),新表成为 active。 - 如填写的是过去日期,系统自动触发受影响窗口的重算(
recompute_range),跨表周期变为降级,新表内周期重新归属。
通过 API
POST /api/energy/meters
Content-Type: application/json
{
"label": "4G 新表 2026",
"started_at": "2026-07-01T00:00:00",
"reason": "meter_swap",
"note": "荷兰电网公司换装"
}
started_at 必须 ≥ 当前 active 表的 started_at(拒绝倒挂)。
查询所有表(时间线):
GET /api/energy/meters
编辑 label / 备注 / 修正日期:
PATCH /api/energy/meters/{id}
追溯换表(started_at 在过去)
若换表日期在过去但当时忘了声明:
- 声明新表,填写过去的安装日期。
- 系统自动对
[started_at, now]范围内的周期触发recompute_range:- 跨表边界周期 → 降级。
- 归属新表窗口内的周期 →
meter_id更新为新表。
- MQTT 累计量锚点切换至新表起点,下一次 expose 推送时累计量从新表起点重算。
累计量归零(per-meter)
MQTT 上报的累计成本实体(import_cost_total / export_revenue_total)锚点 = 当前 active electricity 表的 started_at。换表后:
- 累计量从新表安装时刻起重新累加,不跨表维护历史偏移。
- 日归零实体(
*_today)不受影响(其窗口必落在当前表内)。
已知行为:HA 长期统计的一次性负 blip
累计实体 state_class: total,换表归零时其值会下台阶。Home Assistant 长期统计(energy dashboard / statistics)可能在换表那一刻记录一次性负 blip。
这是已知、可接受的行为:HA 自身存旧值做 down-sample,短期 blip 不影响日常电费读数。已与用户确认先这样观察。如需消除 blip,可在未来通过 last_reset 信号通知 HA(见设计文档 §9 后续杠杆,本里程碑不做)。
回填(迁移时的初始表)
首次升级到含 M7 迁移的版本时:
- 若库内已有任何
dsmr_reading/energy_cost_period,迁移自动创建一条reason="initial"的初始表(started_at = 最早 dsmr_reading.recorded_at),并回填现有energy_cost_period.meter_id。 - 若是全新空库,初始表不创建(无历史数据)。
- 回填幂等:重复跑迁移不会创建多条初始表;回填后对账(非降级周期
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;保留旧 source/binding/history 可使审计和重算可重复,不能通过删除历史来“修复”边界周期。
非目标(本里程碑不做)
- Gas 计费 strategy。
- "家庭(home)"分组实体;多合同时间线积分。
- 自动识别换表(DSMR 帧无电表序列号,无法自动识别,靠用户显式声明)。
last_reset信号(消除 HA 长期统计 blip)。