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 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. **Before** the window: **manual**, **charge 0 W**, **export limit = live PV** (`total_pv_power`, updated **immediately** on every change when states are written). **Combiner:** export via **`ac_output_limit`** (max_load / MQTT parallel), **`set_output_power`** = minimal home load (0 W). **Does not** change the app station setting **grid export cap** (`grid_export_limit` / feed-in limit) keep “unlimited” in the app if you use that. **Active** window: **manual**, **AC output limit** = full live PV (combiner max **4800 W**), **AC charge limit** = calculated slow charge (`missing_Wh / remaining_hours`). **After** the window: restore selected mode. Status: `curtailment.live_pv_w`, `export_w`, `max_charge_w`; check `sensors.ac_output_power`.
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
 
@@ -74,7 +74,7 @@
74
74
  "cloud_state": "Cloud-Status",
75
75
  "wifi_state": "WLAN-Status",
76
76
  "CurtailmentAvoidance": "Abregelungsvermeidung",
77
- "curtailment_hint": "Prognose (solarprognose) erkennt Überproduktions-Tage. Combiner: ac_output_limit (max_load), Hauslast-Preset 0 W — ändert NICHT das App-Feld „Einspeise-Limit/Netzeinspeisung“ (grid_export_limit). Vor dem Fenster: Laden 0 W. Active: volle PV am AC-Ausgang + berechnetes Laden. Status: live_pv_w, export_w, max_charge_w.",
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",
@@ -74,7 +74,7 @@
74
74
  "cloud_state": "Cloud state",
75
75
  "wifi_state": "WiFi state",
76
76
  "CurtailmentAvoidance": "Curtailment avoidance",
77
- "curtailment_hint": "Forecast (solarprognose) detects overproduction days. Combiner: ac_output_limit (max_load), home load preset 0 W — does NOT change the app grid export cap (grid_export_limit). Before: charge 0 W. Active: AC output = live PV (max 4800 W), slow charge via ac_charge_limit. Status: live_pv_w, export_w, max_charge_w. Requires MQTT.",
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 resolveActiveExportW(livePvW, _maxChargeW) {
146
- if (livePvW <= 0) {
146
+ function calcMissingChargeWh(batteryCapacityWh, socPercent) {
147
+ if (batteryCapacityWh <= 0) {
147
148
  return 0;
148
149
  }
149
- return Math.round(livePvW);
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(batteryCapacityWh, socPercent, hoursRemaining) {
153
+ function calcMaxChargeW(missingWh, hoursRemaining) {
152
154
  const hours = Math.max(1, hoursRemaining);
153
- if (batteryCapacityWh <= 0) {
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/**\n * Active window: AC output limit = full PV (charging capped separately via ac_charge_limit).\n * Slow battery fill uses maxChargeW only; do not subtract it from the export limit.\n */\nexport function resolveActiveExportW(livePvW: number, _maxChargeW: number): number {\n\tif (livePvW <= 0) {\n\t\treturn 0;\n\t}\n\treturn Math.round(livePvW);\n}\n\nexport function calcMaxChargeW(batteryCapacityWh: number, socPercent: number, hoursRemaining: number): number {\n\tconst hours = Math.max(1, hoursRemaining);\n\tif (batteryCapacityWh <= 0) {\n\t\treturn 0;\n\t}\n\tconst missingWh = ((100 - socPercent) / 100) * batteryCapacityWh;\n\treturn Math.max(0, Math.round(missingWh / hours));\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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",
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 clampExportW(powerW, role) {
42
+ function clampAcOutputW(powerW, role) {
47
43
  if (powerW <= 0) {
48
44
  return 0;
49
45
  }
50
- const hardwareMax = role === "combiner" ? import_curtailmentPower.COMBINER_MAX_AC_OUTPUT_W : 1e5;
51
- return Math.min(hardwareMax, Math.max(100, Math.round(powerW)));
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 applyOptionalControl(host, deviceId, control, value, ctx) {
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 applyCombinerExportLimit(host, deviceId, exportW, ctx) {
102
- const lastAc = lastAppliedExportW.get(deviceId);
103
- if (lastAc !== exportW) {
104
- await host.applyControl(deviceId, "ac_output_limit", exportW, ctx);
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
- const id = device.deviceId;
121
- if (device.role === "combiner") {
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, chargeW, modeAfter, opts) {
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 applyManualAndExportSwitches(host, device);
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 applyChargeLimit(host, device, chargeW);
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 maxChargeW = window.today && phase === "active" ? (0, import_curtailmentPower.calcMaxChargeW)(device.batteryCapacityWh, soc, remaining) : 0;
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, ctx.chargeW, config.modeAfter);
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, charge=${ctx.chargeW}W, export=${ctx.exportW}W, SOC=${ctx.soc}%`
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, ctx.chargeW, config.modeAfter, {
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, ctx.chargeW, config.modeAfter, {
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, charge=${ctx.chargeW}W, export=${ctx.exportW}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;AAAA,iCAA+B;AAC/B,+BAA4E;AAC5E,iCAKO;AACP,8BAMO;AACP,+BAAsC;AA+B/B,MAAM,2BAA2B;AAExC,MAAM,qBAAqB,oBAAI,IAAoB;AACnD,MAAM,uBAAuB,oBAAI,IAAoB;AACrD,MAAM,qBAAqB,oBAAI,IAAoB;AACnD,MAAM,mBAAmB,oBAAI,IAA8B;AAE3D,SAAS,aAAqB;AArD9B;AAsDC,QAAM,QAAQ,IAAI,KAAK,eAAe,SAAS;AAAA,IAC9C,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,EACT,CAAC,EAAE,cAAc,oBAAI,KAAK,CAAC;AAC3B,QAAM,KAAI,WAAM,KAAK,OAAK,EAAE,SAAS,MAAM,MAAjC,mBAAoC;AAC9C,SAAO,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;AAChD;AAEA,SAAS,aAAa,QAAgB,MAAqC;AAC1E,MAAI,UAAU,GAAG;AAChB,WAAO;AAAA,EACR;AACA,QAAM,cAAc,SAAS,aAAa,mDAA2B;AACrE,SAAO,KAAK,IAAI,aAAa,KAAK,IAAI,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC;AAC/D;AAEA,eAAe,eAAe,MAA6B,UAAmC;AAC7F,QAAM,aAAa;AAAA,IAClB,GAAG,KAAK,SAAS,cAAc,QAAQ;AAAA,IACvC,GAAG,KAAK,SAAS,iBAAiB,QAAQ;AAAA,IAC1C,GAAG,KAAK,SAAS,iBAAiB,QAAQ;AAAA,EAC3C;AACA,aAAW,MAAM,YAAY;AAC5B,UAAM,KAAK,MAAM,KAAK,cAAc,EAAE;AACtC,SAAI,yBAAI,SAAQ,SAAQ,yBAAI,SAAQ,QAAW;AAC9C,YAAM,IAAI,OAAO,GAAG,GAAG;AACvB,UAAI,CAAC,OAAO,MAAM,CAAC,GAAG;AACrB,eAAO,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,CAAC,CAAC;AAAA,MACpC;AAAA,IACD;AAAA,EACD;AACA,SAAO;AACR;AAEA,eAAe,qBACd,MACA,UACA,SACA,OACA,KACgB;AAChB,MAAI;AACH,UAAM,KAAK,aAAa,UAAU,SAAS,OAAO,GAAG;AAAA,EACtD,SAAS,KAAK;AACb,SAAK,IAAI,MAAM,gCAAgC,OAAO,aAAc,IAAc,OAAO,EAAE;AAAA,EAC5F;AACD;AAEA,eAAe,6BACd,MACA,QACgB;AAChB,QAAM,MAAM,KAAK,iBAAiB,OAAO,QAAQ;AACjD,QAAM,KAAK,aAAa,OAAO,UAAU,qBAAqB,UAAU,GAAG;AAC3E,QAAM,qBAAqB,MAAM,OAAO,UAAU,uBAAuB,MAAM,GAAG;AAClF,QAAM,qBAAqB,MAAM,OAAO,UAAU,qBAAqB,MAAM,GAAG;AACjF;AAEA,eAAe,iBACd,MACA,QACA,SACgB;AAChB,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC;AAC/C,QAAM,OAAO,mBAAmB,IAAI,OAAO,QAAQ;AACnD,MAAI,SAAS,SAAS;AACrB;AAAA,EACD;AACA,QAAM,MAAM,KAAK,iBAAiB,OAAO,QAAQ;AACjD,QAAM,KAAK,aAAa,OAAO,UAAU,mBAAmB,SAAS,GAAG;AACxE,qBAAmB,IAAI,OAAO,UAAU,OAAO;AAChD;AAEA,SAAS,iBAAiB,MAA6B,UAAkB,SAA0B;AAhInG;AAiIC,QAAM,YAAW,UAAK,sBAAL,8BAAyB;AAC1C,MAAI,EAAC,qCAAU,SAAQ;AACtB,WAAO;AAAA,EACR;AACA,SAAO,SAAS,SAAS,OAAO;AACjC;AAEA,eAAe,yBACd,MACA,UACA,SACA,KACgB;AAChB,QAAM,SAAS,mBAAmB,IAAI,QAAQ;AAC9C,MAAI,WAAW,SAAS;AAEvB,UAAM,KAAK,aAAa,UAAU,mBAAmB,SAAS,GAAG;AACjE,uBAAmB,IAAI,UAAU,OAAO;AAAA,EACzC;AAEA,QAAM,QAAQ;AACd,QAAM,WAAW,qBAAqB,IAAI,QAAQ;AAClD,MAAI,aAAa,OAAO;AAEvB,UAAM,qBAAqB,MAAM,UAAU,oBAAoB,OAAO,GAAG;AACzE,yBAAqB,IAAI,UAAU,KAAK;AAAA,EACzC;AACD;AAEA,eAAe,iBACd,MACA,QACA,eACgB;AAChB,QAAM,UAAU,aAAa,eAAe,OAAO,IAAI;AACvD,MAAI,WAAW,GAAG;AACjB;AAAA,EACD;AACA,QAAM,MAAM,KAAK,iBAAiB,OAAO,QAAQ;AACjD,QAAM,KAAK,OAAO;AAElB,MAAI,OAAO,SAAS,YAAY;AAC/B,UAAM,yBAAyB,MAAM,IAAI,SAAS,GAAG;AACrD;AAAA,EACD;AAEA,QAAM,OAAO,mBAAmB,IAAI,EAAE;AACtC,MAAI,SAAS,SAAS;AACrB;AAAA,EACD;AACA,MAAI,iBAAiB,MAAM,IAAI,iBAAiB,GAAG;AAClD,UAAM,KAAK,aAAa,IAAI,mBAAmB,SAAS,GAAG;AAAA,EAC5D;AACA,MAAI,iBAAiB,MAAM,IAAI,kBAAkB,GAAG;AACnD,UAAM,KAAK,aAAa,IAAI,oBAAoB,SAAS,GAAG;AAAA,EAC7D;AACA,qBAAmB,IAAI,IAAI,OAAO;AACnC;AAEA,eAAe,gBACd,MACA,QACA,WACgB;AAChB,qBAAmB,OAAO,OAAO,QAAQ;AACzC,uBAAqB,OAAO,OAAO,QAAQ;AAC3C,qBAAmB,OAAO,OAAO,QAAQ;AACzC,mBAAiB,OAAO,OAAO,QAAQ;AACvC,QAAM,MAAM,KAAK,iBAAiB,OAAO,QAAQ;AACjD,QAAM,KAAK,aAAa,OAAO,UAAU,qBAAqB,WAAW,GAAG;AAC7E;AAEA,eAAe,0BACd,MACA,QACA,OACA,SACA,SACA,WACA,MACgB;AAChB,QAAM,YAAY,iBAAiB,IAAI,OAAO,QAAQ;AACtD,QAAM,eAAe,cAAc;AAEnC,MAAI,UAAU,WAAW,UAAU,QAAQ;AAC1C,UAAM,gBAAgB,MAAM,QAAQ,SAAS;AAC7C;AAAA,EACD;AAEA,MAAI,gBAAgB,EAAC,6BAAM,WAAU;AACpC,UAAM,6BAA6B,MAAM,MAAM;AAC/C,qBAAiB,IAAI,OAAO,UAAU,KAAK;AAAA,EAC5C;AAEA,MAAI,6BAAM,UAAU;AACnB;AAAA,EACD;AAEA,QAAM,iBAAiB,MAAM,QAAQ,OAAO;AAC5C,QAAM,iBAAiB,MAAM,QAAQ,OAAO;AAC7C;AAcA,eAAe,mBACd,MACA,QACA,UACA,SACA,gBAC4B;AAC5B,QAAM,YAAQ,2CAAe,MAAM;AACnC,QAAM,aAAS,oDAAwB,UAAU,KAAK;AACtD,QAAM,YAAQ,yCAAa,QAAQ,OAAO;AAC1C,QAAM,UACL,mBAAmB,UAAa,kBAAkB,IAC/C,KAAK,MAAM,cAAc,IACzB,OAAO,QACN,UAAM,0CAAiB,MAAM,OAAO,QAAQ,IAC5C;AACL,QAAM,gBAAY,sDAA0B,QAAQ,OAAO;AAC3D,QAAM,MAAM,OAAO,SAAS,UAAU,WAAW,MAAM,eAAe,MAAM,OAAO,QAAQ,IAAI;AAC/F,QAAM,aACL,OAAO,SAAS,UAAU,eAAW,wCAAe,OAAO,mBAAmB,KAAK,SAAS,IAAI;AACjG,QAAM,EAAE,SAAS,QAAQ,QAAI,qDAA4B,OAAO,SAAS,YAAY,UAAU,SAAS,MAAM;AAC9G,SAAO,EAAE,OAAO,QAAQ,OAAO,SAAS,YAAY,SAAS,SAAS,WAAW,IAAI;AACtF;AAEA,eAAe,oBAAoB,MAA6B,KAAsC;AACrG,QAAM,KAAK,SAAS,+CAAsB,OAAO,IAAI,OAAO,OAAO,IAAI;AACvE,QAAM,KAAK;AAAA,IACV,+CAAsB;AAAA,IACtB,IAAI,OAAO,QAAQ,GAAG,IAAI,OAAO,UAAU,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,QAAQ;AAAA,IAC9E;AAAA,EACD;AACA,QAAM,KAAK;AAAA,IACV,+CAAsB;AAAA,IACtB,IAAI,OAAO,QAAQ,GAAG,IAAI,OAAO,QAAQ,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,QAAQ;AAAA,IAC5E;AAAA,EACD;AACA,QAAM,KAAK,SAAS,+CAAsB,YAAY,IAAI,YAAY,IAAI;AAC1E,QAAM,KAAK,SAAS,+CAAsB,SAAS,IAAI,SAAS,IAAI;AACpE,QAAM,KAAK,SAAS,+CAAsB,gBAAgB,IAAI,WAAW,IAAI;AAC7E,QAAM,KAAK,SAAS,+CAAsB,OAAO,IAAI,OAAO,IAAI;AAChE,QAAM,KAAK,SAAS,+CAAsB,UAAU,IAAI,OAAO,IAAI;AACnE,QAAM,KAAK,SAAS,+CAAsB,SAAS,IAAI,SAAS,IAAI;AACrE;AAEA,eAAe,qBACd,MACA,QACA,QACA,UACA,SACA,MACgB;AAtSjB;AAuSC,QAAM,MAAM,MAAM,mBAAmB,MAAM,QAAQ,UAAU,SAAS,6BAAM,cAAc;AAC1F,QAAM,oBAAoB,MAAM,GAAG;AAEnC,MAAI,CAAC,IAAI,OAAO,OAAO;AACtB;AAAA,EACD;AAEA,MAAI,IAAI,UAAU,YAAY,IAAI,UAAU,UAAU;AACrD,QAAI,EAAC,6BAAM,gBAAe;AACzB,YAAM,0BAA0B,MAAM,QAAQ,IAAI,OAAO,IAAI,SAAS,IAAI,SAAS,OAAO,SAAS;AAAA,IACpG;AACA;AAAA,EACD;AAEA,MAAI,EAAC,6BAAM,gBAAe;AACzB,UAAM,YACL,OAAO,SAAS,gBAAc,YAAO,UAAP,mBAAc,UACzC,WAAW,OAAO,MAAM,KAAK,GAAG,CAAC,KAAK,OAAO,MAAM,MAAM,YACzD;AACJ,SAAK,IAAI;AAAA,MACR,gBAAgB,OAAO,QAAQ,YAAY,IAAI,KAAK,WAAW,IAAI,KAAK,IAAI,SAAS,YAC1E,IAAI,OAAO,SAAS,IAAI,IAAI,OAAO,OAAO,aAAa,IAAI,OAAO,aAClE,IAAI,OAAO,aAAa,IAAI,OAAO,UAAU,IAAI,GAAG;AAAA,IAChE;AAAA,EACD;AAEA,MAAI;AACH,UAAM,0BAA0B,MAAM,QAAQ,IAAI,OAAO,IAAI,SAAS,IAAI,SAAS,OAAO,WAAW;AAAA,MACpG,WAAU,6BAAM,kBAAiB,iBAAiB,IAAI,OAAO,QAAQ,MAAM,IAAI;AAAA,IAChF,CAAC;AAAA,EACF,SAAS,KAAK;AACb,SAAK,IAAI,KAAK,kCAAkC,OAAO,QAAQ,KAAM,IAAc,OAAO,EAAE;AAAA,EAC7F;AACD;AAGA,eAAsB,yBACrB,MACA,QACA,UACA,SACgB;AAChB,MAAI,CAAC,OAAO,WAAW,UAAU,GAAG;AACnC;AAAA,EACD;AACA,QAAM,cAAU,oDAA0B,MAAM,EAAE,OAAO,OAAK,EAAE,WAAW,EAAE,aAAa,QAAQ;AAClG,MAAI,CAAC,QAAQ,QAAQ;AACpB;AAAA,EACD;AACA,QAAM,YAAY,OAAO,oBAAoB,sCAAsC,KAAK;AACxF,QAAM,WAAW,UAAM;AAAA,IACtB;AAAA,IACA,QAAM,KAAK,qBAAqB,EAAE;AAAA,IAClC,KAAK,wBAAwB,QAAM,KAAK,sBAAuB,EAAE,IAAI;AAAA,EACtE;AACA,QAAM,UAAU,WAAW;AAC3B,aAAW,UAAU,SAAS;AAC7B,UAAM,MAAM,MAAM,mBAAmB,MAAM,QAAQ,UAAU,SAAS,OAAO;AAC7E,QAAI,CAAC,IAAI,OAAO,SAAU,IAAI,UAAU,YAAY,IAAI,UAAU,UAAW;AAC5E;AAAA,IACD;AACA,UAAM,oBAAoB,MAAM,GAAG;AACnC,QAAI;AACH,YAAM,0BAA0B,MAAM,QAAQ,IAAI,OAAO,IAAI,SAAS,IAAI,SAAS,OAAO,WAAW;AAAA,QACpG,UAAU,iBAAiB,IAAI,OAAO,QAAQ,MAAM,IAAI;AAAA,MACzD,CAAC;AACD,WAAK,IAAI;AAAA,QACR,0BAA0B,OAAO,QAAQ,YAAY,IAAI,KAAK,YAAY,IAAI,OAAO,aAC1E,IAAI,OAAO,aAAa,IAAI,OAAO;AAAA,MAC/C;AAAA,IACD,SAAS,KAAK;AACb,WAAK,IAAI,KAAK,oCAAoC,OAAO,QAAQ,KAAM,IAAc,OAAO,EAAE;AAAA,IAC/F;AAAA,EACD;AACD;AAEA,eAAsB,wBACrB,MACA,QACgB;AAChB,MAAI,CAAC,OAAO,SAAS;AACpB,UAAM,KAAK,SAAS,+CAAsB,OAAO,YAAY,IAAI;AACjE;AAAA,EACD;AAEA,QAAM,cAAU,oDAA0B,MAAM,EAAE,OAAO,OAAK,EAAE,OAAO;AACvE,MAAI,CAAC,QAAQ,QAAQ;AACpB,SAAK,IAAI,MAAM,sDAAsD;AACrE,UAAM,KAAK,SAAS,+CAAsB,OAAO,cAAc,IAAI;AACnE;AAAA,EACD;AAEA,QAAM,YAAY,OAAO,oBAAoB,sCAAsC,KAAK;AACxF,QAAM,WAAW,UAAM;AAAA,IACtB;AAAA,IACA,QAAM,KAAK,qBAAqB,EAAE;AAAA,IAClC,KAAK,wBAAwB,QAAM,KAAK,sBAAuB,EAAE,IAAI;AAAA,EACtE;AACA,QAAM,UAAU,WAAW;AAE3B,aAAW,UAAU,SAAS;AAC7B,UAAM,qBAAqB,MAAM,QAAQ,QAAQ,UAAU,OAAO;AAAA,EACnE;AACD;",
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 (active phase, W)",
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 (active phase, 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": ";;;;;;;;;;;;;;;;;;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,SAAS,GAAG,mBAAmB;AAAA,EAC/B,SAAS,GAAG,mBAAmB;AAAA,EAC/B,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,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;",
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.13",
4
+ "version": "0.10.14",
5
5
  "messagebox": true,
6
6
  "news": {
7
- "0.10.13": {
8
- "en": "Curtailment: no longer sets grid_export_limit (app feed-in cap); combiner ac_output_limit only",
9
- "de": "Abregelung: setzt grid_export_limit (App Einspeise-Limit) nicht mehr; nur ac_output_limit",
10
- "ru": "Abregelung: без grid_export_limit",
11
- "pt": "Abregelung: sem grid_export_limit",
12
- "nl": "Abregelung: geen grid_export_limit",
13
- "fr": "Abregelung: sans grid_export_limit",
14
- "it": "Abregelung: senza grid_export_limit",
15
- "es": "Abregelung: sin grid_export_limit",
16
- "pl": "Abregelung: bez grid_export_limit",
17
- "uk": "Abregelung: без grid_export_limit",
18
- "zh-cn": "限电不再修改 grid_export_limit"
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.anker-solix",
3
- "version": "0.10.13",
3
+ "version": "0.10.14",
4
4
  "description": "ioBroker adapter for Anker Solix (based on ha-anker-solix)",
5
5
  "author": {
6
6
  "name": "MatthiasUlrich1",