iobroker.bmw 4.3.3 → 4.3.5
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 +15 -2
- package/admin/jsonConfig.json +36 -2
- package/io-package.json +30 -29
- package/main.js +162 -16
- package/package.json +11 -11
- package/admin/admin.d.ts +0 -93
- package/admin/style.css +0 -43
- package/admin/tsconfig.json +0 -9
- package/admin/words.js +0 -66
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ A detailed datapoint description you can find here [telematic.json](telematic.js
|
|
|
40
40
|
|
|
41
41
|
### 1. BMW ConnectedDrive Portal Setup
|
|
42
42
|
|
|
43
|
-
1. Visit the BMW ConnectedDrive portal: **https://www.bmw.de/de-de/mybmw/vehicle-overview**
|
|
43
|
+
1. Visit the BMW ConnectedDrive portal: **https://www.bmw.de/de-de/mybmw/vehicle-overview** or https://www.mini.de/de-de/mymini/vehicle-overview
|
|
44
44
|
2. Navigate to the **BMW CarData** section (you'll see various service categories)
|
|
45
45
|
|
|
46
46
|

|
|
@@ -54,7 +54,7 @@ A detailed datapoint description you can find here [telematic.json](telematic.js
|
|
|
54
54
|
|
|
55
55
|

|
|
56
56
|
|
|
57
|
-
# **CRITICAL**: Click one service and wait 30 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.
|
|
57
|
+
# **CRITICAL**: Click one service and wait 30 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. If it is not working try all letters as lowercase.
|
|
58
58
|
|
|
59
59
|
### 2. CarData Streaming Configuration
|
|
60
60
|
|
|
@@ -214,6 +214,19 @@ This adapter is available at: [https://github.com/TA2k/ioBroker.bmw](https://git
|
|
|
214
214
|
Placeholder for the next version (at the beginning of the line):
|
|
215
215
|
### **WORK IN PROGRESS**
|
|
216
216
|
-->
|
|
217
|
+
### 4.3.5 (2026-04-11)
|
|
218
|
+
|
|
219
|
+
- (hombach) fix repo checker warnings
|
|
220
|
+
- (hombach) fix vulnerability
|
|
221
|
+
- (hombach) update dependencies
|
|
222
|
+
- (hombach) remove old admin files
|
|
223
|
+
|
|
224
|
+
### 4.3.4 (2026-02-28)
|
|
225
|
+
|
|
226
|
+
- enhance docu and logging
|
|
227
|
+
- (hombach) fix vulnerability
|
|
228
|
+
- (hombach) update dependencies
|
|
229
|
+
|
|
217
230
|
### 4.3.3 (2026-01-02)
|
|
218
231
|
|
|
219
232
|
- (hombach) year 2026 changes
|
package/admin/jsonConfig.json
CHANGED
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
"zh-cn": "BMW CarData客户端ID"
|
|
98
98
|
},
|
|
99
99
|
"help": {
|
|
100
|
-
"en": "Client ID from BMW ConnectedDrive portal CarData section.
|
|
101
|
-
"de": "Client ID aus dem BMW ConnectedDrive Portal CarData Bereich.
|
|
100
|
+
"en": "Client ID from BMW ConnectedDrive portal CarData section. If it doesn't work, try making all letters lowercase.",
|
|
101
|
+
"de": "Client ID aus dem BMW ConnectedDrive Portal CarData Bereich. Falls es nicht funktioniert, dann alle Buchstaben kleinschreiben.",
|
|
102
102
|
"ru": "Client ID из раздела CarData портала BMW ConnectedDrive. Требуется для OAuth2 аутентификации.",
|
|
103
103
|
"pt": "Client ID da seção CarData do portal BMW ConnectedDrive. Necessário para autenticação OAuth2.",
|
|
104
104
|
"nl": "Client ID uit BMW ConnectedDrive portal CarData sectie. Vereist voor OAuth2 authenticatie.",
|
|
@@ -255,6 +255,40 @@
|
|
|
255
255
|
"lg": 6,
|
|
256
256
|
"xl": 6
|
|
257
257
|
},
|
|
258
|
+
"reverseGeocode": {
|
|
259
|
+
"type": "checkbox",
|
|
260
|
+
"label": {
|
|
261
|
+
"en": "Enable Reverse Geocoding",
|
|
262
|
+
"de": "Reverse Geocoding aktivieren",
|
|
263
|
+
"ru": "Включить обратное геокодирование",
|
|
264
|
+
"pt": "Ativar geocodificação reversa",
|
|
265
|
+
"nl": "Reverse geocoding inschakelen",
|
|
266
|
+
"fr": "Activer le géocodage inversé",
|
|
267
|
+
"it": "Abilita geocodifica inversa",
|
|
268
|
+
"es": "Habilitar geocodificación inversa",
|
|
269
|
+
"pl": "Włącz odwrotne geokodowanie",
|
|
270
|
+
"zh-cn": "启用反向地理编码"
|
|
271
|
+
},
|
|
272
|
+
"help": {
|
|
273
|
+
"en": "Convert GPS coordinates (latitude/longitude) to street address using OpenStreetMap Nominatim. Updates after position has not changed for 30 seconds.",
|
|
274
|
+
"de": "GPS-Koordinaten (Breitengrad/Längengrad) in Straßenadresse umwandeln mit OpenStreetMap Nominatim. Aktualisierung wenn Position sich 30 Sekunden nicht geändert hat.",
|
|
275
|
+
"ru": "Преобразование GPS координат (широта/долгота) в адрес улицы с помощью OpenStreetMap Nominatim. Обновление после того, как позиция не менялась 30 секунд.",
|
|
276
|
+
"pt": "Converter coordenadas GPS (latitude/longitude) em endereço usando OpenStreetMap Nominatim. Atualiza após a posição não mudar por 30 segundos.",
|
|
277
|
+
"nl": "GPS-coördinaten (breedtegraad/lengtegraad) omzetten naar adres met OpenStreetMap Nominatim. Update nadat positie 30 seconden niet is veranderd.",
|
|
278
|
+
"fr": "Convertir les coordonnées GPS (latitude/longitude) en adresse avec OpenStreetMap Nominatim. Mise à jour après que la position n'a pas changé pendant 30 secondes.",
|
|
279
|
+
"it": "Converti le coordinate GPS (latitudine/longitudine) in indirizzo usando OpenStreetMap Nominatim. Aggiornamento dopo che la posizione non è cambiata per 30 secondi.",
|
|
280
|
+
"es": "Convertir coordenadas GPS (latitud/longitud) a dirección usando OpenStreetMap Nominatim. Se actualiza después de que la posición no cambie durante 30 segundos.",
|
|
281
|
+
"pl": "Konwertuj współrzędne GPS (szerokość/długość geograficzną) na adres za pomocą OpenStreetMap Nominatim. Aktualizacja po 30 sekundach bez zmiany pozycji.",
|
|
282
|
+
"zh-cn": "使用OpenStreetMap Nominatim将GPS坐标(纬度/经度)转换为街道地址。位置30秒未变化后更新。"
|
|
283
|
+
},
|
|
284
|
+
"default": false,
|
|
285
|
+
"newLine": true,
|
|
286
|
+
"xs": 12,
|
|
287
|
+
"sm": 12,
|
|
288
|
+
"md": 6,
|
|
289
|
+
"lg": 6,
|
|
290
|
+
"xl": 6
|
|
291
|
+
},
|
|
258
292
|
"_api_endpoints": {
|
|
259
293
|
"type": "header",
|
|
260
294
|
"text": {
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "bmw",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.5",
|
|
5
5
|
"news": {
|
|
6
|
+
"4.3.5": {
|
|
7
|
+
"en": "fix repo checker warnings\nfix vulnerability\nupdate dependencies\nremove old admin files",
|
|
8
|
+
"de": "repo checker warnungen beheben\nfehlerbehebung\naktualisierung der abhängigkeiten\nalte admin-dateien entfernen",
|
|
9
|
+
"ru": "объявления repo checker\nисправить уязвимость\nобновление зависимостей\nудалить старые файлы admin",
|
|
10
|
+
"pt": "corrigir avisos de repo checker\ncorrigir vulnerabilidade\natualizar dependências\nremover arquivos de administração antigos",
|
|
11
|
+
"nl": "repareren repo-checker waarschuwingen\nkwetsbaarheid herstellen\nafhankelijkheden bijwerken\noude beheerbestanden verwijderen",
|
|
12
|
+
"fr": "corriger les avertissements de repo checker\nfixer la vulnérabilité\nmettre à jour les dépendances\nsupprimer les anciens fichiers d'administration",
|
|
13
|
+
"it": "correggere gli avvisi di errore repo\ncorrezione della vulnerabilità\naggiornamento dipendenze\nrimuovere vecchi file di amministrazione",
|
|
14
|
+
"es": "arregla las advertencias del chequeador de reposo\ncorrección de vulnerabilidad\ndependencias de actualización\neliminar archivos de administración antiguos",
|
|
15
|
+
"pl": "naprawić ostrzeżenia kontrolera repo\nnaprawić podatność\naktualizacji zależności\nusuwanie starych plików admin",
|
|
16
|
+
"uk": "виправити попередження репо-реєстраторів\nвиправити вразливість\nоновлення залежності\nвидалити старі файли адміністратора",
|
|
17
|
+
"zh-cn": "修复 repo 检查器警告\n消除脆弱性\n更新依赖关系\n删除旧管理文件"
|
|
18
|
+
},
|
|
19
|
+
"4.3.4": {
|
|
20
|
+
"en": "enhance docu and logging\nfix vulnerability\nupdate dependencies",
|
|
21
|
+
"de": "docu und protokollierung verbessern\nfehlerbehebung\naktualisierung der abhängigkeiten",
|
|
22
|
+
"ru": "усиление доку и лесозаготовок\nисправить уязвимость\nобновление зависимостей",
|
|
23
|
+
"pt": "melhorar o docu e o registo\ncorrigir vulnerabilidade\natualizar dependências",
|
|
24
|
+
"nl": "document en houtkap verbeteren\nkwetsbaarheid herstellen\nafhankelijkheden bijwerken",
|
|
25
|
+
"fr": "améliorer docu et l'exploitation forestière\nfixer la vulnérabilité\nmettre à jour les dépendances",
|
|
26
|
+
"it": "migliorare docu e logging\ncorrezione della vulnerabilità\naggiornamento dipendenze",
|
|
27
|
+
"es": "mejorar docu y logging\ncorrección de vulnerabilidad\ndependencias de actualización",
|
|
28
|
+
"pl": "zwiększyć doku i logowanie\nnaprawić podatność\naktualizacji zależności",
|
|
29
|
+
"uk": "посилити docu і журналювання\nвиправити вразливість\nоновлення залежності",
|
|
30
|
+
"zh-cn": "加强docu和记录\n消除脆弱性\n更新依赖关系"
|
|
31
|
+
},
|
|
6
32
|
"4.3.3": {
|
|
7
33
|
"en": "year 2026 changes\nupdate dependencies",
|
|
8
34
|
"de": "jahr 2026 änderungen\naktualisierung der abhängigkeiten",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "poprawić odświeżanie symboli\nnaprawić pobieranie obrazu",
|
|
68
94
|
"uk": "поліпшення освіження токени\nфіксація зображення fetching",
|
|
69
95
|
"zh-cn": "改进符号刷新\n固定图像获取"
|
|
70
|
-
},
|
|
71
|
-
"4.1.1": {
|
|
72
|
-
"en": "Add API fetching via Container and move other apis to manually fetching",
|
|
73
|
-
"de": "Fügen Sie API-Fetching über Container hinzu und verschieben Sie andere Apis, um manuelles Abholen",
|
|
74
|
-
"ru": "Добавьте извлечение API через контейнер и переместите другие apis для ручного извлечения",
|
|
75
|
-
"pt": "Adicionar a obtenção da API via Container e mover outros apis para obter manualmente",
|
|
76
|
-
"nl": "API-ophalen toevoegen via Container en andere apis verplaatsen naar handmatig ophalen",
|
|
77
|
-
"fr": "Ajouter l'API récupérer via Container et déplacer d'autres apis pour récupérer manuellement",
|
|
78
|
-
"it": "Aggiungere API fetching tramite Container e spostare altri apis per recuperare manualmente",
|
|
79
|
-
"es": "Añadir API buscar a través de Container y mover otros apis para buscar manualmente",
|
|
80
|
-
"pl": "Dodaj API pobieranie przez kontener i przenieś inne apis do ręcznego pobierania",
|
|
81
|
-
"uk": "Додати API fetching через контейнер і перемістити інші apis для вручну fetching",
|
|
82
|
-
"zh-cn": "通过容器添加 API 获取并移动其他 API 手动获取"
|
|
83
|
-
},
|
|
84
|
-
"4.0.5": {
|
|
85
|
-
"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",
|
|
86
|
-
"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",
|
|
87
|
-
"ru": "BREAKING: Полная миграция на BMW CarData API\nBREAKING: Аутентификация через OAuth2 device flow (без имени пользователя/пароля)\nBREAKING: Удалено дистанционное управление (CarData API только для чтения)\nNEW: Потоковая передача MQTT в реальном времени для мгновенных обновлений\nNEW: Управление квотой 50 API вызовов за 24 часа\nNEW: Полный сбор данных со всех конечных точек CarData",
|
|
88
|
-
"pt": "BREAKING: Migração completa para a BMW CarData API\nBREAKING: Autenticação OAuth2 device flow (sem mais utilizador/senha)\nBREAKING: Controles remotos removidos (CarData API é apenas leitura)\nNEW: Streaming MQTT em tempo real para atualizações instantâneas\nNEW: Gestão de quota de 50 chamadas API por 24h\nNEW: Coleta abrangente de dados de todos os endpoints CarData",
|
|
89
|
-
"nl": "BREAKING: Volledige migratie naar de BMW CarData API\nBREAKING: OAuth2 device flow authenticatie (geen gebruikersnaam/wachtwoord meer)\nBREAKING: Afstandsbedieningen verwijderd (CarData API is alleen-lezen)\nNEW: Real-time MQTT streaming voor directe updates\nNEW: Quota van 50 API-aanroepen per 24u\nNEW: Uitgebreide gegevensverzameling van alle CarData endpoints",
|
|
90
|
-
"fr": "BREAKING : Migration complète vers l’API BMW CarData\nBREAKING : Authentification par OAuth2 device flow (plus de nom d’utilisateur/mot de passe)\nBREAKING : Commandes à distance supprimées (CarData API est en lecture seule)\nNEW : Streaming MQTT en temps réel pour des mises à jour instantanées\nNEW : Gestion du quota de 50 appels API par 24h\nNEW : Collecte complète des données de tous les endpoints CarData",
|
|
91
|
-
"it": "BREAKING: Migrazione completa alla BMW CarData API\nBREAKING: Autenticazione OAuth2 device flow (niente più nome utente/password)\nBREAKING: Controlli remoti rimossi (CarData API è solo in lettura)\nNEW: Streaming MQTT in tempo reale per aggiornamenti istantanei\nNEW: Gestione quota di 50 chiamate API per 24h\nNEW: Raccolta completa dei dati da tutti gli endpoint CarData",
|
|
92
|
-
"es": "BREAKING: Migración completa a la BMW CarData API\nBREAKING: Autenticación OAuth2 device flow (sin más usuario/contraseña)\nBREAKING: Controles remotos eliminados (CarData API es solo lectura)\nNEW: Streaming MQTT en tiempo real para actualizaciones instantáneas\nNEW: Gestión de cuota de 50 llamadas API por 24h\nNEW: Recolección completa de datos de todos los endpoints CarData",
|
|
93
|
-
"pl": "BREAKING: Pełna migracja do BMW CarData API\nBREAKING: Uwierzytelnianie OAuth2 device flow (bez nazwy użytkownika/hasła)\nBREAKING: Usunięto zdalne sterowanie (CarData API tylko do odczytu)\nNEW: Strumieniowanie MQTT w czasie rzeczywistym dla natychmiastowych aktualizacji\nNEW: Zarządzanie limitem 50 wywołań API na 24h\nNEW: Kompleksowe zbieranie danych ze wszystkich endpointów CarData",
|
|
94
|
-
"uk": "BREAKING: Повна міграція на BMW CarData API\nBREAKING: Автентифікація через OAuth2 device flow (без імені користувача/пароля)\nBREAKING: Видалено дистанційне керування (CarData API лише для читання)\nNEW: Потокове MQTT у реальному часі для миттєвих оновлень\nNEW: Керування квотою 50 викликів API на 24 години\nNEW: Повний збір даних з усіх кінцевих точок CarData",
|
|
95
|
-
"zh-cn": "BREAKING:完全迁移到 BMW CarData API\nBREAKING:OAuth2 device flow 身份验证(不再需要用户名/密码)\nBREAKING:远程控制已移除(CarData API 仅支持读取)\nNEW:实时 MQTT 流式传输,立即更新\nNEW:24 小时内 50 次 API 调用配额管理\nNEW:从所有 CarData 端点全面收集数据"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
],
|
|
159
159
|
"globalDependencies": [
|
|
160
160
|
{
|
|
161
|
-
"admin": ">=7.6.
|
|
161
|
+
"admin": ">=7.6.20"
|
|
162
162
|
}
|
|
163
163
|
]
|
|
164
164
|
},
|
|
@@ -169,7 +169,8 @@
|
|
|
169
169
|
"cardataStreamingUsername": "",
|
|
170
170
|
"interval": 60,
|
|
171
171
|
"brand": "bmw",
|
|
172
|
-
"ignorelist": ""
|
|
172
|
+
"ignorelist": "",
|
|
173
|
+
"reverseGeocode": false
|
|
173
174
|
},
|
|
174
175
|
"objects": [],
|
|
175
176
|
"instanceObjects": [
|
package/main.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
const utils = require('@iobroker/adapter-core');
|
|
5
5
|
const axios = require('axios');
|
|
6
6
|
|
|
7
|
-
const crypto = require('crypto');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
8
|
const qs = require('qs');
|
|
9
9
|
const Json2iob = require('json2iob');
|
|
10
10
|
const axiosRetry = require('axios-retry').default;
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
13
|
|
|
14
14
|
// BMW CarData API quota limit (calls per 24 hours)
|
|
15
15
|
const API_QUOTA_LIMIT = 50;
|
|
@@ -50,6 +50,10 @@ class Bmw extends utils.Adapter {
|
|
|
50
50
|
// Flag to track initial login (not adapter restart)
|
|
51
51
|
this.initialLogin = false;
|
|
52
52
|
|
|
53
|
+
// Position tracking for reverse geocoding debounce
|
|
54
|
+
this.lastPosition = {}; // { vin: { lat, lon, timestamp } }
|
|
55
|
+
this.reverseGeocodeTimers = {}; // { vin: timer }
|
|
56
|
+
|
|
53
57
|
// Initialize descriptions and states from telematic.json
|
|
54
58
|
this.description = {};
|
|
55
59
|
this.states = {};
|
|
@@ -199,8 +203,8 @@ class Bmw extends utils.Adapter {
|
|
|
199
203
|
this.log.info('Periodic telematic data updates disabled (interval = 0)');
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
this.log.info(
|
|
203
|
-
this.log.info(
|
|
206
|
+
this.log.info(`BMW CarData adapter startup complete`);
|
|
207
|
+
this.log.info(`MQTT streaming: enabled`);
|
|
204
208
|
this.log.info(
|
|
205
209
|
`API quota: ${
|
|
206
210
|
API_QUOTA_LIMIT - this.apiCalls.length
|
|
@@ -213,7 +217,7 @@ class Bmw extends utils.Adapter {
|
|
|
213
217
|
|
|
214
218
|
async login() {
|
|
215
219
|
if (!this.config.clientId) {
|
|
216
|
-
this.log.error(
|
|
220
|
+
this.log.error(`BMW CarData Client ID not configured! Please set up in adapter settings.`);
|
|
217
221
|
return false;
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -222,7 +226,7 @@ class Bmw extends utils.Adapter {
|
|
|
222
226
|
|
|
223
227
|
try {
|
|
224
228
|
// Step 1: Get device code
|
|
225
|
-
this.log.debug(
|
|
229
|
+
this.log.debug(`Starting BMW CarData device authorization flow`);
|
|
226
230
|
this.log.debug(`Auth API Base: ${this.authApiBase}`);
|
|
227
231
|
this.log.debug(`Client ID: ${this.config.clientId}`);
|
|
228
232
|
this.log.debug(`Code Challenge: ${codeChallenge}`);
|
|
@@ -259,9 +263,9 @@ class Bmw extends utils.Adapter {
|
|
|
259
263
|
|
|
260
264
|
// Special handling for 400 Bad Request - likely client configuration issue
|
|
261
265
|
if (error.response.status === 400) {
|
|
262
|
-
this.log.error(
|
|
266
|
+
this.log.error(`===================================================`);
|
|
263
267
|
this.log.error(`BMW CLIENT ID CONFIGURATION ERROR (400 Bad Request)`);
|
|
264
|
-
this.log.error(
|
|
268
|
+
this.log.error(`===================================================`);
|
|
265
269
|
this.log.error(`This error usually means:`);
|
|
266
270
|
this.log.error(`1. CarData API access is not activated for your Client ID`);
|
|
267
271
|
this.log.error(`2. CarData Streaming is not enabled for your Client ID`);
|
|
@@ -271,12 +275,12 @@ class Bmw extends utils.Adapter {
|
|
|
271
275
|
this.log.error(`1. Visit BMW ConnectedDrive portal: https://www.bmw.de/de-de/mybmw/vehicle-overview`);
|
|
272
276
|
this.log.error(`2. Go to CarData section`);
|
|
273
277
|
this.log.error(
|
|
274
|
-
`3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection
|
|
278
|
+
`3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection!`,
|
|
275
279
|
);
|
|
276
280
|
this.log.error(`4. If not activated, enable both services`);
|
|
277
281
|
this.log.error(`5. If already activated, delete and recreate your Client ID`);
|
|
278
282
|
this.log.error(`6. Update the adapter configuration with the new Client ID`);
|
|
279
|
-
this.log.error(
|
|
283
|
+
this.log.error(`===================================================`);
|
|
280
284
|
}
|
|
281
285
|
}
|
|
282
286
|
if (error.request) {
|
|
@@ -895,9 +899,14 @@ class Bmw extends utils.Adapter {
|
|
|
895
899
|
});
|
|
896
900
|
|
|
897
901
|
this.mqtt.on('error', async error => {
|
|
898
|
-
this.log.error(`MQTT error: ${error}`);
|
|
899
902
|
this.setState('info.mqttConnected', false, true);
|
|
900
903
|
|
|
904
|
+
// Check if it's a connection timeout (server not reachable)
|
|
905
|
+
if (error.message && error.message.includes('connack timeout')) {
|
|
906
|
+
this.log.warn(`BMW MQTT server not reachable (Connack timeout) - will retry automatically`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
901
910
|
// Check if it's an authentication error indicating expired token
|
|
902
911
|
if (
|
|
903
912
|
error.message &&
|
|
@@ -914,6 +923,7 @@ class Bmw extends utils.Adapter {
|
|
|
914
923
|
this.log.warn(`MQTT will retry connection with current token via built-in reconnection`);
|
|
915
924
|
}
|
|
916
925
|
} else {
|
|
926
|
+
this.log.error(`MQTT error: ${error}`);
|
|
917
927
|
this.log.debug(`Non-authentication MQTT error - letting built-in client handle reconnection`);
|
|
918
928
|
}
|
|
919
929
|
});
|
|
@@ -963,6 +973,9 @@ class Bmw extends utils.Adapter {
|
|
|
963
973
|
});
|
|
964
974
|
|
|
965
975
|
await this.setState(`${vin}.lastStreamUpdate`, new Date().toISOString(), true);
|
|
976
|
+
|
|
977
|
+
// Check for position updates and trigger reverse geocoding
|
|
978
|
+
this.checkPositionFromData(vin, data.data);
|
|
966
979
|
}
|
|
967
980
|
}
|
|
968
981
|
} catch (error) {
|
|
@@ -970,6 +983,33 @@ class Bmw extends utils.Adapter {
|
|
|
970
983
|
}
|
|
971
984
|
}
|
|
972
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Check for position data and trigger reverse geocoding if available
|
|
988
|
+
*
|
|
989
|
+
* @param {string} vin - The vehicle VIN
|
|
990
|
+
* @param {object} data - The data object containing telematic data
|
|
991
|
+
*/
|
|
992
|
+
checkPositionFromData(vin, data) {
|
|
993
|
+
if (!this.config.reverseGeocode) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const latKey = 'vehicle.cabin.infotainment.navigation.currentLocation.latitude';
|
|
998
|
+
const lonKey = 'vehicle.cabin.infotainment.navigation.currentLocation.longitude';
|
|
999
|
+
|
|
1000
|
+
const latData = data[latKey];
|
|
1001
|
+
const lonData = data[lonKey];
|
|
1002
|
+
|
|
1003
|
+
if (latData?.value != null && lonData?.value != null) {
|
|
1004
|
+
const latitude = parseFloat(latData.value);
|
|
1005
|
+
const longitude = parseFloat(lonData.value);
|
|
1006
|
+
|
|
1007
|
+
if (!isNaN(latitude) && !isNaN(longitude)) {
|
|
1008
|
+
this.checkAndTriggerReverseGeocode(vin, latitude, longitude);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
973
1013
|
/**
|
|
974
1014
|
* Clean up existing containers that start with "ioBroker"
|
|
975
1015
|
*/
|
|
@@ -1095,8 +1135,8 @@ class Bmw extends utils.Adapter {
|
|
|
1095
1135
|
};
|
|
1096
1136
|
|
|
1097
1137
|
// Read telematic.json file
|
|
1098
|
-
const fs = require('fs');
|
|
1099
|
-
const path = require('path');
|
|
1138
|
+
const fs = require('node:fs');
|
|
1139
|
+
const path = require('node:path');
|
|
1100
1140
|
const telematicPath = path.join(__dirname, 'telematic.json');
|
|
1101
1141
|
|
|
1102
1142
|
if (!fs.existsSync(telematicPath)) {
|
|
@@ -1113,7 +1153,7 @@ class Bmw extends utils.Adapter {
|
|
|
1113
1153
|
|
|
1114
1154
|
const containerData = {
|
|
1115
1155
|
name: `ioBroker BMW Telematic Data - ${new Date().toISOString()}`,
|
|
1116
|
-
purpose:
|
|
1156
|
+
purpose: `Container for BMW telematic data endpoints used by ioBroker adapter`,
|
|
1117
1157
|
technicalDescriptors: technicalDescriptors,
|
|
1118
1158
|
};
|
|
1119
1159
|
|
|
@@ -1344,6 +1384,107 @@ class Bmw extends utils.Adapter {
|
|
|
1344
1384
|
return createSuccess;
|
|
1345
1385
|
}
|
|
1346
1386
|
|
|
1387
|
+
/**
|
|
1388
|
+
* Reverse geocode position using OpenStreetMap Nominatim
|
|
1389
|
+
*
|
|
1390
|
+
* @param {number} latitude - The latitude value
|
|
1391
|
+
* @param {number} longitude - The longitude value
|
|
1392
|
+
* @param {string} vin - The vehicle VIN
|
|
1393
|
+
*/
|
|
1394
|
+
async reversePosition(latitude, longitude, vin) {
|
|
1395
|
+
this.log.debug(`Reverse geocoding for ${vin}: ${latitude}, ${longitude}`);
|
|
1396
|
+
|
|
1397
|
+
try {
|
|
1398
|
+
const response = await axios.get('https://nominatim.openstreetmap.org/reverse', {
|
|
1399
|
+
params: { lat: latitude, lon: longitude, format: 'json', zoom: 18 },
|
|
1400
|
+
headers: { 'User-Agent': 'ioBroker/bmw-adapter' },
|
|
1401
|
+
timeout: 10000,
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const body = response.data;
|
|
1405
|
+
if (!body?.display_name) {
|
|
1406
|
+
this.log.debug(`No address found for ${latitude}, ${longitude}`);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const address = body.address || {};
|
|
1411
|
+
const road = address.road || address.street || '';
|
|
1412
|
+
const number = address.house_number || '';
|
|
1413
|
+
const postcode = address.postcode || '';
|
|
1414
|
+
const city = address.city || address.town || address.village || address.municipality || '';
|
|
1415
|
+
const country = address.country || '';
|
|
1416
|
+
|
|
1417
|
+
let fullAddress = road;
|
|
1418
|
+
if (number) {
|
|
1419
|
+
fullAddress += ` ${number}`;
|
|
1420
|
+
}
|
|
1421
|
+
if (city) {
|
|
1422
|
+
fullAddress += `, ${postcode ? `${postcode} ` : ''}${city}`;
|
|
1423
|
+
}
|
|
1424
|
+
if (country) {
|
|
1425
|
+
fullAddress += `, ${country}`;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Store address data using json2iob
|
|
1429
|
+
await this.json2iob.parse(
|
|
1430
|
+
`${vin}.position.address`,
|
|
1431
|
+
{
|
|
1432
|
+
displayName: fullAddress,
|
|
1433
|
+
road: road,
|
|
1434
|
+
house_number: number,
|
|
1435
|
+
postcode: postcode,
|
|
1436
|
+
city: city,
|
|
1437
|
+
country: country,
|
|
1438
|
+
latitude: latitude,
|
|
1439
|
+
longitude: longitude,
|
|
1440
|
+
},
|
|
1441
|
+
{ channelName: 'Reverse Geocoded Address' },
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
this.log.info(`Address updated for ${vin}: ${fullAddress}`);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
this.log.error(`Reverse geocoding failed: ${error.message}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Check and trigger reverse geocoding with debounce
|
|
1452
|
+
* Only triggers if position hasn't changed for 30 seconds
|
|
1453
|
+
*
|
|
1454
|
+
* @param {string} vin - The vehicle VIN
|
|
1455
|
+
* @param {number} latitude - Current latitude
|
|
1456
|
+
* @param {number} longitude - Current longitude
|
|
1457
|
+
*/
|
|
1458
|
+
checkAndTriggerReverseGeocode(vin, latitude, longitude) {
|
|
1459
|
+
if (!this.config.reverseGeocode) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const DEBOUNCE_MS = 30000; // 30 seconds
|
|
1464
|
+
const lastPos = this.lastPosition[vin];
|
|
1465
|
+
|
|
1466
|
+
// Check if position has changed
|
|
1467
|
+
const positionChanged = !lastPos || lastPos.lat !== latitude || lastPos.lon !== longitude;
|
|
1468
|
+
|
|
1469
|
+
if (positionChanged) {
|
|
1470
|
+
// Position changed - update tracking and reset timer
|
|
1471
|
+
this.lastPosition[vin] = { lat: latitude, lon: longitude };
|
|
1472
|
+
|
|
1473
|
+
// Clear existing timer
|
|
1474
|
+
if (this.reverseGeocodeTimers[vin]) {
|
|
1475
|
+
clearTimeout(this.reverseGeocodeTimers[vin]);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Set new debounce timer
|
|
1479
|
+
this.reverseGeocodeTimers[vin] = setTimeout(() => {
|
|
1480
|
+
this.log.debug(`Position stable for 30s, triggering reverse geocode for ${vin}`);
|
|
1481
|
+
this.reversePosition(latitude, longitude, vin);
|
|
1482
|
+
}, DEBOUNCE_MS);
|
|
1483
|
+
|
|
1484
|
+
this.log.debug(`Position changed for ${vin}, debounce timer reset`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1347
1488
|
/**
|
|
1348
1489
|
* Is called when adapter shuts down - callback has to be called under any circumstances!
|
|
1349
1490
|
*
|
|
@@ -1355,6 +1496,11 @@ class Bmw extends utils.Adapter {
|
|
|
1355
1496
|
clearInterval(this.updateInterval);
|
|
1356
1497
|
clearInterval(this.refreshTokenInterval);
|
|
1357
1498
|
|
|
1499
|
+
// Clear reverse geocode timers
|
|
1500
|
+
for (const vin of Object.keys(this.reverseGeocodeTimers)) {
|
|
1501
|
+
clearTimeout(this.reverseGeocodeTimers[vin]);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1358
1504
|
// Close MQTT connection
|
|
1359
1505
|
if (this.mqtt) {
|
|
1360
1506
|
this.mqtt.end();
|
|
@@ -1516,7 +1662,7 @@ class Bmw extends utils.Adapter {
|
|
|
1516
1662
|
|
|
1517
1663
|
this.log.info(`Successfully fetched charging history for ${vin}: ${chargingData.totalSessions} sessions`);
|
|
1518
1664
|
} else {
|
|
1519
|
-
throw new Error(
|
|
1665
|
+
throw new Error(`Failed to fetch charging history`);
|
|
1520
1666
|
}
|
|
1521
1667
|
break;
|
|
1522
1668
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.bmw",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.5",
|
|
4
4
|
"description": "Adapter for BMW",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TA2k",
|
|
@@ -22,21 +22,21 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@iobroker/adapter-core": "^3.3.2",
|
|
25
|
-
"axios": "^1.
|
|
25
|
+
"axios": "^1.15.0",
|
|
26
26
|
"axios-retry": "^4.5.0",
|
|
27
|
-
"json2iob": "^2.6.
|
|
28
|
-
"mqtt": "^5.
|
|
29
|
-
"qs": "^6.14.
|
|
27
|
+
"json2iob": "^2.6.22",
|
|
28
|
+
"mqtt": "^5.15.0",
|
|
29
|
+
"qs": "^6.14.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@alcalzone/release-script": "^5.
|
|
33
|
-
"@alcalzone/release-script-plugin-iobroker": "^
|
|
34
|
-
"@alcalzone/release-script-plugin-license": "^
|
|
35
|
-
"@alcalzone/release-script-plugin-manual-review": "^
|
|
32
|
+
"@alcalzone/release-script": "^5.1.1",
|
|
33
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
|
|
34
|
+
"@alcalzone/release-script-plugin-license": "^5.1.1",
|
|
35
|
+
"@alcalzone/release-script-plugin-manual-review": "^5.1.1",
|
|
36
36
|
"@iobroker/eslint-config": "^2.2.0",
|
|
37
37
|
"@iobroker/testing": "^5.2.2",
|
|
38
|
-
"@tsconfig/node20": "^20.1.
|
|
39
|
-
"@types/node": "^25.
|
|
38
|
+
"@tsconfig/node20": "^20.1.9",
|
|
39
|
+
"@types/node": "^25.2.1",
|
|
40
40
|
"@types/qs": "^6.14.0",
|
|
41
41
|
"globals": "^16.5.0",
|
|
42
42
|
"typescript": "~5.9.3"
|
package/admin/admin.d.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
declare let systemDictionary: Record<string, Record<string, string>>;
|
|
2
|
-
|
|
3
|
-
declare let load: (settings: Record<string, unknown>, onChange: (hasChanges: boolean) => void) => void;
|
|
4
|
-
declare let save: (callback: (settings: Record<string, unknown>) => void) => void;
|
|
5
|
-
|
|
6
|
-
// make load and save exist on the window object
|
|
7
|
-
interface Window {
|
|
8
|
-
load: typeof load;
|
|
9
|
-
save: typeof save;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
declare const instance: number;
|
|
13
|
-
declare const adapter: string;
|
|
14
|
-
/** Translates text */
|
|
15
|
-
declare function _(text: string, arg1?: string, arg2?: string, arg3?: string): string;
|
|
16
|
-
declare const socket: ioBrokerSocket;
|
|
17
|
-
declare function sendTo(
|
|
18
|
-
instance: any | null,
|
|
19
|
-
command: string,
|
|
20
|
-
message: any,
|
|
21
|
-
callback: (result: SendToResult) => void | Promise<void>,
|
|
22
|
-
): void;
|
|
23
|
-
|
|
24
|
-
interface SendToResult {
|
|
25
|
-
error?: string | Error;
|
|
26
|
-
result?: any;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// tslint:disable-next-line:class-name
|
|
30
|
-
interface ioBrokerSocket {
|
|
31
|
-
emit(
|
|
32
|
-
command: "subscribeObjects",
|
|
33
|
-
pattern: string,
|
|
34
|
-
callback?: (err?: string) => void | Promise<void>,
|
|
35
|
-
): void;
|
|
36
|
-
emit(
|
|
37
|
-
command: "subscribeStates",
|
|
38
|
-
pattern: string,
|
|
39
|
-
callback?: (err?: string) => void | Promise<void>,
|
|
40
|
-
): void;
|
|
41
|
-
emit(
|
|
42
|
-
command: "unsubscribeObjects",
|
|
43
|
-
pattern: string,
|
|
44
|
-
callback?: (err?: string) => void | Promise<void>,
|
|
45
|
-
): void;
|
|
46
|
-
emit(
|
|
47
|
-
command: "unsubscribeStates",
|
|
48
|
-
pattern: string,
|
|
49
|
-
callback?: (err?: string) => void | Promise<void>,
|
|
50
|
-
): void;
|
|
51
|
-
|
|
52
|
-
emit(
|
|
53
|
-
event: "getObjectView",
|
|
54
|
-
view: "system",
|
|
55
|
-
type: "device",
|
|
56
|
-
options: ioBroker.GetObjectViewParams,
|
|
57
|
-
callback: (
|
|
58
|
-
err: string | undefined,
|
|
59
|
-
result?: any,
|
|
60
|
-
) => void | Promise<void>,
|
|
61
|
-
): void;
|
|
62
|
-
emit(
|
|
63
|
-
event: "getStates",
|
|
64
|
-
callback: (
|
|
65
|
-
err: string | undefined,
|
|
66
|
-
result?: Record<string, any>,
|
|
67
|
-
) => void,
|
|
68
|
-
): void;
|
|
69
|
-
emit(
|
|
70
|
-
event: "getState",
|
|
71
|
-
id: string,
|
|
72
|
-
callback: (err: string | undefined, result?: ioBroker.State) => void,
|
|
73
|
-
): void;
|
|
74
|
-
emit(
|
|
75
|
-
event: "setState",
|
|
76
|
-
id: string,
|
|
77
|
-
state: unknown,
|
|
78
|
-
callback: (err: string | undefined, result?: any) => void,
|
|
79
|
-
): void;
|
|
80
|
-
|
|
81
|
-
on(event: "objectChange", handler: ioBroker.ObjectChangeHandler): void;
|
|
82
|
-
on(event: "stateChange", handler: ioBroker.StateChangeHandler): void;
|
|
83
|
-
removeEventHandler(
|
|
84
|
-
event: "objectChange",
|
|
85
|
-
handler: ioBroker.ObjectChangeHandler,
|
|
86
|
-
): void;
|
|
87
|
-
removeEventHandler(
|
|
88
|
-
event: "stateChange",
|
|
89
|
-
handler: ioBroker.StateChangeHandler,
|
|
90
|
-
): void;
|
|
91
|
-
|
|
92
|
-
// TODO: other events
|
|
93
|
-
}
|
package/admin/style.css
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/* You can delete those if you want. I just found them very helpful */
|
|
2
|
-
* {
|
|
3
|
-
box-sizing: border-box
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
body {
|
|
7
|
-
overflow: hidden;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.adapter-body {
|
|
11
|
-
overflow: auto;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.m {
|
|
15
|
-
/* Don't cut off dropdowns! */
|
|
16
|
-
overflow: initial;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.m.adapter-container,
|
|
20
|
-
.m.adapter-container>div.App {
|
|
21
|
-
/* Fix layout/scrolling issues with tabs */
|
|
22
|
-
height: 100%;
|
|
23
|
-
width: 100%;
|
|
24
|
-
position: relative;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.m .select-wrapper+label {
|
|
28
|
-
/* The positioning for dropdown labels is messed up */
|
|
29
|
-
transform: none !important;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
label>i[title] {
|
|
33
|
-
/* Display the help cursor for the tooltip icons and fix their positioning */
|
|
34
|
-
cursor: help;
|
|
35
|
-
margin-left: 0.25em;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.dropdown-content {
|
|
39
|
-
/* Don't wrap text in dropdowns */
|
|
40
|
-
white-space: nowrap;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Add your styles here */
|
package/admin/tsconfig.json
DELETED
package/admin/words.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line no-unused-vars
|
|
2
|
-
/*global systemDictionary:true */
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
systemDictionary = {
|
|
6
|
-
'bmw adapter settings': {
|
|
7
|
-
en: 'Adapter settings for bmw',
|
|
8
|
-
de: 'Adaptereinstellungen für bmw',
|
|
9
|
-
ru: 'Настройки адаптера для bmw',
|
|
10
|
-
pt: 'Configurações do adaptador para bmw',
|
|
11
|
-
nl: 'Adapterinstellingen voor bmw',
|
|
12
|
-
fr: "Paramètres d'adaptateur pour bmw",
|
|
13
|
-
it: "Impostazioni dell'adattatore per bmw",
|
|
14
|
-
es: 'Ajustes del adaptador para bmw',
|
|
15
|
-
pl: 'Ustawienia adaptera dla bmw',
|
|
16
|
-
'zh-cn': 'bmw2的适配器设置',
|
|
17
|
-
},
|
|
18
|
-
'App Email': {
|
|
19
|
-
en: 'App Email',
|
|
20
|
-
de: 'App-E-Mail',
|
|
21
|
-
ru: 'Электронная почта приложения',
|
|
22
|
-
pt: 'Email do aplicativo',
|
|
23
|
-
nl: 'App-e-mail',
|
|
24
|
-
fr: "Courriel de l'application",
|
|
25
|
-
it: "E-mail dell'app",
|
|
26
|
-
es: 'Correo electrónico de la aplicación',
|
|
27
|
-
pl: 'E-mail aplikacji',
|
|
28
|
-
'zh-cn': '应用电子邮件',
|
|
29
|
-
},
|
|
30
|
-
'App Password': {
|
|
31
|
-
en: 'App Password',
|
|
32
|
-
de: 'App-Passwort',
|
|
33
|
-
ru: 'Пароль приложения',
|
|
34
|
-
pt: 'Senha de app',
|
|
35
|
-
nl: 'App-wachtwoord',
|
|
36
|
-
fr: "Mot de passe de l'application",
|
|
37
|
-
it: "Password dell'app",
|
|
38
|
-
es: 'Contraseña de la aplicación',
|
|
39
|
-
pl: 'Hasło do aplikacji',
|
|
40
|
-
'zh-cn': '应用密码',
|
|
41
|
-
},
|
|
42
|
-
'Update interval (in minutes)': {
|
|
43
|
-
en: 'Update interval (in minutes)',
|
|
44
|
-
de: 'Aktualisierungsintervall (in Minuten)',
|
|
45
|
-
ru: 'Интервал обновления (в минутах)',
|
|
46
|
-
pt: 'Intervalo de atualização (em minutos)',
|
|
47
|
-
nl: 'Update-interval (in minuten)',
|
|
48
|
-
fr: 'Intervalle de mise à jour (en minutes)',
|
|
49
|
-
it: 'Intervallo di aggiornamento (in minuti)',
|
|
50
|
-
es: 'Intervalo de actualización (en minutos)',
|
|
51
|
-
pl: 'Interwał aktualizacji (w minutach)',
|
|
52
|
-
'zh-cn': '更新间隔(分钟)',
|
|
53
|
-
},
|
|
54
|
-
Brand: {
|
|
55
|
-
en: 'Brand',
|
|
56
|
-
de: 'Marke',
|
|
57
|
-
ru: 'Марка',
|
|
58
|
-
pt: 'Marca',
|
|
59
|
-
nl: 'Brand',
|
|
60
|
-
fr: 'Marque',
|
|
61
|
-
it: 'Marca',
|
|
62
|
-
es: 'Marca',
|
|
63
|
-
pl: 'Brandy',
|
|
64
|
-
'zh-cn': '陪审团',
|
|
65
|
-
},
|
|
66
|
-
};
|