iobroker.bmw 4.0.2 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -19
- package/admin/jsonConfig.json +84 -229
- package/io-package.json +4 -8
- package/lib/tools.js +53 -53
- package/main.js +90 -96
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@ This adapter integrates BMW vehicles into ioBroker using the new BMW CarData API
|
|
|
44
44
|
|
|
45
45
|

|
|
46
46
|
|
|
47
|
-
**CRITICAL**: Click one service and wait 20 seconds if you see an error message, then click again
|
|
47
|
+
# **CRITICAL**: Click one service and wait 20 seconds if you see an error message, then click again. Don't press on "Gerät Authentifizieren/Devict authentication" Enter the client_id in iobroker settings.
|
|
48
48
|
|
|
49
|
-
### 2.
|
|
49
|
+
### 2. CarData Streaming Configuration
|
|
50
50
|
|
|
51
51
|
**YOU MUST CONFIGURE CARDATA STREAMING AND SELECT ALL 244 DATA POINTS**
|
|
52
52
|
|
|
@@ -74,13 +74,10 @@ After creating your Client ID, configure streaming:
|
|
|
74
74
|
5. **Configure API Endpoints** - Select which data to fetch:
|
|
75
75
|
- **Basic Data** ✅ - Essential vehicle information (recommended)
|
|
76
76
|
- **Charging History** ✅ - Charging sessions and history (recommended)
|
|
77
|
-
- **Vehicle
|
|
78
|
-
- **Charging
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **Destination Information** - Navigation and destination data
|
|
82
|
-
- **Location** - GPS position and location services
|
|
83
|
-
- **Statistics** - Driving statistics and analytics
|
|
77
|
+
- **Vehicle Image** - Vehicle image for display purposes
|
|
78
|
+
- **Location Based Charging Settings** - Location-specific charging preferences
|
|
79
|
+
- **Smart Maintenance Tyre Diagnosis** - Tyre condition and diagnosis data
|
|
80
|
+
- **Telematic Data** - Trip information and driving behavior analytics
|
|
84
81
|
6. Configure **VIN ignore list** if needed
|
|
85
82
|
|
|
86
83
|
**💡 Tip:** Only enable endpoints you actually need to conserve your 50 API calls per 24-hour quota. MQTT streaming provides real-time data without using quota.
|
|
@@ -100,7 +97,6 @@ Vehicle data is organized under `bmw.0.VIN.*` where `VIN` represents your Vehicl
|
|
|
100
97
|
### Main Folder Structure
|
|
101
98
|
|
|
102
99
|
- **`bmw.0.VIN.api.*`** - API Data (Periodic Updates)
|
|
103
|
-
|
|
104
100
|
- Data fetched via BMW CarData REST API
|
|
105
101
|
- Uses API quota (50 calls per 24 hours)
|
|
106
102
|
- Updated based on configured interval
|
|
@@ -114,17 +110,14 @@ Vehicle data is organized under `bmw.0.VIN.*` where `VIN` represents your Vehicl
|
|
|
114
110
|
|
|
115
111
|
### Available API Endpoints (Configurable)
|
|
116
112
|
|
|
117
|
-
You can enable/disable these endpoints in adapter settings:
|
|
113
|
+
You can enable/disable these endpoints in adapter settings (BMW CarData API v1):
|
|
118
114
|
|
|
119
115
|
- `bmw.0.VIN.api.basicData.*` - Vehicle information, model, brand, series ✅ **(Default: Enabled)**
|
|
120
116
|
- `bmw.0.VIN.api.chargingHistory.*` - Charging sessions and history ✅ **(Default: Enabled)**
|
|
121
|
-
- `bmw.0.VIN.api.
|
|
122
|
-
- `bmw.0.VIN.api.
|
|
123
|
-
- `bmw.0.VIN.api.
|
|
124
|
-
- `bmw.0.VIN.api.
|
|
125
|
-
- `bmw.0.VIN.api.location.*` - GPS position and location services
|
|
126
|
-
- `bmw.0.VIN.api.statistics.*` - Driving statistics and analytics
|
|
127
|
-
- `bmw.0.VIN.api.vehicleState.*` - Current vehicle status and conditions ✅ **(Default: Enabled)**
|
|
117
|
+
- `bmw.0.VIN.api.image.*` - Vehicle image for display purposes
|
|
118
|
+
- `bmw.0.VIN.api.locationBasedChargingSettings.*` - Location-specific charging preferences and settings
|
|
119
|
+
- `bmw.0.VIN.api.smartMaintenanceTyreDiagnosis.*` - Smart maintenance system tyre condition and diagnosis
|
|
120
|
+
- `bmw.0.VIN.api.telematicData.*` - Vehicle telematic data including trip information and driving behavior
|
|
128
121
|
|
|
129
122
|
### Metadata
|
|
130
123
|
|
|
@@ -196,7 +189,7 @@ This adapter is available at: [https://github.com/TA2k/ioBroker.bmw](https://git
|
|
|
196
189
|
|
|
197
190
|
## Changelog
|
|
198
191
|
|
|
199
|
-
### 4.0.
|
|
192
|
+
### 4.0.3 (2025-10-01)
|
|
200
193
|
|
|
201
194
|
- **BREAKING:** Complete migration to BMW CarData API with OAuth2 Device Flow authentication
|
|
202
195
|
- **BREAKING:** Removed username/password authentication (deprecated by BMW)
|
package/admin/jsonConfig.json
CHANGED
|
@@ -318,270 +318,125 @@
|
|
|
318
318
|
"newLine": false,
|
|
319
319
|
"sm": 4
|
|
320
320
|
},
|
|
321
|
-
"
|
|
321
|
+
"fetchImage": {
|
|
322
322
|
"type": "checkbox",
|
|
323
323
|
"label": {
|
|
324
|
-
"en": "Fetch
|
|
325
|
-
"de": "
|
|
326
|
-
"ru": "Получать
|
|
327
|
-
"pt": "Buscar
|
|
328
|
-
"nl": "
|
|
329
|
-
"fr": "Récupérer
|
|
330
|
-
"it": "Recupera
|
|
331
|
-
"es": "Obtener
|
|
332
|
-
"pl": "Pobierz
|
|
333
|
-
"zh-cn": "
|
|
324
|
+
"en": "Fetch Vehicle Image",
|
|
325
|
+
"de": "Fahrzeugbild abrufen",
|
|
326
|
+
"ru": "Получать изображение автомобиля",
|
|
327
|
+
"pt": "Buscar imagem do veículo",
|
|
328
|
+
"nl": "Voertuigafbeelding ophalen",
|
|
329
|
+
"fr": "Récupérer l'image du véhicule",
|
|
330
|
+
"it": "Recupera immagine veicolo",
|
|
331
|
+
"es": "Obtener imagen del vehículo",
|
|
332
|
+
"pl": "Pobierz obraz pojazdu",
|
|
333
|
+
"zh-cn": "获取车辆图像"
|
|
334
334
|
},
|
|
335
335
|
"help": {
|
|
336
|
-
"en": "
|
|
337
|
-
"de": "
|
|
338
|
-
"ru": "
|
|
339
|
-
"pt": "
|
|
340
|
-
"nl": "
|
|
341
|
-
"fr": "
|
|
342
|
-
"it": "
|
|
343
|
-
"es": "
|
|
344
|
-
"pl": "
|
|
345
|
-
"zh-cn": "
|
|
336
|
+
"en": "Vehicle image for display purposes",
|
|
337
|
+
"de": "Fahrzeugbild für Anzeigezwecke",
|
|
338
|
+
"ru": "Изображение автомобиля для отображения",
|
|
339
|
+
"pt": "Imagem do veículo para fins de exibição",
|
|
340
|
+
"nl": "Voertuigafbeelding voor weergavedoeleinden",
|
|
341
|
+
"fr": "Image du véhicule à des fins d'affichage",
|
|
342
|
+
"it": "Immagine del veicolo per scopi di visualizzazione",
|
|
343
|
+
"es": "Imagen del vehículo para fines de visualización",
|
|
344
|
+
"pl": "Obraz pojazdu do celów wyświetlania",
|
|
345
|
+
"zh-cn": "用于显示目的的车辆图像"
|
|
346
346
|
},
|
|
347
347
|
"default": false,
|
|
348
348
|
"newLine": false,
|
|
349
349
|
"sm": 4
|
|
350
350
|
},
|
|
351
|
-
"
|
|
351
|
+
"fetchLocationBasedChargingSettings": {
|
|
352
352
|
"type": "checkbox",
|
|
353
353
|
"label": {
|
|
354
|
-
"en": "Fetch Charging
|
|
355
|
-
"de": "
|
|
356
|
-
"ru": "Получать
|
|
357
|
-
"pt": "Buscar
|
|
358
|
-
"nl": "
|
|
359
|
-
"fr": "Récupérer les
|
|
360
|
-
"it": "Recupera
|
|
361
|
-
"es": "Obtener
|
|
362
|
-
"pl": "Pobierz
|
|
363
|
-
"zh-cn": "
|
|
354
|
+
"en": "Fetch Location Based Charging Settings",
|
|
355
|
+
"de": "Standortbasierte Ladeeinstellungen abrufen",
|
|
356
|
+
"ru": "Получать настройки зарядки на основе местоположения",
|
|
357
|
+
"pt": "Buscar configurações de carregamento baseadas em localização",
|
|
358
|
+
"nl": "Locatiegebaseerde laadinstellingen ophalen",
|
|
359
|
+
"fr": "Récupérer les paramètres de charge basés sur l'emplacement",
|
|
360
|
+
"it": "Recupera impostazioni ricarica basate sulla posizione",
|
|
361
|
+
"es": "Obtener configuraciones de carga basadas en ubicación",
|
|
362
|
+
"pl": "Pobierz ustawienia ładowania oparte na lokalizacji",
|
|
363
|
+
"zh-cn": "获取基于位置的充电设置"
|
|
364
364
|
},
|
|
365
365
|
"help": {
|
|
366
|
-
"en": "
|
|
367
|
-
"de": "
|
|
368
|
-
"ru": "
|
|
369
|
-
"pt": "
|
|
370
|
-
"nl": "
|
|
371
|
-
"fr": "
|
|
372
|
-
"it": "
|
|
373
|
-
"es": "
|
|
374
|
-
"pl": "
|
|
375
|
-
"zh-cn": "
|
|
366
|
+
"en": "Location-specific charging preferences and settings",
|
|
367
|
+
"de": "Standortspezifische Ladepräferenzen und -einstellungen",
|
|
368
|
+
"ru": "Предпочтения и настройки зарядки для конкретных местоположений",
|
|
369
|
+
"pt": "Preferências e configurações de carregamento específicas do local",
|
|
370
|
+
"nl": "Locatiespecifieke laadvoorkeuren en -instellingen",
|
|
371
|
+
"fr": "Préférences et paramètres de charge spécifiques à l'emplacement",
|
|
372
|
+
"it": "Preferenze e impostazioni di ricarica specifiche per la posizione",
|
|
373
|
+
"es": "Preferencias y configuraciones de carga específicas de ubicación",
|
|
374
|
+
"pl": "Preferencje i ustawienia ładowania specyficzne dla lokalizacji",
|
|
375
|
+
"zh-cn": "特定位置的充电偏好和设置"
|
|
376
376
|
},
|
|
377
377
|
"default": false,
|
|
378
378
|
"newLine": true,
|
|
379
379
|
"sm": 4
|
|
380
380
|
},
|
|
381
|
-
"
|
|
381
|
+
"fetchSmartMaintenanceTyreDiagnosis": {
|
|
382
382
|
"type": "checkbox",
|
|
383
383
|
"label": {
|
|
384
|
-
"en": "Fetch
|
|
385
|
-
"de": "
|
|
386
|
-
"ru": "Получать
|
|
387
|
-
"pt": "Buscar
|
|
388
|
-
"nl": "
|
|
389
|
-
"fr": "Récupérer le
|
|
390
|
-
"it": "Recupera
|
|
391
|
-
"es": "Obtener
|
|
392
|
-
"pl": "Pobierz
|
|
393
|
-
"zh-cn": "
|
|
384
|
+
"en": "Fetch Smart Maintenance Tyre Diagnosis",
|
|
385
|
+
"de": "Smart Maintenance Reifendiagnose abrufen",
|
|
386
|
+
"ru": "Получать диагностику шин Smart Maintenance",
|
|
387
|
+
"pt": "Buscar diagnóstico de pneus Smart Maintenance",
|
|
388
|
+
"nl": "Smart Maintenance bandendiagnose ophalen",
|
|
389
|
+
"fr": "Récupérer le diagnostic des pneus Smart Maintenance",
|
|
390
|
+
"it": "Recupera diagnosi pneumatici Smart Maintenance",
|
|
391
|
+
"es": "Obtener diagnóstico de neumáticos Smart Maintenance",
|
|
392
|
+
"pl": "Pobierz diagnozę opon Smart Maintenance",
|
|
393
|
+
"zh-cn": "获取智能维护轮胎诊断"
|
|
394
394
|
},
|
|
395
395
|
"help": {
|
|
396
|
-
"en": "
|
|
397
|
-
"de": "
|
|
398
|
-
"ru": "
|
|
399
|
-
"pt": "
|
|
400
|
-
"nl": "
|
|
401
|
-
"fr": "
|
|
402
|
-
"it": "
|
|
403
|
-
"es": "
|
|
404
|
-
"pl": "
|
|
405
|
-
"zh-cn": "
|
|
396
|
+
"en": "Smart maintenance system tyre condition and diagnosis data",
|
|
397
|
+
"de": "Smart Maintenance System Reifenzustand und Diagnosedaten",
|
|
398
|
+
"ru": "Данные состояния и диагностики шин системы Smart Maintenance",
|
|
399
|
+
"pt": "Dados de condição e diagnóstico de pneus do sistema Smart Maintenance",
|
|
400
|
+
"nl": "Smart Maintenance systeem bandentoestand en diagnosegegevens",
|
|
401
|
+
"fr": "Données de condition et de diagnostic des pneus du système Smart Maintenance",
|
|
402
|
+
"it": "Dati di condizione e diagnosi pneumatici del sistema Smart Maintenance",
|
|
403
|
+
"es": "Datos de condición y diagnóstico de neumáticos del sistema Smart Maintenance",
|
|
404
|
+
"pl": "Dane stanu i diagnozy opon systemu Smart Maintenance",
|
|
405
|
+
"zh-cn": "智能维护系统轮胎状况和诊断数据"
|
|
406
406
|
},
|
|
407
407
|
"default": false,
|
|
408
408
|
"newLine": false,
|
|
409
409
|
"sm": 4
|
|
410
410
|
},
|
|
411
|
-
"
|
|
411
|
+
"fetchTelematicData": {
|
|
412
412
|
"type": "checkbox",
|
|
413
413
|
"label": {
|
|
414
|
-
"en": "Fetch
|
|
415
|
-
"de": "
|
|
416
|
-
"ru": "Получать
|
|
417
|
-
"pt": "Buscar
|
|
418
|
-
"nl": "
|
|
419
|
-
"fr": "Récupérer les
|
|
420
|
-
"it": "Recupera
|
|
421
|
-
"es": "Obtener
|
|
422
|
-
"pl": "Pobierz
|
|
423
|
-
"zh-cn": "
|
|
414
|
+
"en": "Fetch Telematic Data",
|
|
415
|
+
"de": "Telematikdaten abrufen",
|
|
416
|
+
"ru": "Получать телематические данные",
|
|
417
|
+
"pt": "Buscar dados telemáticos",
|
|
418
|
+
"nl": "Telematicagegevens ophalen",
|
|
419
|
+
"fr": "Récupérer les données télématiques",
|
|
420
|
+
"it": "Recupera dati telematici",
|
|
421
|
+
"es": "Obtener datos telemáticos",
|
|
422
|
+
"pl": "Pobierz dane telematyczne",
|
|
423
|
+
"zh-cn": "获取远程信息处理数据"
|
|
424
424
|
},
|
|
425
425
|
"help": {
|
|
426
|
-
"en": "
|
|
427
|
-
"de": "
|
|
428
|
-
"ru": "
|
|
429
|
-
"pt": "Dados de
|
|
430
|
-
"nl": "
|
|
431
|
-
"fr": "Données de
|
|
432
|
-
"it": "Dati
|
|
433
|
-
"es": "Datos de
|
|
434
|
-
"pl": "Dane
|
|
435
|
-
"zh-cn": "
|
|
426
|
+
"en": "Vehicle telematic data including trip information and driving behavior",
|
|
427
|
+
"de": "Fahrzeugtelematikdaten einschließlich Reiseinformationen und Fahrverhalten",
|
|
428
|
+
"ru": "Телематические данные автомобиля, включая информацию о поездках и поведении при вождении",
|
|
429
|
+
"pt": "Dados telemáticos do veículo incluindo informações de viagem e comportamento de direção",
|
|
430
|
+
"nl": "Voertuigtelematicagegevens inclusief reisinformatie en rijgedrag",
|
|
431
|
+
"fr": "Données télématiques du véhicule incluant les informations de voyage et le comportement de conduite",
|
|
432
|
+
"it": "Dati telematici del veicolo incluse informazioni di viaggio e comportamento di guida",
|
|
433
|
+
"es": "Datos telemáticos del vehículo incluyendo información de viaje y comportamiento de conducción",
|
|
434
|
+
"pl": "Dane telematyczne pojazdu obejmujące informacje o podróży i zachowanie podczas jazdy",
|
|
435
|
+
"zh-cn": "车辆远程信息处理数据,包括行程信息和驾驶行为"
|
|
436
436
|
},
|
|
437
437
|
"default": false,
|
|
438
438
|
"newLine": false,
|
|
439
439
|
"sm": 4
|
|
440
|
-
},
|
|
441
|
-
"fetchLocation": {
|
|
442
|
-
"type": "checkbox",
|
|
443
|
-
"label": {
|
|
444
|
-
"en": "Fetch Location",
|
|
445
|
-
"de": "Standort abrufen",
|
|
446
|
-
"ru": "Получать местоположение",
|
|
447
|
-
"pt": "Buscar localização",
|
|
448
|
-
"nl": "Locatie ophalen",
|
|
449
|
-
"fr": "Récupérer la localisation",
|
|
450
|
-
"it": "Recupera posizione",
|
|
451
|
-
"es": "Obtener ubicación",
|
|
452
|
-
"pl": "Pobierz lokalizację",
|
|
453
|
-
"zh-cn": "获取位置"
|
|
454
|
-
},
|
|
455
|
-
"help": {
|
|
456
|
-
"en": "GPS position and location services",
|
|
457
|
-
"de": "GPS-Position und Standortdienste",
|
|
458
|
-
"ru": "GPS позиция и службы определения местоположения",
|
|
459
|
-
"pt": "Posição GPS e serviços de localização",
|
|
460
|
-
"nl": "GPS positie en locatiediensten",
|
|
461
|
-
"fr": "Position GPS et services de localisation",
|
|
462
|
-
"it": "Posizione GPS e servizi di localizzazione",
|
|
463
|
-
"es": "Posición GPS y servicios de ubicación",
|
|
464
|
-
"pl": "Pozycja GPS i usługi lokalizacji",
|
|
465
|
-
"zh-cn": "GPS位置和位置服务"
|
|
466
|
-
},
|
|
467
|
-
"default": false,
|
|
468
|
-
"newLine": true,
|
|
469
|
-
"sm": 4
|
|
470
|
-
},
|
|
471
|
-
"fetchStatistics": {
|
|
472
|
-
"type": "checkbox",
|
|
473
|
-
"label": {
|
|
474
|
-
"en": "Fetch Statistics",
|
|
475
|
-
"de": "Statistiken abrufen",
|
|
476
|
-
"ru": "Получать статистику",
|
|
477
|
-
"pt": "Buscar estatísticas",
|
|
478
|
-
"nl": "Statistieken ophalen",
|
|
479
|
-
"fr": "Récupérer les statistiques",
|
|
480
|
-
"it": "Recupera statistiche",
|
|
481
|
-
"es": "Obtener estadísticas",
|
|
482
|
-
"pl": "Pobierz statystyki",
|
|
483
|
-
"zh-cn": "获取统计信息"
|
|
484
|
-
},
|
|
485
|
-
"help": {
|
|
486
|
-
"en": "Driving statistics and analytics",
|
|
487
|
-
"de": "Fahrstatistiken und Analysen",
|
|
488
|
-
"ru": "Статистика вождения и аналитика",
|
|
489
|
-
"pt": "Estatísticas de direção e análises",
|
|
490
|
-
"nl": "Rijstatistieken en analyses",
|
|
491
|
-
"fr": "Statistiques de conduite et analyses",
|
|
492
|
-
"it": "Statistiche di guida e analisi",
|
|
493
|
-
"es": "Estadísticas de conducción y análisis",
|
|
494
|
-
"pl": "Statystyki jazdy i analizy",
|
|
495
|
-
"zh-cn": "驾驶统计和分析"
|
|
496
|
-
},
|
|
497
|
-
"default": false,
|
|
498
|
-
"newLine": false,
|
|
499
|
-
"sm": 4
|
|
500
|
-
},
|
|
501
|
-
"fetchVehicleState": {
|
|
502
|
-
"type": "checkbox",
|
|
503
|
-
"label": {
|
|
504
|
-
"en": "Fetch Vehicle State",
|
|
505
|
-
"de": "Fahrzeugstatus abrufen",
|
|
506
|
-
"ru": "Получать состояние автомобиля",
|
|
507
|
-
"pt": "Buscar estado do veículo",
|
|
508
|
-
"nl": "Voertuigstatus ophalen",
|
|
509
|
-
"fr": "Récupérer l'état du véhicule",
|
|
510
|
-
"it": "Recupera stato veicolo",
|
|
511
|
-
"es": "Obtener estado del vehículo",
|
|
512
|
-
"pl": "Pobierz stan pojazdu",
|
|
513
|
-
"zh-cn": "获取车辆状态"
|
|
514
|
-
},
|
|
515
|
-
"help": {
|
|
516
|
-
"en": "Current vehicle status and conditions",
|
|
517
|
-
"de": "Aktueller Fahrzeugstatus und -zustand",
|
|
518
|
-
"ru": "Текущий статус и состояние автомобиля",
|
|
519
|
-
"pt": "Status e condições atuais do veículo",
|
|
520
|
-
"nl": "Huidige voertuigstatus en -condities",
|
|
521
|
-
"fr": "État et conditions actuels du véhicule",
|
|
522
|
-
"it": "Stato e condizioni attuali del veicolo",
|
|
523
|
-
"es": "Estado y condiciones actuales del vehículo",
|
|
524
|
-
"pl": "Aktualny stan i warunki pojazdu",
|
|
525
|
-
"zh-cn": "当前车辆状态和条件"
|
|
526
|
-
},
|
|
527
|
-
"default": true,
|
|
528
|
-
"newLine": false,
|
|
529
|
-
"sm": 4
|
|
530
|
-
},
|
|
531
|
-
"_breaking": {
|
|
532
|
-
"type": "header",
|
|
533
|
-
"text": {
|
|
534
|
-
"en": "⚠️ Breaking Changes in v4.0",
|
|
535
|
-
"de": "⚠️ Breaking Changes in v4.0",
|
|
536
|
-
"ru": "⚠️ Критические изменения в v4.0",
|
|
537
|
-
"pt": "⚠️ Mudanças críticas na v4.0",
|
|
538
|
-
"nl": "⚠️ Breaking changes in v4.0",
|
|
539
|
-
"fr": "⚠️ Changements critiques en v4.0",
|
|
540
|
-
"it": "⚠️ Cambiamenti critici nella v4.0",
|
|
541
|
-
"es": "⚠️ Cambios críticos en v4.0",
|
|
542
|
-
"pl": "⚠️ Krytyczne zmiany w v4.0",
|
|
543
|
-
"zh-cn": "⚠️ v4.0中的重大变更"
|
|
544
|
-
},
|
|
545
|
-
"size": 5,
|
|
546
|
-
"color": "orange"
|
|
547
|
-
},
|
|
548
|
-
"_removed": {
|
|
549
|
-
"type": "staticText",
|
|
550
|
-
"text": {
|
|
551
|
-
"en": "❌ Removed: Username/password login, remote controls (lock/unlock, climate, charging), second user support, captcha requirement",
|
|
552
|
-
"de": "❌ Entfernt: Benutzername/Passwort-Login, Fernbedienung (Verriegeln/Entriegeln, Klima, Laden), zweiter Benutzer, Captcha-Anforderung",
|
|
553
|
-
"ru": "❌ Удалено: Логин по имени пользователя/паролю, удаленное управление (блокировка/разблокировка, климат, зарядка), поддержка второго пользователя, требование капчи",
|
|
554
|
-
"pt": "❌ Removido: Login com nome de usuário/senha, controles remotos (trancar/destrancar, clima, carregamento), suporte ao segundo usuário, requisito de captcha",
|
|
555
|
-
"nl": "❌ Verwijderd: Gebruikersnaam/wachtwoord login, remote controls (vergrendelen/ontgrendelen, klimaat, laden), tweede gebruiker ondersteuning, captcha vereiste",
|
|
556
|
-
"fr": "❌ Supprimé: Connexion nom d'utilisateur/mot de passe, contrôles à distance (verrouiller/déverrouiller, climatisation, charge), support du deuxième utilisateur, exigence de captcha",
|
|
557
|
-
"it": "❌ Rimosso: Login username/password, controlli remoti (bloccare/sbloccare, clima, ricarica), supporto secondo utente, requisito captcha",
|
|
558
|
-
"es": "❌ Eliminado: Login de usuario/contraseña, controles remotos (bloquear/desbloquear, clima, carga), soporte de segundo usuario, requisito de captcha",
|
|
559
|
-
"pl": "❌ Usunięto: Login nazwa użytkownika/hasło, zdalne sterowanie (blokowanie/odblokowanie, klimat, ładowanie), obsługa drugiego użytkownika, wymóg captcha",
|
|
560
|
-
"zh-cn": "❌ 已移除:用户名/密码登录,远程控制(锁定/解锁,气候,充电),第二用户支持,验证码要求"
|
|
561
|
-
},
|
|
562
|
-
"style": {
|
|
563
|
-
"color": "red",
|
|
564
|
-
"fontSize": "smaller"
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
"_added": {
|
|
568
|
-
"type": "staticText",
|
|
569
|
-
"text": {
|
|
570
|
-
"en": "✅ Added: OAuth2 device flow authentication, real-time MQTT streaming, 50 API calls per 24h quota management, comprehensive data from all CarData endpoints",
|
|
571
|
-
"de": "✅ Hinzugefügt: OAuth2 Device Flow Authentifizierung, Echtzeit MQTT Streaming, 50 API Aufrufe pro 24h Quota Management, umfassende Daten von allen CarData Endpunkten",
|
|
572
|
-
"ru": "✅ Добавлено: OAuth2 device flow аутентификация, MQTT поток в реальном времени, управление квотой 50 API вызовов за 24ч, всесторонние данные со всех CarData конечных точек",
|
|
573
|
-
"pt": "✅ Adicionado: Autenticação OAuth2 device flow, streaming MQTT em tempo real, gerenciamento de quota de 50 chamadas API por 24h, dados abrangentes de todos os endpoints CarData",
|
|
574
|
-
"nl": "✅ Toegevoegd: OAuth2 device flow authenticatie, realtime MQTT streaming, 50 API oproepen per 24u quota beheer, uitgebreide data van alle CarData endpoints",
|
|
575
|
-
"fr": "✅ Ajouté: Authentification OAuth2 device flow, streaming MQTT en temps réel, gestion de quota de 50 appels API par 24h, données complètes de tous les endpoints CarData",
|
|
576
|
-
"it": "✅ Aggiunto: Autenticazione OAuth2 device flow, streaming MQTT in tempo reale, gestione quota 50 chiamate API per 24h, dati completi da tutti gli endpoint CarData",
|
|
577
|
-
"es": "✅ Agregado: Autenticación OAuth2 device flow, streaming MQTT en tiempo real, gestión de cuota de 50 llamadas API por 24h, datos completos de todos los endpoints CarData",
|
|
578
|
-
"pl": "✅ Dodano: Uwierzytelnianie OAuth2 device flow, streaming MQTT w czasie rzeczywistym, zarządzanie kwotą 50 wywołań API na 24h, kompleksowe dane ze wszystkich punktów końcowych CarData",
|
|
579
|
-
"zh-cn": "✅ 已添加:OAuth2设备流身份验证,实时MQTT流,每24小时50次API调用配额管理,来自所有CarData端点的全面数据"
|
|
580
|
-
},
|
|
581
|
-
"style": {
|
|
582
|
-
"color": "green",
|
|
583
|
-
"fontSize": "smaller"
|
|
584
|
-
}
|
|
585
440
|
}
|
|
586
441
|
}
|
|
587
442
|
}
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "bmw",
|
|
4
|
-
"version": "4.0.
|
|
4
|
+
"version": "4.0.3",
|
|
5
5
|
"news": {
|
|
6
|
-
"4.0.
|
|
6
|
+
"4.0.3": {
|
|
7
7
|
"en": "BREAKING: Complete migration to BMW CarData API\nBREAKING: OAuth2 device flow authentication (no more username/password)\nBREAKING: Remote controls removed (CarData API is read-only)\nNEW: Real-time MQTT streaming for instant updates\nNEW: 50 API calls per 24h quota management\nNEW: Comprehensive data collection from all CarData endpoints",
|
|
8
8
|
"de": "BREAKING: Vollständige Migration zur BMW CarData API\nBREAKING: OAuth2 Device Flow Authentifizierung (keine Benutzername/Passwort mehr)\nBREAKING: Fernbedienungen entfernt (CarData API ist nur lesend)\nNEU: Echtzeit MQTT Streaming für sofortige Updates\nNEU: 50 API Aufrufe pro 24h Quota Management\nNEU: Umfassende Datensammlung von allen CarData Endpunkten"
|
|
9
9
|
},
|
|
@@ -125,12 +125,8 @@
|
|
|
125
125
|
"uk": "Adapter for BMW CarData API з потоком MQTT в реальному часі",
|
|
126
126
|
"zh-cn": "带有实时MQTT流的BMW CarData API适配器"
|
|
127
127
|
},
|
|
128
|
-
"authors": [
|
|
129
|
-
|
|
130
|
-
],
|
|
131
|
-
"keywords": [
|
|
132
|
-
"BMW"
|
|
133
|
-
],
|
|
128
|
+
"authors": ["TA2k <tombox2020@gmail.com>"],
|
|
129
|
+
"keywords": ["BMW"],
|
|
134
130
|
"licenseInformation": {
|
|
135
131
|
"license": "MIT",
|
|
136
132
|
"type": "free"
|
package/lib/tools.js
CHANGED
|
@@ -7,11 +7,11 @@ const axios = require('axios').default;
|
|
|
7
7
|
* @returns {it is Record<string, any>} True if the variable is a real object, false otherwise.
|
|
8
8
|
*/
|
|
9
9
|
function isObject(it) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// This is necessary because:
|
|
11
|
+
// typeof null === 'object'
|
|
12
|
+
// typeof [] === 'object'
|
|
13
|
+
// [] instanceof Object === true
|
|
14
|
+
return Object.prototype.toString.call(it) === '[object Object]';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -21,10 +21,10 @@ function isObject(it) {
|
|
|
21
21
|
* @returns {it is any[]} True if the variable is an array, false otherwise.
|
|
22
22
|
*/
|
|
23
23
|
function isArray(it) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
if (typeof Array.isArray === 'function') {
|
|
25
|
+
return Array.isArray(it);
|
|
26
|
+
}
|
|
27
|
+
return Object.prototype.toString.call(it) === '[object Array]';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -36,15 +36,15 @@ function isArray(it) {
|
|
|
36
36
|
* @returns {Promise<string>} A promise that resolves to the translated text.
|
|
37
37
|
*/
|
|
38
38
|
async function translateText(text, targetLang, yandexApiKey) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
if (targetLang === 'en') {
|
|
40
|
+
return text;
|
|
41
|
+
} else if (!text) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
if (yandexApiKey) {
|
|
45
|
+
return translateYandex(text, targetLang, yandexApiKey);
|
|
46
|
+
}
|
|
47
|
+
return translateGoogle(text, targetLang);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -56,21 +56,21 @@ async function translateText(text, targetLang, yandexApiKey) {
|
|
|
56
56
|
* @returns {Promise<string>} A promise that resolves to the translated text.
|
|
57
57
|
*/
|
|
58
58
|
async function translateYandex(text, targetLang, apiKey) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
if (targetLang === 'zh-cn') {
|
|
60
|
+
targetLang = 'zh';
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent(
|
|
64
|
+
text,
|
|
65
|
+
)}&lang=en-${targetLang}`;
|
|
66
|
+
const response = await axios({ url, timeout: 15000 });
|
|
67
|
+
if (response.data && response.data.text && isArray(response.data.text)) {
|
|
68
|
+
return response.data.text[0];
|
|
69
|
+
}
|
|
70
|
+
throw new Error('Invalid response for translate request');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
throw new Error(`Could not translate to "${targetLang}": ${e}`);
|
|
73
|
+
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -81,27 +81,27 @@ async function translateYandex(text, targetLang, apiKey) {
|
|
|
81
81
|
* @returns {Promise<string>} A promise that resolves to the translated text.
|
|
82
82
|
*/
|
|
83
83
|
async function translateGoogle(text, targetLang) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
84
|
+
try {
|
|
85
|
+
const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(
|
|
86
|
+
text,
|
|
87
|
+
)}&ie=UTF-8&oe=UTF-8`;
|
|
88
|
+
const response = await axios({ url, timeout: 15000 });
|
|
89
|
+
if (isArray(response.data)) {
|
|
90
|
+
// we got a valid response
|
|
91
|
+
return response.data[0][0][0];
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Invalid response for translate request');
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e.response && e.response.status === 429) {
|
|
96
|
+
throw new Error(`Could not translate to "${targetLang}": Rate-limited by Google Translate`);
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(`Could not translate to "${targetLang}": ${e}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
module.exports = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
isArray,
|
|
105
|
+
isObject,
|
|
106
|
+
translateText,
|
|
107
107
|
};
|
package/main.js
CHANGED
|
@@ -81,6 +81,7 @@ class Bmw extends utils.Adapter {
|
|
|
81
81
|
this.apiCalls = JSON.parse(apiCallsHistoryState.val);
|
|
82
82
|
this.log.debug(`Restored ${this.apiCalls.length} API call timestamps from history`);
|
|
83
83
|
} catch (error) {
|
|
84
|
+
this.log.error(`Failed to parse API calls history: ${error.message}`);
|
|
84
85
|
this.log.warn('Failed to parse API calls history, starting fresh');
|
|
85
86
|
this.apiCalls = [];
|
|
86
87
|
}
|
|
@@ -101,7 +102,7 @@ class Bmw extends utils.Adapter {
|
|
|
101
102
|
// Try to refresh tokens
|
|
102
103
|
await this.refreshToken();
|
|
103
104
|
} catch (error) {
|
|
104
|
-
this.log.warn(
|
|
105
|
+
this.log.warn(`Failed to parse stored session, starting new login ${error.message}`);
|
|
105
106
|
await this.login();
|
|
106
107
|
}
|
|
107
108
|
} else {
|
|
@@ -118,30 +119,36 @@ class Bmw extends utils.Adapter {
|
|
|
118
119
|
// Connect MQTT after successful auth
|
|
119
120
|
await this.connectMQTT();
|
|
120
121
|
// Start periodic token refresh (every 45 minutes)
|
|
121
|
-
this.refreshTokenInterval = setInterval(
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
this.refreshTokenInterval = setInterval(
|
|
123
|
+
async () => {
|
|
124
|
+
await this.refreshToken();
|
|
125
|
+
},
|
|
126
|
+
45 * 60 * 1000,
|
|
127
|
+
);
|
|
124
128
|
|
|
125
129
|
// Start periodic API updates (respecting quota limits)
|
|
126
130
|
if (this.vinArray.length > 0) {
|
|
127
131
|
this.log.info(`Setting up periodic updates every ${this.config.interval} minutes for ${this.vinArray.length} vehicle(s)`);
|
|
128
|
-
this.updateInterval = setInterval(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
this.updateInterval = setInterval(
|
|
133
|
+
async () => {
|
|
134
|
+
// Update quota states (expired calls removed automatically)
|
|
135
|
+
this.updateQuotaStates();
|
|
136
|
+
|
|
137
|
+
// Periodic API data refresh - MQTT provides real-time updates
|
|
138
|
+
const headers = {
|
|
139
|
+
Authorization: `Bearer ${this.session.access_token}`,
|
|
140
|
+
'x-version': 'v1',
|
|
141
|
+
Accept: 'application/json',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (const vin of this.vinArray) {
|
|
145
|
+
this.log.debug(`Periodic API refresh for ${vin}`);
|
|
146
|
+
await this.fetchAllVehicleData(vin, headers);
|
|
147
|
+
break; // Only one vehicle per interval to conserve quota
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
this.config.interval * 60 * 1000,
|
|
151
|
+
);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
this.log.info('BMW CarData adapter startup complete');
|
|
@@ -175,7 +182,7 @@ class Bmw extends utils.Adapter {
|
|
|
175
182
|
code_challenge: codeChallenge,
|
|
176
183
|
code_challenge_method: 'S256',
|
|
177
184
|
};
|
|
178
|
-
this.log.debug(
|
|
185
|
+
this.log.debug(`Device code request data: ${JSON.stringify(requestData)}`);
|
|
179
186
|
|
|
180
187
|
const deviceResponse = await this.requestClient({
|
|
181
188
|
method: 'post',
|
|
@@ -186,17 +193,17 @@ class Bmw extends utils.Adapter {
|
|
|
186
193
|
},
|
|
187
194
|
data: requestData,
|
|
188
195
|
})
|
|
189
|
-
.then(
|
|
190
|
-
this.log.debug(
|
|
196
|
+
.then(res => {
|
|
197
|
+
this.log.debug(`Device code response: ${JSON.stringify(res.data)}`);
|
|
191
198
|
return res;
|
|
192
199
|
})
|
|
193
|
-
.catch(
|
|
194
|
-
this.log.error(
|
|
195
|
-
this.log.error(
|
|
200
|
+
.catch(error => {
|
|
201
|
+
this.log.error(`Device code request failed: ${error.message}`);
|
|
202
|
+
this.log.error(`Error stack: ${error.stack}`);
|
|
196
203
|
if (error.response) {
|
|
197
|
-
this.log.error(
|
|
198
|
-
this.log.error(
|
|
199
|
-
this.log.error(
|
|
204
|
+
this.log.error(`Response status: ${error.response.status}`);
|
|
205
|
+
this.log.error(`Response headers: ${JSON.stringify(error.response.headers)}`);
|
|
206
|
+
this.log.error(`Response data: ${JSON.stringify(error.response.data)}`);
|
|
200
207
|
|
|
201
208
|
// Special handling for 400 Bad Request - likely client configuration issue
|
|
202
209
|
if (error.response.status === 400) {
|
|
@@ -211,7 +218,9 @@ class Bmw extends utils.Adapter {
|
|
|
211
218
|
this.log.error('To fix this issue:');
|
|
212
219
|
this.log.error('1. Visit BMW ConnectedDrive portal: https://www.bmw.de/de-de/mybmw/vehicle-overview');
|
|
213
220
|
this.log.error('2. Go to CarData section');
|
|
214
|
-
this.log.error(
|
|
221
|
+
this.log.error(
|
|
222
|
+
'3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection',
|
|
223
|
+
);
|
|
215
224
|
this.log.error('4. If not activated, enable both services');
|
|
216
225
|
this.log.error('5. If already activated, delete and recreate your Client ID');
|
|
217
226
|
this.log.error('6. Update the adapter configuration with the new Client ID');
|
|
@@ -220,12 +229,11 @@ class Bmw extends utils.Adapter {
|
|
|
220
229
|
}
|
|
221
230
|
if (error.request) {
|
|
222
231
|
this.log.error(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
})
|
|
232
|
+
`Request details: ${JSON.stringify({
|
|
233
|
+
method: error.request.method,
|
|
234
|
+
url: error.request.url,
|
|
235
|
+
headers: error.request._headers,
|
|
236
|
+
})}`,
|
|
229
237
|
);
|
|
230
238
|
}
|
|
231
239
|
return false; // Return false instead of throwing
|
|
@@ -263,7 +271,7 @@ class Bmw extends utils.Adapter {
|
|
|
263
271
|
device_code: device_code,
|
|
264
272
|
code_verifier: codeVerifier,
|
|
265
273
|
};
|
|
266
|
-
this.log.debug(
|
|
274
|
+
this.log.debug(`Token request data: ${JSON.stringify(tokenRequestData)}`);
|
|
267
275
|
|
|
268
276
|
const tokenResponse = await this.requestClient({
|
|
269
277
|
method: 'post',
|
|
@@ -302,7 +310,7 @@ class Bmw extends utils.Adapter {
|
|
|
302
310
|
return true;
|
|
303
311
|
} catch (error) {
|
|
304
312
|
const errorCode = error.response?.data?.error;
|
|
305
|
-
this.log.debug(
|
|
313
|
+
this.log.debug(`Token polling error: ${errorCode || error.message}`);
|
|
306
314
|
|
|
307
315
|
if (errorCode === 'authorization_pending') {
|
|
308
316
|
this.log.debug('Authorization still pending, continuing to poll...');
|
|
@@ -315,10 +323,10 @@ class Bmw extends utils.Adapter {
|
|
|
315
323
|
this.log.error('Authorization code expired, please restart adapter');
|
|
316
324
|
return false;
|
|
317
325
|
} else {
|
|
318
|
-
this.log.error(
|
|
326
|
+
this.log.error(`Token request failed: ${errorCode || error.message}`);
|
|
319
327
|
if (error.response) {
|
|
320
|
-
this.log.error(
|
|
321
|
-
this.log.error(
|
|
328
|
+
this.log.error(`Token response status: ${error.response.status}`);
|
|
329
|
+
this.log.error(`Token response data: ${JSON.stringify(error.response.data)}`);
|
|
322
330
|
}
|
|
323
331
|
return false;
|
|
324
332
|
}
|
|
@@ -359,7 +367,7 @@ class Bmw extends utils.Adapter {
|
|
|
359
367
|
url: `${this.carDataApiBase}/customers/vehicles/mappings`,
|
|
360
368
|
headers: headers,
|
|
361
369
|
})
|
|
362
|
-
.then(async
|
|
370
|
+
.then(async res => {
|
|
363
371
|
this.log.debug(JSON.stringify(res.data));
|
|
364
372
|
const mappings = res.data;
|
|
365
373
|
|
|
@@ -373,7 +381,7 @@ class Bmw extends utils.Adapter {
|
|
|
373
381
|
}
|
|
374
382
|
|
|
375
383
|
for (const mapping of mappings) {
|
|
376
|
-
if (mapping.
|
|
384
|
+
if (mapping.vin) {
|
|
377
385
|
const vin = mapping.vin;
|
|
378
386
|
|
|
379
387
|
// Check ignore list
|
|
@@ -406,10 +414,10 @@ class Bmw extends utils.Adapter {
|
|
|
406
414
|
}
|
|
407
415
|
}
|
|
408
416
|
})
|
|
409
|
-
.catch(
|
|
410
|
-
this.log.error(
|
|
417
|
+
.catch(error => {
|
|
418
|
+
this.log.error(`BMW CarData vehicle discovery failed: ${error.message}`);
|
|
411
419
|
if (error.response) {
|
|
412
|
-
this.log.error(
|
|
420
|
+
this.log.error(`Response: ${JSON.stringify(error.response.data)}`);
|
|
413
421
|
if (error.response.status === 403 || error.response.status === 429) {
|
|
414
422
|
this.log.warn('Rate limit exceeded or access denied');
|
|
415
423
|
}
|
|
@@ -438,7 +446,7 @@ class Bmw extends utils.Adapter {
|
|
|
438
446
|
native: {},
|
|
439
447
|
});
|
|
440
448
|
|
|
441
|
-
// Define available API endpoints with config mapping
|
|
449
|
+
// Define available API endpoints with config mapping (BMW CarData API v1)
|
|
442
450
|
const apiEndpoints = [
|
|
443
451
|
{
|
|
444
452
|
name: 'basicData',
|
|
@@ -453,51 +461,33 @@ class Bmw extends utils.Adapter {
|
|
|
453
461
|
channel: 'Charging History',
|
|
454
462
|
},
|
|
455
463
|
{
|
|
456
|
-
name: '
|
|
457
|
-
configKey: '
|
|
458
|
-
url: `/customers/vehicles/${vin}/
|
|
459
|
-
channel: '
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
name: 'chargingSessions',
|
|
463
|
-
configKey: 'fetchChargingSessions',
|
|
464
|
-
url: `/customers/vehicles/${vin}/chargingSessions`,
|
|
465
|
-
channel: 'Charging Sessions',
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
name: 'climateNow',
|
|
469
|
-
configKey: 'fetchClimateNow',
|
|
470
|
-
url: `/customers/vehicles/${vin}/climateNow`,
|
|
471
|
-
channel: 'Climate Control',
|
|
472
|
-
},
|
|
473
|
-
{
|
|
474
|
-
name: 'destinationInformation',
|
|
475
|
-
configKey: 'fetchDestinationInformation',
|
|
476
|
-
url: `/customers/vehicles/${vin}/destinationInformation`,
|
|
477
|
-
channel: 'Destination Information',
|
|
464
|
+
name: 'image',
|
|
465
|
+
configKey: 'fetchImage',
|
|
466
|
+
url: `/customers/vehicles/${vin}/image`,
|
|
467
|
+
channel: 'Vehicle Image',
|
|
478
468
|
},
|
|
479
469
|
{
|
|
480
|
-
name: '
|
|
481
|
-
configKey: '
|
|
482
|
-
url: `/customers/vehicles/${vin}/
|
|
483
|
-
channel: '
|
|
470
|
+
name: 'locationBasedChargingSettings',
|
|
471
|
+
configKey: 'fetchLocationBasedChargingSettings',
|
|
472
|
+
url: `/customers/vehicles/${vin}/locationBasedChargingSettings`,
|
|
473
|
+
channel: 'Location Based Charging Settings',
|
|
484
474
|
},
|
|
485
475
|
{
|
|
486
|
-
name: '
|
|
487
|
-
configKey: '
|
|
488
|
-
url: `/customers/vehicles/${vin}/
|
|
489
|
-
channel: '
|
|
476
|
+
name: 'smartMaintenanceTyreDiagnosis',
|
|
477
|
+
configKey: 'fetchSmartMaintenanceTyreDiagnosis',
|
|
478
|
+
url: `/customers/vehicles/${vin}/smartMaintenanceTyreDiagnosis`,
|
|
479
|
+
channel: 'Smart Maintenance Tyre Diagnosis',
|
|
490
480
|
},
|
|
491
481
|
{
|
|
492
|
-
name: '
|
|
493
|
-
configKey: '
|
|
494
|
-
url: `/customers/vehicles/${vin}/
|
|
495
|
-
channel: '
|
|
482
|
+
name: 'telematicData',
|
|
483
|
+
configKey: 'fetchTelematicData',
|
|
484
|
+
url: `/customers/vehicles/${vin}/telematicData`,
|
|
485
|
+
channel: 'Telematic Data',
|
|
496
486
|
},
|
|
497
487
|
];
|
|
498
488
|
|
|
499
489
|
// Filter endpoints based on user configuration
|
|
500
|
-
const enabledEndpoints = apiEndpoints.filter(
|
|
490
|
+
const enabledEndpoints = apiEndpoints.filter(endpoint => this.config[endpoint.configKey] === true);
|
|
501
491
|
|
|
502
492
|
this.log.info(`Fetching ${enabledEndpoints.length} configured API endpoints for ${vin}...`);
|
|
503
493
|
|
|
@@ -572,11 +562,13 @@ class Bmw extends utils.Adapter {
|
|
|
572
562
|
|
|
573
563
|
updateQuotaStates() {
|
|
574
564
|
const now = Date.now();
|
|
575
|
-
if (!this.apiCalls)
|
|
565
|
+
if (!this.apiCalls) {
|
|
566
|
+
this.apiCalls = [];
|
|
567
|
+
}
|
|
576
568
|
|
|
577
569
|
// Remove calls older than 24h
|
|
578
570
|
const originalLength = this.apiCalls.length;
|
|
579
|
-
this.apiCalls = this.apiCalls.filter(
|
|
571
|
+
this.apiCalls = this.apiCalls.filter(time => now - time < 24 * 60 * 60 * 1000);
|
|
580
572
|
|
|
581
573
|
// Save history if calls were removed due to expiration
|
|
582
574
|
if (this.apiCalls.length !== originalLength) {
|
|
@@ -612,7 +604,7 @@ class Bmw extends utils.Adapter {
|
|
|
612
604
|
}
|
|
613
605
|
|
|
614
606
|
sleep(ms) {
|
|
615
|
-
return new Promise(
|
|
607
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
616
608
|
}
|
|
617
609
|
|
|
618
610
|
async cleanObjects(vin) {
|
|
@@ -676,7 +668,7 @@ class Bmw extends utils.Adapter {
|
|
|
676
668
|
refresh_token: this.session.refresh_token,
|
|
677
669
|
client_id: this.config.clientId,
|
|
678
670
|
};
|
|
679
|
-
this.log.debug(
|
|
671
|
+
this.log.debug(`Refresh request data: ${JSON.stringify(refreshData)}`);
|
|
680
672
|
|
|
681
673
|
await this.requestClient({
|
|
682
674
|
method: 'post',
|
|
@@ -686,7 +678,7 @@ class Bmw extends utils.Adapter {
|
|
|
686
678
|
},
|
|
687
679
|
data: qs.stringify(refreshData),
|
|
688
680
|
})
|
|
689
|
-
.then(async
|
|
681
|
+
.then(async res => {
|
|
690
682
|
// Store refreshed tokens (keep existing session structure)
|
|
691
683
|
this.session = res.data;
|
|
692
684
|
this.setState('cardataauth.session', JSON.stringify(this.session), true);
|
|
@@ -703,7 +695,7 @@ class Bmw extends utils.Adapter {
|
|
|
703
695
|
|
|
704
696
|
return res.data;
|
|
705
697
|
})
|
|
706
|
-
.catch(async
|
|
698
|
+
.catch(async error => {
|
|
707
699
|
this.log.error('Token refresh failed:', error.message);
|
|
708
700
|
this.log.error('Error stack:', error.stack);
|
|
709
701
|
if (error.response) {
|
|
@@ -760,9 +752,9 @@ class Bmw extends utils.Adapter {
|
|
|
760
752
|
|
|
761
753
|
// Subscribe to all vehicle topics for this CarData Streaming username
|
|
762
754
|
const topic = `${this.config.cardataStreamingUsername}/+`;
|
|
763
|
-
this.mqtt.subscribe(topic,
|
|
755
|
+
this.mqtt.subscribe(topic, err => {
|
|
764
756
|
if (err) {
|
|
765
|
-
this.log.error(
|
|
757
|
+
this.log.error(`MQTT subscription failed: ${err.message}`);
|
|
766
758
|
} else {
|
|
767
759
|
this.log.debug(`Subscribed to MQTT topic: ${topic}`);
|
|
768
760
|
}
|
|
@@ -773,8 +765,8 @@ class Bmw extends utils.Adapter {
|
|
|
773
765
|
this.handleMQTTMessage(topic, message);
|
|
774
766
|
});
|
|
775
767
|
|
|
776
|
-
this.mqtt.on('error',
|
|
777
|
-
this.log.error(
|
|
768
|
+
this.mqtt.on('error', error => {
|
|
769
|
+
this.log.error(`MQTT error: ${error.message}`);
|
|
778
770
|
this.setState('info.mqttConnected', false, true);
|
|
779
771
|
});
|
|
780
772
|
|
|
@@ -797,7 +789,9 @@ class Bmw extends utils.Adapter {
|
|
|
797
789
|
const topicParts = topic.split('/');
|
|
798
790
|
|
|
799
791
|
if (topicParts.length >= 2) {
|
|
800
|
-
|
|
792
|
+
/*
|
|
793
|
+
34ddc38-93220-423330-93101-fcd292373/WBY11HH: {"vin":"WBY11HH","entityId":"34ddc38-93220-423330-93101-fcd292373","topic":"WBY11HH","timestamp":"2025-10-01T10:12:40.809Z","data":{"vehicle.powertrain.electric.battery.charging.preferenceSmartCharging":{"timestamp":"2025-10-01T10:12:39Z","value":"PRICE_OPTIMIZED"}}}
|
|
794
|
+
*/
|
|
801
795
|
const vin = topicParts[1];
|
|
802
796
|
|
|
803
797
|
if (data.vin && data.data) {
|
|
@@ -860,7 +854,7 @@ class Bmw extends utils.Adapter {
|
|
|
860
854
|
}
|
|
861
855
|
}
|
|
862
856
|
} catch (error) {
|
|
863
|
-
this.log.warn(
|
|
857
|
+
this.log.warn(`Failed to parse MQTT message: ${error.message}`);
|
|
864
858
|
}
|
|
865
859
|
}
|
|
866
860
|
|
|
@@ -915,7 +909,7 @@ if (require.main !== module) {
|
|
|
915
909
|
/**
|
|
916
910
|
* @param {Partial<utils.AdapterOptions>} [options] - Optional adapter configuration options.
|
|
917
911
|
*/
|
|
918
|
-
module.exports =
|
|
912
|
+
module.exports = options => new Bmw(options);
|
|
919
913
|
} else {
|
|
920
914
|
// otherwise start the instance directly
|
|
921
915
|
new Bmw();
|