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 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 but has not yet been added to the ioBroker adapter catalog.
39
+ The adapter is published on npm and available from ioBroker's Latest adapter repository.
40
40
 
41
41
  ## Support
42
42
 
43
43
  [![Donate](https://camo.githubusercontent.com/ef8c60305542390406855547e32850909812a73c4abada236de22a6dc88b313f/68747470733a2f2f7777772e70617970616c6f626a656374732e636f6d2f64655f44452f44452f692f62746e2f62746e5f646f6e61746543435f4c472e676966)](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
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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": "Номер телефона",
@@ -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": "Номер телефону",
@@ -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
- pollInProgress = false;
123
- refreshQueued = false;
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
- if (!this.stopping) {
268
- this.pollTimer = this.setInterval(() => this.triggerRefresh(), intervalSeconds * 1e3);
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.pollInProgress) {
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.refreshQueued = false;
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. SAIC message: ${details}` : details;
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.refreshQueued && followUpUsed) {
482
- this.refreshQueued = false;
483
- } else if (this.refreshQueued) {
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.refreshQueued && !this.stopping);
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.pollInProgress = false;
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
- void this.executeVehicleSetting(settingVehicle.vin, settingAction.id);
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
- if (import_status_metadata.excludedVehicleStatusFields.some((excludedKey) => excludedKey === key)) {
838
- await this.delObjectAsync(`${statusId}.${(0, import_status_metadata.getVehicleStatusGroup)(key)}.${key}`);
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)(key)) {
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)(key, value);
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)(key);
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 = key === "frontLeftSeatHeatLevel" ? "driverSeatHeatLevel" : key === "frontRightSeatHeatLevel" ? "passengerSeatHeatLevel" : void 0;
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}.${key}`);
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
- for (const [field, value] of fields) {
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
- if (excludedFields.some((excludedField) => excludedField === field)) {
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 && field !== "realtimePower" && field !== "batteryAvailableEnergy") {
1031
+ if (value === void 0 && canonicalField !== "realtimePower" && canonicalField !== "batteryAvailableEnergy") {
929
1032
  await this.delObjectAsync(stateId);
930
1033
  continue;
931
1034
  }
932
- const normalizedValue = field === "realtimePower" ? (0, import_status_metadata.calculateRealtimePower)(charging.chrgMgmtData.bmsPackCrnt, charging.chrgMgmtData.bmsPackVol) : value === void 0 ? void 0 : (0, import_status_metadata.normalizeChargingValue)(field, value, series);
933
- if (normalizedValue === void 0 && field !== "batteryAvailableEnergy") {
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)(field, series);
1040
+ const metadata = (0, import_status_metadata.getChargingFieldMetadata)(canonicalField, series);
938
1041
  const common = {
939
- name: (0, import_runtime_translations.getChargingFieldName)(field),
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.refreshQueued = false;
1318
- if (this.pollTimer) {
1319
- this.clearInterval(this.pollTimer);
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