n8n-nodes-jygse-vw-weconnect 0.1.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/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # n8n-nodes-vw-weconnect
2
+
3
+ This is an n8n community node for [VW We Connect](https://www.volkswagen.de/de/besitzer-und-nutzer/myvolkswagen.html).
4
+
5
+ Control your Volkswagen T6.1, Multivan, and other VW vehicles via We Connect. Perfect for Home Assistant integration!
6
+
7
+ ## Features
8
+
9
+ - **Get Position** - Get GPS coordinates of your vehicle (is the bus at home?)
10
+ - **Get Vehicle Status** - Doors, windows, fuel level, etc.
11
+ - **Get Heater Status** - Check Standheizung status
12
+ - **Start Heater** - Start the Standheizung (auxiliary heater)
13
+ - **Stop Heater** - Stop the Standheizung
14
+
15
+ ## Use Case Example
16
+
17
+ Automate your Standheizung with n8n + Home Assistant:
18
+ - Check if the vehicle is at home (position)
19
+ - Check outside temperature (Home Assistant)
20
+ - If cold and vehicle is home → Start Standheizung
21
+
22
+ ## Installation
23
+
24
+ ### In n8n
25
+
26
+ 1. Go to **Settings > Community Nodes**
27
+ 2. Select **Install**
28
+ 3. Enter `n8n-nodes-vw-weconnect`
29
+ 4. Agree to the risks and select **Install**
30
+
31
+ ### Manual Installation
32
+
33
+ ```bash
34
+ npm install n8n-nodes-vw-weconnect
35
+ ```
36
+
37
+ ## Credentials
38
+
39
+ You need your VW We Connect / Volkswagen ID credentials:
40
+
41
+ 1. **E-Mail** - Your Volkswagen ID email
42
+ 2. **Password** - Your Volkswagen ID password
43
+ 3. **S-PIN** - Your 4-digit S-PIN for remote control functions
44
+
45
+ You can find/set your S-PIN in the We Connect app under Settings.
46
+
47
+ ## Configuration
48
+
49
+ ### VIN (Vehicle Identification Number)
50
+
51
+ Your 17-character VIN can be found:
52
+ - In your vehicle documents (Fahrzeugschein)
53
+ - In the We Connect app
54
+ - On the vehicle (dashboard, door frame)
55
+
56
+ Example: `WVWZZZAUZJW123456`
57
+
58
+ ## Operations
59
+
60
+ | Operation | Description |
61
+ |-----------|-------------|
62
+ | Get Position | Returns GPS coordinates (latitude, longitude) |
63
+ | Get Vehicle Status | Returns door/window status, fuel level, mileage |
64
+ | Get Heater Status | Returns current Standheizung status |
65
+ | Start Heater | Starts Standheizung for specified duration |
66
+ | Stop Heater | Stops the Standheizung |
67
+
68
+ ## Example Workflow
69
+
70
+ ```
71
+ [Schedule Trigger: 6:00 AM]
72
+
73
+ [VW We Connect: Get Position]
74
+
75
+ [IF: Position is at home coordinates]
76
+
77
+ [Home Assistant: Get outside temperature]
78
+
79
+ [IF: Temperature < 5°C]
80
+
81
+ [VW We Connect: Start Heater (30 min)]
82
+ ```
83
+
84
+ ## Important Notes
85
+
86
+ - **API Limitations**: VW may block accounts with too many requests. Use reasonable intervals.
87
+ - **S-PIN Required**: Remote control functions (heater) require your S-PIN.
88
+ - **Terms & Conditions**: Make sure you've accepted the latest We Connect terms in the app.
89
+
90
+ ## Compatibility
91
+
92
+ Tested with:
93
+ - VW T6.1 Multivan
94
+ - Should work with other We Connect compatible vehicles
95
+
96
+ ## Author
97
+
98
+ Created by **jygse**
99
+
100
+ ## License
101
+
102
+ MIT
103
+
104
+ ## Links
105
+
106
+ - [VW We Connect](https://www.volkswagen.de/de/besitzer-und-nutzer/myvolkswagen.html)
107
+ - [n8n Community Nodes](https://docs.n8n.io/integrations/community-nodes/)
@@ -0,0 +1,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class VwWeConnectCredentials implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VwWeConnectCredentials = void 0;
4
+ class VwWeConnectCredentials {
5
+ constructor() {
6
+ this.name = 'vwWeConnectCredentials';
7
+ this.displayName = 'VW We Connect Credentials';
8
+ this.documentationUrl = 'https://www.volkswagen.de/de/besitzer-und-nutzer/myvolkswagen.html';
9
+ this.properties = [
10
+ {
11
+ displayName: 'E-Mail',
12
+ name: 'email',
13
+ type: 'string',
14
+ placeholder: 'name@example.com',
15
+ default: '',
16
+ required: true,
17
+ description: 'Your VW We Connect / Volkswagen ID email address',
18
+ },
19
+ {
20
+ displayName: 'Password',
21
+ name: 'password',
22
+ type: 'string',
23
+ typeOptions: {
24
+ password: true,
25
+ },
26
+ default: '',
27
+ required: true,
28
+ description: 'Your VW We Connect / Volkswagen ID password',
29
+ },
30
+ {
31
+ displayName: 'S-PIN',
32
+ name: 'spin',
33
+ type: 'string',
34
+ typeOptions: {
35
+ password: true,
36
+ },
37
+ default: '',
38
+ required: true,
39
+ description: 'Your 4-digit S-PIN for remote control functions (Standheizung, etc.)',
40
+ },
41
+ ];
42
+ }
43
+ }
44
+ exports.VwWeConnectCredentials = VwWeConnectCredentials;
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class VwWeConnect implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,526 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VwWeConnect = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ class VwWeConnect {
6
+ constructor() {
7
+ this.description = {
8
+ displayName: 'VW We Connect',
9
+ name: 'vwWeConnect',
10
+ icon: 'file:volkswagen.svg',
11
+ group: ['transform'],
12
+ version: 1,
13
+ subtitle: '={{$parameter["operation"]}}',
14
+ description: 'Control your Volkswagen via We Connect - Standheizung, Position, etc.',
15
+ defaults: {
16
+ name: 'VW We Connect',
17
+ },
18
+ inputs: ['main'],
19
+ outputs: ['main'],
20
+ credentials: [
21
+ {
22
+ name: 'vwWeConnectCredentials',
23
+ required: true,
24
+ },
25
+ ],
26
+ properties: [
27
+ {
28
+ displayName: 'Operation',
29
+ name: 'operation',
30
+ type: 'options',
31
+ noDataExpression: true,
32
+ options: [
33
+ {
34
+ name: 'Get Position',
35
+ value: 'getPosition',
36
+ description: 'Get the current GPS position of your vehicle',
37
+ action: 'Get the current GPS position of your vehicle',
38
+ },
39
+ {
40
+ name: 'Get Vehicle Status',
41
+ value: 'getVehicleStatus',
42
+ description: 'Get general vehicle status (doors, windows, fuel, etc.)',
43
+ action: 'Get general vehicle status',
44
+ },
45
+ {
46
+ name: 'Get Heater Status',
47
+ value: 'getHeaterStatus',
48
+ description: 'Get the current status of the Standheizung',
49
+ action: 'Get the current status of the Standheizung',
50
+ },
51
+ {
52
+ name: 'Start Heater',
53
+ value: 'startHeater',
54
+ description: 'Start the Standheizung (auxiliary heater)',
55
+ action: 'Start the Standheizung',
56
+ },
57
+ {
58
+ name: 'Stop Heater',
59
+ value: 'stopHeater',
60
+ description: 'Stop the Standheizung (auxiliary heater)',
61
+ action: 'Stop the Standheizung',
62
+ },
63
+ ],
64
+ default: 'getPosition',
65
+ },
66
+ {
67
+ displayName: 'VIN (Vehicle Identification Number)',
68
+ name: 'vin',
69
+ type: 'string',
70
+ default: '',
71
+ required: true,
72
+ description: 'Your vehicle identification number (17 characters, found in vehicle documents or We Connect app)',
73
+ placeholder: 'WVWZZZAUZJW123456',
74
+ },
75
+ {
76
+ displayName: 'Heater Duration (Minutes)',
77
+ name: 'heaterDuration',
78
+ type: 'number',
79
+ default: 30,
80
+ description: 'How long the heater should run (in minutes)',
81
+ displayOptions: {
82
+ show: {
83
+ operation: ['startHeater'],
84
+ },
85
+ },
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ async execute() {
91
+ const items = this.getInputData();
92
+ const returnData = [];
93
+ const credentials = await this.getCredentials('vwWeConnectCredentials');
94
+ const email = credentials.email;
95
+ const password = credentials.password;
96
+ const spin = credentials.spin;
97
+ for (let i = 0; i < items.length; i++) {
98
+ try {
99
+ const operation = this.getNodeParameter('operation', i);
100
+ const vin = this.getNodeParameter('vin', i).toUpperCase();
101
+ // Login and get session
102
+ const session = await vwLogin(this, email, password);
103
+ let result;
104
+ switch (operation) {
105
+ case 'getPosition':
106
+ result = await getPosition(this, session, vin);
107
+ break;
108
+ case 'getVehicleStatus':
109
+ result = await getVehicleStatus(this, session, vin);
110
+ break;
111
+ case 'getHeaterStatus':
112
+ result = await getHeaterStatus(this, session, vin);
113
+ break;
114
+ case 'startHeater':
115
+ const duration = this.getNodeParameter('heaterDuration', i);
116
+ result = await controlHeater(this, session, vin, spin, 'start', duration);
117
+ break;
118
+ case 'stopHeater':
119
+ result = await controlHeater(this, session, vin, spin, 'stop');
120
+ break;
121
+ default:
122
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Unknown operation: ${operation}` });
123
+ }
124
+ returnData.push({
125
+ json: result,
126
+ pairedItem: { item: i },
127
+ });
128
+ }
129
+ catch (error) {
130
+ if (this.continueOnFail()) {
131
+ returnData.push({
132
+ json: { error: error.message },
133
+ pairedItem: { item: i },
134
+ });
135
+ continue;
136
+ }
137
+ throw error;
138
+ }
139
+ }
140
+ return [returnData];
141
+ }
142
+ }
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';
148
+ async function vwLogin(context, email, password) {
149
+ try {
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({
161
+ method: 'GET',
162
+ url: `${authorizeUrl}?${authorizeParams.toString()}`,
163
+ headers: {
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',
165
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
166
+ },
167
+ returnFullResponse: true,
168
+ ignoreHttpStatusErrors: true,
169
+ });
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({
266
+ method: 'POST',
267
+ url: 'https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode',
268
+ headers: {
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',
270
+ 'Accept': 'application/json',
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',
278
+ },
279
+ });
280
+ return {
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: '',
286
+ };
287
+ }
288
+ catch (error) {
289
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), {
290
+ message: 'Login failed. Please check your credentials.',
291
+ description: error.message,
292
+ });
293
+ }
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
+ }
325
+ async function getPosition(context, session, vin) {
326
+ try {
327
+ // Try the new CARIAD API first
328
+ const response = await context.helpers.httpRequest({
329
+ method: 'GET',
330
+ url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/parkingposition`,
331
+ headers: {
332
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
333
+ 'Accept': 'application/json',
334
+ 'Authorization': `Bearer ${session.accessToken}`,
335
+ },
336
+ });
337
+ return {
338
+ operation: 'getPosition',
339
+ vin: vin,
340
+ position: response,
341
+ timestamp: new Date().toISOString(),
342
+ };
343
+ }
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
+ }
370
+ }
371
+ }
372
+ async function getVehicleStatus(context, session, vin) {
373
+ try {
374
+ // Try the new CARIAD API first
375
+ const response = await context.helpers.httpRequest({
376
+ method: 'GET',
377
+ url: `https://emea.bff.cariad.digital/vehicle/v1/vehicles/${vin}/selectivestatus?jobs=all`,
378
+ headers: {
379
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
380
+ 'Accept': 'application/json',
381
+ 'Authorization': `Bearer ${session.accessToken}`,
382
+ },
383
+ });
384
+ return {
385
+ operation: 'getVehicleStatus',
386
+ vin: vin,
387
+ status: response,
388
+ timestamp: new Date().toISOString(),
389
+ };
390
+ }
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
+ }
417
+ }
418
+ }
419
+ async function getHeaterStatus(context, session, vin) {
420
+ try {
421
+ const homeRegion = await getHomeRegion(context, session, vin);
422
+ const response = await context.helpers.httpRequest({
423
+ method: 'GET',
424
+ url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater`,
425
+ headers: {
426
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36',
427
+ 'Accept': 'application/json',
428
+ 'Authorization': `Bearer ${session.accessToken}`,
429
+ },
430
+ });
431
+ return {
432
+ operation: 'getHeaterStatus',
433
+ vin: vin,
434
+ heaterStatus: response,
435
+ timestamp: new Date().toISOString(),
436
+ };
437
+ }
438
+ catch (error) {
439
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), {
440
+ message: 'Failed to get heater status',
441
+ description: error.message,
442
+ });
443
+ }
444
+ }
445
+ async function controlHeater(context, session, vin, spin, action, duration) {
446
+ try {
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
+ },
487
+ };
488
+ if (action === 'start' && duration) {
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;
504
+ }
505
+ const response = await context.helpers.httpRequest({
506
+ method: 'POST',
507
+ url: `${homeRegion}/api/bs/climatisation/v1/VW/DE/vehicles/${vin}/climater/actions`,
508
+ headers: headers,
509
+ body: actionBody,
510
+ });
511
+ return {
512
+ operation: action === 'start' ? 'startHeater' : 'stopHeater',
513
+ vin: vin,
514
+ action: action,
515
+ duration: duration,
516
+ response: response,
517
+ timestamp: new Date().toISOString(),
518
+ };
519
+ }
520
+ catch (error) {
521
+ throw new n8n_workflow_1.NodeApiError(context.getNode(), {
522
+ message: `Failed to ${action} heater`,
523
+ description: error.message,
524
+ });
525
+ }
526
+ }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <circle cx="50" cy="50" r="48" fill="#001E50" stroke="#001E50" stroke-width="2"/>
3
+ <circle cx="50" cy="50" r="42" fill="none" stroke="#FFFFFF" stroke-width="3"/>
4
+ <path d="M30 30 L50 70 L70 30" fill="none" stroke="#FFFFFF" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M36 30 L50 58 L64 30" fill="none" stroke="#FFFFFF" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "n8n-nodes-jygse-vw-weconnect",
3
+ "version": "0.1.0",
4
+ "description": "n8n community node for VW We Connect - Control your Volkswagen T6.1 and other VW vehicles",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "volkswagen",
8
+ "vw",
9
+ "weconnect",
10
+ "we-connect",
11
+ "t6",
12
+ "standheizung",
13
+ "heater",
14
+ "position"
15
+ ],
16
+ "license": "MIT",
17
+ "homepage": "",
18
+ "author": {
19
+ "name": "jygse"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": ""
24
+ },
25
+ "main": "index.js",
26
+ "scripts": {
27
+ "build": "tsc && gulp build:icons",
28
+ "dev": "tsc --watch",
29
+ "format": "prettier nodes credentials --write",
30
+ "lint": "eslint nodes credentials package.json",
31
+ "lintfix": "eslint nodes credentials package.json --fix",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "n8n": {
38
+ "n8nNodesApiVersion": 1,
39
+ "credentials": [
40
+ "dist/credentials/VwWeConnectCredentials.credentials.js"
41
+ ],
42
+ "nodes": [
43
+ "dist/nodes/VwWeConnect/VwWeConnect.node.js"
44
+ ]
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.10.0",
48
+ "@typescript-eslint/parser": "^6.0.0",
49
+ "eslint": "^8.56.0",
50
+ "gulp": "^4.0.2",
51
+ "n8n-workflow": "^1.0.0",
52
+ "prettier": "^3.1.0",
53
+ "typescript": "^5.3.0"
54
+ },
55
+ "peerDependencies": {
56
+ "n8n-workflow": "*"
57
+ }
58
+ }