Files
home-automation/docs/references/Tibber-and-NL-Energy-Pricing.md
T

262 lines
17 KiB
Markdown
Raw Permalink Normal View History

# 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>`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 为 EURdemo 为 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 key2018 历史日)**`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 分钟价(换成自己账户 tokendemo token 拿不到 viewer 真实数据)
curl -s -X POST https://api.tibber.com/v1-beta/gql \
-H "Authorization: Bearer <YOUR_TOKEN>" -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_HOURLYdemo2018-11-0296 节点)**
```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 + 用户合同)
> 所有金额**含 VATincl. 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 第一档(010000 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),但两者对住户都是成本、不互相抵消**
- 买侧 inkoopvergoeding 已经**包含在 Tibber API 的 `total` 里**(见下 §3.1 的实证拆解),买电按 `total` 计价即已含它。
- 卖侧 verkoopvergoeding 则是从回送价里**额外扣掉**的一笔——所以回送价 = `total 0.0248`,比买价低 0.0248/kWh。
- ⚠️ **早期版本误判为"一进一出抵消 → 回送=total"**,这是错的:`total` 里那笔 inkoopvergoeding 不会退回来充抵 verkoopvergoeding。代码里用 `energy.sell_fee`(默认 0.0248)建模这笔卖侧费用。
---
## 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."
> **Worked exampleTibber NL 原文)**"Stel dat tussen 14:00 en 14:15 de totale stroomprijs €0,28 per kWh incl. is, dan krijg je €0,28 €0,0248 verkoopvergoeding = **€0,2552** per teruggeleverde kWh terug."
即净计量期内回送价 = `beursprijs + energiebelasting + inkoopvergoeding + btw verkoopvergoeding`,而官方例子直接写成 **`回送价 = totale stroomprijs verkoopvergoeding = total 0.0248`**。能源税**退回**(留在 total 里没动),只有 verkoopvergoeding 这 0.0248 被扣。
**✅ 实证(本项目生产库,2026-07-20 三个刻钟)**:按 21% VAT 拆 `total``total = 现货×1.21 + energiebelasting(0.11085) + inkoopvergoeding(0.0248)`,三段解出的 inkoop 都精确等于 **0.0248**。→ **我们存的 `tibber_price.total` 就是官方 "totale stroomprijs"(含 inkoopvergoeding 的买价)**,因此:
- 买价 `buy = total`(已含 inkoopvergoeding,正确)。
- 净计量回送价 `sell = total verkoopvergoeding = total 0.0248`
- ⚠️ 所以 saldering 下"回送 1 度"仍比"用 1 度"少 0.0248——**不是完全 1:1**。代码用 `sell_fee` 建模这笔扣减,`sell_adjust` 只负责在净计量期把能源税补回(`sell_adjust = energy_tax`)。
### 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 动态合同**
- 买价 `buy = price.total`(含 energy_tax + VAT + inkoopvergoeding
- 卖价 `sell = price.total energy_tax sell_fee sell_adjust`
- `sell_fee`verkoopvergoeding(卖侧上网费,默认 **0.0248**,含 VAT),**始终扣除**——即使净计量期也扣(见 §3.1)。
- `sell_adjust`:手动修正项(默认 0)。**净计量期**设为 `energy_tax`(把能源税补回),得 `sell = total sell_fee`;**2027 取消净计量后**设为 0,得 `sell = total energy_tax sell_fee`(无能源税、纯市场价再扣上网费)。
- **固定合同(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` 为 UTCZ**`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 ReaderHA 集成):`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 完全抵掉~~**已核实(2026-07**`total` 含 inkoopvergoeding0.0248),净计量回送价 = `total verkoopvergoeding(0.0248)`,两费**不抵消**;代码以 `sell_fee`(默认 0.0248)建模。仍待真实账单核对 `sell_fee` / VAT 口径的最终残差。
3. **双费率寄存器映射**:确认 `_1`=dal/`_2`=normal 没接反(差价小但要对)。
4. **能源税年值**:按当年实际值与年用电档位核 `energy_tax`
5. **固定合同数值**:回送两档价、电网费、heffingskorting 待用户从账单填。
</content>