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 +9 -1
- package/lib/tools.js +3 -3
- package/main.js +217 -204
- package/package.json +18 -12
package/io-package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "toyota",
|
|
4
|
-
"version": "0.
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
'
|
|
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:
|
|
216
|
+
url: authorizeUrl,
|
|
246
217
|
headers: {
|
|
247
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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:
|
|
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
|
-
'
|
|
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
|
-
|
|
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: '
|
|
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/
|
|
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/
|
|
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:
|
|
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:
|
|
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
|
|
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/
|
|
685
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"axios": "^1.
|
|
28
|
-
"http-cookie-agent": "^
|
|
29
|
-
"json2iob": "^2.6.
|
|
30
|
-
"jsonwebtoken": "^9.0.
|
|
31
|
-
"qs": "^6.
|
|
32
|
-
"tough-cookie": "^
|
|
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
|
-
"@
|
|
36
|
-
"@
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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}",
|