iobroker.mg-saic 0.0.3 → 0.0.4

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
@@ -11,7 +11,9 @@ The adapter is intended for connecting MG and SAIC vehicles to ioBroker through
11
11
 
12
12
  ### Before you start
13
13
 
14
- - Node.js 22.19.0 or later is required.
14
+ - Node.js 24.0.0 or later is required.
15
+ - ioBroker js-controller 7.2.2 or later is required.
16
+ - ioBroker Admin 8.2.0 or later is required.
15
17
  - 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.
16
18
  - Have the account password ready. The adapter stores it as a protected setting.
17
19
  - Select the region for the account. Available regions are European Union, China, Australia, Brazil, Israel, Turkey, Thailand, and Rest of World. India is not supported.
@@ -34,7 +36,18 @@ The adapter is intended for connecting MG and SAIC vehicles to ioBroker through
34
36
 
35
37
  The adapter is published on npm but has not yet been added to the ioBroker adapter catalog.
36
38
 
39
+ ## Support
40
+
41
+ [![Donate](https://camo.githubusercontent.com/ef8c60305542390406855547e32850909812a73c4abada236de22a6dc88b313f/68747470733a2f2f7777772e70617970616c6f626a656374732e636f6d2f64655f44452f44452f692f62746e2f62746e5f646f6e61746543435f4c472e676966)](https://www.paypal.com/donate/?hosted_button_id=3RJQGSJNRSHUJ)
42
+
37
43
  ## Changelog
44
+ ### 0.0.4 (2026-10-24)
45
+
46
+ - (Voodoo2man) Corrected GPS coordinates.
47
+ - (Voodoo2man) Fixed incorrect datapoint values.
48
+ - (Voodoo2man) Removed duplicate and irrelevant datapoints.
49
+ - (Voodoo2man) Set min version of admin to 8.2.0, js-controller 7.2.2, node.js 24
50
+
38
51
  ### 0.0.3 (2026-10-21)
39
52
 
40
53
  - (Voodoo2man) Improved release automation, updated Node.js release checks, and removed unpublished version metadata.
package/build/main.js CHANGED
@@ -26,6 +26,7 @@ var import_config = require("./config");
26
26
  var import_saic_client = require("./saic-client");
27
27
  var import_regions = require("./regions");
28
28
  var import_refresh_command = require("./refresh-command");
29
+ var import_pending_setting_edits = require("./pending-setting-edits");
29
30
  var import_status_metadata = require("./status-metadata");
30
31
  var import_runtime_translations = require("./runtime-translations");
31
32
  const chargingGroups = [
@@ -97,6 +98,12 @@ const settingActions = [
97
98
  { id: "chargingScheduleApply" },
98
99
  { id: "batteryHeatingScheduleApply" }
99
100
  ];
101
+ const settingInputsByAction = {
102
+ climateTemperatureApply: ["climateTemperature"],
103
+ chargingSettingsApply: ["chargeTargetSoc", "chargingCurrentLimit"],
104
+ chargingScheduleApply: ["chargingScheduleStart", "chargingScheduleEnd", "chargingScheduleMode"],
105
+ batteryHeatingScheduleApply: ["batteryHeatingScheduleEnabled", "batteryHeatingScheduleStart"]
106
+ };
100
107
  const EXPOSE_SOURCE_STATUS_DATAPOINTS = false;
101
108
  class MgSaic extends utils.Adapter {
102
109
  constructor(options = {}) {
@@ -122,6 +129,9 @@ class MgSaic extends utils.Adapter {
122
129
  lastStatusTimes = /* @__PURE__ */ new Map();
123
130
  lastCanonicalStatusTimes = /* @__PURE__ */ new Map();
124
131
  vehicleCommandsInProgress = /* @__PURE__ */ new Set();
132
+ pendingSettingEdits = /* @__PURE__ */ new Map();
133
+ pendingSettingEditsWrite = Promise.resolve();
134
+ settingEditSequence = 0;
125
135
  async onReady() {
126
136
  var _a, _b, _c, _d, _e;
127
137
  if (this.stopping) {
@@ -132,6 +142,16 @@ class MgSaic extends utils.Adapter {
132
142
  await this.extendObjectAsync("info.backend", { common: { name: (0, import_runtime_translations.getRuntimeName)("backend") } });
133
143
  await this.extendObjectAsync("info.connection", { common: { name: (0, import_runtime_translations.getRuntimeName)("connection") } });
134
144
  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
+ });
135
155
  await this.setObjectNotExistsAsync("commands", {
136
156
  type: "channel",
137
157
  common: { name: (0, import_runtime_translations.getRuntimeName)("commands") },
@@ -230,6 +250,7 @@ class MgSaic extends utils.Adapter {
230
250
  }
231
251
  await this.delObjectAsync(`vehicles.${vehicle.vin}.canonicalStatus`, { recursive: true });
232
252
  }
253
+ await this.restorePendingSettingEdits();
233
254
  await this.setState("info.connection", true, true);
234
255
  this.log.info(`SAIC account authenticated; discovered ${this.vehicles.length} vehicle(s).`);
235
256
  if (this.vehicles.length === 0) {
@@ -470,6 +491,7 @@ class MgSaic extends utils.Adapter {
470
491
  if (this.stopping) {
471
492
  return;
472
493
  }
494
+ const eventId = (0, import_refresh_command.normalizeStateChangeId)(id, this.namespace);
473
495
  if ((0, import_refresh_command.isRefreshCommand)(id, this.namespace, state)) {
474
496
  void this.setStateAsync("commands.refresh", { val: false, ack: true }).catch((error) => {
475
497
  const details = error instanceof Error ? error.message : "Unknown state error";
@@ -482,10 +504,10 @@ class MgSaic extends utils.Adapter {
482
504
  return;
483
505
  }
484
506
  const settingAction = settingActions.find(
485
- ({ id: actionId }) => this.vehicles.some(({ vin }) => id === `${this.namespace}.${vehicleCommandPath(vin, actionId)}`)
507
+ ({ id: actionId }) => this.vehicles.some(({ vin }) => eventId === `${this.namespace}.${vehicleCommandPath(vin, actionId)}`)
486
508
  );
487
509
  const settingVehicle = this.vehicles.find(
488
- ({ vin }) => settingAction && id === `${this.namespace}.${vehicleCommandPath(vin, settingAction.id)}`
510
+ ({ vin }) => settingAction && eventId === `${this.namespace}.${vehicleCommandPath(vin, settingAction.id)}`
489
511
  );
490
512
  if (settingAction && settingVehicle) {
491
513
  void this.executeVehicleSetting(settingVehicle.vin, settingAction.id);
@@ -493,17 +515,16 @@ class MgSaic extends utils.Adapter {
493
515
  }
494
516
  const settingInput = this.vehicles.flatMap(
495
517
  ({ vin }) => Object.keys(settingGroups).filter((stateId) => stateId !== "driverSeatHeatLevel" && stateId !== "passengerSeatHeatLevel").map((stateId) => ({ vin, stateId }))
496
- ).find(({ vin, stateId }) => id === `${this.namespace}.${vehicleCommandPath(vin, stateId)}`);
518
+ ).find(({ vin, stateId }) => eventId === `${this.namespace}.${vehicleCommandPath(vin, stateId)}`);
497
519
  if (settingInput) {
498
- void this.setStateAsync(vehicleCommandPath(settingInput.vin, settingInput.stateId), {
499
- val: state.val,
500
- ack: true
501
- });
520
+ const stateId = vehicleCommandPath(settingInput.vin, settingInput.stateId);
521
+ this.pendingSettingEdits.set(stateId, ++this.settingEditSequence);
522
+ void this.acknowledgeSettingEdit(stateId, state.val);
502
523
  return;
503
524
  }
504
525
  const seatHeating = this.vehicles.flatMap(
505
526
  ({ vin }) => ["driverSeatHeatLevel", "passengerSeatHeatLevel"].map((stateId) => ({ vin, stateId }))
506
- ).find(({ vin, stateId }) => id === `${this.namespace}.${vehicleCommandPath(vin, stateId)}`);
527
+ ).find(({ vin, stateId }) => eventId === `${this.namespace}.${vehicleCommandPath(vin, stateId)}`);
507
528
  if (seatHeating) {
508
529
  const seat = seatHeating.stateId === "driverSeatHeatLevel" ? "driver" : "passenger";
509
530
  void this.executeSeatHeating(seatHeating.vin, seat, state.val);
@@ -513,16 +534,51 @@ class MgSaic extends utils.Adapter {
513
534
  return;
514
535
  }
515
536
  const commandEntry = vehicleCommands.find(
516
- ({ id: commandId }) => this.vehicles.some(({ vin }) => id === `${this.namespace}.${vehicleCommandPath(vin, commandId)}`)
537
+ ({ id: commandId }) => this.vehicles.some(({ vin }) => eventId === `${this.namespace}.${vehicleCommandPath(vin, commandId)}`)
517
538
  );
518
539
  const vehicle = this.vehicles.find(
519
- ({ vin }) => commandEntry && id === `${this.namespace}.${vehicleCommandPath(vin, commandEntry.id)}`
540
+ ({ vin }) => commandEntry && eventId === `${this.namespace}.${vehicleCommandPath(vin, commandEntry.id)}`
520
541
  );
521
542
  if (!commandEntry || !vehicle) {
522
543
  return;
523
544
  }
524
545
  void this.executeVehicleCommand(vehicle.vin, commandEntry.id);
525
546
  }
547
+ async acknowledgeSettingEdit(stateId, value) {
548
+ try {
549
+ await this.persistPendingSettingEdits();
550
+ await this.setStateAsync(stateId, { val: value, ack: true });
551
+ } catch (error) {
552
+ const details = error instanceof Error ? error.message : "Unknown state error";
553
+ this.log.error(`Could not persist pending setting ${stateId}: ${details}`);
554
+ }
555
+ }
556
+ async persistPendingSettingEdits() {
557
+ const serialized = (0, import_pending_setting_edits.serializePendingSettingEdits)(this.pendingSettingEdits.keys());
558
+ const write = this.pendingSettingEditsWrite.catch(() => void 0).then(async () => {
559
+ await this.setStateAsync("internal.pendingSettingEdits", { val: serialized, ack: true });
560
+ });
561
+ this.pendingSettingEditsWrite = write;
562
+ await write;
563
+ }
564
+ async restorePendingSettingEdits() {
565
+ const validStateIds = new Set(
566
+ this.vehicles.flatMap(
567
+ ({ vin }) => Object.keys(settingGroups).filter((stateId) => stateId !== "driverSeatHeatLevel" && stateId !== "passengerSeatHeatLevel").map((stateId) => vehicleCommandPath(vin, stateId))
568
+ )
569
+ );
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)) {
572
+ this.pendingSettingEdits.set(stateId, ++this.settingEditSequence);
573
+ }
574
+ for (const stateId of validStateIds) {
575
+ const currentState = await this.getStateAsync(stateId);
576
+ if (currentState && !currentState.ack && !this.pendingSettingEdits.has(stateId)) {
577
+ this.pendingSettingEdits.set(stateId, ++this.settingEditSequence);
578
+ }
579
+ }
580
+ await this.persistPendingSettingEdits();
581
+ }
526
582
  async executeSeatHeating(vin, seat, value) {
527
583
  const stateId = vehicleCommandPath(vin, `${seat}SeatHeatLevel`);
528
584
  if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 3) {
@@ -565,6 +621,9 @@ class MgSaic extends utils.Adapter {
565
621
  }
566
622
  async executeVehicleSetting(vin, action) {
567
623
  const stateId = vehicleCommandPath(vin, action);
624
+ const editVersions = new Map(
625
+ settingInputsByAction[action].map((settingId) => vehicleCommandPath(vin, settingId)).filter((settingId) => this.pendingSettingEdits.has(settingId)).map((settingId) => [settingId, this.pendingSettingEdits.get(settingId)])
626
+ );
568
627
  if (this.vehicleCommandsInProgress.has(vin)) {
569
628
  await this.setStateAsync(stateId, { val: false, ack: true });
570
629
  this.log.warn(`Ignored ${action} for ${vin}: another vehicle command is still running`);
@@ -587,6 +646,26 @@ class MgSaic extends utils.Adapter {
587
646
  setting,
588
647
  this.abortController.signal
589
648
  );
649
+ const clearedEdits = [];
650
+ for (const [settingId, version] of editVersions) {
651
+ if (this.pendingSettingEdits.get(settingId) === version) {
652
+ this.pendingSettingEdits.delete(settingId);
653
+ clearedEdits.push([settingId, version]);
654
+ }
655
+ }
656
+ try {
657
+ await this.persistPendingSettingEdits();
658
+ } catch (error) {
659
+ for (const [settingId, version] of clearedEdits) {
660
+ if (!this.pendingSettingEdits.has(settingId)) {
661
+ this.pendingSettingEdits.set(settingId, version);
662
+ }
663
+ }
664
+ const details = error instanceof Error ? error.message : "Unknown state error";
665
+ this.log.warn(
666
+ `SAIC ${action} completed for ${vin}, but pending edits could not be persisted: ${details}`
667
+ );
668
+ }
590
669
  this.log.info(`SAIC ${action} completed for ${vin}`);
591
670
  } catch (error) {
592
671
  if (!this.stopping) {
@@ -685,6 +764,10 @@ class MgSaic extends utils.Adapter {
685
764
  return;
686
765
  }
687
766
  const statusId = `vehicles.${vin}.${channel}`;
767
+ for (const key of import_status_metadata.excludedVehicleStatusFields) {
768
+ const stateId = `${statusId}.${(0, import_status_metadata.getVehicleStatusGroup)(key)}.${key}`;
769
+ await this.delObjectAsync(stateId);
770
+ }
688
771
  await this.setObjectNotExistsAsync(statusId, {
689
772
  type: "channel",
690
773
  common: { name: (0, import_runtime_translations.getRuntimeName)(channelName) },
@@ -706,6 +789,10 @@ class MgSaic extends utils.Adapter {
706
789
  if (this.stopping) {
707
790
  return;
708
791
  }
792
+ if (import_status_metadata.excludedVehicleStatusFields.some((excludedKey) => excludedKey === key)) {
793
+ await this.delObjectAsync(`${statusId}.${(0, import_status_metadata.getVehicleStatusGroup)(key)}.${key}`);
794
+ continue;
795
+ }
709
796
  const group = (0, import_status_metadata.getVehicleStatusGroup)(key);
710
797
  const groupId = `${statusId}.${group}`;
711
798
  const stateId = `${groupId}.${key}`;
@@ -720,6 +807,11 @@ class MgSaic extends utils.Adapter {
720
807
  }
721
808
  continue;
722
809
  }
810
+ const normalizedValue = (0, import_status_metadata.normalizeVehicleStatusValue)(key, value);
811
+ if (normalizedValue === void 0) {
812
+ await this.delObjectAsync(stateId);
813
+ continue;
814
+ }
723
815
  await this.setObjectNotExistsAsync(groupId, {
724
816
  type: "channel",
725
817
  common: { name: (0, import_runtime_translations.getRuntimeName)(group) },
@@ -754,10 +846,18 @@ class MgSaic extends utils.Adapter {
754
846
  if (this.stopping) {
755
847
  return;
756
848
  }
757
- await this.setStateAsync(stateId, { val: (0, import_status_metadata.normalizeVehicleStatusValue)(key, value), ack: true });
849
+ await this.setStateAsync(stateId, { val: normalizedValue, ack: true });
758
850
  if (this.stopping) {
759
851
  return;
760
852
  }
853
+ const seatSetting = key === "frontLeftSeatHeatLevel" ? "driverSeatHeatLevel" : key === "frontRightSeatHeatLevel" ? "passengerSeatHeatLevel" : void 0;
854
+ if (seatSetting && Number.isInteger(value) && value >= 0 && value <= 3) {
855
+ const settingId = vehicleCommandPath(vin, seatSetting);
856
+ const settingState = await this.getStateAsync(settingId);
857
+ if (!settingState || settingState.ack) {
858
+ await this.setStateAsync(settingId, { val: value, ack: true });
859
+ }
860
+ }
761
861
  await this.delObjectAsync(`${statusId}.${key}`);
762
862
  }
763
863
  }
@@ -766,10 +866,18 @@ class MgSaic extends utils.Adapter {
766
866
  const rootId = `vehicles.${vin}.status.charging`;
767
867
  for (const { source, id } of chargingGroups) {
768
868
  const groupId = `${rootId}.${id}`;
869
+ const excludedFields = id === "session" ? import_status_metadata.excludedChargingSessionFields : import_status_metadata.excludedChargingStatisticsFields;
870
+ for (const field of excludedFields) {
871
+ await this.delObjectAsync(`${groupId}.${field}`);
872
+ }
769
873
  for (const [field, value] of Object.entries(charging[source])) {
770
874
  if (this.stopping) {
771
875
  return;
772
876
  }
877
+ if (excludedFields.some((excludedField) => excludedField === field)) {
878
+ await this.delObjectAsync(`${groupId}.${field}`);
879
+ continue;
880
+ }
773
881
  const stateId = `${groupId}.${field}`;
774
882
  if (value === void 0) {
775
883
  await this.delObjectAsync(stateId);
@@ -792,6 +900,23 @@ class MgSaic extends utils.Adapter {
792
900
  val: (0, import_status_metadata.normalizeChargingValue)(field, value),
793
901
  ack: true
794
902
  });
903
+ if (field === "bmsOnBdChrgTrgtSOCDspCmd" && typeof value === "number" && value >= 1 && value <= 7) {
904
+ const targetSocId = vehicleCommandPath(vin, "chargeTargetSoc");
905
+ const targetSocState = await this.getStateAsync(targetSocId);
906
+ if ((0, import_status_metadata.shouldApplySettingFeedback)(targetSocState, this.pendingSettingEdits.has(targetSocId))) {
907
+ await this.setStateAsync(targetSocId, {
908
+ val: (0, import_status_metadata.normalizeChargingValue)(field, value),
909
+ ack: true
910
+ });
911
+ }
912
+ }
913
+ }
914
+ }
915
+ for (const [setting, value] of Object.entries((0, import_status_metadata.getChargingSettingFeedback)(charging.chrgMgmtData))) {
916
+ const stateId = vehicleCommandPath(vin, setting);
917
+ const state = await this.getStateAsync(stateId);
918
+ if ((0, import_status_metadata.shouldApplySettingFeedback)(state, this.pendingSettingEdits.has(stateId))) {
919
+ await this.setStateAsync(stateId, { val: value, ack: true });
795
920
  }
796
921
  }
797
922
  await this.delObjectAsync(`vehicles.${vin}.charging`, { recursive: true });