iobroker.mg-saic 0.1.0 → 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 +10 -7
- package/admin/jsonConfig.json +2 -1
- package/build/config.js +5 -2
- package/build/config.js.map +2 -2
- package/build/main.js +60 -15
- package/build/main.js.map +2 -2
- package/build/pending-setting-edits.js +24 -0
- package/build/pending-setting-edits.js.map +2 -2
- package/build/runtime-translations.js +777 -64
- package/build/runtime-translations.js.map +2 -2
- package/build/status-metadata.js +3 -3
- package/build/status-metadata.js.map +2 -2
- package/io-package.json +14 -1
- package/package.json +6 -6
- /package/{MG.png → admin/MG.png} +0 -0
package/README.md
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/iobroker.mg-saic)
|
|
4
4
|
[](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
|
|
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
|
|
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,11 @@ The adapter is published on npm but has not yet been added to the ioBroker adapt
|
|
|
41
43
|
[](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
|
+
|
|
44
51
|
### 0.1.0 (2026-11-01)
|
|
45
52
|
|
|
46
53
|
- (Voodoo2man) Reported real-time charging power in kW using the pack current and voltage.
|
|
@@ -66,10 +73,6 @@ Older changelog entries are available in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
|
66
73
|
|
|
67
74
|
- (Voodoo2man) Added regional iSmart account setup, vehicle status, and supported remote controls.
|
|
68
75
|
|
|
69
|
-
### 0.0.1 (2026-10-20)
|
|
70
|
-
|
|
71
|
-
- (Voodoo2man) Initial Version
|
|
72
|
-
|
|
73
76
|
## License
|
|
74
77
|
|
|
75
78
|
MIT License
|
package/admin/jsonConfig.json
CHANGED
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 <
|
|
31
|
-
errors.push(
|
|
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
|
package/build/config.js.map
CHANGED
|
@@ -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)
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiD;AAkB1C,MAAM,gCAAgC;
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -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.
|
|
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
|
|
571
|
-
for (const stateId of (0, import_pending_setting_edits.parsePendingSettingEdits)(
|
|
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) {
|