Tibber's API `total` already includes the buy-side inkoopvergoeding
(verified from production data: total = spot×1.21 + energy_tax 0.11085 +
inkoopvergoeding 0.0248). Under net metering Tibber pays back
`total − verkoopvergoeding` per returned kWh (NL: EUR 0.28 -> 0.2552), so the
two EUR 0.0248 fees do NOT cancel — the feed-in price sits 0.0248 below buy.
Model the verkoopvergoeding as a first-class, always-subtracted contract
field `energy.sell_fee` (default 0.0248) instead of folding it into
`sell_adjust`. New sell formula:
sell = total − energy_tax − sell_fee − sell_adjust
`sell_adjust` now carries only the net-metering energy-tax refund
(= −energy_tax). Applied in both the billing strategy and the /prices
endpoint; recorded in the pricing snapshot. Frontend renders the field
automatically (dynamic profile form). Docs (references, m6) corrected to
drop the wrong "fees cancel" premise.
17 KiB
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 登录后生成(每个 Tibber 账户一个)。 - 在线 Explorer:developer.tibber.com/explorer,自带一个公共 demo token。
- ⚠️ demo token 现在只能做 schema introspection 与历史 demo 数据查询;对
viewer的真实数据查询返回UNAUTHENTICATED。要拿自己家的实时价,必须用自己账户的 token。
- ⚠️ demo token 现在只能做 schema introspection 与历史 demo 数据查询;对
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 个刻钟):
{ 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 发布)。探测账户能力 + 当前价:
{ 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 + 样本响应
# 15 分钟价(换成自己账户 token;demo 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)
{"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 节点)
{"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),但两者对住户都是成本、不互相抵消:
- 买侧 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 example(Tibber 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_adjustsell_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)
{
"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 / 账单)
- 真实 token 复核:跑 §1.4 的 15 分钟 curl,确认 NL 返回真 15 分钟价(非重复小时价)+ 币种 EUR。
卖价残差:确认→ 已核实(2026-07):total里 purchase fee 是否被卖侧 sales fee 完全抵掉total含 inkoopvergoeding(0.0248),净计量回送价 =total − verkoopvergoeding(0.0248),两费不抵消;代码以sell_fee(默认 0.0248)建模。仍待真实账单核对sell_fee/ VAT 口径的最终残差。- 双费率寄存器映射:确认
_1=dal/_2=normal 没接反(差价小但要对)。 - 能源税年值:按当年实际值与年用电档位核
energy_tax。 - 固定合同数值:回送两档价、电网费、heffingskorting 待用户从账单填。