n8n-nodes-vw-weconnect 0.1.0 → 0.1.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.
|
@@ -141,42 +141,148 @@ class VwWeConnect {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
exports.VwWeConnect = VwWeConnect;
|
|
144
|
+
// VW OAuth2 Configuration
|
|
145
|
+
const VW_CLIENT_ID = '9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com';
|
|
146
|
+
const VW_SCOPE = 'openid profile mbb';
|
|
147
|
+
const VW_REDIRECT_URI = 'weconnect://authenticated';
|
|
144
148
|
async function vwLogin(context, email, password) {
|
|
145
|
-
// VW We Connect uses a complex OAuth2 flow
|
|
146
|
-
// Step 1: Get initial login page and CSRF token
|
|
147
|
-
const baseUrl = 'https://www.portal.volkswagen-we.com';
|
|
148
|
-
const loginUrl = `${baseUrl}/portal/en_GB/web/guest/home`;
|
|
149
149
|
try {
|
|
150
|
-
//
|
|
151
|
-
|
|
150
|
+
// Step 1: Get authorization page and extract form data
|
|
151
|
+
const authorizeUrl = 'https://identity.vwgroup.io/oidc/v1/authorize';
|
|
152
|
+
const authorizeParams = new URLSearchParams({
|
|
153
|
+
client_id: VW_CLIENT_ID,
|
|
154
|
+
scope: VW_SCOPE,
|
|
155
|
+
response_type: 'code',
|
|
156
|
+
redirect_uri: VW_REDIRECT_URI,
|
|
157
|
+
nonce: generateNonce(),
|
|
158
|
+
state: generateNonce(),
|
|
159
|
+
});
|
|
160
|
+
const authorizeResponse = await context.helpers.httpRequest({
|
|
152
161
|
method: 'GET',
|
|
153
|
-
url:
|
|
162
|
+
url: `${authorizeUrl}?${authorizeParams.toString()}`,
|
|
154
163
|
headers: {
|
|
155
|
-
'User-Agent': 'Mozilla/5.0 (
|
|
164
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
|
156
165
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
157
166
|
},
|
|
167
|
+
returnFullResponse: true,
|
|
168
|
+
ignoreHttpStatusErrors: true,
|
|
158
169
|
});
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
// Extract CSRF token and relay state from the response
|
|
171
|
+
const htmlContent = authorizeResponse.body;
|
|
172
|
+
const csrfMatch = htmlContent.match(/name="_csrf"\s+value="([^"]+)"/);
|
|
173
|
+
const relayStateMatch = htmlContent.match(/name="relayState"\s+value="([^"]+)"/);
|
|
174
|
+
const hmacMatch = htmlContent.match(/name="hmac"\s+value="([^"]+)"/);
|
|
175
|
+
if (!csrfMatch) {
|
|
176
|
+
throw new Error('Could not extract CSRF token from login page');
|
|
177
|
+
}
|
|
178
|
+
const csrf = csrfMatch[1];
|
|
179
|
+
const relayState = relayStateMatch ? relayStateMatch[1] : '';
|
|
180
|
+
const hmac = hmacMatch ? hmacMatch[1] : '';
|
|
181
|
+
// Step 2: Submit email
|
|
182
|
+
const identifierResponse = await context.helpers.httpRequest({
|
|
183
|
+
method: 'POST',
|
|
184
|
+
url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/identifier',
|
|
185
|
+
headers: {
|
|
186
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
|
187
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
188
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
189
|
+
},
|
|
190
|
+
body: new URLSearchParams({
|
|
191
|
+
_csrf: csrf,
|
|
192
|
+
relayState: relayState,
|
|
193
|
+
hmac: hmac,
|
|
194
|
+
email: email,
|
|
195
|
+
}).toString(),
|
|
196
|
+
returnFullResponse: true,
|
|
197
|
+
ignoreHttpStatusErrors: true,
|
|
198
|
+
});
|
|
199
|
+
// Extract new CSRF for password submission
|
|
200
|
+
const identifierHtml = identifierResponse.body;
|
|
201
|
+
const csrf2Match = identifierHtml.match(/name="_csrf"\s+value="([^"]+)"/);
|
|
202
|
+
const relayState2Match = identifierHtml.match(/name="relayState"\s+value="([^"]+)"/);
|
|
203
|
+
const hmac2Match = identifierHtml.match(/name="hmac"\s+value="([^"]+)"/);
|
|
204
|
+
const csrf2 = csrf2Match ? csrf2Match[1] : csrf;
|
|
205
|
+
const relayState2 = relayState2Match ? relayState2Match[1] : relayState;
|
|
206
|
+
const hmac2 = hmac2Match ? hmac2Match[1] : hmac;
|
|
207
|
+
// Step 3: Submit password
|
|
208
|
+
const authResponse = await context.helpers.httpRequest({
|
|
209
|
+
method: 'POST',
|
|
210
|
+
url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/authenticate',
|
|
211
|
+
headers: {
|
|
212
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
|
213
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
214
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
215
|
+
},
|
|
216
|
+
body: new URLSearchParams({
|
|
217
|
+
_csrf: csrf2,
|
|
218
|
+
relayState: relayState2,
|
|
219
|
+
hmac: hmac2,
|
|
220
|
+
email: email,
|
|
221
|
+
password: password,
|
|
222
|
+
}).toString(),
|
|
223
|
+
returnFullResponse: true,
|
|
224
|
+
ignoreHttpStatusErrors: true,
|
|
225
|
+
});
|
|
226
|
+
// Get the redirect URL which contains the authorization code
|
|
227
|
+
const headers = authResponse.headers;
|
|
228
|
+
let redirectUrl = headers.location;
|
|
229
|
+
if (!redirectUrl) {
|
|
230
|
+
// Check if we got a redirect in the response
|
|
231
|
+
const authHtml = authResponse.body;
|
|
232
|
+
const redirectMatch = authHtml.match(/URL=([^"]+)"/i);
|
|
233
|
+
if (redirectMatch) {
|
|
234
|
+
redirectUrl = redirectMatch[1];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Follow redirects to get the auth code
|
|
238
|
+
let authCode = '';
|
|
239
|
+
let maxRedirects = 10;
|
|
240
|
+
while (maxRedirects > 0 && redirectUrl && !authCode) {
|
|
241
|
+
if (redirectUrl.includes('code=')) {
|
|
242
|
+
const codeMatch = redirectUrl.match(/code=([^&]+)/);
|
|
243
|
+
if (codeMatch) {
|
|
244
|
+
authCode = codeMatch[1];
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const followResponse = await context.helpers.httpRequest({
|
|
249
|
+
method: 'GET',
|
|
250
|
+
url: redirectUrl.startsWith('http') ? redirectUrl : `https://identity.vwgroup.io${redirectUrl}`,
|
|
251
|
+
headers: {
|
|
252
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
|
253
|
+
},
|
|
254
|
+
returnFullResponse: true,
|
|
255
|
+
ignoreHttpStatusErrors: true,
|
|
256
|
+
});
|
|
257
|
+
const followHeaders = followResponse.headers;
|
|
258
|
+
redirectUrl = followHeaders.location;
|
|
259
|
+
maxRedirects--;
|
|
260
|
+
}
|
|
261
|
+
if (!authCode) {
|
|
262
|
+
throw new Error('Could not obtain authorization code. Please check your credentials.');
|
|
263
|
+
}
|
|
264
|
+
// Step 4: Exchange auth code for tokens
|
|
265
|
+
const tokenResponse = await context.helpers.httpRequest({
|
|
165
266
|
method: 'POST',
|
|
166
|
-
url:
|
|
267
|
+
url: 'https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode',
|
|
167
268
|
headers: {
|
|
168
|
-
'User-Agent': 'Mozilla/5.0 (
|
|
269
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
|
169
270
|
'Accept': 'application/json',
|
|
170
271
|
'Content-Type': 'application/json',
|
|
272
|
+
'X-Client-Id': VW_CLIENT_ID,
|
|
273
|
+
},
|
|
274
|
+
body: {
|
|
275
|
+
auth_code: authCode,
|
|
276
|
+
id_token: '',
|
|
277
|
+
brand: 'vw',
|
|
171
278
|
},
|
|
172
|
-
body: loginPayload,
|
|
173
279
|
});
|
|
174
|
-
// For now, return a mock session - the actual implementation
|
|
175
|
-
// requires handling VW's complex OAuth2 flow with multiple redirects
|
|
176
280
|
return {
|
|
177
|
-
accessToken:
|
|
178
|
-
refreshToken:
|
|
179
|
-
|
|
281
|
+
accessToken: tokenResponse.access_token,
|
|
282
|
+
refreshToken: tokenResponse.refresh_token,
|
|
283
|
+
idToken: tokenResponse.id_token || '',
|
|
284
|
+
homeRegion: 'https://mal-1a.prd.ece.vwg-connect.com',
|
|
285
|
+
userId: '',
|
|
180
286
|
};
|
|
181
287
|
}
|
|
182
288
|
catch (error) {
|
|
@@ -186,17 +292,46 @@ async function vwLogin(context, email, password) {
|
|
|
186
292
|
});
|
|
187
293
|
}
|
|
188
294
|
}
|
|
295
|
+
function generateNonce() {
|
|
296
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
297
|
+
let result = '';
|
|
298
|
+
for (let i = 0; i < 32; i++) {
|
|
299
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
async function getHomeRegion(context, session, vin) {
|
|
304
|
+
try {
|
|
305
|
+
const response = await context.helpers.httpRequest({
|
|
306
|
+
method: 'GET',
|
|
307
|
+
url: `https://mal-1a.prd.ece.vwg-connect.com/api/cs/vds/v1/vehicles/${vin}/homeRegion`,
|
|
308
|
+
headers: {
|
|
309
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
310
|
+
'Accept': 'application/json',
|
|
311
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const homeRegion = response.homeRegion;
|
|
315
|
+
if (homeRegion && homeRegion.baseUri) {
|
|
316
|
+
const baseUri = homeRegion.baseUri;
|
|
317
|
+
return baseUri.content || session.homeRegion;
|
|
318
|
+
}
|
|
319
|
+
return session.homeRegion;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return session.homeRegion;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
189
325
|
async function getPosition(context, session, vin) {
|
|
190
|
-
const baseUrl = 'https://www.portal.volkswagen-we.com';
|
|
191
326
|
try {
|
|
327
|
+
// Try the new CARIAD API first
|
|
192
328
|
const response = await context.helpers.httpRequest({
|
|
193
329
|
method: 'GET',
|
|
194
|
-
url:
|
|
330
|
+
url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/parkingposition`,
|
|
195
331
|
headers: {
|
|
196
|
-
'User-Agent': 'Mozilla/5.0 (
|
|
332
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
197
333
|
'Accept': 'application/json',
|
|
198
334
|
'Authorization': `Bearer ${session.accessToken}`,
|
|
199
|
-
'X-CSRF-Token': session.csrf,
|
|
200
335
|
},
|
|
201
336
|
});
|
|
202
337
|
return {
|
|
@@ -206,24 +341,44 @@ async function getPosition(context, session, vin) {
|
|
|
206
341
|
timestamp: new Date().toISOString(),
|
|
207
342
|
};
|
|
208
343
|
}
|
|
209
|
-
catch
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
344
|
+
catch {
|
|
345
|
+
// Fallback to older API
|
|
346
|
+
try {
|
|
347
|
+
const homeRegion = await getHomeRegion(context, session, vin);
|
|
348
|
+
const response = await context.helpers.httpRequest({
|
|
349
|
+
method: 'GET',
|
|
350
|
+
url: `${homeRegion}/api/bs/cf/v1/VW/DE/vehicles/${vin}/position`,
|
|
351
|
+
headers: {
|
|
352
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
353
|
+
'Accept': 'application/json',
|
|
354
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
operation: 'getPosition',
|
|
359
|
+
vin: vin,
|
|
360
|
+
position: response,
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
throw new n8n_workflow_1.NodeApiError(context.getNode(), {
|
|
366
|
+
message: 'Failed to get vehicle position',
|
|
367
|
+
description: error.message,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
214
370
|
}
|
|
215
371
|
}
|
|
216
372
|
async function getVehicleStatus(context, session, vin) {
|
|
217
|
-
const baseUrl = 'https://www.portal.volkswagen-we.com';
|
|
218
373
|
try {
|
|
374
|
+
// Try the new CARIAD API first
|
|
219
375
|
const response = await context.helpers.httpRequest({
|
|
220
376
|
method: 'GET',
|
|
221
|
-
url:
|
|
377
|
+
url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/selectivestatus?jobs=all`,
|
|
222
378
|
headers: {
|
|
223
|
-
'User-Agent': 'Mozilla/5.0 (
|
|
379
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
224
380
|
'Accept': 'application/json',
|
|
225
381
|
'Authorization': `Bearer ${session.accessToken}`,
|
|
226
|
-
'X-CSRF-Token': session.csrf,
|
|
227
382
|
},
|
|
228
383
|
});
|
|
229
384
|
return {
|
|
@@ -233,24 +388,44 @@ async function getVehicleStatus(context, session, vin) {
|
|
|
233
388
|
timestamp: new Date().toISOString(),
|
|
234
389
|
};
|
|
235
390
|
}
|
|
236
|
-
catch
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
391
|
+
catch {
|
|
392
|
+
// Fallback to older API
|
|
393
|
+
try {
|
|
394
|
+
const homeRegion = await getHomeRegion(context, session, vin);
|
|
395
|
+
const response = await context.helpers.httpRequest({
|
|
396
|
+
method: 'GET',
|
|
397
|
+
url: `${homeRegion}/api/bs/vsr/v1/VW/DE/vehicles/${vin}/status`,
|
|
398
|
+
headers: {
|
|
399
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
400
|
+
'Accept': 'application/json',
|
|
401
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
operation: 'getVehicleStatus',
|
|
406
|
+
vin: vin,
|
|
407
|
+
status: response,
|
|
408
|
+
timestamp: new Date().toISOString(),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
throw new n8n_workflow_1.NodeApiError(context.getNode(), {
|
|
413
|
+
message: 'Failed to get vehicle status',
|
|
414
|
+
description: error.message,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
241
417
|
}
|
|
242
418
|
}
|
|
243
419
|
async function getHeaterStatus(context, session, vin) {
|
|
244
|
-
const baseUrl = 'https://www.portal.volkswagen-we.com';
|
|
245
420
|
try {
|
|
421
|
+
const homeRegion = await getHomeRegion(context, session, vin);
|
|
246
422
|
const response = await context.helpers.httpRequest({
|
|
247
423
|
method: 'GET',
|
|
248
|
-
url: `${
|
|
424
|
+
url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater`,
|
|
249
425
|
headers: {
|
|
250
|
-
'User-Agent': 'Mozilla/5.0 (
|
|
426
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
251
427
|
'Accept': 'application/json',
|
|
252
428
|
'Authorization': `Bearer ${session.accessToken}`,
|
|
253
|
-
'X-CSRF-Token': session.csrf,
|
|
254
429
|
},
|
|
255
430
|
});
|
|
256
431
|
return {
|
|
@@ -268,26 +443,70 @@ async function getHeaterStatus(context, session, vin) {
|
|
|
268
443
|
}
|
|
269
444
|
}
|
|
270
445
|
async function controlHeater(context, session, vin, spin, action, duration) {
|
|
271
|
-
const baseUrl = 'https://www.portal.volkswagen-we.com';
|
|
272
|
-
const endpoint = action === 'start' ? 'start' : 'stop';
|
|
273
446
|
try {
|
|
274
|
-
const
|
|
275
|
-
|
|
447
|
+
const homeRegion = await getHomeRegion(context, session, vin);
|
|
448
|
+
// First, get a security token for the action
|
|
449
|
+
const secTokenResponse = await context.helpers.httpRequest({
|
|
450
|
+
method: 'GET',
|
|
451
|
+
url: `${homeRegion}/api/rolesrights/authorization/v2/vehicles/${vin}/services/rclima_v1/operations/P_START_CLIMA_AU/security-pin-auth-requested`,
|
|
452
|
+
headers: {
|
|
453
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
454
|
+
'Accept': 'application/json',
|
|
455
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
const securityPinAuthInfo = secTokenResponse.securityPinAuthInfo;
|
|
459
|
+
let secToken = '';
|
|
460
|
+
if (securityPinAuthInfo && securityPinAuthInfo.securityToken) {
|
|
461
|
+
// Complete S-PIN authentication
|
|
462
|
+
const spinAuthResponse = await context.helpers.httpRequest({
|
|
463
|
+
method: 'POST',
|
|
464
|
+
url: `${homeRegion}/api/rolesrights/authorization/v2/security-pin-auth-completed`,
|
|
465
|
+
headers: {
|
|
466
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
467
|
+
'Accept': 'application/json',
|
|
468
|
+
'Content-Type': 'application/json',
|
|
469
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
470
|
+
},
|
|
471
|
+
body: {
|
|
472
|
+
securityPinAuthentication: {
|
|
473
|
+
securityPin: spin,
|
|
474
|
+
securityToken: securityPinAuthInfo.securityToken,
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
const authInfo = spinAuthResponse.securityPinAuthInfo;
|
|
479
|
+
secToken = (authInfo === null || authInfo === void 0 ? void 0 : authInfo.securityToken) || '';
|
|
480
|
+
}
|
|
481
|
+
// Build the action request
|
|
482
|
+
const actionName = action === 'start' ? 'startClimatisation' : 'stopClimatisation';
|
|
483
|
+
const actionBody = {
|
|
484
|
+
action: {
|
|
485
|
+
type: actionName,
|
|
486
|
+
},
|
|
276
487
|
};
|
|
277
488
|
if (action === 'start' && duration) {
|
|
278
|
-
|
|
489
|
+
actionBody.action.settings = {
|
|
490
|
+
targetTemperature: 2930, // ~20°C in deciKelvin
|
|
491
|
+
climatisationWithoutHVpower: true,
|
|
492
|
+
heaterSource: 'auxiliary',
|
|
493
|
+
runTime: duration,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const headers = {
|
|
497
|
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
|
|
498
|
+
'Accept': 'application/json',
|
|
499
|
+
'Content-Type': 'application/json',
|
|
500
|
+
'Authorization': `Bearer ${session.accessToken}`,
|
|
501
|
+
};
|
|
502
|
+
if (secToken) {
|
|
503
|
+
headers['x-mbbSecToken'] = secToken;
|
|
279
504
|
}
|
|
280
505
|
const response = await context.helpers.httpRequest({
|
|
281
506
|
method: 'POST',
|
|
282
|
-
url: `${
|
|
283
|
-
headers:
|
|
284
|
-
|
|
285
|
-
'Accept': 'application/json',
|
|
286
|
-
'Content-Type': 'application/json',
|
|
287
|
-
'Authorization': `Bearer ${session.accessToken}`,
|
|
288
|
-
'X-CSRF-Token': session.csrf,
|
|
289
|
-
},
|
|
290
|
-
body: body,
|
|
507
|
+
url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater/actions`,
|
|
508
|
+
headers: headers,
|
|
509
|
+
body: actionBody,
|
|
291
510
|
});
|
|
292
511
|
return {
|
|
293
512
|
operation: action === 'start' ? 'startHeater' : 'stopHeater',
|
package/package.json
CHANGED