iobroker.bmw 2.8.4 → 2.9.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-2024 TA2k <tombox2020@gmail.com>
3
+ Copyright (c) 2021-2030 TA2k <tombox2020@gmail.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -25,6 +25,15 @@ bmw.0.VIN.properties
25
25
  bmw.0.VIN.remotev2
26
26
 
27
27
  ## Changelog
28
+ ### 2.9.1 (2025-01-28)
29
+
30
+ - fix Remote Controls
31
+ - add Mitbenutzer Login for remote controls
32
+
33
+ ### 2.9.0 (2024-11-28)
34
+
35
+ - added new remotes as switch and updated values
36
+ - added retry logice for remotes
28
37
 
29
38
  ### 2.8.4 (2024-11-21)
30
39
 
@@ -76,7 +85,7 @@ bmw.0.VIN.remotev2
76
85
 
77
86
  MIT License
78
87
 
79
- Copyright (c) 2021-2024 TA2k <tombox2020@gmail.com>
88
+ Copyright (c) 2021-2030 TA2k <tombox2020@gmail.com>
80
89
 
81
90
  Permission is hereby granted, free of charge, to any person obtaining a copy
82
91
  of this software and associated documentation files (the "Software"), to deal
@@ -84,6 +84,15 @@
84
84
  <div class="row">
85
85
  <div class="col s6 input-field" id="captchaResponse">
86
86
  Check Captcha Box and press Submit. Save the form.
87
+ <p></p>
88
+ If this is not working use the IP of the ioBroker instance in the browser and try again.
89
+ <p></p>
90
+ Or try
91
+ <a href="https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html" target="_blank"
92
+ >https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html</a
93
+ >
94
+ and paste the response in Captcha field.
95
+ <p></p>
87
96
  <div>
88
97
  <form id="captcha_form" action="#" method="post">
89
98
  <!-- hCaptcha widget -->
@@ -105,6 +114,9 @@
105
114
  const responseElement = document.getElementById('captchaResponse');
106
115
 
107
116
  if (hCaptchaResponse) {
117
+ if (typeof hCaptchaResponse === 'object') {
118
+ hCaptchaResponse = JSON.stringify(hCaptchaResponse);
119
+ }
108
120
  document.getElementById('captcha').value = hCaptchaResponse;
109
121
  //trigger change event
110
122
  var event = new Event('change');
@@ -144,6 +156,20 @@
144
156
  <label for="interval" class="translate">Update interval (in minutes)"</label>
145
157
  </div>
146
158
  </div>
159
+ <div class="row">
160
+ <div class="col s6 input-field">
161
+ Because of API Quota limits you can use a second account to send remotes to improve the reliability.
162
+ <p></p>
163
+ <input type="text" class="value" id="musername" />
164
+ <label for="musername" class="translate">Mitnutzer App Email</label>
165
+ </div>
166
+ </div>
167
+ <div class="row">
168
+ <div class="col s6 input-field">
169
+ <input type="password" class="value" id="mpassword" />
170
+ <label for="mpassword" class="translate">Mitnutzer App Password</label>
171
+ </div>
172
+ </div>
147
173
  <div class="row">
148
174
  <div class="col s6 input-field">
149
175
  <input type="text" class="value" id="ignorelist" />
package/admin/style.css CHANGED
@@ -2,23 +2,34 @@
2
2
  * {
3
3
  box-sizing: border-box
4
4
  }
5
+
6
+ body {
7
+ overflow: hidden;
8
+ }
9
+
10
+ .adapter-body {
11
+ overflow: auto;
12
+ }
13
+
5
14
  .m {
6
15
  /* Don't cut off dropdowns! */
7
16
  overflow: initial;
8
17
  }
18
+
9
19
  .m.adapter-container,
10
- .m.adapter-container > div.App {
20
+ .m.adapter-container>div.App {
11
21
  /* Fix layout/scrolling issues with tabs */
12
22
  height: 100%;
13
23
  width: 100%;
14
24
  position: relative;
15
25
  }
16
- .m .select-wrapper + label {
26
+
27
+ .m .select-wrapper+label {
17
28
  /* The positioning for dropdown labels is messed up */
18
29
  transform: none !important;
19
30
  }
20
31
 
21
- label > i[title] {
32
+ label>i[title] {
22
33
  /* Display the help cursor for the tooltip icons and fix their positioning */
23
34
  cursor: help;
24
35
  margin-left: 0.25em;
@@ -29,4 +40,4 @@ label > i[title] {
29
40
  white-space: nowrap;
30
41
  }
31
42
 
32
- /* Add your styles here */
43
+ /* Add your styles here */
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "bmw",
4
- "version": "2.8.4",
4
+ "version": "2.9.1",
5
5
  "news": {
6
+ "2.9.1": {
7
+ "en": "fix Remote Controls\nadd Mitbenutzer Login for remote controls",
8
+ "de": "remote Controls reparieren\nmitbenutzer Login für Fernbedienungen hinzufügen",
9
+ "ru": "починить удаленные управления\nдобавить логин Mitbenutzer для дистанционного управления",
10
+ "pt": "corrigir controles remotos\nadicionar Mitbenutzer Login para controles remotos",
11
+ "nl": "fix afstandsbedieningen\nmitbenutzer Login toevoegen voor afstandsbedieningen",
12
+ "fr": "correction des télécommandes\najouter Mitbenutzer Login pour les télécommandes",
13
+ "it": "correzione di controlli remoti\naggiungere Mitbenutzer Login per i comandi remoti",
14
+ "es": "controles remotos de fijación\nañadir Mitbenutzer Login para controles remotos",
15
+ "pl": "naprawić zdalne sterowanie\ndodaj Mitbenutzer Login do zdalnego sterowania",
16
+ "uk": "виправити дистанційні керування\nдодати Mitbenutzer Увійти для дистанційного керування",
17
+ "zh-cn": "修复远程控制\n添加远程控制的 Mitbenutzer 登录"
18
+ },
19
+ "2.9.0": {
20
+ "en": "added new remotes as switch and updated values\nadded retry logic for remotes",
21
+ "de": "neue remotes als switch und mit aktualisierten Werte hinzugefügt\nzusätzliche retry-logik für remotes hinzufgefügt",
22
+ "ru": "добавлены новые пульты в качестве переключателя и обновленных значений\nдобавленная ретри-логика для пультов",
23
+ "pt": "adicionado novos controles remotos como interruptor e valores atualizados\nadicionado lógica de retração para remotos",
24
+ "nl": "nieuwe remotes toegevoegd als switch en bijgewerkte waarden\ntoegevoegd retry logica voor remotes",
25
+ "fr": "ajout de nouvelles télécommandes comme valeurs de commutation et de mise à jour\nretry logique ajoutée pour les télécommandes",
26
+ "it": "aggiunto nuovi remoti come switch e valori aggiornati\naggiunto retry logica per i remoti",
27
+ "es": "nuevos mandos como conmutación y valores actualizados\nlógica de retry añadido para los remotos",
28
+ "pl": "dodano nowe piloty jako przełącznik i zaktualizowane wartości\ndodano logikę retry dla pilotów",
29
+ "uk": "додано нові пульти як перемикач і оновлені значення\nдодана логіка для пультів",
30
+ "zh-cn": "作为切换和更新值添加新远程\n为远程添加重试逻辑"
31
+ },
6
32
  "2.8.4": {
7
33
  "en": "improved charging sessios parsing\nadded remote to fetch charging session from a specific month\nadd raw JSON of charging session for export",
8
34
  "de": "verbesserte charging sessios Import\nRemote hinzugefügt, um charging sessios aus einem bestimmten Monat abzurufen\nRohes JSON der charging sessios für den Export hinzugefügt",
@@ -58,14 +84,6 @@
58
84
  "2.8.0": {
59
85
  "en": "Add support service demand and trip api",
60
86
  "de": "Support für Service und Trips API hinzugefügt"
61
- },
62
- "2.7.2": {
63
- "en": "Move Rate Limit messages to debug",
64
- "de": "Rate Limit Nachrichten in debug verschoben"
65
- },
66
- "2.7.0": {
67
- "en": "Improve rate limit handling",
68
- "de": "Rate Limit Handling verbessert"
69
87
  }
70
88
  },
71
89
  "titleLang": {
@@ -94,8 +112,12 @@
94
112
  "uk": "Adapter for BMW",
95
113
  "zh-cn": "宝马适配器"
96
114
  },
97
- "authors": ["TA2k <tombox2020@gmail.com>"],
98
- "keywords": ["BMW"],
115
+ "authors": [
116
+ "TA2k <tombox2020@gmail.com>"
117
+ ],
118
+ "keywords": [
119
+ "BMW"
120
+ ],
99
121
  "license": "MIT",
100
122
  "platform": "Javascript/Node.js",
101
123
  "main": "main.js",
@@ -126,11 +148,17 @@
126
148
  }
127
149
  ]
128
150
  },
129
- "encryptedNative": ["password"],
130
- "protectedNative": ["password"],
151
+ "encryptedNative": [
152
+ "password"
153
+ ],
154
+ "protectedNative": [
155
+ "password"
156
+ ],
131
157
  "native": {
132
158
  "username": "",
133
159
  "password": "",
160
+ "musername": "",
161
+ "mpassword": "",
134
162
  "interval": 5,
135
163
  "brand": "bmw",
136
164
  "ignorelist": "",
package/main.js CHANGED
@@ -14,6 +14,7 @@ const crypto = require('crypto');
14
14
  const qs = require('qs');
15
15
  const Json2iob = require('json2iob');
16
16
  const tough = require('tough-cookie');
17
+ const axiosRetry = require('axios-retry').default;
17
18
  class Bmw extends utils.Adapter {
18
19
  /**
19
20
  * @param {Partial<utils.AdapterOptions>} [options={}]
@@ -27,8 +28,8 @@ class Bmw extends utils.Adapter {
27
28
  this.on('stateChange', this.onStateChange.bind(this));
28
29
  this.on('unload', this.onUnload.bind(this));
29
30
  this.userAgent = 'My%20BMW/8932 CFNetwork/978.0.7 Darwin/18.7.0';
30
- this.userAgentDart = 'Dart/2.14 (dart:io)';
31
- this.xuserAgent = 'android(SP1A.210812.016.C1);brand;99.0.0(99999);row';
31
+ this.userAgentDart = 'Dart/3.3 (dart:io)';
32
+ this.xuserAgent = 'android(AP2A.240605.024);brand;4.99.9(96892);row';
32
33
  this.updateInterval;
33
34
  this.reLoginTimeout;
34
35
  this.refreshTokenInterval;
@@ -168,6 +169,12 @@ class Bmw extends utils.Adapter {
168
169
  withCredentials: true,
169
170
  httpsAgent: new HttpsCookieAgent({ cookies: { jar: this.cookieJar } }),
170
171
  });
172
+ axiosRetry(this.requestClient, {
173
+ retries: 0,
174
+ retryDelay: () => {
175
+ return 5000;
176
+ },
177
+ });
171
178
  }
172
179
 
173
180
  /**
@@ -200,15 +207,37 @@ class Bmw extends utils.Adapter {
200
207
 
201
208
  await this.login();
202
209
  }
210
+ if (this.config.musername && this.config.mpassword) {
211
+ const msessionState = await this.getStateAsync('auth.session');
212
+ if (msessionState && msessionState.val) {
213
+ this.msession = JSON.parse(msessionState.val);
214
+ this.log.debug(JSON.stringify(this.msession));
215
+ await this.refreshToken(true);
216
+ } else {
217
+ if (!this.config.captcha) {
218
+ this.log.error('Please generate a captcha in the instance settings to login the m user');
219
+ return;
220
+ }
221
+ await this.login(true);
222
+ }
223
+ }
203
224
 
204
225
  if (this.session.access_token) {
205
226
  this.log.info(`Start getting ${this.config.brand} vehicles`);
206
227
  await this.getVehiclesv2(true);
207
228
  await this.cleanObjects();
208
229
  await this.sleep(5000);
209
- await this.updateDemands();
210
- await this.sleep(5000);
211
- await this.updateTrips();
230
+ this.log.info('Initial first update of the vehicles');
231
+ await this.updateDevices();
232
+ this.log.info('First update of Trips and Demands in 10 minutes');
233
+ this.setTimeout(
234
+ async () => {
235
+ await this.updateDemands();
236
+ await this.sleep(5000);
237
+ await this.updateTrips();
238
+ },
239
+ 1000 * 60 * 10,
240
+ );
212
241
  this.updateInterval = setInterval(
213
242
  async () => {
214
243
  await this.sleep(2000);
@@ -229,12 +258,21 @@ class Bmw extends utils.Adapter {
229
258
  async () => {
230
259
  await this.refreshToken();
231
260
  await this.sleep(5000);
261
+ if (this.config.musername && this.config.mpassword) {
262
+ await this.refreshToken(true);
263
+ }
232
264
  },
233
265
  (this.session.expires_in - 123) * 1000,
234
266
  );
235
267
  }
236
268
  }
237
- async login() {
269
+ async login(loginSecondUser) {
270
+ let username = this.config.username;
271
+ let password = this.config.password;
272
+ if (loginSecondUser) {
273
+ username = this.config.musername;
274
+ password = this.config.mpassword;
275
+ }
238
276
  const headers = {
239
277
  Accept: 'application/json, text/plain, */*',
240
278
  'User-Agent':
@@ -253,8 +291,8 @@ class Bmw extends utils.Adapter {
253
291
  nonce: 'login_nonce',
254
292
  code_challenge_method: 'S256',
255
293
  code_challenge: codeChallenge,
256
- username: this.config.username,
257
- password: this.config.password,
294
+ username: username,
295
+ password: password,
258
296
  grant_type: 'authorization_code',
259
297
  };
260
298
 
@@ -349,7 +387,7 @@ class Bmw extends utils.Adapter {
349
387
  })
350
388
  .then(async (res) => {
351
389
  this.log.debug(JSON.stringify(res.data));
352
- this.session = res.data;
390
+
353
391
  await this.extendObject('auth', {
354
392
  type: 'channel',
355
393
  common: {
@@ -368,7 +406,23 @@ class Bmw extends utils.Adapter {
368
406
  },
369
407
  native: {},
370
408
  });
371
-
409
+ if (loginSecondUser) {
410
+ this.msession = res.data;
411
+ await this.extendObject('auth.msession', {
412
+ type: 'state',
413
+ common: {
414
+ name: 'MSession Token',
415
+ type: 'string',
416
+ role: 'value',
417
+ read: true,
418
+ write: false,
419
+ },
420
+ native: {},
421
+ });
422
+ await this.setState('auth.msession', JSON.stringify(this.msession), true);
423
+ } else {
424
+ this.session = res.data;
425
+ }
372
426
  this.setState('auth.session', JSON.stringify(this.session), true);
373
427
  this.setState('info.connection', true, true);
374
428
  return res.data;
@@ -455,16 +509,20 @@ class Bmw extends utils.Adapter {
455
509
  });
456
510
 
457
511
  const remoteArray = [
458
- { command: 'door-lock' },
459
- { command: 'door-unlock' },
460
- { command: 'horn-blow' },
461
- { command: 'light-flash' },
462
- { command: 'vehicle-finder' },
463
- { command: 'climate-now_START' },
464
- { command: 'climate-now_STOP' },
465
- { command: 'start-charging' },
466
- { command: 'stop-charging' },
512
+ { command: 'door-lock', name: 'Lock Doors (Not updated)' },
513
+ { command: 'door-unlock', name: 'Unlock Doors (Not updated)' },
514
+ { command: 'door', name: 'Door Lock True=Lock, False=Unlock' },
515
+ { command: 'horn-blow', name: 'Horn Blow' },
516
+ { command: 'light-flash', name: 'Light Flash' },
517
+ { command: 'vehicle-finder', name: 'Trigger Vehicle Finder' },
518
+ { command: 'climate-now_START', name: 'Start Climate (Not updated)' },
519
+ { command: 'climate-now_STOP', name: 'Stop Climate (Not updated)' },
520
+ { command: 'climate-now', name: 'Climate True=Start, False=Stop' },
521
+ { command: 'start-charging', name: 'Start Charging (Not updated)' },
522
+ { command: 'stop-charging', name: 'Stop Charging (Not updated)' },
523
+ { command: 'charging', name: 'Charging True=Start, False=Stop' },
467
524
  { command: 'force-refresh', name: 'Force Refresh' },
525
+ { command: 'fetch-images', name: 'Fetch Images of the car in the image folder' },
468
526
  {
469
527
  command: 'fetch-charges',
470
528
  name: 'Fetch Charge Sessions/Statistics for month',
@@ -487,13 +545,12 @@ class Bmw extends utils.Adapter {
487
545
  native: {},
488
546
  });
489
547
  });
548
+
490
549
  this.json2iob.parse(vehicle.vin, vehicle, {
491
550
  forceIndex: true,
492
551
  descriptions: this.description,
493
552
  channelName: vehicleName,
494
553
  });
495
-
496
- await this.updateChargingSessionv2(vehicle.vin, 200);
497
554
  }
498
555
  })
499
556
  .catch((error) => {
@@ -535,7 +592,22 @@ class Bmw extends utils.Adapter {
535
592
  })
536
593
  .then(async (res) => {
537
594
  this.log.debug(JSON.stringify(res.data));
595
+ if (res.data.state && res.data.state.electricChargingState && !res.data.state.electricChargingState.remainingChargingMinutes) {
596
+ res.data.state.electricChargingState.remainingChargingMinutes = 0;
597
+ }
538
598
  this.json2iob.parse(vin, res.data, { forceIndex: true, descriptions: this.description });
599
+ await this.extendObject(vin + '.state.rawJSON', {
600
+ type: 'state',
601
+ common: {
602
+ name: 'Raw Data as JSON',
603
+ type: 'string',
604
+ role: 'json',
605
+ write: false,
606
+ read: true,
607
+ },
608
+ native: {},
609
+ });
610
+ this.setState(vin + '.state.rawJSON', JSON.stringify(res.data), true);
539
611
  })
540
612
  .catch(async (error) => {
541
613
  if (error.response && error.response.status === 429) {
@@ -888,9 +960,71 @@ class Bmw extends utils.Adapter {
888
960
  ':00';
889
961
  return date_format_str;
890
962
  }
891
-
892
- async refreshToken() {
963
+ async fetchImages(vin) {
964
+ const viewsArray = [
965
+ 'FrontView',
966
+ 'RearView',
967
+ 'FrontLeft',
968
+ 'FrontRight',
969
+ 'RearLeft',
970
+ 'RearRight',
971
+ 'SideViewLeft',
972
+ 'Dashboard',
973
+ 'DriverDoor',
974
+ 'RearView',
975
+ ];
976
+ const headers = {
977
+ 'user-agent': this.userAgentDart,
978
+ 'x-user-agent': this.xuserAgent.replace(';brand;', `;${this.config.brand};`),
979
+ authorization: 'Bearer ' + this.session.access_token,
980
+ 'accept-language': 'de-DE',
981
+ '24-hour-format': 'true',
982
+ 'bmw-vin': vin,
983
+ accept: 'image/png',
984
+ 'bmw-app-vehicle-type': 'connected',
985
+ };
986
+ for (const view of viewsArray) {
987
+ this.log.info('Fetch image from ' + view + 'to bmw.0.' + vin + '.images.' + view);
988
+ await this.requestClient({
989
+ method: 'get',
990
+ url: 'https://cocoapi.bmwgroup.com/eadrax-ics/v5/presentation/vehicles/images',
991
+ params: {
992
+ carView: view,
993
+ toCrop: true,
994
+ },
995
+ headers: headers,
996
+ responseType: 'arraybuffer',
997
+ })
998
+ .then(async (res) => {
999
+ //save base64 image to state
1000
+ const base64 = Buffer.from(res.data, 'binary').toString('base64');
1001
+ await this.setObjectNotExistsAsync(vin + '.images.' + view, {
1002
+ type: 'state',
1003
+ common: {
1004
+ name: view,
1005
+ type: 'string',
1006
+ role: 'state',
1007
+ read: true,
1008
+ write: false,
1009
+ },
1010
+ native: {},
1011
+ });
1012
+ await this.setState(vin + '.images.' + view, 'data:image/png;base64,' + base64, true);
1013
+ })
1014
+ .catch((error) => {
1015
+ this.log.error('fetch images failed ' + view);
1016
+ this.log.error(error);
1017
+ error.response && this.log.error(JSON.stringify(error.response.data));
1018
+ });
1019
+ await this.sleep(5000);
1020
+ }
1021
+ }
1022
+ async refreshToken(useSecondUser) {
893
1023
  this.log.debug('refresh token');
1024
+ let refresh_token = this.session.refresh_token;
1025
+ if (useSecondUser) {
1026
+ refresh_token = this.msession.refresh_token;
1027
+ }
894
1028
  await this.requestClient({
895
1029
  method: 'post',
896
1030
  url: 'https://customer.bmwgroup.com/gcdm/oauth/token',
@@ -900,18 +1034,24 @@ class Bmw extends utils.Adapter {
900
1034
  Accept: '*/*',
901
1035
  Authorization: 'Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==',
902
1036
  },
903
- data: 'redirect_uri=com.bmw.connected://oauth&refresh_token=' + this.session.refresh_token + '&grant_type=refresh_token',
1037
+ data: 'redirect_uri=com.bmw.connected://oauth&refresh_token=' + refresh_token + '&grant_type=refresh_token',
904
1038
  })
905
1039
  .then((res) => {
906
1040
  this.log.debug(JSON.stringify(res.data));
907
- this.session = res.data;
1041
+ if (useSecondUser) {
1042
+ this.msession = res.data;
1043
+
1044
+ this.setState('auth.msession', JSON.stringify(this.msession), true);
1045
+ } else {
1046
+ this.session = res.data;
1047
+ }
908
1048
 
909
1049
  this.setState('auth.session', JSON.stringify(this.session), true);
910
1050
  this.setState('info.connection', true, true);
911
1051
  return res.data;
912
1052
  })
913
1053
  .catch((error) => {
914
- this.log.error('refresh token failed');
1054
+ this.log.error('refresh token failed. Please delete bmw.0.auth.session and restart the adapter');
915
1055
  this.log.error(error);
916
1056
  error.response && this.log.error(JSON.stringify(error.response.data));
917
1057
  this.log.error('Start relogin in 1min');
@@ -970,24 +1110,52 @@ class Bmw extends utils.Adapter {
970
1110
  this.updateDevices();
971
1111
  return;
972
1112
  }
1113
+ if (command === 'fetch-images') {
1114
+ this.log.info('fetch images');
1115
+ await this.fetchImages(vin);
1116
+ return;
1117
+ }
973
1118
  if (command === 'fetch-charges') {
974
1119
  this.log.info('fetch charges');
975
1120
  await this.updateChargingSessionv2(vin, 200, state.val);
976
1121
  return;
977
1122
  }
1123
+ if (command === 'charging') {
1124
+ command = 'start-charging';
1125
+ if (!state.val) {
1126
+ command = 'stop-charging';
1127
+ }
1128
+ }
1129
+ if (command === 'climate-now') {
1130
+ command = 'climate-now_START';
1131
+ if (!state.val) {
1132
+ command = 'climate-now_STOP';
1133
+ }
1134
+ }
1135
+ if (command === 'door') {
1136
+ command = 'door-lock';
1137
+ if (!state.val) {
1138
+ command = 'door-unlock';
1139
+ }
1140
+ }
978
1141
  const action = command.split('_')[1];
979
1142
  command = command.split('_')[0];
980
-
1143
+ let access_token = this.session.access_token;
1144
+ if (this.msession) {
1145
+ this.log.debug('Use second user for remote command');
1146
+ access_token = this.msession.access_token;
1147
+ }
981
1148
  const headers = {
982
1149
  'user-agent': this.userAgentDart,
983
1150
  'x-user-agent': this.xuserAgent.replace(';brand;', `;${this.config.brand};`),
984
- authorization: 'Bearer ' + this.session.access_token,
1151
+ authorization: 'Bearer ' + access_token,
985
1152
  'accept-language': 'de-DE',
986
1153
  host: 'cocoapi.bmwgroup.com',
987
1154
  '24-hour-format': 'true',
988
1155
  'Content-Type': 'text/plain',
1156
+ 'bmw-vin': vin,
989
1157
  };
990
- let url = 'https://cocoapi.bmwgroup.com/eadrax-vrccs/v3/presentation/remote-commands/' + vin + '/' + command;
1158
+ let url = 'https://cocoapi.bmwgroup.com/eadrax-vrccs/v4/presentation/remote-commands/' + command;
991
1159
  if (action) {
992
1160
  url += '?action=' + action;
993
1161
  }
@@ -996,9 +1164,49 @@ class Bmw extends utils.Adapter {
996
1164
  method: 'post',
997
1165
  url: url,
998
1166
  headers: headers,
1167
+ 'axios-retry': {
1168
+ retries: 5,
1169
+ // only 403 rate limit
1170
+ retryCondition: (error) => {
1171
+ this.log.debug(error);
1172
+ error.response && this.log.debug(JSON.stringify(error.response.data));
1173
+ return error.response && error.response.status === 403;
1174
+ },
1175
+ onRetry: (retryCount, error) => {
1176
+ this.log.info('Retry ' + retryCount);
1177
+ this.log.debug(error);
1178
+ error.response && this.log.info(JSON.stringify(error.response.data));
1179
+ this.log.warn('Rate Limit exceeded, retry in 5 seconds');
1180
+ },
1181
+ onMaxRetryTimesExceeded: () => {
1182
+ this.log.error('3 Retries failed');
1183
+ },
1184
+ },
999
1185
  })
1000
1186
  .then((res) => {
1001
1187
  this.log.debug(JSON.stringify(res.data));
1188
+ const eventId = res.data.eventId;
1189
+ this.log.debug('Check Status of event in 10sec');
1190
+ this.setTimeout(() => {
1191
+ this.checkEventStatus(eventId, headers).then(async (res) => {
1192
+ if (res === 'EXECUTED') {
1193
+ this.log.info('Remote command executed');
1194
+ } else {
1195
+ this.log.info('Remote Event is not finished it is: ' + res);
1196
+ await this.sleep(10000);
1197
+ this.checkEventStatus(eventId, headers).then((res) => {
1198
+ if (res === 'EXECUTED') {
1199
+ this.log.info('Remote command executed');
1200
+ } else if (res === 'RUNNING') {
1201
+ this.log.info('Remote command is still running');
1202
+ } else {
1203
+ this.log.error('Remote command failed ' + res);
1204
+ }
1205
+ });
1206
+ }
1207
+ });
1208
+ }, 10 * 1000);
1209
+
1002
1210
  return res.data;
1003
1211
  })
1004
1212
  .catch((error) => {
@@ -1011,35 +1219,64 @@ class Bmw extends utils.Adapter {
1011
1219
  this.refreshTimeout = setTimeout(async () => {
1012
1220
  this.log.info('Refresh values');
1013
1221
  await this.updateDevices();
1014
- }, 10 * 1000);
1222
+ }, 12 * 1000);
1015
1223
  } else {
1016
- // const resultDict = { chargingStatus: "CHARGE_NOW", doorLockState: "DOOR_LOCK" };
1017
- // const idArray = id.split(".");
1018
- // const stateName = idArray[idArray.length - 1];
1224
+ const resultDict = { chargingStatus: 'charging', combinedSecurityState: 'door', activity: 'climate-now' };
1225
+ const idArray = id.split('.');
1226
+ const stateName = idArray[idArray.length - 1];
1019
1227
  const vin = id.split('.')[2];
1020
- // if (resultDict[stateName]) {
1021
- // let value = true;
1022
- // if (!state.val || state.val === "INVALID" || state.val === "NOT_CHARGING" || state.val === "ERROR" || state.val === "UNLOCKED") {
1023
- // value = false;
1024
- // }
1025
- // await this.setStateAsync(vin + ".remote." + resultDict[stateName], value, true);
1228
+ if (resultDict[stateName]) {
1229
+ let value = true;
1230
+ if (
1231
+ !state.val ||
1232
+ state.val === 'INVALID' ||
1233
+ state.val === 'INACTIVE' ||
1234
+ state.val === 'NOT_CHARGING' ||
1235
+ state.val === 'ERROR' ||
1236
+ state.val === 'UNLOCKED'
1237
+ ) {
1238
+ value = false;
1239
+ }
1240
+ await this.setState(vin + '.remotev2.' + resultDict[stateName], value, true);
1241
+ }
1242
+
1243
+ // if (id.indexOf('.chargingStatus') !== -1 && state.val !== 'CHARGING') {
1244
+ // await this.setObjectNotExistsAsync(vin + '.status.chargingTimeRemaining', {
1245
+ // type: 'state',
1246
+ // common: {
1247
+ // name: 'chargingTimeRemaining',
1248
+ // role: 'value',
1249
+ // type: 'number',
1250
+ // write: false,
1251
+ // read: true,
1252
+ // },
1253
+ // native: {},
1254
+ // });
1255
+ // this.setState(vin + '.status.chargingTimeRemaining', 0, true);
1026
1256
  // }
1257
+ }
1258
+ }
1259
+ }
1027
1260
 
1028
- if (id.indexOf('.chargingStatus') !== -1 && state.val !== 'CHARGING') {
1029
- await this.setObjectNotExistsAsync(vin + '.status.chargingTimeRemaining', {
1030
- type: 'state',
1031
- common: {
1032
- name: 'chargingTimeRemaining',
1033
- role: 'value',
1034
- type: 'number',
1035
- write: false,
1036
- read: true,
1037
- },
1038
- native: {},
1039
- });
1040
- this.setState(vin + '.status.chargingTimeRemaining', 0, true);
1261
+ async checkEventStatus(eventId, headers) {
1262
+ try {
1263
+ const res = await this.requestClient({
1264
+ method: 'post',
1265
+ url: 'https://cocoapi.bmwgroup.com/eadrax-vrccs/v4/presentation/remote-commands/eventStatus?eventId=' + eventId,
1266
+ headers: headers,
1267
+ });
1268
+ this.log.debug(JSON.stringify(res.data));
1269
+ return res.data.rsEventStatus;
1270
+ } catch (error) {
1271
+ this.log.error('Remote command status failed');
1272
+ this.log.error(error);
1273
+ if (error.response) {
1274
+ this.log.error(JSON.stringify(error.response.data));
1275
+ if (error.response.status === 403) {
1276
+ return 'Rate Limit exceeded';
1041
1277
  }
1042
1278
  }
1279
+ return 'Failed';
1043
1280
  }
1044
1281
  }
1045
1282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.bmw",
3
- "version": "2.8.4",
3
+ "version": "2.9.1",
4
4
  "description": "Adapter for BMW",
5
5
  "author": {
6
6
  "name": "TA2k",
@@ -16,13 +16,14 @@
16
16
  "url": "https://github.com/TA2k/ioBroker.bmw"
17
17
  },
18
18
  "dependencies": {
19
- "@iobroker/adapter-core": "^3.2.2",
20
- "axios": "^1.7.7",
21
- "http-cookie-agent": "^6.0.6",
19
+ "@iobroker/adapter-core": "^3.2.3",
20
+ "axios": "^1.7.9",
21
+ "axios-retry": "^4.5.0",
22
+ "http-cookie-agent": "^6.0.8",
22
23
  "json-bigint": "^1.0.0",
23
- "json2iob": "^2.6.12",
24
- "qs": "^6.13.1",
25
- "tough-cookie": "^5.0.0"
24
+ "json2iob": "^2.6.14",
25
+ "qs": "^6.14.0",
26
+ "tough-cookie": "^5.1.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@alcalzone/release-script": "^3.8.0",
@@ -30,16 +31,16 @@
30
31
  "@alcalzone/release-script-plugin-license": "^3.7.0",
31
32
  "@alcalzone/release-script-plugin-manual-review": "^3.7.0",
32
33
  "@eslint/eslintrc": "^3.2.0",
33
- "@eslint/js": "^9.15.0",
34
- "@iobroker/testing": "^5.0.0",
34
+ "@eslint/js": "^9.19.0",
35
+ "@iobroker/testing": "^5.0.3",
35
36
  "@tsconfig/node16": "^16.1.3",
36
- "@types/node": "^22.9.1",
37
- "eslint": "^9.15.0",
37
+ "@types/node": "^22.12.0",
38
+ "eslint": "^9.19.0",
38
39
  "eslint-config-prettier": "^9.1.0",
39
- "eslint-plugin-prettier": "^5.2.1",
40
- "globals": "^15.12.0",
41
- "prettier": "^3.3.3",
42
- "typescript": "~5.6.3"
40
+ "eslint-plugin-prettier": "^5.2.3",
41
+ "globals": "^15.14.0",
42
+ "prettier": "^3.4.2",
43
+ "typescript": "~5.7.3"
43
44
  },
44
45
  "main": "main.js",
45
46
  "scripts": {