iobroker.toyota 0.2.6 → 0.4.0

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/io-package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "toyota",
4
- "version": "0.2.6",
4
+ "version": "0.4.0",
5
5
  "news": {
6
+ "0.4.0": {
7
+ "en": "Align with MyToyota APK 2.25.2: fix climate command (start/stop), update electric status endpoint, update headers",
8
+ "de": "Angleichung an MyToyota APK 2.25.2: Klimasteuerung (start/stop) korrigiert, Electric-Status-Endpunkt und Header aktualisiert"
9
+ },
10
+ "0.2.7": {
11
+ "en": "Align with MyToyota APK 2.25.2: fix climate command (start/stop), update electric status endpoint, update headers",
12
+ "de": "Angleichung an MyToyota APK 2.25.2: Klimasteuerung (start/stop) korrigiert, Electric-Status-Endpunkt und Header aktualisiert"
13
+ },
6
14
  "0.2.6": {
7
15
  "en": "Skip endpoints that are not available",
8
16
  "de": "Überspringen von Endpunkten, die nicht verfügbar sind"
package/lib/tools.js CHANGED
@@ -64,7 +64,7 @@ async function translateYandex(text, targetLang, apiKey) {
64
64
  }
65
65
  throw new Error('Invalid response for translate request');
66
66
  } catch (e) {
67
- throw new Error(`Could not translate to "${targetLang}": ${e}`);
67
+ throw new Error(`Could not translate to "${targetLang}": ${e}`, { cause: e });
68
68
  }
69
69
  }
70
70
 
@@ -87,9 +87,9 @@ async function translateGoogle(text, targetLang) {
87
87
  throw new Error('Invalid response for translate request');
88
88
  } catch (e) {
89
89
  if (e.response && e.response.status === 429) {
90
- throw new Error(`Could not translate to "${targetLang}": Rate-limited by Google Translate`);
90
+ throw new Error(`Could not translate to "${targetLang}": Rate-limited by Google Translate`, { cause: e });
91
91
  } else {
92
- throw new Error(`Could not translate to "${targetLang}": ${e}`);
92
+ throw new Error(`Could not translate to "${targetLang}": ${e}`, { cause: e });
93
93
  }
94
94
  }
95
95
  }
package/main.js CHANGED
@@ -13,6 +13,9 @@ const qs = require('qs');
13
13
  const tough = require('tough-cookie');
14
14
  const { HttpsCookieAgent } = require('http-cookie-agent/http');
15
15
  const jwt = require('jsonwebtoken');
16
+ const { v4: uuidv4 } = require('uuid');
17
+ const crypto = require('crypto');
18
+
16
19
  class Toyota extends utils.Adapter {
17
20
  /**
18
21
  * @param {Partial<utils.AdapterOptions>} [options={}]
@@ -43,8 +46,10 @@ class Toyota extends utils.Adapter {
43
46
  this.updateInterval = null;
44
47
  this.reLoginTimeout = null;
45
48
  this.refreshTokenTimeout = null;
46
- this.hostName = 'myt-agg.toyota-europe.com';
47
49
  this.brand = 'T';
50
+ // Aligned with MyToyota APK 2.25.2
51
+ this.CLIENT_VERSION = '2.25.2';
52
+ this.API_VERSION = 'resource=2.1, protocol=1.0';
48
53
  }
49
54
 
50
55
  /**
@@ -65,7 +70,6 @@ class Toyota extends utils.Adapter {
65
70
  if (this.config.type === 'lexus') {
66
71
  this.log.info('Login to Lexus');
67
72
  this.brand = 'L';
68
- this.hostName = 'lexuslink-agg.toyota-europe.com';
69
73
  }
70
74
  if (!this.config.username || !this.config.password) {
71
75
  this.log.error('No username or password set');
@@ -92,6 +96,7 @@ class Toyota extends utils.Adapter {
92
96
  }, 3500 * 1000);
93
97
  }
94
98
  }
99
+
95
100
  async login() {
96
101
  this.log.info('Login to Toyota');
97
102
  const firstResponse = await this.requestClient({
@@ -99,20 +104,7 @@ class Toyota extends utils.Adapter {
99
104
  maxBodyLength: Infinity,
100
105
  url: 'https://b2c-login.toyota-europe.com/json/realms/root/realms/tme/authenticate?authIndexType=service&authIndexValue=oneapp',
101
106
  headers: {
102
- 'x-osname': 'iOS',
103
- 'x-brand': this.brand,
104
- accept: '*/*',
105
- 'x-channel': 'ONEAPP',
106
- brand: this.brand,
107
- 'x-region': 'EU',
108
- 'x-appbrand': this.brand,
109
- 'x-correlationid': 'B10AD742-22D0-4211-8B25-B213BE9A8A00',
110
- 'x-osversion': '16.7.2',
111
- 'accept-language': 'de-DE,de;q=0.9',
112
- region: 'EU',
113
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
114
- 'accept-api-version': 'resource=2.0, protocol=1.0',
115
- 'x-appversion': '2.4.2',
107
+ ...this.buildLoginHeaders('B10AD742-22D0-4211-8B25-B213BE9A8A00'),
116
108
  Cookie: 'route=e8e8b55de08efd3c4b34265c0069d319',
117
109
  },
118
110
  })
@@ -127,26 +119,14 @@ class Toyota extends utils.Adapter {
127
119
  this.log.error(JSON.stringify(error.response.data));
128
120
  }
129
121
  });
122
+
130
123
  const secondResponse = await this.requestClient({
131
124
  method: 'post',
132
125
  maxBodyLength: Infinity,
133
126
  url: 'https://b2c-login.toyota-europe.com/json/realms/root/realms/tme/authenticate?authIndexType=service&authIndexValue=oneapp',
134
127
  headers: {
135
- 'x-appbrand': this.brand,
136
- 'x-osname': 'iOS',
137
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
138
- 'x-region': 'EU',
139
- region: 'EU',
140
- brand: this.brand,
141
- 'x-channel': 'ONEAPP',
142
- 'x-osversion': '16.7.2',
143
- 'x-brand': this.brand,
144
- 'accept-language': 'de-DE,de;q=0.9',
145
- 'x-correlationid': 'BDD02A22-CD76-4877-90A9-196EDA5DC695',
146
- 'x-appversion': '2.4.2',
147
- accept: '*/*',
128
+ ...this.buildLoginHeaders('BDD02A22-CD76-4877-90A9-196EDA5DC695'),
148
129
  'content-type': 'application/json',
149
- 'accept-api-version': 'resource=2.0, protocol=1.0',
150
130
  },
151
131
  data: firstResponse,
152
132
  })
@@ -161,30 +141,20 @@ class Toyota extends utils.Adapter {
161
141
  this.log.error(JSON.stringify(error.response.data));
162
142
  }
163
143
  });
144
+
164
145
  if (!secondResponse || !secondResponse.callbacks) {
165
146
  this.log.error('No Second Response');
147
+ return;
166
148
  }
167
149
  secondResponse.callbacks[0].input[0].value = this.config.username;
150
+
168
151
  const thirdResponse = await this.requestClient({
169
152
  method: 'post',
170
153
  maxBodyLength: Infinity,
171
154
  url: 'https://b2c-login.toyota-europe.com/json/realms/root/realms/tme/authenticate?authIndexType=service&authIndexValue=oneapp',
172
155
  headers: {
173
- 'x-appbrand': this.brand,
174
- 'x-osname': 'iOS',
175
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
176
- 'x-region': 'EU',
177
- region: 'EU',
178
- brand: this.brand,
179
- 'x-channel': 'ONEAPP',
180
- 'x-osversion': '16.7.2',
181
- 'x-brand': this.brand,
182
- 'accept-language': 'de-DE,de;q=0.9',
183
- 'x-correlationid': 'BDD02A22-CD76-4877-90A9-196EDA5DC695',
184
- 'x-appversion': '2.4.2',
185
- accept: '*/*',
156
+ ...this.buildLoginHeaders('BDD02A22-CD76-4877-90A9-196EDA5DC695'),
186
157
  'content-type': 'application/json',
187
- 'accept-api-version': 'resource=2.0, protocol=1.0',
188
158
  },
189
159
  data: secondResponse,
190
160
  })
@@ -199,31 +169,21 @@ class Toyota extends utils.Adapter {
199
169
  this.log.error(JSON.stringify(error.response.data));
200
170
  }
201
171
  });
172
+
202
173
  if (!thirdResponse || !thirdResponse.callbacks) {
203
174
  this.log.error('No Third Response');
175
+ return;
204
176
  }
205
177
 
206
178
  thirdResponse.callbacks[0].input[0].value = this.config.password;
207
- const idToken = await this.requestClient({
179
+
180
+ await this.requestClient({
208
181
  method: 'post',
209
182
  maxBodyLength: Infinity,
210
183
  url: 'https://b2c-login.toyota-europe.com/json/realms/root/realms/tme/authenticate?authIndexType=service&authIndexValue=oneapp',
211
184
  headers: {
212
- 'x-appbrand': this.brand,
213
- 'x-osname': 'iOS',
214
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
215
- 'x-region': 'EU',
216
- region: 'EU',
217
- brand: this.brand,
218
- 'x-channel': 'ONEAPP',
219
- 'x-osversion': '16.7.2',
220
- 'x-brand': this.brand,
221
- 'accept-language': 'de-DE,de;q=0.9',
222
- 'x-correlationid': 'BDD02A22-CD76-4877-90A9-196EDA5DC695',
223
- 'x-appversion': '2.4.2',
224
- accept: '*/*',
185
+ ...this.buildLoginHeaders('BDD02A22-CD76-4877-90A9-196EDA5DC695'),
225
186
  'content-type': 'application/json',
226
- 'accept-api-version': 'resource=2.0, protocol=1.0',
227
187
  },
228
188
  data: thirdResponse,
229
189
  })
@@ -239,25 +199,24 @@ class Toyota extends utils.Adapter {
239
199
  }
240
200
  });
241
201
 
202
+ // PKCE S256: random 64-byte verifier, base64url(SHA-256(verifier)) as challenge
203
+ // Keep verifier local so concurrent login() calls do not overwrite each other.
204
+ const codeVerifier = crypto.randomBytes(64).toString('base64url');
205
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
206
+ const authorizeUrl =
207
+ 'https://b2c-login.toyota-europe.com/oauth2/realms/root/realms/tme/authorize' +
208
+ '?response_type=code&realm=tme&redirect_uri=com.toyota.oneapp:/oauth2Callback' +
209
+ '&client_id=oneapp&scope=' +
210
+ encodeURIComponent('openid profile write') +
211
+ '&code_challenge_method=S256&code_challenge=' +
212
+ codeChallenge;
242
213
  const tokenResponse = await this.requestClient({
243
214
  method: 'get',
244
215
  maxBodyLength: Infinity,
245
- url: 'https://b2c-login.toyota-europe.com/oauth2/realms/root/realms/tme/authorize?response_type=code&realm=tme&redirect_uri=com.toyota.oneapp:/oauth2Callback&client_id=oneapp&scope=openid%20profile%20write&code_challenge_method=S256&code_challenge=Bx88SxgIEnvxrsobwijnUlzg3rrb-zNV4wzDlndWFVc',
216
+ url: authorizeUrl,
246
217
  headers: {
247
- 'x-osname': 'iOS',
248
- 'x-brand': this.brand,
218
+ ...this.buildLoginHeaders('0F34C246-11F3-4584-AB13-0EA5DA96CB41'),
249
219
  accept: '*/*',
250
- 'x-channel': 'ONEAPP',
251
- brand: this.brand,
252
- 'x-correlationid': '0F34C246-11F3-4584-AB13-0EA5DA96CB41',
253
- 'x-region': 'EU',
254
- 'x-appbrand': this.brand,
255
- 'x-osversion': '16.7.2',
256
- 'accept-language': 'de-DE,de;q=0.9',
257
- region: 'EU',
258
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
259
- 'accept-api-version': 'resource=2.0, protocol=1.0',
260
- 'x-appversion': '2.4.2',
261
220
  },
262
221
  })
263
222
  .then((res) => {
@@ -272,38 +231,28 @@ class Toyota extends utils.Adapter {
272
231
  this.log.error(error);
273
232
  error.response && this.log.error(JSON.stringify(error.response.data));
274
233
  });
234
+
275
235
  if (!tokenResponse || !tokenResponse.code) {
276
236
  this.log.error('No Token Response');
277
237
  return;
278
238
  }
239
+
279
240
  this.log.info('Start token exchange');
280
241
  await this.requestClient({
281
242
  method: 'post',
282
243
  maxBodyLength: Infinity,
283
244
  url: 'https://b2c-login.toyota-europe.com/oauth2/realms/root/realms/tme/access_token',
284
245
  headers: {
285
- 'x-appbrand': this.brand,
286
- 'x-osname': 'iOS',
287
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
288
- 'x-region': 'EU',
289
- region: 'EU',
290
- brand: this.brand,
291
- 'x-channel': 'ONEAPP',
292
- 'x-osversion': '16.7.2',
293
- 'x-brand': this.brand,
294
- authorization: 'Basic b25lYXBwOm9uZWFwcA==',
295
- 'accept-language': 'de-DE,de;q=0.9',
296
- 'x-correlationid': 'E422A08C-1A04-415E-BB08-2386EE06CF90',
297
- 'x-appversion': '2.4.2',
246
+ ...this.buildLoginHeaders('E422A08C-1A04-415E-BB08-2386EE06CF90'),
298
247
  accept: '*/*',
299
248
  'content-type': 'application/x-www-form-urlencoded',
300
- 'accept-api-version': 'resource=2.0, protocol=1.0',
249
+ authorization: 'Basic b25lYXBwOm9uZWFwcA==',
301
250
  },
302
251
  data: {
303
252
  grant_type: 'authorization_code',
304
253
  redirect_uri: 'com.toyota.oneapp:/oauth2Callback',
305
254
  code: tokenResponse.code,
306
- code_verifier: 'tsY5-j-ZLYxNVnmz6wmJ9PTm2Ly7QpfXcQnhyU09Pog',
255
+ code_verifier: codeVerifier,
307
256
  client_id: 'oneapp',
308
257
  },
309
258
  })
@@ -326,34 +275,22 @@ class Toyota extends utils.Adapter {
326
275
  }
327
276
  });
328
277
  }
278
+
329
279
  async refreshToken() {
330
280
  await this.requestClient({
331
281
  method: 'post',
332
282
  maxBodyLength: Infinity,
333
283
  url: 'https://b2c-login.toyota-europe.com/oauth2/realms/root/realms/tme/access_token',
334
284
  headers: {
335
- 'x-appbrand': this.brand,
336
- 'x-osname': 'iOS',
337
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
338
- 'x-region': 'EU',
339
- region: 'EU',
340
- brand: this.brand,
341
- 'x-channel': 'ONEAPP',
342
- 'x-osversion': '16.7.2',
343
- 'x-brand': this.brand,
344
- authorization: 'Basic b25lYXBwOm9uZWFwcA==',
345
- 'accept-language': 'de-DE,de;q=0.9',
346
- 'x-correlationid': 'E422A08C-1A04-415E-BB08-2386EE06CF90',
347
- 'x-appversion': '2.4.2',
285
+ ...this.buildLoginHeaders('E422A08C-1A04-415E-BB08-2386EE06CF90'),
348
286
  accept: '*/*',
349
287
  'content-type': 'application/x-www-form-urlencoded',
350
- 'accept-api-version': 'resource=2.0, protocol=1.0',
288
+ authorization: 'Basic b25lYXBwOm9uZWFwcA==',
351
289
  },
352
290
  data: {
353
291
  grant_type: 'refresh_token',
354
292
  redirect_uri: 'com.toyota.oneapp:/oauth2Callback',
355
293
  client_id: 'oneapp',
356
- code_verifier: 'plain',
357
294
  refresh_token: this.session.refresh_token,
358
295
  },
359
296
  })
@@ -369,33 +306,137 @@ class Toyota extends utils.Adapter {
369
306
  }
370
307
  });
371
308
  }
309
+
310
+ generate_hmac_sha256(key, message) {
311
+ return crypto.createHmac('sha256', key).update(message).digest('hex');
312
+ }
313
+
314
+ /**
315
+ * Headers for the ForgeRock authentication flow (login / token exchange).
316
+ * Mirrors the Android APK interceptors (b.smali + auth flow).
317
+ * @param {string} [correlationId] optional fixed correlation ID for login steps
318
+ */
319
+ buildLoginHeaders(correlationId) {
320
+ return {
321
+ 'x-osname': 'Android',
322
+ 'x-osversion': '14',
323
+ 'x-brand': this.brand,
324
+ 'x-appbrand': this.brand,
325
+ brand: this.brand,
326
+ 'x-channel': 'ONEAPP',
327
+ 'x-region': 'EU',
328
+ region: 'EU',
329
+ 'x-appversion': this.CLIENT_VERSION,
330
+ 'x-correlationid': correlationId || uuidv4(),
331
+ 'accept-language': 'de-DE,de;q=0.9',
332
+ accept: 'application/json',
333
+ 'user-agent': 'okhttp/4.10.0',
334
+ 'accept-api-version': this.API_VERSION,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Returns true if an axios error response signals that the requested endpoint
340
+ * is not applicable for the current VIN (e.g. /electric/status on a non-EV car).
341
+ * Toyota answers with HTTP 4xx and responseCode ONE-GLOBAL-RC-10004.
342
+ * @param {{status: number, data?: any} | undefined} response
343
+ */
344
+ isCapabilityNotPresent(response) {
345
+ if (!response || typeof response.status !== 'number' || response.status < 400 || response.status >= 500) {
346
+ return false;
347
+ }
348
+ const messages = response.data && response.data.status && response.data.status.messages;
349
+ if (!Array.isArray(messages)) {
350
+ return false;
351
+ }
352
+ return messages.some((m) => m && m.responseCode === 'ONE-GLOBAL-RC-10004');
353
+ }
354
+
355
+ /**
356
+ * Build the standard set of headers used for all ctpa-oneapi calls.
357
+ * Aligned with pytoyoda controller._prepare_headers.
358
+ * @param {string} [vin] optional vin header for vehicle-scoped endpoints
359
+ */
360
+ buildApiHeaders(vin) {
361
+ const headers = {
362
+ 'x-api-key': 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
363
+ API_KEY: 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
364
+ 'x-guid': this.uuid,
365
+ guid: this.uuid,
366
+ 'x-client-ref': this.generate_hmac_sha256(this.CLIENT_VERSION, this.uuid),
367
+ 'x-correlationid': uuidv4(),
368
+ 'x-appversion': this.CLIENT_VERSION,
369
+ 'x-channel': 'ONEAPP',
370
+ 'x-brand': this.brand,
371
+ 'x-region': 'EU',
372
+ 'x-user-region': 'EU',
373
+ authorization: 'Bearer ' + this.session.access_token,
374
+ 'user-agent': 'okhttp/4.10.0',
375
+ };
376
+ // x-appbrand and brand are only set for Lexus (L); Toyota (T) omits them
377
+ if (this.brand !== 'T') {
378
+ headers['x-appbrand'] = this.brand;
379
+ headers.brand = this.brand;
380
+ }
381
+ if (vin) {
382
+ headers.vin = vin;
383
+ }
384
+ return headers;
385
+ }
386
+
387
+ /**
388
+ * Wake the car and trigger a fresh vehicle/climate/EV status pull.
389
+ * After ~20s the regular updateDevices() loop reads the now-current data.
390
+ * @param {string} vin
391
+ */
392
+ async forceRefresh(vin) {
393
+ const refreshEndpoints = [
394
+ { name: 'status', url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/remote/status' },
395
+ {
396
+ name: 'climate',
397
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/remote/refresh-climate-status',
398
+ },
399
+ {
400
+ name: 'electric',
401
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v2/remote/electric/realtime-status',
402
+ },
403
+ ];
404
+ for (const ep of refreshEndpoints) {
405
+ await this.requestClient({
406
+ method: 'post',
407
+ url: ep.url,
408
+ headers: this.buildApiHeaders(vin),
409
+ })
410
+ .then((res) => {
411
+ this.log.debug(`force-refresh ${ep.name}: ${JSON.stringify(res.data)}`);
412
+ })
413
+ .catch((error) => {
414
+ // Non-EV cars typically answer the electric realtime-status request with 404 or 422 - log quietly.
415
+ // 401/403 must NOT be swallowed: they signal an auth problem and have to surface like for any other endpoint.
416
+ if (
417
+ ep.name === 'electric' &&
418
+ error.response &&
419
+ (error.response.status === 404 || error.response.status === 422)
420
+ ) {
421
+ this.log.debug(`force-refresh electric not supported (${error.response.status})`);
422
+ return;
423
+ }
424
+ this.log.warn(`force-refresh ${ep.name} failed: ${error.message}`);
425
+ error.response && this.log.debug(JSON.stringify(error.response.data));
426
+ });
427
+ }
428
+ this.refreshTimeout && clearTimeout(this.refreshTimeout);
429
+ this.refreshTimeout = setTimeout(async () => {
430
+ await this.updateDevices();
431
+ }, 20 * 1000);
432
+ }
433
+
372
434
  async getDeviceList() {
373
435
  await this.requestClient({
374
436
  method: 'get',
375
437
  maxBodyLength: Infinity,
376
438
  url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v2/vehicle/guid',
377
- headers: {
378
- 'x-appbrand': this.brand,
379
- 'x-device-timezone': 'CEST',
380
- 'x-osname': 'iOS',
381
- guid: this.uuid,
382
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
383
- 'x-guid': this.uuid,
384
- 'x-region': 'EU',
385
- region: 'EU',
386
- brand: this.brand,
387
- 'x-channel': 'ONEAPP',
388
- 'x-osversion': '16.7.2',
389
- 'x-locale': 'de-DE',
390
- 'x-brand': this.brand,
391
- authorization: 'Bearer ' + this.session.access_token,
392
- 'accept-language': 'de-DE,de;q=0.9',
393
- 'x-correlationid': '7683DC30-D4DA-4FEC-850E-F3557A7DCEF4',
394
- 'x-appversion': '2.4.2',
395
- accept: '*/*',
396
- 'x-user-region': 'DE',
397
- 'x-api-key': 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
398
- },
439
+ headers: this.buildApiHeaders(),
399
440
  })
400
441
  .then(async (res) => {
401
442
  this.log.debug(JSON.stringify(res.data));
@@ -437,7 +478,8 @@ class Toyota extends utils.Adapter {
437
478
 
438
479
  const remoteArray = [
439
480
  { command: 'climate-control', name: 'True = Start, False = Stop' },
440
- { command: 'refresh', name: 'Refresh Status' },
481
+ { command: 'refresh', name: 'Re-poll cached status from cloud' },
482
+ { command: 'force-refresh', name: 'Wake car and request fresh status, climate and EV data' },
441
483
  { command: 'door', name: 'True = Lock, False = Unlock' },
442
484
  ];
443
485
  for (const remote of remoteArray) {
@@ -463,6 +505,8 @@ class Toyota extends utils.Adapter {
463
505
  error.response && this.log.error(JSON.stringify(error.response.data));
464
506
  });
465
507
  }
508
+
509
+ // ...existing code...
466
510
  async cleanOldObjects(vin) {
467
511
  const remoteState = await this.getObjectAsync(vin + '.addtionalInfo');
468
512
  if (remoteState) {
@@ -470,11 +514,12 @@ class Toyota extends utils.Adapter {
470
514
  await this.delObjectAsync(vin, { recursive: true });
471
515
  }
472
516
  }
517
+
473
518
  async updateDevices() {
474
519
  const statusArray = [
475
520
  {
476
521
  path: 'status',
477
- url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/global/remote/status',
522
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/vehicle/status',
478
523
  desc: 'Status of the car',
479
524
  },
480
525
  {
@@ -484,45 +529,37 @@ class Toyota extends utils.Adapter {
484
529
  },
485
530
  {
486
531
  path: 'climate',
487
- url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/global/remote/climate-status',
532
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/vehicle/climate-status',
488
533
  desc: 'Climate of the car',
489
534
  },
535
+ {
536
+ path: 'electric',
537
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/vehicle/electric/status',
538
+ desc: 'Electric/PHEV status (battery, charging, range)',
539
+ },
540
+ {
541
+ path: 'location',
542
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/location',
543
+ desc: 'Current vehicle location',
544
+ },
545
+ {
546
+ path: 'health',
547
+ url: 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/vehiclehealth/status',
548
+ desc: 'Vehicle health and service warnings',
549
+ },
490
550
  ];
491
551
 
492
- const headers = {
493
- 'x-appbrand': this.brand,
494
- 'x-device-timezone': 'CEST',
495
- 'x-osname': 'iOS',
496
- guid: this.uuid,
497
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
498
- 'x-guid': this.uuid,
499
- 'x-region': 'EU',
500
- region: 'EU',
501
- brand: this.brand,
502
- 'x-channel': 'ONEAPP',
503
- 'x-osversion': '16.7.2',
504
- 'x-locale': 'de-DE',
505
- 'x-brand': this.brand,
506
- authorization: 'Bearer ' + this.session.access_token,
507
- 'accept-language': 'de-DE,de;q=0.9',
508
- 'x-correlationid': '7683DC30-D4DA-4FEC-850E-F3557A7DCEF4',
509
- 'x-appversion': '2.4.2',
510
- accept: '*/*',
511
- 'x-user-region': 'DE',
512
- 'x-api-key': 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
513
- };
514
552
  for (const vin of this.deviceArray) {
515
553
  for (const element of statusArray) {
516
554
  if (this.blockedEndpoints[vin] && this.blockedEndpoints[vin].includes(element.path)) {
517
555
  continue;
518
556
  }
519
557
  const url = element.url.replace('$vin', vin);
520
- headers.vin = vin;
521
558
 
522
559
  await this.requestClient({
523
560
  method: 'get',
524
561
  url: url,
525
- headers: headers,
562
+ headers: this.buildApiHeaders(vin),
526
563
  })
527
564
  .then((res) => {
528
565
  this.log.debug(JSON.stringify(res.data));
@@ -559,6 +596,14 @@ class Toyota extends utils.Adapter {
559
596
  this.blockedEndpoints[vin].push(element.path);
560
597
  return;
561
598
  }
599
+ // Endpoint not applicable for this VIN (e.g. /electric/status on a non-EV/PHEV car):
600
+ // Toyota answers HTTP 4xx with responseCode ONE-GLOBAL-RC-10004 every poll. Block it once.
601
+ if (this.isCapabilityNotPresent(error.response)) {
602
+ this.log.info(`${element.path} not available for ${vin}. Skip until restart`);
603
+ this.blockedEndpoints[vin] = this.blockedEndpoints[vin] || [];
604
+ this.blockedEndpoints[vin].push(element.path);
605
+ return;
606
+ }
562
607
  }
563
608
  this.log.error(url);
564
609
  this.log.error(error);
@@ -579,38 +624,14 @@ class Toyota extends utils.Adapter {
579
624
  },
580
625
  ];
581
626
 
582
- const headers = {
583
- 'x-appbrand': this.brand,
584
- 'x-device-timezone': 'CEST',
585
- 'x-osname': 'iOS',
586
-
587
- guid: this.uuid,
588
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
589
- 'x-guid': this.uuid,
590
- 'x-region': 'EU',
591
- region: 'EU',
592
- brand: this.brand,
593
- 'x-channel': 'ONEAPP',
594
- 'x-osversion': '16.7.2',
595
- 'x-locale': 'de-DE',
596
- 'x-brand': this.brand,
597
- authorization: 'Bearer ' + this.session.access_token,
598
- 'accept-language': 'de-DE,de;q=0.9',
599
- 'x-correlationid': '7683DC30-D4DA-4FEC-850E-F3557A7DCEF4',
600
- 'x-appversion': '2.4.2',
601
- accept: '*/*',
602
- 'x-user-region': 'DE',
603
- 'x-api-key': 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
604
- };
605
627
  for (const vin of this.deviceArray) {
606
628
  statusArray.forEach(async (element) => {
607
629
  const url = element.url.replace('$vin', vin);
608
- headers.vin = vin;
609
630
 
610
631
  await this.requestClient({
611
632
  method: 'get',
612
633
  url: url,
613
- headers: headers,
634
+ headers: this.buildApiHeaders(vin),
614
635
  })
615
636
  .then((res) => {
616
637
  this.log.debug(JSON.stringify(res.data));
@@ -618,6 +639,12 @@ class Toyota extends utils.Adapter {
618
639
  return;
619
640
  }
620
641
  const data = res.data.payload;
642
+ for (const trip of data.trips) {
643
+ delete trip.route;
644
+ }
645
+ for (const summary of data.summary) {
646
+ delete summary.histograms;
647
+ }
621
648
  this.json2iob.parse(vin + '.' + element.path, data, {
622
649
  forceIndex: true,
623
650
  channelName: element.desc,
@@ -643,6 +670,7 @@ class Toyota extends utils.Adapter {
643
670
  });
644
671
  }
645
672
  }
673
+
646
674
  /**
647
675
  * Is called when adapter shuts down - callback has to be called under any circumstances!
648
676
  * @param {() => void} callback
@@ -656,7 +684,7 @@ class Toyota extends utils.Adapter {
656
684
  this.updateInterval && clearInterval(this.updateInterval);
657
685
  this.refreshTokenInterval && clearInterval(this.refreshTokenInterval);
658
686
  callback();
659
- } catch (e) {
687
+ } catch {
660
688
  callback();
661
689
  }
662
690
  }
@@ -678,11 +706,16 @@ class Toyota extends utils.Adapter {
678
706
  this.updateDevices();
679
707
  return;
680
708
  }
709
+ if (path === 'force-refresh') {
710
+ await this.forceRefresh(deviceId);
711
+ return;
712
+ }
681
713
  const data = {};
682
714
  let url = 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/global/remote/command';
683
715
  if (path === 'climate-control') {
684
- url = 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v1/global/remote/climate-control';
685
- data.command = state.val ? 'engine-start' : 'engine-stop';
716
+ url = 'https://ctpa-oneapi.tceu-ctp-prd.toyotaconnectedeurope.io/v2/remote/climate-control';
717
+ // V2 climate-control expects command "start"/"stop" (not the old v1 engine-start/engine-stop)
718
+ data.command = state.val ? 'start' : 'stop';
686
719
  }
687
720
  if (path === 'door') {
688
721
  data.command = state.val ? 'door-lock' : 'door-unlock';
@@ -693,34 +726,14 @@ class Toyota extends utils.Adapter {
693
726
  await this.requestClient({
694
727
  method: 'post',
695
728
  url: url,
696
- headers: {
697
- 'x-appbrand': this.brand,
698
- 'x-device-timezone': 'CEST',
699
- 'x-osname': 'iOS',
700
- guid: '091a3196-0d1d-4ba6-92c4-d44967cd6567',
701
- 'user-agent': 'Toyota/134 CFNetwork/1410.0.3 Darwin/22.6.0',
702
- 'x-guid': '091a3196-0d1d-4ba6-92c4-d44967cd6567',
703
- 'x-region': 'EU',
704
- region: 'EU',
705
- brand: this.brand,
706
- 'x-channel': 'ONEAPP',
707
- vin: 'JTMGBRFV90D151864',
708
- 'x-osversion': '16.7.2',
709
- 'x-locale': 'de-DE',
710
- 'x-brand': this.brand,
711
- authorization: 'Bearer ' + this.session.access_token,
712
- 'accept-language': 'de-DE,de;q=0.9',
713
- 'x-correlationid': 'D7F048C1-F0A1-4920-AA37-264C8A1FB4A3',
714
- 'x-appversion': '2.4.2',
715
- accept: '*/*',
716
- 'content-type': 'application/json',
717
- 'x-api-key': 'tTZipv6liF74PwMfk9Ed68AQ0bISswwf3iHQdqcF',
718
- 'x-user-region': 'DE',
719
- },
729
+ headers: this.buildApiHeaders(deviceId),
720
730
  data: data,
721
731
  })
722
732
  .then((res) => {
723
733
  this.log.debug(JSON.stringify(res.data));
734
+ if (path === 'climate-control' && res.data?.payload?.returnCode !== '000000') {
735
+ this.log.error('climate-control command rejected: ' + JSON.stringify(res.data));
736
+ }
724
737
  return res.data;
725
738
  })
726
739
  .catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.toyota",
3
- "version": "0.2.6",
3
+ "version": "0.4.0",
4
4
  "description": "Adapter for myT Toyota/Lexus App",
5
5
  "author": {
6
6
  "name": "TA2k",
@@ -23,21 +23,27 @@
23
23
  "url": "https://github.com/TA2k/ioBroker.toyota"
24
24
  },
25
25
  "dependencies": {
26
- "@iobroker/adapter-core": "^3.0.4",
27
- "axios": "^1.6.5",
28
- "http-cookie-agent": "^5.0.4",
29
- "json2iob": "^2.6.6",
30
- "jsonwebtoken": "^9.0.2",
31
- "qs": "^6.11.2",
32
- "tough-cookie": "^4.1.3"
26
+ "@iobroker/adapter-core": "^3.4.3",
27
+ "axios": "^1.20.0",
28
+ "http-cookie-agent": "^8.0.0",
29
+ "json2iob": "^2.6.28",
30
+ "jsonwebtoken": "^9.0.3",
31
+ "qs": "^6.16.0",
32
+ "tough-cookie": "^6.0.2",
33
+ "uuid": "^14.0.2"
33
34
  },
34
35
  "devDependencies": {
35
- "@iobroker/testing": "^4.1.0",
36
- "@types/node": "^20.11.4",
37
- "eslint": "^8.56.0",
38
- "typescript": "~5.3.3"
36
+ "@eslint/js": "^10.0.1",
37
+ "@iobroker/testing": "^5.3.0",
38
+ "@types/node": "^26.6.4",
39
+ "eslint": "^10.12.0",
40
+ "globals": "^17.13.0",
41
+ "typescript": "~6.0.3"
39
42
  },
40
43
  "main": "main.js",
44
+ "engines": {
45
+ "node": ">=22.13.0"
46
+ },
41
47
  "files": [
42
48
  "admin{,/!(src)/**}/!(tsconfig|tsconfig.*).json",
43
49
  "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",