# Integrationen bauen

Die [API-Referenz](/api) beschreibt, was die Endpunkte tun. Diese Seite beschreibt, wie sich
ein Client verhalten sollte, der **unbeaufsichtigt bei jemandem zu Hause läuft** — Home
Assistant, ioBroker, ... — damit er sparsam, selbstheilend und angenehm einzurichten ist.

Die folgenden Regeln sind die, denen auch die offizielle
[Home-Assistant-Integration](/de/v2/integrations/home-assistant) folgt. Wer einen eigenen
Client schreibt, umgeht damit die beiden Fehler, die uns am häufigsten begegnen: das
Kontingent des Nutzers für zwischengespeicherte Daten zu verbrennen, und bei einem Planwechsel
kaputtzugehen.

:::tip
Die wichtigste Regel: **Auch eine Cache-Antwort kostet eine Anfrage.** pvnode
rechnet nichts nach Zeitplan neu — wer vor `next_poll_at` erneut fragt, bekommt exakt dieselbe
Antwort und zahlt trotzdem dafür.
:::

## 1. Einrichtung: Key erfragen, dann den Standort auswählen lassen

Verlangen Sie nie, dass ein Nutzer eine `site_id` einfügt. Standorte zu lesen ist auf **jedem
Plan** offen, ein Client kann sie also immer auflisten:

```bash
GET /v2/sites/
Authorization: Bearer YOUR_API_KEY
```

```json
[
  {"id": "site_ai8wsa9jvu3y6uqt...", "name": "Zuhause", "timezone": "Europe/Berlin"},
  {"id": "site_rv8wm5k2p1x4c7t8...", "name": "Garage", "timezone": "Europe/Berlin"}
]
```

Der empfohlene Ablauf hat zwei Schritte:

1. **API-Key.** Prüfen Sie ihn mit `GET /v2/sites/`. Eine 401 bedeutet, dass der Key falsch
   ist — und Sie haben dafür kein Prognose-Kontingent ausgegeben.
2. **Standort-Auswahl.** Zeigen Sie die Namen, speichern Sie die `id`. Bei genau einem
   Standort überspringen Sie den Schritt und wählen ihn automatisch.

Nutzen Sie die `site_id` als **stabile Identität** der Konfiguration — daran erkennen Sie eine
doppelte Einrichtung, und sie überlebt das Umbenennen des Standorts.

Derselbe Aufruf liefert die Strings mit `slope`, `orientation` und `power_kw` — genau das, was
Sie für Namen pro Solarfläche brauchen. Strings haben **kein Namensfeld**, leiten
Sie also einen ab (`"Süd 30° · 9,9 kWp"`), statt einen rohen Index anzuzeigen.

:::note
`GET /v2/sites/` und `GET /v2/sites/{site_id}` zählen nicht auf das Prognose-Kontingent und
tragen keine `RequestLimit-*`-Header. Rufen Sie sie bei der Einrichtung auf und dann, wenn
sich die Stringliste geändert haben könnte — aber nicht bei jeder Aktualisierung; günstig ist
nicht kostenlos.
:::

## 2. Den Plan erkennen, nicht fest einprogrammieren

Jede Prognose-Antwort trägt beides:

```json
"included":  ["default", "strings"],
"available": ["default", "weather", "irradiance", "clearsky", "strings", "variability"]
```

`included` ist, was Sie angefragt haben. `available` ist, was der Plan erlauben **würde**.
Wenn Sie Ihren Funktionsumfang aus `available` ableiten, braucht ein Planwechsel weder ein
neues Release noch eine Aktion des Nutzers:

- **Erste Anfrage:** konservativ fragen — Plan-gebundene Gruppen weglassen.
- **`available` lesen.** Steht dort eine Gruppe, die Sie wollen, fragen Sie sie ab der
  nächsten Abfrage mit an.
- **Bei einem Downgrade** ist `available` eine Antwort veraltet: Es kann eine Gruppe
  versprechen, die gerade entzogen wurde. Behandeln Sie die daraus folgende **403** als
  Signal, nicht als Fehler — Gruppe streichen, weiterlaufen.

Heute ist `variability` die einzige plangebundene Gruppe. Sie ohne Berechtigung anzufragen
wird abgelehnt, **bevor die Anfrage gezählt wird** — ein Probelauf kostet also nichts.

```python
want_variability = "variability" in previous_response.get("available", [])
```

Bei einer frischen Installation gibt es keine vorherige Antwort — fragen Sie dann einmal
**mit** der Gruppe an. Die 403 ist kostenlos und klärt die Frage sofort.

## 3. Auf `next_poll_at` pollen, nie auf einen eigenen Takt

`next_poll_at` ist der früheste Zeitpunkt, zu dem eine Anfrage eine neue Prognose liefert:

```json
"computed_at":  "2026-06-09T08:00:00+02:00",
"next_poll_at": "2026-06-09T12:00:00+02:00"
```

Leiten Sie Ihre nächste Abfrage daraus ab, statt aus einem festen Intervall. Ein fest
verdrahtetes „alle 30 Minuten" ist in beide Richtungen falsch: verschwenderisch bei einem Plan
mit einem Update pro Tag, veraltet bei einem Plan mit 96 Updates pro Tag.

```python
delay = parse(response["next_poll_at"]) - now() + timedelta(seconds=30)
delay = min(max(delay, timedelta(minutes=1)), timedelta(hours=24))
```

Drei Details, die sich zu übernehmen lohnen:

- **Ein kleiner Offset** (etwa 30 s) verhindert, dass Sie kurz vor der Slot-Grenze landen
  und doch die alte Antwort bekommen.
- **Das Ergebnis beschränken.** Nicht, weil ein Serverfehler erwartet wird, sondern weil eine
  negative oder absurde Verzögerung sonst zur Anfrageschleife wird. Eine Minute bis 24 Stunden
  ist ein vernünftiger Rahmen.
- **Einen Rückfall vorsehen.** Fehlt das Feld, wählen Sie ein konservatives Intervall (eine
  Stunde) und laufen weiter, statt stehenzubleiben.

`next_poll_at` ist eine **Empfehlung**, keine Zusage — eine andere Anfrage kann eine frühere
Neuberechnung auslösen. Darauf zu pollen ist die günstigste korrekte Strategie.

:::warning
**Bauen Sie den Zeitplan nicht selbst nach.** Es liegt nahe, sich auszurechnen, wann die Slots
liegen, und einen Timer danach zu stellen. Tun Sie es nicht: Wie sie verteilt sind, wird sich
ändern — wir wollen nachts, wenn nichts erzeugt wird, weniger davon vergeben und dafür mehr
in den Stunden, auf die es ankommt. Ein Client, der das Feld liest, geht das automatisch mit.
Einer, der sich seinen Takt selbst ausgerechnet hat, fragt irgendwann still zu den falschen
Zeiten ab.
:::

## 4. Ein Neustart darf keine Anfrage kosten

Speichern Sie die letzte Antwort zusammen mit ihrem `next_poll_at`. Liegt dieser Zeitpunkt
beim Start noch in der Zukunft, stellen Sie die gespeicherten Daten wieder her und zeigen sie
sofort an — ohne die API aufzurufen.

```text
Start → gespeicherte Antwort laden
      → next_poll_at noch in der Zukunft?  → verwenden, nächste Abfrage planen
      → sonst                              → abrufen
```

Ohne das verbrennt jeder Neustart, jedes Neuladen der Konfiguration und jede Absturzschleife
Kontingent für Daten, die der Client bereits hatte. Bei einem Plan mit einem Update pro Tag
können ein paar Neustarts den Tag komplett aufbrauchen.

Dem gespeicherten `available` dürfen Sie beim Wiederherstellen trauen: Es hält die Berechtigung
des Plans fest, nicht das, was zufällig angefragt wurde.

## 5. Werte zwischen den Abfragen lokal weiterbewegen

Die Prognose ist eine 15-Minuten-Zeitreihe über mehrere Tage. Werte wie „Leistung jetzt",
„Restenergie heute" oder „Zeitpunkt des Maximums" können Sie **aus Daten ableiten, die Sie schon
haben** — sie brauchen keine neue Anfrage.

Berechnen Sie sie über einen lokalen Takt (z.B. alle 5 Minuten) aus der gespeicherten
Zeitreihe neu und aktualisieren Sie Ihre Oberfläche. So bekommt auch ein Nutzer mit einem Update
pro Tag einen Wert, der sich über den Tag bewegt.

Nur die Prognose**kurve** muss abgefragt werden. Alles, was Sie daraus ablesen, nicht.

## 6. Die Kontingent-Header auslesen

Jede Prognose-Antwort — auch die **429** — trägt:

```http
RequestLimit-Limit: 10000
RequestLimit-Used: 4231
RequestLimit-Remaining: 5769
RequestLimit-Reset: 2026-09-01T00:00:00Z
```

`Remaining` anzuzeigen lohnt sich: Es macht die Kosten eines manuellen Aktualisieren-Knopfs
sichtbar und erklärt eine stehengebliebene Integration auf einen Blick.

:::warning
`Limit` und `Remaining` sind **Strings**. Auf Plänen ohne Deckel enthalten sie wörtlich
`unmetered` statt einer Zahl. Parsen Sie defensiv — `int(header)` scheitert bei diesen Nutzern.
:::

Der Zähler läuft pro **(Nutzer, Endpunkt, Monat)**. Für einen Client folgen daraus zwei Dinge:

- Die Zahlen gelten **kontoweit**, nicht pro Standort. Wenn Sie mehrere Standorte
  unterstützen, beschriften Sie den Wert nicht mit „verbleibende Anfragen für diesen
  Standort" — jede Instanz zeigt denselben Wert.
- Prognose und Historie haben **getrennte Budgets**. Das eine zu verbrauchen berührt das
  andere nicht.

Bei einer **429** hören Sie bis `RequestLimit-Reset` mit dem Pollen auf, statt es erneut zu
versuchen. Das Kontingent setzt sich am 1. des Folgemonats um 00:00 UTC zurück.

## 7. Fehlerbehandlung

pvnode nutzt **403** für mehrere voneinander unabhängige Situationen, und ein guter Client
reagiert auf jede anders. Unterscheiden Sie sie am `detail`-Text:

| Status | `detail` enthält          | Bedeutung                          | Empfohlene Reaktion                                        |
|--------|---------------------------|------------------------------------|------------------------------------------------------------|
| 401    | —                         | Key ungültig oder widerrufen       | Nach einem neuen Key fragen; nicht in Schleife wiederholen. |
| 403    | `variability`             | Plan ohne Band                     | **Kein Fehler.** Gruppe streichen, einmal wiederholen.      |
| 403    | `does not include`        | Plan ohne Zugang zur Forecast API  | Anhalten, den nötigen Plan benennen.                        |
| 403    | `inactive` / `site limit` | Standort nach Downgrade deaktiviert| Anhalten, auf Upgrade oder Löschen eines Standorts hinweisen. |
| 404    | —                         | Standort gelöscht                  | Anhalten, zur Neukonfiguration auffordern.                  |
| 429    | —                         | Monatskontingent aufgebraucht      | Bis `RequestLimit-Reset` warten und es anzeigen.            |
| 5xx    | —                         | Vorübergehend                      | Mit Backoff wiederholen, gespeicherte Prognose weiter zeigen.|

Ein vorübergehender Fehler sollte die Oberfläche nie leeren. Lassen Sie die letzte gute
Prognose stehen und markieren Sie sie als veraltet — eine Prognose von vor einer Stunde ist
immer noch eine brauchbare Prognose.

## 8. Den Client identifizieren

Senden Sie einen `User-Agent`, der Ihre Integration und deren Version nennt:

```http
User-Agent: my-integration/1.4.0 SomePlatform/2026.8
```

Er reist auf Anfragen mit, die ohnehin stattfinden, und enthält nichts über den Nutzer. Er
zeigt uns, welche Integrationen tatsächlich im Einsatz sind, und lässt uns Maintainer
erreichen, bevor eine Änderung sie trifft.

## Checkliste

- [ ] Standort über `GET /v2/sites/` gewählt
- [ ] `site_id` ist die stabile Identität der Konfiguration
- [ ] Funktionsumfang aus `available` abgeleitet, nicht aus fest verdrahteten Plannamen
- [ ] Eine entzogene Gruppe führt zu einem erneuten Versuch, nicht zu einem Setup-Fehler
- [ ] Poll-Intervall aus `next_poll_at`, beschränkt, mit Rückfall
- [ ] Letzte Antwort gespeichert — ein Neustart kostet keine Anfrage
- [ ] Abgeleitete Werte zwischen den Abfragen lokal neu berechnet
- [ ] `RequestLimit-*` defensiv geparst (`unmetered`!) und angezeigt
- [ ] 429 wartet bis `Reset`
- [ ] Die drei Bedeutungen von 403 getrennt behandelt
- [ ] Veraltete Daten bleiben bei einer Störung sichtbar
- [ ] `User-Agent` identifiziert den Client

Fragen, oder fehlt etwas? Schreiben Sie an [support@pvnode.com](mailto:support@pvnode.com).
