iobroker.bmw 4.0.3 → 4.0.4

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
@@ -189,7 +189,7 @@ This adapter is available at: [https://github.com/TA2k/ioBroker.bmw](https://git
189
189
 
190
190
  ## Changelog
191
191
 
192
- ### 4.0.3 (2025-10-01)
192
+ ### 4.0.4 (2025-10-01)
193
193
 
194
194
  - **BREAKING:** Complete migration to BMW CarData API with OAuth2 Device Flow authentication
195
195
  - **BREAKING:** Removed username/password authentication (deprecated by BMW)
package/io-package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "bmw",
4
- "version": "4.0.3",
4
+ "version": "4.0.4",
5
5
  "news": {
6
- "4.0.3": {
6
+ "4.0.4": {
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
  },
package/main.js CHANGED
@@ -119,36 +119,30 @@ class Bmw extends utils.Adapter {
119
119
  // Connect MQTT after successful auth
120
120
  await this.connectMQTT();
121
121
  // Start periodic token refresh (every 45 minutes)
122
- this.refreshTokenInterval = setInterval(
123
- async () => {
124
- await this.refreshToken();
125
- },
126
- 45 * 60 * 1000,
127
- );
122
+ this.refreshTokenInterval = setInterval(async () => {
123
+ await this.refreshToken();
124
+ }, 45 * 60 * 1000);
128
125
 
129
126
  // Start periodic API updates (respecting quota limits)
130
127
  if (this.vinArray.length > 0) {
131
128
  this.log.info(`Setting up periodic updates every ${this.config.interval} minutes for ${this.vinArray.length} vehicle(s)`);
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
- );
129
+ this.updateInterval = setInterval(async () => {
130
+ // Update quota states (expired calls removed automatically)
131
+ this.updateQuotaStates();
132
+
133
+ // Periodic API data refresh - MQTT provides real-time updates
134
+ const headers = {
135
+ Authorization: `Bearer ${this.session.access_token}`,
136
+ 'x-version': 'v1',
137
+ Accept: 'application/json',
138
+ };
139
+
140
+ for (const vin of this.vinArray) {
141
+ this.log.debug(`Periodic API refresh for ${vin}`);
142
+ await this.fetchAllVehicleData(vin, headers);
143
+ break; // Only one vehicle per interval to conserve quota
144
+ }
145
+ }, this.config.interval * 60 * 1000);
152
146
  }
153
147
 
154
148
  this.log.info('BMW CarData adapter startup complete');
@@ -193,11 +187,11 @@ class Bmw extends utils.Adapter {
193
187
  },
194
188
  data: requestData,
195
189
  })
196
- .then(res => {
190
+ .then((res) => {
197
191
  this.log.debug(`Device code response: ${JSON.stringify(res.data)}`);
198
192
  return res;
199
193
  })
200
- .catch(error => {
194
+ .catch((error) => {
201
195
  this.log.error(`Device code request failed: ${error.message}`);
202
196
  this.log.error(`Error stack: ${error.stack}`);
203
197
  if (error.response) {
@@ -218,9 +212,7 @@ class Bmw extends utils.Adapter {
218
212
  this.log.error('To fix this issue:');
219
213
  this.log.error('1. Visit BMW ConnectedDrive portal: https://www.bmw.de/de-de/mybmw/vehicle-overview');
220
214
  this.log.error('2. Go to CarData section');
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
+ this.log.error('3. Check if CarData API and CarData Streaming are both activated. Sometimes it needs 30s to save the selection');
224
216
  this.log.error('4. If not activated, enable both services');
225
217
  this.log.error('5. If already activated, delete and recreate your Client ID');
226
218
  this.log.error('6. Update the adapter configuration with the new Client ID');
@@ -233,7 +225,7 @@ class Bmw extends utils.Adapter {
233
225
  method: error.request.method,
234
226
  url: error.request.url,
235
227
  headers: error.request._headers,
236
- })}`,
228
+ })}`
237
229
  );
238
230
  }
239
231
  return false; // Return false instead of throwing
@@ -367,7 +359,7 @@ class Bmw extends utils.Adapter {
367
359
  url: `${this.carDataApiBase}/customers/vehicles/mappings`,
368
360
  headers: headers,
369
361
  })
370
- .then(async res => {
362
+ .then(async (res) => {
371
363
  this.log.debug(JSON.stringify(res.data));
372
364
  const mappings = res.data;
373
365
 
@@ -414,7 +406,7 @@ class Bmw extends utils.Adapter {
414
406
  }
415
407
  }
416
408
  })
417
- .catch(error => {
409
+ .catch((error) => {
418
410
  this.log.error(`BMW CarData vehicle discovery failed: ${error.message}`);
419
411
  if (error.response) {
420
412
  this.log.error(`Response: ${JSON.stringify(error.response.data)}`);
@@ -487,7 +479,7 @@ class Bmw extends utils.Adapter {
487
479
  ];
488
480
 
489
481
  // Filter endpoints based on user configuration
490
- const enabledEndpoints = apiEndpoints.filter(endpoint => this.config[endpoint.configKey] === true);
482
+ const enabledEndpoints = apiEndpoints.filter((endpoint) => this.config[endpoint.configKey] === true);
491
483
 
492
484
  this.log.info(`Fetching ${enabledEndpoints.length} configured API endpoints for ${vin}...`);
493
485
 
@@ -568,7 +560,7 @@ class Bmw extends utils.Adapter {
568
560
 
569
561
  // Remove calls older than 24h
570
562
  const originalLength = this.apiCalls.length;
571
- this.apiCalls = this.apiCalls.filter(time => now - time < 24 * 60 * 60 * 1000);
563
+ this.apiCalls = this.apiCalls.filter((time) => now - time < 24 * 60 * 60 * 1000);
572
564
 
573
565
  // Save history if calls were removed due to expiration
574
566
  if (this.apiCalls.length !== originalLength) {
@@ -604,7 +596,7 @@ class Bmw extends utils.Adapter {
604
596
  }
605
597
 
606
598
  sleep(ms) {
607
- return new Promise(resolve => setTimeout(resolve, ms));
599
+ return new Promise((resolve) => setTimeout(resolve, ms));
608
600
  }
609
601
 
610
602
  async cleanObjects(vin) {
@@ -678,7 +670,7 @@ class Bmw extends utils.Adapter {
678
670
  },
679
671
  data: qs.stringify(refreshData),
680
672
  })
681
- .then(async res => {
673
+ .then(async (res) => {
682
674
  // Store refreshed tokens (keep existing session structure)
683
675
  this.session = res.data;
684
676
  this.setState('cardataauth.session', JSON.stringify(this.session), true);
@@ -695,7 +687,7 @@ class Bmw extends utils.Adapter {
695
687
 
696
688
  return res.data;
697
689
  })
698
- .catch(async error => {
690
+ .catch(async (error) => {
699
691
  this.log.error('Token refresh failed:', error.message);
700
692
  this.log.error('Error stack:', error.stack);
701
693
  if (error.response) {
@@ -738,7 +730,7 @@ class Bmw extends utils.Adapter {
738
730
  keepalive: 30,
739
731
  clean: true,
740
732
  rejectUnauthorized: true,
741
- reconnectPeriod: 5000,
733
+ reconnectPeriod: 30000, // Increased from 5000ms to 30000ms (30 seconds)
742
734
  connectTimeout: 30000,
743
735
  };
744
736
 
@@ -752,7 +744,7 @@ class Bmw extends utils.Adapter {
752
744
 
753
745
  // Subscribe to all vehicle topics for this CarData Streaming username
754
746
  const topic = `${this.config.cardataStreamingUsername}/+`;
755
- this.mqtt.subscribe(topic, err => {
747
+ this.mqtt.subscribe(topic, (err) => {
756
748
  if (err) {
757
749
  this.log.error(`MQTT subscription failed: ${err.message}`);
758
750
  } else {
@@ -765,18 +757,46 @@ class Bmw extends utils.Adapter {
765
757
  this.handleMQTTMessage(topic, message);
766
758
  });
767
759
 
768
- this.mqtt.on('error', error => {
760
+ this.mqtt.on('error', async (error) => {
769
761
  this.log.error(`MQTT error: ${error.message}`);
770
762
  this.setState('info.mqttConnected', false, true);
763
+
764
+ // Check if it's an authentication error indicating expired token
765
+ if (
766
+ error.message &&
767
+ (error.message.includes('Bad username or password') ||
768
+ error.message.includes('Connection refused') ||
769
+ error.message.includes('Not authorized'))
770
+ ) {
771
+ this.log.warn('MQTT authentication failed - attempting token refresh');
772
+ try {
773
+ await this.refreshToken();
774
+ this.log.info('Token refreshed successfully');
775
+
776
+ // Close current MQTT connection and reconnect with new token
777
+ if (this.mqtt) {
778
+ this.mqtt.end(false); // Force close without waiting
779
+ }
780
+
781
+ // Reconnect with fresh credentials after a short delay
782
+ setTimeout(async () => {
783
+ this.log.debug('Reconnecting MQTT with refreshed token');
784
+ await this.connectMQTT();
785
+ }, 5000);
786
+ } catch (refreshError) {
787
+ this.log.error('Token refresh failed: ' + refreshError.message);
788
+ this.log.warn('Will retry MQTT connection with current token');
789
+ }
790
+ }
771
791
  });
772
792
 
773
793
  this.mqtt.on('close', () => {
774
- this.log.warn('MQTT connection closed');
794
+ this.log.info('MQTT connection closed');
775
795
  this.setState('info.mqttConnected', false, true);
776
796
  });
777
797
 
778
798
  this.mqtt.on('reconnect', () => {
779
- this.log.debug('MQTT reconnecting...');
799
+ this.log.info('MQTT reconnecting...');
780
800
  });
781
801
 
782
802
  return true;
@@ -909,7 +929,7 @@ if (require.main !== module) {
909
929
  /**
910
930
  * @param {Partial<utils.AdapterOptions>} [options] - Optional adapter configuration options.
911
931
  */
912
- module.exports = options => new Bmw(options);
932
+ module.exports = (options) => new Bmw(options);
913
933
  } else {
914
934
  // otherwise start the instance directly
915
935
  new Bmw();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.bmw",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "description": "Adapter for BMW",
5
5
  "author": {
6
6
  "name": "TA2k",