# 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 为 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 <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_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 待用户从账单填。
</content>
