iobroker.bmw 4.1.0 → 4.1.1

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 CHANGED
@@ -213,7 +213,8 @@ If you're not seeing expected data in `VIN.api.*`:
213
213
  This adapter is available at: [https://github.com/TA2k/ioBroker.bmw](https://github.com/TA2k/ioBroker.bmw)
214
214
 
215
215
  ## Changelog
216
- ### 4.1.0 (2025-10-03)
216
+
217
+ ### 4.1.1 (2025-10-03)
217
218
 
218
219
  - Add API fetching via Container and move other apis to manually fetching
219
220
 
package/io-package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "bmw",
4
- "version": "4.1.0",
4
+ "version": "4.1.1",
5
5
  "news": {
6
- "4.1.0": {
6
+ "4.1.1": {
7
7
  "en": "Add API fetching via Container and move other apis to manually fetching",
8
8
  "de": "Fügen Sie API-Fetching über Container hinzu und verschieben Sie andere Apis, um manuelles Abholen",
9
9
  "ru": "Добавьте извлечение API через контейнер и переместите другие apis для ручного извлечения",
@@ -121,12 +121,8 @@
121
121
  "uk": "Adapter for BMW CarData API з потоком MQTT в реальному часі",
122
122
  "zh-cn": "带有实时MQTT流的BMW CarData API适配器"
123
123
  },
124
- "authors": [
125
- "TA2k <tombox2020@gmail.com>"
126
- ],
127
- "keywords": [
128
- "BMW"
129
- ],
124
+ "authors": ["TA2k <tombox2020@gmail.com>"],
125
+ "keywords": ["BMW"],
130
126
  "licenseInformation": {
131
127
  "license": "MIT",
132
128
  "type": "free"
package/main.js CHANGED
@@ -129,62 +129,72 @@ class Bmw extends utils.Adapter {
129
129
  // Connect MQTT after successful auth
130
130
  await this.connectMQTT();
131
131
  // Start periodic token refresh (every 45 minutes)
132
- this.refreshTokenInterval = setInterval(async () => {
133
- await this.refreshToken();
134
- }, 45 * 60 * 1000);
132
+ this.refreshTokenInterval = setInterval(
133
+ async () => {
134
+ await this.refreshToken();
135
+ },
136
+ 56 * 60 * 1000,
137
+ );
135
138
 
136
139
  // Start periodic telematic data updates (respecting quota limits)
137
140
  if (this.vinArray.length > 0 && this.config.interval > 0) {
138
- this.log.info(`Setting up periodic telematic data updates every ${this.config.interval} minutes for ${this.vinArray.length} vehicle(s)`);
139
- this.updateInterval = setInterval(async () => {
140
- // Update quota states (expired calls removed automatically)
141
- this.updateQuotaStates();
142
-
143
- // Periodic telematic data refresh - MQTT provides real-time updates
144
- if (!this.containerId) {
145
- this.log.warn('No container ID available for periodic telematic data fetch, setting up container...');
146
- const setupSuccess = await this.setupTelematicContainer();
147
- if (!setupSuccess) {
148
- this.log.error('Failed to setup telematic container for periodic updates');
149
- return;
141
+ this.log.info(
142
+ `Setting up periodic telematic data updates every ${this.config.interval} minutes for ${this.vinArray.length} vehicle(s)`,
143
+ );
144
+ this.updateInterval = setInterval(
145
+ async () => {
146
+ // Update quota states (expired calls removed automatically)
147
+ this.updateQuotaStates();
148
+
149
+ // Periodic telematic data refresh - MQTT provides real-time updates
150
+ if (!this.containerId) {
151
+ this.log.warn('No container ID available for periodic telematic data fetch, setting up container...');
152
+ const setupSuccess = await this.setupTelematicContainer();
153
+ if (!setupSuccess) {
154
+ this.log.error('Failed to setup telematic container for periodic updates');
155
+ return;
156
+ }
150
157
  }
151
- }
152
158
 
153
- for (const vin of this.vinArray) {
154
- this.log.debug(`Periodic telematic data refresh for ${vin}`);
155
- try {
156
- const telematicData = await this.getTelematicContainer(vin, this.containerId);
157
- if (telematicData && telematicData.telematicData) {
158
- // Store telematic data directly in stream folder
159
- await this.json2iob.parse(`${vin}.stream`, telematicData.telematicData, {
160
- descriptions: this.description,
161
- forceIndex: true,
162
- });
163
-
164
- // Update lastAPIUpdate timestamp
165
- await this.extendObject(`${vin}.stream.lastAPIUpdate`, {
166
- type: 'state',
167
- common: {
168
- name: 'Last Telematic API Update',
169
- type: 'string',
170
- role: 'date',
171
- read: true,
172
- write: false,
173
- },
174
- native: {},
175
- });
176
- await this.setState(`${vin}.stream.lastAPIUpdate`, new Date().toISOString(), true);
177
-
178
- this.log.debug(`✓ Periodic telematic data update for ${vin}: ${Object.keys(telematicData.telematicData).length} data points`);
179
- } else {
180
- this.log.warn(`No telematic data retrieved for ${vin} during periodic update`);
159
+ for (const vin of this.vinArray) {
160
+ this.log.debug(`Periodic telematic data refresh for ${vin}`);
161
+ try {
162
+ const telematicData = await this.getTelematicContainer(vin, this.containerId);
163
+ if (telematicData && telematicData.telematicData) {
164
+ // Store telematic data directly in stream folder
165
+ await this.json2iob.parse(`${vin}.stream`, telematicData.telematicData, {
166
+ descriptions: this.description,
167
+ forceIndex: true,
168
+ });
169
+
170
+ // Update lastAPIUpdate timestamp
171
+ await this.extendObject(`${vin}.stream.lastAPIUpdate`, {
172
+ type: 'state',
173
+ common: {
174
+ name: 'Last Telematic API Update',
175
+ type: 'string',
176
+ role: 'date',
177
+ read: true,
178
+ write: false,
179
+ },
180
+ native: {},
181
+ });
182
+ await this.setState(`${vin}.stream.lastAPIUpdate`, new Date().toISOString(), true);
183
+
184
+ this.log.debug(
185
+ `✓ Periodic telematic data update for ${vin}: ${Object.keys(telematicData.telematicData).length} data points`,
186
+ );
187
+ } else {
188
+ this.log.warn(`No telematic data retrieved for ${vin} during periodic update`);
189
+ }
190
+ } catch (error) {
191
+ this.log.error(`Periodic telematic data fetch failed for ${vin}: ${error.message}`);
181
192
  }
182
- } catch (error) {
183
- this.log.error(`Periodic telematic data fetch failed for ${vin}: ${error.message}`);
193
+ break; // Only one vehicle per interval to conserve quota
184
194
  }
185
- break; // Only one vehicle per interval to conserve quota
186
- }
187
- }, this.config.interval * 60 * 1000);
195
+ },
196
+ this.config.interval * 60 * 1000,
197
+ );
188
198
  } else if (this.config.interval === 0) {
189
199
  this.log.info('Periodic telematic data updates disabled (interval = 0)');
190
200
  }
@@ -194,7 +204,7 @@ class Bmw extends utils.Adapter {
194
204
  this.log.info(
195
205
  `API quota: ${
196
206
  API_QUOTA_LIMIT - this.apiCalls.length
197
- }/${API_QUOTA_LIMIT} calls remaining for static data. Updates via MQTT do not count against quota.`
207
+ }/${API_QUOTA_LIMIT} calls remaining for API calls. Updates via MQTT do not count against quota.`,
198
208
  );
199
209
  } else {
200
210
  this.log.error('BMW CarData authentication failed');
@@ -235,11 +245,11 @@ class Bmw extends utils.Adapter {
235
245
  },
236
246
  data: requestData,
237
247
  })
238
- .then((res) => {
248
+ .then(res => {
239
249
  this.log.debug(`Device code response: ${JSON.stringify(res.data)}`);
240
250
  return res;
241
251
  })
242
- .catch((error) => {
252
+ .catch(error => {
243
253
  this.log.error(`Device code request failed: ${error.message}`);
244
254
  this.log.error(`Error stack: ${error.stack}`);
245
255
  if (error.response) {
@@ -260,7 +270,9 @@ class Bmw extends utils.Adapter {
260
270
  this.log.error('To fix this issue:');
261
271
  this.log.error('1. Visit BMW ConnectedDrive portal: https://www.bmw.de/de-de/mybmw/vehicle-overview');
262
272
  this.log.error('2. Go to CarData section');
263
- this.log.error('3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection');
273
+ this.log.error(
274
+ '3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection',
275
+ );
264
276
  this.log.error('4. If not activated, enable both services');
265
277
  this.log.error('5. If already activated, delete and recreate your Client ID');
266
278
  this.log.error('6. Update the adapter configuration with the new Client ID');
@@ -273,7 +285,7 @@ class Bmw extends utils.Adapter {
273
285
  method: error.request.method,
274
286
  url: error.request.url,
275
287
  headers: error.request._headers,
276
- })}`
288
+ })}`,
277
289
  );
278
290
  }
279
291
  return false; // Return false instead of throwing
@@ -392,7 +404,7 @@ class Bmw extends utils.Adapter {
392
404
  method: error.request.method,
393
405
  url: error.request.url,
394
406
  headers: error.request._headers,
395
- })}`
407
+ })}`,
396
408
  );
397
409
  }
398
410
  return false;
@@ -413,9 +425,9 @@ class Bmw extends utils.Adapter {
413
425
  url: `${this.carDataApiBase}/customers/vehicles/mappings`,
414
426
  headers: headers,
415
427
  },
416
- 'fetch vehicle mappings'
428
+ 'fetch vehicle mappings',
417
429
  )
418
- .then(async (res) => {
430
+ .then(async res => {
419
431
  this.log.debug(JSON.stringify(res.data));
420
432
  const mappings = res.data;
421
433
 
@@ -484,7 +496,7 @@ class Bmw extends utils.Adapter {
484
496
  }
485
497
  }
486
498
  })
487
- .catch((error) => {
499
+ .catch(error => {
488
500
  this.log.error(`BMW CarData vehicle discovery failed: ${error.message}`);
489
501
  if (error.response) {
490
502
  this.log.error(`Response: ${JSON.stringify(error.response.data)}`);
@@ -606,7 +618,7 @@ class Bmw extends utils.Adapter {
606
618
 
607
619
  // Remove calls older than 24h
608
620
  const originalLength = this.apiCalls.length;
609
- this.apiCalls = this.apiCalls.filter((time) => now - time < 24 * 60 * 60 * 1000);
621
+ this.apiCalls = this.apiCalls.filter(time => now - time < 24 * 60 * 60 * 1000);
610
622
 
611
623
  // Save history if calls were removed due to expiration
612
624
  if (this.apiCalls.length !== originalLength) {
@@ -637,7 +649,7 @@ class Bmw extends utils.Adapter {
637
649
  return true;
638
650
  }
639
651
 
640
- this.log.warn(`API quota for static dataexhausted: ${used}/${API_QUOTA_LIMIT} calls used in last 24h. Stream data still working.`);
652
+ this.log.warn(`API quota for api data exhausted: ${used}/${API_QUOTA_LIMIT} calls used in last 24h. Stream data still working.`);
641
653
  return false;
642
654
  }
643
655
 
@@ -676,7 +688,7 @@ class Bmw extends utils.Adapter {
676
688
  }
677
689
 
678
690
  sleep(ms) {
679
- return new Promise((resolve) => setTimeout(resolve, ms));
691
+ return new Promise(resolve => setTimeout(resolve, ms));
680
692
  }
681
693
 
682
694
  async cleanObjects(vin) {
@@ -750,23 +762,36 @@ class Bmw extends utils.Adapter {
750
762
  },
751
763
  data: qs.stringify(refreshData),
752
764
  })
753
- .then(async (res) => {
765
+ .then(async res => {
754
766
  // Store refreshed tokens (keep existing session structure)
755
767
  this.session = res.data;
756
768
  this.setState('cardataauth.session', JSON.stringify(this.session), true);
757
769
  this.setState('info.connection', true, true);
758
- this.log.debug('Tokens refreshed successfully - MQTT will auto-reconnect with new credentials via transformWsUrl');
759
-
770
+ this.log.debug('Tokens refreshed successfully - MQTT will auto-reconnect with new credentials');
771
+ this.mqtt?.options && (this.mqtt.options.password = this.session.id_token);
760
772
  return res.data;
761
773
  })
762
- .catch(async (error) => {
774
+ .catch(async error => {
763
775
  // Log complete error object first
764
- this.log.error(`Token refresh failed - complete error: ${error}`);
765
- +error.stack && this.log.error(`Error stack: ${error.stack}`);
766
- error.response && this.log.error(`Response: ${JSON.stringify(error.response.data)}`);
776
+ this.log.error(error);
777
+
778
+ // HTTP response errors - check status code
779
+ if (error.response) {
780
+ this.log.error(`Response status: ${JSON.stringify(error.response)}`);
781
+ const status = error.response.status;
782
+ if (status >= 400 && status < 500) {
783
+ // 4xx errors indicate authentication problems - reset needed
784
+ this.log.error(`Token refresh failed with HTTP ${status} auth error - starting new device flow`);
785
+ this.setState('info.connection', false, true);
786
+ return await this.login();
787
+ }
788
+ }
789
+
790
+ this.log.warn(
791
+ 'Token refresh failed, will retry on next refresh cycle. You can also delete bmw.0.cardataauth.session state to force re-login.',
792
+ );
767
793
  this.setState('info.connection', false, true);
768
- this.log.info('Starting new device authorization flow due to token refresh failure');
769
- return await this.login();
794
+ return;
770
795
  });
771
796
  }
772
797
 
@@ -784,14 +809,6 @@ class Bmw extends utils.Adapter {
784
809
 
785
810
  const mqtt = require('mqtt');
786
811
 
787
- // Transform function to refresh credentials on each reconnection attempt
788
- const transformWsUrl = (url, options, client) => {
789
- // Update password with the latest id_token on each reconnect
790
- client.options.password = this.session.id_token;
791
- this.log.debug('MQTT transformWsUrl: Updated credentials with latest token');
792
- return url; // URL stays the same, just update credentials
793
- };
794
-
795
812
  const options = {
796
813
  host: 'customer.streaming-cardata.bmwgroup.com',
797
814
  port: 9000,
@@ -803,7 +820,6 @@ class Bmw extends utils.Adapter {
803
820
  rejectUnauthorized: true,
804
821
  reconnectPeriod: 30000, // Built-in reconnection every 30 seconds
805
822
  connectTimeout: 30000,
806
- transformWsUrl: transformWsUrl, // Hook to refresh credentials on reconnect
807
823
  };
808
824
 
809
825
  this.log.debug(`Connecting to BMW MQTT: ${options.host}:${options.port}`);
@@ -816,7 +832,7 @@ class Bmw extends utils.Adapter {
816
832
 
817
833
  // Subscribe to all vehicle topics for this CarData Streaming username
818
834
  const topic = `${this.config.cardataStreamingUsername}/+`;
819
- this.mqtt.subscribe(topic, (err) => {
835
+ this.mqtt.subscribe(topic, err => {
820
836
  if (err) {
821
837
  this.log.error(`MQTT subscription failed: ${err.message}`);
822
838
  } else {
@@ -829,7 +845,7 @@ class Bmw extends utils.Adapter {
829
845
  this.handleMQTTMessage(topic, message);
830
846
  });
831
847
 
832
- this.mqtt.on('error', async (error) => {
848
+ this.mqtt.on('error', async error => {
833
849
  this.log.error(`MQTT error: ${error}`);
834
850
  this.setState('info.mqttConnected', false, true);
835
851
 
@@ -961,7 +977,7 @@ class Bmw extends utils.Adapter {
961
977
  url: `${this.carDataApiBase}/customers/containers`,
962
978
  headers: headers,
963
979
  },
964
- 'list containers'
980
+ 'list containers',
965
981
  );
966
982
 
967
983
  const containers = response.data.containers || [];
@@ -978,7 +994,7 @@ class Bmw extends utils.Adapter {
978
994
  url: `${this.carDataApiBase}/customers/containers/${container.id}`,
979
995
  headers: headers,
980
996
  },
981
- `delete container ${container.id}`
997
+ `delete container ${container.id}`,
982
998
  );
983
999
  this.log.debug(`Deleted ioBroker container: ${container.id} (${container.name})`);
984
1000
  } catch (error) {
@@ -1038,7 +1054,7 @@ class Bmw extends utils.Adapter {
1038
1054
  await this.setState(`${testVin}.stream.lastAPIUpdate`, new Date().toISOString(), true);
1039
1055
 
1040
1056
  this.log.info(
1041
- `Existing container is valid and working - retrieved ${Object.keys(telematicData.telematicData).length} telematic data points`
1057
+ `Existing container is valid and working - retrieved ${Object.keys(telematicData.telematicData).length} telematic data points`,
1042
1058
  );
1043
1059
  return true;
1044
1060
  } else {
@@ -1074,7 +1090,7 @@ class Bmw extends utils.Adapter {
1074
1090
  const telematicData = JSON.parse(fs.readFileSync(telematicPath, 'utf8'));
1075
1091
 
1076
1092
  // Extract all technical identifiers and ensure no trailing commas in JSON
1077
- const technicalDescriptors = telematicData.map((item) => item.technical_identifier).filter((identifier) => identifier); // Remove any undefined/null values
1093
+ const technicalDescriptors = telematicData.map(item => item.technical_identifier).filter(identifier => identifier); // Remove any undefined/null values
1078
1094
 
1079
1095
  this.log.info(`Creating container with ${technicalDescriptors.length} technical identifiers from telematic.json`);
1080
1096
 
@@ -1091,7 +1107,7 @@ class Bmw extends utils.Adapter {
1091
1107
  headers: headers,
1092
1108
  data: containerData,
1093
1109
  },
1094
- 'create telematic container'
1110
+ 'create telematic container',
1095
1111
  );
1096
1112
 
1097
1113
  this.containerId = response.data.containerId;
@@ -1157,7 +1173,7 @@ class Bmw extends utils.Adapter {
1157
1173
  containerId: containerId,
1158
1174
  },
1159
1175
  },
1160
- `get telematic data for ${vin}`
1176
+ `get telematic data for ${vin}`,
1161
1177
  );
1162
1178
 
1163
1179
  // Filter out telematic data entries with null timestamps (not relevant for the car)
@@ -1177,7 +1193,7 @@ class Bmw extends utils.Adapter {
1177
1193
  this.log.info(
1178
1194
  `Telematic data retrieved for ${vin}: ${filteredCount} relevant data points (${
1179
1195
  originalCount - filteredCount
1180
- } null timestamp entries filtered out)`
1196
+ } null timestamp entries filtered out)`,
1181
1197
  );
1182
1198
  } else {
1183
1199
  this.log.info(`Telematic data retrieved successfully for ${vin} (no telematicData in response)`);
@@ -1234,7 +1250,7 @@ class Bmw extends utils.Adapter {
1234
1250
  headers: headers,
1235
1251
  params: params,
1236
1252
  },
1237
- `fetch charging history for ${vin}${nextToken ? ' (paginated)' : ''}`
1253
+ `fetch charging history for ${vin}${nextToken ? ' (paginated)' : ''}`,
1238
1254
  );
1239
1255
 
1240
1256
  const chargingData = response.data;
@@ -1407,7 +1423,7 @@ class Bmw extends utils.Adapter {
1407
1423
  url: `${this.carDataApiBase}/customers/vehicles/${vin}/basicData`,
1408
1424
  headers: headers,
1409
1425
  },
1410
- `fetch basicData for ${vin}`
1426
+ `fetch basicData for ${vin}`,
1411
1427
  );
1412
1428
 
1413
1429
  await this.json2iob.parse(`${vin}.api.basicData`, basicResponse.data, {
@@ -1473,7 +1489,7 @@ class Bmw extends utils.Adapter {
1473
1489
  url: `${this.carDataApiBase}/customers/vehicles/${vin}/image`,
1474
1490
  headers: headers,
1475
1491
  },
1476
- `fetch image for ${vin}`
1492
+ `fetch image for ${vin}`,
1477
1493
  );
1478
1494
 
1479
1495
  await this.json2iob.parse(`${vin}.api.image`, imageResponse.data, {
@@ -1493,7 +1509,7 @@ class Bmw extends utils.Adapter {
1493
1509
  url: `${this.carDataApiBase}/customers/vehicles/${vin}/locationBasedChargingSettings`,
1494
1510
  headers: headers,
1495
1511
  },
1496
- `fetch locationBasedChargingSettings for ${vin}`
1512
+ `fetch locationBasedChargingSettings for ${vin}`,
1497
1513
  );
1498
1514
 
1499
1515
  await this.json2iob.parse(`${vin}.api.locationBasedChargingSettings`, locationResponse.data, {
@@ -1513,7 +1529,7 @@ class Bmw extends utils.Adapter {
1513
1529
  url: `${this.carDataApiBase}/customers/vehicles/${vin}/smartMaintenanceTyreDiagnosis`,
1514
1530
  headers: headers,
1515
1531
  },
1516
- `fetch smartMaintenanceTyreDiagnosis for ${vin}`
1532
+ `fetch smartMaintenanceTyreDiagnosis for ${vin}`,
1517
1533
  );
1518
1534
 
1519
1535
  await this.json2iob.parse(`${vin}.api.smartMaintenanceTyreDiagnosis`, tyreResponse.data, {
@@ -1551,7 +1567,7 @@ if (require.main !== module) {
1551
1567
  /**
1552
1568
  * @param {Partial<utils.AdapterOptions>} [options] - Optional adapter configuration options.
1553
1569
  */
1554
- module.exports = (options) => new Bmw(options);
1570
+ module.exports = options => new Bmw(options);
1555
1571
  } else {
1556
1572
  // otherwise start the instance directly
1557
1573
  new Bmw();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.bmw",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Adapter for BMW",
5
5
  "author": {
6
6
  "name": "TA2k",