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 +1 -1
- package/io-package.json +2 -2
- package/main.js +65 -45
- package/package.json +1 -1
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.
|
|
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.
|
|
4
|
+
"version": "4.0.4",
|
|
5
5
|
"news": {
|
|
6
|
-
"4.0.
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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();
|