Skip to content

Commit 134b97a

Browse files
committed
feat(tibber): add Tibber Data API bridge for VW Group vehicles
Optional vehicle telemetry source via Tibber's commercial API. Mirrors evcc PR #30487. Triggered for any config.type=id/audi*/skoda*/seat/ seatcupra when tibberClientId+Secret are set; runs parallel to runEuDataAct and the legacy login. Pipeline: - lib/tibber.js: OAuth2 auth-code+PKCE client, REST helpers for /homes, /homes/{id}/devices, /homes/{id}/devices/{id}, capability extraction (storage.stateOfCharge, storage.targetStateOfCharge, range.remaining, connector.status, charging.status) per evcc. - main.js: runTibber() exchanges the user's pasted authorize code on first start, persists the rotating refresh_token in a runtime state (info.tibberRefreshToken) — NOT in native config, otherwise every rotation would restart the adapter. Clears native.tibberCode after successful exchange. - main.js: pollTibber() runs on its own this.tibberInterval timer (default 5 min, configurable), caches the device discovery for an hour to avoid N+1 listHomes/listDevices calls per cycle. - admin/index_m.html: 4 new fields + inline JS that builds the Tibber authorize URL using a fixed PKCE challenge baked into both lib and admin. UI updates the URL live when the user types the client_id. Codex review applied: - HTTP timeout 30s on every Tibber request (P1-1). - vinFromDevice now returns null for malformed external_ids (P1-4) so the caller falls back to the device UUID — no colon in object IDs. - Device cache to avoid N+1 calls per poll cycle (P1-3), refreshed hourly so newly linked vehicles are picked up. - Comment marking the PKCE-challenge duplication between lib and admin with a grep recipe (P2-3). Live-verified: code exchange + refresh-token rotation + listHomes work against a real Tibber account. listDevices returns [] for accounts that have not yet had Tibber's backend ingest the OEM connection — known limitation, mirrors evcc's experience.
1 parent 277b7df commit 134b97a

4 files changed

Lines changed: 713 additions & 1 deletion

File tree

admin/index_m.html

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,112 @@
192192
<span class="helper-text translate">Leer lassen wenn keine Bridge-App läuft. Beispiel: http://192.168.1.100:8765</span>
193193
</div>
194194
</div>
195+
196+
<div class="row">
197+
<div class="col s12">
198+
<h6 class="translate">Tibber Data API (optional, nur für VW ID)</h6>
199+
<span class="helper-text translate">
200+
Tibber liefert über deren Data API SoC, Range und Lade-Status für VW/Audi/Škoda/Cupra Fahrzeuge.
201+
<br><br>
202+
<b>Voraussetzungen:</b>
203+
<ol>
204+
<li>Tibber-Account anlegen (geht NUR über die Tibber-App, nicht über die Webseite). Ein Tibber-Stromvertrag ist NICHT nötig.</li>
205+
<li>In der Tibber-App das Fahrzeug verbinden (Connect Vehicle).</li>
206+
<li>Im Browser <a href="https://data-api.tibber.com/clients/manage" target="_blank">https://data-api.tibber.com/clients/manage</a> öffnen, mit dem gleichen Tibber-Account einloggen, OAuth2-Client anlegen mit:
207+
<ul>
208+
<li>Name: frei wählbar (z.B. "ioBroker"); darf NICHT mit "tibber" beginnen.</li>
209+
<li>Scopes: <b>alle anhaken</b> (Tibber verlangt mindestens einen zusätzlichen Scope; alles ankreuzen ist sicher).</li>
210+
<li>Redirect URI: <b><code>http://localhost/</code></b> (mit Slash am Ende, exakte Schreibweise!).</li>
211+
</ul>
212+
</li>
213+
<li>Client ID und Client Secret hier unten einfügen.</li>
214+
<li>Den Adapter speichern. Beim nächsten Start steht im Log eine Authorize-URL — diese im Browser öffnen, mit Tibber einloggen, auf der Berechtigungs-Seite alle Häkchen lassen und unten <b>"Yes, allow"</b> klicken. Nach dem Klick landest du auf einer leeren <code>http://localhost/?code=...&amp;state=...</code>-Seite (Browser zeigt Fehler — egal). Den <code>code=</code>-Wert aus der Adressleiste hier ins "Tibber Authorize Code"-Feld einfügen und nochmal speichern.</li>
215+
<li>Adapter tauscht den Code gegen einen Refresh-Token, der danach automatisch rotiert.</li>
216+
</ol>
217+
</span>
218+
</div>
219+
</div>
220+
<div class="row">
221+
<div class="col s6 input-field">
222+
<input type="text" class="value" id="tibberClientId" />
223+
<label for="tibberClientId" class="translate">Tibber Client ID</label>
224+
</div>
225+
<div class="col s6 input-field">
226+
<input type="password" class="value" id="tibberClientSecret" />
227+
<label for="tibberClientSecret" class="translate">Tibber Client Secret</label>
228+
</div>
229+
</div>
230+
<div class="row">
231+
<div class="col s12">
232+
<span class="helper-text translate" id="tibberAuthHint">
233+
Authorize-URL erscheint hier sobald die Client ID oben eingetragen ist.
234+
</span>
235+
<a id="tibberAuthLink" href="#" target="_blank" style="display:none; word-break:break-all;"></a>
236+
</div>
237+
</div>
238+
<div class="row">
239+
<div class="col s6 input-field">
240+
<input type="text" class="value" id="tibberCode" placeholder="aus der localhost-URL nach Browser-Login" />
241+
<label for="tibberCode" class="translate">Tibber Authorize Code (einmalig, wird beim ersten Start verbraucht)</label>
242+
</div>
243+
<div class="col s6 input-field">
244+
<input type="number" class="value" id="tibberInterval" min="1" />
245+
<label for="tibberInterval" class="translate">Tibber Update-Interval (Minuten, Default 5)</label>
246+
</div>
247+
</div>
248+
<script>
249+
// Tibber Authorize URL builder. Uses a fixed PKCE challenge baked
250+
// into lib/tibber.js so the URL can be assembled in pure JS without
251+
// a backend roundtrip. Verifier is the secret known to the adapter.
252+
(function () {
253+
var TIBBER_AUTH_URL = "https://thewall.tibber.com/connect/authorize";
254+
var TIBBER_REDIRECT = "http://localhost/";
255+
var TIBBER_SCOPES = [
256+
"openid", "profile", "email", "offline_access",
257+
"data-api-user-read", "data-api-homes-read", "data-api-vehicles-read",
258+
"data-api-chargers-read", "data-api-energy-systems-read",
259+
"data-api-thermostats-read", "data-api-inverters-read",
260+
].join(" ");
261+
var TIBBER_CHALLENGE = "Oey1jcnhbUa_fxI9A2NtdVrIk-QxD-9ARobHcVpOj7A";
262+
function refresh() {
263+
var idEl = document.getElementById("tibberClientId");
264+
var hint = document.getElementById("tibberAuthHint");
265+
var link = document.getElementById("tibberAuthLink");
266+
if (!idEl || !hint || !link) return;
267+
var cid = (idEl.value || "").trim();
268+
if (!cid) {
269+
link.style.display = "none";
270+
hint.textContent =
271+
"Authorize-URL erscheint hier sobald die Client ID oben eingetragen ist.";
272+
return;
273+
}
274+
var url =
275+
TIBBER_AUTH_URL +
276+
"?response_type=code" +
277+
"&client_id=" + encodeURIComponent(cid) +
278+
"&redirect_uri=" + encodeURIComponent(TIBBER_REDIRECT) +
279+
"&scope=" + encodeURIComponent(TIBBER_SCOPES) +
280+
"&state=iobroker" +
281+
"&code_challenge=" + TIBBER_CHALLENGE +
282+
"&code_challenge_method=S256";
283+
link.href = url;
284+
link.textContent = url;
285+
link.style.display = "inline";
286+
hint.textContent =
287+
"Klick auf den Link, mit Tibber einloggen, 'Yes, allow' klicken, dann den 'code='-Wert aus der localhost-URL ins Feld unten kopieren.";
288+
}
289+
document.addEventListener("DOMContentLoaded", function () {
290+
var idEl = document.getElementById("tibberClientId");
291+
if (idEl) {
292+
idEl.addEventListener("input", refresh);
293+
idEl.addEventListener("change", refresh);
294+
// Re-render once after the load() handler has filled the
295+
// value from settings.
296+
setTimeout(refresh, 100);
297+
}
298+
});
299+
})();
300+
</script>
195301
</div>
196302
</body>
197303
</html>

io-package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,11 @@
229229
"tripShortTerm": false,
230230
"tripLongTerm": false,
231231
"tripCyclic": false,
232-
"tokenBridgeUrl": ""
232+
"tokenBridgeUrl": "",
233+
"tibberClientId": "",
234+
"tibberClientSecret": "",
235+
"tibberCode": "",
236+
"tibberInterval": 5
233237
},
234238
"objects": [],
235239
"instanceObjects": [

0 commit comments

Comments
 (0)