iobroker.anker-solix 0.10.13 → 0.10.14
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 +1 -1
- package/admin/i18n/de.json +1 -1
- package/admin/i18n/en.json +1 -1
- package/build/lib/curtailmentPower.js +14 -6
- package/build/lib/curtailmentPower.js.map +2 -2
- package/build/lib/curtailmentRunner.js +22 -81
- package/build/lib/curtailmentRunner.js.map +2 -2
- package/build/lib/curtailmentStates.js +14 -1
- package/build/lib/curtailmentStates.js.map +2 -2
- package/io-package.json +13 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -288,7 +288,7 @@ German guides/videos linked from the [HA README](https://github.com/thomluther/h
|
|
|
288
288
|
|
|
289
289
|
## Curtailment avoidance (optional)
|
|
290
290
|
|
|
291
|
-
Tab **Abregelungsvermeidung** / **Curtailment avoidance**: [solarprognose](https://github.com/ioBroker/ioBroker.solarprognose) detects overproduction days. **
|
|
291
|
+
Tab **Abregelungsvermeidung** / **Curtailment avoidance**: [solarprognose](https://github.com/ioBroker/ioBroker.solarprognose) detects overproduction days. **Controls only:** **manual** mode + **`ac_output_limit`** (AC output / export). **Does not** change station base settings (grid export cap, `allow_grid_export`, home load preset, AC charge limit). **Before:** `ac_output_limit` = live PV. **Active:** `missing_charge_wh`, `max_charge_w` = `missing_charge_wh` ÷ `remaining_hours`, `export_w` = `live_pv_w` − `max_charge_w`, `ac_output_limit` = `export_w`. **After:** restore selected mode. States: `curtailment.live_pv_w`, `missing_charge_wh`, `max_charge_w`, `export_w`, `remaining_hours`.
|
|
292
292
|
|
|
293
293
|
**Admin:** checkbox *Combiner box present* — without combiner: device ID + solarbank type + battery Wh; with combiner: combiner ID + up to **4** solarbank slots (each slot can be *none*). **Combiner:** total AC limit = **sum** of per-unit limits (SB2 **1000** W, SB3 Pro **1200** W, SB4 Pro **2500** W). **Standalone:** always **800** W.
|
|
294
294
|
|
package/admin/i18n/de.json
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"cloud_state": "Cloud-Status",
|
|
75
75
|
"wifi_state": "WLAN-Status",
|
|
76
76
|
"CurtailmentAvoidance": "Abregelungsvermeidung",
|
|
77
|
-
"curtailment_hint": "
|
|
77
|
+
"curtailment_hint": "Nur Benutzerdefiniert + AC-Ausgangsleistung (ac_output_limit). Keine Grundeinstellungen (Einspeise-Limit, allow_grid_export, Hauslast). Active: missing_charge_wh, max_charge_w = missing_wh ÷ remaining_hours, export_w = live_pv_w − max_charge_w. Vorher: export = live_pv_w.",
|
|
78
78
|
"enableCurtailmentAvoidance": "Abregelungsvermeidung aktivieren",
|
|
79
79
|
"curtailmentForecastPath": "Objektpfad Prognose (Basis hourly, z. B. solarprognose.0.forecast.00.hourly)",
|
|
80
80
|
"curtailmentModeAfter": "Nutzungsmodus nach Abregelungsfenster",
|
package/admin/i18n/en.json
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"cloud_state": "Cloud state",
|
|
75
75
|
"wifi_state": "WiFi state",
|
|
76
76
|
"CurtailmentAvoidance": "Curtailment avoidance",
|
|
77
|
-
"curtailment_hint": "
|
|
77
|
+
"curtailment_hint": "Manual mode + ac_output_limit only (no grid export cap / allow_grid_export / home load). Active: missing_charge_wh, max_charge_w = missing_wh ÷ remaining_hours, export_w = live_pv_w − max_charge_w. Before: export = live_pv_w. Requires MQTT.",
|
|
78
78
|
"enableCurtailmentAvoidance": "Enable curtailment avoidance",
|
|
79
79
|
"curtailmentForecastPath": "Forecast base path (hourly, e.g. solarprognose.0.forecast.00.hourly)",
|
|
80
80
|
"curtailmentModeAfter": "Usage mode after curtailment window",
|
|
@@ -21,6 +21,7 @@ __export(curtailmentPower_exports, {
|
|
|
21
21
|
COMBINER_MAX_AC_OUTPUT_W: () => COMBINER_MAX_AC_OUTPUT_W,
|
|
22
22
|
PV_SENSOR_IDS: () => PV_SENSOR_IDS,
|
|
23
23
|
calcMaxChargeW: () => calcMaxChargeW,
|
|
24
|
+
calcMissingChargeWh: () => calcMissingChargeWh,
|
|
24
25
|
isPvGenerationSensor: () => isPvGenerationSensor,
|
|
25
26
|
isPvSensorEntity: () => isPvSensorEntity,
|
|
26
27
|
parsePvSensorStateId: () => parsePvSensorStateId,
|
|
@@ -142,20 +143,26 @@ function resolveBeforeExportW(livePvW, forecast, nowHour, window) {
|
|
|
142
143
|
return (0, import_curtailmentForecast.forecastExportTargetW)(forecast, nowHour, window);
|
|
143
144
|
}
|
|
144
145
|
const COMBINER_MAX_AC_OUTPUT_W = 4800;
|
|
145
|
-
function
|
|
146
|
-
if (
|
|
146
|
+
function calcMissingChargeWh(batteryCapacityWh, socPercent) {
|
|
147
|
+
if (batteryCapacityWh <= 0) {
|
|
147
148
|
return 0;
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
const soc = Math.min(100, Math.max(0, socPercent));
|
|
151
|
+
return Math.max(0, Math.round((100 - soc) / 100 * batteryCapacityWh));
|
|
150
152
|
}
|
|
151
|
-
function calcMaxChargeW(
|
|
153
|
+
function calcMaxChargeW(missingWh, hoursRemaining) {
|
|
152
154
|
const hours = Math.max(1, hoursRemaining);
|
|
153
|
-
if (
|
|
155
|
+
if (missingWh <= 0) {
|
|
154
156
|
return 0;
|
|
155
157
|
}
|
|
156
|
-
const missingWh = (100 - socPercent) / 100 * batteryCapacityWh;
|
|
157
158
|
return Math.max(0, Math.round(missingWh / hours));
|
|
158
159
|
}
|
|
160
|
+
function resolveActiveExportW(livePvW, maxChargeW) {
|
|
161
|
+
if (livePvW <= 0) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
return Math.max(0, Math.round(livePvW - Math.max(0, maxChargeW)));
|
|
165
|
+
}
|
|
159
166
|
function resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHour, window) {
|
|
160
167
|
if (phase === "before") {
|
|
161
168
|
return { exportW: resolveBeforeExportW(livePvW, forecast, nowHour, window), chargeW: 0 };
|
|
@@ -170,6 +177,7 @@ function resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHo
|
|
|
170
177
|
COMBINER_MAX_AC_OUTPUT_W,
|
|
171
178
|
PV_SENSOR_IDS,
|
|
172
179
|
calcMaxChargeW,
|
|
180
|
+
calcMissingChargeWh,
|
|
173
181
|
isPvGenerationSensor,
|
|
174
182
|
isPvSensorEntity,
|
|
175
183
|
parsePvSensorStateId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/curtailmentPower.ts"],
|
|
4
|
-
"sourcesContent": ["import { forecastExportTargetW } from \"./curtailmentForecast\";\nimport type { CurtailmentPhase, CurtailmentWindow, HourlyForecast } from \"./curtailmentTypes\";\n\n/** Sensors that reflect current PV generation (W). */\nexport const PV_SENSOR_IDS = [\"total_pv_power\", \"input_power\", \"solar_power_total\"] as const;\n\n/** Optional power-flow sensors: sum \u2248 total PV when direct sensors are missing. */\nconst PV_FLOW_SUM_IDS = [\"pv_to_home_power\", \"pv_to_battery_power\", \"photovoltaic_to_grid_power\"] as const;\n\nexport type PvSensorId = (typeof PV_SENSOR_IDS)[number];\n\nexport interface CurtailmentPowerHost {\n\tnamespace: string;\n\tgetStateAsync: (id: string) => Promise<ioBroker.State | null | undefined>;\n\tgetDeviceEntities?: (deviceId: string) => Record<string, unknown> | undefined;\n\t/** Anker site UUID for system.*.sensors.total_pv_power (preferred PV source). */\n\tgetDeviceSiteId?: (deviceId: string) => string | undefined;\n}\n\nexport function systemTotalPvStatePath(namespace: string, siteId: string): string {\n\treturn `${namespace}.system.${siteId}.sensors.total_pv_power`;\n}\n\nexport function pvSensorStatePaths(namespace: string, deviceId: string): string[] {\n\tconst paths: string[] = [];\n\tfor (const channel of [\"solarbank\", \"combiner_box\"] as const) {\n\t\tfor (const sensor of [...PV_SENSOR_IDS, ...PV_FLOW_SUM_IDS]) {\n\t\t\tpaths.push(`${namespace}.${channel}.${deviceId}.sensors.${sensor}`);\n\t\t}\n\t}\n\treturn paths;\n}\n\nexport function parseSystemPvStateId(namespace: string, stateId: string): { siteId: string } | undefined {\n\tconst prefix = `${namespace}.`;\n\tif (!stateId.startsWith(prefix)) {\n\t\treturn undefined;\n\t}\n\tconst rest = stateId.slice(prefix.length);\n\tconst match = /^system\\.([^.]+)\\.sensors\\.total_pv_power$/.exec(rest);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn { siteId: match[1] ?? \"\" };\n}\n\nexport function parsePvSensorStateId(\n\tnamespace: string,\n\tstateId: string,\n): { deviceId: string; sensor: PvSensorId } | undefined {\n\tconst prefix = `${namespace}.`;\n\tif (!stateId.startsWith(prefix) || !stateId.includes(\".sensors.\")) {\n\t\treturn undefined;\n\t}\n\tconst rest = stateId.slice(prefix.length);\n\tconst match = /^(?:solarbank|combiner_box)\\.([^.]+)\\.sensors\\.(total_pv_power|input_power)$/.exec(rest);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn { deviceId: match[1] ?? \"\", sensor: match[2] as PvSensorId };\n}\n\nexport function isPvSensorEntity(entityId: string): entityId is PvSensorId {\n\treturn (PV_SENSOR_IDS as readonly string[]).includes(entityId);\n}\n\nexport function isPvGenerationSensor(entityId: string): boolean {\n\treturn isPvSensorEntity(entityId) || (PV_FLOW_SUM_IDS as readonly string[]).includes(entityId);\n}\n\n/** Best estimate of current PV generation (W) from poll entity map. */\nexport function readPvFromEntities(entities: Record<string, unknown> | undefined): number {\n\tif (!entities) {\n\t\treturn 0;\n\t}\n\tlet max = 0;\n\tfor (const key of PV_SENSOR_IDS) {\n\t\tconst n = Number(entities[key]);\n\t\tif (Number.isFinite(n) && n > max) {\n\t\t\tmax = n;\n\t\t}\n\t}\n\tif (max > 0) {\n\t\treturn Math.round(max);\n\t}\n\tlet flowSum = 0;\n\tfor (const key of PV_FLOW_SUM_IDS) {\n\t\tconst n = Number(entities[key]);\n\t\tif (Number.isFinite(n) && n > 0) {\n\t\t\tflowSum += n;\n\t\t}\n\t}\n\tif (flowSum > 0) {\n\t\treturn Math.round(flowSum);\n\t}\n\treturn 0;\n}\n\nasync function readSystemTotalPvW(host: CurtailmentPowerHost, siteId: string): Promise<number> {\n\tconst st = await host.getStateAsync(systemTotalPvStatePath(host.namespace, siteId));\n\tconst n = Number(st?.val);\n\treturn Number.isFinite(n) && n > 0 ? Math.round(n) : 0;\n}\n\n/** Read live PV generation (W): system.total_pv_power first, then device sensors. */\nexport async function readLivePvPowerW(host: CurtailmentPowerHost, deviceId: string): Promise<number> {\n\tconst siteId = host.getDeviceSiteId?.(deviceId)?.trim();\n\tif (siteId) {\n\t\tconst fromSystem = await readSystemTotalPvW(host, siteId);\n\t\tif (fromSystem > 0) {\n\t\t\treturn fromSystem;\n\t\t}\n\t}\n\tconst fromPoll = readPvFromEntities(host.getDeviceEntities?.(deviceId));\n\tif (fromPoll > 0) {\n\t\treturn fromPoll;\n\t}\n\tlet max = 0;\n\tfor (const id of pvSensorStatePaths(host.namespace, deviceId)) {\n\t\tconst st = await host.getStateAsync(id);\n\t\tconst n = Number(st?.val);\n\t\tif (Number.isFinite(n) && n > max) {\n\t\t\tmax = n;\n\t\t}\n\t}\n\treturn max > 0 ? Math.round(max) : 0;\n}\n\n/** Before window: export all live generation, no battery charging. */\nexport function resolveBeforeExportW(\n\tlivePvW: number,\n\tforecast: HourlyForecast,\n\tnowHour: number,\n\twindow: CurtailmentWindow,\n): number {\n\tif (livePvW > 0) {\n\t\treturn livePvW;\n\t}\n\treturn forecastExportTargetW(forecast, nowHour, window);\n}\n\n/** Combiner / multisystem AC output (max_load_parallel MQTT steps up to 4800 W). */\nexport const COMBINER_MAX_AC_OUTPUT_W = 4800;\n\n
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { forecastExportTargetW } from \"./curtailmentForecast\";\nimport type { CurtailmentPhase, CurtailmentWindow, HourlyForecast } from \"./curtailmentTypes\";\n\n/** Sensors that reflect current PV generation (W). */\nexport const PV_SENSOR_IDS = [\"total_pv_power\", \"input_power\", \"solar_power_total\"] as const;\n\n/** Optional power-flow sensors: sum \u2248 total PV when direct sensors are missing. */\nconst PV_FLOW_SUM_IDS = [\"pv_to_home_power\", \"pv_to_battery_power\", \"photovoltaic_to_grid_power\"] as const;\n\nexport type PvSensorId = (typeof PV_SENSOR_IDS)[number];\n\nexport interface CurtailmentPowerHost {\n\tnamespace: string;\n\tgetStateAsync: (id: string) => Promise<ioBroker.State | null | undefined>;\n\tgetDeviceEntities?: (deviceId: string) => Record<string, unknown> | undefined;\n\t/** Anker site UUID for system.*.sensors.total_pv_power (preferred PV source). */\n\tgetDeviceSiteId?: (deviceId: string) => string | undefined;\n}\n\nexport function systemTotalPvStatePath(namespace: string, siteId: string): string {\n\treturn `${namespace}.system.${siteId}.sensors.total_pv_power`;\n}\n\nexport function pvSensorStatePaths(namespace: string, deviceId: string): string[] {\n\tconst paths: string[] = [];\n\tfor (const channel of [\"solarbank\", \"combiner_box\"] as const) {\n\t\tfor (const sensor of [...PV_SENSOR_IDS, ...PV_FLOW_SUM_IDS]) {\n\t\t\tpaths.push(`${namespace}.${channel}.${deviceId}.sensors.${sensor}`);\n\t\t}\n\t}\n\treturn paths;\n}\n\nexport function parseSystemPvStateId(namespace: string, stateId: string): { siteId: string } | undefined {\n\tconst prefix = `${namespace}.`;\n\tif (!stateId.startsWith(prefix)) {\n\t\treturn undefined;\n\t}\n\tconst rest = stateId.slice(prefix.length);\n\tconst match = /^system\\.([^.]+)\\.sensors\\.total_pv_power$/.exec(rest);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn { siteId: match[1] ?? \"\" };\n}\n\nexport function parsePvSensorStateId(\n\tnamespace: string,\n\tstateId: string,\n): { deviceId: string; sensor: PvSensorId } | undefined {\n\tconst prefix = `${namespace}.`;\n\tif (!stateId.startsWith(prefix) || !stateId.includes(\".sensors.\")) {\n\t\treturn undefined;\n\t}\n\tconst rest = stateId.slice(prefix.length);\n\tconst match = /^(?:solarbank|combiner_box)\\.([^.]+)\\.sensors\\.(total_pv_power|input_power)$/.exec(rest);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn { deviceId: match[1] ?? \"\", sensor: match[2] as PvSensorId };\n}\n\nexport function isPvSensorEntity(entityId: string): entityId is PvSensorId {\n\treturn (PV_SENSOR_IDS as readonly string[]).includes(entityId);\n}\n\nexport function isPvGenerationSensor(entityId: string): boolean {\n\treturn isPvSensorEntity(entityId) || (PV_FLOW_SUM_IDS as readonly string[]).includes(entityId);\n}\n\n/** Best estimate of current PV generation (W) from poll entity map. */\nexport function readPvFromEntities(entities: Record<string, unknown> | undefined): number {\n\tif (!entities) {\n\t\treturn 0;\n\t}\n\tlet max = 0;\n\tfor (const key of PV_SENSOR_IDS) {\n\t\tconst n = Number(entities[key]);\n\t\tif (Number.isFinite(n) && n > max) {\n\t\t\tmax = n;\n\t\t}\n\t}\n\tif (max > 0) {\n\t\treturn Math.round(max);\n\t}\n\tlet flowSum = 0;\n\tfor (const key of PV_FLOW_SUM_IDS) {\n\t\tconst n = Number(entities[key]);\n\t\tif (Number.isFinite(n) && n > 0) {\n\t\t\tflowSum += n;\n\t\t}\n\t}\n\tif (flowSum > 0) {\n\t\treturn Math.round(flowSum);\n\t}\n\treturn 0;\n}\n\nasync function readSystemTotalPvW(host: CurtailmentPowerHost, siteId: string): Promise<number> {\n\tconst st = await host.getStateAsync(systemTotalPvStatePath(host.namespace, siteId));\n\tconst n = Number(st?.val);\n\treturn Number.isFinite(n) && n > 0 ? Math.round(n) : 0;\n}\n\n/** Read live PV generation (W): system.total_pv_power first, then device sensors. */\nexport async function readLivePvPowerW(host: CurtailmentPowerHost, deviceId: string): Promise<number> {\n\tconst siteId = host.getDeviceSiteId?.(deviceId)?.trim();\n\tif (siteId) {\n\t\tconst fromSystem = await readSystemTotalPvW(host, siteId);\n\t\tif (fromSystem > 0) {\n\t\t\treturn fromSystem;\n\t\t}\n\t}\n\tconst fromPoll = readPvFromEntities(host.getDeviceEntities?.(deviceId));\n\tif (fromPoll > 0) {\n\t\treturn fromPoll;\n\t}\n\tlet max = 0;\n\tfor (const id of pvSensorStatePaths(host.namespace, deviceId)) {\n\t\tconst st = await host.getStateAsync(id);\n\t\tconst n = Number(st?.val);\n\t\tif (Number.isFinite(n) && n > max) {\n\t\t\tmax = n;\n\t\t}\n\t}\n\treturn max > 0 ? Math.round(max) : 0;\n}\n\n/** Before window: export all live generation, no battery charging. */\nexport function resolveBeforeExportW(\n\tlivePvW: number,\n\tforecast: HourlyForecast,\n\tnowHour: number,\n\twindow: CurtailmentWindow,\n): number {\n\tif (livePvW > 0) {\n\t\treturn livePvW;\n\t}\n\treturn forecastExportTargetW(forecast, nowHour, window);\n}\n\n/** Combiner / multisystem AC output (max_load_parallel MQTT steps up to 4800 W). */\nexport const COMBINER_MAX_AC_OUTPUT_W = 4800;\n\n/** Wh still required to reach 100 % SOC (active phase). */\nexport function calcMissingChargeWh(batteryCapacityWh: number, socPercent: number): number {\n\tif (batteryCapacityWh <= 0) {\n\t\treturn 0;\n\t}\n\tconst soc = Math.min(100, Math.max(0, socPercent));\n\treturn Math.max(0, Math.round(((100 - soc) / 100) * batteryCapacityWh));\n}\n\n/** Max AC charge power (W) = missing Wh \u00F7 remaining curtailment hours. */\nexport function calcMaxChargeW(missingWh: number, hoursRemaining: number): number {\n\tconst hours = Math.max(1, hoursRemaining);\n\tif (missingWh <= 0) {\n\t\treturn 0;\n\t}\n\treturn Math.max(0, Math.round(missingWh / hours));\n}\n\n/** Active window: AC output (export) = live PV \u2212 max charge power. */\nexport function resolveActiveExportW(livePvW: number, maxChargeW: number): number {\n\tif (livePvW <= 0) {\n\t\treturn 0;\n\t}\n\treturn Math.max(0, Math.round(livePvW - Math.max(0, maxChargeW)));\n}\n\nexport function resolveCurtailmentSetpoints(\n\tphase: CurtailmentPhase,\n\tlivePvW: number,\n\tmaxChargeW: number,\n\tforecast: HourlyForecast,\n\tnowHour: number,\n\twindow: CurtailmentWindow,\n): { exportW: number; chargeW: number } {\n\tif (phase === \"before\") {\n\t\treturn { exportW: resolveBeforeExportW(livePvW, forecast, nowHour, window), chargeW: 0 };\n\t}\n\tif (phase === \"active\") {\n\t\treturn { exportW: resolveActiveExportW(livePvW, maxChargeW), chargeW: maxChargeW };\n\t}\n\treturn { exportW: 0, chargeW: 0 };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var curtailmentRunner_exports = {};
|
|
20
20
|
__export(curtailmentRunner_exports, {
|
|
21
|
-
COMBINER_MIN_HOME_LOAD_W: () => COMBINER_MIN_HOME_LOAD_W,
|
|
22
21
|
runCurtailmentAvoidance: () => runCurtailmentAvoidance,
|
|
23
22
|
runCurtailmentOnPvChange: () => runCurtailmentOnPvChange
|
|
24
23
|
});
|
|
@@ -28,10 +27,7 @@ var import_curtailmentConfig = require("./curtailmentConfig");
|
|
|
28
27
|
var import_curtailmentForecast = require("./curtailmentForecast");
|
|
29
28
|
var import_curtailmentPower = require("./curtailmentPower");
|
|
30
29
|
var import_curtailmentStates = require("./curtailmentStates");
|
|
31
|
-
const COMBINER_MIN_HOME_LOAD_W = 0;
|
|
32
30
|
const lastAppliedExportW = /* @__PURE__ */ new Map();
|
|
33
|
-
const lastAppliedHomeLoadW = /* @__PURE__ */ new Map();
|
|
34
|
-
const lastAppliedChargeW = /* @__PURE__ */ new Map();
|
|
35
31
|
const lastAppliedPhase = /* @__PURE__ */ new Map();
|
|
36
32
|
function berlinHour() {
|
|
37
33
|
var _a;
|
|
@@ -43,12 +39,12 @@ function berlinHour() {
|
|
|
43
39
|
const h = (_a = parts.find((p) => p.type === "hour")) == null ? void 0 : _a.value;
|
|
44
40
|
return Math.min(23, Math.max(0, Number(h) || 0));
|
|
45
41
|
}
|
|
46
|
-
function
|
|
42
|
+
function clampAcOutputW(powerW, role) {
|
|
47
43
|
if (powerW <= 0) {
|
|
48
44
|
return 0;
|
|
49
45
|
}
|
|
50
|
-
const hardwareMax = role === "combiner" ?
|
|
51
|
-
return Math.min(hardwareMax, Math.max(
|
|
46
|
+
const hardwareMax = role === "combiner" ? 4800 : 1e5;
|
|
47
|
+
return Math.min(hardwareMax, Math.max(0, Math.round(powerW)));
|
|
52
48
|
}
|
|
53
49
|
async function readSocPercent(host, deviceId) {
|
|
54
50
|
const candidates = [
|
|
@@ -67,82 +63,27 @@ async function readSocPercent(host, deviceId) {
|
|
|
67
63
|
}
|
|
68
64
|
return 0;
|
|
69
65
|
}
|
|
70
|
-
async function
|
|
71
|
-
try {
|
|
72
|
-
await host.applyControl(deviceId, control, value, ctx);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
host.log.debug(`Curtailment optional control ${control} skipped: ${err.message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async function applyManualAndExportSwitches(host, device) {
|
|
66
|
+
async function applyManualMode(host, device) {
|
|
78
67
|
const ctx = host.getDeviceContext(device.deviceId);
|
|
79
68
|
await host.applyControl(device.deviceId, "preset_usage_mode", "manual", ctx);
|
|
80
|
-
await applyOptionalControl(host, device.deviceId, "preset_allow_export", true, ctx);
|
|
81
|
-
await applyOptionalControl(host, device.deviceId, "allow_grid_export", true, ctx);
|
|
82
|
-
}
|
|
83
|
-
async function applyChargeLimit(host, device, chargeW) {
|
|
84
|
-
const rounded = Math.max(0, Math.round(chargeW));
|
|
85
|
-
const last = lastAppliedChargeW.get(device.deviceId);
|
|
86
|
-
if (last === rounded) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const ctx = host.getDeviceContext(device.deviceId);
|
|
90
|
-
await host.applyControl(device.deviceId, "ac_charge_limit", rounded, ctx);
|
|
91
|
-
lastAppliedChargeW.set(device.deviceId, rounded);
|
|
92
|
-
}
|
|
93
|
-
function deviceHasControl(host, deviceId, control) {
|
|
94
|
-
var _a;
|
|
95
|
-
const writable = (_a = host.getDeviceWritable) == null ? void 0 : _a.call(host, deviceId);
|
|
96
|
-
if (!(writable == null ? void 0 : writable.length)) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
return writable.includes(control);
|
|
100
69
|
}
|
|
101
|
-
async function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
lastAppliedExportW.set(deviceId, exportW);
|
|
106
|
-
}
|
|
107
|
-
const homeW = COMBINER_MIN_HOME_LOAD_W;
|
|
108
|
-
const lastHome = lastAppliedHomeLoadW.get(deviceId);
|
|
109
|
-
if (lastHome !== homeW) {
|
|
110
|
-
await applyOptionalControl(host, deviceId, "set_output_power", homeW, ctx);
|
|
111
|
-
lastAppliedHomeLoadW.set(deviceId, homeW);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async function applyExportLimit(host, device, exportTargetW) {
|
|
115
|
-
const exportW = clampExportW(exportTargetW, device.role);
|
|
116
|
-
if (exportW <= 0) {
|
|
70
|
+
async function applyAcOutputLimit(host, device, targetW) {
|
|
71
|
+
const acOutputW = clampAcOutputW(targetW, device.role);
|
|
72
|
+
const last = lastAppliedExportW.get(device.deviceId);
|
|
73
|
+
if (last === acOutputW) {
|
|
117
74
|
return;
|
|
118
75
|
}
|
|
119
76
|
const ctx = host.getDeviceContext(device.deviceId);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await applyCombinerExportLimit(host, id, exportW, ctx);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const last = lastAppliedExportW.get(id);
|
|
126
|
-
if (last === exportW) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (deviceHasControl(host, id, "ac_output_limit")) {
|
|
130
|
-
await host.applyControl(id, "ac_output_limit", exportW, ctx);
|
|
131
|
-
}
|
|
132
|
-
if (deviceHasControl(host, id, "set_output_power")) {
|
|
133
|
-
await host.applyControl(id, "set_output_power", exportW, ctx);
|
|
134
|
-
}
|
|
135
|
-
lastAppliedExportW.set(id, exportW);
|
|
77
|
+
await host.applyControl(device.deviceId, "ac_output_limit", acOutputW, ctx);
|
|
78
|
+
lastAppliedExportW.set(device.deviceId, acOutputW);
|
|
136
79
|
}
|
|
137
80
|
async function applyAfterPhase(host, device, modeAfter) {
|
|
138
81
|
lastAppliedExportW.delete(device.deviceId);
|
|
139
|
-
lastAppliedHomeLoadW.delete(device.deviceId);
|
|
140
|
-
lastAppliedChargeW.delete(device.deviceId);
|
|
141
82
|
lastAppliedPhase.delete(device.deviceId);
|
|
142
83
|
const ctx = host.getDeviceContext(device.deviceId);
|
|
143
84
|
await host.applyControl(device.deviceId, "preset_usage_mode", modeAfter, ctx);
|
|
144
85
|
}
|
|
145
|
-
async function applyCurtailmentSetpoints(host, device, phase, exportW,
|
|
86
|
+
async function applyCurtailmentSetpoints(host, device, phase, exportW, modeAfter, opts) {
|
|
146
87
|
const prevPhase = lastAppliedPhase.get(device.deviceId);
|
|
147
88
|
const phaseChanged = prevPhase !== phase;
|
|
148
89
|
if (phase === "after" || phase === "idle") {
|
|
@@ -150,14 +91,13 @@ async function applyCurtailmentSetpoints(host, device, phase, exportW, chargeW,
|
|
|
150
91
|
return;
|
|
151
92
|
}
|
|
152
93
|
if (phaseChanged || !(opts == null ? void 0 : opts.modeOnly)) {
|
|
153
|
-
await
|
|
94
|
+
await applyManualMode(host, device);
|
|
154
95
|
lastAppliedPhase.set(device.deviceId, phase);
|
|
155
96
|
}
|
|
156
97
|
if (opts == null ? void 0 : opts.modeOnly) {
|
|
157
98
|
return;
|
|
158
99
|
}
|
|
159
|
-
await
|
|
160
|
-
await applyExportLimit(host, device, exportW);
|
|
100
|
+
await applyAcOutputLimit(host, device, exportW);
|
|
161
101
|
}
|
|
162
102
|
async function buildDeviceContext(host, device, forecast, nowHour, livePvOverride) {
|
|
163
103
|
const limit = (0, import_curtailmentProfiles.acExportLimitW)(device);
|
|
@@ -166,9 +106,10 @@ async function buildDeviceContext(host, device, forecast, nowHour, livePvOverrid
|
|
|
166
106
|
const livePvW = livePvOverride !== void 0 && livePvOverride >= 0 ? Math.round(livePvOverride) : window.today ? await (0, import_curtailmentPower.readLivePvPowerW)(host, device.deviceId) : 0;
|
|
167
107
|
const remaining = (0, import_curtailmentForecast.remainingCurtailmentHours)(window, nowHour);
|
|
168
108
|
const soc = window.today && phase === "active" ? await readSocPercent(host, device.deviceId) : 0;
|
|
169
|
-
const
|
|
109
|
+
const missingChargeWh = window.today && phase === "active" ? (0, import_curtailmentPower.calcMissingChargeWh)(device.batteryCapacityWh, soc) : 0;
|
|
110
|
+
const maxChargeW = window.today && phase === "active" ? (0, import_curtailmentPower.calcMaxChargeW)(missingChargeWh, remaining) : 0;
|
|
170
111
|
const { exportW, chargeW } = (0, import_curtailmentPower.resolveCurtailmentSetpoints)(phase, livePvW, maxChargeW, forecast, nowHour, window);
|
|
171
|
-
return { limit, window, phase, livePvW, maxChargeW, exportW, chargeW, remaining, soc };
|
|
112
|
+
return { limit, window, phase, livePvW, missingChargeWh, maxChargeW, exportW, chargeW, remaining, soc };
|
|
172
113
|
}
|
|
173
114
|
async function publishDeviceStates(host, ctx) {
|
|
174
115
|
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.today, ctx.window.today, true);
|
|
@@ -182,6 +123,7 @@ async function publishDeviceStates(host, ctx) {
|
|
|
182
123
|
ctx.window.today ? `${ctx.window.endHour.toString().padStart(2, "0")}:00` : "",
|
|
183
124
|
true
|
|
184
125
|
);
|
|
126
|
+
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.missingChargeWh, ctx.missingChargeWh, true);
|
|
185
127
|
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.maxChargeW, ctx.maxChargeW, true);
|
|
186
128
|
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.exportW, ctx.exportW, true);
|
|
187
129
|
await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.remainingHours, ctx.remaining, true);
|
|
@@ -198,18 +140,18 @@ async function runDeviceCurtailment(host, device, config, forecast, nowHour, opt
|
|
|
198
140
|
}
|
|
199
141
|
if (ctx.phase !== "before" && ctx.phase !== "active") {
|
|
200
142
|
if (!(opts == null ? void 0 : opts.setpointsOnly)) {
|
|
201
|
-
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW,
|
|
143
|
+
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter);
|
|
202
144
|
}
|
|
203
145
|
return;
|
|
204
146
|
}
|
|
205
147
|
if (!(opts == null ? void 0 : opts.setpointsOnly)) {
|
|
206
148
|
const unitsHint = device.role === "combiner" && ((_a = device.units) == null ? void 0 : _a.length) ? `, units=${device.units.join("+")} (${device.units.length} banks)` : "";
|
|
207
149
|
host.log.info(
|
|
208
|
-
`Curtailment [${device.deviceId}]: phase=${ctx.phase}, limit=${ctx.limit}W${unitsHint}, window ${ctx.window.startHour}-${ctx.window.endHour}h, livePv=${ctx.livePvW}W,
|
|
150
|
+
`Curtailment [${device.deviceId}]: phase=${ctx.phase}, limit=${ctx.limit}W${unitsHint}, window ${ctx.window.startHour}-${ctx.window.endHour}h, livePv=${ctx.livePvW}W, missingWh=${ctx.missingChargeWh}, maxCharge=${ctx.maxChargeW}W, export=${ctx.exportW}W, SOC=${ctx.soc}%`
|
|
209
151
|
);
|
|
210
152
|
}
|
|
211
153
|
try {
|
|
212
|
-
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW,
|
|
154
|
+
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {
|
|
213
155
|
modeOnly: (opts == null ? void 0 : opts.setpointsOnly) && lastAppliedPhase.get(device.deviceId) === ctx.phase
|
|
214
156
|
});
|
|
215
157
|
} catch (err) {
|
|
@@ -238,11 +180,11 @@ async function runCurtailmentOnPvChange(host, config, deviceId, livePvW) {
|
|
|
238
180
|
}
|
|
239
181
|
await publishDeviceStates(host, ctx);
|
|
240
182
|
try {
|
|
241
|
-
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW,
|
|
183
|
+
await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {
|
|
242
184
|
modeOnly: lastAppliedPhase.get(device.deviceId) === ctx.phase
|
|
243
185
|
});
|
|
244
186
|
host.log.debug(
|
|
245
|
-
`Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W,
|
|
187
|
+
`Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W, missingWh=${ctx.missingChargeWh}, maxCharge=${ctx.maxChargeW}W, export=${ctx.exportW}W`
|
|
246
188
|
);
|
|
247
189
|
} catch (err) {
|
|
248
190
|
host.log.warn(`Curtailment PV follow failed for ${device.deviceId}: ${err.message}`);
|
|
@@ -273,7 +215,6 @@ async function runCurtailmentAvoidance(host, config) {
|
|
|
273
215
|
}
|
|
274
216
|
// Annotate the CommonJS export names for ESM import in node:
|
|
275
217
|
0 && (module.exports = {
|
|
276
|
-
COMBINER_MIN_HOME_LOAD_W,
|
|
277
218
|
runCurtailmentAvoidance,
|
|
278
219
|
runCurtailmentOnPvChange
|
|
279
220
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/curtailmentRunner.ts"],
|
|
4
|
-
"sourcesContent": ["import { acExportLimitW } from \"./curtailmentProfiles\";\nimport { resolveCurtailmentDevices, type CurtailmentStructuredNative } from \"./curtailmentConfig\";\nimport {\n\tcurrentPhase,\n\tdetectCurtailmentWindow,\n\treadHourlyForecast,\n\tremainingCurtailmentHours,\n} from \"./curtailmentForecast\";\nimport {\n\tCOMBINER_MAX_AC_OUTPUT_W,\n\tcalcMaxChargeW,\n\treadLivePvPowerW,\n\tresolveCurtailmentSetpoints,\n\ttype CurtailmentPowerHost,\n} from \"./curtailmentPower\";\nimport { CURTAILMENT_STATE_IDS } from \"./curtailmentStates\";\nimport type { CurtailmentDeviceConfig, CurtailmentDeviceRole, CurtailmentPhase } from \"./curtailmentTypes\";\nimport type { DeviceControlContext } from \"./types\";\n\nexport interface CurtailmentRunnerConfig extends CurtailmentStructuredNative {\n\tenabled: boolean;\n\tforecastBasePath: string;\n\tmodeAfter: \"smartmeter\" | \"smart\";\n}\n\nexport interface CurtailmentRunnerHost extends CurtailmentPowerHost {\n\t/** Writable control ids from last poll (e.g. set_output_power on combiner). */\n\tgetDeviceWritable?: (deviceId: string) => string[] | undefined;\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\tgetForeignObjectAsync?: (id: string) => Promise<ioBroker.Object | 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\n/** Combiner manual schedule: home load preset (W), not grid export \u2014 keep minimal so surplus goes to grid. */\nexport const COMBINER_MIN_HOME_LOAD_W = 0;\n\nconst lastAppliedExportW = new Map<string, number>();\nconst lastAppliedHomeLoadW = new Map<string, number>();\nconst lastAppliedChargeW = new Map<string, number>();\nconst lastAppliedPhase = new Map<string, CurtailmentPhase>();\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\nfunction clampExportW(powerW: number, role: CurtailmentDeviceRole): number {\n\tif (powerW <= 0) {\n\t\treturn 0;\n\t}\n\tconst hardwareMax = role === \"combiner\" ? COMBINER_MAX_AC_OUTPUT_W : 100_000;\n\treturn Math.min(hardwareMax, Math.max(100, Math.round(powerW)));\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\nasync function applyOptionalControl(\n\thost: CurtailmentRunnerHost,\n\tdeviceId: string,\n\tcontrol: string,\n\tvalue: string | number | boolean,\n\tctx: DeviceControlContext | undefined,\n): Promise<void> {\n\ttry {\n\t\tawait host.applyControl(deviceId, control, value, ctx);\n\t} catch (err) {\n\t\thost.log.debug(`Curtailment optional control ${control} skipped: ${(err as Error).message}`);\n\t}\n}\n\nasync function applyManualAndExportSwitches(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n): Promise<void> {\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"preset_usage_mode\", \"manual\", ctx);\n\tawait applyOptionalControl(host, device.deviceId, \"preset_allow_export\", true, ctx);\n\tawait applyOptionalControl(host, device.deviceId, \"allow_grid_export\", true, ctx);\n}\n\nasync function applyChargeLimit(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tchargeW: number,\n): Promise<void> {\n\tconst rounded = Math.max(0, Math.round(chargeW));\n\tconst last = lastAppliedChargeW.get(device.deviceId);\n\tif (last === rounded) {\n\t\treturn;\n\t}\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"ac_charge_limit\", rounded, ctx);\n\tlastAppliedChargeW.set(device.deviceId, rounded);\n}\n\nfunction deviceHasControl(host: CurtailmentRunnerHost, deviceId: string, control: string): boolean {\n\tconst writable = host.getDeviceWritable?.(deviceId);\n\tif (!writable?.length) {\n\t\treturn true;\n\t}\n\treturn writable.includes(control);\n}\n\nasync function applyCombinerExportLimit(\n\thost: CurtailmentRunnerHost,\n\tdeviceId: string,\n\texportW: number,\n\tctx: DeviceControlContext | undefined,\n): Promise<void> {\n\tconst lastAc = lastAppliedExportW.get(deviceId);\n\tif (lastAc !== exportW) {\n\t\t// max_load_parallel (MQTT) \u2014 AC output cap only; station feed-in cap stays user/app controlled.\n\t\tawait host.applyControl(deviceId, \"ac_output_limit\", exportW, ctx);\n\t\tlastAppliedExportW.set(deviceId, exportW);\n\t}\n\n\tconst homeW = COMBINER_MIN_HOME_LOAD_W;\n\tconst lastHome = lastAppliedHomeLoadW.get(deviceId);\n\tif (lastHome !== homeW) {\n\t\t// set_output_power = manual home load preset, not export.\n\t\tawait applyOptionalControl(host, deviceId, \"set_output_power\", homeW, ctx);\n\t\tlastAppliedHomeLoadW.set(deviceId, homeW);\n\t}\n}\n\nasync function applyExportLimit(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\texportTargetW: number,\n): Promise<void> {\n\tconst exportW = clampExportW(exportTargetW, device.role);\n\tif (exportW <= 0) {\n\t\treturn;\n\t}\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tconst id = device.deviceId;\n\n\tif (device.role === \"combiner\") {\n\t\tawait applyCombinerExportLimit(host, id, exportW, ctx);\n\t\treturn;\n\t}\n\n\tconst last = lastAppliedExportW.get(id);\n\tif (last === exportW) {\n\t\treturn;\n\t}\n\tif (deviceHasControl(host, id, \"ac_output_limit\")) {\n\t\tawait host.applyControl(id, \"ac_output_limit\", exportW, ctx);\n\t}\n\tif (deviceHasControl(host, id, \"set_output_power\")) {\n\t\tawait host.applyControl(id, \"set_output_power\", exportW, ctx);\n\t}\n\tlastAppliedExportW.set(id, exportW);\n}\n\nasync function applyAfterPhase(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tmodeAfter: \"smartmeter\" | \"smart\",\n): Promise<void> {\n\tlastAppliedExportW.delete(device.deviceId);\n\tlastAppliedHomeLoadW.delete(device.deviceId);\n\tlastAppliedChargeW.delete(device.deviceId);\n\tlastAppliedPhase.delete(device.deviceId);\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"preset_usage_mode\", modeAfter, ctx);\n}\n\nasync function applyCurtailmentSetpoints(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tphase: CurtailmentPhase,\n\texportW: number,\n\tchargeW: number,\n\tmodeAfter: \"smartmeter\" | \"smart\",\n\topts?: { modeOnly?: boolean },\n): Promise<void> {\n\tconst prevPhase = lastAppliedPhase.get(device.deviceId);\n\tconst phaseChanged = prevPhase !== phase;\n\n\tif (phase === \"after\" || phase === \"idle\") {\n\t\tawait applyAfterPhase(host, device, modeAfter);\n\t\treturn;\n\t}\n\n\tif (phaseChanged || !opts?.modeOnly) {\n\t\tawait applyManualAndExportSwitches(host, device);\n\t\tlastAppliedPhase.set(device.deviceId, phase);\n\t}\n\n\tif (opts?.modeOnly) {\n\t\treturn;\n\t}\n\n\tawait applyChargeLimit(host, device, chargeW);\n\tawait applyExportLimit(host, device, exportW);\n}\n\ninterface DeviceRunContext {\n\tlimit: number;\n\twindow: ReturnType<typeof detectCurtailmentWindow>;\n\tphase: CurtailmentPhase;\n\tlivePvW: number;\n\tmaxChargeW: number;\n\texportW: number;\n\tchargeW: number;\n\tremaining: number;\n\tsoc: number;\n}\n\nasync function buildDeviceContext(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tforecast: Awaited<ReturnType<typeof readHourlyForecast>>,\n\tnowHour: number,\n\tlivePvOverride?: number,\n): Promise<DeviceRunContext> {\n\tconst limit = acExportLimitW(device);\n\tconst window = detectCurtailmentWindow(forecast, limit);\n\tconst phase = currentPhase(window, nowHour);\n\tconst livePvW =\n\t\tlivePvOverride !== undefined && livePvOverride >= 0\n\t\t\t? Math.round(livePvOverride)\n\t\t\t: window.today\n\t\t\t\t? await readLivePvPowerW(host, device.deviceId)\n\t\t\t\t: 0;\n\tconst remaining = remainingCurtailmentHours(window, nowHour);\n\tconst soc = window.today && phase === \"active\" ? await readSocPercent(host, device.deviceId) : 0;\n\tconst maxChargeW =\n\t\twindow.today && phase === \"active\" ? calcMaxChargeW(device.batteryCapacityWh, soc, remaining) : 0;\n\tconst { exportW, chargeW } = resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHour, window);\n\treturn { limit, window, phase, livePvW, maxChargeW, exportW, chargeW, remaining, soc };\n}\n\nasync function publishDeviceStates(host: CurtailmentRunnerHost, ctx: DeviceRunContext): Promise<void> {\n\tawait host.setState(CURTAILMENT_STATE_IDS.today, ctx.window.today, true);\n\tawait host.setState(\n\t\tCURTAILMENT_STATE_IDS.start,\n\t\tctx.window.today ? `${ctx.window.startHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\ttrue,\n\t);\n\tawait host.setState(\n\t\tCURTAILMENT_STATE_IDS.end,\n\t\tctx.window.today ? `${ctx.window.endHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\ttrue,\n\t);\n\tawait host.setState(CURTAILMENT_STATE_IDS.maxChargeW, ctx.maxChargeW, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.exportW, ctx.exportW, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.remainingHours, ctx.remaining, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.phase, ctx.phase, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.acLimitW, ctx.limit, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.livePvW, ctx.livePvW, true);\n}\n\nasync function runDeviceCurtailment(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tconfig: CurtailmentRunnerConfig,\n\tforecast: Awaited<ReturnType<typeof readHourlyForecast>>,\n\tnowHour: number,\n\topts?: { livePvOverride?: number; setpointsOnly?: boolean },\n): Promise<void> {\n\tconst ctx = await buildDeviceContext(host, device, forecast, nowHour, opts?.livePvOverride);\n\tawait publishDeviceStates(host, ctx);\n\n\tif (!ctx.window.today) {\n\t\treturn;\n\t}\n\n\tif (ctx.phase !== \"before\" && ctx.phase !== \"active\") {\n\t\tif (!opts?.setpointsOnly) {\n\t\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (!opts?.setpointsOnly) {\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=${ctx.phase}, limit=${ctx.limit}W${unitsHint}, ` +\n\t\t\t\t`window ${ctx.window.startHour}-${ctx.window.endHour}h, livePv=${ctx.livePvW}W, ` +\n\t\t\t\t`charge=${ctx.chargeW}W, export=${ctx.exportW}W, SOC=${ctx.soc}%`,\n\t\t);\n\t}\n\n\ttry {\n\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter, {\n\t\t\tmodeOnly: opts?.setpointsOnly && lastAppliedPhase.get(device.deviceId) === ctx.phase,\n\t\t});\n\t} catch (err) {\n\t\thost.log.warn(`Curtailment control failed for ${device.deviceId}: ${(err as Error).message}`);\n\t}\n}\n\n/** Immediate follow-up when live PV changes (during sync / MQTT). */\nexport async function runCurtailmentOnPvChange(\n\thost: CurtailmentRunnerHost,\n\tconfig: CurtailmentRunnerConfig,\n\tdeviceId: string,\n\tlivePvW: number,\n): Promise<void> {\n\tif (!config.enabled || livePvW < 0) {\n\t\treturn;\n\t}\n\tconst devices = resolveCurtailmentDevices(config).filter(d => d.enabled && d.deviceId === deviceId);\n\tif (!devices.length) {\n\t\treturn;\n\t}\n\tconst basePath = (config.forecastBasePath || \"solarprognose.0.forecast.00.hourly\").trim();\n\tconst forecast = await readHourlyForecast(\n\t\tbasePath,\n\t\tid => host.getForeignStateAsync(id),\n\t\thost.getForeignObjectAsync ? id => host.getForeignObjectAsync!(id) : undefined,\n\t);\n\tconst nowHour = berlinHour();\n\tfor (const device of devices) {\n\t\tconst ctx = await buildDeviceContext(host, device, forecast, nowHour, livePvW);\n\t\tif (!ctx.window.today || (ctx.phase !== \"before\" && ctx.phase !== \"active\")) {\n\t\t\tcontinue;\n\t\t}\n\t\tawait publishDeviceStates(host, ctx);\n\t\ttry {\n\t\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter, {\n\t\t\t\tmodeOnly: lastAppliedPhase.get(device.deviceId) === ctx.phase,\n\t\t\t});\n\t\t\thost.log.debug(\n\t\t\t\t`Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W, ` +\n\t\t\t\t\t`charge=${ctx.chargeW}W, export=${ctx.exportW}W`,\n\t\t\t);\n\t\t} catch (err) {\n\t\t\thost.log.warn(`Curtailment PV follow failed for ${device.deviceId}: ${(err as Error).message}`);\n\t\t}\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 = resolveCurtailmentDevices(config).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(\n\t\tbasePath,\n\t\tid => host.getForeignStateAsync(id),\n\t\thost.getForeignObjectAsync ? id => host.getForeignObjectAsync!(id) : undefined,\n\t);\n\tconst nowHour = berlinHour();\n\n\tfor (const device of devices) {\n\t\tawait runDeviceCurtailment(host, device, config, forecast, nowHour);\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA
|
|
4
|
+
"sourcesContent": ["import { acExportLimitW } from \"./curtailmentProfiles\";\nimport { resolveCurtailmentDevices, type CurtailmentStructuredNative } from \"./curtailmentConfig\";\nimport {\n\tcurrentPhase,\n\tdetectCurtailmentWindow,\n\treadHourlyForecast,\n\tremainingCurtailmentHours,\n} from \"./curtailmentForecast\";\nimport {\n\tcalcMaxChargeW,\n\tcalcMissingChargeWh,\n\treadLivePvPowerW,\n\tresolveCurtailmentSetpoints,\n\ttype CurtailmentPowerHost,\n} from \"./curtailmentPower\";\nimport { CURTAILMENT_STATE_IDS } from \"./curtailmentStates\";\nimport type { CurtailmentDeviceConfig, CurtailmentDeviceRole, CurtailmentPhase } from \"./curtailmentTypes\";\nimport type { DeviceControlContext } from \"./types\";\n\nexport interface CurtailmentRunnerConfig extends CurtailmentStructuredNative {\n\tenabled: boolean;\n\tforecastBasePath: string;\n\tmodeAfter: \"smartmeter\" | \"smart\";\n}\n\nexport interface CurtailmentRunnerHost extends CurtailmentPowerHost {\n\t/** Writable control ids from last poll (e.g. ac_output_limit on combiner). */\n\tgetDeviceWritable?: (deviceId: string) => string[] | undefined;\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\tgetForeignObjectAsync?: (id: string) => Promise<ioBroker.Object | 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\nconst lastAppliedExportW = new Map<string, number>();\nconst lastAppliedPhase = new Map<string, CurtailmentPhase>();\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\nfunction clampAcOutputW(powerW: number, role: CurtailmentDeviceRole): number {\n\tif (powerW <= 0) {\n\t\treturn 0;\n\t}\n\tconst hardwareMax = role === \"combiner\" ? 4800 : 100_000;\n\treturn Math.min(hardwareMax, Math.max(0, Math.round(powerW)));\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\nasync function applyManualMode(host: CurtailmentRunnerHost, device: CurtailmentDeviceConfig): Promise<void> {\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"preset_usage_mode\", \"manual\", ctx);\n}\n\nasync function applyAcOutputLimit(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\ttargetW: number,\n): Promise<void> {\n\tconst acOutputW = clampAcOutputW(targetW, device.role);\n\tconst last = lastAppliedExportW.get(device.deviceId);\n\tif (last === acOutputW) {\n\t\treturn;\n\t}\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"ac_output_limit\", acOutputW, ctx);\n\tlastAppliedExportW.set(device.deviceId, acOutputW);\n}\n\nasync function applyAfterPhase(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tmodeAfter: \"smartmeter\" | \"smart\",\n): Promise<void> {\n\tlastAppliedExportW.delete(device.deviceId);\n\tlastAppliedPhase.delete(device.deviceId);\n\tconst ctx = host.getDeviceContext(device.deviceId);\n\tawait host.applyControl(device.deviceId, \"preset_usage_mode\", modeAfter, ctx);\n}\n\nasync function applyCurtailmentSetpoints(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tphase: CurtailmentPhase,\n\texportW: number,\n\tmodeAfter: \"smartmeter\" | \"smart\",\n\topts?: { modeOnly?: boolean },\n): Promise<void> {\n\tconst prevPhase = lastAppliedPhase.get(device.deviceId);\n\tconst phaseChanged = prevPhase !== phase;\n\n\tif (phase === \"after\" || phase === \"idle\") {\n\t\tawait applyAfterPhase(host, device, modeAfter);\n\t\treturn;\n\t}\n\n\tif (phaseChanged || !opts?.modeOnly) {\n\t\tawait applyManualMode(host, device);\n\t\tlastAppliedPhase.set(device.deviceId, phase);\n\t}\n\n\tif (opts?.modeOnly) {\n\t\treturn;\n\t}\n\n\tawait applyAcOutputLimit(host, device, exportW);\n}\n\ninterface DeviceRunContext {\n\tlimit: number;\n\twindow: ReturnType<typeof detectCurtailmentWindow>;\n\tphase: CurtailmentPhase;\n\tlivePvW: number;\n\tmissingChargeWh: number;\n\tmaxChargeW: number;\n\texportW: number;\n\tchargeW: number;\n\tremaining: number;\n\tsoc: number;\n}\n\nasync function buildDeviceContext(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tforecast: Awaited<ReturnType<typeof readHourlyForecast>>,\n\tnowHour: number,\n\tlivePvOverride?: number,\n): Promise<DeviceRunContext> {\n\tconst limit = acExportLimitW(device);\n\tconst window = detectCurtailmentWindow(forecast, limit);\n\tconst phase = currentPhase(window, nowHour);\n\tconst livePvW =\n\t\tlivePvOverride !== undefined && livePvOverride >= 0\n\t\t\t? Math.round(livePvOverride)\n\t\t\t: window.today\n\t\t\t\t? await readLivePvPowerW(host, device.deviceId)\n\t\t\t\t: 0;\n\tconst remaining = remainingCurtailmentHours(window, nowHour);\n\tconst soc = window.today && phase === \"active\" ? await readSocPercent(host, device.deviceId) : 0;\n\tconst missingChargeWh = window.today && phase === \"active\" ? calcMissingChargeWh(device.batteryCapacityWh, soc) : 0;\n\tconst maxChargeW = window.today && phase === \"active\" ? calcMaxChargeW(missingChargeWh, remaining) : 0;\n\tconst { exportW, chargeW } = resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHour, window);\n\treturn { limit, window, phase, livePvW, missingChargeWh, maxChargeW, exportW, chargeW, remaining, soc };\n}\n\nasync function publishDeviceStates(host: CurtailmentRunnerHost, ctx: DeviceRunContext): Promise<void> {\n\tawait host.setState(CURTAILMENT_STATE_IDS.today, ctx.window.today, true);\n\tawait host.setState(\n\t\tCURTAILMENT_STATE_IDS.start,\n\t\tctx.window.today ? `${ctx.window.startHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\ttrue,\n\t);\n\tawait host.setState(\n\t\tCURTAILMENT_STATE_IDS.end,\n\t\tctx.window.today ? `${ctx.window.endHour.toString().padStart(2, \"0\")}:00` : \"\",\n\t\ttrue,\n\t);\n\tawait host.setState(CURTAILMENT_STATE_IDS.missingChargeWh, ctx.missingChargeWh, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.maxChargeW, ctx.maxChargeW, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.exportW, ctx.exportW, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.remainingHours, ctx.remaining, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.phase, ctx.phase, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.acLimitW, ctx.limit, true);\n\tawait host.setState(CURTAILMENT_STATE_IDS.livePvW, ctx.livePvW, true);\n}\n\nasync function runDeviceCurtailment(\n\thost: CurtailmentRunnerHost,\n\tdevice: CurtailmentDeviceConfig,\n\tconfig: CurtailmentRunnerConfig,\n\tforecast: Awaited<ReturnType<typeof readHourlyForecast>>,\n\tnowHour: number,\n\topts?: { livePvOverride?: number; setpointsOnly?: boolean },\n): Promise<void> {\n\tconst ctx = await buildDeviceContext(host, device, forecast, nowHour, opts?.livePvOverride);\n\tawait publishDeviceStates(host, ctx);\n\n\tif (!ctx.window.today) {\n\t\treturn;\n\t}\n\n\tif (ctx.phase !== \"before\" && ctx.phase !== \"active\") {\n\t\tif (!opts?.setpointsOnly) {\n\t\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter);\n\t\t}\n\t\treturn;\n\t}\n\n\tif (!opts?.setpointsOnly) {\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=${ctx.phase}, limit=${ctx.limit}W${unitsHint}, ` +\n\t\t\t\t`window ${ctx.window.startHour}-${ctx.window.endHour}h, livePv=${ctx.livePvW}W, ` +\n\t\t\t\t`missingWh=${ctx.missingChargeWh}, maxCharge=${ctx.maxChargeW}W, export=${ctx.exportW}W, SOC=${ctx.soc}%`,\n\t\t);\n\t}\n\n\ttry {\n\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {\n\t\t\tmodeOnly: opts?.setpointsOnly && lastAppliedPhase.get(device.deviceId) === ctx.phase,\n\t\t});\n\t} catch (err) {\n\t\thost.log.warn(`Curtailment control failed for ${device.deviceId}: ${(err as Error).message}`);\n\t}\n}\n\n/** Immediate follow-up when live PV changes (during sync / MQTT). */\nexport async function runCurtailmentOnPvChange(\n\thost: CurtailmentRunnerHost,\n\tconfig: CurtailmentRunnerConfig,\n\tdeviceId: string,\n\tlivePvW: number,\n): Promise<void> {\n\tif (!config.enabled || livePvW < 0) {\n\t\treturn;\n\t}\n\tconst devices = resolveCurtailmentDevices(config).filter(d => d.enabled && d.deviceId === deviceId);\n\tif (!devices.length) {\n\t\treturn;\n\t}\n\tconst basePath = (config.forecastBasePath || \"solarprognose.0.forecast.00.hourly\").trim();\n\tconst forecast = await readHourlyForecast(\n\t\tbasePath,\n\t\tid => host.getForeignStateAsync(id),\n\t\thost.getForeignObjectAsync ? id => host.getForeignObjectAsync!(id) : undefined,\n\t);\n\tconst nowHour = berlinHour();\n\tfor (const device of devices) {\n\t\tconst ctx = await buildDeviceContext(host, device, forecast, nowHour, livePvW);\n\t\tif (!ctx.window.today || (ctx.phase !== \"before\" && ctx.phase !== \"active\")) {\n\t\t\tcontinue;\n\t\t}\n\t\tawait publishDeviceStates(host, ctx);\n\t\ttry {\n\t\t\tawait applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {\n\t\t\t\tmodeOnly: lastAppliedPhase.get(device.deviceId) === ctx.phase,\n\t\t\t});\n\t\t\thost.log.debug(\n\t\t\t\t`Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W, ` +\n\t\t\t\t\t`missingWh=${ctx.missingChargeWh}, maxCharge=${ctx.maxChargeW}W, export=${ctx.exportW}W`,\n\t\t\t);\n\t\t} catch (err) {\n\t\t\thost.log.warn(`Curtailment PV follow failed for ${device.deviceId}: ${(err as Error).message}`);\n\t\t}\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 = resolveCurtailmentDevices(config).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(\n\t\tbasePath,\n\t\tid => host.getForeignStateAsync(id),\n\t\thost.getForeignObjectAsync ? id => host.getForeignObjectAsync!(id) : undefined,\n\t);\n\tconst nowHour = berlinHour();\n\n\tfor (const device of devices) {\n\t\tawait runDeviceCurtailment(host, device, config, forecast, nowHour);\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,6 +28,7 @@ const CURTAILMENT_STATE_IDS = {
|
|
|
28
28
|
today: `${CURTAILMENT_CHANNEL}.today`,
|
|
29
29
|
start: `${CURTAILMENT_CHANNEL}.curtailment_start`,
|
|
30
30
|
end: `${CURTAILMENT_CHANNEL}.curtailment_end`,
|
|
31
|
+
missingChargeWh: `${CURTAILMENT_CHANNEL}.missing_charge_wh`,
|
|
31
32
|
maxChargeW: `${CURTAILMENT_CHANNEL}.max_charge_w`,
|
|
32
33
|
exportW: `${CURTAILMENT_CHANNEL}.export_w`,
|
|
33
34
|
livePvW: `${CURTAILMENT_CHANNEL}.live_pv_w`,
|
|
@@ -75,10 +76,22 @@ async function setupCurtailmentStates(adapter) {
|
|
|
75
76
|
def: ""
|
|
76
77
|
}
|
|
77
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: CURTAILMENT_STATE_IDS.missingChargeWh,
|
|
81
|
+
common: {
|
|
82
|
+
name: "Missing charge energy to full SOC (active phase, Wh)",
|
|
83
|
+
type: "number",
|
|
84
|
+
role: "value.energy",
|
|
85
|
+
unit: "Wh",
|
|
86
|
+
read: true,
|
|
87
|
+
write: false,
|
|
88
|
+
def: 0
|
|
89
|
+
}
|
|
90
|
+
},
|
|
78
91
|
{
|
|
79
92
|
id: CURTAILMENT_STATE_IDS.maxChargeW,
|
|
80
93
|
common: {
|
|
81
|
-
name: "Max AC charge power (
|
|
94
|
+
name: "Max AC charge power (missing Wh \xF7 remaining hours, W)",
|
|
82
95
|
type: "number",
|
|
83
96
|
role: "value.power",
|
|
84
97
|
unit: "W",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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\texportW: `${CURTAILMENT_CHANNEL}.export_w`,\n\tlivePvW: `${CURTAILMENT_CHANNEL}.live_pv_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 AC charge power (
|
|
5
|
-
"mappings": "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",
|
|
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\tmissingChargeWh: `${CURTAILMENT_CHANNEL}.missing_charge_wh`,\n\tmaxChargeW: `${CURTAILMENT_CHANNEL}.max_charge_w`,\n\texportW: `${CURTAILMENT_CHANNEL}.export_w`,\n\tlivePvW: `${CURTAILMENT_CHANNEL}.live_pv_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.missingChargeWh,\n\t\t\tcommon: {\n\t\t\t\tname: \"Missing charge energy to full SOC (active phase, Wh)\",\n\t\t\t\ttype: \"number\",\n\t\t\t\trole: \"value.energy\",\n\t\t\t\tunit: \"Wh\",\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.maxChargeW,\n\t\t\tcommon: {\n\t\t\t\tname: \"Max AC charge power (missing Wh \u00F7 remaining hours, W)\",\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.exportW,\n\t\t\tcommon: {\n\t\t\t\tname: \"AC/grid export target (W)\",\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.livePvW,\n\t\t\tcommon: {\n\t\t\t\tname: \"Live PV generation (W)\",\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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "anker-solix",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.14",
|
|
5
5
|
"messagebox": true,
|
|
6
6
|
"news": {
|
|
7
|
-
"0.10.
|
|
8
|
-
"en": "Curtailment:
|
|
9
|
-
"de": "Abregelung:
|
|
10
|
-
"ru": "Abregelung:
|
|
11
|
-
"pt": "Abregelung:
|
|
12
|
-
"nl": "Abregelung:
|
|
13
|
-
"fr": "Abregelung:
|
|
14
|
-
"it": "Abregelung:
|
|
15
|
-
"es": "Abregelung:
|
|
16
|
-
"pl": "Abregelung:
|
|
17
|
-
"uk": "Abregelung:
|
|
18
|
-
"zh-cn": "
|
|
7
|
+
"0.10.14": {
|
|
8
|
+
"en": "Curtailment: manual + ac_output_limit only; missing_charge_wh; active export = PV − charge",
|
|
9
|
+
"de": "Abregelung: nur Benutzerdefiniert + ac_output_limit; missing_charge_wh; active: Einspeisung = PV − Laden",
|
|
10
|
+
"ru": "Abregelung: manual + ac_output_limit",
|
|
11
|
+
"pt": "Abregelung: manual + ac_output_limit",
|
|
12
|
+
"nl": "Abregelung: manual + ac_output_limit",
|
|
13
|
+
"fr": "Abregelung: manual + ac_output_limit",
|
|
14
|
+
"it": "Abregelung: manual + ac_output_limit",
|
|
15
|
+
"es": "Abregelung: manual + ac_output_limit",
|
|
16
|
+
"pl": "Abregelung: manual + ac_output_limit",
|
|
17
|
+
"uk": "Abregelung: manual + ac_output_limit",
|
|
18
|
+
"zh-cn": "限电仅 manual + ac_output_limit"
|
|
19
19
|
},
|
|
20
20
|
"0.10.12": {
|
|
21
21
|
"en": "Curtailment combiner: ac_output_limit (max_load) for export; home load preset 0 W",
|