iobroker.bmw 2.9.0 → 2.9.2

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,11 @@ bmw.0.VIN.properties
25
25
  bmw.0.VIN.remotev2
26
26
 
27
27
  ## Changelog
28
+ ### 2.9.2 (2025-01-29)
29
+
30
+ - fix Remote Controls
31
+ - add Mitbenutzer Login for remote controls
32
+
28
33
  ### 2.9.0 (2024-11-28)
29
34
 
30
35
  - added new remotes as switch and updated values
@@ -80,7 +85,7 @@ bmw.0.VIN.remotev2
80
85
 
81
86
  MIT License
82
87
 
83
- Copyright (c) 2021-2024 TA2k <tombox2020@gmail.com>
88
+ Copyright (c) 2021-2030 TA2k <tombox2020@gmail.com>
84
89
 
85
90
  Permission is hereby granted, free of charge, to any person obtaining a copy
86
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 -->
@@ -147,6 +156,20 @@
147
156
  <label for="interval" class="translate">Update interval (in minutes)"</label>
148
157
  </div>
149
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>
150
173
  <div class="row">
151
174
  <div class="col s6 input-field">
152
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.9.0",
4
+ "version": "2.9.2",
5
5
  "news": {
6
+ "2.9.2": {
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.1": {
20
+ "en": "fix Remote Controls\nadd Mitbenutzer Login for remote controls",
21
+ "de": "remote Controls reparieren\nmitbenutzer Login für Fernbedienungen hinzufügen",
22
+ "ru": "починить удаленные управления\nдобавить логин Mitbenutzer для дистанционного управления",
23
+ "pt": "corrigir controles remotos\nadicionar Mitbenutzer Login para controles remotos",
24
+ "nl": "fix afstandsbedieningen\nmitbenutzer Login toevoegen voor afstandsbedieningen",
25
+ "fr": "correction des télécommandes\najouter Mitbenutzer Login pour les télécommandes",
26
+ "it": "correzione di controlli remoti\naggiungere Mitbenutzer Login per i comandi remoti",
27
+ "es": "controles remotos de fijación\nañadir Mitbenutzer Login para controles remotos",
28
+ "pl": "naprawić zdalne sterowanie\ndodaj Mitbenutzer Login do zdalnego sterowania",
29
+ "uk": "виправити дистанційні керування\nдодати Mitbenutzer Увійти для дистанційного керування",
30
+ "zh-cn": "修复远程控制\n添加远程控制的 Mitbenutzer 登录"
31
+ },
6
32
  "2.9.0": {
7
33
  "en": "added new remotes as switch and updated values\nadded retry logic for remotes",
8
34
  "de": "neue remotes als switch und mit aktualisierten Werte hinzugefügt\nzusätzliche retry-logik für remotes hinzufgefügt",
@@ -67,14 +93,6 @@
67
93
  "pl": "naprawianie zdalnych poleceń",
68
94
  "uk": "фіксувати віддалені команди",
69
95
  "zh-cn": "修正远程命令"
70
- },
71
- "2.8.0": {
72
- "en": "Add support service demand and trip api",
73
- "de": "Support für Service und Trips API hinzugefügt"
74
- },
75
- "2.7.2": {
76
- "en": "Move Rate Limit messages to debug",
77
- "de": "Rate Limit Nachrichten in debug verschoben"
78
96
  }
79
97
  },
80
98
  "titleLang": {
@@ -103,8 +121,12 @@
103
121
  "uk": "Adapter for BMW",
104
122
  "zh-cn": "宝马适配器"
105
123
  },
106
- "authors": ["TA2k <tombox2020@gmail.com>"],
107
- "keywords": ["BMW"],
124
+ "authors": [
125
+ "TA2k <tombox2020@gmail.com>"
126
+ ],
127
+ "keywords": [
128
+ "BMW"
129
+ ],
108
130
  "license": "MIT",
109
131
  "platform": "Javascript/Node.js",
110
132
  "main": "main.js",
@@ -135,11 +157,17 @@
135
157
  }
136
158
  ]
137
159
  },
138
- "encryptedNative": ["password"],
139
- "protectedNative": ["password"],
160
+ "encryptedNative": [
161
+ "password"
162
+ ],
163
+ "protectedNative": [
164
+ "password"
165
+ ],
140
166
  "native": {
141
167
  "username": "",
142
168
  "password": "",
169
+ "musername": "",
170
+ "mpassword": "",
143
171
  "interval": 5,
144
172
  "brand": "bmw",
145
173
  "ignorelist": "",
package/main.js CHANGED
@@ -28,8 +28,8 @@ class Bmw extends utils.Adapter {
28
28
  this.on('stateChange', this.onStateChange.bind(this));
29
29
  this.on('unload', this.onUnload.bind(this));
30
30
  this.userAgent = 'My%20BMW/8932 CFNetwork/978.0.7 Darwin/18.7.0';
31
- this.userAgentDart = 'Dart/2.14 (dart:io)';
32
- 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';
33
33
  this.updateInterval;
34
34
  this.reLoginTimeout;
35
35
  this.refreshTokenInterval;
@@ -207,6 +207,20 @@ class Bmw extends utils.Adapter {
207
207
 
208
208
  await this.login();
209
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
+ }
210
224
 
211
225
  if (this.session.access_token) {
212
226
  this.log.info(`Start getting ${this.config.brand} vehicles`);
@@ -244,12 +258,21 @@ class Bmw extends utils.Adapter {
244
258
  async () => {
245
259
  await this.refreshToken();
246
260
  await this.sleep(5000);
261
+ if (this.config.musername && this.config.mpassword) {
262
+ await this.refreshToken(true);
263
+ }
247
264
  },
248
265
  (this.session.expires_in - 123) * 1000,
249
266
  );
250
267
  }
251
268
  }
252
- 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
+ }
253
276
  const headers = {
254
277
  Accept: 'application/json, text/plain, */*',
255
278
  'User-Agent':
@@ -268,8 +291,8 @@ class Bmw extends utils.Adapter {
268
291
  nonce: 'login_nonce',
269
292
  code_challenge_method: 'S256',
270
293
  code_challenge: codeChallenge,
271
- username: this.config.username,
272
- password: this.config.password,
294
+ username: username,
295
+ password: password,
273
296
  grant_type: 'authorization_code',
274
297
  };
275
298
 
@@ -364,7 +387,7 @@ class Bmw extends utils.Adapter {
364
387
  })
365
388
  .then(async (res) => {
366
389
  this.log.debug(JSON.stringify(res.data));
367
- this.session = res.data;
390
+
368
391
  await this.extendObject('auth', {
369
392
  type: 'channel',
370
393
  common: {
@@ -383,7 +406,23 @@ class Bmw extends utils.Adapter {
383
406
  },
384
407
  native: {},
385
408
  });
386
-
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
+ }
387
426
  this.setState('auth.session', JSON.stringify(this.session), true);
388
427
  this.setState('info.connection', true, true);
389
428
  return res.data;
@@ -483,6 +522,7 @@ class Bmw extends utils.Adapter {
483
522
  { command: 'stop-charging', name: 'Stop Charging (Not updated)' },
484
523
  { command: 'charging', name: 'Charging True=Start, False=Stop' },
485
524
  { command: 'force-refresh', name: 'Force Refresh' },
525
+ { command: 'fetch-images', name: 'Fetch Images of the car in the image folder' },
486
526
  {
487
527
  command: 'fetch-charges',
488
528
  name: 'Fetch Charge Sessions/Statistics for month',
@@ -556,6 +596,18 @@ class Bmw extends utils.Adapter {
556
596
  res.data.state.electricChargingState.remainingChargingMinutes = 0;
557
597
  }
558
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);
559
611
  })
560
612
  .catch(async (error) => {
561
613
  if (error.response && error.response.status === 429) {
@@ -908,9 +960,71 @@ class Bmw extends utils.Adapter {
908
960
  ':00';
909
961
  return date_format_str;
910
962
  }
911
-
912
- 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) {
913
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
+ }
914
1028
  await this.requestClient({
915
1029
  method: 'post',
916
1030
  url: 'https://customer.bmwgroup.com/gcdm/oauth/token',
@@ -920,11 +1034,17 @@ class Bmw extends utils.Adapter {
920
1034
  Accept: '*/*',
921
1035
  Authorization: 'Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==',
922
1036
  },
923
- 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',
924
1038
  })
925
1039
  .then((res) => {
926
1040
  this.log.debug(JSON.stringify(res.data));
927
- 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
+ }
928
1048
 
929
1049
  this.setState('auth.session', JSON.stringify(this.session), true);
930
1050
  this.setState('info.connection', true, true);
@@ -990,6 +1110,11 @@ class Bmw extends utils.Adapter {
990
1110
  this.updateDevices();
991
1111
  return;
992
1112
  }
1113
+ if (command === 'fetch-images') {
1114
+ this.log.info('fetch images');
1115
+ await this.fetchImages(vin);
1116
+ return;
1117
+ }
993
1118
  if (command === 'fetch-charges') {
994
1119
  this.log.info('fetch charges');
995
1120
  await this.updateChargingSessionv2(vin, 200, state.val);
@@ -1015,11 +1140,15 @@ class Bmw extends utils.Adapter {
1015
1140
  }
1016
1141
  const action = command.split('_')[1];
1017
1142
  command = command.split('_')[0];
1018
-
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
+ }
1019
1148
  const headers = {
1020
1149
  'user-agent': this.userAgentDart,
1021
1150
  'x-user-agent': this.xuserAgent.replace(';brand;', `;${this.config.brand};`),
1022
- authorization: 'Bearer ' + this.session.access_token,
1151
+ authorization: 'Bearer ' + access_token,
1023
1152
  'accept-language': 'de-DE',
1024
1153
  host: 'cocoapi.bmwgroup.com',
1025
1154
  '24-hour-format': 'true',
@@ -1063,11 +1192,13 @@ class Bmw extends utils.Adapter {
1063
1192
  if (res === 'EXECUTED') {
1064
1193
  this.log.info('Remote command executed');
1065
1194
  } else {
1066
- this.log.info('Event is not finished it is: ' + res);
1195
+ this.log.info('Remote Event is not finished it is: ' + res);
1067
1196
  await this.sleep(10000);
1068
1197
  this.checkEventStatus(eventId, headers).then((res) => {
1069
1198
  if (res === 'EXECUTED') {
1070
1199
  this.log.info('Remote command executed');
1200
+ } else if (res === 'RUNNING') {
1201
+ this.log.info('Remote command is still running');
1071
1202
  } else {
1072
1203
  this.log.error('Remote command failed ' + res);
1073
1204
  }
@@ -1137,10 +1268,10 @@ class Bmw extends utils.Adapter {
1137
1268
  this.log.debug(JSON.stringify(res.data));
1138
1269
  return res.data.rsEventStatus;
1139
1270
  } catch (error) {
1140
- this.log.error('Remote command status failed');
1141
- this.log.error(error);
1271
+ this.log.warn('Cannot Fetch the status of the sent command. Status is Unknown');
1272
+ this.log.warn(error);
1142
1273
  if (error.response) {
1143
- this.log.error(JSON.stringify(error.response.data));
1274
+ this.log.warn(JSON.stringify(error.response.data));
1144
1275
  if (error.response.status === 403) {
1145
1276
  return 'Rate Limit exceeded';
1146
1277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.bmw",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "Adapter for BMW",
5
5
  "author": {
6
6
  "name": "TA2k",
@@ -16,14 +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",
19
+ "@iobroker/adapter-core": "^3.2.3",
20
+ "axios": "^1.7.9",
21
21
  "axios-retry": "^4.5.0",
22
- "http-cookie-agent": "^6.0.6",
22
+ "http-cookie-agent": "^6.0.8",
23
23
  "json-bigint": "^1.0.0",
24
- "json2iob": "^2.6.12",
25
- "qs": "^6.13.1",
26
- "tough-cookie": "^5.0.0"
24
+ "json2iob": "^2.6.14",
25
+ "qs": "^6.14.0",
26
+ "tough-cookie": "^5.1.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@alcalzone/release-script": "^3.8.0",
@@ -31,16 +31,16 @@
31
31
  "@alcalzone/release-script-plugin-license": "^3.7.0",
32
32
  "@alcalzone/release-script-plugin-manual-review": "^3.7.0",
33
33
  "@eslint/eslintrc": "^3.2.0",
34
- "@eslint/js": "^9.15.0",
35
- "@iobroker/testing": "^5.0.0",
34
+ "@eslint/js": "^9.19.0",
35
+ "@iobroker/testing": "^5.0.3",
36
36
  "@tsconfig/node16": "^16.1.3",
37
- "@types/node": "^22.9.1",
38
- "eslint": "^9.15.0",
37
+ "@types/node": "^22.12.0",
38
+ "eslint": "^9.19.0",
39
39
  "eslint-config-prettier": "^9.1.0",
40
- "eslint-plugin-prettier": "^5.2.1",
41
- "globals": "^15.12.0",
42
- "prettier": "^3.3.3",
43
- "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"
44
44
  },
45
45
  "main": "main.js",
46
46
  "scripts": {