iobroker.bmw 2.1.1 → 2.5.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.
Files changed (4) hide show
  1. package/README.md +20 -1
  2. package/io-package.json +23 -2
  3. package/main.js +118 -179
  4. package/package.json +10 -11
package/README.md CHANGED
@@ -16,10 +16,29 @@
16
16
 
17
17
  Adapter for BMW
18
18
 
19
- Under remote you can control your car
19
+ **Aktueller Status**
20
+
21
+ bmw.0.VIN.properties
22
+
23
+ **Remote Befehle sind möglich unter**
24
+
25
+ bmw.0.VIN.remotev2
26
+
20
27
 
21
28
  ## Changelog
22
29
 
30
+ ### 2.5.0
31
+
32
+ - Fix login
33
+
34
+ ### 2.4.1
35
+
36
+ - Add support for MINI and force refresh remote
37
+
38
+ ### 2.3.0
39
+
40
+ - Disable v1 Endpoints
41
+
23
42
  ### 2.1.1
24
43
 
25
44
  - Upgrade to statusV2 and remoteV2
package/io-package.json CHANGED
@@ -1,9 +1,25 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "bmw",
4
- "version": "2.1.1",
4
+ "version": "2.5.1",
5
5
  "news": {
6
- "2.1.1": {
6
+ "2.5.1": {
7
+ "en": "Add login error message",
8
+ "de": "Loginproblem Nachricht hinzugefügt"
9
+ },
10
+ "2.5.0": {
11
+ "en": "Fix Login",
12
+ "de": "Loginproblem behoben"
13
+ },
14
+ "2.4.1": {
15
+ "en": "Add support for MINI and a force refresh remote",
16
+ "de": "Support für MINI hinzugefügt und ein Update erzwingen remote"
17
+ },
18
+ "2.3.0": {
19
+ "en": "Disable v1 Endpoints",
20
+ "de": "Deaktivieren v1 Endpunkte wurden entfernt."
21
+ },
22
+ "2.1.2": {
7
23
  "en": "Upgrade to statusV2 and remoteV2",
8
24
  "de": "Status und Remote Kontrolle auf v2 der neuen BMW App geupdated"
9
25
  },
@@ -64,6 +80,11 @@
64
80
  "connectionType": "cloud",
65
81
  "dataSource": "poll",
66
82
  "materialize": true,
83
+ "plugins": {
84
+ "sentry": {
85
+ "dsn": "https://f976d718acc2489fb0e1991d4c8d26a0@sentry.iobroker.net/148"
86
+ }
87
+ },
67
88
  "dependencies": [
68
89
  {
69
90
  "js-controller": ">=3.0.0"
package/main.js CHANGED
@@ -8,6 +8,8 @@
8
8
  // you need to create an adapter
9
9
  const utils = require("@iobroker/adapter-core");
10
10
  const axios = require("axios");
11
+
12
+ const crypto = require("crypto");
11
13
  const qs = require("qs");
12
14
  const { extractKeys } = require("./lib/extractKeys");
13
15
  const axiosCookieJarSupport = require("axios-cookiejar-support").default;
@@ -48,15 +50,16 @@ class Bmw extends utils.Adapter {
48
50
  this.statusBlock = {};
49
51
  this.nonChargingHistory = {};
50
52
  this.subscribeStates("*");
51
-
53
+ if (!this.config.username || !this.config.password) {
54
+ this.log.error("Please set username and password");
55
+ return;
56
+ }
52
57
  await this.login();
53
58
  if (this.session.access_token) {
54
59
  await this.getVehicles();
55
60
  await this.cleanObjects();
56
61
  await this.getVehiclesv2();
57
- await this.updateVehicles();
58
62
  this.updateInterval = setInterval(async () => {
59
- await this.updateVehicles();
60
63
  await this.getVehiclesv2();
61
64
  }, this.config.interval * 60 * 1000);
62
65
  this.refreshTokenInterval = setInterval(() => {
@@ -71,17 +74,21 @@ class Bmw extends utils.Adapter {
71
74
  "Accept-Language": "de-de",
72
75
  "Content-Type": "application/x-www-form-urlencoded",
73
76
  };
77
+ const [code_verifier, codeChallenge] = this.getCodeChallenge();
74
78
  const data = {
75
79
  client_id: "31c357a0-7a1d-4590-aa99-33b97244d048",
76
80
  response_type: "code",
77
81
  scope: "openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user",
78
82
  redirect_uri: "com.bmw.connected://oauth",
79
- state: "cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw",
83
+ state: "cwU-gIE27j67poy2UcL3KQ",
80
84
  nonce: "login_nonce",
85
+ code_challenge_method: "S256",
86
+ code_challenge: codeChallenge,
81
87
  username: this.config.username,
82
88
  password: this.config.password,
83
89
  grant_type: "authorization_code",
84
90
  };
91
+
85
92
  const authUrl = await this.requestClient({
86
93
  method: "post",
87
94
  url: "https://customer.bmwgroup.com/gcdm/oauth/authenticate",
@@ -100,7 +107,13 @@ class Bmw extends utils.Adapter {
100
107
  this.log.error(JSON.stringify(error.response.data));
101
108
  }
102
109
  if (error.response && error.response.status === 401) {
103
- this.log.error("Please check username and password");
110
+ this.log.error("Please check username and password or too many logins in 5 minutes");
111
+
112
+ this.log.error("Start relogin in 5min");
113
+ this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
114
+ this.reLoginTimeout = setTimeout(() => {
115
+ this.login();
116
+ }, 5000 * 60 * 1);
104
117
  }
105
118
  if (error.response && error.response.status === 400) {
106
119
  this.log.error("Please check username and password");
@@ -153,7 +166,7 @@ class Bmw extends utils.Adapter {
153
166
  "Accept-Language": "de-de",
154
167
  Authorization: "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg==",
155
168
  },
156
- data: "code=" + code + "&code_verifier=7PsmfPS5MpaNt0jEcPpi-B7M7u0gs1Nzw6ex0Y9pa-0&redirect_uri=com.bmw.connected://oauth&grant_type=authorization_code",
169
+ data: "code=" + code + "&redirect_uri=com.bmw.connected://oauth&grant_type=authorization_code&code_verifier=" + code_verifier,
157
170
  })
158
171
  .then((res) => {
159
172
  this.log.debug(JSON.stringify(res.data));
@@ -168,6 +181,17 @@ class Bmw extends utils.Adapter {
168
181
  }
169
182
  });
170
183
  }
184
+ getCodeChallenge() {
185
+ let hash = "";
186
+ let result = "";
187
+ const chars = "0123456789abcdef";
188
+ result = "";
189
+ for (let i = 64; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
190
+ hash = crypto.createHash("sha256").update(result).digest("base64");
191
+ hash = hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
192
+
193
+ return [result, hash];
194
+ }
171
195
  async getVehicles() {
172
196
  const headers = {
173
197
  "Content-Type": "application/json",
@@ -206,34 +230,6 @@ class Bmw extends utils.Adapter {
206
230
  native: {},
207
231
  });
208
232
 
209
- // const remoteArray = [
210
- // { command: "CHARGE_NOW" },
211
- // { command: "CLIMATE_NOW" },
212
- // { command: "DOOR_LOCK" },
213
- // { command: "DOOR_UNLOCK" },
214
- // { command: "GET_VEHICLES" },
215
- // { command: "GET_VEHICLE_STATUS" },
216
- // { command: "HORN_BLOW" },
217
- // { command: "LIGHT_FLASH" },
218
- // { command: "VEHICLE_FINDER" },
219
- // { command: "CLIMATE_NOW" },
220
- // { command: "START_CHARGING" },
221
- // { command: "STOP_CHARGING" },
222
- // { command: "START_PRECONDITIONING" },
223
- // ];
224
- // remoteArray.forEach((remote) => {
225
- // this.setObjectNotExists(vehicle.vin + ".remote." + remote.command, {
226
- // type: "state",
227
- // common: {
228
- // name: remote.name || "",
229
- // type: remote.type || "boolean",
230
- // role: remote.role || "boolean",
231
- // write: true,
232
- // read: true,
233
- // },
234
- // native: {},
235
- // });
236
- // });
237
233
  this.extractKeys(this, vehicle.vin + ".general", vehicle);
238
234
  }
239
235
  })
@@ -243,76 +239,80 @@ class Bmw extends utils.Adapter {
243
239
  });
244
240
  }
245
241
  async getVehiclesv2() {
246
- const headers = {
247
- "user-agent": "Dart/2.10 (dart:io)",
248
- "x-user-agent": "android(v1.07_20200330);bmw;1.5.2(8932)",
249
- authorization: "Bearer " + this.session.access_token,
250
- "accept-language": "de-DE",
251
- host: "cocoapi.bmwgroup.com",
252
- "24-hour-format": "true",
253
- };
242
+ const brands = ["bmw", "mini"];
243
+ for (const brand of brands) {
244
+ const headers = {
245
+ "user-agent": "Dart/2.10 (dart:io)",
246
+ "x-user-agent": "android(v1.07_20200330);" + brand + ";1.5.2(8932)",
247
+ authorization: "Bearer " + this.session.access_token,
248
+ "accept-language": "de-DE",
249
+ host: "cocoapi.bmwgroup.com",
250
+ "24-hour-format": "true",
251
+ };
254
252
 
255
- await this.requestClient({
256
- method: "get",
257
- url: "https://cocoapi.bmwgroup.com/eadrax-vcs/v1/vehicles?apptimezone=120&appDateTime=" + Date.now() + "&tireGuardMode=ENABLED",
258
- headers: headers,
259
- })
260
- .then(async (res) => {
261
- this.log.debug(JSON.stringify(res.data));
262
-
263
- for (const vehicle of res.data) {
264
- await this.setObjectNotExistsAsync(vehicle.vin, {
265
- type: "device",
266
- common: {
267
- name: vehicle.model,
268
- },
269
- native: {},
270
- });
253
+ await this.requestClient({
254
+ method: "get",
255
+ url: "https://cocoapi.bmwgroup.com/eadrax-vcs/v1/vehicles?apptimezone=120&appDateTime=" + Date.now() + "&tireGuardMode=ENABLED",
256
+ headers: headers,
257
+ })
258
+ .then(async (res) => {
259
+ this.log.debug(JSON.stringify(res.data));
271
260
 
272
- await this.setObjectNotExistsAsync(vehicle.vin + ".properties", {
273
- type: "channel",
274
- common: {
275
- name: "Current status of the car v2",
276
- },
277
- native: {},
278
- });
279
- await this.setObjectNotExistsAsync(vehicle.vin + ".remotev2", {
280
- type: "channel",
281
- common: {
282
- name: "Remote Controls",
283
- },
284
- native: {},
285
- });
261
+ for (const vehicle of res.data) {
262
+ await this.setObjectNotExistsAsync(vehicle.vin, {
263
+ type: "device",
264
+ common: {
265
+ name: vehicle.model,
266
+ },
267
+ native: {},
268
+ });
286
269
 
287
- const remoteArray = [
288
- { command: "door-lock" },
289
- { command: "door-unlock" },
290
- { command: "horn-blow" },
291
- { command: "light-flash" },
292
- { command: "vehicle-finder" },
293
- { command: "climate-now_START" },
294
- { command: "climate-now_STOP" },
295
- ];
296
- remoteArray.forEach((remote) => {
297
- this.setObjectNotExists(vehicle.vin + ".remotev2." + remote.command, {
298
- type: "state",
270
+ await this.setObjectNotExistsAsync(vehicle.vin + ".properties", {
271
+ type: "channel",
299
272
  common: {
300
- name: remote.name || "",
301
- type: remote.type || "boolean",
302
- role: remote.role || "boolean",
303
- write: true,
304
- read: true,
273
+ name: "Current status of the car v2",
305
274
  },
306
275
  native: {},
307
276
  });
308
- });
309
- this.extractKeys(this, vehicle.vin, vehicle, "infoLabel");
310
- this.updateChargingSessionv2(vehicle.vin);
311
- }
312
- })
313
- .catch((error) => {
314
- this.log.error(error);
315
- });
277
+ await this.setObjectNotExistsAsync(vehicle.vin + ".remotev2", {
278
+ type: "channel",
279
+ common: {
280
+ name: "Remote Controls",
281
+ },
282
+ native: {},
283
+ });
284
+
285
+ const remoteArray = [
286
+ { command: "door-lock" },
287
+ { command: "door-unlock" },
288
+ { command: "horn-blow" },
289
+ { command: "light-flash" },
290
+ { command: "vehicle-finder" },
291
+ { command: "climate-now_START" },
292
+ { command: "climate-now_STOP" },
293
+ { command: "force-refresh", name: "Force Refresh" },
294
+ ];
295
+ remoteArray.forEach((remote) => {
296
+ this.setObjectNotExists(vehicle.vin + ".remotev2." + remote.command, {
297
+ type: "state",
298
+ common: {
299
+ name: remote.name || "",
300
+ type: remote.type || "boolean",
301
+ role: remote.role || "boolean",
302
+ write: true,
303
+ read: true,
304
+ },
305
+ native: {},
306
+ });
307
+ });
308
+ this.extractKeys(this, vehicle.vin, vehicle, null, true);
309
+ this.updateChargingSessionv2(vehicle.vin);
310
+ }
311
+ })
312
+ .catch((error) => {
313
+ this.log.error(error);
314
+ });
315
+ }
316
316
  }
317
317
  async updateChargingSessionv2(vin) {
318
318
  if (this.nonChargingHistory[vin]) {
@@ -365,7 +365,7 @@ class Bmw extends utils.Adapter {
365
365
  this.extractKeys(this, vin + element.path + dateFormatted, data);
366
366
  })
367
367
  .catch((error) => {
368
- if (error.response && error.response.status === 422) {
368
+ if (error.response && (error.response.status === 422 || error.response.status === 403)) {
369
369
  this.log.info("No charging session available. Ignore " + vin);
370
370
  this.nonChargingHistory[vin] = true;
371
371
  return;
@@ -376,87 +376,20 @@ class Bmw extends utils.Adapter {
376
376
  });
377
377
  }
378
378
  }
379
- async updateVehicles() {
380
- const date = this.getDate();
381
379
 
382
- const statusArray = [
383
- { path: "statusv1", url: "https://b2vapi.bmwgroup.com/webapi/v1/user/vehicles/$vin/status", desc: "Current status of the car v1" },
384
- { path: "chargingprofile", url: "https://b2vapi.bmwgroup.com/webapi/v1/user/vehicles/$vin/chargingprofile", desc: "Charging profile of the car v1" },
385
- { path: "lastTrip", url: "https://b2vapi.bmwgroup.com/webapi/v1/user/vehicles/$vin/statistics/lastTrip", desc: "Last trip of the car v1" },
386
- { path: "allTrips", url: "https://b2vapi.bmwgroup.com/webapi/v1/user/vehicles/$vin/statistics/allTrips", desc: "All trips of the car v1" },
387
- { path: "serviceExecutionHistory", url: "https://b2vapi.bmwgroup.com/webapi/v1/user/vehicles/$vin/serviceExecutionHistory", desc: "Remote execution history v1" },
388
- { path: "apiV2", url: "https://b2vapi.bmwgroup.com/api/vehicle/v2/$vin", desc: "Limited v2 Api of the car" },
389
- // { path: "socnavigation", url: "https://b2vapi.bmwgroup.com/api/vehicle/navigation/v1/$vin" },
390
- ];
391
-
392
- const headers = {
393
- "Content-Type": "application/x-www-form-urlencoded",
394
- Accept: "application/json",
395
- Authorization: "Bearer " + this.session.access_token,
396
- };
397
- this.vinArray.forEach((vin) => {
398
- statusArray.forEach(async (element) => {
399
- let url = element.url.replace("$vin", vin);
400
- if (element.path === "statusv1") {
401
- if (this.statusBlock[vin]) {
402
- return;
403
- }
404
- url += "?deviceTime=" + date + "&dlat=0&dlon=0";
405
- }
406
- await this.requestClient({
407
- method: "get",
408
- url: url,
409
- headers: headers,
410
- })
411
- .then((res) => {
412
- this.log.debug(JSON.stringify(res.data));
413
- if (!res.data) {
414
- return;
415
- }
416
- let data = res.data;
417
- const keys = Object.keys(res.data);
418
- if (keys.length === 1) {
419
- data = res.data[keys[0]];
420
- }
421
- let forceIndex = null;
422
- const preferedArrayName = null;
423
- if (element.path === "serviceExecutionHistory") {
424
- forceIndex = true;
425
- }
426
-
427
- this.extractKeys(this, vin + "." + element.path, data, preferedArrayName, forceIndex, false, element.desc);
428
- })
429
- .catch((error) => {
430
- if (error.response && error.response.status === 401) {
431
- error.response && this.log.debug(JSON.stringify(error.response.data));
432
- this.log.info(element.path + " receive 401 error. Refresh Token in 30 seconds");
433
- clearTimeout(this.refreshTokenTimeout);
434
- this.refreshTokenTimeout = setTimeout(() => {
435
- this.refreshToken();
436
- }, 1000 * 30);
437
-
438
- return;
439
- }
440
- if (error.response && error.response.status === 404) {
441
- if (element.path === "statusv1") {
442
- this.statusBlock[vin] = true;
443
- }
444
- }
445
-
446
- this.log.error(url);
447
- this.log.error(error);
448
- error.response && this.log.error(JSON.stringify(error.response.data));
449
- });
450
- });
451
- });
452
- }
453
380
  async cleanObjects() {
454
381
  for (const vin of this.vinArray) {
455
- const remoteState = await this.getObjectAsync(vin + ".remote");
382
+ const remoteState = await this.getObjectAsync(vin + ".apiV2");
456
383
 
457
384
  if (remoteState) {
458
- this.log.debug("clean " + vin);
385
+ this.log.debug("clean old states" + vin);
386
+ await this.delObjectAsync(vin + ".statusv1", { recursive: true });
387
+ await this.delObjectAsync(vin + ".lastTrip", { recursive: true });
388
+ await this.delObjectAsync(vin + ".allTrips", { recursive: true });
459
389
  await this.delObjectAsync(vin + ".status", { recursive: true });
390
+ await this.delObjectAsync(vin + ".chargingprofile", { recursive: true });
391
+ await this.delObjectAsync(vin + ".serviceExecutionHistory", { recursive: true });
392
+ await this.delObjectAsync(vin + ".apiV2", { recursive: true });
460
393
  await this.delObject(vin + ".remote", { recursive: true });
461
394
  await this.delObject("_DatenNeuLaden");
462
395
  await this.delObject("_LetzterDatenabrufOK");
@@ -505,6 +438,7 @@ class Bmw extends utils.Adapter {
505
438
  this.log.error(error);
506
439
  error.response && this.log.error(JSON.stringify(error.response.data));
507
440
  this.log.error("Start relogin in 1min");
441
+ this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
508
442
  this.reLoginTimeout = setTimeout(() => {
509
443
  this.login();
510
444
  }, 1000 * 60 * 1);
@@ -536,16 +470,22 @@ class Bmw extends utils.Adapter {
536
470
  async onStateChange(id, state) {
537
471
  if (state) {
538
472
  if (!state.ack) {
473
+ if (id.indexOf(".remotev2.") === -1) {
474
+ this.log.warn("Please use remotev2 to control");
475
+ return;
476
+ }
477
+
539
478
  const vin = id.split(".")[2];
540
- const version = id.split(".")[3];
541
479
 
542
480
  let command = id.split(".")[4];
543
- const action = command.split("_")[1];
544
- command = command.split("_")[0];
545
- if (version === "remote") {
546
- this.log.warn("Please use remotev2");
481
+ if (command === "force-refresh") {
482
+ this.log.debug("force refresh");
483
+ this.getVehiclesv2();
547
484
  return;
548
485
  }
486
+ const action = command.split("_")[1];
487
+ command = command.split("_")[0];
488
+
549
489
  const headers = {
550
490
  "user-agent": "Dart/2.10 (dart:io)",
551
491
  "x-user-agent": "android(v1.07_20200330);bmw;1.5.2(8932)",
@@ -576,7 +516,6 @@ class Bmw extends utils.Adapter {
576
516
  }
577
517
  });
578
518
  this.refreshTimeout = setTimeout(async () => {
579
- await this.updateVehicles();
580
519
  await this.getVehiclesv2();
581
520
  }, 10 * 1000);
582
521
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.bmw",
3
- "version": "2.1.1",
3
+ "version": "2.5.1",
4
4
  "description": "Adapter for BMW",
5
5
  "author": {
6
6
  "name": "TA2k",
@@ -17,32 +17,31 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@iobroker/adapter-core": "^2.5.1",
20
- "axios": "^0.21.1",
20
+ "axios": "^0.24.0",
21
21
  "json-bigint": "^1.0.0",
22
22
  "qs": "^6.10.1",
23
23
  "axios-cookiejar-support": "^1.0.1",
24
24
  "tough-cookie": "^4.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@iobroker/testing": "^2.4.4",
28
- "@types/chai": "^4.2.21",
27
+ "@iobroker/testing": "^2.5.2",
28
+ "@types/chai": "^4.2.22",
29
29
  "@types/chai-as-promised": "^7.1.4",
30
30
  "@types/gulp": "^4.0.9",
31
31
  "@types/mocha": "^9.0.0",
32
- "@types/node": "^14.17.7",
32
+ "@types/node": "^14.17.32",
33
33
  "@types/proxyquire": "^1.3.28",
34
- "@types/sinon": "^10.0.2",
34
+ "@types/sinon": "^10.0.6",
35
35
  "@types/sinon-chai": "^3.2.5",
36
- "axios": "^0.21.1",
37
36
  "chai": "^4.3.4",
38
37
  "chai-as-promised": "^7.1.1",
39
- "eslint": "^7.32.0",
38
+ "eslint": "^8.1.0",
40
39
  "gulp": "^4.0.2",
41
- "mocha": "^9.0.3",
40
+ "mocha": "^9.1.3",
42
41
  "proxyquire": "^2.1.3",
43
- "sinon": "^11.1.2",
42
+ "sinon": "^12.0.0",
44
43
  "sinon-chai": "^3.7.0",
45
- "typescript": "^4.3.5"
44
+ "typescript": "~4.4.4"
46
45
  },
47
46
  "main": "main.js",
48
47
  "scripts": {