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 +14 -1
- package/build/main.js +136 -11
- package/build/main.js.map +2 -2
- package/build/pending-setting-edits.js +47 -0
- package/build/pending-setting-edits.js.map +7 -0
- package/build/refresh-command.js +8 -3
- package/build/refresh-command.js.map +2 -2
- package/build/status-metadata.js +93 -0
- package/build/status-metadata.js.map +2 -2
- package/io-package.json +16 -3
- package/package.json +4 -4
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
|
|
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
|
+
[](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 }) =>
|
|
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 &&
|
|
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 }) =>
|
|
518
|
+
).find(({ vin, stateId }) => eventId === `${this.namespace}.${vehicleCommandPath(vin, stateId)}`);
|
|
497
519
|
if (settingInput) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 }) =>
|
|
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 }) =>
|
|
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 &&
|
|
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:
|
|
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 });
|