diff --git a/docs/references/Tibber-and-NL-Energy-Pricing.md b/docs/references/Tibber-and-NL-Energy-Pricing.md new file mode 100644 index 0000000..4aacf6e --- /dev/null +++ b/docs/references/Tibber-and-NL-Energy-Pricing.md @@ -0,0 +1,249 @@ +# Tibber API + 荷兰电价 + DSMR Reader 参考 + +> 本文件汇总 M6(动态/固定电价 → 实时买卖电费计算)所需的全部外部事实,供 `docs/design/m6-tibber-dynamic-energy.md` 与实现者直接 refer。 +> 来源:Tibber GraphQL introspection(实证)、Tibber Explorer demo 数据、Tibber NL support 文档、用户实际合同与 DSMR Reader 配置(2026-06 整理)。 +> 凡标「✅ 实证」的为代码/数据验证过的事实;金额类以用户真实账单 / 合同生效后真实 token 为最终准(见末尾「待真实数据核对」)。 + +--- + +## 1. Tibber GraphQL API + +- **Endpoint**:`POST https://api.tibber.com/v1-beta/gql` +- **认证**:HTTP header `Authorization: Bearer `;token 在 [developer.tibber.com](https://developer.tibber.com) 登录后生成(每个 Tibber 账户一个)。 +- **在线 Explorer**:[developer.tibber.com/explorer](https://developer.tibber.com/explorer),自带一个公共 **demo token**。 + - ⚠️ **demo token 现在只能做 schema introspection 与历史 demo 数据查询**;对 `viewer` 的真实数据查询返回 `UNAUTHENTICATED`。要拿自己家的实时价,必须用**自己账户的 token**。 + +### 1.1 `Price` 类型(✅ introspection 实证) + +`Price` 对象恰好 6 个字段: + +| 字段 | 类型 | 含义 | +| --- | --- | --- | +| `total` | Float | **全包价 = energy + tax**(即 App 里的 All-in Price,含 spot+能源税+VAT 等)| +| `energy` | Float | 纯能源/现货分量(ex-VAT 市场价)| +| `tax` | Float | 税费分量(能源税 + VAT 合并)| +| `startsAt` | String | 该价格段起点(ISO8601,**含时区偏移**,如 `+02:00`)| +| `currency` | String! | 币种(唯一 non-null 字段;NL 为 EUR,demo 为 SEK)| +| `level` | PriceLevel | 价格档枚举(`VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE/VERY_EXPENSIVE`)| + +**✅ 实证**:`total == energy + tax` 精确成立(见 §1.4 样本)。 + +### 1.2 15 分钟价格:`Subscription.priceInfoRange`(✅ introspection 实证) + +- **字段**:`currentSubscription.priceInfoRange` +- **参数**:`resolution: PriceInfoRangeResolution!`(必填)、`first: Int`、`last: Int`、`before: String`、`after: String`(游标分页) +- **resolution 枚举 `PriceInfoRangeResolution`**:值 `DAILY` / `HOURLY` / `QUARTER_HOURLY` + - ⚠️ 注意别用错枚举:另有 `PriceResolution`(只有 HOURLY/DAILY,**无 QUARTER_HOURLY**)和 `PriceInfoResolution`——`priceInfoRange` 用的是 **`PriceInfoRangeResolution`**。 +- **返回**:`SubscriptionPriceConnection { nodes: [Price]!, edges: [...], pageInfo }` +- **废弃说明**:老的 `priceInfo.range`(`deprecationReason: "use Subscription.priceInfoRange instead"`)**不支持 QUARTER_HOURLY**,必须改用 `Subscription.priceInfoRange`。 +- **✅ 实证(demo key,2018 历史日)**:`QUARTER_HOURLY` 返回 **96 个节点、间隔精确 15 分钟**;无真 15 分钟价的历史日,API 把**小时价重复成 4 个相同刻钟**(00/15/30/45 同值,到下一整点才变);NL 当前数据则是真 15 分钟。 +- **解析注意**:**不要假设固定 96 个节点 / 固定 15 分钟间隔**,按 `startsAt` 逐点存最稳。 + +**15 分钟查询(拉今天 96 个刻钟)**: +```graphql +{ viewer { homes { id currentSubscription { + priceInfoRange(resolution: QUARTER_HOURLY, first: 96) { + nodes { startsAt total energy tax currency level } +} } } } } +``` + +### 1.3 小时价 / 当前价 / 探测:`priceInfo` + +`currentSubscription.priceInfo` 含 `current`(当前价)、`today[]`、`tomorrow[]`(次日价约 13:00 CET 发布)。探测账户能力 + 当前价: +```graphql +{ viewer { homes { + id appNickname + features { realTimeConsumptionEnabled } + currentSubscription { priceInfo { + current { total energy tax startsAt level currency } + today { total energy tax startsAt level } + tomorrow { total energy tax startsAt level } +} } } } } +``` + +### 1.4 现成 curl + 样本响应 + +```bash +# 15 分钟价(换成自己账户 token;demo token 拿不到 viewer 真实数据) +curl -s -X POST https://api.tibber.com/v1-beta/gql \ + -H "Authorization: Bearer " -H "Content-Type: application/json" \ + -d '{"query":"{ viewer { homes { id currentSubscription { priceInfoRange(resolution: QUARTER_HOURLY, first: 96) { nodes { startsAt total energy tax currency } } } } } }"}' +``` + +**样本:`priceInfo`(demo,瑞典家庭,SEK)** +```json +{"current": {"total": 0.8239, "energy": 0.5663, "tax": 0.2576, + "startsAt": "2026-06-23T14:00:00.000+02:00", "level": "NORMAL", "currency": "SEK"}, + "today": [{"total": 1.276, "energy": 0.928, "tax": 0.348, + "startsAt": "2026-06-23T00:00:00.000+02:00", "level": "VERY_EXPENSIVE"}, … 24 项], + "tomorrow":[… 24 项]} +``` + +**样本:`priceInfoRange` QUARTER_HOURLY(demo,2018-11-02,96 节点)** +```json +{"nodes": [ + {"startsAt":"2018-11-02T00:00:00.000+01:00","total":0.63, "energy":0.435,"tax":0.195,"currency":"SEK"}, + {"startsAt":"2018-11-02T00:15:00.000+01:00","total":0.63, "energy":0.435,"tax":0.195,"currency":"SEK"}, + {"startsAt":"2018-11-02T00:30:00.000+01:00","total":0.63, "energy":0.435,"tax":0.195,"currency":"SEK"}, + {"startsAt":"2018-11-02T00:45:00.000+01:00","total":0.63, "energy":0.435,"tax":0.195,"currency":"SEK"}, + {"startsAt":"2018-11-02T01:00:00.000+01:00","total":0.6141,"energy":0.4223,"tax":0.1918,"currency":"SEK"}, + … 共 96 个,末节点 23:45 total 0.6342]} +``` +(注意 00:00–00:45 四个刻钟同值 = 小时价被重复;`total==energy+tax`。) + +--- + +## 2. 荷兰电价构成(Tibber NL + 用户合同) + +> 所有金额**含 VAT(incl. btw)**,除非另注。VAT/BTW 标准税率 **21%**。 + +| 分量 | 荷兰语 | 单位 | 值 / 说明 | +| --- | --- | --- | --- | +| 现货/市场价 | marktprijs / dynamische kwartierprijs | €/kWh | 每 15 分钟跟随交易所;= API `energy` | +| 买侧采购费 | **inkoopvergoeding** | €/kWh | **€0.0248**(覆盖 onbalans + 绿证;见下「相等」事实)| +| 卖侧上网费 | **verkoopvergoeding** | €/kWh | **€0.0248**(2026-01-01 起)| +| 能源税 | energiebelasting | €/kWh(含 VAT)| 2026 第一档(0–10000 kWh)≈ **€0.1108**;2025 ≈ €0.1228;高档更低 | +| 增值税 | BTW | % | **21%** | +| 固定月费 | vast bedrag / leveringskosten | €/月/合同 | Tibber **€5.99/月**(电、气各一次)| +| 电网维护费 | netbeheerkosten / systeembeheerkosten | €/天或/月 | 电网公司定、供应商代收;按地区,2026 约 +3.38% | +| 能源税抵扣 | heffingskorting / vermindering energiebelasting | €/年/连接 | 政府年度减免,从总费用扣 | +| ODE | Opslag Duurzame Energie | €/kWh | **现为 0**(已并入 energiebelasting)| + +**✅ 关键事实(Tibber NL 文档原文)**: +> "De verkoopvergoeding van 2,48 cent is gelijk aan de inkoopvergoeding die je bij je afgenomen stroom betaalt." +> (卖侧 verkoopvergoeding 2.48 分 = 买侧 inkoopvergoeding。) + +→ **买卖服务费相等(均 €0.0248/kWh)**,在买卖里一进一出**相互抵消**。 + +--- + +## 3. 净计量(saldering)、回送(teruglevering)、负电价、2027 + +### 3.1 回送价(净计量期内,文档原文) +> "Op het moment dat je teruglevert geven we je per kWh de beursprijs die op dat moment geldt …, inclusief energiebelasting en inkoopvergoeding plus de btw minus de verkoopvergoeding." + +即净计量期内回送价 = `beursprijs + energiebelasting + inkoopvergoeding + btw − verkoopvergoeding`。因 inkoopvergoeding = verkoopvergoeding 抵消 → **= 全额零售价**(spot+能源税+VAT),正是 saldering "回送 1 度 = 用 1 度"的本质。 + +### 3.2 年末盈余 / 取消净计量后(文档原文,Scenario 2) +> "Voor de overproductie van 500 kWh heb je recht op de beursprijs en de inkoopvergoeding, maar heb je geen recht op de energiebelasting. … ontvang je nog een factuur van ons voor de te veel uitgekeerde belastingen …" + +即**超额回送(或 2027 取消净计量后)**:盈余按 `beursprijs + inkoopvergoeding − verkoopvergoeding`(**无能源税**)计价 → 因两费抵消 → **= 纯 spot**。 + +### 3.3 净计量机制 +- 法定**按自然年**结算;可抵扣到「用电量」为止(回送抵到用电为止)。 +- Tibber 不用预付,**净计量周期在第 12 张账单后结束**。 +- **2027 起荷兰取消净计量(saldering stopt in 2027)**——用户据此设计:**不再考虑净计量,直接按实时买卖算**。 + +### 3.4 负电价 +- 负价时**用电**:理论上你拿钱(但仍计能源税,净计量期内税会退回)。 +- 负价时**回送**:你要为回送的电付那个负价(= 倒贴)。 +- 因 Tibber 给的是 all-in `total`(已含能源税),**只有 spot 负到比能源税还多,total 才转负**;中等负价时 total 仍正(照付)。 +- **结论:负电价不需要特殊处理**——`buy=total` 与 `sell=total−能源税` 符号自洽。 + +--- + +## 4. 本项目采用的买/卖价公式(M6) + +> spot 取 API `energy`;`total = energy + tax`(全包)。**买价直接用 `total`**,卖价从 `total` 扣掉卖电不交的能源税。 + +- **Tibber 动态合同**(post-2027 口径): + - 买价 `buy = price.total` + - 卖价 `sell = price.total − energy_tax_per_kwh − sell_adjust`(`sell_adjust` 默认 0;含 VAT 归己;买卖费抵消已隐含在 total 里) +- **固定合同(manual,双费率)**: + - 买价 `buy_档 = energy_buy_档 + energy_tax`(档 ∈ {normal, dal}) + - 卖价 `sell_档 = sell_档`(回送价,**无能源税**) +- **固定费/抵扣不进每度**:`network_fee`、`management_fee`(月→天)、`heffingskorting`(年→天)在**日/月/年汇总**层加减。 +- 详见 `docs/design/m6-tibber-dynamic-energy.md` §3.4。 + +--- + +## 5. 用户当前固定合同(manual 实例,2026-06) + +> 双费率(NL:`_1`=dal/低谷、`_2`=normal/高峰)。以下为当前值,会随合同阶段/年度变(系统按"加新版本+生效日期"留底)。 + +| 项 | 当前值 | 备注 | +| --- | --- | --- | +| 能源价 normal(高) | €0.133/kWh | 买侧,含 VAT,不含能源税 | +| 能源价 dal(低) | €0.127/kWh | 买侧 | +| 回送价 normal | (现与 dal 同)| 卖侧,分档但现同价;系统仍分开填 | +| 回送价 dal | — | 卖侧 | +| 能源税 energiebelasting | 待填(≈€0.1108)| 含 VAT,加到买价 | +| ODE | 0 | 已并入能源税 | +| 电网维护费 | 待填 | 按天(合同按天收)| +| 供应商管理费 | ≈€0.329/天(≈€9.87/月)| 对应 Tibber 的 €5.99/月 | +| heffingskorting | 待填 | 年度减免,汇总时扣 | +| 回送阶梯罚金 terugleverkosten | **不做** | 按自然年累计、阶梯式;用户住不到年底算不准,M6 不实现 | + +--- + +## 6. DSMR Reader → MQTT + +### 6.1 取数方式 +- 用 **Telegram JSON**(单 topic **`dsmr/json`**,一条 = 一帧完整 telegram 的 JSON),**不用** split-topic(每字段一 topic、要按 id 拼)、**不用** raw(未解析 OBIS)。 +- DSMR Reader **每秒一条**。本项目按 **10 秒降采样**(仅秒数整 10 落盘)、**整帧存 JSON blob**(不做字段 allowlist)。 + +### 6.2 telegram JSON 字段(DSMR Reader 配置的 JSON mapping) +``` +id, timestamp, +electricity_delivered_1, electricity_delivered_2, # 进口累计 kWh(_1=dal 低谷, _2=normal 高峰) +electricity_returned_1, electricity_returned_2, # 出口/回送累计 kWh +electricity_currently_delivered, electricity_currently_returned, # 瞬时进/出口功率 kW +phase_currently_delivered_l1/l2/l3, phase_currently_returned_l1/l2/l3, # 各相瞬时功率 kW +phase_voltage_l1/l2/l3, # 各相电压 V +phase_power_current_l1/l2/l3, # 各相电流 A +extra_device_timestamp, extra_device_delivered # 燃气表(m³,每 5 分钟更新) +``` + +### 6.3 实测样本(单相,`dsmr/json`) +```json +{ + "id": 200086230, + "timestamp": "2026-06-23T12:16:48Z", + "electricity_delivered_1": "20915.154", + "electricity_returned_1": "2979.905", + "electricity_delivered_2": "15212.090", + "electricity_returned_2": "6786.406", + "electricity_currently_delivered": "0.000", + "electricity_currently_returned": "2.704", + "phase_currently_delivered_l1": "0.000", + "phase_currently_delivered_l2": null, + "phase_currently_delivered_l3": null, + "extra_device_timestamp": "2026-06-23T12:15:00Z", + "extra_device_delivered": "6208.234", + "phase_currently_returned_l1": "2.704", + "phase_currently_returned_l2": null, + "phase_currently_returned_l3": null, + "phase_voltage_l1": "237.0", + "phase_voltage_l2": null, + "phase_voltage_l3": null +} +``` + +### 6.4 解析要点(实测确认) +- **数值都是 JSON 字符串**(`"20915.154"`、`"0.000"`)→ 计费读取转 **Decimal**(累计寄存器算钱要精度)。 +- **缺测相位 = `null`**(不是缺 key)→ 视为缺测;单相只有 `*_l1`,三相后 `_l2/_l3` 由 null 变数字。 +- **进口总量** = `electricity_delivered_1 + _2`;**出口总量** = `electricity_returned_1 + _2`(双费率档对动态合同无意义,求和;对固定合同分档计价)。 +- **`timestamp` 为 UTC(Z)**;`id` 自增,做幂等去重。 +- 历史样本里电压 key 曾被错配成带前缀 `dsmr/reading/phase_voltage_l1`(DSMR Reader 的 JSON mapping 配置笔误,用户已改对)——解析仍按"键名容错"。 +- **整表寄存器与相数无关**:`delivered/returned_1/2` 是整表总量,三相只多了各相瞬时通道 → **计费逻辑相数无关**。 +- 燃气 `extra_device_delivered`(m³)一并存入 blob;燃气计费 M6 不做(数据先留,未来按 commodity 扩展)。 + +--- + +## 7. 来源 URL + +- Tibber GraphQL reference / explorer:`https://developer.tibber.com/docs/reference#rootsubscription`、`https://developer.tibber.com/explorer` +- Tibber NL 费用构成:`https://support.tibber.com/nl/articles/5605892-de-kosten-bij-tibber` +- Tibber NL 净计量与回送:`https://support.tibber.com/nl/articles/4669873-salderen-en-terugleveren-bij-tibber` +- DSMR Reader(HA 集成):`https://www.home-assistant.io/integrations/dsmr_reader/` + +--- + +## 8. 待真实数据核对(合同生效后用真实 token / 账单) + +1. **真实 token 复核**:跑 §1.4 的 15 分钟 curl,确认 NL 返回**真** 15 分钟价(非重复小时价)+ 币种 EUR。 +2. **卖价残差**:确认 `total` 里 purchase fee 是否被卖侧 sales fee 完全抵掉、回送 VAT 口径 → 调 `sell_adjust`(默认 0)。 +3. **双费率寄存器映射**:确认 `_1`=dal/`_2`=normal 没接反(差价小但要对)。 +4. **能源税年值**:按当年实际值与年用电档位核 `energy_tax`。 +5. **固定合同数值**:回送两档价、电网费、heffingskorting 待用户从账单填。 +