iobroker.renault 0.0.23 → 0.0.25

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,20 @@
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
+ **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0.
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
+ ## Renault / Dacia / Alpine adapter for ioBroker
15
+
16
+ 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
17
 
16
18
  ## Installation / Login
17
19
 
18
20
  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.
21
+ 2. Open the adapter configuration and enter the credentials of your **My Renault** (or **My Dacia** / **My Alpine**) account: app email and app password.
22
+ 3. Select the **brand** matching your app: `Renault / Dacia` or `Alpine` (they use separate accounts and API keys).
23
+ 4. Set the **country** to the two-letter country code matching your account (e.g. `de`, `fr`, `it`, `es`).
24
+ 5. Optionally set the polling **interval** in minutes and the **API key** (leave empty for auto-detect).
25
+ 6. Save and the instance will start polling.
23
26
 
24
27
  ## Remote control
25
28
 
@@ -47,6 +50,20 @@ ioBroker forum: <https://forum.iobroker.net/topic/48074/test-adapter-renault-v0-
47
50
  ### **WORK IN PROGRESS**
48
51
  -->
49
52
 
53
+ ### 0.0.25
54
+
55
+ - (typhosj) retry the connection with growing delay (5 to 60 minutes) when login or vehicle list fail at startup; a login rejected by the account service is not retried
56
+ - (typhosj) `info.connection` is true only after the account was found and turns false when the token refresh fails
57
+ - (typhosj) a temporary server error (5xx) on the first poll no longer disables that endpoint until restart
58
+ - (typhosj) fix crash for vehicles without vehicle details and for the refresh button before login
59
+ - (typhosj) no longer write password, session cookie, id token or account data into the log
60
+ - (typhosj) add missing admin translations, remove unused dependencies
61
+ - (typhosj) require Node.js 22 or newer, test with Node.js 26, update dependencies
62
+
63
+ ### 0.0.24
64
+
65
+ - (TA2k) add Alpine support: brand selection (Alpine accounts use the same Renault Gigya/Kamereon tenant, only product/account type MYALPINE differs)
66
+
50
67
  ### 0.0.23
51
68
 
52
69
  - (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",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API-Schlüssel",
11
13
  "API Key (leave empty for auto-detect)": "API-Schlüssel (leer lassen für automatische Erkennung)",
12
14
  "If empty, the API key will be set automatically": "Wenn leer, wird der API-Schlüssel automatisch gesetzt",
13
- "Click here for current API Key": "Hier klicken für aktuellen API-Schlüssel"
15
+ "Click here for current API Key": "Hier klicken für aktuellen API-Schlüssel",
16
+ "Charge history entries to keep": "Aufzubewahrende Einträge der Ladehistorie",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Anzahl der neuesten Einträge, die je Ladehistorie-Endpunkt behalten werden (0 = unbegrenzt)"
14
18
  }
@@ -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",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API Key",
11
13
  "API Key (leave empty for auto-detect)": "API Key (leave empty for auto-detect)",
12
14
  "If empty, the API key will be set automatically": "If empty, the API key will be set automatically",
13
- "Click here for current API Key": "Click here for current API Key"
15
+ "Click here for current API Key": "Click here for current API Key",
16
+ "Charge history entries to keep": "Charge history entries to keep",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Number of most recent entries kept per charge-history endpoint (0 = unlimited)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Correo electrónico de la app",
4
4
  "App Password": "Contraseña de la app",
5
5
  "Country": "País",
6
+ "Brand": "Marca",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia y Alpine usan cuentas y claves API diferentes",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Código de país de dos letras, p. ej. de, fr, it, es",
7
9
  "Polling": "Sondeo",
8
10
  "Update interval in minutes": "Intervalo de actualización en minutos",
@@ -10,5 +12,7 @@
10
12
  "API Key": "Clave API",
11
13
  "API Key (leave empty for auto-detect)": "Clave API (déjelo vacío para detección automática)",
12
14
  "If empty, the API key will be set automatically": "Si está vacío, la clave API se establecerá automáticamente",
13
- "Click here for current API Key": "Haga clic aquí para la clave API actual"
15
+ "Click here for current API Key": "Haga clic aquí para la clave API actual",
16
+ "Charge history entries to keep": "Entradas del historial de carga a conservar",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Número de entradas más recientes que se conservan por endpoint del historial de carga (0 = ilimitado)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Email de l'application",
4
4
  "App Password": "Mot de passe de l'application",
5
5
  "Country": "Pays",
6
+ "Brand": "Marque",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia et Alpine utilisent des comptes et des clés API différents",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Code pays à deux lettres, par exemple de, fr, it, es",
7
9
  "Polling": "Interrogation",
8
10
  "Update interval in minutes": "Intervalle de mise à jour en minutes",
@@ -10,5 +12,7 @@
10
12
  "API Key": "Clé API",
11
13
  "API Key (leave empty for auto-detect)": "Clé API (laisser vide pour détection automatique)",
12
14
  "If empty, the API key will be set automatically": "Si vide, la clé API sera définie automatiquement",
13
- "Click here for current API Key": "Cliquez ici pour la clé API actuelle"
15
+ "Click here for current API Key": "Cliquez ici pour la clé API actuelle",
16
+ "Charge history entries to keep": "Entrées de l'historique de charge à conserver",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Nombre d'entrées les plus récentes conservées par point d'accès de l'historique de charge (0 = illimité)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Email dell'app",
4
4
  "App Password": "Password dell'app",
5
5
  "Country": "Paese",
6
+ "Brand": "Marchio",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia e Alpine usano account e chiavi API diversi",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Codice paese di due lettere, ad esempio de, fr, it, es",
7
9
  "Polling": "Polling",
8
10
  "Update interval in minutes": "Intervallo di aggiornamento in minuti",
@@ -10,5 +12,7 @@
10
12
  "API Key": "Chiave API",
11
13
  "API Key (leave empty for auto-detect)": "Chiave API (lascia vuoto per il rilevamento automatico)",
12
14
  "If empty, the API key will be set automatically": "Se vuoto, la chiave API verrà impostata automaticamente",
13
- "Click here for current API Key": "Fai clic qui per la chiave API attuale"
15
+ "Click here for current API Key": "Fai clic qui per la chiave API attuale",
16
+ "Charge history entries to keep": "Voci della cronologia di ricarica da conservare",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Numero di voci più recenti conservate per ogni endpoint della cronologia di ricarica (0 = illimitato)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "App E-mail",
4
4
  "App Password": "App Wachtwoord",
5
5
  "Country": "Land",
6
+ "Brand": "Merk",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia en Alpine gebruiken verschillende accounts en API-sleutels",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Tweeletterige landcode, bijv. de, fr, it, es",
7
9
  "Polling": "Polling",
8
10
  "Update interval in minutes": "Update interval in minuten",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API-sleutel",
11
13
  "API Key (leave empty for auto-detect)": "API-sleutel (laat leeg voor auto-detectie)",
12
14
  "If empty, the API key will be set automatically": "Indien leeg, wordt de API-sleutel automatisch ingesteld",
13
- "Click here for current API Key": "Klik hier voor de huidige API-sleutel"
15
+ "Click here for current API Key": "Klik hier voor de huidige API-sleutel",
16
+ "Charge history entries to keep": "Te bewaren items van de laadgeschiedenis",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Aantal meest recente items dat per laadgeschiedenis-endpoint wordt bewaard (0 = onbeperkt)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Email aplikacji",
4
4
  "App Password": "Hasło aplikacji",
5
5
  "Country": "Kraj",
6
+ "Brand": "Marka",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia i Alpine używają różnych kont i kluczy API",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Dwuliterowy kod kraju, np. de, fr, it, es",
7
9
  "Polling": "Odpytywanie",
8
10
  "Update interval in minutes": "Interwał aktualizacji w minutach",
@@ -10,5 +12,7 @@
10
12
  "API Key": "Klucz API",
11
13
  "API Key (leave empty for auto-detect)": "Klucz API (pozostaw puste, aby wykryć automatycznie)",
12
14
  "If empty, the API key will be set automatically": "Jeśli puste, klucz API zostanie ustawiony automatycznie",
13
- "Click here for current API Key": "Kliknij tutaj, aby uzyskać aktualny klucz API"
15
+ "Click here for current API Key": "Kliknij tutaj, aby uzyskać aktualny klucz API",
16
+ "Charge history entries to keep": "Liczba zachowywanych wpisów historii ładowania",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Liczba najnowszych wpisów zachowywanych dla każdego endpointu historii ładowania (0 = bez limitu)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Email do aplicativo",
4
4
  "App Password": "Senha do aplicativo",
5
5
  "Country": "País",
6
+ "Brand": "Marca",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia e Alpine utilizam contas e chaves de API diferentes",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Código de país de duas letras, por exemplo de, fr, it, es",
7
9
  "Polling": "Sondagem",
8
10
  "Update interval in minutes": "Intervalo de atualização em minutos",
@@ -10,5 +12,7 @@
10
12
  "API Key": "Chave API",
11
13
  "API Key (leave empty for auto-detect)": "Chave API (deixe em branco para detecção automática)",
12
14
  "If empty, the API key will be set automatically": "Se vazio, a chave API será definida automaticamente",
13
- "Click here for current API Key": "Clique aqui para a chave API atual"
15
+ "Click here for current API Key": "Clique aqui para a chave API atual",
16
+ "Charge history entries to keep": "Entradas do histórico de carregamento a manter",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Número de entradas mais recentes mantidas por endpoint do histórico de carregamento (0 = ilimitado)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Электронная почта приложения",
4
4
  "App Password": "Пароль приложения",
5
5
  "Country": "Страна",
6
+ "Brand": "Марка",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia и Alpine используют разные учётные записи и API-ключи",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Двухбуквенный код страны, например de, fr, it, es",
7
9
  "Polling": "Опрос",
8
10
  "Update interval in minutes": "Интервал обновления в минутах",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API ключ",
11
13
  "API Key (leave empty for auto-detect)": "API ключ (оставьте пустым для автоопределения)",
12
14
  "If empty, the API key will be set automatically": "Если пусто, API ключ будет установлен автоматически",
13
- "Click here for current API Key": "Нажмите здесь для получения текущего API ключа"
15
+ "Click here for current API Key": "Нажмите здесь для получения текущего API ключа",
16
+ "Charge history entries to keep": "Сохраняемые записи истории зарядки",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Количество последних записей, сохраняемых для каждого эндпоинта истории зарядки (0 = без ограничений)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "Електронна пошта застосунку",
4
4
  "App Password": "Пароль застосунку",
5
5
  "Country": "Країна",
6
+ "Brand": "Марка",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia та Alpine використовують різні облікові записи та API-ключі",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "Дволітерний код країни, наприклад de, fr, it, es",
7
9
  "Polling": "Опитування",
8
10
  "Update interval in minutes": "Інтервал оновлення в хвилинах",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API ключ",
11
13
  "API Key (leave empty for auto-detect)": "API ключ (залиште порожнім для автовизначення)",
12
14
  "If empty, the API key will be set automatically": "Якщо порожньо, API ключ буде встановлено автоматично",
13
- "Click here for current API Key": "Натисніть тут для отримання поточного API ключа"
15
+ "Click here for current API Key": "Натисніть тут для отримання поточного API ключа",
16
+ "Charge history entries to keep": "Записи історії заряджання, що зберігаються",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "Кількість останніх записів, що зберігаються для кожного ендпоінта історії заряджання (0 = без обмежень)"
14
18
  }
@@ -3,6 +3,8 @@
3
3
  "App Email": "应用邮箱",
4
4
  "App Password": "应用密码",
5
5
  "Country": "国家",
6
+ "Brand": "品牌",
7
+ "Renault/Dacia and Alpine use different accounts and API keys": "Renault/Dacia 和 Alpine 使用不同的账户和 API 密钥",
6
8
  "Two-letter country code, e.g. de, fr, it, es": "两位字母的国家代码,例如 de, fr, it, es",
7
9
  "Polling": "轮询",
8
10
  "Update interval in minutes": "更新间隔(分钟)",
@@ -10,5 +12,7 @@
10
12
  "API Key": "API 密钥",
11
13
  "API Key (leave empty for auto-detect)": "API 密钥(留空自动检测)",
12
14
  "If empty, the API key will be set automatically": "如果为空,API 密钥将自动设置",
13
- "Click here for current API Key": "点击此处获取当前 API 密钥"
15
+ "Click here for current API Key": "点击此处获取当前 API 密钥",
16
+ "Charge history entries to keep": "保留的充电历史记录条数",
17
+ "Number of most recent entries kept per charge-history endpoint (0 = unlimited)": "每个充电历史接口保留的最新记录条数(0 = 不限制)"
14
18
  }
@@ -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,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "renault",
4
- "version": "0.0.23",
4
+ "version": "0.0.25",
5
5
  "news": {
6
+ "0.0.25": {
7
+ "en": "more robust startup with retry, truthful connection state, no secrets in the log, require Node.js 22+",
8
+ "de": "robusterer Start mit Wiederholung, ehrlicher Verbindungsstatus, keine Geheimnisse im Log, benötigt Node.js 22+",
9
+ "ru": "более надёжный запуск с повтором, честный статус соединения, без секретов в логе, требуется Node.js 22+",
10
+ "pt": "arranque mais robusto com nova tentativa, estado de ligação fiável, sem segredos no registo, requer Node.js 22+",
11
+ "nl": "robuustere start met herhaling, eerlijke verbindingsstatus, geen geheimen in het logboek, vereist Node.js 22+",
12
+ "fr": "démarrage plus robuste avec nouvelle tentative, état de connexion fiable, aucun secret dans le journal, nécessite Node.js 22+",
13
+ "it": "avvio più robusto con ripetizione, stato di connessione affidabile, nessun segreto nel log, richiede Node.js 22+",
14
+ "es": "inicio más robusto con reintento, estado de conexión fiable, sin secretos en el registro, requiere Node.js 22+",
15
+ "pl": "solidniejszy start z ponawianiem, prawdziwy status połączenia, bez sekretów w logu, wymaga Node.js 22+",
16
+ "uk": "надійніший запуск із повтором, чесний статус з'єднання, без секретів у журналі, потрібен Node.js 22+",
17
+ "zh-cn": "启动更稳健并带重试、连接状态真实、日志中不含密钥、需要 Node.js 22+"
18
+ },
19
+ "0.0.24": {
20
+ "en": "add Alpine support (select brand Alpine in the settings)",
21
+ "de": "Alpine-Unterstützung hinzugefügt (Marke Alpine in den Einstellungen wählen)",
22
+ "ru": "Добавлена поддержка Alpine (выберите марку Alpine в настройках)",
23
+ "pt": "Suporte para Alpine adicionado (selecione a marca Alpine nas definições)",
24
+ "nl": "Alpine-ondersteuning toegevoegd (selecteer merk Alpine in de instellingen)",
25
+ "fr": "Prise en charge d'Alpine ajoutée (sélectionnez la marque Alpine dans les paramètres)",
26
+ "it": "Aggiunto supporto Alpine (seleziona il marchio Alpine nelle impostazioni)",
27
+ "es": "Añadido soporte para Alpine (seleccione la marca Alpine en los ajustes)",
28
+ "pl": "Dodano obsługę Alpine (wybierz markę Alpine w ustawieniach)",
29
+ "uk": "Додано підтримку Alpine (виберіть марку Alpine в налаштуваннях)",
30
+ "zh-cn": "新增 Alpine 支持(在设置中选择品牌 Alpine)"
31
+ },
6
32
  "0.0.23": {
7
33
  "en": "align API headers with My Renault Android app (User-Agent okhttp/5.3.0, X-Amzn-Trace-Id)",
8
34
  "de": "API-Header an My-Renault-Android-App angeglichen (User-Agent okhttp/5.3.0, X-Amzn-Trace-Id)",
@@ -67,32 +93,6 @@
67
93
  "pl": "Naprawiono start ładowania i zmniejszono logi błędów",
68
94
  "uk": "Виправлено старт зарядки та зменшено журнали помилок",
69
95
  "zh-cn": "修复充电启动并减少错误日志"
70
- },
71
- "0.0.14": {
72
- "en": "add charging stop",
73
- "de": "Ladestopp hinzugefügt",
74
- "ru": "Добавлена остановка зарядки",
75
- "pt": "Adicionada paragem de carga",
76
- "nl": "Stoppen van het laden toegevoegd",
77
- "fr": "Ajout de l'arrêt de la charge",
78
- "it": "Aggiunto stop della carica",
79
- "es": "Añadida parada de carga",
80
- "pl": "Dodano zatrzymanie ładowania",
81
- "uk": "Додано зупинку зарядки",
82
- "zh-cn": "添加充电停止"
83
- },
84
- "0.0.13": {
85
- "en": "add remote object for force refresh",
86
- "de": "Remote-Objekt für erzwungene Aktualisierung hinzugefügt",
87
- "ru": "Добавлен удалённый объект для принудительного обновления",
88
- "pt": "Adicionado objeto remoto para forçar atualização",
89
- "nl": "Remote-object voor geforceerde verversing toegevoegd",
90
- "fr": "Ajout d'un objet distant pour le rafraîchissement forcé",
91
- "it": "Aggiunto oggetto remoto per il refresh forzato",
92
- "es": "Añadido objeto remoto para actualización forzada",
93
- "pl": "Dodano obiekt zdalny do wymuszonego odświeżenia",
94
- "uk": "Додано віддалений об'єкт для примусового оновлення",
95
- "zh-cn": "添加用于强制刷新的 remote 对象"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -163,10 +163,12 @@
163
163
  "native": {
164
164
  "username": "",
165
165
  "country": "de",
166
+ "brand": "renault",
166
167
  "password": "",
167
168
  "apiKeyUpdate": "",
168
169
  "interval": 10,
169
- "disableChargeFetching": false
170
+ "disableChargeFetching": false,
171
+ "chargeHistoryLimit": 100
170
172
  },
171
173
  "objects": [],
172
174
  "instanceObjects": [
@@ -190,6 +192,19 @@
190
192
  "def": false
191
193
  },
192
194
  "native": {}
195
+ },
196
+ {
197
+ "_id": "info.migrationV1",
198
+ "type": "state",
199
+ "common": {
200
+ "role": "indicator",
201
+ "name": "Migration V1 (charge history forceIndex) executed",
202
+ "type": "boolean",
203
+ "read": true,
204
+ "write": false,
205
+ "def": false
206
+ },
207
+ "native": {}
193
208
  }
194
209
  ]
195
210
  }
package/main.js CHANGED
@@ -29,6 +29,14 @@ class Renault extends utils.Adapter {
29
29
  this.firstUpdate = true;
30
30
  this.requestClient = axios.create();
31
31
  this.userAgent = 'okhttp/5.3.0';
32
+ /** @type {string} */
33
+ this.brand = 'renault';
34
+ /** @type {string[]} */
35
+ this.accountTypes = [];
36
+ this.updateInterval = null;
37
+ this.refreshTokenInterval = null;
38
+ this.startAttempt = 0;
39
+ this.loginRejected = false;
32
40
  }
33
41
 
34
42
  /** APK rI2.smali (WiredHeaderAppVersionInterceptor): build={brand}-android-{version};trId={uuid} on wired Kamereon host */
@@ -38,7 +46,9 @@ class Renault extends utils.Adapter {
38
46
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
39
47
  return v.toString(16);
40
48
  });
41
- return 'build=renault-android-6.11.2;trId=' + uuid;
49
+ const brand = this.brand;
50
+ const version = brand === 'alpine' ? '6.10.1' : '6.11.2';
51
+ return 'build=' + brand + '-android-' + version + ';trId=' + uuid;
42
52
  }
43
53
 
44
54
  /**
@@ -51,14 +61,21 @@ class Renault extends utils.Adapter {
51
61
  this.log.info('Set interval to minimum 0.5');
52
62
  this.config.interval = 0.5;
53
63
  }
54
- this.updateInterval = null;
55
64
  this.reLoginTimeout = null;
56
65
  this.refreshTokenTimeout = null;
57
66
  this.country = this.config.country || 'de';
67
+ this.brand = this.config.brand || 'renault';
58
68
  this.session = {};
59
- //DE API Key
69
+ //DE API Key (shared by Renault, Dacia and Alpine - same Gigya/Kamereon tenant)
60
70
  this.apiKey = '3_VgdkgtIRH3AdHvJm-cjV2ug2EFE0lxt0IJzMC4MFqZjFpn_GYFXVdNZ19L7wZX0N';
61
71
  this.apiKeyUpdate = 'YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J';
72
+ if (this.brand === 'alpine') {
73
+ this.product = 'MYALPINE';
74
+ this.accountTypes = ['MYALPINE'];
75
+ } else {
76
+ this.product = 'MYRENAULT';
77
+ this.accountTypes = ['MYRENAULT', 'MYDACIA'];
78
+ }
62
79
  try {
63
80
  await this.requestClient({
64
81
  method: 'get',
@@ -66,17 +83,15 @@ class Renault extends utils.Adapter {
66
83
  })
67
84
 
68
85
  .then((res) => {
69
- this.log.debug(JSON.stringify(res.data));
70
-
71
86
  if (res.data.split('KAMEREON_APIKEY = "')[2] && res.data.split('KAMEREON_APIKEY = "')[2].split('"')[0]) {
72
87
  this.apiKeyUpdate = res.data.split('KAMEREON_APIKEY = "')[2].split('"')[0];
73
88
  }
74
89
  })
75
90
  .catch((error) => {
76
- this.log.debug(error);
91
+ this.log.debug('API key lookup failed: ' + error);
77
92
  });
78
93
  } catch (error) {
79
- this.log.debug(error);
94
+ this.log.debug('API key lookup failed: ' + error);
80
95
  }
81
96
  if (this.config.apiKeyUpdate) {
82
97
  this.apiKeyUpdate = this.config.apiKeyUpdate;
@@ -84,10 +99,17 @@ class Renault extends utils.Adapter {
84
99
 
85
100
  this.subscribeStates('*');
86
101
 
87
- await this.login();
102
+ await this.connectAndPoll();
103
+ }
88
104
 
89
- if (this.session.id_token && this.session_data && this.account) {
90
- await this.getDeviceList();
105
+ /**
106
+ * Log in, load the vehicles and start polling. A network or server failure is retried with
107
+ * growing delay (5 min doubling up to 60 min). A login the account service rejected is not
108
+ * retried, so wrong credentials cannot lock the account.
109
+ */
110
+ async connectAndPoll() {
111
+ if ((await this.login()) && (await this.getDeviceList())) {
112
+ await this.migrateChargeHistoryV1();
91
113
  await this.updateDevices();
92
114
  this.updateInterval = setInterval(
93
115
  async () => {
@@ -98,9 +120,33 @@ class Renault extends utils.Adapter {
98
120
  this.refreshTokenInterval = setInterval(() => {
99
121
  this.refreshToken();
100
122
  }, 3500 * 1000);
123
+ return;
124
+ }
125
+ if (this.loginRejected) {
126
+ this.log.error('Login rejected. Check email and password in the adapter settings, then restart the instance.');
127
+ return;
101
128
  }
129
+ const delayMinutes = Math.min(5 * 2 ** this.startAttempt, 60);
130
+ this.startAttempt++;
131
+ this.log.warn('Connection to the Renault cloud failed. Next attempt in ' + delayMinutes + ' minutes');
132
+ this.setTimeout(
133
+ () => this.connectAndPoll().catch((error) => this.log.error('Connection attempt failed: ' + error)),
134
+ delayMinutes * 60 * 1000,
135
+ );
102
136
  }
137
+
138
+ /**
139
+ * Log in and look up the account. Sets info.connection accordingly.
140
+ *
141
+ * @returns {Promise<boolean>} true when session, id token and account are available
142
+ */
103
143
  async login() {
144
+ const ok = await this.loginSteps();
145
+ this.setState('info.connection', ok, true);
146
+ return ok;
147
+ }
148
+
149
+ async loginSteps() {
104
150
  this.session_data = await this.requestClient({
105
151
  method: 'post',
106
152
  url: 'https://accounts.eu1.gigya.com/accounts.login',
@@ -120,23 +166,24 @@ class Renault extends utils.Adapter {
120
166
  }),
121
167
  })
122
168
  .then((res) => {
123
- this.log.debug(JSON.stringify(res.data));
124
169
  if (res.data.errorMessage) {
170
+ this.loginRejected = true;
125
171
  this.log.error(JSON.stringify(res.data));
126
172
  return;
127
173
  }
128
174
  return res.data.sessionInfo;
129
175
  })
130
176
  .catch((error) => {
131
- this.log.error(error);
177
+ this.log.error('Login failed: ' + error);
132
178
  if (error.response) {
133
179
  this.log.error(JSON.stringify(error.response.data));
134
180
  }
135
181
  });
136
182
  if (!this.session_data) {
137
183
  this.log.error('No session found for this account. Please login in the app. Maybe a new password is needed.');
138
- return;
184
+ return false;
139
185
  }
186
+ this.session = {};
140
187
  await this.requestClient({
141
188
  method: 'post',
142
189
  url: 'https://accounts.eu1.gigya.com/accounts.getJWT',
@@ -157,19 +204,24 @@ class Renault extends utils.Adapter {
157
204
  }),
158
205
  })
159
206
  .then((res) => {
160
- this.log.debug(JSON.stringify(res.data));
161
207
  this.session = res.data;
162
- this.setState('info.connection', true, true);
163
208
  })
164
209
  .catch((error) => {
165
- this.log.error(error);
210
+ this.log.error('Getting the id token failed: ' + error);
166
211
  if (error.response) {
167
212
  this.log.error(JSON.stringify(error.response.data));
168
213
  }
169
214
  });
170
- await this.requestClient({
215
+ if (!this.session.id_token) {
216
+ return false;
217
+ }
218
+ return await this.requestClient({
171
219
  method: 'post',
172
- url: 'https://apis.renault.com/myr/api/v1/connection?&country=DE&product=MYRENAULT&locale=de-DE&displayAccounts=MYRENAULT',
220
+ url:
221
+ 'https://apis.renault.com/myr/api/v1/connection?&country=DE&product=' +
222
+ this.product +
223
+ '&locale=de-DE&displayAccounts=' +
224
+ this.product,
173
225
  headers: {
174
226
  'Content-Type': 'application/json',
175
227
  Accept: '*/*',
@@ -180,33 +232,41 @@ class Renault extends utils.Adapter {
180
232
  },
181
233
  })
182
234
  .then((res) => {
183
- this.log.debug(JSON.stringify(res.data));
184
-
235
+ const accountTypes = this.accountTypes;
185
236
  const filteredAccounts = res.data.currentUser.accounts.filter(function (el) {
186
- return (el.accountType === 'MYRENAULT' || el.accountType === 'MYDACIA') && el.accountStatus === 'ACTIVE';
237
+ return accountTypes.includes(el.accountType) && el.accountStatus === 'ACTIVE';
187
238
  });
188
239
  if (filteredAccounts.length === 0) {
189
240
  this.log.error('No Account found');
190
- this.log.error('All accounts: ' + res.data.currentUser.accounts);
191
- this.log.error('Filtered accounts: ' + filteredAccounts);
192
- return;
241
+ this.log.error(
242
+ 'Accounts of this login: ' +
243
+ JSON.stringify(res.data.currentUser.accounts.map((el) => el.accountType + ' ' + el.accountStatus)),
244
+ );
245
+ return false;
193
246
  }
194
247
 
195
248
  this.account = filteredAccounts[0];
249
+ return true;
196
250
  })
197
251
  .catch((error) => {
198
- this.log.error('Error while getting account');
199
- this.log.error(error);
252
+ this.log.error('Error while getting account: ' + error);
200
253
  if (error.response) {
201
254
  this.log.error(JSON.stringify(error.response.data));
202
255
  if (error.response.data && JSON.stringify(error.response.data).indexOf('apikey') !== -1) {
203
256
  this.log.error('Wrong API Key. Please update API Key in adapter settings');
204
257
  }
205
258
  }
259
+ return false;
206
260
  });
207
261
  }
262
+
263
+ /**
264
+ * Load the vehicles of the account and create their objects.
265
+ *
266
+ * @returns {Promise<boolean>} true when the vehicle list was loaded
267
+ */
208
268
  async getDeviceList() {
209
- await this.requestClient({
269
+ return await this.requestClient({
210
270
  method: 'get',
211
271
  url:
212
272
  'https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/' +
@@ -227,10 +287,11 @@ class Renault extends utils.Adapter {
227
287
  .then(async (res) => {
228
288
  this.log.debug(JSON.stringify(res.data));
229
289
 
290
+ this.deviceArray = [];
230
291
  for (const device of res.data.vehicleLinks) {
231
292
  this.deviceArray.push(device.vin);
232
293
  let name = device.vehicleDetails?.modelSCR || device.brand;
233
- if (device.vehicleDetails.model && device.vehicleDetails.model.label) {
294
+ if (device.vehicleDetails?.model?.label) {
234
295
  name += device.vehicleDetails.model.label;
235
296
  }
236
297
 
@@ -259,7 +320,12 @@ class Renault extends utils.Adapter {
259
320
 
260
321
  const remoteArray = [
261
322
  { command: 'actions/hvac-start', name: 'True = Start, False = Stop' },
262
- { command: 'hvac-temperature', name: 'HVAC Temperature', type: 'number', role: 'value' },
323
+ {
324
+ command: 'hvac-temperature',
325
+ name: 'HVAC Temperature',
326
+ type: /** @type {ioBroker.CommonType} */ ('number'),
327
+ role: 'value',
328
+ },
263
329
  { command: 'actions/charging-start', name: 'True = Start, False = Stop' },
264
330
  { command: 'charge/pause-resume', name: 'True = Start, False = Stop' },
265
331
  { command: 'charge/start', name: 'True = Start, False = Stop' },
@@ -270,7 +336,7 @@ class Renault extends utils.Adapter {
270
336
  type: 'state',
271
337
  common: {
272
338
  name: remote.name || '',
273
- type: remote.type || 'boolean',
339
+ type: /** @type {ioBroker.CommonType} */ (remote.type || 'boolean'),
274
340
  role: remote.role || 'button',
275
341
  write: true,
276
342
  read: true,
@@ -281,20 +347,49 @@ class Renault extends utils.Adapter {
281
347
  delete device.mileage;
282
348
  this.json2iob.parse(device.vin + '.general', device);
283
349
  }
350
+ return true;
284
351
  })
285
352
  .catch((error) => {
286
- this.log.error('Error while getting vehicle list');
287
- this.log.error(error);
353
+ this.log.error('Error while getting vehicle list: ' + error);
288
354
  error.response && this.log.error(JSON.stringify(error.response.data));
355
+ return false;
289
356
  });
290
357
  }
291
358
 
359
+ /**
360
+ * One-shot migration: with v0.0.24 the charge-history and charges endpoints switched from
361
+ * date-named channels (e.g. `20220802`, `2022-04-22T11:46:38Z`) to numeric forceIndex slots.
362
+ * Without cleanup the old objects would linger forever next to the new ones. The marker state
363
+ * `info.migrationV1` ensures this only runs once per instance.
364
+ */
365
+ async migrateChargeHistoryV1() {
366
+ try {
367
+ const marker = await this.getStateAsync('info.migrationV1');
368
+ if (marker && marker.val === true) {
369
+ return;
370
+ }
371
+ this.log.info('Running charge history migration V1: cleaning up legacy date-named channels');
372
+ for (const vin of this.deviceArray) {
373
+ for (const path of ['charge-history', 'charges']) {
374
+ await this.delObjectAsync(vin + '.' + path, { recursive: true }).catch(() => {});
375
+ }
376
+ }
377
+ await this.setState('info.migrationV1', { val: true, ack: true });
378
+ this.log.info('Charge history migration V1 done');
379
+ } catch (e) {
380
+ this.log.warn('Charge history migration V1 failed: ' + e);
381
+ }
382
+ }
383
+
292
384
  async updateDevices() {
293
- if (!this.account.accountId) {
385
+ if (!this.account?.accountId) {
294
386
  this.log.error('No accountId found');
295
387
  return;
296
388
  }
297
389
  const curDate = new Date().toISOString().split('T')[0];
390
+ // Charge history: limit start date to ~1 year back (My Renault app paginates yearly).
391
+ // Keeping the range bounded prevents the API from returning years of data on every poll.
392
+ const historyStart = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
298
393
 
299
394
  const statusArray = [
300
395
  {
@@ -405,22 +500,28 @@ class Renault extends utils.Adapter {
405
500
  url:
406
501
  'https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/' +
407
502
  this.account.accountId +
408
- '/kamereon/kca/car-adapter/v1/cars/$vin/charge-history?type=day&start=1970-01-01&end=' +
503
+ '/kamereon/kca/car-adapter/v1/cars/$vin/charge-history?type=day&start=' +
504
+ historyStart +
505
+ '&end=' +
409
506
  curDate +
410
507
  '&country=' +
411
508
  this.country,
412
509
  desc: 'Charging history of the car',
510
+ isHistory: true,
413
511
  });
414
512
  statusArray.push({
415
513
  path: 'charges',
416
514
  url:
417
515
  'https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/' +
418
516
  this.account.accountId +
419
- '/kamereon/kca/car-adapter/v1/cars/$vin/charges?start=1970-01-01&end=' +
517
+ '/kamereon/kca/car-adapter/v1/cars/$vin/charges?start=' +
518
+ historyStart +
519
+ '&end=' +
420
520
  curDate +
421
521
  '&country=' +
422
522
  this.country,
423
523
  desc: 'Charges of the car',
524
+ isHistory: true,
424
525
  });
425
526
  }
426
527
 
@@ -444,7 +545,7 @@ class Renault extends utils.Adapter {
444
545
  url: url,
445
546
  headers: { ...headers, 'X-Amzn-Trace-Id': this.buildTraceId() },
446
547
  })
447
- .then((res) => {
548
+ .then(async (res) => {
448
549
  this.log.debug(JSON.stringify(res.data));
449
550
  if (!res.data) {
450
551
  return;
@@ -454,9 +555,24 @@ class Renault extends utils.Adapter {
454
555
  data = res.data.data.attributes;
455
556
  }
456
557
 
457
- const forceIndex = undefined;
558
+ let forceIndex = undefined;
458
559
  const preferedArrayName = undefined;
459
560
 
561
+ // Charge history endpoints return arrays keyed by date — without forceIndex
562
+ // json2iob creates a new channel per date and never cleans them up. Cap to the
563
+ // configured limit (default 100, matches the My Renault app pagination) and use
564
+ // numeric indices so old entries are overwritten on each poll. The one-shot
565
+ // migrateChargeHistoryV1() removes the legacy date-named channels on first start.
566
+ if (element.isHistory) {
567
+ forceIndex = true;
568
+ const arrayKey = element.path === 'charge-history' ? 'chargeSummaries' : 'charges';
569
+ const limit = Number(this.config.chargeHistoryLimit);
570
+ const cap = Number.isFinite(limit) && limit > 0 ? limit : 0;
571
+ if (cap > 0 && data && Array.isArray(data[arrayKey]) && data[arrayKey].length > cap) {
572
+ data = { ...data, [arrayKey]: data[arrayKey].slice(-cap) };
573
+ }
574
+ }
575
+
460
576
  this.json2iob.parse(vin + '.' + element.path, data, {
461
577
  forceIndex: forceIndex,
462
578
  preferedArrayName: preferedArrayName,
@@ -476,20 +592,13 @@ class Renault extends utils.Adapter {
476
592
  return;
477
593
  }
478
594
  if (this.firstUpdate) {
479
- if (
480
- error.response.status === 403 ||
481
- error.response.status === 404 ||
482
- error.response.status === 500 ||
483
- error.response.status === 501 ||
484
- error.response.status === 502 ||
485
- error.response.status === 400
486
- ) {
595
+ if (error.response.status === 400 || error.response.status === 403 || error.response.status === 404) {
487
596
  if (!this.ignoreState[vin]) {
488
597
  this.ignoreState[vin] = [];
489
598
  }
490
599
  this.ignoreState[vin].push(element.path);
491
600
  this.log.info('Feature not found for ' + vin + '. Ignore ' + element.path + ' for updates.');
492
- this.log.debug(error);
601
+ this.log.debug(String(error));
493
602
  error.response && this.log.debug(JSON.stringify(error.response.data));
494
603
  return;
495
604
  }
@@ -500,7 +609,7 @@ class Renault extends utils.Adapter {
500
609
  return;
501
610
  }
502
611
  this.log.error(url);
503
- this.log.error(error);
612
+ this.log.error(String(error));
504
613
  error.response && this.log.error(JSON.stringify(error.response.data));
505
614
  });
506
615
  }
@@ -533,13 +642,12 @@ class Renault extends utils.Adapter {
533
642
  }),
534
643
  })
535
644
  .then((res) => {
536
- this.log.debug(JSON.stringify(res.data));
537
645
  this.session = res.data;
538
646
  this.setState('info.connection', true, true);
539
647
  })
540
648
  .catch((error) => {
541
- this.log.error('refresh token failed');
542
- this.log.error(error);
649
+ this.setState('info.connection', false, true);
650
+ this.log.error('refresh token failed: ' + error);
543
651
  error.response && this.log.error(JSON.stringify(error.response.data));
544
652
  this.log.error('Start relogin in 1min');
545
653
  this.reLoginTimeout = setTimeout(
@@ -550,9 +658,6 @@ class Renault extends utils.Adapter {
550
658
  );
551
659
  });
552
660
  }
553
- sleep(ms) {
554
- return new Promise((resolve) => setTimeout(resolve, ms));
555
- }
556
661
  toCamelCase(string) {
557
662
  if (!string) {
558
663
  return;
@@ -575,7 +680,7 @@ class Renault extends utils.Adapter {
575
680
  this.reLoginTimeout && clearTimeout(this.reLoginTimeout);
576
681
  this.refreshTokenTimeout && clearTimeout(this.refreshTokenTimeout);
577
682
  this.updateInterval && clearInterval(this.updateInterval);
578
- clearInterval(this.refreshTokenInterval);
683
+ this.refreshTokenInterval && clearInterval(this.refreshTokenInterval);
579
684
  callback();
580
685
  } catch (e) {
581
686
  this.log.error('Error onUnload: ' + e);
@@ -596,15 +701,15 @@ class Renault extends utils.Adapter {
596
701
  if (path === 'hvac-temperature') {
597
702
  return;
598
703
  }
704
+ if (!this.account) {
705
+ this.log.error('No account found');
706
+ return;
707
+ }
599
708
  if (path === 'refresh') {
600
709
  this.log.info('Force refresh');
601
710
  this.updateDevices();
602
711
  return;
603
712
  }
604
- if (!this.account) {
605
- this.log.error('No account found');
606
- return;
607
- }
608
713
  const command = path.split('/')[1];
609
714
  let action = state.val ? 'start' : 'cancel';
610
715
  let midPart = 'kca/car-adapter/v1/cars/';
@@ -652,7 +757,7 @@ class Renault extends utils.Adapter {
652
757
  return res.data;
653
758
  })
654
759
  .catch((error) => {
655
- this.log.error(error);
760
+ this.log.error('Command ' + path + ' failed: ' + error);
656
761
  if (error.response) {
657
762
  this.log.error(JSON.stringify(error.response.data));
658
763
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iobroker.renault",
3
- "version": "0.0.23",
4
- "description": "Adapter for Renault and Renault",
3
+ "version": "0.0.25",
4
+ "description": "Adapter for Renault, Dacia and Alpine",
5
5
  "author": {
6
6
  "name": "TA2k",
7
7
  "email": "tombox2020@gmail.com"
@@ -25,22 +25,20 @@
25
25
  "url": "https://github.com/TA2k/ioBroker.renault"
26
26
  },
27
27
  "dependencies": {
28
- "@iobroker/adapter-core": "^3.3.2",
28
+ "@iobroker/adapter-core": "^3.4.3",
29
29
  "axios": "^1.16.1",
30
- "json-bigint": "^1.0.0",
31
30
  "json2iob": "^2.6.22",
32
31
  "qs": "^6.15.2"
33
32
  },
34
33
  "devDependencies": {
35
- "@alcalzone/release-script": "^5.2.0",
34
+ "@alcalzone/release-script": "^5.2.1",
36
35
  "@alcalzone/release-script-plugin-iobroker": "^5.2.0",
37
- "@alcalzone/release-script-plugin-license": "^5.2.0",
36
+ "@alcalzone/release-script-plugin-license": "^5.2.2",
38
37
  "@alcalzone/release-script-plugin-manual-review": "^5.2.0",
39
38
  "@eslint/eslintrc": "^3.3.5",
40
39
  "@eslint/js": "^10.0.1",
41
- "@iobroker/testing": "^5.2.2",
42
- "@tsconfig/node18": "^18.2.6",
43
- "@types/node": "^25.9.1",
40
+ "@iobroker/testing": "^6.2.2",
41
+ "@types/node": "^22.20.3",
44
42
  "eslint": "^10.4.0",
45
43
  "eslint-config-prettier": "^10.1.8",
46
44
  "eslint-plugin-prettier": "^5.5.5",
@@ -50,7 +48,7 @@
50
48
  },
51
49
  "main": "main.js",
52
50
  "engines": {
53
- "node": ">=20"
51
+ "node": ">=22"
54
52
  },
55
53
  "files": [
56
54
  "admin{,/!(src)/**}/!(tsconfig|tsconfig.*).json",
@@ -1 +0,0 @@
1
- module.exports = {};