iobroker.anker-solix 0.9.8 → 0.10.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 +28 -0
- package/admin/i18n/de.json +11 -1
- package/admin/i18n/en.json +11 -1
- package/admin/jsonConfig.json +89 -0
- package/build/lib/curtailmentForecast.js +106 -0
- package/build/lib/curtailmentForecast.js.map +7 -0
- package/build/lib/curtailmentProfiles.js +135 -0
- package/build/lib/curtailmentProfiles.js.map +7 -0
- package/build/lib/curtailmentRunner.js +134 -0
- package/build/lib/curtailmentRunner.js.map +7 -0
- package/build/lib/curtailmentStates.js +138 -0
- package/build/lib/curtailmentStates.js.map +7 -0
- package/build/lib/curtailmentTypes.js +29 -0
- package/build/lib/curtailmentTypes.js.map +7 -0
- package/build/main.js +49 -0
- package/build/main.js.map +2 -2
- package/io-package.json +72 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -286,8 +286,36 @@ German guides/videos linked from the [HA README](https://github.com/thomluther/h
|
|
|
286
286
|
|
|
287
287
|
---
|
|
288
288
|
|
|
289
|
+
## Curtailment avoidance (optional)
|
|
290
|
+
|
|
291
|
+
Tab **Abregelungsvermeidung** / **Curtailment avoidance**: reads hourly forecast from the [solarprognose](https://github.com/ioBroker/ioBroker.solarprognose) adapter (default path `solarprognose.0.forecast.00.hourly`, states `04.power` … `20.power`). When forecast PV exceeds the configured AC export limit, the adapter switches to **manual** mode during that window and sets **AC charge limit** to use spare battery capacity (`missing_Wh / (curtailment_hours + 1)`). Before/after the window, **self-consumption** or **smart** mode (configurable). Status: `anker-solix.0.curtailment.*`.
|
|
292
|
+
|
|
293
|
+
**Combiner (Power Dock):** up to **4** solarbanks; total AC limit = **sum** of per-unit limits (SB2 **1000** W, SB3 Pro **1200** W, SB4 Pro **2500** W per unit). **Standalone** solarbank: always **800** W. Configure via `units` in the devices JSON (see Admin example).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
289
297
|
## Changelog
|
|
290
298
|
|
|
299
|
+
### 0.10.3
|
|
300
|
+
|
|
301
|
+
- CI: curtailment unit tests use Mocha/Chai (fixes adapter-check lint)
|
|
302
|
+
|
|
303
|
+
### 0.10.2
|
|
304
|
+
|
|
305
|
+
- Curtailment AC limits: standalone 800 W; combiner per unit SB2 1000, SB3 1200, SB4 2500 W
|
|
306
|
+
|
|
307
|
+
### 0.10.1
|
|
308
|
+
|
|
309
|
+
- Curtailment: Combiner limit = sum of per-unit profiles (max 4 mixed solarbanks)
|
|
310
|
+
|
|
311
|
+
### 0.10.0
|
|
312
|
+
|
|
313
|
+
- Optional **curtailment avoidance** via solarprognose forecast (Admin tab, `curtailment.*` states)
|
|
314
|
+
|
|
315
|
+
### 0.9.9
|
|
316
|
+
|
|
317
|
+
- `package.json` keyword `ioBroker`; entity group headers with schema `size` property
|
|
318
|
+
|
|
291
319
|
### 0.9.8
|
|
292
320
|
|
|
293
321
|
- Admin UI: all option/entity fields with lg/xl breakpoints; CI release fix
|
package/admin/i18n/de.json
CHANGED
|
@@ -72,5 +72,15 @@
|
|
|
72
72
|
"min_soc": "Mindest-SOC Reserve (%)",
|
|
73
73
|
"grid_export_limit": "Einspeise-Limit (W)",
|
|
74
74
|
"cloud_state": "Cloud-Status",
|
|
75
|
-
"wifi_state": "WLAN-Status"
|
|
75
|
+
"wifi_state": "WLAN-Status",
|
|
76
|
+
"CurtailmentAvoidance": "Abregelungsvermeidung",
|
|
77
|
+
"curtailment_hint": "Liest die stündliche PV-Prognose vom Adapter solarprognose. An Überproduktions-Tagen: vor dem Fenster normaler Export-Modus; während der Abregelung Modus Benutzerdefiniert mit berechnetem AC-Ladelimit; danach gewählter Modus. MQTT erforderlich. Status unter curtailment.*",
|
|
78
|
+
"enableCurtailmentAvoidance": "Abregelungsvermeidung aktivieren",
|
|
79
|
+
"curtailmentForecastPath": "Objektpfad Prognose (stündlich, ohne .power)",
|
|
80
|
+
"curtailmentModeBefore": "Nutzungsmodus vor Abregelungsfenster",
|
|
81
|
+
"curtailmentModeAfter": "Nutzungsmodus nach Abregelungsfenster",
|
|
82
|
+
"mode_smartmeter": "Eigenverbrauch",
|
|
83
|
+
"mode_smart": "Smart-Modus",
|
|
84
|
+
"curtailmentDevicesJson": "Geräte JSON (Beispiel unten)",
|
|
85
|
+
"curtailment_example": "Beispiel Combiner (max. 4 Solarbänke, gemischte Typen – Limit = Summe):\n[{\"deviceId\":\"COMBINER_SN\",\"enabled\":true,\"role\":\"combiner\",\"batteryCapacityWh\":15000,\"units\":[\"solarbank3pro\",\"solarbank3pro\",\"solarbank4pro\",\"solarbank2\"]}]\n→ 1200+1200+2500+1000 = 5900 W. Einzeln: immer 800 W. Am Combiner je Einheit: solarbank2 1000 W, solarbank3pro 1200 W, solarbank4pro 2500 W."
|
|
76
86
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -72,5 +72,15 @@
|
|
|
72
72
|
"min_soc": "Minimum SOC reserve (%)",
|
|
73
73
|
"grid_export_limit": "Grid export limit (W)",
|
|
74
74
|
"cloud_state": "Cloud state",
|
|
75
|
-
"wifi_state": "WiFi state"
|
|
75
|
+
"wifi_state": "WiFi state",
|
|
76
|
+
"CurtailmentAvoidance": "Curtailment avoidance",
|
|
77
|
+
"curtailment_hint": "Uses hourly PV forecast from the solarprognose adapter. On overproduction days: before the window, normal export mode; during curtailment, manual mode with calculated AC charge limit; after the window, restore selected mode. Requires MQTT. Status states under curtailment.*",
|
|
78
|
+
"enableCurtailmentAvoidance": "Enable curtailment avoidance",
|
|
79
|
+
"curtailmentForecastPath": "Forecast object path (hourly, without .power)",
|
|
80
|
+
"curtailmentModeBefore": "Usage mode before curtailment window",
|
|
81
|
+
"curtailmentModeAfter": "Usage mode after curtailment window",
|
|
82
|
+
"mode_smartmeter": "Self-consumption (Eigenverbrauch)",
|
|
83
|
+
"mode_smart": "Smart mode",
|
|
84
|
+
"curtailmentDevicesJson": "Devices JSON (see example below)",
|
|
85
|
+
"curtailment_example": "Combiner example (max. 4 banks, mixed types – limit = sum):\n[{\"deviceId\":\"COMBINER_SN\",\"enabled\":true,\"role\":\"combiner\",\"batteryCapacityWh\":15000,\"units\":[\"solarbank3pro\",\"solarbank3pro\",\"solarbank4pro\",\"solarbank2\"]}]\n→ 1200+1200+2500+1000 = 5900 W. Standalone: always 800 W. On combiner per unit: solarbank2 1000 W, solarbank3pro 1200 W, solarbank4pro 2500 W."
|
|
76
86
|
}
|
package/admin/jsonConfig.json
CHANGED
|
@@ -249,6 +249,7 @@
|
|
|
249
249
|
"_grp_energy": {
|
|
250
250
|
"type": "header",
|
|
251
251
|
"text": "grp_energy",
|
|
252
|
+
"size": 5,
|
|
252
253
|
"xs": 12,
|
|
253
254
|
"sm": 12,
|
|
254
255
|
"md": 12,
|
|
@@ -278,6 +279,7 @@
|
|
|
278
279
|
"_grp_solarbank": {
|
|
279
280
|
"type": "header",
|
|
280
281
|
"text": "grp_solarbank",
|
|
282
|
+
"size": 5,
|
|
281
283
|
"xs": 12,
|
|
282
284
|
"sm": 12,
|
|
283
285
|
"md": 12,
|
|
@@ -367,6 +369,7 @@
|
|
|
367
369
|
"_grp_devices": {
|
|
368
370
|
"type": "header",
|
|
369
371
|
"text": "grp_devices",
|
|
372
|
+
"size": 5,
|
|
370
373
|
"xs": 12,
|
|
371
374
|
"sm": 12,
|
|
372
375
|
"md": 12,
|
|
@@ -469,6 +472,92 @@
|
|
|
469
472
|
}
|
|
470
473
|
}
|
|
471
474
|
},
|
|
475
|
+
"_tab_curtailment": {
|
|
476
|
+
"type": "panel",
|
|
477
|
+
"label": "CurtailmentAvoidance",
|
|
478
|
+
"items": {
|
|
479
|
+
"_curtailment_hint": {
|
|
480
|
+
"type": "staticText",
|
|
481
|
+
"text": "curtailment_hint",
|
|
482
|
+
"xs": 12,
|
|
483
|
+
"sm": 12,
|
|
484
|
+
"md": 12,
|
|
485
|
+
"lg": 12,
|
|
486
|
+
"xl": 12
|
|
487
|
+
},
|
|
488
|
+
"enableCurtailmentAvoidance": {
|
|
489
|
+
"type": "checkbox",
|
|
490
|
+
"label": "enableCurtailmentAvoidance",
|
|
491
|
+
"default": false,
|
|
492
|
+
"xs": 12,
|
|
493
|
+
"sm": 12,
|
|
494
|
+
"md": 12,
|
|
495
|
+
"lg": 12,
|
|
496
|
+
"xl": 12
|
|
497
|
+
},
|
|
498
|
+
"curtailmentForecastPath": {
|
|
499
|
+
"type": "text",
|
|
500
|
+
"label": "curtailmentForecastPath",
|
|
501
|
+
"default": "solarprognose.0.forecast.00.hourly",
|
|
502
|
+
"xs": 12,
|
|
503
|
+
"sm": 12,
|
|
504
|
+
"md": 8,
|
|
505
|
+
"lg": 8,
|
|
506
|
+
"xl": 8
|
|
507
|
+
},
|
|
508
|
+
"curtailmentModeBefore": {
|
|
509
|
+
"type": "select",
|
|
510
|
+
"label": "curtailmentModeBefore",
|
|
511
|
+
"options": [
|
|
512
|
+
{ "label": "mode_smartmeter", "value": "smartmeter" },
|
|
513
|
+
{ "label": "mode_smart", "value": "smart" }
|
|
514
|
+
],
|
|
515
|
+
"default": "smartmeter",
|
|
516
|
+
"xs": 12,
|
|
517
|
+
"sm": 6,
|
|
518
|
+
"md": 4,
|
|
519
|
+
"lg": 4,
|
|
520
|
+
"xl": 4
|
|
521
|
+
},
|
|
522
|
+
"curtailmentModeAfter": {
|
|
523
|
+
"type": "select",
|
|
524
|
+
"label": "curtailmentModeAfter",
|
|
525
|
+
"options": [
|
|
526
|
+
{ "label": "mode_smartmeter", "value": "smartmeter" },
|
|
527
|
+
{ "label": "mode_smart", "value": "smart" }
|
|
528
|
+
],
|
|
529
|
+
"default": "smartmeter",
|
|
530
|
+
"xs": 12,
|
|
531
|
+
"sm": 6,
|
|
532
|
+
"md": 4,
|
|
533
|
+
"lg": 4,
|
|
534
|
+
"xl": 4
|
|
535
|
+
},
|
|
536
|
+
"curtailmentDevicesJson": {
|
|
537
|
+
"type": "text",
|
|
538
|
+
"label": "curtailmentDevicesJson",
|
|
539
|
+
"default": "[]",
|
|
540
|
+
"xs": 12,
|
|
541
|
+
"sm": 12,
|
|
542
|
+
"md": 12,
|
|
543
|
+
"lg": 12,
|
|
544
|
+
"xl": 12
|
|
545
|
+
},
|
|
546
|
+
"_curtailment_example": {
|
|
547
|
+
"type": "staticText",
|
|
548
|
+
"text": "curtailment_example",
|
|
549
|
+
"style": {
|
|
550
|
+
"fontSize": "0.85em",
|
|
551
|
+
"whiteSpace": "pre-wrap"
|
|
552
|
+
},
|
|
553
|
+
"xs": 12,
|
|
554
|
+
"sm": 12,
|
|
555
|
+
"md": 12,
|
|
556
|
+
"lg": 12,
|
|
557
|
+
"xl": 12
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
},
|
|
472
561
|
"_tab_terms": {
|
|
473
562
|
"type": "panel",
|
|
474
563
|
"label": "Terms",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var curtailmentForecast_exports = {};
|
|
20
|
+
__export(curtailmentForecast_exports, {
|
|
21
|
+
currentPhase: () => currentPhase,
|
|
22
|
+
detectCurtailmentWindow: () => detectCurtailmentWindow,
|
|
23
|
+
readHourlyForecast: () => readHourlyForecast,
|
|
24
|
+
remainingCurtailmentHours: () => remainingCurtailmentHours
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(curtailmentForecast_exports);
|
|
27
|
+
const FORECAST_HOURS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
|
|
28
|
+
async function readHourlyForecast(basePath, getState) {
|
|
29
|
+
const base = basePath.replace(/\.$/, "");
|
|
30
|
+
const hours = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const h of FORECAST_HOURS) {
|
|
32
|
+
const hourKey = h.toString().padStart(2, "0");
|
|
33
|
+
const candidates = [
|
|
34
|
+
`${base}.${hourKey}.power`,
|
|
35
|
+
`${base}.${h}.power`,
|
|
36
|
+
`${base}.${hourKey}-hour.power`,
|
|
37
|
+
`${base}.hour_${hourKey}.power`
|
|
38
|
+
];
|
|
39
|
+
for (const id of candidates) {
|
|
40
|
+
const st = await getState(id);
|
|
41
|
+
if ((st == null ? void 0 : st.val) !== null && (st == null ? void 0 : st.val) !== void 0 && st.val !== "") {
|
|
42
|
+
const w = Number(st.val);
|
|
43
|
+
if (!Number.isNaN(w)) {
|
|
44
|
+
hours.set(h, w);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { hours };
|
|
51
|
+
}
|
|
52
|
+
function detectCurtailmentWindow(forecast, acLimitW) {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
const overHours = [];
|
|
55
|
+
for (const [h, power] of forecast.hours) {
|
|
56
|
+
if (power > acLimitW) {
|
|
57
|
+
overHours.push(h);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!overHours.length) {
|
|
61
|
+
return {
|
|
62
|
+
today: false,
|
|
63
|
+
startHour: 0,
|
|
64
|
+
endHour: 0,
|
|
65
|
+
durationHours: 0,
|
|
66
|
+
chargeDivisorHours: 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
overHours.sort((a, b) => a - b);
|
|
70
|
+
const startHour = (_a = overHours[0]) != null ? _a : 0;
|
|
71
|
+
const endHour = (_b = overHours[overHours.length - 1]) != null ? _b : 0;
|
|
72
|
+
const durationHours = endHour - startHour + 1;
|
|
73
|
+
return {
|
|
74
|
+
today: true,
|
|
75
|
+
startHour,
|
|
76
|
+
endHour,
|
|
77
|
+
durationHours,
|
|
78
|
+
chargeDivisorHours: durationHours + 1
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function currentPhase(window, nowHour) {
|
|
82
|
+
if (!window.today) {
|
|
83
|
+
return "idle";
|
|
84
|
+
}
|
|
85
|
+
if (nowHour < window.startHour) {
|
|
86
|
+
return "before";
|
|
87
|
+
}
|
|
88
|
+
if (nowHour <= window.endHour) {
|
|
89
|
+
return "active";
|
|
90
|
+
}
|
|
91
|
+
return "after";
|
|
92
|
+
}
|
|
93
|
+
function remainingCurtailmentHours(window, nowHour) {
|
|
94
|
+
if (!window.today || nowHour > window.endHour) {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
return Math.max(0, window.endHour - nowHour + 1);
|
|
98
|
+
}
|
|
99
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
100
|
+
0 && (module.exports = {
|
|
101
|
+
currentPhase,
|
|
102
|
+
detectCurtailmentWindow,
|
|
103
|
+
readHourlyForecast,
|
|
104
|
+
remainingCurtailmentHours
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=curtailmentForecast.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/curtailmentForecast.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CurtailmentPhase, CurtailmentWindow, HourlyForecast } from \"./curtailmentTypes\";\n\nconst FORECAST_HOURS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] as const;\n\ntype StateReader = (id: string) => Promise<ioBroker.State | null | undefined>;\n\n/** Read hourly forecast power (W) from solarprognose (or compatible) object tree. */\nexport async function readHourlyForecast(basePath: string, getState: StateReader): Promise<HourlyForecast> {\n\tconst base = basePath.replace(/\\.$/, \"\");\n\tconst hours = new Map<number, number>();\n\n\tfor (const h of FORECAST_HOURS) {\n\t\tconst hourKey = h.toString().padStart(2, \"0\");\n\t\tconst candidates = [\n\t\t\t`${base}.${hourKey}.power`,\n\t\t\t`${base}.${h}.power`,\n\t\t\t`${base}.${hourKey}-hour.power`,\n\t\t\t`${base}.hour_${hourKey}.power`,\n\t\t];\n\t\tfor (const id of candidates) {\n\t\t\tconst st = await getState(id);\n\t\t\tif (st?.val !== null && st?.val !== undefined && st.val !== \"\") {\n\t\t\t\tconst w = Number(st.val);\n\t\t\t\tif (!Number.isNaN(w)) {\n\t\t\t\t\thours.set(h, w);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn { hours };\n}\n\nexport function detectCurtailmentWindow(forecast: HourlyForecast, acLimitW: number): CurtailmentWindow {\n\tconst overHours: number[] = [];\n\tfor (const [h, power] of forecast.hours) {\n\t\tif (power > acLimitW) {\n\t\t\toverHours.push(h);\n\t\t}\n\t}\n\tif (!overHours.length) {\n\t\treturn {\n\t\t\ttoday: false,\n\t\t\tstartHour: 0,\n\t\t\tendHour: 0,\n\t\t\tdurationHours: 0,\n\t\t\tchargeDivisorHours: 0,\n\t\t};\n\t}\n\toverHours.sort((a, b) => a - b);\n\tconst startHour = overHours[0] ?? 0;\n\tconst endHour = overHours[overHours.length - 1] ?? 0;\n\tconst durationHours = endHour - startHour + 1;\n\treturn {\n\t\ttoday: true,\n\t\tstartHour,\n\t\tendHour,\n\t\tdurationHours,\n\t\tchargeDivisorHours: durationHours + 1,\n\t};\n}\n\nexport function currentPhase(window: CurtailmentWindow, nowHour: number): CurtailmentPhase {\n\tif (!window.today) {\n\t\treturn \"idle\";\n\t}\n\tif (nowHour < window.startHour) {\n\t\treturn \"before\";\n\t}\n\tif (nowHour <= window.endHour) {\n\t\treturn \"active\";\n\t}\n\treturn \"after\";\n}\n\nexport function remainingCurtailmentHours(window: CurtailmentWindow, nowHour: number): number {\n\tif (!window.today || nowHour > window.endHour) {\n\t\treturn 0;\n\t}\n\treturn Math.max(0, window.endHour - nowHour + 1);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,MAAM,iBAAiB,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE;AAKpF,eAAsB,mBAAmB,UAAkB,UAAgD;AAC1G,QAAM,OAAO,SAAS,QAAQ,OAAO,EAAE;AACvC,QAAM,QAAQ,oBAAI,IAAoB;AAEtC,aAAW,KAAK,gBAAgB;AAC/B,UAAM,UAAU,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG;AAC5C,UAAM,aAAa;AAAA,MAClB,GAAG,IAAI,IAAI,OAAO;AAAA,MAClB,GAAG,IAAI,IAAI,CAAC;AAAA,MACZ,GAAG,IAAI,IAAI,OAAO;AAAA,MAClB,GAAG,IAAI,SAAS,OAAO;AAAA,IACxB;AACA,eAAW,MAAM,YAAY;AAC5B,YAAM,KAAK,MAAM,SAAS,EAAE;AAC5B,WAAI,yBAAI,SAAQ,SAAQ,yBAAI,SAAQ,UAAa,GAAG,QAAQ,IAAI;AAC/D,cAAM,IAAI,OAAO,GAAG,GAAG;AACvB,YAAI,CAAC,OAAO,MAAM,CAAC,GAAG;AACrB,gBAAM,IAAI,GAAG,CAAC;AACd;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACA,SAAO,EAAE,MAAM;AAChB;AAEO,SAAS,wBAAwB,UAA0B,UAAqC;AAjCvG;AAkCC,QAAM,YAAsB,CAAC;AAC7B,aAAW,CAAC,GAAG,KAAK,KAAK,SAAS,OAAO;AACxC,QAAI,QAAQ,UAAU;AACrB,gBAAU,KAAK,CAAC;AAAA,IACjB;AAAA,EACD;AACA,MAAI,CAAC,UAAU,QAAQ;AACtB,WAAO;AAAA,MACN,OAAO;AAAA,MACP,WAAW;AAAA,MACX,SAAS;AAAA,MACT,eAAe;AAAA,MACf,oBAAoB;AAAA,IACrB;AAAA,EACD;AACA,YAAU,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC9B,QAAM,aAAY,eAAU,CAAC,MAAX,YAAgB;AAClC,QAAM,WAAU,eAAU,UAAU,SAAS,CAAC,MAA9B,YAAmC;AACnD,QAAM,gBAAgB,UAAU,YAAY;AAC5C,SAAO;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB,gBAAgB;AAAA,EACrC;AACD;AAEO,SAAS,aAAa,QAA2B,SAAmC;AAC1F,MAAI,CAAC,OAAO,OAAO;AAClB,WAAO;AAAA,EACR;AACA,MAAI,UAAU,OAAO,WAAW;AAC/B,WAAO;AAAA,EACR;AACA,MAAI,WAAW,OAAO,SAAS;AAC9B,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAEO,SAAS,0BAA0B,QAA2B,SAAyB;AAC7F,MAAI,CAAC,OAAO,SAAS,UAAU,OAAO,SAAS;AAC9C,WAAO;AAAA,EACR;AACA,SAAO,KAAK,IAAI,GAAG,OAAO,UAAU,UAAU,CAAC;AAChD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var curtailmentProfiles_exports = {};
|
|
20
|
+
__export(curtailmentProfiles_exports, {
|
|
21
|
+
acExportLimitW: () => acExportLimitW,
|
|
22
|
+
combinerAcExportLimitW: () => combinerAcExportLimitW,
|
|
23
|
+
parseCurtailmentDevicesJson: () => parseCurtailmentDevicesJson
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(curtailmentProfiles_exports);
|
|
26
|
+
var import_curtailmentTypes = require("./curtailmentTypes");
|
|
27
|
+
const PROFILE_LIMITS = {
|
|
28
|
+
solarbank2: { standalone: 800, combinerPerUnit: 1e3 },
|
|
29
|
+
solarbank3pro: { standalone: 800, combinerPerUnit: 1200 },
|
|
30
|
+
solarbank4pro: { standalone: 800, combinerPerUnit: 2500 }
|
|
31
|
+
};
|
|
32
|
+
function normalizeProfile(raw) {
|
|
33
|
+
return raw in PROFILE_LIMITS ? raw : "solarbank3pro";
|
|
34
|
+
}
|
|
35
|
+
function combinerAcExportLimitW(units) {
|
|
36
|
+
var _a;
|
|
37
|
+
if (!units.length) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (const profile of units.slice(0, import_curtailmentTypes.COMBINER_MAX_UNITS)) {
|
|
42
|
+
const limits = (_a = PROFILE_LIMITS[profile]) != null ? _a : PROFILE_LIMITS.solarbank3pro;
|
|
43
|
+
sum += limits.combinerPerUnit;
|
|
44
|
+
}
|
|
45
|
+
return sum;
|
|
46
|
+
}
|
|
47
|
+
function parseUnitsList(raw, fallbackProfile) {
|
|
48
|
+
if (!Array.isArray(raw)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const units = [];
|
|
52
|
+
for (const entry of raw) {
|
|
53
|
+
if (units.length >= import_curtailmentTypes.COMBINER_MAX_UNITS) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (typeof entry === "string") {
|
|
57
|
+
units.push(normalizeProfile(entry));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entry && typeof entry === "object") {
|
|
61
|
+
const p = entry.profile;
|
|
62
|
+
if (typeof p === "string") {
|
|
63
|
+
units.push(normalizeProfile(p));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!units.length && fallbackProfile) {
|
|
68
|
+
return [fallbackProfile];
|
|
69
|
+
}
|
|
70
|
+
return units;
|
|
71
|
+
}
|
|
72
|
+
function acExportLimitW(device) {
|
|
73
|
+
var _a, _b, _c;
|
|
74
|
+
if (device.role === "combiner") {
|
|
75
|
+
if ((_a = device.units) == null ? void 0 : _a.length) {
|
|
76
|
+
return combinerAcExportLimitW(device.units);
|
|
77
|
+
}
|
|
78
|
+
const limits2 = (_b = PROFILE_LIMITS[device.profile]) != null ? _b : PROFILE_LIMITS.solarbank3pro;
|
|
79
|
+
const n = Math.min(import_curtailmentTypes.COMBINER_MAX_UNITS, Math.max(1, Number(device.unitCount) || 1));
|
|
80
|
+
return limits2.combinerPerUnit * n;
|
|
81
|
+
}
|
|
82
|
+
const limits = (_c = PROFILE_LIMITS[device.profile]) != null ? _c : PROFILE_LIMITS.solarbank3pro;
|
|
83
|
+
return limits.standalone;
|
|
84
|
+
}
|
|
85
|
+
function parseCurtailmentDevicesJson(raw) {
|
|
86
|
+
if (!(raw == null ? void 0 : raw.trim())) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
if (!Array.isArray(parsed)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const item of parsed) {
|
|
96
|
+
if (!item || typeof item !== "object") {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const o = item;
|
|
100
|
+
const deviceId = typeof o.deviceId === "string" || typeof o.deviceId === "number" ? String(o.deviceId).trim() : "";
|
|
101
|
+
if (!deviceId) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const profileRaw = typeof o.profile === "string" ? o.profile : "solarbank3pro";
|
|
105
|
+
const profile = normalizeProfile(profileRaw);
|
|
106
|
+
const roleRaw = typeof o.role === "string" ? o.role : "standalone";
|
|
107
|
+
const role = roleRaw === "combiner" ? "combiner" : "standalone";
|
|
108
|
+
const batteryCapacityWh = Math.max(0, Number(o.batteryCapacityWh) || 0);
|
|
109
|
+
if (batteryCapacityWh <= 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const units = role === "combiner" ? parseUnitsList(o.units, profile) : void 0;
|
|
113
|
+
const unitCount = role === "combiner" && !(units == null ? void 0 : units.length) ? Math.min(import_curtailmentTypes.COMBINER_MAX_UNITS, Math.max(1, Number(o.unitCount) || 1)) : void 0;
|
|
114
|
+
out.push({
|
|
115
|
+
deviceId,
|
|
116
|
+
enabled: o.enabled !== false,
|
|
117
|
+
role,
|
|
118
|
+
profile,
|
|
119
|
+
batteryCapacityWh,
|
|
120
|
+
units: (units == null ? void 0 : units.length) ? units : void 0,
|
|
121
|
+
unitCount
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
acExportLimitW,
|
|
132
|
+
combinerAcExportLimitW,
|
|
133
|
+
parseCurtailmentDevicesJson
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=curtailmentProfiles.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/curtailmentProfiles.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CurtailmentDeviceConfig, CurtailmentDeviceProfile, CurtailmentDeviceRole } from \"./curtailmentTypes\";\nimport { COMBINER_MAX_UNITS } from \"./curtailmentTypes\";\n\n/**\n * Max AC export (W): standalone solarbank always 800 W (all models).\n * On combiner: per connected unit (solarbank2 1000, solarbank3pro 1200, solarbank4pro 2500).\n */\nconst PROFILE_LIMITS: Record<CurtailmentDeviceProfile, { standalone: number; combinerPerUnit: number }> = {\n\tsolarbank2: { standalone: 800, combinerPerUnit: 1000 },\n\tsolarbank3pro: { standalone: 800, combinerPerUnit: 1200 },\n\tsolarbank4pro: { standalone: 800, combinerPerUnit: 2500 },\n};\n\nfunction normalizeProfile(raw: string): CurtailmentDeviceProfile {\n\treturn raw in PROFILE_LIMITS ? (raw as CurtailmentDeviceProfile) : \"solarbank3pro\";\n}\n\n/** Sum combiner AC limits for up to 4 mixed solarbank profiles. */\nexport function combinerAcExportLimitW(units: CurtailmentDeviceProfile[]): number {\n\tif (!units.length) {\n\t\treturn 0;\n\t}\n\tlet sum = 0;\n\tfor (const profile of units.slice(0, COMBINER_MAX_UNITS)) {\n\t\tconst limits = PROFILE_LIMITS[profile] ?? PROFILE_LIMITS.solarbank3pro;\n\t\tsum += limits.combinerPerUnit;\n\t}\n\treturn sum;\n}\n\nfunction parseUnitsList(raw: unknown, fallbackProfile: CurtailmentDeviceProfile): CurtailmentDeviceProfile[] {\n\tif (!Array.isArray(raw)) {\n\t\treturn [];\n\t}\n\tconst units: CurtailmentDeviceProfile[] = [];\n\tfor (const entry of raw) {\n\t\tif (units.length >= COMBINER_MAX_UNITS) {\n\t\t\tbreak;\n\t\t}\n\t\tif (typeof entry === \"string\") {\n\t\t\tunits.push(normalizeProfile(entry));\n\t\t\tcontinue;\n\t\t}\n\t\tif (entry && typeof entry === \"object\") {\n\t\t\tconst p = (entry as Record<string, unknown>).profile;\n\t\t\tif (typeof p === \"string\") {\n\t\t\t\tunits.push(normalizeProfile(p));\n\t\t\t}\n\t\t}\n\t}\n\tif (!units.length && fallbackProfile) {\n\t\treturn [fallbackProfile];\n\t}\n\treturn units;\n}\n\nexport function acExportLimitW(device: CurtailmentDeviceConfig): number {\n\tif (device.role === \"combiner\") {\n\t\tif (device.units?.length) {\n\t\t\treturn combinerAcExportLimitW(device.units);\n\t\t}\n\t\t// Legacy: same profile \u00D7 unitCount\n\t\tconst limits = PROFILE_LIMITS[device.profile] ?? PROFILE_LIMITS.solarbank3pro;\n\t\tconst n = Math.min(COMBINER_MAX_UNITS, Math.max(1, Number(device.unitCount) || 1));\n\t\treturn limits.combinerPerUnit * n;\n\t}\n\tconst limits = PROFILE_LIMITS[device.profile] ?? PROFILE_LIMITS.solarbank3pro;\n\treturn limits.standalone;\n}\n\nexport function parseCurtailmentDevicesJson(raw: string): CurtailmentDeviceConfig[] {\n\tif (!raw?.trim()) {\n\t\treturn [];\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as unknown;\n\t\tif (!Array.isArray(parsed)) {\n\t\t\treturn [];\n\t\t}\n\t\tconst out: CurtailmentDeviceConfig[] = [];\n\t\tfor (const item of parsed) {\n\t\t\tif (!item || typeof item !== \"object\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst o = item as Record<string, unknown>;\n\t\t\tconst deviceId =\n\t\t\t\ttypeof o.deviceId === \"string\" || typeof o.deviceId === \"number\" ? String(o.deviceId).trim() : \"\";\n\t\t\tif (!deviceId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst profileRaw = typeof o.profile === \"string\" ? o.profile : \"solarbank3pro\";\n\t\t\tconst profile = normalizeProfile(profileRaw);\n\t\t\tconst roleRaw = typeof o.role === \"string\" ? o.role : \"standalone\";\n\t\t\tconst role: CurtailmentDeviceRole = roleRaw === \"combiner\" ? \"combiner\" : \"standalone\";\n\t\t\tconst batteryCapacityWh = Math.max(0, Number(o.batteryCapacityWh) || 0);\n\t\t\tif (batteryCapacityWh <= 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst units = role === \"combiner\" ? parseUnitsList(o.units, profile) : undefined;\n\t\t\tconst unitCount =\n\t\t\t\trole === \"combiner\" && !units?.length\n\t\t\t\t\t? Math.min(COMBINER_MAX_UNITS, Math.max(1, Number(o.unitCount) || 1))\n\t\t\t\t\t: undefined;\n\n\t\t\tout.push({\n\t\t\t\tdeviceId,\n\t\t\t\tenabled: o.enabled !== false,\n\t\t\t\trole,\n\t\t\t\tprofile,\n\t\t\t\tbatteryCapacityWh,\n\t\t\t\tunits: units?.length ? units : undefined,\n\t\t\t\tunitCount,\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t} catch {\n\t\treturn [];\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["limits"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var curtailmentRunner_exports = {};
|
|
20
|
+
__export(curtailmentRunner_exports, {
|
|
21
|
+
runCurtailmentAvoidance: () => runCurtailmentAvoidance
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(curtailmentRunner_exports);
|
|
24
|
+
var import_curtailmentProfiles = require("./curtailmentProfiles");
|
|
25
|
+
var import_curtailmentForecast = require("./curtailmentForecast");
|
|
26
|
+
var import_curtailmentStates = require("./curtailmentStates");
|
|
27
|
+
function berlinHour() {
|
|
28
|
+
var _a;
|
|
29
|
+
const parts = new Intl.DateTimeFormat("de-DE", {
|
|
30
|
+
timeZone: "Europe/Berlin",
|
|
31
|
+
hour: "numeric",
|
|
32
|
+
hour12: false
|
|
33
|
+
}).formatToParts(/* @__PURE__ */ new Date());
|
|
34
|
+
const h = (_a = parts.find((p) => p.type === "hour")) == null ? void 0 : _a.value;
|
|
35
|
+
return Math.min(23, Math.max(0, Number(h) || 0));
|
|
36
|
+
}
|
|
37
|
+
async function readSocPercent(host, deviceId) {
|
|
38
|
+
const candidates = [
|
|
39
|
+
`${host.namespace}.solarbank.${deviceId}.sensors.state_of_charge`,
|
|
40
|
+
`${host.namespace}.combiner_box.${deviceId}.sensors.state_of_charge`,
|
|
41
|
+
`${host.namespace}.combiner_box.${deviceId}.sensors.battery_soc`
|
|
42
|
+
];
|
|
43
|
+
for (const id of candidates) {
|
|
44
|
+
const st = await host.getStateAsync(id);
|
|
45
|
+
if ((st == null ? void 0 : st.val) !== null && (st == null ? void 0 : st.val) !== void 0) {
|
|
46
|
+
const n = Number(st.val);
|
|
47
|
+
if (!Number.isNaN(n)) {
|
|
48
|
+
return Math.min(100, Math.max(0, n));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
function calcMaxChargeW(batteryCapacityWh, socPercent, chargeDivisorHours) {
|
|
55
|
+
if (chargeDivisorHours <= 0 || batteryCapacityWh <= 0) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const missingWh = (100 - socPercent) / 100 * batteryCapacityWh;
|
|
59
|
+
return Math.max(0, Math.round(missingWh / chargeDivisorHours));
|
|
60
|
+
}
|
|
61
|
+
async function applyPhaseControls(host, device, phase, maxChargeW, modeBefore, modeAfter) {
|
|
62
|
+
const ctx = host.getDeviceContext(device.deviceId);
|
|
63
|
+
const controlDeviceId = device.deviceId;
|
|
64
|
+
if (phase === "before") {
|
|
65
|
+
await host.applyControl(controlDeviceId, "preset_usage_mode", modeBefore, ctx);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (phase === "active") {
|
|
69
|
+
await host.applyControl(controlDeviceId, "preset_usage_mode", "manual", ctx);
|
|
70
|
+
if (maxChargeW > 0) {
|
|
71
|
+
await host.applyControl(controlDeviceId, "ac_charge_limit", maxChargeW, ctx);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (phase === "after" || phase === "idle") {
|
|
76
|
+
await host.applyControl(controlDeviceId, "preset_usage_mode", modeAfter, ctx);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function runCurtailmentAvoidance(host, config) {
|
|
80
|
+
var _a;
|
|
81
|
+
if (!config.enabled) {
|
|
82
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.phase, "disabled", true);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const devices = (0, import_curtailmentProfiles.parseCurtailmentDevicesJson)(config.devicesJson).filter((d) => d.enabled);
|
|
86
|
+
if (!devices.length) {
|
|
87
|
+
host.log.debug("Curtailment avoidance: no enabled devices configured");
|
|
88
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.phase, "no_devices", true);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const basePath = (config.forecastBasePath || "solarprognose.0.forecast.00.hourly").trim();
|
|
92
|
+
const forecast = await (0, import_curtailmentForecast.readHourlyForecast)(basePath, (id) => host.getForeignStateAsync(id));
|
|
93
|
+
const nowHour = berlinHour();
|
|
94
|
+
for (const device of devices) {
|
|
95
|
+
const limit = (0, import_curtailmentProfiles.acExportLimitW)(device);
|
|
96
|
+
const window = (0, import_curtailmentForecast.detectCurtailmentWindow)(forecast, limit);
|
|
97
|
+
const phase = (0, import_curtailmentForecast.currentPhase)(window, nowHour);
|
|
98
|
+
const soc = await readSocPercent(host, device.deviceId);
|
|
99
|
+
const maxChargeW = window.today ? calcMaxChargeW(device.batteryCapacityWh, soc, window.chargeDivisorHours) : 0;
|
|
100
|
+
const remaining = (0, import_curtailmentForecast.remainingCurtailmentHours)(window, nowHour);
|
|
101
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.today, window.today, true);
|
|
102
|
+
await host.setState(
|
|
103
|
+
import_curtailmentStates.CURTAILMENT_STATE_IDS.start,
|
|
104
|
+
window.today ? `${window.startHour.toString().padStart(2, "0")}:00` : "",
|
|
105
|
+
true
|
|
106
|
+
);
|
|
107
|
+
await host.setState(
|
|
108
|
+
import_curtailmentStates.CURTAILMENT_STATE_IDS.end,
|
|
109
|
+
window.today ? `${window.endHour.toString().padStart(2, "0")}:00` : "",
|
|
110
|
+
true
|
|
111
|
+
);
|
|
112
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.maxChargeW, maxChargeW, true);
|
|
113
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.remainingHours, remaining, true);
|
|
114
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.phase, phase, true);
|
|
115
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.acLimitW, limit, true);
|
|
116
|
+
if (!window.today) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const unitsHint = device.role === "combiner" && ((_a = device.units) == null ? void 0 : _a.length) ? `, units=${device.units.join("+")} (${device.units.length} banks)` : "";
|
|
120
|
+
host.log.info(
|
|
121
|
+
`Curtailment [${device.deviceId}]: phase=${phase}, limit=${limit}W${unitsHint}, window ${window.startHour}-${window.endHour}h, maxCharge=${maxChargeW}W, SOC=${soc}%`
|
|
122
|
+
);
|
|
123
|
+
try {
|
|
124
|
+
await applyPhaseControls(host, device, phase, maxChargeW, config.modeBefore, config.modeAfter);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
host.log.warn(`Curtailment control failed for ${device.deviceId}: ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
131
|
+
0 && (module.exports = {
|
|
132
|
+
runCurtailmentAvoidance
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=curtailmentRunner.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/curtailmentRunner.ts"],
|
|
4
|
+
"sourcesContent": ["import { acExportLimitW, parseCurtailmentDevicesJson } from \"./curtailmentProfiles\";\nimport {\n\tcurrentPhase,\n\tdetectCurtailmentWindow,\n\treadHourlyForecast,\n\tremainingCurtailmentHours,\n} from \"./curtailmentForecast\";\nimport { CURTAILMENT_STATE_IDS } from \"./curtailmentStates\";\nimport type { CurtailmentDeviceConfig, CurtailmentPhase } from \"./curtailmentTypes\";\nimport type { DeviceControlContext } from \"./types\";\n\nexport interface CurtailmentRunnerConfig {\n\tenabled: boolean;\n\tforecastBasePath: string;\n\tdevicesJson: string;\n\t/** Usage mode before curtailment window (maximize export). */\n\tmodeBefore: \"smartmeter\" | \"smart\";\n\t/** Usage mode after curtailment window. */\n\tmodeAfter: \"smartmeter\" | \"smart\";\n}\n\nexport interface CurtailmentRunnerHost {\n\tnamespace: string;\n\tlog: {\n\t\tdebug: (msg: string) => void;\n\t\tinfo: (msg: string) => void;\n\t\twarn: (msg: string) => void;\n\t};\n\tgetForeignStateAsync: (id: string) => Promise<ioBroker.State | null | undefined>;\n\tgetStateAsync: (id: string) => Promise<ioBroker.State | null | undefined>;\n\tsetState: (id: string, val: unknown, ack?: boolean) => Promise<void>;\n\tgetDeviceContext: (deviceId: string) => DeviceControlContext | undefined;\n\tapplyControl: (\n\t\tdeviceId: string,\n\t\tcontrol: string,\n\t\tvalue: string | number | boolean,\n\t\tdeviceContext?: DeviceControlContext,\n\t) => Promise<void>;\n}\n\nfunction berlinHour(): number {\n\tconst parts = new Intl.DateTimeFormat(\"de-DE\", {\n\t\ttimeZone: \"Europe/Berlin\",\n\t\thour: \"numeric\",\n\t\thour12: false,\n\t}).formatToParts(new Date());\n\tconst h = parts.find(p => p.type === \"hour\")?.value;\n\treturn Math.min(23, Math.max(0, Number(h) || 0));\n}\n\nasync function readSocPercent(host: CurtailmentRunnerHost, deviceId: string): Promise<number> {\n\tconst candidates = [\n\t\t`${host.namespace}.solarbank.${deviceId}.sensors.state_of_charge`,\n\t\t`${host.namespace}.combiner_box.${deviceId}.sensors.state_of_charge`,\n\t\t`${host.namespace}.combiner_box.${deviceId}.sensors.battery_soc`,\n\t];\n\tfor (const id of candidates) {\n\t\tconst st = await host.getStateAsync(id);\n\t\tif (st?.val !== null && st?.val !== undefined) {\n\t\t\tconst n = Number(st.val);\n\t\t\tif (!Number.isNaN(n)) {\n\t\t\t\treturn Math.min(100, Math.max(0, n));\n\t\t\t}\n\t\t}\n\t}\n\treturn 0;\n}\n\nfunction calcMaxChargeW(batteryCapacityWh: number, socPercent: number, chargeDivisorHours: number): number {\n\tif (chargeDivisorHours <= 0 || batteryCapacityWh <= 0) {\n\t\treturn 0;\n\t}\n\tconst missingWh = ((100 - socPercent) / 100) * batteryCapacityWh;\n\treturn Math.max(0, Math.round(missingWh / chargeDivisorHours));\n}\n\nasync function applyPhaseControls(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tphase: CurtailmentPhase,\n\tmaxChargeW: number,\n\tmodeBefore: \"smartmeter\" | \"smart\",\n\tmodeAfter: \"smartmeter\" | \"smart\",\n): Promise<void> {\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tconst controlDeviceId = device.deviceId;\n\n\tif (phase === \"before\") {\n\t\tawait host.applyControl(controlDeviceId, \"preset_usage_mode\", modeBefore, ctx);\n\t\treturn;\n\t}\n\tif (phase === \"active\") {\n\t\tawait host.applyControl(controlDeviceId, \"preset_usage_mode\", \"manual\", ctx);\n\t\tif (maxChargeW > 0) {\n\t\t\tawait host.applyControl(controlDeviceId, \"ac_charge_limit\", maxChargeW, ctx);\n\t\t}\n\t\treturn;\n\t}\n\tif (phase === \"after\" || phase === \"idle\") {\n\t\tawait host.applyControl(controlDeviceId, \"preset_usage_mode\", modeAfter, ctx);\n\t}\n}\n\nexport async function runCurtailmentAvoidance(\n\thost: CurtailmentRunnerHost,\n\tconfig: CurtailmentRunnerConfig,\n): Promise<void> {\n\tif (!config.enabled) {\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.phase, \"disabled\", true);\n\t\treturn;\n\t}\n\n\tconst devices = parseCurtailmentDevicesJson(config.devicesJson).filter(d => d.enabled);\n\tif (!devices.length) {\n\t\thost.log.debug(\"Curtailment avoidance: no enabled devices configured\");\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.phase, \"no_devices\", true);\n\t\treturn;\n\t}\n\n\tconst basePath = (config.forecastBasePath || \"solarprognose.0.forecast.00.hourly\").trim();\n\tconst forecast = await readHourlyForecast(basePath, id => host.getForeignStateAsync(id));\n\tconst nowHour = berlinHour();\n\n\t// Use strictest (lowest) limit among enabled devices for shared forecast, or per-device loop\n\tfor (const device of devices) {\n\t\tconst limit = acExportLimitW(device);\n\t\tconst window = detectCurtailmentWindow(forecast, limit);\n\t\tconst phase = currentPhase(window, nowHour);\n\t\tconst soc = await readSocPercent(host, device.deviceId);\n\t\tconst maxChargeW = window.today ? calcMaxChargeW(device.batteryCapacityWh, soc, window.chargeDivisorHours) : 0;\n\t\tconst remaining = remainingCurtailmentHours(window, nowHour);\n\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.today, window.today, true);\n\t\tawait host.setState(\n\t\t\tCURTAILMENT_STATE_IDS.start,\n\t\t\twindow.today ? `${window.startHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\t\ttrue,\n\t\t);\n\t\tawait host.setState(\n\t\t\tCURTAILMENT_STATE_IDS.end,\n\t\t\twindow.today ? `${window.endHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\t\ttrue,\n\t\t);\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.maxChargeW, maxChargeW, true);\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.remainingHours, remaining, true);\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.phase, phase, true);\n\t\tawait host.setState(CURTAILMENT_STATE_IDS.acLimitW, limit, true);\n\n\t\tif (!window.today) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst unitsHint =\n\t\t\tdevice.role === \"combiner\" && device.units?.length\n\t\t\t\t? `, units=${device.units.join(\"+\")} (${device.units.length} banks)`\n\t\t\t\t: \"\";\n\t\thost.log.info(\n\t\t\t`Curtailment [${device.deviceId}]: phase=${phase}, limit=${limit}W${unitsHint}, ` +\n\t\t\t\t`window ${window.startHour}-${window.endHour}h, maxCharge=${maxChargeW}W, SOC=${soc}%`,\n\t\t);\n\n\t\ttry {\n\t\t\tawait applyPhaseControls(host, device, phase, maxChargeW, config.modeBefore, config.modeAfter);\n\t\t} catch (err) {\n\t\t\thost.log.warn(`Curtailment control failed for ${device.deviceId}: ${(err as Error).message}`);\n\t\t}\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var curtailmentStates_exports = {};
|
|
20
|
+
__export(curtailmentStates_exports, {
|
|
21
|
+
CURTAILMENT_CHANNEL: () => CURTAILMENT_CHANNEL,
|
|
22
|
+
CURTAILMENT_STATE_IDS: () => CURTAILMENT_STATE_IDS,
|
|
23
|
+
setupCurtailmentStates: () => setupCurtailmentStates
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(curtailmentStates_exports);
|
|
26
|
+
const CURTAILMENT_CHANNEL = "curtailment";
|
|
27
|
+
const CURTAILMENT_STATE_IDS = {
|
|
28
|
+
today: `${CURTAILMENT_CHANNEL}.today`,
|
|
29
|
+
start: `${CURTAILMENT_CHANNEL}.curtailment_start`,
|
|
30
|
+
end: `${CURTAILMENT_CHANNEL}.curtailment_end`,
|
|
31
|
+
maxChargeW: `${CURTAILMENT_CHANNEL}.max_charge_w`,
|
|
32
|
+
remainingHours: `${CURTAILMENT_CHANNEL}.remaining_hours`,
|
|
33
|
+
phase: `${CURTAILMENT_CHANNEL}.phase`,
|
|
34
|
+
acLimitW: `${CURTAILMENT_CHANNEL}.ac_limit_w`
|
|
35
|
+
};
|
|
36
|
+
async function setupCurtailmentStates(adapter) {
|
|
37
|
+
await adapter.setObjectNotExistsAsync(CURTAILMENT_CHANNEL, {
|
|
38
|
+
type: "channel",
|
|
39
|
+
common: { name: "Curtailment avoidance" },
|
|
40
|
+
native: {}
|
|
41
|
+
});
|
|
42
|
+
const states = [
|
|
43
|
+
{
|
|
44
|
+
id: CURTAILMENT_STATE_IDS.today,
|
|
45
|
+
common: {
|
|
46
|
+
name: "Curtailment expected today",
|
|
47
|
+
type: "boolean",
|
|
48
|
+
role: "indicator",
|
|
49
|
+
read: true,
|
|
50
|
+
write: false,
|
|
51
|
+
def: false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: CURTAILMENT_STATE_IDS.start,
|
|
56
|
+
common: {
|
|
57
|
+
name: "Curtailment window start (hour)",
|
|
58
|
+
type: "string",
|
|
59
|
+
role: "text",
|
|
60
|
+
read: true,
|
|
61
|
+
write: false,
|
|
62
|
+
def: ""
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: CURTAILMENT_STATE_IDS.end,
|
|
67
|
+
common: {
|
|
68
|
+
name: "Curtailment window end (hour)",
|
|
69
|
+
type: "string",
|
|
70
|
+
role: "text",
|
|
71
|
+
read: true,
|
|
72
|
+
write: false,
|
|
73
|
+
def: ""
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: CURTAILMENT_STATE_IDS.maxChargeW,
|
|
78
|
+
common: {
|
|
79
|
+
name: "Max charge power (calculated)",
|
|
80
|
+
type: "number",
|
|
81
|
+
role: "value.power",
|
|
82
|
+
unit: "W",
|
|
83
|
+
read: true,
|
|
84
|
+
write: false,
|
|
85
|
+
def: 0
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: CURTAILMENT_STATE_IDS.remainingHours,
|
|
90
|
+
common: {
|
|
91
|
+
name: "Remaining curtailment hours",
|
|
92
|
+
type: "number",
|
|
93
|
+
role: "value",
|
|
94
|
+
unit: "h",
|
|
95
|
+
read: true,
|
|
96
|
+
write: false,
|
|
97
|
+
def: 0
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: CURTAILMENT_STATE_IDS.phase,
|
|
102
|
+
common: {
|
|
103
|
+
name: "Curtailment phase",
|
|
104
|
+
type: "string",
|
|
105
|
+
role: "text",
|
|
106
|
+
read: true,
|
|
107
|
+
write: false,
|
|
108
|
+
def: "idle"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: CURTAILMENT_STATE_IDS.acLimitW,
|
|
113
|
+
common: {
|
|
114
|
+
name: "AC export limit (active group)",
|
|
115
|
+
type: "number",
|
|
116
|
+
role: "value.power",
|
|
117
|
+
unit: "W",
|
|
118
|
+
read: true,
|
|
119
|
+
write: false,
|
|
120
|
+
def: 0
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
for (const st of states) {
|
|
125
|
+
await adapter.setObjectNotExistsAsync(st.id, {
|
|
126
|
+
type: "state",
|
|
127
|
+
common: st.common,
|
|
128
|
+
native: {}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
133
|
+
0 && (module.exports = {
|
|
134
|
+
CURTAILMENT_CHANNEL,
|
|
135
|
+
CURTAILMENT_STATE_IDS,
|
|
136
|
+
setupCurtailmentStates
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=curtailmentStates.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/curtailmentStates.ts"],
|
|
4
|
+
"sourcesContent": ["export const CURTAILMENT_CHANNEL = \"curtailment\";\n\nexport const CURTAILMENT_STATE_IDS = {\n\ttoday: `${CURTAILMENT_CHANNEL}.today`,\n\tstart: `${CURTAILMENT_CHANNEL}.curtailment_start`,\n\tend: `${CURTAILMENT_CHANNEL}.curtailment_end`,\n\tmaxChargeW: `${CURTAILMENT_CHANNEL}.max_charge_w`,\n\tremainingHours: `${CURTAILMENT_CHANNEL}.remaining_hours`,\n\tphase: `${CURTAILMENT_CHANNEL}.phase`,\n\tacLimitW: `${CURTAILMENT_CHANNEL}.ac_limit_w`,\n} as const;\n\nexport async function setupCurtailmentStates(adapter: ioBroker.Adapter): Promise<void> {\n\tawait adapter.setObjectNotExistsAsync(CURTAILMENT_CHANNEL, {\n\t\ttype: \"channel\",\n\t\tcommon: { name: \"Curtailment avoidance\" },\n\t\tnative: {},\n\t});\n\n\tconst states: Array<{\n\t\tid: string;\n\t\tcommon: ioBroker.StateCommon;\n\t}> = [\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.today,\n\t\t\tcommon: {\n\t\t\t\tname: \"Curtailment expected today\",\n\t\t\t\ttype: \"boolean\",\n\t\t\t\trole: \"indicator\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.start,\n\t\t\tcommon: {\n\t\t\t\tname: \"Curtailment window start (hour)\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"text\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.end,\n\t\t\tcommon: {\n\t\t\t\tname: \"Curtailment window end (hour)\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"text\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.maxChargeW,\n\t\t\tcommon: {\n\t\t\t\tname: \"Max charge power (calculated)\",\n\t\t\t\ttype: \"number\",\n\t\t\t\trole: \"value.power\",\n\t\t\t\tunit: \"W\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.remainingHours,\n\t\t\tcommon: {\n\t\t\t\tname: \"Remaining curtailment hours\",\n\t\t\t\ttype: \"number\",\n\t\t\t\trole: \"value\",\n\t\t\t\tunit: \"h\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.phase,\n\t\t\tcommon: {\n\t\t\t\tname: \"Curtailment phase\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"text\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: \"idle\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: CURTAILMENT_STATE_IDS.acLimitW,\n\t\t\tcommon: {\n\t\t\t\tname: \"AC export limit (active group)\",\n\t\t\t\ttype: \"number\",\n\t\t\t\trole: \"value.power\",\n\t\t\t\tunit: \"W\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: 0,\n\t\t\t},\n\t\t},\n\t];\n\n\tfor (const st of states) {\n\t\tawait adapter.setObjectNotExistsAsync(st.id, {\n\t\t\ttype: \"state\",\n\t\t\tcommon: st.common,\n\t\t\tnative: {},\n\t\t});\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,MAAM,sBAAsB;AAE5B,MAAM,wBAAwB;AAAA,EACpC,OAAO,GAAG,mBAAmB;AAAA,EAC7B,OAAO,GAAG,mBAAmB;AAAA,EAC7B,KAAK,GAAG,mBAAmB;AAAA,EAC3B,YAAY,GAAG,mBAAmB;AAAA,EAClC,gBAAgB,GAAG,mBAAmB;AAAA,EACtC,OAAO,GAAG,mBAAmB;AAAA,EAC7B,UAAU,GAAG,mBAAmB;AACjC;AAEA,eAAsB,uBAAuB,SAA0C;AACtF,QAAM,QAAQ,wBAAwB,qBAAqB;AAAA,IAC1D,MAAM;AAAA,IACN,QAAQ,EAAE,MAAM,wBAAwB;AAAA,IACxC,QAAQ,CAAC;AAAA,EACV,CAAC;AAED,QAAM,SAGD;AAAA,IACJ;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI,sBAAsB;AAAA,MAC1B,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,IACD;AAAA,EACD;AAEA,aAAW,MAAM,QAAQ;AACxB,UAAM,QAAQ,wBAAwB,GAAG,IAAI;AAAA,MAC5C,MAAM;AAAA,MACN,QAAQ,GAAG;AAAA,MACX,QAAQ,CAAC;AAAA,IACV,CAAC;AAAA,EACF;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var curtailmentTypes_exports = {};
|
|
20
|
+
__export(curtailmentTypes_exports, {
|
|
21
|
+
COMBINER_MAX_UNITS: () => COMBINER_MAX_UNITS
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(curtailmentTypes_exports);
|
|
24
|
+
const COMBINER_MAX_UNITS = 4;
|
|
25
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
26
|
+
0 && (module.exports = {
|
|
27
|
+
COMBINER_MAX_UNITS
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=curtailmentTypes.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/curtailmentTypes.ts"],
|
|
4
|
+
"sourcesContent": ["/** Device profile for AC export (inverter) limits used in forecast comparison. */\nexport type CurtailmentDeviceProfile = \"solarbank2\" | \"solarbank3pro\" | \"solarbank4pro\";\n\nexport type CurtailmentDeviceRole = \"standalone\" | \"combiner\";\n\n/** Max solarbanks per combiner (Anker Power Dock). */\nexport const COMBINER_MAX_UNITS = 4;\n\nexport interface CurtailmentDeviceConfig {\n\tdeviceId: string;\n\tenabled: boolean;\n\trole: CurtailmentDeviceRole;\n\t/** Standalone solarbank profile, or fallback when combiner has no `units` list. */\n\tprofile: CurtailmentDeviceProfile;\n\t/** Total battery capacity (Wh) for this unit or combiner system. */\n\tbatteryCapacityWh: number;\n\t/**\n\t * Combiner only: each connected solarbank profile (max 4).\n\t * Total AC export limit = sum of per-unit combiner limits.\n\t */\n\tunits?: CurtailmentDeviceProfile[];\n\t/** @deprecated Use `units` with per-bank profiles. Kept for backward compatibility. */\n\tunitCount?: number;\n}\n\nexport type CurtailmentPhase = \"idle\" | \"before\" | \"active\" | \"after\";\n\nexport interface CurtailmentWindow {\n\ttoday: boolean;\n\tstartHour: number;\n\tendHour: number;\n\tdurationHours: number;\n\t/** Hours used in charge formula (duration + 1). */\n\tchargeDivisorHours: number;\n}\n\nexport interface HourlyForecast {\n\t/** Hour 0\u201323 \u2192 expected PV power (W). */\n\thours: Map<number, number>;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMO,MAAM,qBAAqB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/build/main.js
CHANGED
|
@@ -26,6 +26,8 @@ var path = __toESM(require("node:path"));
|
|
|
26
26
|
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
27
27
|
var import_configHelpers = require("./lib/configHelpers");
|
|
28
28
|
var import_controlQueue = require("./lib/controlQueue");
|
|
29
|
+
var import_curtailmentRunner = require("./lib/curtailmentRunner");
|
|
30
|
+
var import_curtailmentStates = require("./lib/curtailmentStates");
|
|
29
31
|
var import_ensurePython = require("./lib/ensurePython");
|
|
30
32
|
var import_pythonBridge = require("./lib/pythonBridge");
|
|
31
33
|
var import_services = require("./lib/services");
|
|
@@ -174,6 +176,7 @@ class AnkerSolix extends utils.Adapter {
|
|
|
174
176
|
const detailHint = result.refreshDetails ? "devices+mqtt" : "sites";
|
|
175
177
|
const intervalHint = result.intervalcount !== void 0 && result.deviceintervals !== void 0 ? `, next detail in ~${result.intervalcount} polls` : "";
|
|
176
178
|
this.log.debug(`Poll OK (${(_c = pollDevices == null ? void 0 : pollDevices.length) != null ? _c : 0} devices, ${detailHint}${intervalHint})`);
|
|
179
|
+
await this.runCurtailmentAvoidanceIfEnabled();
|
|
177
180
|
} catch (error) {
|
|
178
181
|
await this.setState("info.connection", false, true);
|
|
179
182
|
const msg = error.message || String(error);
|
|
@@ -272,6 +275,51 @@ class AnkerSolix extends utils.Adapter {
|
|
|
272
275
|
});
|
|
273
276
|
}
|
|
274
277
|
}
|
|
278
|
+
async applyAdapterControl(deviceId, control, value, deviceContext) {
|
|
279
|
+
await (0, import_pythonBridge.runBridge)(
|
|
280
|
+
"set",
|
|
281
|
+
{
|
|
282
|
+
...this.getBridgeConfig(),
|
|
283
|
+
deviceId,
|
|
284
|
+
control,
|
|
285
|
+
value,
|
|
286
|
+
deviceContext
|
|
287
|
+
},
|
|
288
|
+
this.config.pythonPath || "",
|
|
289
|
+
this.log
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
getCurtailmentHost() {
|
|
293
|
+
return {
|
|
294
|
+
namespace: this.namespace,
|
|
295
|
+
log: this.log,
|
|
296
|
+
getForeignStateAsync: (id) => this.getForeignStateAsync(id),
|
|
297
|
+
getStateAsync: (id) => this.getStateAsync(id),
|
|
298
|
+
setState: async (id, val, ack) => {
|
|
299
|
+
await this.setState(id, val, ack != null ? ack : true);
|
|
300
|
+
},
|
|
301
|
+
getDeviceContext: (deviceId) => this.deviceContexts.get(deviceId),
|
|
302
|
+
applyControl: (deviceId, control, value, deviceContext) => this.applyAdapterControl(deviceId, control, value, deviceContext)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async runCurtailmentAvoidanceIfEnabled() {
|
|
306
|
+
if (!this.config.enableCurtailmentAvoidance) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const modeBefore = this.config.curtailmentModeBefore === "smart" ? "smart" : "smartmeter";
|
|
311
|
+
const modeAfter = this.config.curtailmentModeAfter === "smart" ? "smart" : "smartmeter";
|
|
312
|
+
await (0, import_curtailmentRunner.runCurtailmentAvoidance)(this.getCurtailmentHost(), {
|
|
313
|
+
enabled: true,
|
|
314
|
+
forecastBasePath: (this.config.curtailmentForecastPath || "solarprognose.0.forecast.00.hourly").trim(),
|
|
315
|
+
devicesJson: this.config.curtailmentDevicesJson || "[]",
|
|
316
|
+
modeBefore,
|
|
317
|
+
modeAfter
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
this.log.warn(`Curtailment avoidance: ${err.message}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
275
323
|
schedulePollAfterControl() {
|
|
276
324
|
if (this.pollAfterControlTimer) {
|
|
277
325
|
clearTimeout(this.pollAfterControlTimer);
|
|
@@ -423,6 +471,7 @@ class AnkerSolix extends utils.Adapter {
|
|
|
423
471
|
native: {}
|
|
424
472
|
});
|
|
425
473
|
await (0, import_services.setupServiceStates)(this);
|
|
474
|
+
await (0, import_curtailmentStates.setupCurtailmentStates)(this);
|
|
426
475
|
await this.setState("info.connection", false, true);
|
|
427
476
|
const intervalSec = Math.max(30, Number(this.config.scanInterval) || 60);
|
|
428
477
|
this.log.info(
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["/*\n * ioBroker Anker Solix adapter\n * Based on https://github.com/thomluther/ha-anker-solix (Home Assistant integration)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport * as utils from \"@iobroker/adapter-core\";\n\nimport { parseSelectedDeviceIds } from \"./lib/configHelpers\";\nimport { ControlQueue } from \"./lib/controlQueue\";\nimport { runPythonInstaller } from \"./lib/ensurePython\";\nimport { ensureBridgeDaemon, runBridge, stopBridgeDaemon } from \"./lib/pythonBridge\";\nimport { SERVICE_STATES, setupServiceStates } from \"./lib/services\";\nimport { parseControlStateId, syncDevices } from \"./lib/stateSync\";\nimport type { BridgeConfig, BridgeDevice, BridgeServiceConfig, DeviceControlContext } from \"./lib/types\";\n\nclass AnkerSolix extends utils.Adapter {\n\tprivate pollTimer: ioBroker.Interval | undefined;\n\tprivate readonly controlQueue = new ControlQueue();\n\tprivate readonly deviceContexts = new Map<string, DeviceControlContext>();\n\tprivate pollAfterControlTimer: NodeJS.Timeout | undefined;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"anker-solix\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate getAuthCacheDir(): string {\n\t\treturn path.join(utils.getAbsoluteInstanceDataDir(this), \"authcache\");\n\t}\n\n\tprivate getAuthCacheFile(): string {\n\t\tconst email = (this.config.username || \"\").trim();\n\t\treturn path.join(this.getAuthCacheDir(), `${email}.json`);\n\t}\n\n\tprivate logAuthCacheStatus(): void {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst cacheFile = this.getAuthCacheFile();\n\t\tconst email = (this.config.username || \"\").trim();\n\t\tif (!email) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(cacheDir, { recursive: true });\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Cannot create authcache folder ${cacheDir}: ${(err as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (fs.existsSync(cacheFile)) {\n\t\t\tthis.log.debug(`Anker login cache present: ${cacheFile}`);\n\t\t\treturn;\n\t\t}\n\t\tlet other = \"\";\n\t\ttry {\n\t\t\tconst names = fs.readdirSync(cacheDir).filter(f => f.endsWith(\".json\"));\n\t\t\tif (names.length) {\n\t\t\t\tother = ` Found other file(s) in folder: ${names.join(\", \")} (username must match filename).`;\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t\tthis.log.warn(\n\t\t\t`No Anker login cache at ${cacheFile}.${other} ` +\n\t\t\t\t\"Without this file every adapter restart triggers a new API login (often captcha 100032). \" +\n\t\t\t\t\"Copy <email>.json from a working Anker/Solix integration (e.g. ha-anker-solix) into that folder, then restart.\",\n\t\t);\n\t}\n\n\tprivate getBridgeConfig(): BridgeConfig {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst selectedIds = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\n\t\treturn {\n\t\t\tusername: this.config.username,\n\t\t\tpassword: this.config.password,\n\t\t\tcountry: this.config.country || \"DE\",\n\t\t\tmqttUsage: this.config.mqttUsage !== false,\n\t\t\tcacheDir,\n\t\t\tenableAllDevices: this.config.enableAllDevices !== false,\n\t\t\tselectedSiteId: this.config.selectedSiteId || \"\",\n\t\t\tselectedDeviceIds: selectedIds,\n\t\t\tdeviceDetailMultiplier: Math.max(1, Number(this.config.deviceDetailMultiplier) || 10),\n\t\t\trequestDelay: Number(this.config.requestDelay) || 0.3,\n\t\t\trequestTimeout: Number(this.config.requestTimeout) || 10,\n\t\t\tendpointLimit: Number(this.config.endpointLimit) || 10,\n\t\t\tenableCoreEntities: this.config.enableCoreEntities !== false,\n\t\t\tenableEnergyStatistics: !!this.config.enableEnergyStatistics,\n\t\t\tenableEnergyDetail: !!this.config.enableEnergyDetail,\n\t\t\tenablePowerFlows: !!this.config.enablePowerFlows,\n\t\t\tenableDiagnostics: !!this.config.enableDiagnostics,\n\t\t\tenableBinaryIndicators: !!this.config.enableBinaryIndicators,\n\t\t\tenableAdvancedControls: !!this.config.enableAdvancedControls,\n\t\t\tenableSystemOverview: !!this.config.enableSystemOverview,\n\t\t\tenableSitePrice: !!this.config.enableSitePrice,\n\t\t\tenableAccountInfo: !!this.config.enableAccountInfo,\n\t\t\tenableSolarbankMeta: !!this.config.enableSolarbankMeta,\n\t\t\tenableSmartplug: !!this.config.enableSmartplug,\n\t\t\tenablePps: !!this.config.enablePps,\n\t\t\tenableEvCharger: !!this.config.enableEvCharger,\n\t\t\tenableVehicle: !!this.config.enableVehicle,\n\t\t\tenableHes: !!this.config.enableHes,\n\t\t\tenablePowerPanel: !!this.config.enablePowerPanel,\n\t\t\tenableInverter: !!this.config.enableInverter,\n\t\t};\n\t}\n\n\t/** Remove legacy install symlink from old GitHub repo name \"AnkerSolix\". */\n\tprivate cleanupLegacyInstallSymlink(): void {\n\t\tconst alias = path.join(this.adapterDir, \"..\", \"iobroker.AnkerSolix\");\n\t\ttry {\n\t\t\tif (fs.existsSync(alias) && fs.lstatSync(alias).isSymbolicLink()) {\n\t\t\t\tfs.unlinkSync(alias);\n\t\t\t\tthis.log.info(\"Removed legacy symlink iobroker.AnkerSolix\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tprivate async ensurePythonDeps(force = false): Promise<boolean> {\n\t\tif (!force && this.config.autoInstallPython === false) {\n\t\t\treturn true;\n\t\t}\n\t\tconst result = await runPythonInstaller(this.config.pythonPath || \"\", this.log);\n\t\tawait this.setState(\"info.pythonReady\", result.ok, true);\n\t\tif (!result.ok) {\n\t\t\tthis.log.warn(`Python setup: ${result.message}`);\n\t\t}\n\t\treturn result.ok;\n\t}\n\n\tprivate async pollOnce(): Promise<void> {\n\t\tif (!this.config.acceptTerms) {\n\t\t\tthis.log.warn(\"Please accept the usage terms in the adapter configuration.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.config.username?.trim()) {\n\t\t\tthis.log.warn(\"Anker e-mail (username) is required in adapter settings.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.config.password?.trim()) {\n\t\t\tthis.log.warn(\"Password missing \u2013 open instance config in Admin, re-enter Anker password and save.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!(await this.ensurePythonDeps())) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runBridge(\"poll\", this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\t\tconst pollDevices = result.devices as BridgeDevice[] | undefined;\n\t\t\tif (pollDevices?.length) {\n\t\t\t\tthis.rememberDeviceContexts(pollDevices);\n\t\t\t\tawait syncDevices(this, pollDevices);\n\t\t\t}\n\n\t\t\tif (result.nickname) {\n\t\t\t\tawait this.setState(\"account.nickname\", result.nickname, true);\n\t\t\t}\n\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t\tconst detailHint = result.refreshDetails ? \"devices+mqtt\" : \"sites\";\n\t\t\tconst intervalHint =\n\t\t\t\tresult.intervalcount !== undefined && result.deviceintervals !== undefined\n\t\t\t\t\t? `, next detail in ~${result.intervalcount} polls`\n\t\t\t\t\t: \"\";\n\t\t\tthis.log.debug(`Poll OK (${pollDevices?.length ?? 0} devices, ${detailHint}${intervalHint})`);\n\t\t} catch (error) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\tconst msg = (error as Error).message || String(error);\n\t\t\tif (msg.includes(\"CaptchaRequired\") || msg.includes(\"100032\") || msg.toLowerCase().includes(\"captcha\")) {\n\t\t\t\tconst cacheFile = this.getAuthCacheFile();\n\t\t\t\tconst missing = !fs.existsSync(cacheFile);\n\t\t\t\tconst hint = missing\n\t\t\t\t\t? `Erwartete Datei: ${cacheFile} \u2013 von funktionierender Anker/Solix-Integration (z. B. ha-anker-solix) dorthin kopieren, Ordner anlegen falls n\u00F6tig, Adapter neu starten.`\n\t\t\t\t\t: `Cache vorhanden aber ung\u00FCltig: ${cacheFile} \u2013 frische Datei von HA kopieren oder Passwort in Admin neu speichern.`;\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 API-Neulogin n\u00F6tig${missing ? \" (kein Login-Cache)\" : \"\"}. ${hint}`,\n\t\t\t\t);\n\t\t\t\tif (missing) {\n\t\t\t\t\tthis.logAuthCacheStatus();\n\t\t\t\t}\n\t\t\t} else if (msg.includes(\"Cached Anker login is invalid\") || msg.includes(\"invalidated by the mobile app\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Gespeicherter API-Token ung\u00FCltig (abgelaufen oder durch App ersetzt). ` +\n\t\t\t\t\t\t\"Nicht \u201ECache l\u00F6schen\u201C \u2013 stattdessen frische authcache-Datei von HA kopieren oder App kurz abmelden, dann neu starten.\",\n\t\t\t\t);\n\t\t\t} else if (msg.includes(\"InvalidCredentials\") || msg.includes(\"Authentication failed\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Check e-mail, password and country (${this.config.country || \"DE\"}). ` +\n\t\t\t\t\t\t\"In Admin use \u201CInstall Python dependencies\u201D tab or restart after saving config; \" +\n\t\t\t\t\t\t\"try country matching your Anker account region.\",\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tmsg.includes(\"26161\") ||\n\t\t\t\tmsg.includes(\"429\") ||\n\t\t\t\tmsg.includes(\"Too Many Requests\") ||\n\t\t\t\tmsg.includes(\"Failed to request\")\n\t\t\t) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`Poll failed (Anker API limit or temporary error): ${msg} \u2013 ` +\n\t\t\t\t\t\t\"adapter will retry; increase scan interval (e.g. 120 s) if this persists.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.log.error(`Poll failed: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getPrimaryDeviceId(): string {\n\t\tconst selected = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\t\tif (selected[0]) {\n\t\t\treturn selected[0];\n\t\t}\n\t\ttry {\n\t\t\tconst list = JSON.parse(this.config.deviceListJson || '{\"devices\":[]}') as {\n\t\t\t\tdevices?: Array<{ id: string }>;\n\t\t\t};\n\t\t\treturn list.devices?.[0]?.id || \"\";\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\tprivate async handleServiceTrigger(stateId: string): Promise<void> {\n\t\tconst ns = `${this.namespace}.services.`;\n\t\tif (!stateId.startsWith(ns)) {\n\t\t\treturn;\n\t\t}\n\t\tconst action = stateId.slice(ns.length);\n\n\t\tif (action === \"refresh_devices\") {\n\t\t\tawait this.pollOnce();\n\t\t\tawait this.setState(stateId, false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst serviceActions = [\"get_schedule\", \"clear_schedule\", \"export_systems\", \"get_system_info\"];\n\t\tif (!serviceActions.includes(action)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst params: Record<string, unknown> = {\n\t\t\tdeviceId: this.getPrimaryDeviceId(),\n\t\t\tsiteId: this.config.selectedSiteId || \"\",\n\t\t\tincludeMqtt: this.config.mqttUsage !== false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst serviceConfig: BridgeServiceConfig = {\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tservice: action,\n\t\t\t\tparams,\n\t\t\t};\n\t\t\tconst result = await runBridge(\"service\", serviceConfig, this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (action === \"get_schedule\" && result.schedule !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.scheduleJson, JSON.stringify(result.schedule, null, 2), true);\n\t\t\t}\n\t\t\tif (action === \"export_systems\" && result.path) {\n\t\t\t\tawait this.setState(SERVICE_STATES.exportResult, String(result.path), true);\n\t\t\t}\n\t\t\tif (action === \"get_system_info\" && result.system !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.systemInfo, JSON.stringify(result.system, null, 2), true);\n\t\t\t}\n\n\t\t\tawait this.setState(stateId, false, true);\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Service ${action} failed: ${(error as Error).message}`);\n\t\t\tawait this.setState(stateId, false, true);\n\t\t}\n\t}\n\n\tprivate rememberDeviceContexts(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tconst info = device.info;\n\t\t\tthis.deviceContexts.set(info.id, {\n\t\t\t\ttype: info.type,\n\t\t\t\tsite_id: info.site_id,\n\t\t\t\tdevice_pn: info.device_pn || info.model || \"\",\n\t\t\t\tstation_sn: info.station_sn || \"\",\n\t\t\t\tgeneration: info.generation ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate schedulePollAfterControl(): void {\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t}\n\t\tthis.pollAfterControlTimer = setTimeout(() => {\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t\tvoid this.pollOnce();\n\t\t}, 12_000);\n\t}\n\n\tprivate async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n\t\tif (!state || state.ack) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (id.startsWith(`${this.namespace}.services.`) && state.val === true) {\n\t\t\tawait this.handleServiceTrigger(id);\n\t\t\treturn;\n\t\t}\n\n\t\tconst control = parseControlStateId(this.namespace, id);\n\t\tif (!control) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst current = await this.getStateAsync(id);\n\t\tif (\n\t\t\tcurrent?.ack &&\n\t\t\tcurrent.val !== null &&\n\t\t\tcurrent.val !== undefined &&\n\t\t\tString(current.val) === String(state.val)\n\t\t) {\n\t\t\tawait this.setState(id, { val: state.val, ack: true });\n\t\t\treturn;\n\t\t}\n\n\t\tconst value = state.val as string | number | boolean;\n\t\tconst deviceContext = this.deviceContexts.get(control.deviceId);\n\n\t\tthis.controlQueue.enqueue({\n\t\t\tstateId: id,\n\t\t\texecute: async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runBridge(\n\t\t\t\t\t\t\"set\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\t\t\t\tdeviceId: control.deviceId,\n\t\t\t\t\t\t\tcontrol: control.control,\n\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\tdeviceContext,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\t\tthis.log,\n\t\t\t\t\t);\n\t\t\t\t\tawait this.setState(id, { val: value, ack: true });\n\t\t\t\t\tthis.log.info(`Applied ${control.control} on ${control.deviceId}`);\n\t\t\t\t\tthis.schedulePollAfterControl();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = (error as Error).message;\n\t\t\t\t\tif (message.includes(\"429\") || message.includes(\"Too Many Requests\")) {\n\t\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t\t`Control rate-limited for ${id} \u2013 wait ~1 minute before retrying (Anker API limit).`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.error(`Control failed for ${id}: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.setState(id, { val: value, ack: false });\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj?.command) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst respond = (response: unknown): void => {\n\t\t\tif (obj.callback) {\n\t\t\t\tthis.sendTo(obj.from, obj.command, response, obj.callback);\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tif (obj.command === \"clearAuthCache\") {\n\t\t\t\tconst cacheDir = this.getAuthCacheDir();\n\t\t\t\tconst fs = await import(\"node:fs/promises\");\n\t\t\t\ttry {\n\t\t\t\t\tconst files = await fs.readdir(cacheDir);\n\t\t\t\t\tawait Promise.all(files.map(f => fs.unlink(path.join(cacheDir, f)).catch(() => undefined)));\n\t\t\t\t\tawait stopBridgeDaemon();\n\t\t\t\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache cleared (${files.length} file(s) in ${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Next poll requires a new API login; on many hosts Anker returns captcha (100032). \" +\n\t\t\t\t\t\t\t`Restore authcache/${(this.config.username || \"\").trim()}.json from HA or retry login when cloud allows it.`,\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: files.length });\n\t\t\t\t} catch {\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache clear requested but folder empty or missing (${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Adapter must complete a successful API login to create authcache/<email>.json.\",\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: 0 });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"installPython\") {\n\t\t\t\tconst ok = await this.ensurePythonDeps(true);\n\t\t\t\trespond({ ok });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"loadDevices\") {\n\t\t\t\tif (!this.config.username || !this.config.password) {\n\t\t\t\t\trespond({ error: \"Credentials required\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.ensurePythonDeps();\n\t\t\t\tconst result = await runBridge(\n\t\t\t\t\t\"list_devices\",\n\t\t\t\t\tthis.getBridgeConfig(),\n\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\tthis.log,\n\t\t\t\t);\n\t\t\t\tconst payload = {\n\t\t\t\t\tsites: result.sites || [],\n\t\t\t\t\tdevices: result.devices || [],\n\t\t\t\t};\n\t\t\t\trespond({ ok: true, deviceListJson: JSON.stringify(payload, null, 2), ...payload });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trespond({ error: `Unknown command ${obj.command}` });\n\t\t} catch (error) {\n\t\t\trespond({ error: (error as Error).message });\n\t\t}\n\t}\n\n\tprivate async onReady(): Promise<void> {\n\t\tthis.cleanupLegacyInstallSymlink();\n\n\t\tawait this.setObjectNotExistsAsync(\"account\", {\n\t\t\ttype: \"channel\",\n\t\t\tcommon: { name: \"Account\" },\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"account.nickname\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Account nickname\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"info\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"info.pythonReady\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Python dependencies ready\",\n\t\t\t\ttype: \"boolean\",\n\t\t\t\trole: \"indicator\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\n\t\tawait setupServiceStates(this);\n\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\tconst intervalSec = Math.max(30, Number(this.config.scanInterval) || 60);\n\t\tthis.log.info(\n\t\t\t`Anker Solix adapter started (poll every ${intervalSec}s, MQTT: ${this.config.mqttUsage !== false})`,\n\t\t);\n\n\t\tawait this.ensurePythonDeps();\n\n\t\tthis.logAuthCacheStatus();\n\n\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\tthis.subscribeStates(`${this.namespace}.*.control.*`);\n\t\tthis.subscribeStates(`${this.namespace}.services.*`);\n\n\t\tawait this.pollOnce();\n\t\tthis.pollTimer = this.setInterval(() => {\n\t\t\tvoid this.pollOnce();\n\t\t}, intervalSec * 1000);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tif (this.pollTimer) {\n\t\t\tthis.clearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t}\n\t\tvoid stopBridgeDaemon().finally(() => callback());\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AnkerSolix(options);\n} else {\n\t(() => new AnkerSolix())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/*\n * ioBroker Anker Solix adapter\n * Based on https://github.com/thomluther/ha-anker-solix (Home Assistant integration)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport * as utils from \"@iobroker/adapter-core\";\n\nimport { parseSelectedDeviceIds } from \"./lib/configHelpers\";\nimport { ControlQueue } from \"./lib/controlQueue\";\nimport { runCurtailmentAvoidance, type CurtailmentRunnerHost } from \"./lib/curtailmentRunner\";\nimport { setupCurtailmentStates } from \"./lib/curtailmentStates\";\nimport { runPythonInstaller } from \"./lib/ensurePython\";\nimport { ensureBridgeDaemon, runBridge, stopBridgeDaemon } from \"./lib/pythonBridge\";\nimport { SERVICE_STATES, setupServiceStates } from \"./lib/services\";\nimport { parseControlStateId, syncDevices } from \"./lib/stateSync\";\nimport type { BridgeConfig, BridgeDevice, BridgeServiceConfig, DeviceControlContext } from \"./lib/types\";\n\nclass AnkerSolix extends utils.Adapter {\n\tprivate pollTimer: ioBroker.Interval | undefined;\n\tprivate readonly controlQueue = new ControlQueue();\n\tprivate readonly deviceContexts = new Map<string, DeviceControlContext>();\n\tprivate pollAfterControlTimer: NodeJS.Timeout | undefined;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"anker-solix\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate getAuthCacheDir(): string {\n\t\treturn path.join(utils.getAbsoluteInstanceDataDir(this), \"authcache\");\n\t}\n\n\tprivate getAuthCacheFile(): string {\n\t\tconst email = (this.config.username || \"\").trim();\n\t\treturn path.join(this.getAuthCacheDir(), `${email}.json`);\n\t}\n\n\tprivate logAuthCacheStatus(): void {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst cacheFile = this.getAuthCacheFile();\n\t\tconst email = (this.config.username || \"\").trim();\n\t\tif (!email) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(cacheDir, { recursive: true });\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Cannot create authcache folder ${cacheDir}: ${(err as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (fs.existsSync(cacheFile)) {\n\t\t\tthis.log.debug(`Anker login cache present: ${cacheFile}`);\n\t\t\treturn;\n\t\t}\n\t\tlet other = \"\";\n\t\ttry {\n\t\t\tconst names = fs.readdirSync(cacheDir).filter(f => f.endsWith(\".json\"));\n\t\t\tif (names.length) {\n\t\t\t\tother = ` Found other file(s) in folder: ${names.join(\", \")} (username must match filename).`;\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t\tthis.log.warn(\n\t\t\t`No Anker login cache at ${cacheFile}.${other} ` +\n\t\t\t\t\"Without this file every adapter restart triggers a new API login (often captcha 100032). \" +\n\t\t\t\t\"Copy <email>.json from a working Anker/Solix integration (e.g. ha-anker-solix) into that folder, then restart.\",\n\t\t);\n\t}\n\n\tprivate getBridgeConfig(): BridgeConfig {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst selectedIds = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\n\t\treturn {\n\t\t\tusername: this.config.username,\n\t\t\tpassword: this.config.password,\n\t\t\tcountry: this.config.country || \"DE\",\n\t\t\tmqttUsage: this.config.mqttUsage !== false,\n\t\t\tcacheDir,\n\t\t\tenableAllDevices: this.config.enableAllDevices !== false,\n\t\t\tselectedSiteId: this.config.selectedSiteId || \"\",\n\t\t\tselectedDeviceIds: selectedIds,\n\t\t\tdeviceDetailMultiplier: Math.max(1, Number(this.config.deviceDetailMultiplier) || 10),\n\t\t\trequestDelay: Number(this.config.requestDelay) || 0.3,\n\t\t\trequestTimeout: Number(this.config.requestTimeout) || 10,\n\t\t\tendpointLimit: Number(this.config.endpointLimit) || 10,\n\t\t\tenableCoreEntities: this.config.enableCoreEntities !== false,\n\t\t\tenableEnergyStatistics: !!this.config.enableEnergyStatistics,\n\t\t\tenableEnergyDetail: !!this.config.enableEnergyDetail,\n\t\t\tenablePowerFlows: !!this.config.enablePowerFlows,\n\t\t\tenableDiagnostics: !!this.config.enableDiagnostics,\n\t\t\tenableBinaryIndicators: !!this.config.enableBinaryIndicators,\n\t\t\tenableAdvancedControls: !!this.config.enableAdvancedControls,\n\t\t\tenableSystemOverview: !!this.config.enableSystemOverview,\n\t\t\tenableSitePrice: !!this.config.enableSitePrice,\n\t\t\tenableAccountInfo: !!this.config.enableAccountInfo,\n\t\t\tenableSolarbankMeta: !!this.config.enableSolarbankMeta,\n\t\t\tenableSmartplug: !!this.config.enableSmartplug,\n\t\t\tenablePps: !!this.config.enablePps,\n\t\t\tenableEvCharger: !!this.config.enableEvCharger,\n\t\t\tenableVehicle: !!this.config.enableVehicle,\n\t\t\tenableHes: !!this.config.enableHes,\n\t\t\tenablePowerPanel: !!this.config.enablePowerPanel,\n\t\t\tenableInverter: !!this.config.enableInverter,\n\t\t};\n\t}\n\n\t/** Remove legacy install symlink from old GitHub repo name \"AnkerSolix\". */\n\tprivate cleanupLegacyInstallSymlink(): void {\n\t\tconst alias = path.join(this.adapterDir, \"..\", \"iobroker.AnkerSolix\");\n\t\ttry {\n\t\t\tif (fs.existsSync(alias) && fs.lstatSync(alias).isSymbolicLink()) {\n\t\t\t\tfs.unlinkSync(alias);\n\t\t\t\tthis.log.info(\"Removed legacy symlink iobroker.AnkerSolix\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tprivate async ensurePythonDeps(force = false): Promise<boolean> {\n\t\tif (!force && this.config.autoInstallPython === false) {\n\t\t\treturn true;\n\t\t}\n\t\tconst result = await runPythonInstaller(this.config.pythonPath || \"\", this.log);\n\t\tawait this.setState(\"info.pythonReady\", result.ok, true);\n\t\tif (!result.ok) {\n\t\t\tthis.log.warn(`Python setup: ${result.message}`);\n\t\t}\n\t\treturn result.ok;\n\t}\n\n\tprivate async pollOnce(): Promise<void> {\n\t\tif (!this.config.acceptTerms) {\n\t\t\tthis.log.warn(\"Please accept the usage terms in the adapter configuration.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.config.username?.trim()) {\n\t\t\tthis.log.warn(\"Anker e-mail (username) is required in adapter settings.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.config.password?.trim()) {\n\t\t\tthis.log.warn(\"Password missing \u2013 open instance config in Admin, re-enter Anker password and save.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!(await this.ensurePythonDeps())) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runBridge(\"poll\", this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\t\tconst pollDevices = result.devices as BridgeDevice[] | undefined;\n\t\t\tif (pollDevices?.length) {\n\t\t\t\tthis.rememberDeviceContexts(pollDevices);\n\t\t\t\tawait syncDevices(this, pollDevices);\n\t\t\t}\n\n\t\t\tif (result.nickname) {\n\t\t\t\tawait this.setState(\"account.nickname\", result.nickname, true);\n\t\t\t}\n\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t\tconst detailHint = result.refreshDetails ? \"devices+mqtt\" : \"sites\";\n\t\t\tconst intervalHint =\n\t\t\t\tresult.intervalcount !== undefined && result.deviceintervals !== undefined\n\t\t\t\t\t? `, next detail in ~${result.intervalcount} polls`\n\t\t\t\t\t: \"\";\n\t\t\tthis.log.debug(`Poll OK (${pollDevices?.length ?? 0} devices, ${detailHint}${intervalHint})`);\n\n\t\t\tawait this.runCurtailmentAvoidanceIfEnabled();\n\t\t} catch (error) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\tconst msg = (error as Error).message || String(error);\n\t\t\tif (msg.includes(\"CaptchaRequired\") || msg.includes(\"100032\") || msg.toLowerCase().includes(\"captcha\")) {\n\t\t\t\tconst cacheFile = this.getAuthCacheFile();\n\t\t\t\tconst missing = !fs.existsSync(cacheFile);\n\t\t\t\tconst hint = missing\n\t\t\t\t\t? `Erwartete Datei: ${cacheFile} \u2013 von funktionierender Anker/Solix-Integration (z. B. ha-anker-solix) dorthin kopieren, Ordner anlegen falls n\u00F6tig, Adapter neu starten.`\n\t\t\t\t\t: `Cache vorhanden aber ung\u00FCltig: ${cacheFile} \u2013 frische Datei von HA kopieren oder Passwort in Admin neu speichern.`;\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 API-Neulogin n\u00F6tig${missing ? \" (kein Login-Cache)\" : \"\"}. ${hint}`,\n\t\t\t\t);\n\t\t\t\tif (missing) {\n\t\t\t\t\tthis.logAuthCacheStatus();\n\t\t\t\t}\n\t\t\t} else if (msg.includes(\"Cached Anker login is invalid\") || msg.includes(\"invalidated by the mobile app\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Gespeicherter API-Token ung\u00FCltig (abgelaufen oder durch App ersetzt). ` +\n\t\t\t\t\t\t\"Nicht \u201ECache l\u00F6schen\u201C \u2013 stattdessen frische authcache-Datei von HA kopieren oder App kurz abmelden, dann neu starten.\",\n\t\t\t\t);\n\t\t\t} else if (msg.includes(\"InvalidCredentials\") || msg.includes(\"Authentication failed\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Check e-mail, password and country (${this.config.country || \"DE\"}). ` +\n\t\t\t\t\t\t\"In Admin use \u201CInstall Python dependencies\u201D tab or restart after saving config; \" +\n\t\t\t\t\t\t\"try country matching your Anker account region.\",\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tmsg.includes(\"26161\") ||\n\t\t\t\tmsg.includes(\"429\") ||\n\t\t\t\tmsg.includes(\"Too Many Requests\") ||\n\t\t\t\tmsg.includes(\"Failed to request\")\n\t\t\t) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`Poll failed (Anker API limit or temporary error): ${msg} \u2013 ` +\n\t\t\t\t\t\t\"adapter will retry; increase scan interval (e.g. 120 s) if this persists.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.log.error(`Poll failed: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getPrimaryDeviceId(): string {\n\t\tconst selected = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\t\tif (selected[0]) {\n\t\t\treturn selected[0];\n\t\t}\n\t\ttry {\n\t\t\tconst list = JSON.parse(this.config.deviceListJson || '{\"devices\":[]}') as {\n\t\t\t\tdevices?: Array<{ id: string }>;\n\t\t\t};\n\t\t\treturn list.devices?.[0]?.id || \"\";\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\tprivate async handleServiceTrigger(stateId: string): Promise<void> {\n\t\tconst ns = `${this.namespace}.services.`;\n\t\tif (!stateId.startsWith(ns)) {\n\t\t\treturn;\n\t\t}\n\t\tconst action = stateId.slice(ns.length);\n\n\t\tif (action === \"refresh_devices\") {\n\t\t\tawait this.pollOnce();\n\t\t\tawait this.setState(stateId, false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst serviceActions = [\"get_schedule\", \"clear_schedule\", \"export_systems\", \"get_system_info\"];\n\t\tif (!serviceActions.includes(action)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst params: Record<string, unknown> = {\n\t\t\tdeviceId: this.getPrimaryDeviceId(),\n\t\t\tsiteId: this.config.selectedSiteId || \"\",\n\t\t\tincludeMqtt: this.config.mqttUsage !== false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst serviceConfig: BridgeServiceConfig = {\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tservice: action,\n\t\t\t\tparams,\n\t\t\t};\n\t\t\tconst result = await runBridge(\"service\", serviceConfig, this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (action === \"get_schedule\" && result.schedule !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.scheduleJson, JSON.stringify(result.schedule, null, 2), true);\n\t\t\t}\n\t\t\tif (action === \"export_systems\" && result.path) {\n\t\t\t\tawait this.setState(SERVICE_STATES.exportResult, String(result.path), true);\n\t\t\t}\n\t\t\tif (action === \"get_system_info\" && result.system !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.systemInfo, JSON.stringify(result.system, null, 2), true);\n\t\t\t}\n\n\t\t\tawait this.setState(stateId, false, true);\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Service ${action} failed: ${(error as Error).message}`);\n\t\t\tawait this.setState(stateId, false, true);\n\t\t}\n\t}\n\n\tprivate rememberDeviceContexts(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tconst info = device.info;\n\t\t\tthis.deviceContexts.set(info.id, {\n\t\t\t\ttype: info.type,\n\t\t\t\tsite_id: info.site_id,\n\t\t\t\tdevice_pn: info.device_pn || info.model || \"\",\n\t\t\t\tstation_sn: info.station_sn || \"\",\n\t\t\t\tgeneration: info.generation ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async applyAdapterControl(\n\t\tdeviceId: string,\n\t\tcontrol: string,\n\t\tvalue: string | number | boolean,\n\t\tdeviceContext?: DeviceControlContext,\n\t): Promise<void> {\n\t\tawait runBridge(\n\t\t\t\"set\",\n\t\t\t{\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tdeviceId,\n\t\t\t\tcontrol,\n\t\t\t\tvalue,\n\t\t\t\tdeviceContext,\n\t\t\t},\n\t\t\tthis.config.pythonPath || \"\",\n\t\t\tthis.log,\n\t\t);\n\t}\n\n\tprivate getCurtailmentHost(): CurtailmentRunnerHost {\n\t\treturn {\n\t\t\tnamespace: this.namespace,\n\t\t\tlog: this.log,\n\t\t\tgetForeignStateAsync: id => this.getForeignStateAsync(id),\n\t\t\tgetStateAsync: id => this.getStateAsync(id),\n\t\t\tsetState: async (id, val, ack) => {\n\t\t\t\tawait this.setState(id, val as ioBroker.StateValue, ack ?? true);\n\t\t\t},\n\t\t\tgetDeviceContext: deviceId => this.deviceContexts.get(deviceId),\n\t\t\tapplyControl: (deviceId, control, value, deviceContext) =>\n\t\t\t\tthis.applyAdapterControl(deviceId, control, value, deviceContext),\n\t\t};\n\t}\n\n\tprivate async runCurtailmentAvoidanceIfEnabled(): Promise<void> {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst modeBefore = this.config.curtailmentModeBefore === \"smart\" ? \"smart\" : \"smartmeter\";\n\t\t\tconst modeAfter = this.config.curtailmentModeAfter === \"smart\" ? \"smart\" : \"smartmeter\";\n\t\t\tawait runCurtailmentAvoidance(this.getCurtailmentHost(), {\n\t\t\t\tenabled: true,\n\t\t\t\tforecastBasePath: (this.config.curtailmentForecastPath || \"solarprognose.0.forecast.00.hourly\").trim(),\n\t\t\t\tdevicesJson: this.config.curtailmentDevicesJson || \"[]\",\n\t\t\t\tmodeBefore,\n\t\t\t\tmodeAfter,\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Curtailment avoidance: ${(err as Error).message}`);\n\t\t}\n\t}\n\n\tprivate schedulePollAfterControl(): void {\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t}\n\t\tthis.pollAfterControlTimer = setTimeout(() => {\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t\tvoid this.pollOnce();\n\t\t}, 12_000);\n\t}\n\n\tprivate async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n\t\tif (!state || state.ack) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (id.startsWith(`${this.namespace}.services.`) && state.val === true) {\n\t\t\tawait this.handleServiceTrigger(id);\n\t\t\treturn;\n\t\t}\n\n\t\tconst control = parseControlStateId(this.namespace, id);\n\t\tif (!control) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst current = await this.getStateAsync(id);\n\t\tif (\n\t\t\tcurrent?.ack &&\n\t\t\tcurrent.val !== null &&\n\t\t\tcurrent.val !== undefined &&\n\t\t\tString(current.val) === String(state.val)\n\t\t) {\n\t\t\tawait this.setState(id, { val: state.val, ack: true });\n\t\t\treturn;\n\t\t}\n\n\t\tconst value = state.val as string | number | boolean;\n\t\tconst deviceContext = this.deviceContexts.get(control.deviceId);\n\n\t\tthis.controlQueue.enqueue({\n\t\t\tstateId: id,\n\t\t\texecute: async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runBridge(\n\t\t\t\t\t\t\"set\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\t\t\t\tdeviceId: control.deviceId,\n\t\t\t\t\t\t\tcontrol: control.control,\n\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\tdeviceContext,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\t\tthis.log,\n\t\t\t\t\t);\n\t\t\t\t\tawait this.setState(id, { val: value, ack: true });\n\t\t\t\t\tthis.log.info(`Applied ${control.control} on ${control.deviceId}`);\n\t\t\t\t\tthis.schedulePollAfterControl();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = (error as Error).message;\n\t\t\t\t\tif (message.includes(\"429\") || message.includes(\"Too Many Requests\")) {\n\t\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t\t`Control rate-limited for ${id} \u2013 wait ~1 minute before retrying (Anker API limit).`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.error(`Control failed for ${id}: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.setState(id, { val: value, ack: false });\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj?.command) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst respond = (response: unknown): void => {\n\t\t\tif (obj.callback) {\n\t\t\t\tthis.sendTo(obj.from, obj.command, response, obj.callback);\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tif (obj.command === \"clearAuthCache\") {\n\t\t\t\tconst cacheDir = this.getAuthCacheDir();\n\t\t\t\tconst fs = await import(\"node:fs/promises\");\n\t\t\t\ttry {\n\t\t\t\t\tconst files = await fs.readdir(cacheDir);\n\t\t\t\t\tawait Promise.all(files.map(f => fs.unlink(path.join(cacheDir, f)).catch(() => undefined)));\n\t\t\t\t\tawait stopBridgeDaemon();\n\t\t\t\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache cleared (${files.length} file(s) in ${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Next poll requires a new API login; on many hosts Anker returns captcha (100032). \" +\n\t\t\t\t\t\t\t`Restore authcache/${(this.config.username || \"\").trim()}.json from HA or retry login when cloud allows it.`,\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: files.length });\n\t\t\t\t} catch {\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache clear requested but folder empty or missing (${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Adapter must complete a successful API login to create authcache/<email>.json.\",\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: 0 });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"installPython\") {\n\t\t\t\tconst ok = await this.ensurePythonDeps(true);\n\t\t\t\trespond({ ok });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"loadDevices\") {\n\t\t\t\tif (!this.config.username || !this.config.password) {\n\t\t\t\t\trespond({ error: \"Credentials required\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.ensurePythonDeps();\n\t\t\t\tconst result = await runBridge(\n\t\t\t\t\t\"list_devices\",\n\t\t\t\t\tthis.getBridgeConfig(),\n\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\tthis.log,\n\t\t\t\t);\n\t\t\t\tconst payload = {\n\t\t\t\t\tsites: result.sites || [],\n\t\t\t\t\tdevices: result.devices || [],\n\t\t\t\t};\n\t\t\t\trespond({ ok: true, deviceListJson: JSON.stringify(payload, null, 2), ...payload });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trespond({ error: `Unknown command ${obj.command}` });\n\t\t} catch (error) {\n\t\t\trespond({ error: (error as Error).message });\n\t\t}\n\t}\n\n\tprivate async onReady(): Promise<void> {\n\t\tthis.cleanupLegacyInstallSymlink();\n\n\t\tawait this.setObjectNotExistsAsync(\"account\", {\n\t\t\ttype: \"channel\",\n\t\t\tcommon: { name: \"Account\" },\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"account.nickname\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Account nickname\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"info\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"info.pythonReady\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Python dependencies ready\",\n\t\t\t\ttype: \"boolean\",\n\t\t\t\trole: \"indicator\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\n\t\tawait setupServiceStates(this);\n\t\tawait setupCurtailmentStates(this);\n\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\tconst intervalSec = Math.max(30, Number(this.config.scanInterval) || 60);\n\t\tthis.log.info(\n\t\t\t`Anker Solix adapter started (poll every ${intervalSec}s, MQTT: ${this.config.mqttUsage !== false})`,\n\t\t);\n\n\t\tawait this.ensurePythonDeps();\n\n\t\tthis.logAuthCacheStatus();\n\n\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\tthis.subscribeStates(`${this.namespace}.*.control.*`);\n\t\tthis.subscribeStates(`${this.namespace}.services.*`);\n\n\t\tawait this.pollOnce();\n\t\tthis.pollTimer = this.setInterval(() => {\n\t\t\tvoid this.pollOnce();\n\t\t}, intervalSec * 1000);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tif (this.pollTimer) {\n\t\t\tthis.clearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t}\n\t\tvoid stopBridgeDaemon().finally(() => callback());\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AnkerSolix(options);\n} else {\n\t(() => new AnkerSolix())();\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAKA,SAAoB;AACpB,WAAsB;AAEtB,YAAuB;AAEvB,2BAAuC;AACvC,0BAA6B;AAC7B,+BAAoE;AACpE,+BAAuC;AACvC,0BAAmC;AACnC,0BAAgE;AAChE,sBAAmD;AACnD,uBAAiD;AAGjD,MAAM,mBAAmB,MAAM,QAAQ;AAAA,EAC9B;AAAA,EACS,eAAe,IAAI,iCAAa;AAAA,EAChC,iBAAiB,oBAAI,IAAkC;AAAA,EAChE;AAAA,EAED,YAAY,UAAyC,CAAC,GAAG;AAC/D,UAAM;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,IACP,CAAC;AACD,SAAK,GAAG,SAAS,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxC,SAAK,GAAG,eAAe,KAAK,cAAc,KAAK,IAAI,CAAC;AACpD,SAAK,GAAG,WAAW,KAAK,UAAU,KAAK,IAAI,CAAC;AAC5C,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAC3C;AAAA,EAEQ,kBAA0B;AACjC,WAAO,KAAK,KAAK,MAAM,2BAA2B,IAAI,GAAG,WAAW;AAAA,EACrE;AAAA,EAEQ,mBAA2B;AAClC,UAAM,SAAS,KAAK,OAAO,YAAY,IAAI,KAAK;AAChD,WAAO,KAAK,KAAK,KAAK,gBAAgB,GAAG,GAAG,KAAK,OAAO;AAAA,EACzD;AAAA,EAEQ,qBAA2B;AAClC,UAAM,WAAW,KAAK,gBAAgB;AACtC,UAAM,YAAY,KAAK,iBAAiB;AACxC,UAAM,SAAS,KAAK,OAAO,YAAY,IAAI,KAAK;AAChD,QAAI,CAAC,OAAO;AACX;AAAA,IACD;AACA,QAAI;AACH,SAAG,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IAC3C,SAAS,KAAK;AACb,WAAK,IAAI,KAAK,kCAAkC,QAAQ,KAAM,IAAc,OAAO,EAAE;AACrF;AAAA,IACD;AACA,QAAI,GAAG,WAAW,SAAS,GAAG;AAC7B,WAAK,IAAI,MAAM,8BAA8B,SAAS,EAAE;AACxD;AAAA,IACD;AACA,QAAI,QAAQ;AACZ,QAAI;AACH,YAAM,QAAQ,GAAG,YAAY,QAAQ,EAAE,OAAO,OAAK,EAAE,SAAS,OAAO,CAAC;AACtE,UAAI,MAAM,QAAQ;AACjB,gBAAQ,mCAAmC,MAAM,KAAK,IAAI,CAAC;AAAA,MAC5D;AAAA,IACD,QAAQ;AAAA,IAER;AACA,SAAK,IAAI;AAAA,MACR,2BAA2B,SAAS,IAAI,KAAK;AAAA,IAG9C;AAAA,EACD;AAAA,EAEQ,kBAAgC;AACvC,UAAM,WAAW,KAAK,gBAAgB;AACtC,UAAM,kBAAc,6CAAuB,KAAK,OAAO,iBAAiB;AAExE,WAAO;AAAA,MACN,UAAU,KAAK,OAAO;AAAA,MACtB,UAAU,KAAK,OAAO;AAAA,MACtB,SAAS,KAAK,OAAO,WAAW;AAAA,MAChC,WAAW,KAAK,OAAO,cAAc;AAAA,MACrC;AAAA,MACA,kBAAkB,KAAK,OAAO,qBAAqB;AAAA,MACnD,gBAAgB,KAAK,OAAO,kBAAkB;AAAA,MAC9C,mBAAmB;AAAA,MACnB,wBAAwB,KAAK,IAAI,GAAG,OAAO,KAAK,OAAO,sBAAsB,KAAK,EAAE;AAAA,MACpF,cAAc,OAAO,KAAK,OAAO,YAAY,KAAK;AAAA,MAClD,gBAAgB,OAAO,KAAK,OAAO,cAAc,KAAK;AAAA,MACtD,eAAe,OAAO,KAAK,OAAO,aAAa,KAAK;AAAA,MACpD,oBAAoB,KAAK,OAAO,uBAAuB;AAAA,MACvD,wBAAwB,CAAC,CAAC,KAAK,OAAO;AAAA,MACtC,oBAAoB,CAAC,CAAC,KAAK,OAAO;AAAA,MAClC,kBAAkB,CAAC,CAAC,KAAK,OAAO;AAAA,MAChC,mBAAmB,CAAC,CAAC,KAAK,OAAO;AAAA,MACjC,wBAAwB,CAAC,CAAC,KAAK,OAAO;AAAA,MACtC,wBAAwB,CAAC,CAAC,KAAK,OAAO;AAAA,MACtC,sBAAsB,CAAC,CAAC,KAAK,OAAO;AAAA,MACpC,iBAAiB,CAAC,CAAC,KAAK,OAAO;AAAA,MAC/B,mBAAmB,CAAC,CAAC,KAAK,OAAO;AAAA,MACjC,qBAAqB,CAAC,CAAC,KAAK,OAAO;AAAA,MACnC,iBAAiB,CAAC,CAAC,KAAK,OAAO;AAAA,MAC/B,WAAW,CAAC,CAAC,KAAK,OAAO;AAAA,MACzB,iBAAiB,CAAC,CAAC,KAAK,OAAO;AAAA,MAC/B,eAAe,CAAC,CAAC,KAAK,OAAO;AAAA,MAC7B,WAAW,CAAC,CAAC,KAAK,OAAO;AAAA,MACzB,kBAAkB,CAAC,CAAC,KAAK,OAAO;AAAA,MAChC,gBAAgB,CAAC,CAAC,KAAK,OAAO;AAAA,IAC/B;AAAA,EACD;AAAA;AAAA,EAGQ,8BAAoC;AAC3C,UAAM,QAAQ,KAAK,KAAK,KAAK,YAAY,MAAM,qBAAqB;AACpE,QAAI;AACH,UAAI,GAAG,WAAW,KAAK,KAAK,GAAG,UAAU,KAAK,EAAE,eAAe,GAAG;AACjE,WAAG,WAAW,KAAK;AACnB,aAAK,IAAI,KAAK,4CAA4C;AAAA,MAC3D;AAAA,IACD,QAAQ;AAAA,IAER;AAAA,EACD;AAAA,EAEA,MAAc,iBAAiB,QAAQ,OAAyB;AAC/D,QAAI,CAAC,SAAS,KAAK,OAAO,sBAAsB,OAAO;AACtD,aAAO;AAAA,IACR;AACA,UAAM,SAAS,UAAM,wCAAmB,KAAK,OAAO,cAAc,IAAI,KAAK,GAAG;AAC9E,UAAM,KAAK,SAAS,oBAAoB,OAAO,IAAI,IAAI;AACvD,QAAI,CAAC,OAAO,IAAI;AACf,WAAK,IAAI,KAAK,iBAAiB,OAAO,OAAO,EAAE;AAAA,IAChD;AACA,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAc,WAA0B;AA9IzC;AA+IE,QAAI,CAAC,KAAK,OAAO,aAAa;AAC7B,WAAK,IAAI,KAAK,6DAA6D;AAC3E,YAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAClD;AAAA,IACD;AAEA,QAAI,GAAC,UAAK,OAAO,aAAZ,mBAAsB,SAAQ;AAClC,WAAK,IAAI,KAAK,0DAA0D;AACxE,YAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAClD;AAAA,IACD;AACA,QAAI,GAAC,UAAK,OAAO,aAAZ,mBAAsB,SAAQ;AAClC,WAAK,IAAI,KAAK,0FAAqF;AACnG,YAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAClD;AAAA,IACD;AAEA,QAAI,CAAE,MAAM,KAAK,iBAAiB,GAAI;AACrC,YAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAClD;AAAA,IACD;AAEA,QAAI;AACH,YAAM,SAAS,UAAM,+BAAU,QAAQ,KAAK,gBAAgB,GAAG,KAAK,OAAO,cAAc,IAAI,KAAK,GAAG;AAErG,YAAM,cAAc,OAAO;AAC3B,UAAI,2CAAa,QAAQ;AACxB,aAAK,uBAAuB,WAAW;AACvC,kBAAM,8BAAY,MAAM,WAAW;AAAA,MACpC;AAEA,UAAI,OAAO,UAAU;AACpB,cAAM,KAAK,SAAS,oBAAoB,OAAO,UAAU,IAAI;AAAA,MAC9D;AAEA,YAAM,KAAK,SAAS,mBAAmB,MAAM,IAAI;AACjD,YAAM,aAAa,OAAO,iBAAiB,iBAAiB;AAC5D,YAAM,eACL,OAAO,kBAAkB,UAAa,OAAO,oBAAoB,SAC9D,qBAAqB,OAAO,aAAa,WACzC;AACJ,WAAK,IAAI,MAAM,aAAY,gDAAa,WAAb,YAAuB,CAAC,aAAa,UAAU,GAAG,YAAY,GAAG;AAE5F,YAAM,KAAK,iCAAiC;AAAA,IAC7C,SAAS,OAAO;AACf,YAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAClD,YAAM,MAAO,MAAgB,WAAW,OAAO,KAAK;AACpD,UAAI,IAAI,SAAS,iBAAiB,KAAK,IAAI,SAAS,QAAQ,KAAK,IAAI,YAAY,EAAE,SAAS,SAAS,GAAG;AACvG,cAAM,YAAY,KAAK,iBAAiB;AACxC,cAAM,UAAU,CAAC,GAAG,WAAW,SAAS;AACxC,cAAM,OAAO,UACV,oBAAoB,SAAS,sJAC7B,qCAAkC,SAAS;AAC9C,aAAK,IAAI;AAAA,UACR,gBAAgB,GAAG,gCAAwB,UAAU,wBAAwB,EAAE,KAAK,IAAI;AAAA,QACzF;AACA,YAAI,SAAS;AACZ,eAAK,mBAAmB;AAAA,QACzB;AAAA,MACD,WAAW,IAAI,SAAS,+BAA+B,KAAK,IAAI,SAAS,+BAA+B,GAAG;AAC1G,aAAK,IAAI;AAAA,UACR,gBAAgB,GAAG;AAAA,QAEpB;AAAA,MACD,WAAW,IAAI,SAAS,oBAAoB,KAAK,IAAI,SAAS,uBAAuB,GAAG;AACvF,aAAK,IAAI;AAAA,UACR,gBAAgB,GAAG,+CAA0C,KAAK,OAAO,WAAW,IAAI;AAAA,QAGzF;AAAA,MACD,WACC,IAAI,SAAS,OAAO,KACpB,IAAI,SAAS,KAAK,KAClB,IAAI,SAAS,mBAAmB,KAChC,IAAI,SAAS,mBAAmB,GAC/B;AACD,aAAK,IAAI;AAAA,UACR,qDAAqD,GAAG;AAAA,QAEzD;AAAA,MACD,OAAO;AACN,aAAK,IAAI,MAAM,gBAAgB,GAAG,EAAE;AAAA,MACrC;AAAA,IACD;AAAA,EACD;AAAA,EAEQ,qBAA6B;AArOtC;AAsOE,UAAM,eAAW,6CAAuB,KAAK,OAAO,iBAAiB;AACrE,QAAI,SAAS,CAAC,GAAG;AAChB,aAAO,SAAS,CAAC;AAAA,IAClB;AACA,QAAI;AACH,YAAM,OAAO,KAAK,MAAM,KAAK,OAAO,kBAAkB,gBAAgB;AAGtE,eAAO,gBAAK,YAAL,mBAAe,OAAf,mBAAmB,OAAM;AAAA,IACjC,QAAQ;AACP,aAAO;AAAA,IACR;AAAA,EACD;AAAA,EAEA,MAAc,qBAAqB,SAAgC;AAClE,UAAM,KAAK,GAAG,KAAK,SAAS;AAC5B,QAAI,CAAC,QAAQ,WAAW,EAAE,GAAG;AAC5B;AAAA,IACD;AACA,UAAM,SAAS,QAAQ,MAAM,GAAG,MAAM;AAEtC,QAAI,WAAW,mBAAmB;AACjC,YAAM,KAAK,SAAS;AACpB,YAAM,KAAK,SAAS,SAAS,OAAO,IAAI;AACxC;AAAA,IACD;AAEA,UAAM,iBAAiB,CAAC,gBAAgB,kBAAkB,kBAAkB,iBAAiB;AAC7F,QAAI,CAAC,eAAe,SAAS,MAAM,GAAG;AACrC;AAAA,IACD;AAEA,UAAM,SAAkC;AAAA,MACvC,UAAU,KAAK,mBAAmB;AAAA,MAClC,QAAQ,KAAK,OAAO,kBAAkB;AAAA,MACtC,aAAa,KAAK,OAAO,cAAc;AAAA,IACxC;AAEA,QAAI;AACH,YAAM,gBAAqC;AAAA,QAC1C,GAAG,KAAK,gBAAgB;AAAA,QACxB,SAAS;AAAA,QACT;AAAA,MACD;AACA,YAAM,SAAS,UAAM,+BAAU,WAAW,eAAe,KAAK,OAAO,cAAc,IAAI,KAAK,GAAG;AAE/F,UAAI,WAAW,kBAAkB,OAAO,aAAa,QAAW;AAC/D,cAAM,KAAK,SAAS,+BAAe,cAAc,KAAK,UAAU,OAAO,UAAU,MAAM,CAAC,GAAG,IAAI;AAAA,MAChG;AACA,UAAI,WAAW,oBAAoB,OAAO,MAAM;AAC/C,cAAM,KAAK,SAAS,+BAAe,cAAc,OAAO,OAAO,IAAI,GAAG,IAAI;AAAA,MAC3E;AACA,UAAI,WAAW,qBAAqB,OAAO,WAAW,QAAW;AAChE,cAAM,KAAK,SAAS,+BAAe,YAAY,KAAK,UAAU,OAAO,QAAQ,MAAM,CAAC,GAAG,IAAI;AAAA,MAC5F;AAEA,YAAM,KAAK,SAAS,SAAS,OAAO,IAAI;AAAA,IACzC,SAAS,OAAO;AACf,WAAK,IAAI,MAAM,WAAW,MAAM,YAAa,MAAgB,OAAO,EAAE;AACtE,YAAM,KAAK,SAAS,SAAS,OAAO,IAAI;AAAA,IACzC;AAAA,EACD;AAAA,EAEQ,uBAAuB,SAA+B;AArS/D;AAsSE,eAAW,UAAU,SAAS;AAC7B,YAAM,OAAO,OAAO;AACpB,WAAK,eAAe,IAAI,KAAK,IAAI;AAAA,QAChC,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,WAAW,KAAK,aAAa,KAAK,SAAS;AAAA,QAC3C,YAAY,KAAK,cAAc;AAAA,QAC/B,aAAY,UAAK,eAAL,YAAmB;AAAA,MAChC,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAc,oBACb,UACA,SACA,OACA,eACgB;AAChB,cAAM;AAAA,MACL;AAAA,MACA;AAAA,QACC,GAAG,KAAK,gBAAgB;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACD;AAAA,MACA,KAAK,OAAO,cAAc;AAAA,MAC1B,KAAK;AAAA,IACN;AAAA,EACD;AAAA,EAEQ,qBAA4C;AACnD,WAAO;AAAA,MACN,WAAW,KAAK;AAAA,MAChB,KAAK,KAAK;AAAA,MACV,sBAAsB,QAAM,KAAK,qBAAqB,EAAE;AAAA,MACxD,eAAe,QAAM,KAAK,cAAc,EAAE;AAAA,MAC1C,UAAU,OAAO,IAAI,KAAK,QAAQ;AACjC,cAAM,KAAK,SAAS,IAAI,KAA4B,oBAAO,IAAI;AAAA,MAChE;AAAA,MACA,kBAAkB,cAAY,KAAK,eAAe,IAAI,QAAQ;AAAA,MAC9D,cAAc,CAAC,UAAU,SAAS,OAAO,kBACxC,KAAK,oBAAoB,UAAU,SAAS,OAAO,aAAa;AAAA,IAClE;AAAA,EACD;AAAA,EAEA,MAAc,mCAAkD;AAC/D,QAAI,CAAC,KAAK,OAAO,4BAA4B;AAC5C;AAAA,IACD;AACA,QAAI;AACH,YAAM,aAAa,KAAK,OAAO,0BAA0B,UAAU,UAAU;AAC7E,YAAM,YAAY,KAAK,OAAO,yBAAyB,UAAU,UAAU;AAC3E,gBAAM,kDAAwB,KAAK,mBAAmB,GAAG;AAAA,QACxD,SAAS;AAAA,QACT,mBAAmB,KAAK,OAAO,2BAA2B,sCAAsC,KAAK;AAAA,QACrG,aAAa,KAAK,OAAO,0BAA0B;AAAA,QACnD;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF,SAAS,KAAK;AACb,WAAK,IAAI,KAAK,0BAA2B,IAAc,OAAO,EAAE;AAAA,IACjE;AAAA,EACD;AAAA,EAEQ,2BAAiC;AACxC,QAAI,KAAK,uBAAuB;AAC/B,mBAAa,KAAK,qBAAqB;AAAA,IACxC;AACA,SAAK,wBAAwB,WAAW,MAAM;AAC7C,WAAK,wBAAwB;AAC7B,WAAK,KAAK,SAAS;AAAA,IACpB,GAAG,IAAM;AAAA,EACV;AAAA,EAEA,MAAc,cAAc,IAAY,OAAyD;AAChG,QAAI,CAAC,SAAS,MAAM,KAAK;AACxB;AAAA,IACD;AAEA,QAAI,GAAG,WAAW,GAAG,KAAK,SAAS,YAAY,KAAK,MAAM,QAAQ,MAAM;AACvE,YAAM,KAAK,qBAAqB,EAAE;AAClC;AAAA,IACD;AAEA,UAAM,cAAU,sCAAoB,KAAK,WAAW,EAAE;AACtD,QAAI,CAAC,SAAS;AACb;AAAA,IACD;AAEA,UAAM,UAAU,MAAM,KAAK,cAAc,EAAE;AAC3C,SACC,mCAAS,QACT,QAAQ,QAAQ,QAChB,QAAQ,QAAQ,UAChB,OAAO,QAAQ,GAAG,MAAM,OAAO,MAAM,GAAG,GACvC;AACD,YAAM,KAAK,SAAS,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AACrD;AAAA,IACD;AAEA,UAAM,QAAQ,MAAM;AACpB,UAAM,gBAAgB,KAAK,eAAe,IAAI,QAAQ,QAAQ;AAE9D,SAAK,aAAa,QAAQ;AAAA,MACzB,SAAS;AAAA,MACT,SAAS,YAAY;AACpB,YAAI;AACH,oBAAM;AAAA,YACL;AAAA,YACA;AAAA,cACC,GAAG,KAAK,gBAAgB;AAAA,cACxB,UAAU,QAAQ;AAAA,cAClB,SAAS,QAAQ;AAAA,cACjB;AAAA,cACA;AAAA,YACD;AAAA,YACA,KAAK,OAAO,cAAc;AAAA,YAC1B,KAAK;AAAA,UACN;AACA,gBAAM,KAAK,SAAS,IAAI,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AACjD,eAAK,IAAI,KAAK,WAAW,QAAQ,OAAO,OAAO,QAAQ,QAAQ,EAAE;AACjE,eAAK,yBAAyB;AAAA,QAC/B,SAAS,OAAO;AACf,gBAAM,UAAW,MAAgB;AACjC,cAAI,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,mBAAmB,GAAG;AACrE,iBAAK,IAAI;AAAA,cACR,4BAA4B,EAAE;AAAA,YAC/B;AAAA,UACD,OAAO;AACN,iBAAK,IAAI,MAAM,sBAAsB,EAAE,KAAK,OAAO,EAAE;AAAA,UACtD;AACA,gBAAM,KAAK,SAAS,IAAI,EAAE,KAAK,OAAO,KAAK,MAAM,CAAC;AAAA,QACnD;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAc,UAAU,KAAsC;AAC7D,QAAI,EAAC,2BAAK,UAAS;AAClB;AAAA,IACD;AAEA,UAAM,UAAU,CAAC,aAA4B;AAC5C,UAAI,IAAI,UAAU;AACjB,aAAK,OAAO,IAAI,MAAM,IAAI,SAAS,UAAU,IAAI,QAAQ;AAAA,MAC1D;AAAA,IACD;AAEA,QAAI;AACH,UAAI,IAAI,YAAY,kBAAkB;AACrC,cAAM,WAAW,KAAK,gBAAgB;AACtC,cAAMA,MAAK,MAAM,6CAAO,kBAAkB;AAC1C,YAAI;AACH,gBAAM,QAAQ,MAAMA,IAAG,QAAQ,QAAQ;AACvC,gBAAM,QAAQ,IAAI,MAAM,IAAI,OAAKA,IAAG,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,EAAE,MAAM,MAAM,MAAS,CAAC,CAAC;AAC1F,oBAAM,sCAAiB;AACvB,oBAAM,wCAAmB,KAAK,gBAAgB,GAAG,KAAK,OAAO,cAAc,IAAI,KAAK,GAAG;AACvF,eAAK,IAAI;AAAA,YACR,8BAA8B,MAAM,MAAM,eAAe,QAAQ,2GAE1C,KAAK,OAAO,YAAY,IAAI,KAAK,CAAC;AAAA,UAC1D;AACA,kBAAQ,EAAE,IAAI,MAAM,SAAS,MAAM,OAAO,CAAC;AAAA,QAC5C,QAAQ;AACP,eAAK,IAAI;AAAA,YACR,kEAAkE,QAAQ;AAAA,UAE3E;AACA,kBAAQ,EAAE,IAAI,MAAM,SAAS,EAAE,CAAC;AAAA,QACjC;AACA;AAAA,MACD;AAEA,UAAI,IAAI,YAAY,iBAAiB;AACpC,cAAM,KAAK,MAAM,KAAK,iBAAiB,IAAI;AAC3C,gBAAQ,EAAE,GAAG,CAAC;AACd;AAAA,MACD;AAEA,UAAI,IAAI,YAAY,eAAe;AAClC,YAAI,CAAC,KAAK,OAAO,YAAY,CAAC,KAAK,OAAO,UAAU;AACnD,kBAAQ,EAAE,OAAO,uBAAuB,CAAC;AACzC;AAAA,QACD;AACA,cAAM,KAAK,iBAAiB;AAC5B,cAAM,SAAS,UAAM;AAAA,UACpB;AAAA,UACA,KAAK,gBAAgB;AAAA,UACrB,KAAK,OAAO,cAAc;AAAA,UAC1B,KAAK;AAAA,QACN;AACA,cAAM,UAAU;AAAA,UACf,OAAO,OAAO,SAAS,CAAC;AAAA,UACxB,SAAS,OAAO,WAAW,CAAC;AAAA,QAC7B;AACA,gBAAQ,EAAE,IAAI,MAAM,gBAAgB,KAAK,UAAU,SAAS,MAAM,CAAC,GAAG,GAAG,QAAQ,CAAC;AAClF;AAAA,MACD;AAEA,cAAQ,EAAE,OAAO,mBAAmB,IAAI,OAAO,GAAG,CAAC;AAAA,IACpD,SAAS,OAAO;AACf,cAAQ,EAAE,OAAQ,MAAgB,QAAQ,CAAC;AAAA,IAC5C;AAAA,EACD;AAAA,EAEA,MAAc,UAAyB;AACtC,SAAK,4BAA4B;AAEjC,UAAM,KAAK,wBAAwB,WAAW;AAAA,MAC7C,MAAM;AAAA,MACN,QAAQ,EAAE,MAAM,UAAU;AAAA,MAC1B,QAAQ,CAAC;AAAA,IACV,CAAC;AACD,UAAM,KAAK,wBAAwB,oBAAoB;AAAA,MACtD,MAAM;AAAA,MACN,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACR;AAAA,MACA,QAAQ,CAAC;AAAA,IACV,CAAC;AACD,UAAM,KAAK,wBAAwB,oBAAoB;AAAA,MACtD,MAAM;AAAA,MACN,QAAQ;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,MACN;AAAA,MACA,QAAQ,CAAC;AAAA,IACV,CAAC;AAED,cAAM,oCAAmB,IAAI;AAC7B,cAAM,iDAAuB,IAAI;AACjC,UAAM,KAAK,SAAS,mBAAmB,OAAO,IAAI;AAElD,UAAM,cAAc,KAAK,IAAI,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,EAAE;AACvE,SAAK,IAAI;AAAA,MACR,2CAA2C,WAAW,YAAY,KAAK,OAAO,cAAc,KAAK;AAAA,IAClG;AAEA,UAAM,KAAK,iBAAiB;AAE5B,SAAK,mBAAmB;AAExB,cAAM,wCAAmB,KAAK,gBAAgB,GAAG,KAAK,OAAO,cAAc,IAAI,KAAK,GAAG;AAEvF,SAAK,gBAAgB,GAAG,KAAK,SAAS,cAAc;AACpD,SAAK,gBAAgB,GAAG,KAAK,SAAS,aAAa;AAEnD,UAAM,KAAK,SAAS;AACpB,SAAK,YAAY,KAAK,YAAY,MAAM;AACvC,WAAK,KAAK,SAAS;AAAA,IACpB,GAAG,cAAc,GAAI;AAAA,EACtB;AAAA,EAEQ,SAAS,UAA4B;AAC5C,QAAI,KAAK,WAAW;AACnB,WAAK,cAAc,KAAK,SAAS;AACjC,WAAK,YAAY;AAAA,IAClB;AACA,QAAI,KAAK,uBAAuB;AAC/B,mBAAa,KAAK,qBAAqB;AACvC,WAAK,wBAAwB;AAAA,IAC9B;AACA,aAAK,sCAAiB,EAAE,QAAQ,MAAM,SAAS,CAAC;AAAA,EACjD;AACD;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAC5B,SAAO,UAAU,CAAC,YAAuD,IAAI,WAAW,OAAO;AAChG,OAAO;AACN,GAAC,MAAM,IAAI,WAAW,GAAG;AAC1B;",
|
|
6
6
|
"names": ["fs"]
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "anker-solix",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.3",
|
|
5
5
|
"messagebox": true,
|
|
6
6
|
"news": {
|
|
7
|
+
"0.10.3": {
|
|
8
|
+
"en": "CI: curtailment unit tests use Mocha/Chai (lint)",
|
|
9
|
+
"de": "CI: Abregelungs-Tests mit Mocha/Chai (Lint)",
|
|
10
|
+
"ru": "CI: тесты Mocha/Chai",
|
|
11
|
+
"pt": "CI: testes Mocha/Chai",
|
|
12
|
+
"nl": "CI: tests Mocha/Chai",
|
|
13
|
+
"fr": "CI: tests Mocha/Chai",
|
|
14
|
+
"it": "CI: test Mocha/Chai",
|
|
15
|
+
"es": "CI: tests Mocha/Chai",
|
|
16
|
+
"pl": "CI: testy Mocha/Chai",
|
|
17
|
+
"uk": "CI: тести Mocha/Chai",
|
|
18
|
+
"zh-cn": "CI:Mocha/Chai 单元测试"
|
|
19
|
+
},
|
|
20
|
+
"0.10.2": {
|
|
21
|
+
"en": "Curtailment AC limits: standalone 800 W; combiner SB2 1000, SB3 1200, SB4 2500 W",
|
|
22
|
+
"de": "Abregelungs-Limits: einzeln 800 W; Combiner SB2 1000, SB3 1200, SB4 2500 W",
|
|
23
|
+
"ru": "Лимиты AC: 800 W / Combiner",
|
|
24
|
+
"pt": "Limites AC corrigidos",
|
|
25
|
+
"nl": "AC-limieten gecorrigeerd",
|
|
26
|
+
"fr": "Limites AC corrigées",
|
|
27
|
+
"it": "Limiti AC corretti",
|
|
28
|
+
"es": "Límites AC corregidos",
|
|
29
|
+
"pl": "Poprawione limity AC",
|
|
30
|
+
"uk": "Виправлені ліміти AC",
|
|
31
|
+
"zh-cn": "修正 AC 功率限制"
|
|
32
|
+
},
|
|
33
|
+
"0.10.1": {
|
|
34
|
+
"en": "Curtailment: combiner AC limit = sum of up to 4 mixed solarbank profiles",
|
|
35
|
+
"de": "Abregelungsvermeidung: Combiner-Limit = Summe bis zu 4 gemischter Solarbank-Profile",
|
|
36
|
+
"ru": "Combiner: сумма лимитов до 4 банков",
|
|
37
|
+
"pt": "Combiner: soma de até 4 perfis",
|
|
38
|
+
"nl": "Combiner: som van max. 4 profielen",
|
|
39
|
+
"fr": "Combiner: somme de 4 profils max.",
|
|
40
|
+
"it": "Combiner: somma fino a 4 profili",
|
|
41
|
+
"es": "Combiner: suma de hasta 4 perfiles",
|
|
42
|
+
"pl": "Combiner: suma do 4 profili",
|
|
43
|
+
"uk": "Combiner: сума до 4 профілів",
|
|
44
|
+
"zh-cn": "Combiner:最多4种机型功率相加"
|
|
45
|
+
},
|
|
46
|
+
"0.10.0": {
|
|
47
|
+
"en": "Optional curtailment avoidance via solar forecast (solarprognose adapter)",
|
|
48
|
+
"de": "Optionale Abregelungsvermeidung per Solarprognose (Adapter solarprognose)",
|
|
49
|
+
"ru": "Избегание ограничения мощности",
|
|
50
|
+
"pt": "Evitar limitação de potência",
|
|
51
|
+
"nl": "Afbegrenzingsvermijding",
|
|
52
|
+
"fr": "Évitement de limitation",
|
|
53
|
+
"it": "Evita limitazione potenza",
|
|
54
|
+
"es": "Evitar limitación de potencia",
|
|
55
|
+
"pl": "Unikanie ograniczenia mocy",
|
|
56
|
+
"uk": "Уникнення обмеження потужності",
|
|
57
|
+
"zh-cn": "可选防限电(太阳能预测)"
|
|
58
|
+
},
|
|
59
|
+
"0.9.9": {
|
|
60
|
+
"en": "package.json keyword ioBroker; entity group headers with size (schema)",
|
|
61
|
+
"de": "Keyword ioBroker in package.json; Entitäts-Header mit size (Schema)",
|
|
62
|
+
"ru": "Ключевое слово ioBroker; заголовки с size",
|
|
63
|
+
"pt": "Keyword ioBroker; cabeçalhos com size",
|
|
64
|
+
"nl": "Keyword ioBroker; headers met size",
|
|
65
|
+
"fr": "Mot-clé ioBroker; en-têtes avec size",
|
|
66
|
+
"it": "Keyword ioBroker; header con size",
|
|
67
|
+
"es": "Keyword ioBroker; encabezados con size",
|
|
68
|
+
"pl": "Słowo kluczowe ioBroker; nagłówki z size",
|
|
69
|
+
"uk": "Ключове слово ioBroker",
|
|
70
|
+
"zh-cn": "ioBroker 关键词与 header size"
|
|
71
|
+
},
|
|
7
72
|
"0.9.8": {
|
|
8
73
|
"en": "Complete admin responsive layout (lg/xl); green CI release",
|
|
9
74
|
"de": "Admin-Layout lg/xl vervollständigt; grüner CI-Release",
|
|
@@ -138,7 +203,12 @@
|
|
|
138
203
|
"enableAllDevices": true,
|
|
139
204
|
"selectedSiteId": "",
|
|
140
205
|
"selectedDeviceIds": "",
|
|
141
|
-
"deviceListJson": ""
|
|
206
|
+
"deviceListJson": "",
|
|
207
|
+
"enableCurtailmentAvoidance": false,
|
|
208
|
+
"curtailmentForecastPath": "solarprognose.0.forecast.00.hourly",
|
|
209
|
+
"curtailmentModeBefore": "smartmeter",
|
|
210
|
+
"curtailmentModeAfter": "smartmeter",
|
|
211
|
+
"curtailmentDevicesJson": "[]"
|
|
142
212
|
},
|
|
143
213
|
"objects": [],
|
|
144
214
|
"instanceObjects": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.anker-solix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "ioBroker adapter for Anker Solix (based on ha-anker-solix)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "MatthiasUlrich1",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"homepage": "https://github.com/MatthiasUlrich1/ioBroker.anker-solix",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
+
"ioBroker",
|
|
13
14
|
"iobroker",
|
|
14
15
|
"iobroker-adapter",
|
|
15
16
|
"anker",
|