Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

  1. ioBroker Community Home
  2. Deutsch
  3. Tester
  4. Test Adapter BMW/Mini v4.x.x

NEWS

  • UPDATE 31.10.: Amazon Alexa - ioBroker Skill läuft aus ?
    apollon77A
    apollon77
    48
    3
    8.3k

  • Monatsrückblick – September 2025
    BluefoxB
    Bluefox
    13
    1
    1.9k

  • Neues Video "KI im Smart Home" - ioBroker plus n8n
    BluefoxB
    Bluefox
    15
    1
    2.3k

Test Adapter BMW/Mini v4.x.x

Geplant Angeheftet Gesperrt Verschoben Tester
997 Beiträge 95 Kommentatoren 266.9k Aufrufe 85 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Marc BergM Marc Berg

    BMW hat "den Stecker gezogen", wie bereits im Juli angekündigt

    https://community.simon42.com/t/bmw-stoppt-home-assistant-integration-api-zugriff/61084

    0d8d39ac-1ce2-4d3b-99a0-a54808112030-495246811-b9432bc7-1552-4b49-85dc-07317a305eab.png

    Steffe.S.S Offline
    Steffe.S.S Offline
    Steffe.S.
    schrieb am zuletzt editiert von
    #681

    @marc-berg

    bist Du denn auch geblockt und ist es als ein allgemeine Problem zu betrachten?

    Marc BergM 1 Antwort Letzte Antwort
    0
    • Steffe.S.S Steffe.S.

      @marc-berg

      bist Du denn auch geblockt und ist es als ein allgemeine Problem zu betrachten?

      Marc BergM Offline
      Marc BergM Offline
      Marc Berg
      Most Active
      schrieb am zuletzt editiert von
      #682

      @steffe-s sagte in Test Adapter BMW/Mini v2.0.0:

      bist Du denn auch geblockt

      yep

      bmw.0	2025-09-30 06:53:07.416	error	"Blocked"
      

      NUC10I3+Ubuntu+Docker+ioBroker+influxDB2+Node Red+RabbitMQ+Grafana

      Pi-hole, Traefik, Checkmk, Conbee II+Zigbee2MQTT, ESPSomfy-RTS, LoRaWAN, Arduino, KiCad

      Benutzt das Voting im Beitrag, wenn er euch geholfen hat.

      lobomauL 1 Antwort Letzte Antwort
      0
      • Marc BergM Marc Berg

        @steffe-s sagte in Test Adapter BMW/Mini v2.0.0:

        bist Du denn auch geblockt

        yep

        bmw.0	2025-09-30 06:53:07.416	error	"Blocked"
        
        lobomauL Offline
        lobomauL Offline
        lobomau
        schrieb am zuletzt editiert von
        #683

        @marc-berg ich habe heute mal wieder gestartet und bin auch direkt geblockt.
        Ich werde auch von 3.0.0. auf 3.0.1. gehen.
        Dann habe ich erst die Checkboxen gesehen. Die waren immer aktiv. Habe die nun abgewählt:

        172eacb1-f178-462b-ab83-8a98f004f5fb-{16BE67D3-3D72-4D61-9CF4-A232682214D0}.png

        Host: NUC8i3 mit Proxmox:

        • ioBroker CT Debian 13, npm 10.9.3, nodejs 22.20.0
        • Slave: Pi4
        Marc BergM T 2 Antworten Letzte Antwort
        0
        • lobomauL lobomau

          @marc-berg ich habe heute mal wieder gestartet und bin auch direkt geblockt.
          Ich werde auch von 3.0.0. auf 3.0.1. gehen.
          Dann habe ich erst die Checkboxen gesehen. Die waren immer aktiv. Habe die nun abgewählt:

          172eacb1-f178-462b-ab83-8a98f004f5fb-{16BE67D3-3D72-4D61-9CF4-A232682214D0}.png

          Marc BergM Offline
          Marc BergM Offline
          Marc Berg
          Most Active
          schrieb am zuletzt editiert von Marc Berg
          #684

          @lobomau sagte in Test Adapter BMW/Mini v2.0.0:

          Habe die nun abgewählt

          Ich hatte die ganze letzte Zeit das Intervall auf 45min gestellt und die beiden Checkboxen abgewählt, also sehr datensparend. Der Fehler hat nichts mehr mit der Quota zu tun.

          NUC10I3+Ubuntu+Docker+ioBroker+influxDB2+Node Red+RabbitMQ+Grafana

          Pi-hole, Traefik, Checkmk, Conbee II+Zigbee2MQTT, ESPSomfy-RTS, LoRaWAN, Arduino, KiCad

          Benutzt das Voting im Beitrag, wenn er euch geholfen hat.

          1 Antwort Letzte Antwort
          1
          • bahnuhrB Online
            bahnuhrB Online
            bahnuhr
            Forum Testing Most Active
            schrieb am zuletzt editiert von
            #685

            habe auf 3.0.1 upgedatet.

            Fehler ist noch da:

            bmw.0
            2025-09-30 10:00:41.172	warn	Rate limit exceeded, please wait 5 minutes
            
            bmw.0
            2025-09-30 10:00:41.172	error	{"statusCode":403,"message":"Out of call volume quota. Quota will be replenished in 00:59:19."}
            
            bmw.0
            2025-09-30 10:00:41.172	error	AxiosError: Request failed with status code 403
            
            bmw.0
            2025-09-30 10:00:41.171	error	getvehicles v2 failed
            

            Wenn ich helfen konnte, dann Daumen hoch (Pfeil nach oben)!
            Danke.
            gute Forenbeiträge: https://forum.iobroker.net/topic/51555/hinweise-f%C3%BCr-gute-forenbeitr%C3%A4ge
            ScreenToGif :https://www.screentogif.com/downloads.html

            1 Antwort Letzte Antwort
            1
            • Steffe.S.S Offline
              Steffe.S.S Offline
              Steffe.S.
              schrieb am zuletzt editiert von Steffe.S.
              #686

              Vielleicht hilft Euch das hier ja:

              Es gibt ein Issues bei EVCC zur BMW API, hier wird darauf hingewiesen, dass MQTT funktionieren soll.

              BMW/Mini new cloud access restrictions #23952

              M 1 Antwort Letzte Antwort
              1
              • Steffe.S.S Steffe.S.

                Vielleicht hilft Euch das hier ja:

                Es gibt ein Issues bei EVCC zur BMW API, hier wird darauf hingewiesen, dass MQTT funktionieren soll.

                BMW/Mini new cloud access restrictions #23952

                M Offline
                M Offline
                McLane
                schrieb am zuletzt editiert von McLane
                #687

                Habe gerade meine AAIDrive-App getestet, diese Verbindet sich doch auch über die API, oder sehe ich das falsch? Bin jedenfalls froh, dass diese Klasse App noch funktioniert. Diese ist Open-Source, notfalls ändere ich den Code ein wenig, und sende mir die gewünschten Daten via Firebase an den iobroker. Denke es gibt aber Hoffnung für den BMW-Adapter.
                https://github.com/BimmerGestalt/AAIdrive

                Marc BergM 1 Antwort Letzte Antwort
                0
                • M McLane

                  Habe gerade meine AAIDrive-App getestet, diese Verbindet sich doch auch über die API, oder sehe ich das falsch? Bin jedenfalls froh, dass diese Klasse App noch funktioniert. Diese ist Open-Source, notfalls ändere ich den Code ein wenig, und sende mir die gewünschten Daten via Firebase an den iobroker. Denke es gibt aber Hoffnung für den BMW-Adapter.
                  https://github.com/BimmerGestalt/AAIdrive

                  Marc BergM Offline
                  Marc BergM Offline
                  Marc Berg
                  Most Active
                  schrieb am zuletzt editiert von
                  #688

                  @mclane sagte in Test Adapter BMW/Mini v2.0.0:

                  diese Verbindet sich doch auch über die API, oder sehe ich das falsch?

                  Nein, die AAIDrive App kommuniziert direkt mit der BMW App auf dem gleichen Smartphone und nutzt nicht die BMW ConnectedDrive API.

                  NUC10I3+Ubuntu+Docker+ioBroker+influxDB2+Node Red+RabbitMQ+Grafana

                  Pi-hole, Traefik, Checkmk, Conbee II+Zigbee2MQTT, ESPSomfy-RTS, LoRaWAN, Arduino, KiCad

                  Benutzt das Voting im Beitrag, wenn er euch geholfen hat.

                  1 Antwort Letzte Antwort
                  0
                  • lobomauL lobomau

                    @marc-berg ich habe heute mal wieder gestartet und bin auch direkt geblockt.
                    Ich werde auch von 3.0.0. auf 3.0.1. gehen.
                    Dann habe ich erst die Checkboxen gesehen. Die waren immer aktiv. Habe die nun abgewählt:

                    172eacb1-f178-462b-ab83-8a98f004f5fb-{16BE67D3-3D72-4D61-9CF4-A232682214D0}.png

                    T Offline
                    T Offline
                    tombox
                    schrieb am zuletzt editiert von
                    #689

                    @lobomau GitHub version testen

                    S 1 Antwort Letzte Antwort
                    0
                    • T tombox

                      @lobomau GitHub version testen

                      S Nicht stören
                      S Nicht stören
                      stenmic
                      schrieb am zuletzt editiert von
                      #690

                      @tombox hab eben die neuste Version von git getestet. Funktioniert nicht mehr.
                      Jemand müsste den Adapter auf BMW CarData umbauen.

                      lobomauL 1 Antwort Letzte Antwort
                      1
                      • S stenmic

                        @tombox hab eben die neuste Version von git getestet. Funktioniert nicht mehr.
                        Jemand müsste den Adapter auf BMW CarData umbauen.

                        lobomauL Offline
                        lobomauL Offline
                        lobomau
                        schrieb am zuletzt editiert von
                        #691

                        @stenmic @tombox nichts zu machen. Habe auch aktualisiert. Und auch

                        Session found. If the login fails please delete bmw.0.auth.session and restart the adapter
                        

                        beachtet. Aber es bleibt dabei:

                        
                        bmw.0
                        2025-09-30 15:55:23.291	info	First update of Trips and Demands in 10 minutes
                        
                        bmw.0
                        2025-09-30 15:55:23.290	info	Initial first update of the vehicles
                        
                        bmw.0
                        2025-09-30 15:55:13.289	info	Adapter will retry in 3 minutes to get vehicles
                        
                        bmw.0
                        2025-09-30 15:55:13.289	error	"Blocked"
                        
                        bmw.0
                        2025-09-30 15:55:13.289	error	AxiosError: Request failed with status code 489
                        
                        bmw.0
                        2025-09-30 15:55:13.288	error	getvehicles v2 failed
                        
                        bmw.0
                        2025-09-30 15:55:13.160	info	Start getting bmw vehicles
                        
                        bmw.0
                        2025-09-30 15:55:12.387	info	starting. Version 3.0.1 (non-npm: TA2k/ioBroker.bmw) in /opt/iobroker/node_modules/iobroker.bmw, node: v22.20.0, js-controller: 7.0.7
                        
                        host.ioBDebian13
                        2025-09-30 15:55:10.866	info	instance system.adapter.bmw.0 in version "3.0.1" (non-npm: TA2k/ioBroker.bmw) started with pid 29808
                        
                        host.ioBDebian13
                        2025-09-30 15:55:07.283	info	instance system.adapter.bmw.0 terminated with code 11 (ADAPTER_REQUESTED_TERMINATION)
                        

                        Host: NUC8i3 mit Proxmox:

                        • ioBroker CT Debian 13, npm 10.9.3, nodejs 22.20.0
                        • Slave: Pi4
                        T 2 Antworten Letzte Antwort
                        0
                        • lobomauL lobomau

                          @stenmic @tombox nichts zu machen. Habe auch aktualisiert. Und auch

                          Session found. If the login fails please delete bmw.0.auth.session and restart the adapter
                          

                          beachtet. Aber es bleibt dabei:

                          
                          bmw.0
                          2025-09-30 15:55:23.291	info	First update of Trips and Demands in 10 minutes
                          
                          bmw.0
                          2025-09-30 15:55:23.290	info	Initial first update of the vehicles
                          
                          bmw.0
                          2025-09-30 15:55:13.289	info	Adapter will retry in 3 minutes to get vehicles
                          
                          bmw.0
                          2025-09-30 15:55:13.289	error	"Blocked"
                          
                          bmw.0
                          2025-09-30 15:55:13.289	error	AxiosError: Request failed with status code 489
                          
                          bmw.0
                          2025-09-30 15:55:13.288	error	getvehicles v2 failed
                          
                          bmw.0
                          2025-09-30 15:55:13.160	info	Start getting bmw vehicles
                          
                          bmw.0
                          2025-09-30 15:55:12.387	info	starting. Version 3.0.1 (non-npm: TA2k/ioBroker.bmw) in /opt/iobroker/node_modules/iobroker.bmw, node: v22.20.0, js-controller: 7.0.7
                          
                          host.ioBDebian13
                          2025-09-30 15:55:10.866	info	instance system.adapter.bmw.0 in version "3.0.1" (non-npm: TA2k/ioBroker.bmw) started with pid 29808
                          
                          host.ioBDebian13
                          2025-09-30 15:55:07.283	info	instance system.adapter.bmw.0 terminated with code 11 (ADAPTER_REQUESTED_TERMINATION)
                          
                          T Offline
                          T Offline
                          tombox
                          schrieb am zuletzt editiert von tombox
                          #692

                          @lobomau Ok ja bmw hat ihre App API mit approov abgesichert. Das heißt die API kann nur noch von einem echten Android oder iPhone abgesendet werden.
                          https://approov.io/customer/bmw/

                          Es bleibt wirklich nur wie von BMW angekündigt der weg über CarData

                          Aber dieser ist begrenzt auf ready only und damit gibt es auch noch Probleme.
                          Mir fehlt aber gerade die Zeit um das umzusetzen. Ich probiere mal die Tage ob KI das die Tage schnell runterprogrammieren kann

                          Besser wäre es aber auf HA zu wechseln
                          https://github.com/JjyKsi/bmw-cardata-ha

                          https://labs.bmw.com/services/cba15ad5-ed53-4ad0-b8a4-6774b477eaf8

                          bahnuhrB 1 Antwort Letzte Antwort
                          1
                          • T tombox

                            @lobomau Ok ja bmw hat ihre App API mit approov abgesichert. Das heißt die API kann nur noch von einem echten Android oder iPhone abgesendet werden.
                            https://approov.io/customer/bmw/

                            Es bleibt wirklich nur wie von BMW angekündigt der weg über CarData

                            Aber dieser ist begrenzt auf ready only und damit gibt es auch noch Probleme.
                            Mir fehlt aber gerade die Zeit um das umzusetzen. Ich probiere mal die Tage ob KI das die Tage schnell runterprogrammieren kann

                            Besser wäre es aber auf HA zu wechseln
                            https://github.com/JjyKsi/bmw-cardata-ha

                            https://labs.bmw.com/services/cba15ad5-ed53-4ad0-b8a4-6774b477eaf8

                            bahnuhrB Online
                            bahnuhrB Online
                            bahnuhr
                            Forum Testing Most Active
                            schrieb am zuletzt editiert von
                            #693

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

                            Besser wäre es aber auf HA zu wechseln

                            echt jetzt !

                            Wir verweisen hier bewusst auf HA ?


                            Wenn ich helfen konnte, dann Daumen hoch (Pfeil nach oben)!
                            Danke.
                            gute Forenbeiträge: https://forum.iobroker.net/topic/51555/hinweise-f%C3%BCr-gute-forenbeitr%C3%A4ge
                            ScreenToGif :https://www.screentogif.com/downloads.html

                            S W 2 Antworten Letzte Antwort
                            0
                            • bahnuhrB bahnuhr

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

                              Besser wäre es aber auf HA zu wechseln

                              echt jetzt !

                              Wir verweisen hier bewusst auf HA ?

                              S Nicht stören
                              S Nicht stören
                              stenmic
                              schrieb am zuletzt editiert von
                              #694

                              @bahnuhr das dachte ich leider auch.
                              Am Ende nutze ich wahrscheinlich nur noch die VIS (1)

                              bahnuhrB 1 Antwort Letzte Antwort
                              0
                              • S stenmic

                                @bahnuhr das dachte ich leider auch.
                                Am Ende nutze ich wahrscheinlich nur noch die VIS (1)

                                bahnuhrB Online
                                bahnuhrB Online
                                bahnuhr
                                Forum Testing Most Active
                                schrieb am zuletzt editiert von
                                #695

                                @stenmic sagte in Test Adapter BMW/Mini v2.0.0:

                                VIS (1)

                                Na klar, was sonst.

                                Vis 2 hat noch zu viele Fehler.
                                Und, ich müsste alles neu machen.


                                Wenn ich helfen konnte, dann Daumen hoch (Pfeil nach oben)!
                                Danke.
                                gute Forenbeiträge: https://forum.iobroker.net/topic/51555/hinweise-f%C3%BCr-gute-forenbeitr%C3%A4ge
                                ScreenToGif :https://www.screentogif.com/downloads.html

                                1 Antwort Letzte Antwort
                                1
                                • T Offline
                                  T Offline
                                  tippy88
                                  schrieb am zuletzt editiert von
                                  #696

                                  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.

                                  J 1 Antwort Letzte Antwort
                                  4
                                  • T tippy88

                                    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.

                                    J Offline
                                    J Offline
                                    jrichl
                                    schrieb am zuletzt editiert von
                                    #697

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

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

                                    Das wäre ein Traum, ich leide auch darunter, dass BMW hier wenig kundenfreundlich agiert hat.

                                    1 Antwort Letzte Antwort
                                    0
                                    • T Offline
                                      T Offline
                                      tippy88
                                      schrieb am zuletzt editiert von tippy88
                                      #698

                                      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)

                                      8) 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.

                                      1 Antwort Letzte Antwort
                                      2
                                      • bahnuhrB bahnuhr

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

                                        Besser wäre es aber auf HA zu wechseln

                                        echt jetzt !

                                        Wir verweisen hier bewusst auf HA ?

                                        W Offline
                                        W Offline
                                        warp735
                                        schrieb am zuletzt editiert von
                                        #699

                                        @bahnuhr sagte in Test Adapter BMW/Mini v2.0.0:

                                        echt jetzt !

                                        Was erwartest denn? Ich bekomm seit gefühlt zwei Jahren kaum noch Adapterupdates und immer mehr funktioniert nicht mehr. Dann kommen noch so Userunfreundliche Dinge wie VIS 1 auf 2 dazu.

                                        Diese Bild sagt eigentlich alles... Schade, ich nutze iob gern, aber irgendwann wird man wechseln "müssen"

                                        01-10-2025_10-48-23.png

                                        1 Antwort Letzte Antwort
                                        0
                                        • lobomauL lobomau

                                          @stenmic @tombox nichts zu machen. Habe auch aktualisiert. Und auch

                                          Session found. If the login fails please delete bmw.0.auth.session and restart the adapter
                                          

                                          beachtet. Aber es bleibt dabei:

                                          
                                          bmw.0
                                          2025-09-30 15:55:23.291	info	First update of Trips and Demands in 10 minutes
                                          
                                          bmw.0
                                          2025-09-30 15:55:23.290	info	Initial first update of the vehicles
                                          
                                          bmw.0
                                          2025-09-30 15:55:13.289	info	Adapter will retry in 3 minutes to get vehicles
                                          
                                          bmw.0
                                          2025-09-30 15:55:13.289	error	"Blocked"
                                          
                                          bmw.0
                                          2025-09-30 15:55:13.289	error	AxiosError: Request failed with status code 489
                                          
                                          bmw.0
                                          2025-09-30 15:55:13.288	error	getvehicles v2 failed
                                          
                                          bmw.0
                                          2025-09-30 15:55:13.160	info	Start getting bmw vehicles
                                          
                                          bmw.0
                                          2025-09-30 15:55:12.387	info	starting. Version 3.0.1 (non-npm: TA2k/ioBroker.bmw) in /opt/iobroker/node_modules/iobroker.bmw, node: v22.20.0, js-controller: 7.0.7
                                          
                                          host.ioBDebian13
                                          2025-09-30 15:55:10.866	info	instance system.adapter.bmw.0 in version "3.0.1" (non-npm: TA2k/ioBroker.bmw) started with pid 29808
                                          
                                          host.ioBDebian13
                                          2025-09-30 15:55:07.283	info	instance system.adapter.bmw.0 terminated with code 11 (ADAPTER_REQUESTED_TERMINATION)
                                          
                                          T Offline
                                          T Offline
                                          tombox
                                          schrieb am zuletzt editiert von
                                          #700

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

                                          T M 2 Antworten Letzte Antwort
                                          1
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          654

                                          Online

                                          32.4k

                                          Benutzer

                                          81.4k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2025
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe