iobroker.mg-saic 0.1.1 → 0.1.3
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 +11 -11
- package/admin/i18n/de.json +6 -1
- package/admin/i18n/en.json +6 -1
- package/admin/i18n/es.json +6 -1
- package/admin/i18n/fr.json +6 -1
- package/admin/i18n/it.json +6 -1
- package/admin/i18n/nl.json +6 -1
- package/admin/i18n/pl.json +6 -1
- package/admin/i18n/pt.json +6 -1
- package/admin/i18n/ru.json +6 -1
- package/admin/i18n/uk.json +6 -1
- package/admin/i18n/zh-cn.json +6 -1
- package/build/main.js +150 -41
- package/build/main.js.map +2 -2
- package/build/refresh-poll-queue.js +68 -0
- package/build/refresh-poll-queue.js.map +7 -0
- package/build/refresh-poll-timer.js +65 -0
- package/build/refresh-poll-timer.js.map +7 -0
- package/build/saic-client.js +128 -44
- package/build/saic-client.js.map +3 -3
- package/build/status-metadata.js +115 -24
- package/build/status-metadata.js.map +2 -2
- package/io-package.json +29 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,13 +36,23 @@ For manufacturer information, see [MG Motor](https://www.mgmotor.eu/) and [SAIC
|
|
|
36
36
|
|
|
37
37
|
## Installation
|
|
38
38
|
|
|
39
|
-
The adapter is published on npm
|
|
39
|
+
The adapter is published on npm and available from ioBroker's Latest adapter repository.
|
|
40
40
|
|
|
41
41
|
## Support
|
|
42
42
|
|
|
43
43
|
[](https://www.paypal.com/donate/?hosted_button_id=3RJQGSJNRSHUJ)
|
|
44
44
|
|
|
45
45
|
## Changelog
|
|
46
|
+
### 0.1.3 (2026-11-08)
|
|
47
|
+
|
|
48
|
+
- (Voodoo2man) Retry startup initialization after temporary cloud login and vehicle-list outages with bounded backoff, prevent false setting writes from triggering remote setting actions, and add the missing region labels in the Admin UI.
|
|
49
|
+
|
|
50
|
+
### 0.1.2 (2026-11-08)
|
|
51
|
+
|
|
52
|
+
- (Voodoo2man) Prevented unsafe or case-colliding API fields and vehicle IDs from creating duplicate datapoints, kept status IDs stable across field-casing changes, scheduled polling after each completed cycle while honoring queued manual refreshes, and removed an unused cloud-backend translation.
|
|
53
|
+
- (Voodoo2man) Replaced untrusted cloud error text with English code-based diagnostics and standardized the product title as MG/SAIC across locales.
|
|
54
|
+
- (Voodoo2man) Reused dynamic charging datapoints across field-name casing changes and normalized known status fields.
|
|
55
|
+
|
|
46
56
|
### 0.1.1 (2026-11-07)
|
|
47
57
|
|
|
48
58
|
- (Voodoo2man) Lowered the minimum Node.js requirement to 22.22.2.
|
|
@@ -61,18 +71,8 @@ The adapter is published on npm but has not yet been added to the ioBroker adapt
|
|
|
61
71
|
- (Voodoo2man) Removed duplicate and irrelevant datapoints.
|
|
62
72
|
- (Voodoo2man) Set min version of admin to 8.2.0, js-controller 7.2.2, node.js 24
|
|
63
73
|
|
|
64
|
-
### 0.0.3 (2026-10-21)
|
|
65
|
-
|
|
66
|
-
- (Voodoo2man) Improved release automation, updated Node.js release checks, and removed unpublished version metadata.
|
|
67
|
-
- (Voodoo2man) Added charging data
|
|
68
|
-
- (Voodoo2man) Fixed vehicle datapoint units and roles
|
|
69
|
-
|
|
70
74
|
Older changelog entries are available in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
71
75
|
|
|
72
|
-
### 0.0.2 (2026-10-20)
|
|
73
|
-
|
|
74
|
-
- (Voodoo2man) Added regional iSmart account setup, vehicle status, and supported remote controls.
|
|
75
|
-
|
|
76
76
|
## License
|
|
77
77
|
|
|
78
78
|
MIT License
|
package/admin/i18n/de.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Um mögliche Konflikte mit der mobilen App zu vermeiden, verwenden Sie nach Möglichkeit ein separates iSmart-Konto für ioBroker. Teilen Sie das Fahrzeug mit diesem Konto und erteilen Sie die für die gewünschte Nutzung erforderlichen Berechtigungen.",
|
|
7
7
|
"Region": "Region",
|
|
8
8
|
"European Union": "Europäische Union",
|
|
9
|
+
"China": "China",
|
|
10
|
+
"Australia": "Australien",
|
|
11
|
+
"Brazil": "Brasilien",
|
|
12
|
+
"Israel": "Israel",
|
|
13
|
+
"Turkey": "Türkei",
|
|
14
|
+
"Thailand": "Thailand",
|
|
9
15
|
"Rest of World": "Rest der Welt",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Die Cloud-Anbindung ist noch nicht implementiert. Dieser Wert wählt kein funktionsfähiges Backend aus.",
|
|
11
16
|
"Login type": "Anmeldetyp",
|
|
12
17
|
"Email": "E-Mail",
|
|
13
18
|
"Phone number": "Telefonnummer",
|
package/admin/i18n/en.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.",
|
|
7
7
|
"Region": "Region",
|
|
8
8
|
"European Union": "European Union",
|
|
9
|
+
"China": "China",
|
|
10
|
+
"Australia": "Australia",
|
|
11
|
+
"Brazil": "Brazil",
|
|
12
|
+
"Israel": "Israel",
|
|
13
|
+
"Turkey": "Turkey",
|
|
14
|
+
"Thailand": "Thailand",
|
|
9
15
|
"Rest of World": "Rest of World",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Cloud connectivity is not implemented yet. This value does not select a working backend.",
|
|
11
16
|
"Login type": "Login type",
|
|
12
17
|
"Email": "Email",
|
|
13
18
|
"Phone number": "Phone number",
|
package/admin/i18n/es.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Para evitar posibles interferencias de sesión con la aplicación móvil, utiliza una cuenta iSmart independiente para ioBroker siempre que sea posible. Comparte el vehículo con esa cuenta y concede los permisos necesarios para el uso previsto.",
|
|
7
7
|
"Region": "Región",
|
|
8
8
|
"European Union": "Unión Europea",
|
|
9
|
+
"China": "China",
|
|
10
|
+
"Australia": "Australia",
|
|
11
|
+
"Brazil": "Brasil",
|
|
12
|
+
"Israel": "Israel",
|
|
13
|
+
"Turkey": "Turquía",
|
|
14
|
+
"Thailand": "Tailandia",
|
|
9
15
|
"Rest of World": "Resto del mundo",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "La conexión a la nube aún no está implementada. Este valor no selecciona un servicio operativo.",
|
|
11
16
|
"Login type": "Tipo de inicio de sesión",
|
|
12
17
|
"Email": "Correo electrónico",
|
|
13
18
|
"Phone number": "Número de teléfono",
|
package/admin/i18n/fr.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Pour éviter toute interférence de session avec l'application mobile, utilisez si possible un compte iSmart distinct pour ioBroker. Partagez le véhicule avec ce compte et accordez les autorisations nécessaires à l'utilisation souhaitée.",
|
|
7
7
|
"Region": "Région",
|
|
8
8
|
"European Union": "Union européenne",
|
|
9
|
+
"China": "Chine",
|
|
10
|
+
"Australia": "Australie",
|
|
11
|
+
"Brazil": "Brésil",
|
|
12
|
+
"Israel": "Israël",
|
|
13
|
+
"Turkey": "Turquie",
|
|
14
|
+
"Thailand": "Thaïlande",
|
|
9
15
|
"Rest of World": "Reste du monde",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "La connexion au cloud n'est pas encore implémentée. Cette valeur ne sélectionne pas de service fonctionnel.",
|
|
11
16
|
"Login type": "Type de connexion",
|
|
12
17
|
"Email": "E-mail",
|
|
13
18
|
"Phone number": "Numéro de téléphone",
|
package/admin/i18n/it.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Per evitare possibili interferenze di sessione con l'app mobile, utilizza, se possibile, un account iSmart separato per ioBroker. Condividi il veicolo con quell'account e concedi le autorizzazioni necessarie per l'uso previsto.",
|
|
7
7
|
"Region": "Regione",
|
|
8
8
|
"European Union": "Unione europea",
|
|
9
|
+
"China": "Cina",
|
|
10
|
+
"Australia": "Australia",
|
|
11
|
+
"Brazil": "Brasile",
|
|
12
|
+
"Israel": "Israele",
|
|
13
|
+
"Turkey": "Turchia",
|
|
14
|
+
"Thailand": "Thailandia",
|
|
9
15
|
"Rest of World": "Resto del mondo",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "La connessione cloud non è ancora implementata. Questo valore non seleziona un servizio funzionante.",
|
|
11
16
|
"Login type": "Tipo di accesso",
|
|
12
17
|
"Email": "E-mail",
|
|
13
18
|
"Phone number": "Numero di telefono",
|
package/admin/i18n/nl.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Gebruik waar mogelijk een apart iSmart-account voor ioBroker om mogelijke sessieproblemen met de mobiele app te voorkomen. Deel het voertuig met dat account en verleen de benodigde rechten voor het beoogde gebruik.",
|
|
7
7
|
"Region": "Regio",
|
|
8
8
|
"European Union": "Europese Unie",
|
|
9
|
+
"China": "China",
|
|
10
|
+
"Australia": "Australië",
|
|
11
|
+
"Brazil": "Brazilië",
|
|
12
|
+
"Israel": "Israël",
|
|
13
|
+
"Turkey": "Turkije",
|
|
14
|
+
"Thailand": "Thailand",
|
|
9
15
|
"Rest of World": "Rest van de wereld",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Cloudverbinding is nog niet geïmplementeerd. Deze waarde selecteert geen werkende backend.",
|
|
11
16
|
"Login type": "Aanmeldtype",
|
|
12
17
|
"Email": "E-mailadres",
|
|
13
18
|
"Phone number": "Telefoonnummer",
|
package/admin/i18n/pl.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Aby uniknąć możliwych konfliktów sesji z aplikacją mobilną, w miarę możliwości użyj osobnego konta iSmart dla ioBroker. Udostępnij temu kontu pojazd i nadaj uprawnienia wymagane do planowanego użycia.",
|
|
7
7
|
"Region": "Region",
|
|
8
8
|
"European Union": "Unia Europejska",
|
|
9
|
+
"China": "Chiny",
|
|
10
|
+
"Australia": "Australia",
|
|
11
|
+
"Brazil": "Brazylia",
|
|
12
|
+
"Israel": "Izrael",
|
|
13
|
+
"Turkey": "Turcja",
|
|
14
|
+
"Thailand": "Tajlandia",
|
|
9
15
|
"Rest of World": "Reszta świata",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Połączenie z chmurą nie jest jeszcze zaimplementowane. Ta wartość nie wybiera działającej usługi.",
|
|
11
16
|
"Login type": "Typ logowania",
|
|
12
17
|
"Email": "E-mail",
|
|
13
18
|
"Phone number": "Numer telefonu",
|
package/admin/i18n/pt.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Para evitar possíveis interferências de sessão com a aplicação móvel, utilize, sempre que possível, uma conta iSmart separada para o ioBroker. Partilhe o veículo com essa conta e conceda as permissões necessárias para a utilização pretendida.",
|
|
7
7
|
"Region": "Região",
|
|
8
8
|
"European Union": "União Europeia",
|
|
9
|
+
"China": "China",
|
|
10
|
+
"Australia": "Austrália",
|
|
11
|
+
"Brazil": "Brasil",
|
|
12
|
+
"Israel": "Israel",
|
|
13
|
+
"Turkey": "Turquia",
|
|
14
|
+
"Thailand": "Tailândia",
|
|
9
15
|
"Rest of World": "Resto do mundo",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "A ligação à nuvem ainda não está implementada. Este valor não seleciona um serviço funcional.",
|
|
11
16
|
"Login type": "Tipo de início de sessão",
|
|
12
17
|
"Email": "E-mail",
|
|
13
18
|
"Phone number": "Número de telefone",
|
package/admin/i18n/ru.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Чтобы избежать возможных конфликтов сеанса с мобильным приложением, по возможности используйте отдельную учетную запись iSmart для ioBroker. Предоставьте этой учетной записи доступ к автомобилю и необходимые для использования разрешения.",
|
|
7
7
|
"Region": "Регион",
|
|
8
8
|
"European Union": "Европейский союз",
|
|
9
|
+
"China": "Китай",
|
|
10
|
+
"Australia": "Австралия",
|
|
11
|
+
"Brazil": "Бразилия",
|
|
12
|
+
"Israel": "Израиль",
|
|
13
|
+
"Turkey": "Турция",
|
|
14
|
+
"Thailand": "Таиланд",
|
|
9
15
|
"Rest of World": "Остальной мир",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Подключение к облаку пока не реализовано. Это значение не выбирает работающий сервер.",
|
|
11
16
|
"Login type": "Тип входа",
|
|
12
17
|
"Email": "Электронная почта",
|
|
13
18
|
"Phone number": "Номер телефона",
|
package/admin/i18n/uk.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "Щоб уникнути можливих конфліктів сеансів із мобільним застосунком, за можливості використовуйте окремий обліковий запис iSmart для ioBroker. Надайте йому доступ до автомобіля та необхідні для використання дозволи.",
|
|
7
7
|
"Region": "Регіон",
|
|
8
8
|
"European Union": "Європейський Союз",
|
|
9
|
+
"China": "Китай",
|
|
10
|
+
"Australia": "Австралія",
|
|
11
|
+
"Brazil": "Бразилія",
|
|
12
|
+
"Israel": "Ізраїль",
|
|
13
|
+
"Turkey": "Туреччина",
|
|
14
|
+
"Thailand": "Таїланд",
|
|
9
15
|
"Rest of World": "Решта світу",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "Підключення до хмари ще не реалізовано. Це значення не вибирає робочий сервер.",
|
|
11
16
|
"Login type": "Тип входу",
|
|
12
17
|
"Email": "Електронна пошта",
|
|
13
18
|
"Phone number": "Номер телефону",
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
"To avoid possible session interference with the mobile app, use a separate iSmart account for ioBroker where possible. Share the vehicle with that account and grant the permissions needed for your intended use.": "为避免与手机应用的会话发生冲突,请尽可能为 ioBroker 使用单独的 iSmart 账户。将车辆共享给该账户,并授予预期用途所需的权限。",
|
|
7
7
|
"Region": "地区",
|
|
8
8
|
"European Union": "欧盟",
|
|
9
|
+
"China": "中国",
|
|
10
|
+
"Australia": "澳大利亚",
|
|
11
|
+
"Brazil": "巴西",
|
|
12
|
+
"Israel": "以色列",
|
|
13
|
+
"Turkey": "土耳其",
|
|
14
|
+
"Thailand": "泰国",
|
|
9
15
|
"Rest of World": "其他地区",
|
|
10
|
-
"Cloud connectivity is not implemented yet. This value does not select a working backend.": "云连接尚未实现。此选项不会选择可用的后端服务。",
|
|
11
16
|
"Login type": "登录类型",
|
|
12
17
|
"Email": "电子邮箱",
|
|
13
18
|
"Phone number": "电话号码",
|
package/build/main.js
CHANGED
|
@@ -5,6 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
8
12
|
var __copyProps = (to, from, except, desc) => {
|
|
9
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
14
|
for (let key of __getOwnPropNames(from))
|
|
@@ -21,6 +25,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
25
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
26
|
mod
|
|
23
27
|
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var main_exports = {};
|
|
30
|
+
__export(main_exports, {
|
|
31
|
+
calculateInitializationRetryDelayMs: () => calculateInitializationRetryDelayMs
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(main_exports);
|
|
24
34
|
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
25
35
|
var import_promises = require("node:fs/promises");
|
|
26
36
|
var import_node_path = require("node:path");
|
|
@@ -28,6 +38,8 @@ var import_config = require("./config");
|
|
|
28
38
|
var import_saic_client = require("./saic-client");
|
|
29
39
|
var import_regions = require("./regions");
|
|
30
40
|
var import_refresh_command = require("./refresh-command");
|
|
41
|
+
var import_refresh_poll_queue = require("./refresh-poll-queue");
|
|
42
|
+
var import_refresh_poll_timer = require("./refresh-poll-timer");
|
|
31
43
|
var import_pending_setting_edits = require("./pending-setting-edits");
|
|
32
44
|
var import_status_metadata = require("./status-metadata");
|
|
33
45
|
var import_runtime_translations = require("./runtime-translations");
|
|
@@ -108,19 +120,34 @@ const settingInputsByAction = {
|
|
|
108
120
|
};
|
|
109
121
|
const EXPOSE_SOURCE_STATUS_DATAPOINTS = false;
|
|
110
122
|
const PENDING_SETTING_EDITS_FILE = "pending-setting-edits.json";
|
|
123
|
+
const INITIALIZATION_RETRY_DELAY_MS = 6e4;
|
|
124
|
+
const MAX_INITIALIZATION_RETRY_DELAY_MS = 5 * INITIALIZATION_RETRY_DELAY_MS;
|
|
125
|
+
function calculateInitializationRetryDelayMs(retryCount) {
|
|
126
|
+
const attempts = Math.max(1, retryCount);
|
|
127
|
+
if (attempts <= 3) {
|
|
128
|
+
return INITIALIZATION_RETRY_DELAY_MS * attempts;
|
|
129
|
+
}
|
|
130
|
+
return MAX_INITIALIZATION_RETRY_DELAY_MS;
|
|
131
|
+
}
|
|
111
132
|
class MgSaic extends utils.Adapter {
|
|
112
133
|
constructor(options = {}) {
|
|
113
134
|
super({
|
|
114
135
|
...options,
|
|
115
136
|
name: "mg-saic"
|
|
116
137
|
});
|
|
138
|
+
this.pollTimer = new import_refresh_poll_timer.RefreshPollTimer(
|
|
139
|
+
(callback, milliseconds) => this.setTimeout(callback, milliseconds),
|
|
140
|
+
(timer) => this.clearTimeout(timer)
|
|
141
|
+
);
|
|
117
142
|
this.on("ready", this.onReady.bind(this));
|
|
118
143
|
this.on("stateChange", this.onStateChange.bind(this));
|
|
119
144
|
this.on("unload", this.onUnload.bind(this));
|
|
120
145
|
}
|
|
121
146
|
pollTimer;
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
pollIntervalMilliseconds;
|
|
148
|
+
initializationRetryCount = 0;
|
|
149
|
+
initializationRetryTimer;
|
|
150
|
+
refreshPollQueue = new import_refresh_poll_queue.RefreshPollQueue();
|
|
124
151
|
stopping = false;
|
|
125
152
|
unloadCallbackCalled = false;
|
|
126
153
|
abortController = new AbortController();
|
|
@@ -129,6 +156,7 @@ class MgSaic extends utils.Adapter {
|
|
|
129
156
|
accessToken;
|
|
130
157
|
accessTokenExpiresAt = 0;
|
|
131
158
|
vehicles = [];
|
|
159
|
+
vehicleStatusFieldIds = /* @__PURE__ */ new Map();
|
|
132
160
|
lastStatusTimes = /* @__PURE__ */ new Map();
|
|
133
161
|
lastCanonicalStatusTimes = /* @__PURE__ */ new Map();
|
|
134
162
|
vehicleCommandsInProgress = /* @__PURE__ */ new Set();
|
|
@@ -236,6 +264,7 @@ class MgSaic extends utils.Adapter {
|
|
|
236
264
|
await this.setState("info.connection", false, true);
|
|
237
265
|
await this.setState("info.lastError", message, true);
|
|
238
266
|
this.log.warn(message);
|
|
267
|
+
this.scheduleInitializationRetry();
|
|
239
268
|
return;
|
|
240
269
|
}
|
|
241
270
|
try {
|
|
@@ -256,17 +285,16 @@ class MgSaic extends utils.Adapter {
|
|
|
256
285
|
this.log.warn(`Could not restore pending setting edits: ${details}`);
|
|
257
286
|
}
|
|
258
287
|
await this.setState("info.connection", true, true);
|
|
288
|
+
this.initializationRetryCount = 0;
|
|
259
289
|
this.log.info(`SAIC account authenticated; discovered ${this.vehicles.length} vehicle(s).`);
|
|
260
290
|
if (this.vehicles.length === 0) {
|
|
261
291
|
await this.setState("info.lastError", "No vehicles are linked to this SAIC account", true);
|
|
262
292
|
this.log.warn("No vehicles are linked to this SAIC account.");
|
|
263
293
|
return;
|
|
264
294
|
}
|
|
265
|
-
await this.refreshVehicleStatuses();
|
|
266
295
|
const intervalSeconds = (_e = config.pollIntervalSeconds) != null ? _e : import_config.DEFAULT_POLL_INTERVAL_SECONDS;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
296
|
+
this.pollIntervalMilliseconds = intervalSeconds * 1e3;
|
|
297
|
+
await this.refreshVehicleStatuses();
|
|
270
298
|
} catch (error) {
|
|
271
299
|
if (this.stopping) {
|
|
272
300
|
return;
|
|
@@ -276,6 +304,25 @@ class MgSaic extends utils.Adapter {
|
|
|
276
304
|
this.log.error(`Could not initialize vehicle objects: ${details}`);
|
|
277
305
|
}
|
|
278
306
|
}
|
|
307
|
+
scheduleInitializationRetry() {
|
|
308
|
+
if (this.stopping) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.initializationRetryCount += 1;
|
|
312
|
+
const retryDelayMs = calculateInitializationRetryDelayMs(this.initializationRetryCount);
|
|
313
|
+
if (this.initializationRetryTimer !== void 0) {
|
|
314
|
+
this.clearTimeout(this.initializationRetryTimer);
|
|
315
|
+
}
|
|
316
|
+
this.log.info(
|
|
317
|
+
`Retrying SAIC initialization in ${retryDelayMs / 1e3} seconds (${this.initializationRetryCount})`
|
|
318
|
+
);
|
|
319
|
+
this.initializationRetryTimer = this.setTimeout(() => {
|
|
320
|
+
this.initializationRetryTimer = void 0;
|
|
321
|
+
if (!this.stopping) {
|
|
322
|
+
void this.onReady();
|
|
323
|
+
}
|
|
324
|
+
}, retryDelayMs);
|
|
325
|
+
}
|
|
279
326
|
async authenticate() {
|
|
280
327
|
if (!this.client || !this.loginOptions || this.stopping) {
|
|
281
328
|
throw new Error("Adapter is stopping or not initialized");
|
|
@@ -289,26 +336,31 @@ class MgSaic extends utils.Adapter {
|
|
|
289
336
|
await this.setState("info.connection", true, true);
|
|
290
337
|
}
|
|
291
338
|
triggerRefresh() {
|
|
339
|
+
this.pollTimer.clear();
|
|
292
340
|
void this.refreshVehicleStatuses().catch((error) => {
|
|
293
341
|
const details = error instanceof Error ? error.message : "Unknown refresh error";
|
|
294
342
|
this.log.error(`Vehicle status refresh failed: ${details}`);
|
|
295
343
|
});
|
|
296
344
|
}
|
|
345
|
+
scheduleNextPoll() {
|
|
346
|
+
if (this.stopping || this.pollIntervalMilliseconds === void 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.pollTimer.schedule(() => this.triggerRefresh(), this.pollIntervalMilliseconds);
|
|
350
|
+
}
|
|
297
351
|
async refreshVehicleStatuses() {
|
|
298
352
|
var _a;
|
|
299
353
|
if (this.stopping || !this.client || !this.loginOptions || !this.accessToken) {
|
|
300
354
|
return;
|
|
301
355
|
}
|
|
302
|
-
if (this.
|
|
303
|
-
this.refreshQueued = true;
|
|
356
|
+
if (!this.refreshPollQueue.request()) {
|
|
304
357
|
return;
|
|
305
358
|
}
|
|
306
|
-
this.pollInProgress = true;
|
|
307
359
|
const statusErrors = [];
|
|
308
360
|
let followUpUsed = false;
|
|
309
361
|
try {
|
|
310
362
|
do {
|
|
311
|
-
this.
|
|
363
|
+
this.refreshPollQueue.clearQueuedRefresh();
|
|
312
364
|
for (const vehicle of this.vehicles) {
|
|
313
365
|
if (this.stopping) {
|
|
314
366
|
break;
|
|
@@ -375,7 +427,7 @@ class MgSaic extends utils.Adapter {
|
|
|
375
427
|
}
|
|
376
428
|
} else {
|
|
377
429
|
const details = error instanceof Error ? error.message : "Unknown error";
|
|
378
|
-
const diagnostic = error instanceof import_saic_client.SaicRemoteInstructionError ? `SAIC/MG returned remote instruction code 4 for ${vehicle.vin}; this is a response from the MG/SAIC service or vehicle, not an adapter parsing/processing error.
|
|
430
|
+
const diagnostic = error instanceof import_saic_client.SaicRemoteInstructionError ? `SAIC/MG returned remote instruction code 4 for ${vehicle.vin}; this is a response from the MG/SAIC service or vehicle, not an adapter parsing/processing error. Diagnostic: ${details}` : details;
|
|
379
431
|
statusErrors.push(`Vehicle status request failed for ${vehicle.vin}: ${diagnostic}`);
|
|
380
432
|
this.log.warn(
|
|
381
433
|
`Could not retrieve current status for vehicle ${vehicle.vin}: ${diagnostic}`
|
|
@@ -478,17 +530,21 @@ class MgSaic extends utils.Adapter {
|
|
|
478
530
|
this.log.error(`Could not write live status for ${vehicle.vin}: ${details}`);
|
|
479
531
|
}
|
|
480
532
|
}
|
|
481
|
-
if (this.
|
|
482
|
-
this.
|
|
483
|
-
} else if (this.
|
|
533
|
+
if (this.refreshPollQueue.hasQueuedRefresh && followUpUsed) {
|
|
534
|
+
this.refreshPollQueue.clearQueuedRefresh();
|
|
535
|
+
} else if (this.refreshPollQueue.hasQueuedRefresh) {
|
|
484
536
|
followUpUsed = true;
|
|
485
537
|
}
|
|
486
|
-
} while (this.
|
|
538
|
+
} while (this.refreshPollQueue.hasQueuedRefresh && !this.stopping);
|
|
487
539
|
if (!this.stopping) {
|
|
488
540
|
await this.setState("info.lastError", statusErrors.join("; "), true);
|
|
489
541
|
}
|
|
490
542
|
} finally {
|
|
491
|
-
this.
|
|
543
|
+
this.refreshPollQueue.finish(
|
|
544
|
+
this.stopping,
|
|
545
|
+
() => this.triggerRefresh(),
|
|
546
|
+
() => this.scheduleNextPoll()
|
|
547
|
+
);
|
|
492
548
|
}
|
|
493
549
|
}
|
|
494
550
|
onStateChange(id, state) {
|
|
@@ -514,7 +570,9 @@ class MgSaic extends utils.Adapter {
|
|
|
514
570
|
({ vin }) => settingAction && eventId === `${this.namespace}.${vehicleCommandPath(vin, settingAction.id)}`
|
|
515
571
|
);
|
|
516
572
|
if (settingAction && settingVehicle) {
|
|
517
|
-
|
|
573
|
+
if (state.val === true) {
|
|
574
|
+
void this.executeVehicleSetting(settingVehicle.vin, settingAction.id);
|
|
575
|
+
}
|
|
518
576
|
return;
|
|
519
577
|
}
|
|
520
578
|
const settingInput = this.vehicles.flatMap(
|
|
@@ -801,6 +859,21 @@ class MgSaic extends utils.Adapter {
|
|
|
801
859
|
this.vehicleCommandsInProgress.delete(vin);
|
|
802
860
|
}
|
|
803
861
|
}
|
|
862
|
+
async getVehicleStatusFieldIds(channelId) {
|
|
863
|
+
const cachedIds = this.vehicleStatusFieldIds.get(channelId);
|
|
864
|
+
if (cachedIds) {
|
|
865
|
+
return cachedIds;
|
|
866
|
+
}
|
|
867
|
+
const objectIdPrefix = `${this.namespace}.${channelId}.`;
|
|
868
|
+
const objects = await this.getObjectListAsync({
|
|
869
|
+
startkey: objectIdPrefix,
|
|
870
|
+
endkey: `${objectIdPrefix}\u9999`
|
|
871
|
+
});
|
|
872
|
+
const stateIds = objects.rows.filter((row) => row.value.type === "state" && row.id.startsWith(`${this.namespace}.`)).map((row) => row.id.slice(this.namespace.length + 1));
|
|
873
|
+
const fieldIds = (0, import_status_metadata.indexVehicleStatusFieldIds)(stateIds, channelId);
|
|
874
|
+
this.vehicleStatusFieldIds.set(channelId, fieldIds);
|
|
875
|
+
return fieldIds;
|
|
876
|
+
}
|
|
804
877
|
async createVehicleStatusObjects(vin, status, channel = "status", channelName = "currentStatus") {
|
|
805
878
|
if (this.stopping) {
|
|
806
879
|
return;
|
|
@@ -809,10 +882,6 @@ class MgSaic extends utils.Adapter {
|
|
|
809
882
|
return;
|
|
810
883
|
}
|
|
811
884
|
const statusId = `vehicles.${vin}.${channel}`;
|
|
812
|
-
for (const key of import_status_metadata.excludedVehicleStatusFields) {
|
|
813
|
-
const stateId = `${statusId}.${(0, import_status_metadata.getVehicleStatusGroup)(key)}.${key}`;
|
|
814
|
-
await this.delObjectAsync(stateId);
|
|
815
|
-
}
|
|
816
885
|
await this.setObjectNotExistsAsync(statusId, {
|
|
817
886
|
type: "channel",
|
|
818
887
|
common: { name: (0, import_runtime_translations.getRuntimeName)(channelName) },
|
|
@@ -822,6 +891,11 @@ class MgSaic extends utils.Adapter {
|
|
|
822
891
|
if (this.stopping) {
|
|
823
892
|
return;
|
|
824
893
|
}
|
|
894
|
+
const fieldIds = await this.getVehicleStatusFieldIds(statusId);
|
|
895
|
+
for (const key of import_status_metadata.excludedVehicleStatusFields) {
|
|
896
|
+
const stateId = (0, import_status_metadata.resolveVehicleStatusFieldId)(fieldIds, statusId, (0, import_status_metadata.getVehicleStatusGroup)(key), key);
|
|
897
|
+
await this.delObjectAsync(stateId);
|
|
898
|
+
}
|
|
825
899
|
const values = { ...status.basicVehicleStatus };
|
|
826
900
|
if (status.statusTime !== void 0) {
|
|
827
901
|
values.statusTime = status.statusTime;
|
|
@@ -834,15 +908,17 @@ class MgSaic extends utils.Adapter {
|
|
|
834
908
|
if (this.stopping) {
|
|
835
909
|
return;
|
|
836
910
|
}
|
|
837
|
-
|
|
838
|
-
|
|
911
|
+
const stateId = (0, import_status_metadata.resolveVehicleStatusFieldId)(fieldIds, statusId, (0, import_status_metadata.getVehicleStatusGroup)(key), key);
|
|
912
|
+
const separator = stateId.lastIndexOf(".");
|
|
913
|
+
const fieldName = stateId.slice(separator + 1);
|
|
914
|
+
const groupId = stateId.slice(0, separator);
|
|
915
|
+
const group = groupId.slice(statusId.length + 1);
|
|
916
|
+
if (import_status_metadata.excludedVehicleStatusFields.some((excludedKey) => excludedKey.toLowerCase() === fieldName.toLowerCase())) {
|
|
917
|
+
await this.delObjectAsync(stateId);
|
|
839
918
|
continue;
|
|
840
919
|
}
|
|
841
|
-
const group = (0, import_status_metadata.getVehicleStatusGroup)(key);
|
|
842
|
-
const groupId = `${statusId}.${group}`;
|
|
843
|
-
const stateId = `${groupId}.${key}`;
|
|
844
920
|
if (value === void 0) {
|
|
845
|
-
if ((0, import_status_metadata.shouldDeleteUnavailableStatusValue)(
|
|
921
|
+
if ((0, import_status_metadata.shouldDeleteUnavailableStatusValue)(fieldName)) {
|
|
846
922
|
await this.delObjectAsync(stateId);
|
|
847
923
|
} else {
|
|
848
924
|
const previousState = await this.getStateAsync(stateId);
|
|
@@ -852,7 +928,7 @@ class MgSaic extends utils.Adapter {
|
|
|
852
928
|
}
|
|
853
929
|
continue;
|
|
854
930
|
}
|
|
855
|
-
const normalizedValue = (0, import_status_metadata.normalizeVehicleStatusValue)(
|
|
931
|
+
const normalizedValue = (0, import_status_metadata.normalizeVehicleStatusValue)(fieldName, value);
|
|
856
932
|
if (normalizedValue === void 0) {
|
|
857
933
|
await this.delObjectAsync(stateId);
|
|
858
934
|
continue;
|
|
@@ -866,7 +942,7 @@ class MgSaic extends utils.Adapter {
|
|
|
866
942
|
if (this.stopping) {
|
|
867
943
|
return;
|
|
868
944
|
}
|
|
869
|
-
const metadata = (0, import_status_metadata.getVehicleStatusMetadata)(
|
|
945
|
+
const metadata = (0, import_status_metadata.getVehicleStatusMetadata)(fieldName);
|
|
870
946
|
const common = {
|
|
871
947
|
name: metadata.name,
|
|
872
948
|
type: metadata.type,
|
|
@@ -895,7 +971,7 @@ class MgSaic extends utils.Adapter {
|
|
|
895
971
|
if (this.stopping) {
|
|
896
972
|
return;
|
|
897
973
|
}
|
|
898
|
-
const seatSetting =
|
|
974
|
+
const seatSetting = fieldName === "frontLeftSeatHeatLevel" ? "driverSeatHeatLevel" : fieldName === "frontRightSeatHeatLevel" ? "passengerSeatHeatLevel" : void 0;
|
|
899
975
|
if (seatSetting && Number.isInteger(value) && value >= 0 && value <= 3) {
|
|
900
976
|
const settingId = vehicleCommandPath(vin, seatSetting);
|
|
901
977
|
const settingState = await this.getStateAsync(settingId);
|
|
@@ -903,12 +979,32 @@ class MgSaic extends utils.Adapter {
|
|
|
903
979
|
await this.setStateAsync(settingId, { val: value, ack: true });
|
|
904
980
|
}
|
|
905
981
|
}
|
|
906
|
-
await this.delObjectAsync(`${statusId}.${
|
|
982
|
+
await this.delObjectAsync(`${statusId}.${fieldName}`);
|
|
907
983
|
}
|
|
908
984
|
}
|
|
909
985
|
async createVehicleChargingObjects(vin, charging, series) {
|
|
986
|
+
var _a, _b;
|
|
910
987
|
await this.ensureVehicleChargingGroups(vin);
|
|
911
988
|
const rootId = `vehicles.${vin}.status.charging`;
|
|
989
|
+
const existingObjects = await this.getObjectListAsync({
|
|
990
|
+
startkey: `${this.namespace}.${rootId}.`,
|
|
991
|
+
endkey: `${this.namespace}.${rootId}.\u9999`
|
|
992
|
+
});
|
|
993
|
+
const existingFieldIds = new Map(chargingGroups.map(({ id }) => [id, []]));
|
|
994
|
+
for (const { id: objectId, value: object } of existingObjects.rows) {
|
|
995
|
+
if (object.type !== "state") {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
const group = chargingGroups.find(({ id }) => objectId.startsWith(`${this.namespace}.${rootId}.${id}.`));
|
|
999
|
+
if (!group) {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const prefix = `${this.namespace}.${rootId}.${group.id}.`;
|
|
1003
|
+
const fieldId = objectId.slice(prefix.length);
|
|
1004
|
+
if (fieldId && !fieldId.includes(".")) {
|
|
1005
|
+
(_a = existingFieldIds.get(group.id)) == null ? void 0 : _a.push(fieldId);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
912
1008
|
for (const { source, id } of chargingGroups) {
|
|
913
1009
|
const groupId = `${rootId}.${id}`;
|
|
914
1010
|
const excludedFields = id === "session" ? import_status_metadata.excludedChargingSessionFields : import_status_metadata.excludedChargingStatisticsFields;
|
|
@@ -916,27 +1012,34 @@ class MgSaic extends utils.Adapter {
|
|
|
916
1012
|
for (const field of excludedFields) {
|
|
917
1013
|
await this.delObjectAsync(`${groupId}.${field}`);
|
|
918
1014
|
}
|
|
919
|
-
|
|
1015
|
+
const valuesByField = new Map(fields);
|
|
1016
|
+
const resolvedFieldIds = (0, import_status_metadata.resolveChargingFieldIds)(
|
|
1017
|
+
fields.map(([field]) => field),
|
|
1018
|
+
(_b = existingFieldIds.get(id)) != null ? _b : []
|
|
1019
|
+
);
|
|
1020
|
+
for (const [responseField, field] of resolvedFieldIds) {
|
|
920
1021
|
if (this.stopping) {
|
|
921
1022
|
return;
|
|
922
1023
|
}
|
|
923
|
-
|
|
1024
|
+
const value = valuesByField.get(responseField);
|
|
1025
|
+
const canonicalField = (0, import_status_metadata.resolveChargingFieldName)(field);
|
|
1026
|
+
if (excludedFields.some((excludedField) => excludedField === canonicalField)) {
|
|
924
1027
|
await this.delObjectAsync(`${groupId}.${field}`);
|
|
925
1028
|
continue;
|
|
926
1029
|
}
|
|
927
1030
|
const stateId = `${groupId}.${field}`;
|
|
928
|
-
if (value === void 0 &&
|
|
1031
|
+
if (value === void 0 && canonicalField !== "realtimePower" && canonicalField !== "batteryAvailableEnergy") {
|
|
929
1032
|
await this.delObjectAsync(stateId);
|
|
930
1033
|
continue;
|
|
931
1034
|
}
|
|
932
|
-
const normalizedValue =
|
|
933
|
-
if (normalizedValue === void 0 &&
|
|
1035
|
+
const normalizedValue = canonicalField === "realtimePower" ? (0, import_status_metadata.calculateRealtimePower)(charging.chrgMgmtData.bmsPackCrnt, charging.chrgMgmtData.bmsPackVol) : value === void 0 ? void 0 : (0, import_status_metadata.normalizeChargingValue)(canonicalField, value, series);
|
|
1036
|
+
if (normalizedValue === void 0 && canonicalField !== "batteryAvailableEnergy") {
|
|
934
1037
|
await this.delObjectAsync(stateId);
|
|
935
1038
|
continue;
|
|
936
1039
|
}
|
|
937
|
-
const metadata = (0, import_status_metadata.getChargingFieldMetadata)(
|
|
1040
|
+
const metadata = (0, import_status_metadata.getChargingFieldMetadata)(canonicalField, series);
|
|
938
1041
|
const common = {
|
|
939
|
-
name: (0, import_runtime_translations.getChargingFieldName)(
|
|
1042
|
+
name: (0, import_runtime_translations.getChargingFieldName)(canonicalField),
|
|
940
1043
|
type: metadata.type === "boolean" ? "boolean" : typeof normalizedValue === "string" ? "string" : "number",
|
|
941
1044
|
role: typeof normalizedValue === "string" ? "text" : metadata.role,
|
|
942
1045
|
read: true,
|
|
@@ -1314,9 +1417,11 @@ class MgSaic extends utils.Adapter {
|
|
|
1314
1417
|
this.unloadCallbackCalled = true;
|
|
1315
1418
|
this.stopping = true;
|
|
1316
1419
|
this.abortController.abort(new DOMException("Adapter is stopping", "AbortError"));
|
|
1317
|
-
this.
|
|
1318
|
-
|
|
1319
|
-
|
|
1420
|
+
this.refreshPollQueue.clearQueuedRefresh();
|
|
1421
|
+
this.pollTimer.clear();
|
|
1422
|
+
if (this.initializationRetryTimer !== void 0) {
|
|
1423
|
+
this.clearTimeout(this.initializationRetryTimer);
|
|
1424
|
+
this.initializationRetryTimer = void 0;
|
|
1320
1425
|
}
|
|
1321
1426
|
callback();
|
|
1322
1427
|
}
|
|
@@ -1326,4 +1431,8 @@ if (require.main !== module) {
|
|
|
1326
1431
|
} else {
|
|
1327
1432
|
(() => new MgSaic())();
|
|
1328
1433
|
}
|
|
1434
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1435
|
+
0 && (module.exports = {
|
|
1436
|
+
calculateInitializationRetryDelayMs
|
|
1437
|
+
});
|
|
1329
1438
|
//# sourceMappingURL=main.js.map
|