Navigation

    Logo
    • Register
    • Login
    • Search
    • Recent
    • Tags
    • Unread
    • Categories
    • Unreplied
    • Popular
    • GitHub
    • Docu
    • Hilfe
    1. Home
    2. tippy88

    NEWS

    • Monatsrückblick – September 2025

    • Neues Video "KI im Smart Home" - ioBroker plus n8n

    • Neues Video über Aliase, virtuelle Geräte und Kategorien

    T
    • Profile
    • Following 0
    • Followers 0
    • Topics 0
    • Posts 12
    • Best 2
    • Groups 1

    tippy88

    @tippy88

    Starter

    6
    Reputation
    6
    Profile views
    12
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    tippy88 Follow
    Starter

    Best posts made by tippy88

    • RE: Test Adapter BMW/Mini v4.x.x

      Ich habe mit Code Copilot zwei Scripte gebaut, damit funktioniert die BMW Integration.

      1. Verbindung zu BMW CarData und auto-refresh der Tokens vor Ablauf. Kann ebenso Daten über die CarAPI abrufen welche auf 50 Calls pro Tag begrenzt ist.
      2. Script baut eine MQTT Verbindung zum Car Streaming auf. refreshed die Verbindung wenn der ID_Token nach 1h abläuft. Schreibt alle Daten in separate DP.

      Zusätzliche binde ich die CarStream daten an die entsprechende DP des BMW adapters. Dies ist nur nötig bis EVCC auf den Carstream umgestellt hat, aber so bekommt EVCC inzwischen die Live Daten.

      Der CarStream liefert tatsächlich annährend Live Daten. Auf der Fahr in die Arbeit heute morgen wurde mindestens jede Minute der SOC, restkiliometer etc aktualisert.

      Ich kann heute Abend nach der Arbeit gerne die Scripte teilen und versuchen eine Kurze Anleitung dazu zu schreiben.

      posted in Tester
      T
      tippy88
    • RE: Test Adapter BMW/Mini v4.x.x

      Vorab:
      Ich bin kein Entwickler, das hat AI auf meine Anweisungen programmiert. Ich übernehme keine Verantwortung für die vollständige Funktion. Bei mir läuft es und ich habe damit MEIN Ziel erreicht. Beim ersten Start werden evtl viele Error geloggt wenn die States erstmalig angelegt werden.
      Alle Daten landen in: 0_userdata.0.Auto.BMW (anpassbar im Script)

      Also Voraussetzung sollte man für sein Fahrzeug bei BMW schonmal die Cardata und Streaming aktivieren, sowie beim Streaming die entsprechenden Datenpunkte aktivieren:

      5a419122-5196-46a3-b3a1-0d1d64b4a74c-image.png


      [Anleitung] BMW CarData in ioBroker: basicData & telematicData (mit Auto-Container) + Telegram

      1) Voraussetzungen

      • Adapter javascript (z. B. javascript.0)
      • BMW Konto mit Fahrzeug in MyBMW verknüpft
      • (Optional) Telegram: Instanz telegram.0

      2) Script importieren

      1. In ioBroker → Skripte → Neues JavaScript anlegen.
      2. Den bereitgestellten Code vollständig einfügen (bmw-cardata-telemetry-autocontainer.js).
      3. Speichern & Starten.
        Code:
      // file: iobroker/scripts/bmw-cardata-telemetry-autocontainer.js
      // BMW CarData: OAuth Device Code + auto-refresh + auto-create Container + basicData + telematicData(containerId) + flatten + Telegram + Scope-Checks
      
      const PREFIX = '0_userdata.0.Auto.BMW';
      
      const https = require('https');
      const { URL } = require('url');
      const qs = require('querystring');
      
      // ---------- small utils ----------
      function ensureState(id, defVal, common) {
        const full = `${PREFIX}.${id}`;
        if (getObject(full)) return full;
        createState(full, defVal, true, Object.assign({
          name: full,
          type: typeof defVal === 'number' ? 'number' : typeof defVal === 'boolean' ? 'boolean' : 'string',
          role: 'state', read: true, write: true
        }, common || {}));
        return full;
      }
      function val(id) { const s = getState(id); return s && s.val; }
      function setV(id, v, ack = true) { setState(id, v, ack); }
      function seg(s) { return String(s || '').replace(/[^\w.-]+/g, '_').replace(/^_+|_+$/g, ''); }
      function trimSlash(s) { return (s || '').replace(/\/+$/, ''); }
      function toAbs(pathOrUrl, base) { return new URL(pathOrUrl, base || undefined).toString(); }
      function jparse(s, fallback) { try { return JSON.parse(String(s)); } catch { return fallback; } }
      function encodeForm(o) { try { if (typeof URLSearchParams !== 'undefined') return new URLSearchParams(o).toString(); } catch {} return qs.stringify(o); }
      async function ensureAndSet(fullId, value, common) {
        return new Promise((resolve) => {
          if (getObject(fullId)) setState(fullId, value, true, () => resolve());
          else createState(fullId, value, true, Object.assign({ name: fullId, type: 'string', role: 'json', read: true, write: true }, common || {}), () => resolve());
        });
      }
      async function ensureMetric(fullId, value) {
        return new Promise((resolve) => {
          const t = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
          const role = t === 'number' ? 'value' : t === 'boolean' ? 'indicator' : 'text';
          if (!getObject(fullId)) createState(fullId, value, true, { name: fullId, type: t, role, read: true, write: true }, () => resolve());
          else setState(fullId, value, true, () => resolve());
        });
      }
      
      // ---------- Telegram ----------
      ensureState('notify.telegram_enabled', false);
      ensureState('notify.telegram_instance', 'telegram.0');
      ensureState('notify.token_warn_minutes', 10);
      ensureState('notify.on_auth', true);
      ensureState('notify.on_refresh_fail', true);
      ensureState('notify.on_fetch_fail', true);
      ensureState('notify.on_scope_warning', true);
      ensureState('notify.on_container_events', true);
      
      function sendT(text) {
        if (!val(`${PREFIX}.notify.telegram_enabled`)) return;
        const inst = val(`${PREFIX}.notify.telegram_instance`) || 'telegram.0';
        try { sendTo(inst, 'send', { text: String(text), parse_mode: 'Markdown' }); }
        catch (e) { log(`[BMW] Telegram send failed: ${e.message}`, 'warn'); }
      }
      
      // ---------- CONFIG ----------
      ensureState('config.client_id', '');
      ensureState('config.scope', 'authenticate_user openid cardata:api:read carstream:api:read');
      ensureState('config.auth_base_url', 'https://customer.bmwgroup.com');
      ensureState('config.device_code_url', '/gcdm/oauth/device/code');
      ensureState('config.token_url', '/gcdm/oauth/token');
      
      ensureState('config.api_base_url', 'https://api-cardata.bmwgroup.com');
      ensureState('config.api_version', 'v1');
      ensureState('config.accept_language', 'de-DE');
      ensureState('config.refresh_send_scope', true);
      
      // Fahrzeug + Telematik
      ensureState('data.selected_vin', '');             // <VIN>
      ensureState('config.container_id', '');           // <CONTAINER_ID> (wird auto-gesetzt)
      ensureState('config.container_auto_create', true);
      ensureState('config.container_name', 'ChargeStats');
      ensureState('config.container_purpose', 'ioBroker');
      // Default-Descriptors (anpassbar)
      ensureState(
        'config.container_descriptors',
        JSON.stringify([
          'vehicle.drivetrain.electricEngine.charging.level',
          'vehicle.drivetrain.electricEngine.kombiRemainingElectricRange',
          'vehicle.vehicle.travelledDistance',
          'vehicle.body.chargingPort.status'
        ], null, 2)
      );
      
      // ---------- CONTROL ----------
      ensureState('control.startAuth', false, { role: 'button.start' });
      ensureState('control.refreshNow', false, { role: 'button' });
      ensureState('control.fetchBasicData', false, { role: 'button' });
      ensureState('control.fetchTelematicData', false, { role: 'button' });
      ensureState('control.createContainer', false, { role: 'button' });
      
      // ---------- AUTH/TOKENS ----------
      ensureState('auth.status', 'idle');
      ensureState('auth.message', '');
      ensureState('auth.user_code', '');
      ensureState('auth.verification_uri', '');
      ensureState('auth.verification_uri_complete', '');
      ensureState('auth.device_code', '');
      ensureState('auth.expires_at', 0);
      ensureState('auth.interval_sec', 5);
      ensureState('auth.scope_in_effect', '');
      ensureState('auth.scope_warning', '');
      
      ensureState('tokens.access_token', '');
      ensureState('tokens.refresh_token', '');
      ensureState('tokens.access_expires_at', 0);
      
      ensureState('tokens.id_token', '');
      ensureState('tokens.id_expires_at', 0);
      ensureState('tokens.id_header_json', '{}');
      ensureState('tokens.id_claims_json', '{}');
      ensureState('tokens.subject', '');
      ensureState('tokens.audience', '');
      ensureState('tokens.issuer', '');
      ensureState('tokens.issued_at', 0);
      
      // ---------- DIAG ----------
      ensureState('data.last_error', '');
      ensureState('data.last_url', '');
      ensureState('data.last_status', 0);
      ensureState('data.last_headers_json', '{}');
      ensureState('data.last_response', '');
      ensureState('data.last_write_len', 0);
      
      // ---------- HTTP ----------
      async function postForm(urlStr, bodyObj, extraHeaders = {}) {
        const u = new URL(urlStr);
        const body = encodeForm(bodyObj);
        const opts = {
          method: 'POST', hostname: u.hostname, port: u.port || 443, path: u.pathname + (u.search || ''),
          headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body), 'Accept': 'application/json' }, extraHeaders)
        };
        return new Promise((resolve, reject) => {
          const req = https.request(opts, res => {
            let data = ''; res.on('data', d => data += d);
            res.on('end', () => { let json = null; try { json = JSON.parse(data || '{}'); } catch {} resolve({ status: res.statusCode, headers: res.headers, json, text: data }); });
          });
          req.on('error', reject); req.write(body); req.end();
        });
      }
      async function rawRequest(method, urlStr, headers, bodyStr) {
        const u = new URL(urlStr);
        const opts = { method, hostname: u.hostname, port: u.port || 443, path: u.pathname + (u.search || ''), headers: headers || {} };
        return new Promise((resolve, reject) => {
          const req = https.request(opts, res => {
            let data = ''; res.on('data', d => data += d);
            res.on('end', () => { let json = null; try { json = JSON.parse(data || '{}'); } catch {} resolve({ status: res.statusCode, headers: res.headers, json, text: data }); });
          });
          req.on('error', reject);
          if (bodyStr) req.write(bodyStr);
          req.end();
        });
      }
      async function getJson(urlStr, headers = {}) { return rawRequest('GET', urlStr, headers, null); }
      async function postJson(urlStr, obj, headers = {}) {
        const body = JSON.stringify(obj || {});
        const hdrs = Object.assign({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Accept': 'application/json' }, headers || {});
        return rawRequest('POST', urlStr, hdrs, body);
      }
      
      // ---------- Scopes ----------
      const REQUIRED_SCOPES = ['authenticate_user', 'openid', 'cardata:api:read', 'carstream:api:read'];
      function parseScopes(s) { return String(s || '').split(/\s+/).filter(Boolean); }
      function missingScopes(have, need = REQUIRED_SCOPES) { const set = new Set(have); return need.filter(req => !set.has(req)); }
      function checkConfiguredScopes() {
        const cfg = parseScopes(val(`${PREFIX}.config.scope`));
        const miss = missingScopes(cfg);
        if (miss.length) {
          const msg = `Fehlende Scopes in config.scope: ${miss.join(', ')}. Ergänzen & re-autorisieren.`;
          setV(`${PREFIX}.auth.scope_warning`, msg);
          log(`[BMW] ${msg}`, 'warn');
          if (val(`${PREFIX}.notify.on_scope_warning`)) sendT(`⚠️ *BMW Scope-Warnung:* ${msg}`);
        } else setV(`${PREFIX}.auth.scope_warning`, '');
      }
      function updateScopeInEffect(scope) {
        setV(`${PREFIX}.auth.scope_in_effect`, scope || '');
        const cfgScope = String(val(`${PREFIX}.config.scope`) || '');
        if (scope && cfgScope && scope.trim() !== cfgScope.trim()) {
          const msg = 'Tokens mit anderem Scope als config.scope. Für neue Scopes: Re-Auth (startAuth).';
          setV(`${PREFIX}.auth.scope_warning`, msg);
          log(`[BMW] ${msg}`, 'warn');
          if (val(`${PREFIX}.notify.on_scope_warning`)) sendT(`ℹ️ *BMW Hinweis:* ${msg}`);
        }
      }
      
      // ---------- OAuth Device Flow + Refresh ----------
      let pollTimer = null;
      let refreshTimer = null;
      let tokenWatch = null;
      
      function readCfg() {
        return {
          client_id: val(`${PREFIX}.config.client_id`),
          scope: val(`${PREFIX}.config.scope`),
          auth_base_url: trimSlash(val(`${PREFIX}.config.auth_base_url`)),
          device_code_url: val(`${PREFIX}.config.device_code_url`),
          token_url: val(`${PREFIX}.config.token_url`),
      
          api_base_url: trimSlash(val(`${PREFIX}.config.api_base_url`)),
          api_version: String(val(`${PREFIX}.config.api_version`) || 'v1'),
          accept_language: String(val(`${PREFIX}.config.accept_language`) || 'de-DE'),
          refresh_send_scope: !!val(`${PREFIX}.config.refresh_send_scope`),
      
          vin: String(val(`${PREFIX}.data.selected_vin`) || '').trim(),
          container_id: String(val(`${PREFIX}.config.container_id`) || '').trim(),
          container_auto_create: !!val(`${PREFIX}.config.container_auto_create`),
          container_name: String(val(`${PREFIX}.config.container_name`) || 'ChargeStats'),
          container_purpose: String(val(`${PREFIX}.config.container_purpose`) || 'ioBroker'),
          container_descriptors: jparse(val(`${PREFIX}.config.container_descriptors`), []),
      
          notify_on_auth: !!val(`${PREFIX}.notify.on_auth`),
          notify_on_refresh_fail: !!val(`${PREFIX}.notify.on_refresh_fail`),
          notify_on_fetch_fail: !!val(`${PREFIX}.notify.on_fetch_fail`),
          notify_on_container_events: !!val(`${PREFIX}.notify.on_container_events`),
          warn_minutes: Number(val(`${PREFIX}.notify.token_warn_minutes`) || 10)
        };
      }
      function base64urlToBuffer(s) { const pad = 4 - (s.length % 4 || 4); const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(pad); return Buffer.from(b64, 'base64'); }
      function decodeJwt(jwt) { if (!jwt || typeof jwt !== 'string' || jwt.split('.').length < 2) return null; const [h,p]=jwt.split('.'); try { return { header: JSON.parse(base64urlToBuffer(h).toString('utf8')), payload: JSON.parse(base64urlToBuffer(p).toString('utf8')) }; } catch { return null; } }
      function applyIdToken(idt) {
        if (!idt) return; setV(`${PREFIX}.tokens.id_token`, idt);
        const dec = decodeJwt(idt); if (!dec) return;
        const { header, payload } = dec;
        setV(`${PREFIX}.tokens.id_header_json`, JSON.stringify(header, null, 2));
        setV(`${PREFIX}.tokens.id_claims_json`, JSON.stringify(payload, null, 2));
        if (payload?.sub) setV(`${PREFIX}.tokens.subject`, String(payload.sub));
        if (payload?.aud) setV(`${PREFIX}.tokens.audience`, Array.isArray(payload.aud) ? payload.aud.join(',') : String(payload.aud));
        if (payload?.iss) setV(`${PREFIX}.tokens.issuer`, String(payload.iss));
        if (payload?.iat) setV(`${PREFIX}.tokens.issued_at`, Number(payload.iat) * 1000);
        if (payload?.exp) setV(`${PREFIX}.tokens.id_expires_at`, Number(payload.exp) * 1000);
      }
      function startTokenWatch() {
        clearInterval(tokenWatch);
        const warnMs = Math.max(60_000, Number(val(`${PREFIX}.notify.token_warn_minutes`) || 10) * 60_000);
        tokenWatch = setInterval(() => {
          const exp = Number(val(`${PREFIX}.tokens.access_expires_at`)) || 0;
          if (!exp) return;
          const now = Date.now();
          if (exp - now < warnMs && exp > now) {
            clearInterval(tokenWatch);
            if (val(`${PREFIX}.notify.on_auth`)) {
              const minLeft = Math.max(1, Math.round((exp - now) / 60000));
              sendT(`🔐 BMW: Access-Token läuft in ~${minLeft} Min ab. Auto-Refresh läuft, ggf. Re-Auth (startAuth).`);
            }
          }
        }, 60_000);
      }
      async function startDeviceFlow() {
        const c = readCfg();
        if (!c.client_id || !c.scope || !c.auth_base_url || !c.device_code_url || !c.token_url) { setV(`${PREFIX}.auth.status`, 'error'); setV(`${PREFIX}.auth.message`, 'Missing OAuth config'); return; }
        checkConfiguredScopes();
        const deviceUrl = toAbs(c.device_code_url, c.auth_base_url);
      
        clearInterval(pollTimer); clearTimeout(refreshTimer);
        setV(`${PREFIX}.auth.status`, 'starting'); setV(`${PREFIX}.auth.message`, 'Requesting device code…');
      
        try {
          const r = await postForm(deviceUrl, { client_id: c.client_id, scope: c.scope });
          if (r.status >= 400) throw new Error(`Device code error ${r.status}: ${r.text}`);
          const dc = r.json || {}; const now = Date.now();
          setV(`${PREFIX}.auth.user_code`, dc.user_code || '');
          setV(`${PREFIX}.auth.verification_uri`, dc.verification_uri || '');
          setV(`${PREFIX}.auth.verification_uri_complete`, dc.verification_uri_complete || '');
          setV(`${PREFIX}.auth.device_code`, dc.device_code || '');
          setV(`${PREFIX}.auth.expires_at`, now + Number(dc.expires_in || 1800) * 1000);
          setV(`${PREFIX}.auth.interval_sec`, Number(dc.interval || 5));
          setV(`${PREFIX}.auth.status`, 'pending');
          setV(`${PREFIX}.auth.message`, `Öffne ${dc.verification_uri || ''} und gib den Code ${dc.user_code || ''} ein`);
          if (c.notify_on_auth) sendT(`🔐 *BMW Anmeldung*\nCode: *${dc.user_code || ''}*\nURL: ${dc.verification_uri_complete || dc.verification_uri || ''}`);
          pollTimer = setInterval(() => pollForToken().catch(e => log(`[BMW] poll error: ${e.message}`, 'warn')), (Number(dc.interval || 5)) * 1000);
        } catch (e) {
          setV(`${PREFIX}.auth.status`, 'error'); setV(`${PREFIX}.auth.message`, String(e.message || e));
          if (readCfg().notify_on_auth) sendT(`⚠️ BMW startAuth fehlgeschlagen: ${e.message}`);
        }
      }
      async function pollForToken() {
        const c = readCfg();
        const device_code = val(`${PREFIX}.auth.device_code`);
        const exp = Number(val(`${PREFIX}.auth.expires_at`)) || 0;
        if (!device_code) return;
        if (Date.now() >= exp) { setV(`${PREFIX}.auth.status`, 'expired'); setV(`${PREFIX}.auth.message`, 'Device code expired'); clearInterval(pollTimer); return; }
      
        const tokenUrl = toAbs(c.token_url, c.auth_base_url);
        const r = await postForm(tokenUrl, { client_id: c.client_id, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code });
        const b = r.json || {};
        if (r.status >= 400) {
          const err = b.error || '';
          if (err === 'authorization_pending') return;
          if (err === 'slow_down') { const next = (Number(val(`${PREFIX}.auth.interval_sec`)) || 5) + 5; setV(`${PREFIX}.auth.interval_sec`, next); clearInterval(pollTimer); pollTimer = setInterval(() => pollForToken().catch(e => log(`[BMW] poll error: ${e.message}`, 'warn')), next * 1000); return; }
          if (err === 'access_denied') { setV(`${PREFIX}.auth.status`, 'denied'); setV(`${PREFIX}.auth.message`, 'User denied'); clearInterval(pollTimer); if (c.notify_on_auth) sendT('❌ BMW: Anmeldung abgelehnt.'); return; }
          if (err === 'expired_token') { setV(`${PREFIX}.auth.status`, 'expired'); setV(`${PREFIX}.auth.message`, 'Device code expired'); clearInterval(pollTimer); if (c.notify_on_auth) sendT('⌛ BMW: Device-Code abgelaufen.'); return; }
          throw new Error(`Token polling failed (${r.status}): ${JSON.stringify(b)}`);
        }
        clearInterval(pollTimer);
        setV(`${PREFIX}.auth.status`, 'authorized'); setV(`${PREFIX}.auth.message`, 'Authorized');
        const expires_in = Number(b.expires_in || 3600);
        setV(`${PREFIX}.tokens.access_token`, b.access_token || '');
        setV(`${PREFIX}.tokens.refresh_token`, b.refresh_token || '');
        setV(`${PREFIX}.tokens.access_expires_at`, Date.now() + expires_in * 1000);
        if (b.id_token) applyIdToken(b.id_token);
        updateScopeInEffect(c.scope);
        startTokenWatch();
        if (c.notify_on_auth) sendT('✅ BMW: Anmeldung erfolgreich.');
        scheduleRefresh(expires_in);
      }
      function scheduleRefresh(expires_in_sec) {
        clearTimeout(refreshTimer);
        const ms = Math.max(10_000, (expires_in_sec - 60) * 1000);
        refreshTimer = setTimeout(() => refreshToken().catch(e => log(`[BMW] refresh error: ${e.message}`, 'error')), ms);
      }
      async function refreshToken() {
        const c = readCfg();
        const rt = val(`${PREFIX}.tokens.refresh_token`);
        if (!rt) { setV(`${PREFIX}.auth.status`, 'no_refresh_token'); setV(`${PREFIX}.auth.message`, 'No refresh_token'); if (c.notify_on_refresh_fail) sendT('⚠️ BMW: Kein Refresh-Token vorhanden. Bitte neu anmelden.'); return; }
        const tokenUrl = toAbs(c.token_url, c.auth_base_url);
        const form = { client_id: c.client_id, grant_type: 'refresh_token', refresh_token: rt };
        if (c.refresh_send_scope) form.scope = c.scope;
        const r = await postForm(tokenUrl, form);
        if (r.status >= 400) {
          setV(`${PREFIX}.auth.status`, 'refresh_failed');
          setV(`${PREFIX}.auth.message`, `Refresh failed: ${r.text || JSON.stringify(r.json)}`);
          if (c.notify_on_refresh_fail) sendT(`⚠️ BMW: Token-Refresh fehlgeschlagen.\n${r.text || JSON.stringify(r.json)}`);
          return;
        }
        const b = r.json || {};
        setV(`${PREFIX}.tokens.access_token`, b.access_token || '');
        if (b.refresh_token) setV(`${PREFIX}.tokens.refresh_token`, b.refresh_token);
        const expires_in = Number(b.expires_in || 3600);
        setV(`${PREFIX}.tokens.access_expires_at`, Date.now() + expires_in * 1000);
        if (b.id_token) applyIdToken(b.id_token);
        updateScopeInEffect(c.scope);
        startTokenWatch();
        scheduleRefresh(expires_in);
      }
      
      // ---------- API wrappers ----------
      async function apiGet(path, query = null) {
        const c = readCfg();
        if (!c.api_base_url) throw new Error('config.api_base_url not set');
        const token = val(`${PREFIX}.tokens.access_token`); if (!token) throw new Error('No access_token');
        const u = new URL(path, c.api_base_url);
        if (query && typeof query === 'object') Object.entries(query).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') u.searchParams.set(k, String(v)); });
        const url = u.toString();
        const headers = { Authorization: `Bearer ${token}`, accept: 'application/json', 'x-version': c.api_version, 'Accept-Language': c.accept_language };
        let r = await getJson(url, headers);
        if (r.status === 401) { await refreshToken(); headers.Authorization = `Bearer ${val(`${PREFIX}.tokens.access_token`)}`; r = await getJson(url, headers); }
        setV(`${PREFIX}.data.last_url`, url); setV(`${PREFIX}.data.last_status`, r.status); setV(`${PREFIX}.data.last_headers_json`, JSON.stringify(headers, null, 2));
        if (r.status >= 400) { const msg = r.text || JSON.stringify(r.json); setV(`${PREFIX}.data.last_error`, msg); setV(`${PREFIX}.data.last_response`, ''); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ BMW API-Fehler ${r.status}: ${msg}`); throw new Error(`${r.status}: ${msg}`); }
        await ensureAndSet(`${PREFIX}.data.last_response`, r.text || JSON.stringify(r.json || {}), { role: 'json' });
        setV(`${PREFIX}.data.last_error`, ''); return r.json;
      }
      async function apiPostJson(path, bodyObj) {
        const c = readCfg();
        if (!c.api_base_url) throw new Error('config.api_base_url not set');
        const token = val(`${PREFIX}.tokens.access_token`); if (!token) throw new Error('No access_token');
        const url = toAbs(path, c.api_base_url);
        const headers = { Authorization: `Bearer ${token}`, accept: 'application/json', 'x-version': c.api_version };
        let r = await postJson(url, bodyObj, headers);
        if (r.status === 401) { await refreshToken(); headers.Authorization = `Bearer ${val(`${PREFIX}.tokens.access_token`)}`; r = await postJson(url, bodyObj, headers); }
        setV(`${PREFIX}.data.last_url`, url); setV(`${PREFIX}.data.last_status`, r.status); setV(`${PREFIX}.data.last_headers_json`, JSON.stringify(headers, null, 2));
        if (r.status >= 400) { const msg = r.text || JSON.stringify(r.json); setV(`${PREFIX}.data.last_error`, msg); setV(`${PREFIX}.data.last_response`, ''); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ BMW API-Fehler ${r.status}: ${msg}`); throw new Error(`${r.status}: ${msg}`); }
        await ensureAndSet(`${PREFIX}.data.last_response`, r.text || JSON.stringify(r.json || {}), { role: 'json' });
        setV(`${PREFIX}.data.last_error`, ''); return r.json;
      }
      
      // ---------- Container ops ----------
      async function createContainerIfNeeded() {
        const c = readCfg();
        if (c.container_id) return c.container_id;
        // Scope-Precheck
        const miss = missingScopes(parseScopes(c.scope), ['carstream:api:read']);
        if (miss.length) throw new Error(`Scope fehlt (${miss.join(', ')}). Bitte config.scope ergänzen & Re-Auth.`);
        // Body
        const descriptors = Array.isArray(c.container_descriptors) ? c.container_descriptors.filter(Boolean) : [];
        if (!descriptors.length) throw new Error('config.container_descriptors ist leer oder ungültig (JSON-Array erwartet).');
        const body = { name: c.container_name || 'ChargeStats', purpose: c.container_purpose || 'ioBroker', technicalDescriptors: descriptors };
        const resp = await apiPostJson('/customers/containers', body);
        const newId = resp && resp.containerId;
        if (!newId) throw new Error(`Container konnte nicht erstellt werden: ${JSON.stringify(resp)}`);
        // Persist
        setV(`${PREFIX}.config.container_id`, newId);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.containers.${seg(newId)}.meta`, JSON.stringify(resp, null, 2), { role: 'json' });
        if (c.notify_on_container_events) sendT(`🆕 BMW Container erstellt: *${newId}* (name: ${body.name})`);
        log(`[BMW] Container created: ${newId}`, 'info');
        return newId;
      }
      
      // ---------- Fetchers ----------
      async function fetchBasicData() {
        const c = readCfg();
        if (!c.vin) throw new Error('data.selected_vin not set');
        const json = await apiGet(`/customers/vehicles/${encodeURIComponent(c.vin)}/basicData`);
        const out = JSON.stringify(json, null, 2);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.basicData`, out, { role: 'json' });
        setV(`${PREFIX}.data.last_write_len`, out.length);
        log(`[BMW] basicData OK for ${c.vin}`, 'info');
      }
      
      async function fetchTelematicData() {
        const c = readCfg();
        if (!c.vin) throw new Error('data.selected_vin not set');
      
        let cid = c.container_id;
        if (!cid) {
          if (!c.container_auto_create) throw new Error('config.container_id not set and auto_create disabled.');
          cid = await createContainerIfNeeded(); // sets state too
        }
      
        // Now fetch telematics
        const json = await apiGet(`/customers/vehicles/${encodeURIComponent(c.vin)}/telematicData`, { containerId: cid });
        const out = JSON.stringify(json, null, 2);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.telematicData.${seg(cid)}`, out, { role: 'json' });
        setV(`${PREFIX}.data.last_write_len`, out.length);
      
        const attrs = (json && json.telematicData && typeof json.telematicData === 'object') ? json.telematicData : {};
        const base = `${PREFIX}.data.${seg(c.vin)}.telemetry.${seg(cid)}`;
        for (const [attr, rec] of Object.entries(attrs)) {
          const key = `${base}.${seg(attr)}`;
          const rawVal = rec && typeof rec === 'object' ? rec.value : rec;
          const asNum = rawVal !== null && rawVal !== undefined && rawVal !== '' && !isNaN(Number(rawVal)) ? Number(rawVal) : null;
          await ensureMetric(key, asNum !== null ? asNum : String(rawVal ?? ''));
          await ensureMetric(`${key}_unit`, rec?.unit ?? '');
          await ensureMetric(`${key}_ts`, rec?.timestamp ?? '');
        }
        log(`[BMW] telematicData OK for ${c.vin} [${cid}] – ${Object.keys(attrs).length} attributes`, 'info');
      }
      
      // ---------- wiring ----------
      on({ id: `${PREFIX}.control.startAuth`, change: 'ne' }, o => { if (!o?.state?.val) return; setV(`${PREFIX}.control.startAuth`, false); startDeviceFlow(); });
      on({ id: `${PREFIX}.control.refreshNow`, change: 'ne' }, o => { if (!o?.state?.val) return; setV(`${PREFIX}.control.refreshNow`, false); refreshToken(); });
      on({ id: `${PREFIX}.control.fetchBasicData`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.fetchBasicData`, false);
        try { await fetchBasicData(); } catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ basicData Fehlgeschlagen: ${e.message}`); }
      });
      on({ id: `${PREFIX}.control.fetchTelematicData`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.fetchTelematicData`, false);
        try { await fetchTelematicData(); } catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ telematicData Fehlgeschlagen: ${e.message}`); }
      });
      on({ id: `${PREFIX}.control.createContainer`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.createContainer`, false);
        try { const id = await createContainerIfNeeded(); log(`[BMW] Container ready: ${id}`, 'info'); }
        catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_container_events) sendT(`⚠️ Container-Erstellung fehlgeschlagen: ${e.message}`); }
      });
      
      // ---------- bootstrap ----------
      (function bootstrap() {
        checkConfiguredScopes();
        const rt = val(`${PREFIX}.tokens.refresh_token`);
        if (rt) { refreshToken().catch(e => log(`[BMW] initial refresh failed: ${e.message}`, 'warn')); }
        startTokenWatch();
        setV(`${PREFIX}.auth.message`, 'Flow: startAuth → VIN setzen → (auto) createContainer → fetchTelematicData / fetchBasicData');
      })();
      
      

      Persönliche Daten werden nicht im Code, sondern als States gepflegt.


      3) Konfiguration (States setzen)

      Alle States unter: 0_userdata.0.Auto.BMW

      OAuth / BMW

      • config.client_id → <CLIENT_ID>
      • config.scope → authenticate_user openid cardata:api:read carstream:api:read

      API

      • config.api_base_url → https://api-cardata.bmwgroup.com
      • config.api_version → v1
      • config.accept_language → z. B. de-DE
      • config.refresh_send_scope → true (empfohlen)

      Fahrzeug & Telematik

      • data.selected_vin → <VIN> (z. B. WBY61EF0405W24607)

      • Container

        • config.container_id → leer lassen für Auto-Erstellung oder vorhandene ID eintragen

        • config.container_auto_create → true (Default)

        • config.container_name → ChargeStats (frei wählbar)

        • config.container_purpose → ioBroker (frei wählbar)

        • config.container_descriptors → JSON-Array der Technical Descriptors, z. B.:

          [
            "vehicle.drivetrain.electricEngine.charging.level",
            "vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
            "vehicle.vehicle.travelledDistance",
            "vehicle.body.chargingPort.status"
          ]
          

      (Optional) Telegram

      • notify.telegram_enabled → true
      • notify.telegram_instance → telegram.0
      • notify.token_warn_minutes → 10
      • notify.on_auth / notify.on_refresh_fail / notify.on_fetch_fail / notify.on_scope_warning / notify.on_container_events → nach Wunsch true

      4) Erst-Authentifizierung (Device Code)

      1. Button control.startAuth auf true.
      2. In auth.* erscheinen user_code und verification_uri(_complete).
      3. Link öffnen, Code eingeben, BMW-Login bestätigen.
      4. auth.status = authorized, Tokens unter tokens.*.

      Wichtig: Der User-Code-Schritt ist nicht automatisierbar (BMW-Policy). Token-Refresh läuft danach automatisch.


      5) Daten abrufen

      A) basicData

      • Button control.fetchBasicData.

      • Ergebnis:

        • JSON: …data.<VIN>.basicData

      B) telematicData (Auto-Container & Abfrage)

      • Falls config.container_id leer und config.container_auto_create=true:
        → Script ruft POST /customers/containers auf, speichert Antwort nach
        …data.<VIN>.containers.<CONTAINER_ID>.meta und trägt config.container_id automatisch ein.

      • Button control.fetchTelematicData.

      • Ergebnisse:

        • Roh-JSON: …data.<VIN>.telematicData.<CONTAINER_ID>

        • Flache Einzel-Datenpunkte (+ _unit, _ts) unter:
          …data.<VIN>.telemetry.<CONTAINER_ID>.<ATTRIBUT>

        • Beispiel:

          • …telemetry.S00W005P177MV.vehicle_drivetrain_electricEngine_charging_level → 61
          • …telemetry.S00W005P177MV.vehicle_drivetrain_electricEngine_kombiRemainingElectricRange → 244

      Scope-Hinweis: Für telematicData ist carstream:api:read erforderlich – sonst 403.


      6) Optional: Automatisierung

      • Per Zeitplan (CRON) oder kleinem Trigger-Skript regelmäßig (max 50 Aufrufe pro TAG!):

        • control.fetchTelematicData (z. B. alle 30 Min)
        • control.fetchBasicData (z. B. 1× täglich)

      Beispiel (einfacher, separater JavaScript-Cron-Trigger):

      schedule('*/10 * * * *', () => setState('0_userdata.0.Auto.BMW.control.fetchTelematicData', true));
      schedule('0 3 * * *', () => setState('0_userdata.0.Auto.BMW.control.fetchBasicData', true));
      

      7) Datenpunkte (Kurzüberblick)

      • auth.* – Status, Code, Verifizierungs-URL, Warnungen

      • tokens.* – access_token, refresh_token, Ablaufzeiten, optional id_token + Claims

      • config.* – Client/Scope/API/Container-Optionen

      • control.* – Buttons (startAuth, refreshNow, fetchBasicData, fetchTelematicData, createContainer)

      • data.*

        • Diagnose: last_url, last_status, last_headers_json, last_response, last_error
        • Fahrzeug: data.<VIN>.basicData
        • Container-Meta: data.<VIN>.containers.<CONTAINER_ID>.meta
        • Telemetrie (Roh): data.<VIN>.telematicData.<CONTAINER_ID>
        • Telemetrie (flach): data.<VIN>.telemetry.<CONTAINER_ID>.* (+ _unit, _ts)

      😎 Fehler & Troubleshooting

      • 403 (CU-403): Scope/Headers/VIN/Container prüfen. Für Telemetrie carstream:api:read muss im Token enthalten sein → ggf. config.scope setzen und neu autorisieren (control.startAuth).
      • 401: Access-Token abgelaufen → Script refresht automatisch; bei Refresh-Fehler Telegram-Hinweis (falls aktiv) und neu autorisieren.
      • Leere DPs: Script nutzt atomare Writes; prüfe data.last_error & data.last_response.
      • Container nicht erstellt: config.container_descriptors muss gültiges JSON-Array sein. Ansonsten control.createContainer drücken und Logs prüfen.

      9) Sicherheit & Datenschutz

      • Tokens nie veröffentlichen (auch nicht gekürzt).
      • Screens/Logs fürs Forum anonymisieren (<CLIENT_ID>, <VIN>, <CONTAINER_ID>).
      • ioBroker-Admin absichern.

      10) Platzhalter (nur in States setzen)

      • <CLIENT_ID> → config.client_id
      • <VIN> → data.selected_vin
      • <CONTAINER_ID> → config.container_id (leer lassen für Auto-Erstellung)

      [Anleitung] BMW Car Streaming via MQTT.

      1) Voraussetzungen

      • Token-Flow existiert & aktualisiert 0_userdata.0.Auto.BMW.tokens.id_token.

      • BMW MQTT Zugangsdaten aus Portal:

        • Host: customer.streaming-cardata.bmwgroup.com
        • Port: (bei Dir z. B.) 9000
        • Username: Deine GCID aus BMW portal unter Cardata Streaming (GUID, z. B. c4beb4…8916)
        • Topic: <username>/<VIN> (z. B. c4beb4…8916/WBY61EF0405W24607)
      • ioBroker JavaScript-Adapter aktiv.

      2) mqtt installieren (einmalig)

      *In der Javascript Instanz wo das Script laufen soll, unter Settings, als zusätzliches NPM Modul mqtt eintragen.

      3) Script anlegen

      • In Admin → Skripte → JavaScript → Neu → Datei z. B. script.js.common.Auto.BMW.Streaming.
      • CFG unten (Host/Port/Username/VIN) auf Deine Werte setzen.

      4) Script (Copy & Paste)

      // File: script.js.common.Auto.BMW.Streaming
      // Public-safe: KEIN Hardcode von username/VIN. Verbindung NUR wenn beide Config-States gesetzt sind.
      
      'use strict';
      
      const mqtt = require('mqtt');
      
      // ===== CONFIG (nicht-personenbezogen) =====
      const CFG = {
        tokenStateId: '0_userdata.0.Auto.BMW.tokens.id_token',
      
        host: 'customer.streaming-cardata.bmwgroup.com',
        port: 9000,
        protocolVersion: 5,
      
        basePath: '0_userdata.0.Auto.BMW.streaming',
      
        clientId:    `iobroker-bmw-${Math.random().toString(16).slice(2)}`,
        keepalive:   30,
        reconnectMs: 4000,
        quotaCooldownMs: 120000,
      
        stallSeconds: 180,
        tickSeconds:  15,
      
        maxDepth: 6,
        maxKeys:  5000
      };
      
      // ===== RUNTIME =====
      const CFG_STATES = {
        username: '0_userdata.0.Auto.BMW.streaming.config.username',
        vin:      '0_userdata.0.Auto.BMW.streaming.config.vin',
      };
      
      let R = { username: '', vin: '' };
      
      let client = null;
      let currentToken = null;
      let tokenChangeTimer = null;
      let cooldownTimer = null;
      let connecting = false;
      let connected  = false;
      let lastRxTs = 0;
      let healthTimer = null;
      
      // ===== Logging =====
      function info(msg, extra) { log(JSON.stringify({ lvl:'info',  msg, ...(extra||{}) })); }
      function warn(msg, extra) { log(JSON.stringify({ lvl:'warn',  msg, ...(extra||{}) })); }
      function err (msg, extra) { log(JSON.stringify({ lvl:'error', msg, ...(extra||{}) })); }
      
      // ===== Safe Adapter-Helpers (createState-only) =====
      const has = (n) => typeof globalThis[n] === 'function';
      async function getObjectAsyncSafe(id) {
        if (has('getObjectAsync')) return await getObjectAsync(id);
        return await new Promise(res => getObject(id, (_e,o)=>res(o)));
      }
      async function createStateAsyncSafe(id, initVal, common) {
        return await new Promise(res => {
          try { createState(id, initVal, common||{}, () => res()); }
          catch { try { createState(id, initVal, true, common||{}, {}, () => res()); } catch { res(); } }
        });
      }
      async function ensureState(id, common, initVal) {
        const o = await getObjectAsyncSafe(id);
        if (!o) {
          const init = initVal !== undefined ? initVal :
            common?.type==='number' ? 0 : common?.type==='boolean' ? false : '';
          await createStateAsyncSafe(id, init, common);
        }
      }
      async function setStateAsyncSafe(id, state) {
        if (has('setStateAsync')) return await setStateAsync(id, state);
        return await new Promise(res => setState(id, state, res));
      }
      
      // ===== Struktur/Helfer =====
      function j(a,b){ return `${a}.${b}`; }
      function configured(){ return !!(R.username && R.vin); }
      function base(){ return `${CFG.basePath}.${R.vin}`; }
      function brokerUrl(){ return `mqtts://${CFG.host}:${CFG.port}`; }
      function strictTopic(){ return `${R.username}/${R.vin}`; }
      
      async function scaffoldPerVin() {
        const b = base();
        await ensureState(j(b,'connected'),       { name:'Connected',              type:'boolean', role:'indicator.connected', read:true,  write:false }, false);
        await ensureState(j(b,'connectedAt'),     { name:'Connected since (ISO)',  type:'string',  role:'date',                read:true,  write:false }, '');
        await ensureState(j(b,'subscribed'),      { name:'Subscribed topics',      type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'msgCount'),        { name:'Message count',          type:'number',  role:'value',               read:true,  write:false }, 0);
        await ensureState(j(b,'lastJson'),        { name:'Last raw JSON',          type:'string',  role:'json',                read:true,  write:false }, '');
        await ensureState(j(b,'lastTs'),          { name:'Last message ts',        type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'lastTopic'),       { name:'Last topic',             type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'lastEventTime'),   { name:'Event time',             type:'string',  role:'date',                read:true,  write:false }, '');
        await ensureState(j(b,'lastEventTs'),     { name:'Event ts',               type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'lastError'),       { name:'Last error',             type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'metrics.range_km'),{ name:'Remaining range (km)',   type:'number',  role:'value.distance',      read:true,  write:false }, 0);
        await ensureState(j(b,'health.lastRxAgoSec'), { name:'Seconds since last RX', type:'number', role:'value.interval',   read:true,  write:false }, -1);
        await ensureState(j(b,'health.stalled'),  { name:'Stream stalled',         type:'boolean', role:'indicator.problem',   read:true,  write:false }, false);
        await ensureState(j(b,'token.expTs'),     { name:'Token expiry ts',        type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'token.expiresInSec'),{ name:'Token expires in (s)', type:'number',  role:'value.interval',      read:true,  write:false }, 0);
      }
      
      function decodeJwtExpSeconds(jwt) {
        try {
          const parts = String(jwt).split('.');
          if (parts.length < 2) return 0;
          const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
          return Number(payload?.exp || 0);
        } catch { return 0; }
      }
      
      // ===== Config-States (leer) + Laden =====
      async function ensureConfigStates() {
        await ensureState(CFG_STATES.username, { name:'BMW MQTT Username (GCID)', type:'string', role:'text', read:true, write:true }, '');
        await ensureState(CFG_STATES.vin,      { name:'BMW VIN',                  type:'string', role:'text', read:true, write:true }, '');
      }
      function readCfgState(id) {
        const st = getState(id);
        const v = st && typeof st.val === 'string' ? st.val.trim() : '';
        return v || '';
      }
      async function loadRuntimeFromStates() {
        R.username = readCfgState(CFG_STATES.username);
        R.vin      = readCfgState(CFG_STATES.vin);
        info('Runtime config', { username: R.username ? 'set' : 'EMPTY', vin: R.vin ? 'set' : 'EMPTY' });
      }
      
      // ===== Sauberer Logout =====
      function safeEnd() {
        return new Promise(resolve => {
          if (!client) return resolve();
          try {
            const c = client;
            client = null; connecting = false; connected = false;
            let done = false;
            const finish = async () => {
              if (!done) {
                done = true;
                if (configured()) { try { await setStateAsyncSafe(j(base(),'connected'), { val:false, ack:true }); } catch {} }
                resolve();
              }
            };
            c.once('close', finish);
            c.end(false, {}, finish); // WHY: DISCONNECT senden (Single-Session)
            setTimeout(finish, 1500);
          } catch { resolve(); }
        });
      }
      
      // ===== MQTT =====
      async function connectWithToken(token) {
        if (!configured()) { warn('username/VIN not set -> no connect. Set states under 0_userdata.0.Auto.BMW.streaming.config.*'); return; }
        if (!token) { warn('No id_token available yet'); return; }
        if (connecting || connected) return;
        currentToken = token;
      
        await scaffoldPerVin();
      
        const expSec = decodeJwtExpSeconds(token);
        const nowSec = Math.floor(Date.now()/1000);
        await setStateAsyncSafe(j(base(),'token.expTs'),        { val: expSec*1000, ack:true });
        await setStateAsyncSafe(j(base(),'token.expiresInSec'), { val: Math.max(0, expSec-nowSec), ack:true });
      
        const url = brokerUrl();
        const top = strictTopic();
        connecting = true;
      
        const opts = {
          protocolVersion: CFG.protocolVersion,
          clientId: CFG.clientId,
          username: R.username,
          password: token,
          keepalive: CFG.keepalive,
          reconnectPeriod: CFG.reconnectMs,
          clean: true,
          rejectUnauthorized: true,
          servername: CFG.host
        };
      
        info('BMW MQTT connecting', { url, topic: top });
        client = mqtt.connect(url, opts);
      
        client.on('connect', async (connack) => {
          connected = true; connecting = false; lastRxTs = 0;
          await setStateAsyncSafe(j(base(),'connected'),   { val:true, ack:true });
          await setStateAsyncSafe(j(base(),'connectedAt'), { val:new Date().toISOString(), ack:true });
          client.subscribe(top, { qos: 1 }, async (e, granted) => {
            if (e) { err('Subscribe error', { error: String(e) }); return; }
            await setStateAsyncSafe(j(base(),'subscribed'), { val: granted.map(g=>g.topic).join(', '), ack:true });
            info('Subscribed', { granted });
          });
          startHealthTimer();
        });
      
        client.on('reconnect', () => info('Reconnecting...'));
        client.on('offline',   () => info('Offline'));
        client.on('close',     async () => {
          connected = false; stopHealthTimer();
          try { await setStateAsyncSafe(j(base(),'connected'), { val:false, ack:true }); } catch {}
          info('Connection closed');
        });
        client.on('error', async (e) => {
          err('MQTT error', { error: String(e) });
          try { await setStateAsyncSafe(j(base(),'lastError'), { val:String(e), ack:true }); } catch {}
          if (String(e?.message || '').toLowerCase().includes('quota')) enterQuotaCooldown();
        });
        client.on('disconnect', (packet) => {
          const rc = packet?.reasonCode;
          info('Broker disconnect', { reasonCode: rc });
          if (rc === 151) enterQuotaCooldown(); // Quota exceeded
        });
      
        client.on('message', async (_topic, payload) => {
          const b = base();
          const tsNow = Date.now();
          lastRxTs = tsNow;
      
          info('RX', { topic: String(_topic||''), bytes: payload?.length ?? 0 });
      
          await setStateAsyncSafe(j(b,'lastJson'),  { val: payload ? payload.toString('utf8') : '', ack:true });
          await setStateAsyncSafe(j(b,'lastTs'),    { val: tsNow, ack:true });
          await setStateAsyncSafe(j(b,'lastTopic'), { val: String(_topic||''), ack:true });
          const cur = getState(j(b,'msgCount'));
          const nextCnt = (cur && typeof cur.val === 'number' ? cur.val : 0) + 1;
          await setStateAsyncSafe(j(b,'msgCount'), { val: nextCnt, ack:true });
      
          try {
            const obj = JSON.parse(payload ? payload.toString('utf8') : '{}');
      
            const evIso = obj?.timestamp || obj?.time || '';
            const evTs  = evIso ? Date.parse(evIso) || tsNow : tsNow;
            await setStateAsyncSafe(j(b,'lastEventTime'), { val: evIso || new Date(evTs).toISOString(), ack:true });
            await setStateAsyncSafe(j(b,'lastEventTs'),   { val: evTs, ack:true });
      
            const data = obj?.data && typeof obj.data === 'object' ? obj.data : null;
            if (data) {
              for (const dottedKey of Object.keys(data)) {
                const rec = data[dottedKey];
                if (!rec || typeof rec !== 'object') continue;
                const root = `${b}.fields.${dottedKey}`;
                const v = rec.value;
                const vType = (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') ? typeof v : 'string';
      
                await ensureState(`${root}.value`,     { name:`${dottedKey} value`,     type:vType,    role:'value', read:true, write:false }, vType==='number'?0:(vType==='boolean'?false:''));
                await setStateAsyncSafe(`${root}.value`, { val: v, ack:true });
      
                if (rec.unit != null) {
                  await ensureState(`${root}.unit`,    { name:`${dottedKey} unit`,      type:'string', role:'text',  read:true, write:false }, '');
                  await setStateAsyncSafe(`${root}.unit`, { val: String(rec.unit), ack:true });
                }
                if (rec.timestamp != null) {
                  // FIX: entferntes falsches Apostroph im Pfad
                  await ensureState(`${root}.timestamp`, { name:`${dottedKey} time`,   type:'string', role:'date',  read:true, write:false }, '');
                  await setStateAsyncSafe(`${root}.timestamp`, { val: String(rec.timestamp), ack:true });
                }
      
                if (dottedKey === 'vehicle.drivetrain.lastRemainingRange') {
                  await ensureState(j(b,'metrics.range_km'), { name:'Remaining range (km)', type:'number', role:'value.distance', read:true, write:false }, 0);
                  const num = typeof v === 'number' ? v : parseFloat(v);
                  if (!Number.isNaN(num)) await setStateAsyncSafe(j(b,'metrics.range_km'), { val: num, ack:true });
                }
              }
            }
          } catch (e) {
            try { await setStateAsyncSafe(j(base(),'lastError'), { val:`JSON parse failed: ${String(e)}`, ack:true }); } catch {}
          }
        });
      }
      
      // ===== Health =====
      function startHealthTimer() {
        stopHealthTimer();
        healthTimer = setInterval(async () => {
          if (!configured()) return;
          const now = Date.now();
          const ageSec = lastRxTs ? Math.max(0, Math.round((now - lastRxTs)/1000)) : -1;
          await setStateAsyncSafe(j(base(),'health.lastRxAgoSec'), { val: ageSec, ack:true });
          const stalled = ageSec >= 0 && ageSec > CFG.stallSeconds;
          await setStateAsyncSafe(j(base(),'health.stalled'), { val: !!stalled, ack:true });
          if (stalled) {
            warn(`No data for ${ageSec}s -> reconnecting`);
            await safeEnd();
            const st = getState(CFG.tokenStateId);
            const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
            if (tok) connectWithToken(tok);
          }
        }, CFG.tickSeconds * 1000);
      }
      function stopHealthTimer() { if (healthTimer) { clearInterval(healthTimer); healthTimer = null; } }
      
      // ===== Quota =====
      async function enterQuotaCooldown() {
        if (cooldownTimer) return;
        warn(`Quota exceeded -> cooling down for ${CFG.quotaCooldownMs} ms`);
        await safeEnd();
        cooldownTimer = setTimeout(async () => {
          cooldownTimer = null;
          if (!configured()) return;
          const st = getState(CFG.tokenStateId);
          const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
          if (tok) connectWithToken(tok);
        }, CFG.quotaCooldownMs);
      }
      
      // ===== Reconnects =====
      function scheduleReconnect(newToken) {
        if (tokenChangeTimer) clearTimeout(tokenChangeTimer);
        tokenChangeTimer = setTimeout(async () => {
          await safeEnd();
          connectWithToken(newToken);
        }, 800);
      }
      
      // ===== Reconfigure bei Username/VIN-Änderung =====
      async function reconfigureAndReconnect() {
        await safeEnd();
        await loadRuntimeFromStates();
        if (!configured()) { warn('username/VIN not set -> idle.'); return; }
        await scaffoldPerVin();
        const st = getState(CFG.tokenStateId);
        const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
        if (tok) await connectWithToken(tok);
      }
      
      // ===== Boot =====
      (async function start() {
        await ensureConfigStates();
        await loadRuntimeFromStates();
      
        if (configured()) await scaffoldPerVin();
        else warn('Bitte zuerst setzen: 0_userdata.0.Auto.BMW.streaming.config.username und .vin');
      
        const st = getState(CFG.tokenStateId);
        const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
        if (tok) await connectWithToken(tok);
        else warn(`Token state empty: ${CFG.tokenStateId}`);
      
        on({ id: CFG.tokenStateId, change: 'ne' }, (obj) => {
          const newTok = obj?.state?.val ? String(obj.state.val).trim() : '';
          if (!newTok || newTok === currentToken) return;
          info('id_token changed, scheduling graceful reconnect');
          scheduleReconnect(newTok);
        });
      
        on({ id: CFG_STATES.username, change: 'ne' }, () => { info('username changed -> reconfigure'); reconfigureAndReconnect(); });
        on({ id: CFG_STATES.vin,      change: 'ne' }, () => { info('vin changed -> reconfigure');      reconfigureAndReconnect(); });
      
        onStop(async (cb) => {
          stopHealthTimer();
          await safeEnd();
          if (tokenChangeTimer) clearTimeout(tokenChangeTimer);
          if (cooldownTimer)    clearTimeout(cooldownTimer);
          info('BMW MQTT stream script stopped');
          cb();
        }, 3000);
      })().catch(e => err('Startup failed', { error: String(e) }));
      
      

      5) Start & prüfen

      • Daten setzen:

        • username: '0_userdata.0.Auto.BMW.streaming.config.username'
        • vin: '0_userdata.0.Auto.BMW.streaming.config.vin',
      • Log:

        • Connected to BMW MQTT
        • Subscribed → Topic sollte <username>/<VIN> anzeigen.
        • Bei Daten: RX { topic: ..., bytes: N }
      • Objects → 0_userdata.0.Auto.BMW.streaming.<VIN>:

        • connected = true, subscribed = <username>/<VIN>
        • Bei Daten: lastJson, lastEventTime, fields.*, metrics.range_km
        • health.lastRxAgoSec ≈ 0–15, steigt laufend ohne Daten.

      Troubleshooting (kurz)

      • Subscribe error: Not authorized → falscher Topic-Filter. Nur <username>/<VIN> abonnieren (keine Wildcards).
      • invalid password im lokalen mqtt.x-Adapter → Du verbindest versehentlich zu Deinem lokalen Broker. Stelle sicher, dass die URL mqtts://customer.streaming-cardata.bmwgroup.com:9000 ist.
      • Keine Daten, aber connected → oft Portal-Thema (VIN/Descriptors/Region). Teste Fahrten, Start/Stop Laden, Klima.
      • Quota exceeded → Script wartet automatisch (quotaCooldownMs) und verbindet dann wieder.
      • Einzel-Session → Script macht sauberen DISCONNECT bei Stop/Re-Auth. Keine Doppelverbindungen.

      HINWEIS

      Wenn ihr MQTT Streaming verwendet, braucht ihr TelematicData überhaupt nicht abrufen. Alle im BMW portal gewählten Daten kommen dann automatisch über den Stream, allerdings nur bei Änderung. Steht das Auto still und nichts verändert sich, kommen keine Daten.
      Die API selber läuft auch nicht 100% sauber, der BMW support schreibt selbst: "aktuell haben wir Probleme mit unseren CarData Customer Client / API / Streaming Lösungen.
      Unsere Entwickler arbeiten mit Hochdruck an der Beseitigung der Störung - zusammen mit den Partnersystemen."
      Nichtdestotrotz, bei mir läuft es aktuell.

      posted in Tester
      T
      tippy88

    Latest posts made by tippy88

    • RE: Test Adapter BMW/Mini v4.x.x

      @tombox
      Nur als Idee, da der Stream während des Ladevorgangs nicht oder nur teilweise aktualisiert wird könntest du beim Adapter Start einen Default Container erstellen (vorher List and delete existing wegen 10 container limit) der den Ladestand dann z.b. alle 30min über die Telematic API aktualisiert oder den Benutzer es per trigger machen lässt.
      Sobald bei mir nicht alle 30min der Ladestand durch den Stream geupdated wurde trigger ich ein Call zur telematic API, die hat sicher immer den aktuellen Wert.
      Der Container muss nur 1mal angelegt werden mit SOC, Range, Milage, Chargeport:

      "technicalDescriptors": [
          "vehicle.drivetrain.electricEngine.charging.level",
      	"vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
      	"vehicle.vehicle.travelledDistance",
      	"vehicle.body.chargingPort.status"
      	 ]
      
      posted in Tester
      T
      tippy88
    • RE: Test Adapter BMW/Mini v4.x.x

      @mp_trixi BMW.de, Einloggen und dann das Fahrzeug auswählen:
      bc73da5f-d140-425b-b725-33026c1d187a-image.png

      posted in Tester
      T
      tippy88
    • RE: Test Adapter BMW/Mini v4.x.x

      @tombox said in Test Adapter BMW/Mini v2.0.0:

      @lobomau Die KI hat den Adapter geupdated aber bisher konnte ich nicht intensiv testen

      Danke. Wie werden die 2 Wochen Lifetime des User-Code gehandelt, dann auch manuell alle 2 Woche neu autorisieren?
      Ich werde den Adapter dann auch mal testen.

      posted in Tester
      T
      tippy88
    • RE: Test Adapter BMW/Mini v4.x.x

      Vorab:
      Ich bin kein Entwickler, das hat AI auf meine Anweisungen programmiert. Ich übernehme keine Verantwortung für die vollständige Funktion. Bei mir läuft es und ich habe damit MEIN Ziel erreicht. Beim ersten Start werden evtl viele Error geloggt wenn die States erstmalig angelegt werden.
      Alle Daten landen in: 0_userdata.0.Auto.BMW (anpassbar im Script)

      Also Voraussetzung sollte man für sein Fahrzeug bei BMW schonmal die Cardata und Streaming aktivieren, sowie beim Streaming die entsprechenden Datenpunkte aktivieren:

      5a419122-5196-46a3-b3a1-0d1d64b4a74c-image.png


      [Anleitung] BMW CarData in ioBroker: basicData & telematicData (mit Auto-Container) + Telegram

      1) Voraussetzungen

      • Adapter javascript (z. B. javascript.0)
      • BMW Konto mit Fahrzeug in MyBMW verknüpft
      • (Optional) Telegram: Instanz telegram.0

      2) Script importieren

      1. In ioBroker → Skripte → Neues JavaScript anlegen.
      2. Den bereitgestellten Code vollständig einfügen (bmw-cardata-telemetry-autocontainer.js).
      3. Speichern & Starten.
        Code:
      // file: iobroker/scripts/bmw-cardata-telemetry-autocontainer.js
      // BMW CarData: OAuth Device Code + auto-refresh + auto-create Container + basicData + telematicData(containerId) + flatten + Telegram + Scope-Checks
      
      const PREFIX = '0_userdata.0.Auto.BMW';
      
      const https = require('https');
      const { URL } = require('url');
      const qs = require('querystring');
      
      // ---------- small utils ----------
      function ensureState(id, defVal, common) {
        const full = `${PREFIX}.${id}`;
        if (getObject(full)) return full;
        createState(full, defVal, true, Object.assign({
          name: full,
          type: typeof defVal === 'number' ? 'number' : typeof defVal === 'boolean' ? 'boolean' : 'string',
          role: 'state', read: true, write: true
        }, common || {}));
        return full;
      }
      function val(id) { const s = getState(id); return s && s.val; }
      function setV(id, v, ack = true) { setState(id, v, ack); }
      function seg(s) { return String(s || '').replace(/[^\w.-]+/g, '_').replace(/^_+|_+$/g, ''); }
      function trimSlash(s) { return (s || '').replace(/\/+$/, ''); }
      function toAbs(pathOrUrl, base) { return new URL(pathOrUrl, base || undefined).toString(); }
      function jparse(s, fallback) { try { return JSON.parse(String(s)); } catch { return fallback; } }
      function encodeForm(o) { try { if (typeof URLSearchParams !== 'undefined') return new URLSearchParams(o).toString(); } catch {} return qs.stringify(o); }
      async function ensureAndSet(fullId, value, common) {
        return new Promise((resolve) => {
          if (getObject(fullId)) setState(fullId, value, true, () => resolve());
          else createState(fullId, value, true, Object.assign({ name: fullId, type: 'string', role: 'json', read: true, write: true }, common || {}), () => resolve());
        });
      }
      async function ensureMetric(fullId, value) {
        return new Promise((resolve) => {
          const t = typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
          const role = t === 'number' ? 'value' : t === 'boolean' ? 'indicator' : 'text';
          if (!getObject(fullId)) createState(fullId, value, true, { name: fullId, type: t, role, read: true, write: true }, () => resolve());
          else setState(fullId, value, true, () => resolve());
        });
      }
      
      // ---------- Telegram ----------
      ensureState('notify.telegram_enabled', false);
      ensureState('notify.telegram_instance', 'telegram.0');
      ensureState('notify.token_warn_minutes', 10);
      ensureState('notify.on_auth', true);
      ensureState('notify.on_refresh_fail', true);
      ensureState('notify.on_fetch_fail', true);
      ensureState('notify.on_scope_warning', true);
      ensureState('notify.on_container_events', true);
      
      function sendT(text) {
        if (!val(`${PREFIX}.notify.telegram_enabled`)) return;
        const inst = val(`${PREFIX}.notify.telegram_instance`) || 'telegram.0';
        try { sendTo(inst, 'send', { text: String(text), parse_mode: 'Markdown' }); }
        catch (e) { log(`[BMW] Telegram send failed: ${e.message}`, 'warn'); }
      }
      
      // ---------- CONFIG ----------
      ensureState('config.client_id', '');
      ensureState('config.scope', 'authenticate_user openid cardata:api:read carstream:api:read');
      ensureState('config.auth_base_url', 'https://customer.bmwgroup.com');
      ensureState('config.device_code_url', '/gcdm/oauth/device/code');
      ensureState('config.token_url', '/gcdm/oauth/token');
      
      ensureState('config.api_base_url', 'https://api-cardata.bmwgroup.com');
      ensureState('config.api_version', 'v1');
      ensureState('config.accept_language', 'de-DE');
      ensureState('config.refresh_send_scope', true);
      
      // Fahrzeug + Telematik
      ensureState('data.selected_vin', '');             // <VIN>
      ensureState('config.container_id', '');           // <CONTAINER_ID> (wird auto-gesetzt)
      ensureState('config.container_auto_create', true);
      ensureState('config.container_name', 'ChargeStats');
      ensureState('config.container_purpose', 'ioBroker');
      // Default-Descriptors (anpassbar)
      ensureState(
        'config.container_descriptors',
        JSON.stringify([
          'vehicle.drivetrain.electricEngine.charging.level',
          'vehicle.drivetrain.electricEngine.kombiRemainingElectricRange',
          'vehicle.vehicle.travelledDistance',
          'vehicle.body.chargingPort.status'
        ], null, 2)
      );
      
      // ---------- CONTROL ----------
      ensureState('control.startAuth', false, { role: 'button.start' });
      ensureState('control.refreshNow', false, { role: 'button' });
      ensureState('control.fetchBasicData', false, { role: 'button' });
      ensureState('control.fetchTelematicData', false, { role: 'button' });
      ensureState('control.createContainer', false, { role: 'button' });
      
      // ---------- AUTH/TOKENS ----------
      ensureState('auth.status', 'idle');
      ensureState('auth.message', '');
      ensureState('auth.user_code', '');
      ensureState('auth.verification_uri', '');
      ensureState('auth.verification_uri_complete', '');
      ensureState('auth.device_code', '');
      ensureState('auth.expires_at', 0);
      ensureState('auth.interval_sec', 5);
      ensureState('auth.scope_in_effect', '');
      ensureState('auth.scope_warning', '');
      
      ensureState('tokens.access_token', '');
      ensureState('tokens.refresh_token', '');
      ensureState('tokens.access_expires_at', 0);
      
      ensureState('tokens.id_token', '');
      ensureState('tokens.id_expires_at', 0);
      ensureState('tokens.id_header_json', '{}');
      ensureState('tokens.id_claims_json', '{}');
      ensureState('tokens.subject', '');
      ensureState('tokens.audience', '');
      ensureState('tokens.issuer', '');
      ensureState('tokens.issued_at', 0);
      
      // ---------- DIAG ----------
      ensureState('data.last_error', '');
      ensureState('data.last_url', '');
      ensureState('data.last_status', 0);
      ensureState('data.last_headers_json', '{}');
      ensureState('data.last_response', '');
      ensureState('data.last_write_len', 0);
      
      // ---------- HTTP ----------
      async function postForm(urlStr, bodyObj, extraHeaders = {}) {
        const u = new URL(urlStr);
        const body = encodeForm(bodyObj);
        const opts = {
          method: 'POST', hostname: u.hostname, port: u.port || 443, path: u.pathname + (u.search || ''),
          headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(body), 'Accept': 'application/json' }, extraHeaders)
        };
        return new Promise((resolve, reject) => {
          const req = https.request(opts, res => {
            let data = ''; res.on('data', d => data += d);
            res.on('end', () => { let json = null; try { json = JSON.parse(data || '{}'); } catch {} resolve({ status: res.statusCode, headers: res.headers, json, text: data }); });
          });
          req.on('error', reject); req.write(body); req.end();
        });
      }
      async function rawRequest(method, urlStr, headers, bodyStr) {
        const u = new URL(urlStr);
        const opts = { method, hostname: u.hostname, port: u.port || 443, path: u.pathname + (u.search || ''), headers: headers || {} };
        return new Promise((resolve, reject) => {
          const req = https.request(opts, res => {
            let data = ''; res.on('data', d => data += d);
            res.on('end', () => { let json = null; try { json = JSON.parse(data || '{}'); } catch {} resolve({ status: res.statusCode, headers: res.headers, json, text: data }); });
          });
          req.on('error', reject);
          if (bodyStr) req.write(bodyStr);
          req.end();
        });
      }
      async function getJson(urlStr, headers = {}) { return rawRequest('GET', urlStr, headers, null); }
      async function postJson(urlStr, obj, headers = {}) {
        const body = JSON.stringify(obj || {});
        const hdrs = Object.assign({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Accept': 'application/json' }, headers || {});
        return rawRequest('POST', urlStr, hdrs, body);
      }
      
      // ---------- Scopes ----------
      const REQUIRED_SCOPES = ['authenticate_user', 'openid', 'cardata:api:read', 'carstream:api:read'];
      function parseScopes(s) { return String(s || '').split(/\s+/).filter(Boolean); }
      function missingScopes(have, need = REQUIRED_SCOPES) { const set = new Set(have); return need.filter(req => !set.has(req)); }
      function checkConfiguredScopes() {
        const cfg = parseScopes(val(`${PREFIX}.config.scope`));
        const miss = missingScopes(cfg);
        if (miss.length) {
          const msg = `Fehlende Scopes in config.scope: ${miss.join(', ')}. Ergänzen & re-autorisieren.`;
          setV(`${PREFIX}.auth.scope_warning`, msg);
          log(`[BMW] ${msg}`, 'warn');
          if (val(`${PREFIX}.notify.on_scope_warning`)) sendT(`⚠️ *BMW Scope-Warnung:* ${msg}`);
        } else setV(`${PREFIX}.auth.scope_warning`, '');
      }
      function updateScopeInEffect(scope) {
        setV(`${PREFIX}.auth.scope_in_effect`, scope || '');
        const cfgScope = String(val(`${PREFIX}.config.scope`) || '');
        if (scope && cfgScope && scope.trim() !== cfgScope.trim()) {
          const msg = 'Tokens mit anderem Scope als config.scope. Für neue Scopes: Re-Auth (startAuth).';
          setV(`${PREFIX}.auth.scope_warning`, msg);
          log(`[BMW] ${msg}`, 'warn');
          if (val(`${PREFIX}.notify.on_scope_warning`)) sendT(`ℹ️ *BMW Hinweis:* ${msg}`);
        }
      }
      
      // ---------- OAuth Device Flow + Refresh ----------
      let pollTimer = null;
      let refreshTimer = null;
      let tokenWatch = null;
      
      function readCfg() {
        return {
          client_id: val(`${PREFIX}.config.client_id`),
          scope: val(`${PREFIX}.config.scope`),
          auth_base_url: trimSlash(val(`${PREFIX}.config.auth_base_url`)),
          device_code_url: val(`${PREFIX}.config.device_code_url`),
          token_url: val(`${PREFIX}.config.token_url`),
      
          api_base_url: trimSlash(val(`${PREFIX}.config.api_base_url`)),
          api_version: String(val(`${PREFIX}.config.api_version`) || 'v1'),
          accept_language: String(val(`${PREFIX}.config.accept_language`) || 'de-DE'),
          refresh_send_scope: !!val(`${PREFIX}.config.refresh_send_scope`),
      
          vin: String(val(`${PREFIX}.data.selected_vin`) || '').trim(),
          container_id: String(val(`${PREFIX}.config.container_id`) || '').trim(),
          container_auto_create: !!val(`${PREFIX}.config.container_auto_create`),
          container_name: String(val(`${PREFIX}.config.container_name`) || 'ChargeStats'),
          container_purpose: String(val(`${PREFIX}.config.container_purpose`) || 'ioBroker'),
          container_descriptors: jparse(val(`${PREFIX}.config.container_descriptors`), []),
      
          notify_on_auth: !!val(`${PREFIX}.notify.on_auth`),
          notify_on_refresh_fail: !!val(`${PREFIX}.notify.on_refresh_fail`),
          notify_on_fetch_fail: !!val(`${PREFIX}.notify.on_fetch_fail`),
          notify_on_container_events: !!val(`${PREFIX}.notify.on_container_events`),
          warn_minutes: Number(val(`${PREFIX}.notify.token_warn_minutes`) || 10)
        };
      }
      function base64urlToBuffer(s) { const pad = 4 - (s.length % 4 || 4); const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(pad); return Buffer.from(b64, 'base64'); }
      function decodeJwt(jwt) { if (!jwt || typeof jwt !== 'string' || jwt.split('.').length < 2) return null; const [h,p]=jwt.split('.'); try { return { header: JSON.parse(base64urlToBuffer(h).toString('utf8')), payload: JSON.parse(base64urlToBuffer(p).toString('utf8')) }; } catch { return null; } }
      function applyIdToken(idt) {
        if (!idt) return; setV(`${PREFIX}.tokens.id_token`, idt);
        const dec = decodeJwt(idt); if (!dec) return;
        const { header, payload } = dec;
        setV(`${PREFIX}.tokens.id_header_json`, JSON.stringify(header, null, 2));
        setV(`${PREFIX}.tokens.id_claims_json`, JSON.stringify(payload, null, 2));
        if (payload?.sub) setV(`${PREFIX}.tokens.subject`, String(payload.sub));
        if (payload?.aud) setV(`${PREFIX}.tokens.audience`, Array.isArray(payload.aud) ? payload.aud.join(',') : String(payload.aud));
        if (payload?.iss) setV(`${PREFIX}.tokens.issuer`, String(payload.iss));
        if (payload?.iat) setV(`${PREFIX}.tokens.issued_at`, Number(payload.iat) * 1000);
        if (payload?.exp) setV(`${PREFIX}.tokens.id_expires_at`, Number(payload.exp) * 1000);
      }
      function startTokenWatch() {
        clearInterval(tokenWatch);
        const warnMs = Math.max(60_000, Number(val(`${PREFIX}.notify.token_warn_minutes`) || 10) * 60_000);
        tokenWatch = setInterval(() => {
          const exp = Number(val(`${PREFIX}.tokens.access_expires_at`)) || 0;
          if (!exp) return;
          const now = Date.now();
          if (exp - now < warnMs && exp > now) {
            clearInterval(tokenWatch);
            if (val(`${PREFIX}.notify.on_auth`)) {
              const minLeft = Math.max(1, Math.round((exp - now) / 60000));
              sendT(`🔐 BMW: Access-Token läuft in ~${minLeft} Min ab. Auto-Refresh läuft, ggf. Re-Auth (startAuth).`);
            }
          }
        }, 60_000);
      }
      async function startDeviceFlow() {
        const c = readCfg();
        if (!c.client_id || !c.scope || !c.auth_base_url || !c.device_code_url || !c.token_url) { setV(`${PREFIX}.auth.status`, 'error'); setV(`${PREFIX}.auth.message`, 'Missing OAuth config'); return; }
        checkConfiguredScopes();
        const deviceUrl = toAbs(c.device_code_url, c.auth_base_url);
      
        clearInterval(pollTimer); clearTimeout(refreshTimer);
        setV(`${PREFIX}.auth.status`, 'starting'); setV(`${PREFIX}.auth.message`, 'Requesting device code…');
      
        try {
          const r = await postForm(deviceUrl, { client_id: c.client_id, scope: c.scope });
          if (r.status >= 400) throw new Error(`Device code error ${r.status}: ${r.text}`);
          const dc = r.json || {}; const now = Date.now();
          setV(`${PREFIX}.auth.user_code`, dc.user_code || '');
          setV(`${PREFIX}.auth.verification_uri`, dc.verification_uri || '');
          setV(`${PREFIX}.auth.verification_uri_complete`, dc.verification_uri_complete || '');
          setV(`${PREFIX}.auth.device_code`, dc.device_code || '');
          setV(`${PREFIX}.auth.expires_at`, now + Number(dc.expires_in || 1800) * 1000);
          setV(`${PREFIX}.auth.interval_sec`, Number(dc.interval || 5));
          setV(`${PREFIX}.auth.status`, 'pending');
          setV(`${PREFIX}.auth.message`, `Öffne ${dc.verification_uri || ''} und gib den Code ${dc.user_code || ''} ein`);
          if (c.notify_on_auth) sendT(`🔐 *BMW Anmeldung*\nCode: *${dc.user_code || ''}*\nURL: ${dc.verification_uri_complete || dc.verification_uri || ''}`);
          pollTimer = setInterval(() => pollForToken().catch(e => log(`[BMW] poll error: ${e.message}`, 'warn')), (Number(dc.interval || 5)) * 1000);
        } catch (e) {
          setV(`${PREFIX}.auth.status`, 'error'); setV(`${PREFIX}.auth.message`, String(e.message || e));
          if (readCfg().notify_on_auth) sendT(`⚠️ BMW startAuth fehlgeschlagen: ${e.message}`);
        }
      }
      async function pollForToken() {
        const c = readCfg();
        const device_code = val(`${PREFIX}.auth.device_code`);
        const exp = Number(val(`${PREFIX}.auth.expires_at`)) || 0;
        if (!device_code) return;
        if (Date.now() >= exp) { setV(`${PREFIX}.auth.status`, 'expired'); setV(`${PREFIX}.auth.message`, 'Device code expired'); clearInterval(pollTimer); return; }
      
        const tokenUrl = toAbs(c.token_url, c.auth_base_url);
        const r = await postForm(tokenUrl, { client_id: c.client_id, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code });
        const b = r.json || {};
        if (r.status >= 400) {
          const err = b.error || '';
          if (err === 'authorization_pending') return;
          if (err === 'slow_down') { const next = (Number(val(`${PREFIX}.auth.interval_sec`)) || 5) + 5; setV(`${PREFIX}.auth.interval_sec`, next); clearInterval(pollTimer); pollTimer = setInterval(() => pollForToken().catch(e => log(`[BMW] poll error: ${e.message}`, 'warn')), next * 1000); return; }
          if (err === 'access_denied') { setV(`${PREFIX}.auth.status`, 'denied'); setV(`${PREFIX}.auth.message`, 'User denied'); clearInterval(pollTimer); if (c.notify_on_auth) sendT('❌ BMW: Anmeldung abgelehnt.'); return; }
          if (err === 'expired_token') { setV(`${PREFIX}.auth.status`, 'expired'); setV(`${PREFIX}.auth.message`, 'Device code expired'); clearInterval(pollTimer); if (c.notify_on_auth) sendT('⌛ BMW: Device-Code abgelaufen.'); return; }
          throw new Error(`Token polling failed (${r.status}): ${JSON.stringify(b)}`);
        }
        clearInterval(pollTimer);
        setV(`${PREFIX}.auth.status`, 'authorized'); setV(`${PREFIX}.auth.message`, 'Authorized');
        const expires_in = Number(b.expires_in || 3600);
        setV(`${PREFIX}.tokens.access_token`, b.access_token || '');
        setV(`${PREFIX}.tokens.refresh_token`, b.refresh_token || '');
        setV(`${PREFIX}.tokens.access_expires_at`, Date.now() + expires_in * 1000);
        if (b.id_token) applyIdToken(b.id_token);
        updateScopeInEffect(c.scope);
        startTokenWatch();
        if (c.notify_on_auth) sendT('✅ BMW: Anmeldung erfolgreich.');
        scheduleRefresh(expires_in);
      }
      function scheduleRefresh(expires_in_sec) {
        clearTimeout(refreshTimer);
        const ms = Math.max(10_000, (expires_in_sec - 60) * 1000);
        refreshTimer = setTimeout(() => refreshToken().catch(e => log(`[BMW] refresh error: ${e.message}`, 'error')), ms);
      }
      async function refreshToken() {
        const c = readCfg();
        const rt = val(`${PREFIX}.tokens.refresh_token`);
        if (!rt) { setV(`${PREFIX}.auth.status`, 'no_refresh_token'); setV(`${PREFIX}.auth.message`, 'No refresh_token'); if (c.notify_on_refresh_fail) sendT('⚠️ BMW: Kein Refresh-Token vorhanden. Bitte neu anmelden.'); return; }
        const tokenUrl = toAbs(c.token_url, c.auth_base_url);
        const form = { client_id: c.client_id, grant_type: 'refresh_token', refresh_token: rt };
        if (c.refresh_send_scope) form.scope = c.scope;
        const r = await postForm(tokenUrl, form);
        if (r.status >= 400) {
          setV(`${PREFIX}.auth.status`, 'refresh_failed');
          setV(`${PREFIX}.auth.message`, `Refresh failed: ${r.text || JSON.stringify(r.json)}`);
          if (c.notify_on_refresh_fail) sendT(`⚠️ BMW: Token-Refresh fehlgeschlagen.\n${r.text || JSON.stringify(r.json)}`);
          return;
        }
        const b = r.json || {};
        setV(`${PREFIX}.tokens.access_token`, b.access_token || '');
        if (b.refresh_token) setV(`${PREFIX}.tokens.refresh_token`, b.refresh_token);
        const expires_in = Number(b.expires_in || 3600);
        setV(`${PREFIX}.tokens.access_expires_at`, Date.now() + expires_in * 1000);
        if (b.id_token) applyIdToken(b.id_token);
        updateScopeInEffect(c.scope);
        startTokenWatch();
        scheduleRefresh(expires_in);
      }
      
      // ---------- API wrappers ----------
      async function apiGet(path, query = null) {
        const c = readCfg();
        if (!c.api_base_url) throw new Error('config.api_base_url not set');
        const token = val(`${PREFIX}.tokens.access_token`); if (!token) throw new Error('No access_token');
        const u = new URL(path, c.api_base_url);
        if (query && typeof query === 'object') Object.entries(query).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') u.searchParams.set(k, String(v)); });
        const url = u.toString();
        const headers = { Authorization: `Bearer ${token}`, accept: 'application/json', 'x-version': c.api_version, 'Accept-Language': c.accept_language };
        let r = await getJson(url, headers);
        if (r.status === 401) { await refreshToken(); headers.Authorization = `Bearer ${val(`${PREFIX}.tokens.access_token`)}`; r = await getJson(url, headers); }
        setV(`${PREFIX}.data.last_url`, url); setV(`${PREFIX}.data.last_status`, r.status); setV(`${PREFIX}.data.last_headers_json`, JSON.stringify(headers, null, 2));
        if (r.status >= 400) { const msg = r.text || JSON.stringify(r.json); setV(`${PREFIX}.data.last_error`, msg); setV(`${PREFIX}.data.last_response`, ''); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ BMW API-Fehler ${r.status}: ${msg}`); throw new Error(`${r.status}: ${msg}`); }
        await ensureAndSet(`${PREFIX}.data.last_response`, r.text || JSON.stringify(r.json || {}), { role: 'json' });
        setV(`${PREFIX}.data.last_error`, ''); return r.json;
      }
      async function apiPostJson(path, bodyObj) {
        const c = readCfg();
        if (!c.api_base_url) throw new Error('config.api_base_url not set');
        const token = val(`${PREFIX}.tokens.access_token`); if (!token) throw new Error('No access_token');
        const url = toAbs(path, c.api_base_url);
        const headers = { Authorization: `Bearer ${token}`, accept: 'application/json', 'x-version': c.api_version };
        let r = await postJson(url, bodyObj, headers);
        if (r.status === 401) { await refreshToken(); headers.Authorization = `Bearer ${val(`${PREFIX}.tokens.access_token`)}`; r = await postJson(url, bodyObj, headers); }
        setV(`${PREFIX}.data.last_url`, url); setV(`${PREFIX}.data.last_status`, r.status); setV(`${PREFIX}.data.last_headers_json`, JSON.stringify(headers, null, 2));
        if (r.status >= 400) { const msg = r.text || JSON.stringify(r.json); setV(`${PREFIX}.data.last_error`, msg); setV(`${PREFIX}.data.last_response`, ''); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ BMW API-Fehler ${r.status}: ${msg}`); throw new Error(`${r.status}: ${msg}`); }
        await ensureAndSet(`${PREFIX}.data.last_response`, r.text || JSON.stringify(r.json || {}), { role: 'json' });
        setV(`${PREFIX}.data.last_error`, ''); return r.json;
      }
      
      // ---------- Container ops ----------
      async function createContainerIfNeeded() {
        const c = readCfg();
        if (c.container_id) return c.container_id;
        // Scope-Precheck
        const miss = missingScopes(parseScopes(c.scope), ['carstream:api:read']);
        if (miss.length) throw new Error(`Scope fehlt (${miss.join(', ')}). Bitte config.scope ergänzen & Re-Auth.`);
        // Body
        const descriptors = Array.isArray(c.container_descriptors) ? c.container_descriptors.filter(Boolean) : [];
        if (!descriptors.length) throw new Error('config.container_descriptors ist leer oder ungültig (JSON-Array erwartet).');
        const body = { name: c.container_name || 'ChargeStats', purpose: c.container_purpose || 'ioBroker', technicalDescriptors: descriptors };
        const resp = await apiPostJson('/customers/containers', body);
        const newId = resp && resp.containerId;
        if (!newId) throw new Error(`Container konnte nicht erstellt werden: ${JSON.stringify(resp)}`);
        // Persist
        setV(`${PREFIX}.config.container_id`, newId);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.containers.${seg(newId)}.meta`, JSON.stringify(resp, null, 2), { role: 'json' });
        if (c.notify_on_container_events) sendT(`🆕 BMW Container erstellt: *${newId}* (name: ${body.name})`);
        log(`[BMW] Container created: ${newId}`, 'info');
        return newId;
      }
      
      // ---------- Fetchers ----------
      async function fetchBasicData() {
        const c = readCfg();
        if (!c.vin) throw new Error('data.selected_vin not set');
        const json = await apiGet(`/customers/vehicles/${encodeURIComponent(c.vin)}/basicData`);
        const out = JSON.stringify(json, null, 2);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.basicData`, out, { role: 'json' });
        setV(`${PREFIX}.data.last_write_len`, out.length);
        log(`[BMW] basicData OK for ${c.vin}`, 'info');
      }
      
      async function fetchTelematicData() {
        const c = readCfg();
        if (!c.vin) throw new Error('data.selected_vin not set');
      
        let cid = c.container_id;
        if (!cid) {
          if (!c.container_auto_create) throw new Error('config.container_id not set and auto_create disabled.');
          cid = await createContainerIfNeeded(); // sets state too
        }
      
        // Now fetch telematics
        const json = await apiGet(`/customers/vehicles/${encodeURIComponent(c.vin)}/telematicData`, { containerId: cid });
        const out = JSON.stringify(json, null, 2);
        await ensureAndSet(`${PREFIX}.data.${seg(c.vin)}.telematicData.${seg(cid)}`, out, { role: 'json' });
        setV(`${PREFIX}.data.last_write_len`, out.length);
      
        const attrs = (json && json.telematicData && typeof json.telematicData === 'object') ? json.telematicData : {};
        const base = `${PREFIX}.data.${seg(c.vin)}.telemetry.${seg(cid)}`;
        for (const [attr, rec] of Object.entries(attrs)) {
          const key = `${base}.${seg(attr)}`;
          const rawVal = rec && typeof rec === 'object' ? rec.value : rec;
          const asNum = rawVal !== null && rawVal !== undefined && rawVal !== '' && !isNaN(Number(rawVal)) ? Number(rawVal) : null;
          await ensureMetric(key, asNum !== null ? asNum : String(rawVal ?? ''));
          await ensureMetric(`${key}_unit`, rec?.unit ?? '');
          await ensureMetric(`${key}_ts`, rec?.timestamp ?? '');
        }
        log(`[BMW] telematicData OK for ${c.vin} [${cid}] – ${Object.keys(attrs).length} attributes`, 'info');
      }
      
      // ---------- wiring ----------
      on({ id: `${PREFIX}.control.startAuth`, change: 'ne' }, o => { if (!o?.state?.val) return; setV(`${PREFIX}.control.startAuth`, false); startDeviceFlow(); });
      on({ id: `${PREFIX}.control.refreshNow`, change: 'ne' }, o => { if (!o?.state?.val) return; setV(`${PREFIX}.control.refreshNow`, false); refreshToken(); });
      on({ id: `${PREFIX}.control.fetchBasicData`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.fetchBasicData`, false);
        try { await fetchBasicData(); } catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ basicData Fehlgeschlagen: ${e.message}`); }
      });
      on({ id: `${PREFIX}.control.fetchTelematicData`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.fetchTelematicData`, false);
        try { await fetchTelematicData(); } catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_fetch_fail) sendT(`⚠️ telematicData Fehlgeschlagen: ${e.message}`); }
      });
      on({ id: `${PREFIX}.control.createContainer`, change: 'ne' }, async o => {
        if (!o?.state?.val) return; setV(`${PREFIX}.control.createContainer`, false);
        try { const id = await createContainerIfNeeded(); log(`[BMW] Container ready: ${id}`, 'info'); }
        catch (e) { setV(`${PREFIX}.data.last_error`, String(e.message || e)); if (readCfg().notify_on_container_events) sendT(`⚠️ Container-Erstellung fehlgeschlagen: ${e.message}`); }
      });
      
      // ---------- bootstrap ----------
      (function bootstrap() {
        checkConfiguredScopes();
        const rt = val(`${PREFIX}.tokens.refresh_token`);
        if (rt) { refreshToken().catch(e => log(`[BMW] initial refresh failed: ${e.message}`, 'warn')); }
        startTokenWatch();
        setV(`${PREFIX}.auth.message`, 'Flow: startAuth → VIN setzen → (auto) createContainer → fetchTelematicData / fetchBasicData');
      })();
      
      

      Persönliche Daten werden nicht im Code, sondern als States gepflegt.


      3) Konfiguration (States setzen)

      Alle States unter: 0_userdata.0.Auto.BMW

      OAuth / BMW

      • config.client_id → <CLIENT_ID>
      • config.scope → authenticate_user openid cardata:api:read carstream:api:read

      API

      • config.api_base_url → https://api-cardata.bmwgroup.com
      • config.api_version → v1
      • config.accept_language → z. B. de-DE
      • config.refresh_send_scope → true (empfohlen)

      Fahrzeug & Telematik

      • data.selected_vin → <VIN> (z. B. WBY61EF0405W24607)

      • Container

        • config.container_id → leer lassen für Auto-Erstellung oder vorhandene ID eintragen

        • config.container_auto_create → true (Default)

        • config.container_name → ChargeStats (frei wählbar)

        • config.container_purpose → ioBroker (frei wählbar)

        • config.container_descriptors → JSON-Array der Technical Descriptors, z. B.:

          [
            "vehicle.drivetrain.electricEngine.charging.level",
            "vehicle.drivetrain.electricEngine.kombiRemainingElectricRange",
            "vehicle.vehicle.travelledDistance",
            "vehicle.body.chargingPort.status"
          ]
          

      (Optional) Telegram

      • notify.telegram_enabled → true
      • notify.telegram_instance → telegram.0
      • notify.token_warn_minutes → 10
      • notify.on_auth / notify.on_refresh_fail / notify.on_fetch_fail / notify.on_scope_warning / notify.on_container_events → nach Wunsch true

      4) Erst-Authentifizierung (Device Code)

      1. Button control.startAuth auf true.
      2. In auth.* erscheinen user_code und verification_uri(_complete).
      3. Link öffnen, Code eingeben, BMW-Login bestätigen.
      4. auth.status = authorized, Tokens unter tokens.*.

      Wichtig: Der User-Code-Schritt ist nicht automatisierbar (BMW-Policy). Token-Refresh läuft danach automatisch.


      5) Daten abrufen

      A) basicData

      • Button control.fetchBasicData.

      • Ergebnis:

        • JSON: …data.<VIN>.basicData

      B) telematicData (Auto-Container & Abfrage)

      • Falls config.container_id leer und config.container_auto_create=true:
        → Script ruft POST /customers/containers auf, speichert Antwort nach
        …data.<VIN>.containers.<CONTAINER_ID>.meta und trägt config.container_id automatisch ein.

      • Button control.fetchTelematicData.

      • Ergebnisse:

        • Roh-JSON: …data.<VIN>.telematicData.<CONTAINER_ID>

        • Flache Einzel-Datenpunkte (+ _unit, _ts) unter:
          …data.<VIN>.telemetry.<CONTAINER_ID>.<ATTRIBUT>

        • Beispiel:

          • …telemetry.S00W005P177MV.vehicle_drivetrain_electricEngine_charging_level → 61
          • …telemetry.S00W005P177MV.vehicle_drivetrain_electricEngine_kombiRemainingElectricRange → 244

      Scope-Hinweis: Für telematicData ist carstream:api:read erforderlich – sonst 403.


      6) Optional: Automatisierung

      • Per Zeitplan (CRON) oder kleinem Trigger-Skript regelmäßig (max 50 Aufrufe pro TAG!):

        • control.fetchTelematicData (z. B. alle 30 Min)
        • control.fetchBasicData (z. B. 1× täglich)

      Beispiel (einfacher, separater JavaScript-Cron-Trigger):

      schedule('*/10 * * * *', () => setState('0_userdata.0.Auto.BMW.control.fetchTelematicData', true));
      schedule('0 3 * * *', () => setState('0_userdata.0.Auto.BMW.control.fetchBasicData', true));
      

      7) Datenpunkte (Kurzüberblick)

      • auth.* – Status, Code, Verifizierungs-URL, Warnungen

      • tokens.* – access_token, refresh_token, Ablaufzeiten, optional id_token + Claims

      • config.* – Client/Scope/API/Container-Optionen

      • control.* – Buttons (startAuth, refreshNow, fetchBasicData, fetchTelematicData, createContainer)

      • data.*

        • Diagnose: last_url, last_status, last_headers_json, last_response, last_error
        • Fahrzeug: data.<VIN>.basicData
        • Container-Meta: data.<VIN>.containers.<CONTAINER_ID>.meta
        • Telemetrie (Roh): data.<VIN>.telematicData.<CONTAINER_ID>
        • Telemetrie (flach): data.<VIN>.telemetry.<CONTAINER_ID>.* (+ _unit, _ts)

      😎 Fehler & Troubleshooting

      • 403 (CU-403): Scope/Headers/VIN/Container prüfen. Für Telemetrie carstream:api:read muss im Token enthalten sein → ggf. config.scope setzen und neu autorisieren (control.startAuth).
      • 401: Access-Token abgelaufen → Script refresht automatisch; bei Refresh-Fehler Telegram-Hinweis (falls aktiv) und neu autorisieren.
      • Leere DPs: Script nutzt atomare Writes; prüfe data.last_error & data.last_response.
      • Container nicht erstellt: config.container_descriptors muss gültiges JSON-Array sein. Ansonsten control.createContainer drücken und Logs prüfen.

      9) Sicherheit & Datenschutz

      • Tokens nie veröffentlichen (auch nicht gekürzt).
      • Screens/Logs fürs Forum anonymisieren (<CLIENT_ID>, <VIN>, <CONTAINER_ID>).
      • ioBroker-Admin absichern.

      10) Platzhalter (nur in States setzen)

      • <CLIENT_ID> → config.client_id
      • <VIN> → data.selected_vin
      • <CONTAINER_ID> → config.container_id (leer lassen für Auto-Erstellung)

      [Anleitung] BMW Car Streaming via MQTT.

      1) Voraussetzungen

      • Token-Flow existiert & aktualisiert 0_userdata.0.Auto.BMW.tokens.id_token.

      • BMW MQTT Zugangsdaten aus Portal:

        • Host: customer.streaming-cardata.bmwgroup.com
        • Port: (bei Dir z. B.) 9000
        • Username: Deine GCID aus BMW portal unter Cardata Streaming (GUID, z. B. c4beb4…8916)
        • Topic: <username>/<VIN> (z. B. c4beb4…8916/WBY61EF0405W24607)
      • ioBroker JavaScript-Adapter aktiv.

      2) mqtt installieren (einmalig)

      *In der Javascript Instanz wo das Script laufen soll, unter Settings, als zusätzliches NPM Modul mqtt eintragen.

      3) Script anlegen

      • In Admin → Skripte → JavaScript → Neu → Datei z. B. script.js.common.Auto.BMW.Streaming.
      • CFG unten (Host/Port/Username/VIN) auf Deine Werte setzen.

      4) Script (Copy & Paste)

      // File: script.js.common.Auto.BMW.Streaming
      // Public-safe: KEIN Hardcode von username/VIN. Verbindung NUR wenn beide Config-States gesetzt sind.
      
      'use strict';
      
      const mqtt = require('mqtt');
      
      // ===== CONFIG (nicht-personenbezogen) =====
      const CFG = {
        tokenStateId: '0_userdata.0.Auto.BMW.tokens.id_token',
      
        host: 'customer.streaming-cardata.bmwgroup.com',
        port: 9000,
        protocolVersion: 5,
      
        basePath: '0_userdata.0.Auto.BMW.streaming',
      
        clientId:    `iobroker-bmw-${Math.random().toString(16).slice(2)}`,
        keepalive:   30,
        reconnectMs: 4000,
        quotaCooldownMs: 120000,
      
        stallSeconds: 180,
        tickSeconds:  15,
      
        maxDepth: 6,
        maxKeys:  5000
      };
      
      // ===== RUNTIME =====
      const CFG_STATES = {
        username: '0_userdata.0.Auto.BMW.streaming.config.username',
        vin:      '0_userdata.0.Auto.BMW.streaming.config.vin',
      };
      
      let R = { username: '', vin: '' };
      
      let client = null;
      let currentToken = null;
      let tokenChangeTimer = null;
      let cooldownTimer = null;
      let connecting = false;
      let connected  = false;
      let lastRxTs = 0;
      let healthTimer = null;
      
      // ===== Logging =====
      function info(msg, extra) { log(JSON.stringify({ lvl:'info',  msg, ...(extra||{}) })); }
      function warn(msg, extra) { log(JSON.stringify({ lvl:'warn',  msg, ...(extra||{}) })); }
      function err (msg, extra) { log(JSON.stringify({ lvl:'error', msg, ...(extra||{}) })); }
      
      // ===== Safe Adapter-Helpers (createState-only) =====
      const has = (n) => typeof globalThis[n] === 'function';
      async function getObjectAsyncSafe(id) {
        if (has('getObjectAsync')) return await getObjectAsync(id);
        return await new Promise(res => getObject(id, (_e,o)=>res(o)));
      }
      async function createStateAsyncSafe(id, initVal, common) {
        return await new Promise(res => {
          try { createState(id, initVal, common||{}, () => res()); }
          catch { try { createState(id, initVal, true, common||{}, {}, () => res()); } catch { res(); } }
        });
      }
      async function ensureState(id, common, initVal) {
        const o = await getObjectAsyncSafe(id);
        if (!o) {
          const init = initVal !== undefined ? initVal :
            common?.type==='number' ? 0 : common?.type==='boolean' ? false : '';
          await createStateAsyncSafe(id, init, common);
        }
      }
      async function setStateAsyncSafe(id, state) {
        if (has('setStateAsync')) return await setStateAsync(id, state);
        return await new Promise(res => setState(id, state, res));
      }
      
      // ===== Struktur/Helfer =====
      function j(a,b){ return `${a}.${b}`; }
      function configured(){ return !!(R.username && R.vin); }
      function base(){ return `${CFG.basePath}.${R.vin}`; }
      function brokerUrl(){ return `mqtts://${CFG.host}:${CFG.port}`; }
      function strictTopic(){ return `${R.username}/${R.vin}`; }
      
      async function scaffoldPerVin() {
        const b = base();
        await ensureState(j(b,'connected'),       { name:'Connected',              type:'boolean', role:'indicator.connected', read:true,  write:false }, false);
        await ensureState(j(b,'connectedAt'),     { name:'Connected since (ISO)',  type:'string',  role:'date',                read:true,  write:false }, '');
        await ensureState(j(b,'subscribed'),      { name:'Subscribed topics',      type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'msgCount'),        { name:'Message count',          type:'number',  role:'value',               read:true,  write:false }, 0);
        await ensureState(j(b,'lastJson'),        { name:'Last raw JSON',          type:'string',  role:'json',                read:true,  write:false }, '');
        await ensureState(j(b,'lastTs'),          { name:'Last message ts',        type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'lastTopic'),       { name:'Last topic',             type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'lastEventTime'),   { name:'Event time',             type:'string',  role:'date',                read:true,  write:false }, '');
        await ensureState(j(b,'lastEventTs'),     { name:'Event ts',               type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'lastError'),       { name:'Last error',             type:'string',  role:'text',                read:true,  write:false }, '');
        await ensureState(j(b,'metrics.range_km'),{ name:'Remaining range (km)',   type:'number',  role:'value.distance',      read:true,  write:false }, 0);
        await ensureState(j(b,'health.lastRxAgoSec'), { name:'Seconds since last RX', type:'number', role:'value.interval',   read:true,  write:false }, -1);
        await ensureState(j(b,'health.stalled'),  { name:'Stream stalled',         type:'boolean', role:'indicator.problem',   read:true,  write:false }, false);
        await ensureState(j(b,'token.expTs'),     { name:'Token expiry ts',        type:'number',  role:'value.time',          read:true,  write:false }, 0);
        await ensureState(j(b,'token.expiresInSec'),{ name:'Token expires in (s)', type:'number',  role:'value.interval',      read:true,  write:false }, 0);
      }
      
      function decodeJwtExpSeconds(jwt) {
        try {
          const parts = String(jwt).split('.');
          if (parts.length < 2) return 0;
          const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
          return Number(payload?.exp || 0);
        } catch { return 0; }
      }
      
      // ===== Config-States (leer) + Laden =====
      async function ensureConfigStates() {
        await ensureState(CFG_STATES.username, { name:'BMW MQTT Username (GCID)', type:'string', role:'text', read:true, write:true }, '');
        await ensureState(CFG_STATES.vin,      { name:'BMW VIN',                  type:'string', role:'text', read:true, write:true }, '');
      }
      function readCfgState(id) {
        const st = getState(id);
        const v = st && typeof st.val === 'string' ? st.val.trim() : '';
        return v || '';
      }
      async function loadRuntimeFromStates() {
        R.username = readCfgState(CFG_STATES.username);
        R.vin      = readCfgState(CFG_STATES.vin);
        info('Runtime config', { username: R.username ? 'set' : 'EMPTY', vin: R.vin ? 'set' : 'EMPTY' });
      }
      
      // ===== Sauberer Logout =====
      function safeEnd() {
        return new Promise(resolve => {
          if (!client) return resolve();
          try {
            const c = client;
            client = null; connecting = false; connected = false;
            let done = false;
            const finish = async () => {
              if (!done) {
                done = true;
                if (configured()) { try { await setStateAsyncSafe(j(base(),'connected'), { val:false, ack:true }); } catch {} }
                resolve();
              }
            };
            c.once('close', finish);
            c.end(false, {}, finish); // WHY: DISCONNECT senden (Single-Session)
            setTimeout(finish, 1500);
          } catch { resolve(); }
        });
      }
      
      // ===== MQTT =====
      async function connectWithToken(token) {
        if (!configured()) { warn('username/VIN not set -> no connect. Set states under 0_userdata.0.Auto.BMW.streaming.config.*'); return; }
        if (!token) { warn('No id_token available yet'); return; }
        if (connecting || connected) return;
        currentToken = token;
      
        await scaffoldPerVin();
      
        const expSec = decodeJwtExpSeconds(token);
        const nowSec = Math.floor(Date.now()/1000);
        await setStateAsyncSafe(j(base(),'token.expTs'),        { val: expSec*1000, ack:true });
        await setStateAsyncSafe(j(base(),'token.expiresInSec'), { val: Math.max(0, expSec-nowSec), ack:true });
      
        const url = brokerUrl();
        const top = strictTopic();
        connecting = true;
      
        const opts = {
          protocolVersion: CFG.protocolVersion,
          clientId: CFG.clientId,
          username: R.username,
          password: token,
          keepalive: CFG.keepalive,
          reconnectPeriod: CFG.reconnectMs,
          clean: true,
          rejectUnauthorized: true,
          servername: CFG.host
        };
      
        info('BMW MQTT connecting', { url, topic: top });
        client = mqtt.connect(url, opts);
      
        client.on('connect', async (connack) => {
          connected = true; connecting = false; lastRxTs = 0;
          await setStateAsyncSafe(j(base(),'connected'),   { val:true, ack:true });
          await setStateAsyncSafe(j(base(),'connectedAt'), { val:new Date().toISOString(), ack:true });
          client.subscribe(top, { qos: 1 }, async (e, granted) => {
            if (e) { err('Subscribe error', { error: String(e) }); return; }
            await setStateAsyncSafe(j(base(),'subscribed'), { val: granted.map(g=>g.topic).join(', '), ack:true });
            info('Subscribed', { granted });
          });
          startHealthTimer();
        });
      
        client.on('reconnect', () => info('Reconnecting...'));
        client.on('offline',   () => info('Offline'));
        client.on('close',     async () => {
          connected = false; stopHealthTimer();
          try { await setStateAsyncSafe(j(base(),'connected'), { val:false, ack:true }); } catch {}
          info('Connection closed');
        });
        client.on('error', async (e) => {
          err('MQTT error', { error: String(e) });
          try { await setStateAsyncSafe(j(base(),'lastError'), { val:String(e), ack:true }); } catch {}
          if (String(e?.message || '').toLowerCase().includes('quota')) enterQuotaCooldown();
        });
        client.on('disconnect', (packet) => {
          const rc = packet?.reasonCode;
          info('Broker disconnect', { reasonCode: rc });
          if (rc === 151) enterQuotaCooldown(); // Quota exceeded
        });
      
        client.on('message', async (_topic, payload) => {
          const b = base();
          const tsNow = Date.now();
          lastRxTs = tsNow;
      
          info('RX', { topic: String(_topic||''), bytes: payload?.length ?? 0 });
      
          await setStateAsyncSafe(j(b,'lastJson'),  { val: payload ? payload.toString('utf8') : '', ack:true });
          await setStateAsyncSafe(j(b,'lastTs'),    { val: tsNow, ack:true });
          await setStateAsyncSafe(j(b,'lastTopic'), { val: String(_topic||''), ack:true });
          const cur = getState(j(b,'msgCount'));
          const nextCnt = (cur && typeof cur.val === 'number' ? cur.val : 0) + 1;
          await setStateAsyncSafe(j(b,'msgCount'), { val: nextCnt, ack:true });
      
          try {
            const obj = JSON.parse(payload ? payload.toString('utf8') : '{}');
      
            const evIso = obj?.timestamp || obj?.time || '';
            const evTs  = evIso ? Date.parse(evIso) || tsNow : tsNow;
            await setStateAsyncSafe(j(b,'lastEventTime'), { val: evIso || new Date(evTs).toISOString(), ack:true });
            await setStateAsyncSafe(j(b,'lastEventTs'),   { val: evTs, ack:true });
      
            const data = obj?.data && typeof obj.data === 'object' ? obj.data : null;
            if (data) {
              for (const dottedKey of Object.keys(data)) {
                const rec = data[dottedKey];
                if (!rec || typeof rec !== 'object') continue;
                const root = `${b}.fields.${dottedKey}`;
                const v = rec.value;
                const vType = (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') ? typeof v : 'string';
      
                await ensureState(`${root}.value`,     { name:`${dottedKey} value`,     type:vType,    role:'value', read:true, write:false }, vType==='number'?0:(vType==='boolean'?false:''));
                await setStateAsyncSafe(`${root}.value`, { val: v, ack:true });
      
                if (rec.unit != null) {
                  await ensureState(`${root}.unit`,    { name:`${dottedKey} unit`,      type:'string', role:'text',  read:true, write:false }, '');
                  await setStateAsyncSafe(`${root}.unit`, { val: String(rec.unit), ack:true });
                }
                if (rec.timestamp != null) {
                  // FIX: entferntes falsches Apostroph im Pfad
                  await ensureState(`${root}.timestamp`, { name:`${dottedKey} time`,   type:'string', role:'date',  read:true, write:false }, '');
                  await setStateAsyncSafe(`${root}.timestamp`, { val: String(rec.timestamp), ack:true });
                }
      
                if (dottedKey === 'vehicle.drivetrain.lastRemainingRange') {
                  await ensureState(j(b,'metrics.range_km'), { name:'Remaining range (km)', type:'number', role:'value.distance', read:true, write:false }, 0);
                  const num = typeof v === 'number' ? v : parseFloat(v);
                  if (!Number.isNaN(num)) await setStateAsyncSafe(j(b,'metrics.range_km'), { val: num, ack:true });
                }
              }
            }
          } catch (e) {
            try { await setStateAsyncSafe(j(base(),'lastError'), { val:`JSON parse failed: ${String(e)}`, ack:true }); } catch {}
          }
        });
      }
      
      // ===== Health =====
      function startHealthTimer() {
        stopHealthTimer();
        healthTimer = setInterval(async () => {
          if (!configured()) return;
          const now = Date.now();
          const ageSec = lastRxTs ? Math.max(0, Math.round((now - lastRxTs)/1000)) : -1;
          await setStateAsyncSafe(j(base(),'health.lastRxAgoSec'), { val: ageSec, ack:true });
          const stalled = ageSec >= 0 && ageSec > CFG.stallSeconds;
          await setStateAsyncSafe(j(base(),'health.stalled'), { val: !!stalled, ack:true });
          if (stalled) {
            warn(`No data for ${ageSec}s -> reconnecting`);
            await safeEnd();
            const st = getState(CFG.tokenStateId);
            const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
            if (tok) connectWithToken(tok);
          }
        }, CFG.tickSeconds * 1000);
      }
      function stopHealthTimer() { if (healthTimer) { clearInterval(healthTimer); healthTimer = null; } }
      
      // ===== Quota =====
      async function enterQuotaCooldown() {
        if (cooldownTimer) return;
        warn(`Quota exceeded -> cooling down for ${CFG.quotaCooldownMs} ms`);
        await safeEnd();
        cooldownTimer = setTimeout(async () => {
          cooldownTimer = null;
          if (!configured()) return;
          const st = getState(CFG.tokenStateId);
          const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
          if (tok) connectWithToken(tok);
        }, CFG.quotaCooldownMs);
      }
      
      // ===== Reconnects =====
      function scheduleReconnect(newToken) {
        if (tokenChangeTimer) clearTimeout(tokenChangeTimer);
        tokenChangeTimer = setTimeout(async () => {
          await safeEnd();
          connectWithToken(newToken);
        }, 800);
      }
      
      // ===== Reconfigure bei Username/VIN-Änderung =====
      async function reconfigureAndReconnect() {
        await safeEnd();
        await loadRuntimeFromStates();
        if (!configured()) { warn('username/VIN not set -> idle.'); return; }
        await scaffoldPerVin();
        const st = getState(CFG.tokenStateId);
        const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
        if (tok) await connectWithToken(tok);
      }
      
      // ===== Boot =====
      (async function start() {
        await ensureConfigStates();
        await loadRuntimeFromStates();
      
        if (configured()) await scaffoldPerVin();
        else warn('Bitte zuerst setzen: 0_userdata.0.Auto.BMW.streaming.config.username und .vin');
      
        const st = getState(CFG.tokenStateId);
        const tok = st && typeof st.val === 'string' ? st.val.trim() : '';
        if (tok) await connectWithToken(tok);
        else warn(`Token state empty: ${CFG.tokenStateId}`);
      
        on({ id: CFG.tokenStateId, change: 'ne' }, (obj) => {
          const newTok = obj?.state?.val ? String(obj.state.val).trim() : '';
          if (!newTok || newTok === currentToken) return;
          info('id_token changed, scheduling graceful reconnect');
          scheduleReconnect(newTok);
        });
      
        on({ id: CFG_STATES.username, change: 'ne' }, () => { info('username changed -> reconfigure'); reconfigureAndReconnect(); });
        on({ id: CFG_STATES.vin,      change: 'ne' }, () => { info('vin changed -> reconfigure');      reconfigureAndReconnect(); });
      
        onStop(async (cb) => {
          stopHealthTimer();
          await safeEnd();
          if (tokenChangeTimer) clearTimeout(tokenChangeTimer);
          if (cooldownTimer)    clearTimeout(cooldownTimer);
          info('BMW MQTT stream script stopped');
          cb();
        }, 3000);
      })().catch(e => err('Startup failed', { error: String(e) }));
      
      

      5) Start & prüfen

      • Daten setzen:

        • username: '0_userdata.0.Auto.BMW.streaming.config.username'
        • vin: '0_userdata.0.Auto.BMW.streaming.config.vin',
      • Log:

        • Connected to BMW MQTT
        • Subscribed → Topic sollte <username>/<VIN> anzeigen.
        • Bei Daten: RX { topic: ..., bytes: N }
      • Objects → 0_userdata.0.Auto.BMW.streaming.<VIN>:

        • connected = true, subscribed = <username>/<VIN>
        • Bei Daten: lastJson, lastEventTime, fields.*, metrics.range_km
        • health.lastRxAgoSec ≈ 0–15, steigt laufend ohne Daten.

      Troubleshooting (kurz)

      • Subscribe error: Not authorized → falscher Topic-Filter. Nur <username>/<VIN> abonnieren (keine Wildcards).
      • invalid password im lokalen mqtt.x-Adapter → Du verbindest versehentlich zu Deinem lokalen Broker. Stelle sicher, dass die URL mqtts://customer.streaming-cardata.bmwgroup.com:9000 ist.
      • Keine Daten, aber connected → oft Portal-Thema (VIN/Descriptors/Region). Teste Fahrten, Start/Stop Laden, Klima.
      • Quota exceeded → Script wartet automatisch (quotaCooldownMs) und verbindet dann wieder.
      • Einzel-Session → Script macht sauberen DISCONNECT bei Stop/Re-Auth. Keine Doppelverbindungen.

      HINWEIS

      Wenn ihr MQTT Streaming verwendet, braucht ihr TelematicData überhaupt nicht abrufen. Alle im BMW portal gewählten Daten kommen dann automatisch über den Stream, allerdings nur bei Änderung. Steht das Auto still und nichts verändert sich, kommen keine Daten.
      Die API selber läuft auch nicht 100% sauber, der BMW support schreibt selbst: "aktuell haben wir Probleme mit unseren CarData Customer Client / API / Streaming Lösungen.
      Unsere Entwickler arbeiten mit Hochdruck an der Beseitigung der Störung - zusammen mit den Partnersystemen."
      Nichtdestotrotz, bei mir läuft es aktuell.

      posted in Tester
      T
      tippy88
    • RE: Test Adapter BMW/Mini v4.x.x

      Ich habe mit Code Copilot zwei Scripte gebaut, damit funktioniert die BMW Integration.

      1. Verbindung zu BMW CarData und auto-refresh der Tokens vor Ablauf. Kann ebenso Daten über die CarAPI abrufen welche auf 50 Calls pro Tag begrenzt ist.
      2. Script baut eine MQTT Verbindung zum Car Streaming auf. refreshed die Verbindung wenn der ID_Token nach 1h abläuft. Schreibt alle Daten in separate DP.

      Zusätzliche binde ich die CarStream daten an die entsprechende DP des BMW adapters. Dies ist nur nötig bis EVCC auf den Carstream umgestellt hat, aber so bekommt EVCC inzwischen die Live Daten.

      Der CarStream liefert tatsächlich annährend Live Daten. Auf der Fahr in die Arbeit heute morgen wurde mindestens jede Minute der SOC, restkiliometer etc aktualisert.

      Ich kann heute Abend nach der Arbeit gerne die Scripte teilen und versuchen eine Kurze Anleitung dazu zu schreiben.

      posted in Tester
      T
      tippy88
    • RE: Test Adapter VW Connect für VW, ID, Audi, Seat, Skoda

      Servus zusammen,

      Mit welchen Einstellungen bekommt man den Cupra Born verbunden?
      Ich habe Typ Seat Cupra und die Alternative probiert, ohne Erfolg.

      Gruß,
      Tobi

      2025-03-15 20:03:46.834 - info: vw-connect.0 (368631) Login in with seatcupra
      2025-03-15 20:03:47.098 - debug: vw-connect.0 (368631) parseEmailForm
      2025-03-15 20:03:47.224 - debug: vw-connect.0 (368631) emailPasswordForm2
      2025-03-15 20:03:47.461 - debug: vw-connect.0 (368631) ""
      2025-03-15 20:03:47.462 - debug: vw-connect.0 (368631) {"date":"Sat, 15 Mar 2025 19:03:47 GMT","content-length":"0","connection":"keep-alive","apigw-requestid":"He1LEg7PliAEJyQ=","x-content-type-options":"nosniff","x-xss-protection":"0","cache-control":"no-cache, no-store, max-age=0, must-revalidate","pragma":"no-cache","expires":"0","strict-transport-security":"max-age=31536000; includeSubDomains; preload","x-frame-options":"DENY","location":"https://identity.vwgroup.io/oidc/v1/oauth/sso?clientId=3c756d46-f1ba-4d78-9f9a-cff0d5292d51@apps_vw-dilab_com&relayState=7d36211df11215fc80ff9dde4995b72eb04dd8aa&userId=640bb7cdee98cbf7ed283","content-language":"en-US"}
      2025-03-15 20:03:47.727 - debug: vw-connect.0 (368631) Error: Invalid protocol: cupra:
      2025-03-15 20:03:47.800 - info: vw-connect.0 (368631) Login successful
      2025-03-15 20:03:47.970 - error: vw-connect.0 (368631) {"timestamp":"2025-03-15T19:03:47.958942578Z","path":"/v1/users/a5ae66df-76d8-43e9-bc7a-1298db4f586d/garage/vehicles","status":404,"error":"Not Found","message":"No static resource v1/users/ae9-bc7a-1298db4f586d/garage/vehicles.","requestId":"53b38186"}
      2025-03-15 20:03:47.971 - error: vw-connect.0 (368631) 404
      2025-03-15 20:03:47.971 - error: vw-connect.0 (368631) Get Vehicles Failed
      
      posted in Tester
      T
      tippy88
    • RE: Stiebel-ISG - Modbus

      @android51 said in Stiebel-ISG - Modbus:

      Zudem habe ich irgendwo gelesen, dass der Wert ziemlich "geschönt" ist und nicht der tatsächlichen Stromaufnahme entspricht.

      Das Problem ist auch die Rundung, der Wert wird nur mit einer Stelle nach dem Komma angegeben. 0,999 wird halt 0,9 im Modbus
      Ich habe bei mir ein Offset von +100W angelegt. Das kommt ganz gut hin wenn ich Nachts den Ruheverbrauch + Heizung zusammenrechne passt das.

      posted in ioBroker Allgemein
      T
      tippy88
    • RE: Modbus & Fronius GEN24

      UPDATE:

      Ich glaube die Lösung zu haben...Ich weiß nicht ob das neu ist in der FW 1.34 da ich es nie mit älterer FW probiert habe, aber die Steuerung der Ladeleistung funktioniert nur wenn im Battery Management diese Option aktiv ist (Standard ist aus):

      92671882-7ced-43d4-ba30-9f69e27078c9-image.png

      Ohne diese Einstellung läd der Akku nur mit 500W, egal was per Modbus vorgegeben wird.

      posted in ioBroker Allgemein
      T
      tippy88
    • RE: Modbus & Fronius GEN24

      Servus zusammen,

      ich hoffe hier kann mir jemand helfen. Ich habe im iobroker meinen GEN24 laut Anleitung von hier im Modbus eingebunden.
      Auslesen funktioniert auch soweit alles.

      Ich möchte nun gerne die Speicher Ladung/Entladung steuern. Leider funktioniert das nicht wie gewünscht.
      Ich bekomme zwar den Speicher in die "Zwangsladung", aber meine eingegebenen Werte zur maximalen Lade/Entladeleistung werden nicht berücksichtigt.
      Die Ladung erfolgt immer mit ~500W, egal was ich bei 40355_OutWRte und 40356_InWRte eintrage.

      Ich habe ein HVS 7.7, 40345_WChaMax = 7680 W

      Laut Fronius Docu:
      *Beispiel 5: Laden im Bereich von 50% bis 75% der nominalen Leistung
      Dieses Verhalten kann durch Limitierung der maximalen Ladeleistung auf 75%
      und Limitierung der maximalen Entladeleistung auf -50% erreicht werden
      => resultiert in Fenster [1650 W, 2475 W]

      • InWRte = 75% (setze Ladelimit auf 75% von WchaMax)
      • OutWRte = -50% (setze Entladelimit auf -50% von WchaMax)
      • StorCtl_Mod = 3 (schalte beide Grenzwerte aktiv, Bit-Muster: 11)
      • Der Batteriestatus in Fronius Solar.web wechselt zu „Erzwungene Nachladung“*

      Ich setzte also
      InWRte = 7500 (wegen SF=-2)
      OutWRte = -5000 (wegen SF=-2)
      StorCtl_Mod = 3

      Trotzdem läd der Speicher nur mit 500-700W. Wie kann ich ihn zwingen schneller zu laden?

      VG
      Tobias

      7dd747b8-5005-48eb-a880-6ba608638ee1-image.png
      fdd6fa69-c31a-48b3-90c6-424e586b8169-image.png

      posted in ioBroker Allgemein
      T
      tippy88
    • RE: Stiebel-ISG - Modbus

      @unltdnetworx said in Stiebel-ISG - Modbus:

      Also ich beschalte nur den Eingang 1 und bei mir springen die Werte nur zwischen Betriebsstatus 2 und 3 hin und her.

      Ich auch nur.

      @unltdnetworx said in Stiebel-ISG - Modbus:

      Hast du vllt. irgendein Skript am Laufen? Weil es ja unterschiedlich lange dauert, bis der Betriebsstatus wechselt.

      Nur das Skript zum Überschuss heizen. Wenn die Änderung durch ein Skript getriggert wird, dann würde ich dass ja in der histroy von den Eingängen 1 oder 2 sehen, aber da gab es lediglich eine einzige Änderung als ich zum ersten Mal Eingang 1 geschalten und abgeschalten habe.

      Hier, letzte Änderung der SG-Ready Eingänge vor 2 Tagen

      a6dda84a-92b8-4cb0-a0c4-dd56d8927673-image.png

      Allein heute hat sich der Betriebszustand schon dutzende Male geändert zwischen 1 und 2, aber die 1 bleibt immer nur für 1s drin, verstehe ich nicht 🤷‍♂️

      ec4fc05c-a83b-40f2-adf9-7226d8fa2c4b-image.png

      Heizung läuft ansonsten komplett normal, von diesen Wechseln merkt man nichts, sie schaltet nicht aus oder so...

      posted in ioBroker Allgemein
      T
      tippy88
    Community
    Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen
    The ioBroker Community 2014-2023
    logo