iobroker.renault 0.0.23 → 0.0.24

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 CHANGED
@@ -9,17 +9,18 @@
9
9
 
10
10
  **Tests:** ![Test and Release](https://github.com/TA2k/ioBroker.renault/workflows/Test%20and%20Release/badge.svg)
11
11
 
12
- ## Renault / Dacia adapter for ioBroker
12
+ ## Renault / Dacia / Alpine adapter for ioBroker
13
13
 
14
- This adapter connects ioBroker to the My Renault / My Dacia cloud and exposes vehicle status data (battery, charging, HVAC, mileage, ...) as well as remote commands (HVAC start, charging start/stop, force refresh) for compatible Renault and Dacia models such as the Renault Zoe, Megane E-Tech, Kangoo E-Tech and the Dacia Spring.
14
+ This adapter connects ioBroker to the My Renault / My Dacia / My Alpine cloud and exposes vehicle status data (battery, charging, HVAC, mileage, ...) as well as remote commands (HVAC start, charging start/stop, force refresh) for compatible Renault, Dacia and Alpine models such as the Renault Zoe, Megane E-Tech, Kangoo E-Tech, the Dacia Spring and the Alpine A290.
15
15
 
16
16
  ## Installation / Login
17
17
 
18
18
  1. Install the adapter via the ioBroker admin UI.
19
- 2. Open the adapter configuration and enter the credentials of your **My Renault** (or **My Dacia**) account: app email and app password.
20
- 3. Set the **country** to the two-letter country code matching your account (e.g. `de`, `fr`, `it`, `es`).
21
- 4. Optionally set the polling **interval** in minutes and the **API key** (leave empty for auto-detect).
22
- 5. Save and the instance will start polling.
19
+ 2. Open the adapter configuration and enter the credentials of your **My Renault** (or **My Dacia** / **My Alpine**) account: app email and app password.
20
+ 3. Select the **brand** matching your app: `Renault / Dacia` or `Alpine` (they use separate accounts and API keys).
21
+ 4. Set the **country** to the two-letter country code matching your account (e.g. `de`, `fr`, `it`, `es`).
22
+ 5. Optionally set the polling **interval** in minutes and the **API key** (leave empty for auto-detect).
23
+ 6. Save and the instance will start polling.
23
24
 
24
25
  ## Remote control
25
26
 
@@ -47,6 +48,10 @@ ioBroker forum: <https://forum.iobroker.net/topic/48074/test-adapter-renault-v0-
47
48
  ### **WORK IN PROGRESS**
48
49
  -->
49
50
 
51
+ ### 0.0.24
52
+
53
+ - (TA2k) add Alpine support: brand selection (Alpine accounts use the same Renault Gigya/Kamereon tenant, only product/account type MYALPINE differs)
54
+
50
55
  ### 0.0.23
51
56
 
52
57
  - (TA2k) align API headers with My Renault Android app, drop EOL Node 18/20, migrate admin UI to jsonConfig
@@ -3,6 +3,8 @@
3
3
  "App Email": "App E-Mail",
4
4
  "App Password": "App Passwort",
5
5
  "Country": "Land",
6
+ "Brand": "Marke",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia und Alpine nutzen unterschiedliche Konten und API-Schlüssel",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Zweistelliger Ländercode, z.B. de, fr, it, es",
7
9
  "Polling": "Abfrage",
8
10
  "Update interval in minutes": "Aktualisierungsintervall in Minuten",
@@ -3,6 +3,8 @@
3
3
  "App Email": "App Email",
4
4
  "App Password": "App Password",
5
5
  "Country": "Country",
6
+ "Brand": "Brand",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia and Alpine use different accounts and API keys",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Two-letter country code, e.g. de, fr, it, es",
7
9
  "Polling": "Polling",
8
10
  "Update interval in minutes": "Update interval in minutes",
@@ -28,6 +28,22 @@
28
28
  "lg": 4,
29
29
  "xl": 4
30
30
  },
31
+ "brand": {
32
+ "type": "select",
33
+ "label": "Brand",
34
+ "help": "Renault/Dacia and Alpine use different accounts and API keys",
35
+ "default": "renault",
36
+ "options": [
37
+ { "label": "Renault / Dacia", "value": "renault" },
38
+ { "label": "Alpine", "value": "alpine" }
39
+ ],
40
+ "newLine": true,
41
+ "xs": 12,
42
+ "sm": 12,
43
+ "md": 4,
44
+ "lg": 2,
45
+ "xl": 2
46
+ },
31
47
  "country": {
32
48
  "type": "text",
33
49
  "label": "Country",
@@ -67,6 +83,21 @@
67
83
  "lg": 4,
68
84
  "xl": 4
69
85
  },
86
+ "chargeHistoryLimit": {
87
+ "type": "number",
88
+ "label": "Charge history entries to keep",
89
+ "help": "Number of most recent entries kept per charge-history endpoint (0 = unlimited)",
90
+ "min": 0,
91
+ "max": 1000,
92
+ "default": 100,
93
+ "hidden": "data.disableChargeFetching === true",
94
+ "newLine": true,
95
+ "xs": 12,
96
+ "sm": 12,
97
+ "md": 4,
98
+ "lg": 2,
99
+ "xl": 2
100
+ },
70
101
  "_apiHeader": {
71
102
  "type": "header",
72
103
  "text": "API Key",
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "renault",
4
- "version": "0.0.23",
4
+ "version": "0.0.24",
5
5
  "news": {
6
+ "0.0.24": {
7
+ "en": "add Alpine support (select brand Alpine in the settings)",
8
+ "de": "Alpine-Unterstützung hinzugefügt (Marke Alpine in den Einstellungen wählen)",
9
+ "ru": "Добавлена поддержка Alpine (выберите марку Alpine в настройках)",
10
+ "pt": "Suporte para Alpine adicionado (selecione a marca Alpine nas definições)",
11
+ "nl": "Alpine-ondersteuning toegevoegd (selecteer merk Alpine in de instellingen)",
12
+ "fr": "Prise en charge d'Alpine ajoutée (sélectionnez la marque Alpine dans les paramètres)",
13
+ "it": "Aggiunto supporto Alpine (seleziona il marchio Alpine nelle impostazioni)",
14
+ "es": "Añadido soporte para Alpine (seleccione la marca Alpine en los ajustes)",
15
+ "pl": "Dodano obsługę Alpine (wybierz markę Alpine w ustawieniach)",
16
+ "uk": "Додано підтримку Alpine (виберіть марку Alpine в налаштуваннях)",
17
+ "zh-cn": "新增 Alpine 支持(在设置中选择品牌 Alpine)"
18
+ },
6
19
  "0.0.23": {
7
20
  "en": "align API headers with My Renault Android app (User-Agent okhttp/5.3.0, X-Amzn-Trace-Id)",
8
21
  "de": "API-Header an My-Renault-Android-App angeglichen (User-Agent okhttp/5.3.0, X-Amzn-Trace-Id)",
@@ -163,10 +176,12 @@
163
176
  "native": {
164
177
  "username": "",
165
178
  "country": "de",
179
+ "brand": "renault",
166
180
  "password": "",
167
181
  "apiKeyUpdate": "",
168
182
  "interval": 10,
169
- "disableChargeFetching": false
183
+ "disableChargeFetching": false,
184
+ "chargeHistoryLimit": 100
170
185
  },
171
186
  "objects": [],
172
187
  "instanceObjects": [
@@ -190,6 +205,19 @@
190
205
  "def": false
191
206
  },
192
207
  "native": {}
208
+ },
209
+ {
210
+ "_id": "info.migrationV1",
211
+ "type": "state",
212
+ "common": {
213
+ "role": "indicator",
214
+ "name": "Migration V1 (charge history forceIndex) executed",
215
+ "type": "boolean",
216
+ "read": true,
217
+ "write": false,
218
+ "def": false
219
+ },
220
+ "native": {}
193
221
  }
194
222
  ]
195
223
  }
package/main.js CHANGED
@@ -38,7 +38,9 @@ class Renault extends utils.Adapter {
38
38
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
39
39
  return v.toString(16);
40
40
  });
41
- return 'build=renault-android-6.11.2;trId=' + uuid;
41
+ const brand = this.brand || 'renault';
42
+ const version = brand === 'alpine' ? '6.10.1' : '6.11.2';
43
+ return 'build=' + brand + '-android-' + version + ';trId=' + uuid;
42
44
  }
43
45
 
44
46
  /**
@@ -55,10 +57,18 @@ class Renault extends utils.Adapter {
55
57
  this.reLoginTimeout = null;
56
58
  this.refreshTokenTimeout = null;
57
59
  this.country = this.config.country || 'de';
60
+ this.brand = this.config.brand || 'renault';
58
61
  this.session = {};
59
- //DE API Key
62
+ //DE API Key (shared by Renault, Dacia and Alpine - same Gigya/Kamereon tenant)
60
63
  this.apiKey = '3_VgdkgtIRH3AdHvJm-cjV2ug2EFE0lxt0IJzMC4MFqZjFpn_GYFXVdNZ19L7wZX0N';
61
64
  this.apiKeyUpdate = 'YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J';
65
+ if (this.brand === 'alpine') {
66
+ this.product = 'MYALPINE';
67
+ this.accountTypes = ['MYALPINE'];
68
+ } else {
69
+ this.product = 'MYRENAULT';
70
+ this.accountTypes = ['MYRENAULT', 'MYDACIA'];
71
+ }
62
72
  try {
63
73
  await this.requestClient({
64
74
  method: 'get',
@@ -88,6 +98,7 @@ class Renault extends utils.Adapter {
88
98
 
89
99
  if (this.session.id_token && this.session_data && this.account) {
90
100
  await this.getDeviceList();
101
+ await this.migrateChargeHistoryV1();
91
102
  await this.updateDevices();
92
103
  this.updateInterval = setInterval(
93
104
  async () => {
@@ -169,7 +180,11 @@ class Renault extends utils.Adapter {
169
180
  });
170
181
  await this.requestClient({
171
182
  method: 'post',
172
- url: 'https://apis.renault.com/myr/api/v1/connection?&country=DE&product=MYRENAULT&locale=de-DE&displayAccounts=MYRENAULT',
183
+ url:
184
+ 'https://apis.renault.com/myr/api/v1/connection?&country=DE&product=' +
185
+ this.product +
186
+ '&locale=de-DE&displayAccounts=' +
187
+ this.product,
173
188
  headers: {
174
189
  'Content-Type': 'application/json',
175
190
  Accept: '*/*',
@@ -182,8 +197,9 @@ class Renault extends utils.Adapter {
182
197
  .then((res) => {
183
198
  this.log.debug(JSON.stringify(res.data));
184
199
 
200
+ const accountTypes = this.accountTypes;
185
201
  const filteredAccounts = res.data.currentUser.accounts.filter(function (el) {
186
- return (el.accountType === 'MYRENAULT' || el.accountType === 'MYDACIA') && el.accountStatus === 'ACTIVE';
202
+ return accountTypes.includes(el.accountType) && el.accountStatus === 'ACTIVE';
187
203
  });
188
204
  if (filteredAccounts.length === 0) {
189
205
  this.log.error('No Account found');
@@ -259,7 +275,12 @@ class Renault extends utils.Adapter {
259
275
 
260
276
  const remoteArray = [
261
277
  { command: 'actions/hvac-start', name: 'True = Start, False = Stop' },
262
- { command: 'hvac-temperature', name: 'HVAC Temperature', type: 'number', role: 'value' },
278
+ {
279
+ command: 'hvac-temperature',
280
+ name: 'HVAC Temperature',
281
+ type: /** @type {ioBroker.CommonType} */ ('number'),
282
+ role: 'value',
283
+ },
263
284
  { command: 'actions/charging-start', name: 'True = Start, False = Stop' },
264
285
  { command: 'charge/pause-resume', name: 'True = Start, False = Stop' },
265
286
  { command: 'charge/start', name: 'True = Start, False = Stop' },
@@ -270,7 +291,7 @@ class Renault extends utils.Adapter {
270
291
  type: 'state',
271
292
  common: {
272
293
  name: remote.name || '',
273
- type: remote.type || 'boolean',
294
+ type: /** @type {ioBroker.CommonType} */ (remote.type || 'boolean'),
274
295
  role: remote.role || 'button',
275
296
  write: true,
276
297
  read: true,
@@ -289,12 +310,40 @@ class Renault extends utils.Adapter {
289
310
  });
290
311
  }
291
312
 
313
+ /**
314
+ * One-shot migration: with v0.0.24 the charge-history and charges endpoints switched from
315
+ * date-named channels (e.g. `20220802`, `2022-04-22T11:46:38Z`) to numeric forceIndex slots.
316
+ * Without cleanup the old objects would linger forever next to the new ones. The marker state
317
+ * `info.migrationV1` ensures this only runs once per instance.
318
+ */
319
+ async migrateChargeHistoryV1() {
320
+ try {
321
+ const marker = await this.getStateAsync('info.migrationV1');
322
+ if (marker && marker.val === true) {
323
+ return;
324
+ }
325
+ this.log.info('Running charge history migration V1: cleaning up legacy date-named channels');
326
+ for (const vin of this.deviceArray) {
327
+ for (const path of ['charge-history', 'charges']) {
328
+ await this.delObjectAsync(vin + '.' + path, { recursive: true }).catch(() => {});
329
+ }
330
+ }
331
+ await this.setState('info.migrationV1', { val: true, ack: true });
332
+ this.log.info('Charge history migration V1 done');
333
+ } catch (e) {
334
+ this.log.warn('Charge history migration V1 failed: ' + e);
335
+ }
336
+ }
337
+
292
338
  async updateDevices() {
293
339
  if (!this.account.accountId) {
294
340
  this.log.error('No accountId found');
295
341
  return;
296
342
  }
297
343
  const curDate = new Date().toISOString().split('T')[0];
344
+ // Charge history: limit start date to ~1 year back (My Renault app paginates yearly).
345
+ // Keeping the range bounded prevents the API from returning years of data on every poll.
346
+ const historyStart = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
298
347
 
299
348
  const statusArray = [
300
349
  {
@@ -405,22 +454,28 @@ class Renault extends utils.Adapter {
405
454
  url:
406
455
  'https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/' +
407
456
  this.account.accountId +
408
- '/kamereon/kca/car-adapter/v1/cars/$vin/charge-history?type=day&start=1970-01-01&end=' +
457
+ '/kamereon/kca/car-adapter/v1/cars/$vin/charge-history?type=day&start=' +
458
+ historyStart +
459
+ '&end=' +
409
460
  curDate +
410
461
  '&country=' +
411
462
  this.country,
412
463
  desc: 'Charging history of the car',
464
+ isHistory: true,
413
465
  });
414
466
  statusArray.push({
415
467
  path: 'charges',
416
468
  url:
417
469
  'https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/' +
418
470
  this.account.accountId +
419
- '/kamereon/kca/car-adapter/v1/cars/$vin/charges?start=1970-01-01&end=' +
471
+ '/kamereon/kca/car-adapter/v1/cars/$vin/charges?start=' +
472
+ historyStart +
473
+ '&end=' +
420
474
  curDate +
421
475
  '&country=' +
422
476
  this.country,
423
477
  desc: 'Charges of the car',
478
+ isHistory: true,
424
479
  });
425
480
  }
426
481
 
@@ -444,7 +499,7 @@ class Renault extends utils.Adapter {
444
499
  url: url,
445
500
  headers: { ...headers, 'X-Amzn-Trace-Id': this.buildTraceId() },
446
501
  })
447
- .then((res) => {
502
+ .then(async (res) => {
448
503
  this.log.debug(JSON.stringify(res.data));
449
504
  if (!res.data) {
450
505
  return;
@@ -454,9 +509,24 @@ class Renault extends utils.Adapter {
454
509
  data = res.data.data.attributes;
455
510
  }
456
511
 
457
- const forceIndex = undefined;
512
+ let forceIndex = undefined;
458
513
  const preferedArrayName = undefined;
459
514
 
515
+ // Charge history endpoints return arrays keyed by date — without forceIndex
516
+ // json2iob creates a new channel per date and never cleans them up. Cap to the
517
+ // configured limit (default 100, matches the My Renault app pagination) and use
518
+ // numeric indices so old entries are overwritten on each poll. The one-shot
519
+ // migrateChargeHistoryV1() removes the legacy date-named channels on first start.
520
+ if (element.isHistory) {
521
+ forceIndex = true;
522
+ const arrayKey = element.path === 'charge-history' ? 'chargeSummaries' : 'charges';
523
+ const limit = Number(this.config.chargeHistoryLimit);
524
+ const cap = Number.isFinite(limit) && limit > 0 ? limit : 0;
525
+ if (cap > 0 && data && Array.isArray(data[arrayKey]) && data[arrayKey].length > cap) {
526
+ data = { ...data, [arrayKey]: data[arrayKey].slice(-cap) };
527
+ }
528
+ }
529
+
460
530
  this.json2iob.parse(vin + '.' + element.path, data, {
461
531
  forceIndex: forceIndex,
462
532
  preferedArrayName: preferedArrayName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.renault",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Adapter for Renault and Renault",
5
5
  "author": {
6
6
  "name": "TA2k",
@@ -25,27 +25,28 @@
25
25
  "url": "https://github.com/TA2k/ioBroker.renault"
26
26
  },
27
27
  "dependencies": {
28
- "@iobroker/adapter-core": "^3.3.2",
29
- "axios": "^1.16.1",
28
+ "@iobroker/adapter-core": "^3.4.3",
29
+ "axios": "^1.20.0",
30
30
  "json-bigint": "^1.0.0",
31
- "json2iob": "^2.6.22",
32
- "qs": "^6.15.2"
31
+ "json2iob": "^2.6.26",
32
+ "qs": "^6.16.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@alcalzone/release-script": "^5.2.0",
35
+ "@alcalzone/release-script": "^5.2.1",
36
36
  "@alcalzone/release-script-plugin-iobroker": "^5.2.0",
37
- "@alcalzone/release-script-plugin-license": "^5.2.0",
37
+ "@alcalzone/release-script-plugin-license": "^5.2.2",
38
38
  "@alcalzone/release-script-plugin-manual-review": "^5.2.0",
39
- "@eslint/eslintrc": "^3.3.5",
39
+ "@eslint/eslintrc": "^3.3.7",
40
40
  "@eslint/js": "^10.0.1",
41
- "@iobroker/testing": "^5.2.2",
42
- "@tsconfig/node18": "^18.2.6",
43
- "@types/node": "^25.9.1",
44
- "eslint": "^10.4.0",
41
+ "@iobroker/testing": "^5.3.0",
42
+ "@tsconfig/node18": "^18.2.7",
43
+ "@types/mocha": "^10.0.10",
44
+ "@types/node": "^25.9.6",
45
+ "eslint": "^10.10.0",
45
46
  "eslint-config-prettier": "^10.1.8",
46
- "eslint-plugin-prettier": "^5.5.5",
47
- "globals": "^17.6.0",
48
- "prettier": "^3.8.3",
47
+ "eslint-plugin-prettier": "^5.5.6",
48
+ "globals": "^17.12.0",
49
+ "prettier": "^3.9.6",
49
50
  "typescript": "~6.0.3"
50
51
  },
51
52
  "main": "main.js",