iobroker.mg-saic 0.0.4 → 0.1.1

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
@@ -3,15 +3,17 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/iobroker.mg-saic.svg)](https://www.npmjs.com/package/iobroker.mg-saic)
4
4
  [![Downloads](https://img.shields.io/npm/dm/iobroker.mg-saic.svg)](https://www.npmjs.com/package/iobroker.mg-saic)
5
5
 
6
- <img src="MG.png" alt="MG" width="314">
6
+ <img src="admin/MG.png" alt="MG" width="314">
7
7
 
8
8
  The adapter is intended for connecting MG and SAIC vehicles to ioBroker through the iSmart cloud service. It is not affiliated with or endorsed by MG, SAIC, or iSmart.
9
9
 
10
+ For manufacturer information, see [MG Motor](https://www.mgmotor.eu/) and [SAIC Motor](https://www.saicmotor.com/).
11
+
10
12
  ## Setup
11
13
 
12
14
  ### Before you start
13
15
 
14
- - Node.js 24.0.0 or later is required.
16
+ - Node.js 22.22.2 or later is required.
15
17
  - ioBroker js-controller 7.2.2 or later is required.
16
18
  - ioBroker Admin 8.2.0 or later is required.
17
19
  - Use an iSmart account that has access to the vehicle. A separate account shared with the vehicle is recommended to avoid interfering with the mobile app session.
@@ -23,7 +25,7 @@ The adapter is intended for connecting MG and SAIC vehicles to ioBroker through
23
25
  1. Install the adapter and create an instance in ioBroker Admin.
24
26
  2. Select the account region and login type (email or phone number).
25
27
  3. Enter the account's email address or phone number and password. For phone login, also enter the phone country calling code.
26
- 4. Set the status polling interval in seconds. The default is 1800 seconds.
28
+ 4. Set the status polling interval >60 seconds. The default is 1800 seconds.
27
29
  5. Save the settings and start the instance. Vehicles available to the account appear under `vehicles.<VIN>`.
28
30
 
29
31
  ## Features
@@ -41,6 +43,17 @@ The adapter is published on npm but has not yet been added to the ioBroker adapt
41
43
  [![Donate](https://camo.githubusercontent.com/ef8c60305542390406855547e32850909812a73c4abada236de22a6dc88b313f/68747470733a2f2f7777772e70617970616c6f626a656374732e636f6d2f64655f44452f44452f692f62746e2f62746e5f646f6e61746543435f4c472e676966)](https://www.paypal.com/donate/?hosted_button_id=3RJQGSJNRSHUJ)
42
44
 
43
45
  ## Changelog
46
+ ### 0.1.1 (2026-11-07)
47
+
48
+ - (Voodoo2man) Lowered the minimum Node.js requirement to 22.22.2.
49
+ - (Voodoo2man) Improved charging datapoint labels and units, enforced safe polling interval limits, and added manufacturer references.
50
+
51
+ ### 0.1.0 (2026-11-01)
52
+
53
+ - (Voodoo2man) Reported real-time charging power in kW using the pack current and voltage.
54
+ - (Voodoo2man) Applied confirmed profile-specific charging conversions.
55
+ - (Voodoo2man) Calculated available battery energy from battery level and usable capacity.
56
+
44
57
  ### 0.0.4 (2026-10-24)
45
58
 
46
59
  - (Voodoo2man) Corrected GPS coordinates.
@@ -60,10 +73,6 @@ Older changelog entries are available in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
60
73
 
61
74
  - (Voodoo2man) Added regional iSmart account setup, vehicle status, and supported remote controls.
62
75
 
63
- ### 0.0.1 (2026-10-20)
64
-
65
- - (Voodoo2man) Initial Version
66
-
67
76
  ## License
68
77
 
69
78
  MIT License
@@ -110,7 +110,8 @@
110
110
  "type": "number",
111
111
  "label": "Status polling interval (seconds)",
112
112
  "default": 1800,
113
- "min": 1,
113
+ "min": 60,
114
+ "max": 2147483,
114
115
  "step": 1,
115
116
  "xs": 12,
116
117
  "sm": 12,
package/build/config.js CHANGED
@@ -19,16 +19,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var config_exports = {};
20
20
  __export(config_exports, {
21
21
  DEFAULT_POLL_INTERVAL_SECONDS: () => DEFAULT_POLL_INTERVAL_SECONDS,
22
+ MAX_POLL_INTERVAL_SECONDS: () => MAX_POLL_INTERVAL_SECONDS,
22
23
  validateConfig: () => validateConfig
23
24
  });
24
25
  module.exports = __toCommonJS(config_exports);
25
26
  var import_regions = require("./regions");
26
27
  const DEFAULT_POLL_INTERVAL_SECONDS = 1800;
28
+ const MAX_POLL_INTERVAL_SECONDS = 2147483;
27
29
  function validateConfig(config) {
28
30
  var _a, _b, _c;
29
31
  const errors = [];
30
- if (config.pollIntervalSeconds !== void 0 && (!Number.isInteger(config.pollIntervalSeconds) || config.pollIntervalSeconds < 1)) {
31
- errors.push("Polling interval must be a positive integer number of seconds");
32
+ if (config.pollIntervalSeconds !== void 0 && (!Number.isInteger(config.pollIntervalSeconds) || config.pollIntervalSeconds < 60 || config.pollIntervalSeconds > MAX_POLL_INTERVAL_SECONDS)) {
33
+ errors.push(`Polling interval must be an integer between 60 and ${MAX_POLL_INTERVAL_SECONDS} seconds`);
32
34
  }
33
35
  if (!((_a = config.region) == null ? void 0 : _a.trim())) {
34
36
  errors.push("Region/backend code is required");
@@ -54,6 +56,7 @@ function validateConfig(config) {
54
56
  // Annotate the CommonJS export names for ESM import in node:
55
57
  0 && (module.exports = {
56
58
  DEFAULT_POLL_INTERVAL_SECONDS,
59
+ MAX_POLL_INTERVAL_SECONDS,
57
60
  validateConfig
58
61
  });
59
62
  //# sourceMappingURL=config.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/config.ts"],
4
- "sourcesContent": ["import { isIndiaRegion, resolveSaicRegion } from './regions';\n\n/** Settings entered for one iSmart account and backend. */\nexport interface MgSaicConfig {\n\t/** Automatic status polling interval in seconds. */\n\tpollIntervalSeconds?: number;\n\t/** Backend region code. */\n\tregion?: string;\n\t/** Whether the account identifier is an email address or phone number. */\n\taccountType?: string;\n\t/** Email address or phone number used to sign in. */\n\taccountIdentifier?: string;\n\t/** International calling code when using a phone account. */\n\tphoneCountryCode?: string;\n\t/** Account password, decrypted by ioBroker for the adapter process. */\n\tpassword?: string;\n}\n\nexport const DEFAULT_POLL_INTERVAL_SECONDS = 1800;\n\n/**\n * Returns configuration problems without logging any credential values.\n *\n * @param config The instance's native configuration.\n */\nexport function validateConfig(config: MgSaicConfig): string[] {\n\tconst errors: string[] = [];\n\tif (\n\t\tconfig.pollIntervalSeconds !== undefined &&\n\t\t(!Number.isInteger(config.pollIntervalSeconds) || config.pollIntervalSeconds < 1)\n\t) {\n\t\terrors.push('Polling interval must be a positive integer number of seconds');\n\t}\n\tif (!config.region?.trim()) {\n\t\terrors.push('Region/backend code is required');\n\t} else if (isIndiaRegion(config.region)) {\n\t\terrors.push('India TAP connectivity is not implemented yet');\n\t} else if (!resolveSaicRegion(config.region)) {\n\t\terrors.push('Unsupported SAIC region');\n\t}\n\tif (config.accountType !== 'email' && config.accountType !== 'phone') {\n\t\terrors.push('Login type must be email or phone');\n\t}\n\tif (!config.accountIdentifier?.trim()) {\n\t\terrors.push('Email address or phone number is required');\n\t}\n\tif (config.accountType === 'phone' && !config.phoneCountryCode?.trim()) {\n\t\terrors.push('Phone country calling code is required');\n\t}\n\tif (!config.password) {\n\t\terrors.push('Password is required');\n\t}\n\treturn errors;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiD;AAkB1C,MAAM,gCAAgC;AAOtC,SAAS,eAAe,QAAgC;AAzB/D;AA0BC,QAAM,SAAmB,CAAC;AAC1B,MACC,OAAO,wBAAwB,WAC9B,CAAC,OAAO,UAAU,OAAO,mBAAmB,KAAK,OAAO,sBAAsB,IAC9E;AACD,WAAO,KAAK,+DAA+D;AAAA,EAC5E;AACA,MAAI,GAAC,YAAO,WAAP,mBAAe,SAAQ;AAC3B,WAAO,KAAK,iCAAiC;AAAA,EAC9C,eAAW,8BAAc,OAAO,MAAM,GAAG;AACxC,WAAO,KAAK,+CAA+C;AAAA,EAC5D,WAAW,KAAC,kCAAkB,OAAO,MAAM,GAAG;AAC7C,WAAO,KAAK,yBAAyB;AAAA,EACtC;AACA,MAAI,OAAO,gBAAgB,WAAW,OAAO,gBAAgB,SAAS;AACrE,WAAO,KAAK,mCAAmC;AAAA,EAChD;AACA,MAAI,GAAC,YAAO,sBAAP,mBAA0B,SAAQ;AACtC,WAAO,KAAK,2CAA2C;AAAA,EACxD;AACA,MAAI,OAAO,gBAAgB,WAAW,GAAC,YAAO,qBAAP,mBAAyB,SAAQ;AACvE,WAAO,KAAK,wCAAwC;AAAA,EACrD;AACA,MAAI,CAAC,OAAO,UAAU;AACrB,WAAO,KAAK,sBAAsB;AAAA,EACnC;AACA,SAAO;AACR;",
4
+ "sourcesContent": ["import { isIndiaRegion, resolveSaicRegion } from './regions';\n\n/** Settings entered for one iSmart account and backend. */\nexport interface MgSaicConfig {\n\t/** Automatic status polling interval in seconds. */\n\tpollIntervalSeconds?: number;\n\t/** Backend region code. */\n\tregion?: string;\n\t/** Whether the account identifier is an email address or phone number. */\n\taccountType?: string;\n\t/** Email address or phone number used to sign in. */\n\taccountIdentifier?: string;\n\t/** International calling code when using a phone account. */\n\tphoneCountryCode?: string;\n\t/** Account password, decrypted by ioBroker for the adapter process. */\n\tpassword?: string;\n}\n\nexport const DEFAULT_POLL_INTERVAL_SECONDS = 1800;\nexport const MAX_POLL_INTERVAL_SECONDS = 2_147_483;\n\n/**\n * Returns configuration problems without logging any credential values.\n *\n * @param config The instance's native configuration.\n */\nexport function validateConfig(config: MgSaicConfig): string[] {\n\tconst errors: string[] = [];\n\tif (\n\t\tconfig.pollIntervalSeconds !== undefined &&\n\t\t(!Number.isInteger(config.pollIntervalSeconds) ||\n\t\t\tconfig.pollIntervalSeconds < 60 ||\n\t\t\tconfig.pollIntervalSeconds > MAX_POLL_INTERVAL_SECONDS)\n\t) {\n\t\terrors.push(`Polling interval must be an integer between 60 and ${MAX_POLL_INTERVAL_SECONDS} seconds`);\n\t}\n\tif (!config.region?.trim()) {\n\t\terrors.push('Region/backend code is required');\n\t} else if (isIndiaRegion(config.region)) {\n\t\terrors.push('India TAP connectivity is not implemented yet');\n\t} else if (!resolveSaicRegion(config.region)) {\n\t\terrors.push('Unsupported SAIC region');\n\t}\n\tif (config.accountType !== 'email' && config.accountType !== 'phone') {\n\t\terrors.push('Login type must be email or phone');\n\t}\n\tif (!config.accountIdentifier?.trim()) {\n\t\terrors.push('Email address or phone number is required');\n\t}\n\tif (config.accountType === 'phone' && !config.phoneCountryCode?.trim()) {\n\t\terrors.push('Phone country calling code is required');\n\t}\n\tif (!config.password) {\n\t\terrors.push('Password is required');\n\t}\n\treturn errors;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiD;AAkB1C,MAAM,gCAAgC;AACtC,MAAM,4BAA4B;AAOlC,SAAS,eAAe,QAAgC;AA1B/D;AA2BC,QAAM,SAAmB,CAAC;AAC1B,MACC,OAAO,wBAAwB,WAC9B,CAAC,OAAO,UAAU,OAAO,mBAAmB,KAC5C,OAAO,sBAAsB,MAC7B,OAAO,sBAAsB,4BAC7B;AACD,WAAO,KAAK,sDAAsD,yBAAyB,UAAU;AAAA,EACtG;AACA,MAAI,GAAC,YAAO,WAAP,mBAAe,SAAQ;AAC3B,WAAO,KAAK,iCAAiC;AAAA,EAC9C,eAAW,8BAAc,OAAO,MAAM,GAAG;AACxC,WAAO,KAAK,+CAA+C;AAAA,EAC5D,WAAW,KAAC,kCAAkB,OAAO,MAAM,GAAG;AAC7C,WAAO,KAAK,yBAAyB;AAAA,EACtC;AACA,MAAI,OAAO,gBAAgB,WAAW,OAAO,gBAAgB,SAAS;AACrE,WAAO,KAAK,mCAAmC;AAAA,EAChD;AACA,MAAI,GAAC,YAAO,sBAAP,mBAA0B,SAAQ;AACtC,WAAO,KAAK,2CAA2C;AAAA,EACxD;AACA,MAAI,OAAO,gBAAgB,WAAW,GAAC,YAAO,qBAAP,mBAAyB,SAAQ;AACvE,WAAO,KAAK,wCAAwC;AAAA,EACrD;AACA,MAAI,CAAC,OAAO,UAAU;AACrB,WAAO,KAAK,sBAAsB;AAAA,EACnC;AACA,SAAO;AACR;",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -22,6 +22,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  var utils = __toESM(require("@iobroker/adapter-core"));
25
+ var import_promises = require("node:fs/promises");
26
+ var import_node_path = require("node:path");
25
27
  var import_config = require("./config");
26
28
  var import_saic_client = require("./saic-client");
27
29
  var import_regions = require("./regions");
@@ -105,6 +107,7 @@ const settingInputsByAction = {
105
107
  batteryHeatingScheduleApply: ["batteryHeatingScheduleEnabled", "batteryHeatingScheduleStart"]
106
108
  };
107
109
  const EXPOSE_SOURCE_STATUS_DATAPOINTS = false;
110
+ const PENDING_SETTING_EDITS_FILE = "pending-setting-edits.json";
108
111
  class MgSaic extends utils.Adapter {
109
112
  constructor(options = {}) {
110
113
  super({
@@ -142,16 +145,12 @@ class MgSaic extends utils.Adapter {
142
145
  await this.extendObjectAsync("info.backend", { common: { name: (0, import_runtime_translations.getRuntimeName)("backend") } });
143
146
  await this.extendObjectAsync("info.connection", { common: { name: (0, import_runtime_translations.getRuntimeName)("connection") } });
144
147
  await this.extendObjectAsync("info.lastError", { common: { name: (0, import_runtime_translations.getRuntimeName)("lastError") } });
145
- await this.setObjectNotExistsAsync("internal", {
146
- type: "channel",
147
- common: { name: "Internal" },
148
- native: {}
149
- });
150
- await this.setObjectNotExistsAsync("internal.pendingSettingEdits", {
151
- type: "state",
152
- common: { name: "Pending setting edits", type: "string", role: "json", read: true, write: false },
153
- native: {}
154
- });
148
+ try {
149
+ await this.migrateLegacyPendingSettingEdits();
150
+ } catch (error) {
151
+ const details = error instanceof Error ? error.message : "Unknown storage error";
152
+ this.log.warn(`Could not migrate pending setting edits: ${details}`);
153
+ }
155
154
  await this.setObjectNotExistsAsync("commands", {
156
155
  type: "channel",
157
156
  common: { name: (0, import_runtime_translations.getRuntimeName)("commands") },
@@ -250,7 +249,12 @@ class MgSaic extends utils.Adapter {
250
249
  }
251
250
  await this.delObjectAsync(`vehicles.${vehicle.vin}.canonicalStatus`, { recursive: true });
252
251
  }
253
- await this.restorePendingSettingEdits();
252
+ try {
253
+ await this.restorePendingSettingEdits();
254
+ } catch (error) {
255
+ const details = error instanceof Error ? error.message : "Unknown storage error";
256
+ this.log.warn(`Could not restore pending setting edits: ${details}`);
257
+ }
254
258
  await this.setState("info.connection", true, true);
255
259
  this.log.info(`SAIC account authenticated; discovered ${this.vehicles.length} vehicle(s).`);
256
260
  if (this.vehicles.length === 0) {
@@ -329,7 +333,7 @@ class MgSaic extends utils.Adapter {
329
333
  vehicle.vin,
330
334
  this.abortController.signal
331
335
  );
332
- await this.createVehicleChargingObjects(vehicle.vin, charging);
336
+ await this.createVehicleChargingObjects(vehicle.vin, charging, vehicle.series);
333
337
  } catch (error) {
334
338
  if (this.stopping) {
335
339
  break;
@@ -554,21 +558,62 @@ class MgSaic extends utils.Adapter {
554
558
  }
555
559
  }
556
560
  async persistPendingSettingEdits() {
557
- const serialized = (0, import_pending_setting_edits.serializePendingSettingEdits)(this.pendingSettingEdits.keys());
558
561
  const write = this.pendingSettingEditsWrite.catch(() => void 0).then(async () => {
559
- await this.setStateAsync("internal.pendingSettingEdits", { val: serialized, ack: true });
562
+ await this.writePendingSettingEditsFile((0, import_pending_setting_edits.serializePendingSettingEdits)(this.pendingSettingEdits.keys()));
560
563
  });
561
564
  this.pendingSettingEditsWrite = write;
562
565
  await write;
563
566
  }
567
+ async migrateLegacyPendingSettingEdits() {
568
+ const legacyObject = await this.getObjectAsync("internal.pendingSettingEdits");
569
+ if (legacyObject) {
570
+ const legacyState = await this.getStateAsync("internal.pendingSettingEdits");
571
+ const savedFileValue = await this.readPendingSettingEditsFile();
572
+ await (0, import_pending_setting_edits.migrateLegacyPendingSettingEdits)(
573
+ savedFileValue,
574
+ { exists: true, value: legacyState == null ? void 0 : legacyState.val },
575
+ (serialized) => this.writePendingSettingEditsFile(serialized),
576
+ () => this.delObjectAsync("internal.pendingSettingEdits")
577
+ );
578
+ }
579
+ const internalObject = await this.getObjectAsync("internal");
580
+ if (!internalObject) {
581
+ return;
582
+ }
583
+ const children = await this.getObjectListAsync({
584
+ startkey: `${this.namespace}.internal.`,
585
+ endkey: `${this.namespace}.internal.\u9999`
586
+ });
587
+ if (children.rows.length === 0) {
588
+ await this.delObjectAsync("internal");
589
+ }
590
+ }
591
+ async readPendingSettingEditsFile() {
592
+ try {
593
+ return await (0, import_promises.readFile)(this.getPendingSettingEditsFilePath(), "utf8");
594
+ } catch (error) {
595
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
596
+ return void 0;
597
+ }
598
+ throw error;
599
+ }
600
+ }
601
+ async writePendingSettingEditsFile(serialized) {
602
+ const directory = utils.getAbsoluteInstanceDataDir(this);
603
+ await (0, import_promises.mkdir)(directory, { recursive: true });
604
+ await (0, import_promises.writeFile)(this.getPendingSettingEditsFilePath(), serialized, "utf8");
605
+ }
606
+ getPendingSettingEditsFilePath() {
607
+ return (0, import_node_path.join)(utils.getAbsoluteInstanceDataDir(this), PENDING_SETTING_EDITS_FILE);
608
+ }
564
609
  async restorePendingSettingEdits() {
565
610
  const validStateIds = new Set(
566
611
  this.vehicles.flatMap(
567
612
  ({ vin }) => Object.keys(settingGroups).filter((stateId) => stateId !== "driverSeatHeatLevel" && stateId !== "passengerSeatHeatLevel").map((stateId) => vehicleCommandPath(vin, stateId))
568
613
  )
569
614
  );
570
- const savedState = await this.getStateAsync("internal.pendingSettingEdits");
571
- for (const stateId of (0, import_pending_setting_edits.parsePendingSettingEdits)(savedState == null ? void 0 : savedState.val, validStateIds)) {
615
+ const savedFileValue = await this.readPendingSettingEditsFile();
616
+ for (const stateId of (0, import_pending_setting_edits.parsePendingSettingEdits)(savedFileValue, validStateIds)) {
572
617
  this.pendingSettingEdits.set(stateId, ++this.settingEditSequence);
573
618
  }
574
619
  for (const stateId of validStateIds) {
@@ -861,16 +906,17 @@ class MgSaic extends utils.Adapter {
861
906
  await this.delObjectAsync(`${statusId}.${key}`);
862
907
  }
863
908
  }
864
- async createVehicleChargingObjects(vin, charging) {
909
+ async createVehicleChargingObjects(vin, charging, series) {
865
910
  await this.ensureVehicleChargingGroups(vin);
866
911
  const rootId = `vehicles.${vin}.status.charging`;
867
912
  for (const { source, id } of chargingGroups) {
868
913
  const groupId = `${rootId}.${id}`;
869
914
  const excludedFields = id === "session" ? import_status_metadata.excludedChargingSessionFields : import_status_metadata.excludedChargingStatisticsFields;
915
+ const fields = source === "chrgMgmtData" ? (0, import_status_metadata.getChargingManagementFields)(charging.chrgMgmtData) : (0, import_status_metadata.getChargingStatisticsFields)(charging[source]);
870
916
  for (const field of excludedFields) {
871
917
  await this.delObjectAsync(`${groupId}.${field}`);
872
918
  }
873
- for (const [field, value] of Object.entries(charging[source])) {
919
+ for (const [field, value] of fields) {
874
920
  if (this.stopping) {
875
921
  return;
876
922
  }
@@ -879,15 +925,20 @@ class MgSaic extends utils.Adapter {
879
925
  continue;
880
926
  }
881
927
  const stateId = `${groupId}.${field}`;
882
- if (value === void 0) {
928
+ if (value === void 0 && field !== "realtimePower" && field !== "batteryAvailableEnergy") {
883
929
  await this.delObjectAsync(stateId);
884
930
  continue;
885
931
  }
886
- const metadata = (0, import_status_metadata.getChargingFieldMetadata)(field);
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") {
934
+ await this.delObjectAsync(stateId);
935
+ continue;
936
+ }
937
+ const metadata = (0, import_status_metadata.getChargingFieldMetadata)(field, series);
887
938
  const common = {
888
939
  name: (0, import_runtime_translations.getChargingFieldName)(field),
889
- type: metadata.type === "boolean" ? "boolean" : typeof value === "string" ? "string" : "number",
890
- role: typeof value === "string" ? "text" : metadata.role,
940
+ type: metadata.type === "boolean" ? "boolean" : typeof normalizedValue === "string" ? "string" : "number",
941
+ role: typeof normalizedValue === "string" ? "text" : metadata.role,
891
942
  read: true,
892
943
  write: false
893
944
  };
@@ -896,8 +947,11 @@ class MgSaic extends utils.Adapter {
896
947
  }
897
948
  await this.setObjectNotExistsAsync(stateId, { type: "state", common, native: {} });
898
949
  await this.extendObjectAsync(stateId, { common });
950
+ if (normalizedValue === void 0) {
951
+ continue;
952
+ }
899
953
  await this.setStateAsync(stateId, {
900
- val: (0, import_status_metadata.normalizeChargingValue)(field, value),
954
+ val: normalizedValue,
901
955
  ack: true
902
956
  });
903
957
  if (field === "bmsOnBdChrgTrgtSOCDspCmd" && typeof value === "number" && value >= 1 && value <= 7) {
@@ -905,13 +959,31 @@ class MgSaic extends utils.Adapter {
905
959
  const targetSocState = await this.getStateAsync(targetSocId);
906
960
  if ((0, import_status_metadata.shouldApplySettingFeedback)(targetSocState, this.pendingSettingEdits.has(targetSocId))) {
907
961
  await this.setStateAsync(targetSocId, {
908
- val: (0, import_status_metadata.normalizeChargingValue)(field, value),
962
+ val: (0, import_status_metadata.normalizeChargingValue)(field, value, series),
909
963
  ack: true
910
964
  });
911
965
  }
912
966
  }
913
967
  }
914
968
  }
969
+ const capacityState = await this.getStateAsync(`${rootId}.statistics.totalBatteryCapacity`);
970
+ const socState = await this.getStateAsync(`${rootId}.session.bmsPackSOCDsp`);
971
+ const availableEnergy = (0, import_status_metadata.calculateAvailableBatteryEnergy)(
972
+ capacityState == null ? void 0 : capacityState.val,
973
+ socState == null ? void 0 : socState.val,
974
+ charging.rvsChargeStatus.packEnergyKwh
975
+ );
976
+ if (availableEnergy !== void 0) {
977
+ await this.setStateAsync(`${rootId}.statistics.batteryAvailableEnergy`, {
978
+ val: availableEnergy,
979
+ ack: true
980
+ });
981
+ } else {
982
+ await this.setStateAsync(`${rootId}.statistics.batteryAvailableEnergy`, {
983
+ val: null,
984
+ ack: true
985
+ });
986
+ }
915
987
  for (const [setting, value] of Object.entries((0, import_status_metadata.getChargingSettingFeedback)(charging.chrgMgmtData))) {
916
988
  const stateId = vehicleCommandPath(vin, setting);
917
989
  const state = await this.getStateAsync(stateId);