iobroker.anker-solix 0.10.13 → 0.10.15

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,
@@ -28,6 +29,7 @@ __export(curtailmentPower_exports, {
28
29
  pvSensorStatePaths: () => pvSensorStatePaths,
29
30
  readLivePvPowerW: () => readLivePvPowerW,
30
31
  readPvFromEntities: () => readPvFromEntities,
32
+ readSocPercentForCurtailment: () => readSocPercentForCurtailment,
31
33
  resolveActiveExportW: () => resolveActiveExportW,
32
34
  resolveBeforeExportW: () => resolveBeforeExportW,
33
35
  resolveCurtailmentSetpoints: () => resolveCurtailmentSetpoints,
@@ -142,20 +144,59 @@ function resolveBeforeExportW(livePvW, forecast, nowHour, window) {
142
144
  return (0, import_curtailmentForecast.forecastExportTargetW)(forecast, nowHour, window);
143
145
  }
144
146
  const COMBINER_MAX_AC_OUTPUT_W = 4800;
145
- function resolveActiveExportW(livePvW, _maxChargeW) {
146
- if (livePvW <= 0) {
147
+ function calcMissingChargeWh(batteryCapacityWh, socPercent) {
148
+ if (batteryCapacityWh <= 0) {
147
149
  return 0;
148
150
  }
149
- return Math.round(livePvW);
151
+ const soc = Math.min(100, Math.max(0, socPercent));
152
+ return Math.max(0, Math.round((100 - soc) / 100 * batteryCapacityWh));
150
153
  }
151
- function calcMaxChargeW(batteryCapacityWh, socPercent, hoursRemaining) {
154
+ function calcMaxChargeW(missingWh, hoursRemaining) {
152
155
  const hours = Math.max(1, hoursRemaining);
153
- if (batteryCapacityWh <= 0) {
156
+ if (missingWh <= 0) {
154
157
  return 0;
155
158
  }
156
- const missingWh = (100 - socPercent) / 100 * batteryCapacityWh;
157
159
  return Math.max(0, Math.round(missingWh / hours));
158
160
  }
161
+ async function readSocPercentForCurtailment(host, deviceId) {
162
+ var _a, _b, _c;
163
+ const fromEntities = (_a = host.getDeviceEntities) == null ? void 0 : _a.call(host, deviceId);
164
+ if (fromEntities) {
165
+ for (const key of ["state_of_charge", "battery_soc", "total_soc"]) {
166
+ const n = Number(fromEntities[key]);
167
+ if (Number.isFinite(n) && n >= 0 && n <= 100) {
168
+ return Math.round(n);
169
+ }
170
+ }
171
+ }
172
+ const siteId = (_c = (_b = host.getDeviceSiteId) == null ? void 0 : _b.call(host, deviceId)) == null ? void 0 : _c.trim();
173
+ if (siteId) {
174
+ const systemSoc = await host.getStateAsync(`${host.namespace}.system.${siteId}.sensors.state_of_charge`);
175
+ const n = Number(systemSoc == null ? void 0 : systemSoc.val);
176
+ if (Number.isFinite(n) && n >= 0 && n <= 100) {
177
+ return Math.round(n);
178
+ }
179
+ }
180
+ const candidates = [
181
+ `${host.namespace}.combiner_box.${deviceId}.sensors.state_of_charge`,
182
+ `${host.namespace}.combiner_box.${deviceId}.sensors.battery_soc`,
183
+ `${host.namespace}.solarbank.${deviceId}.sensors.state_of_charge`
184
+ ];
185
+ for (const id of candidates) {
186
+ const st = await host.getStateAsync(id);
187
+ const n = Number(st == null ? void 0 : st.val);
188
+ if (Number.isFinite(n) && n >= 0 && n <= 100) {
189
+ return Math.round(n);
190
+ }
191
+ }
192
+ return void 0;
193
+ }
194
+ function resolveActiveExportW(livePvW, maxChargeW) {
195
+ if (livePvW <= 0) {
196
+ return 0;
197
+ }
198
+ return Math.max(0, Math.round(livePvW - Math.max(0, maxChargeW)));
199
+ }
159
200
  function resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHour, window) {
160
201
  if (phase === "before") {
161
202
  return { exportW: resolveBeforeExportW(livePvW, forecast, nowHour, window), chargeW: 0 };
@@ -170,6 +211,7 @@ function resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHo
170
211
  COMBINER_MAX_AC_OUTPUT_W,
171
212
  PV_SENSOR_IDS,
172
213
  calcMaxChargeW,
214
+ calcMissingChargeWh,
173
215
  isPvGenerationSensor,
174
216
  isPvSensorEntity,
175
217
  parsePvSensorStateId,
@@ -177,6 +219,7 @@ function resolveCurtailmentSetpoints(phase, livePvW, maxChargeW, forecast, nowHo
177
219
  pvSensorStatePaths,
178
220
  readLivePvPowerW,
179
221
  readPvFromEntities,
222
+ readSocPercentForCurtailment,
180
223
  resolveActiveExportW,
181
224
  resolveBeforeExportW,
182
225
  resolveCurtailmentSetpoints,
@@ -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/** Read battery SOC (%) for curtailment; undefined if no trustworthy sensor value. */\nexport async function readSocPercentForCurtailment(\n\thost: CurtailmentPowerHost,\n\tdeviceId: string,\n): Promise<number | undefined> {\n\tconst fromEntities = host.getDeviceEntities?.(deviceId);\n\tif (fromEntities) {\n\t\tfor (const key of [\"state_of_charge\", \"battery_soc\", \"total_soc\"] as const) {\n\t\t\tconst n = Number(fromEntities[key]);\n\t\t\tif (Number.isFinite(n) && n >= 0 && n <= 100) {\n\t\t\t\treturn Math.round(n);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst siteId = host.getDeviceSiteId?.(deviceId)?.trim();\n\tif (siteId) {\n\t\tconst systemSoc = await host.getStateAsync(`${host.namespace}.system.${siteId}.sensors.state_of_charge`);\n\t\tconst n = Number(systemSoc?.val);\n\t\tif (Number.isFinite(n) && n >= 0 && n <= 100) {\n\t\t\treturn Math.round(n);\n\t\t}\n\t}\n\n\tconst candidates = [\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\t`${host.namespace}.solarbank.${deviceId}.sensors.state_of_charge`,\n\t];\n\tfor (const id of candidates) {\n\t\tconst st = await host.getStateAsync(id);\n\t\tconst n = Number(st?.val);\n\t\tif (Number.isFinite(n) && n >= 0 && n <= 100) {\n\t\t\treturn Math.round(n);\n\t\t}\n\t}\n\n\treturn undefined;\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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAAsC;AAI/B,MAAM,gBAAgB,CAAC,kBAAkB,eAAe,mBAAmB;AAGlF,MAAM,kBAAkB,CAAC,oBAAoB,uBAAuB,4BAA4B;AAYzF,SAAS,uBAAuB,WAAmB,QAAwB;AACjF,SAAO,GAAG,SAAS,WAAW,MAAM;AACrC;AAEO,SAAS,mBAAmB,WAAmB,UAA4B;AACjF,QAAM,QAAkB,CAAC;AACzB,aAAW,WAAW,CAAC,aAAa,cAAc,GAAY;AAC7D,eAAW,UAAU,CAAC,GAAG,eAAe,GAAG,eAAe,GAAG;AAC5D,YAAM,KAAK,GAAG,SAAS,IAAI,OAAO,IAAI,QAAQ,YAAY,MAAM,EAAE;AAAA,IACnE;AAAA,EACD;AACA,SAAO;AACR;AAEO,SAAS,qBAAqB,WAAmB,SAAiD;AAjCzG;AAkCC,QAAM,SAAS,GAAG,SAAS;AAC3B,MAAI,CAAC,QAAQ,WAAW,MAAM,GAAG;AAChC,WAAO;AAAA,EACR;AACA,QAAM,OAAO,QAAQ,MAAM,OAAO,MAAM;AACxC,QAAM,QAAQ,6CAA6C,KAAK,IAAI;AACpE,MAAI,CAAC,OAAO;AACX,WAAO;AAAA,EACR;AACA,SAAO,EAAE,SAAQ,WAAM,CAAC,MAAP,YAAY,GAAG;AACjC;AAEO,SAAS,qBACf,WACA,SACuD;AAjDxD;AAkDC,QAAM,SAAS,GAAG,SAAS;AAC3B,MAAI,CAAC,QAAQ,WAAW,MAAM,KAAK,CAAC,QAAQ,SAAS,WAAW,GAAG;AAClE,WAAO;AAAA,EACR;AACA,QAAM,OAAO,QAAQ,MAAM,OAAO,MAAM;AACxC,QAAM,QAAQ,+EAA+E,KAAK,IAAI;AACtG,MAAI,CAAC,OAAO;AACX,WAAO;AAAA,EACR;AACA,SAAO,EAAE,WAAU,WAAM,CAAC,MAAP,YAAY,IAAI,QAAQ,MAAM,CAAC,EAAgB;AACnE;AAEO,SAAS,iBAAiB,UAA0C;AAC1E,SAAQ,cAAoC,SAAS,QAAQ;AAC9D;AAEO,SAAS,qBAAqB,UAA2B;AAC/D,SAAO,iBAAiB,QAAQ,KAAM,gBAAsC,SAAS,QAAQ;AAC9F;AAGO,SAAS,mBAAmB,UAAuD;AACzF,MAAI,CAAC,UAAU;AACd,WAAO;AAAA,EACR;AACA,MAAI,MAAM;AACV,aAAW,OAAO,eAAe;AAChC,UAAM,IAAI,OAAO,SAAS,GAAG,CAAC;AAC9B,QAAI,OAAO,SAAS,CAAC,KAAK,IAAI,KAAK;AAClC,YAAM;AAAA,IACP;AAAA,EACD;AACA,MAAI,MAAM,GAAG;AACZ,WAAO,KAAK,MAAM,GAAG;AAAA,EACtB;AACA,MAAI,UAAU;AACd,aAAW,OAAO,iBAAiB;AAClC,UAAM,IAAI,OAAO,SAAS,GAAG,CAAC;AAC9B,QAAI,OAAO,SAAS,CAAC,KAAK,IAAI,GAAG;AAChC,iBAAW;AAAA,IACZ;AAAA,EACD;AACA,MAAI,UAAU,GAAG;AAChB,WAAO,KAAK,MAAM,OAAO;AAAA,EAC1B;AACA,SAAO;AACR;AAEA,eAAe,mBAAmB,MAA4B,QAAiC;AAC9F,QAAM,KAAK,MAAM,KAAK,cAAc,uBAAuB,KAAK,WAAW,MAAM,CAAC;AAClF,QAAM,IAAI,OAAO,yBAAI,GAAG;AACxB,SAAO,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AACtD;AAGA,eAAsB,iBAAiB,MAA4B,UAAmC;AAzGtG;AA0GC,QAAM,UAAS,gBAAK,oBAAL,8BAAuB,cAAvB,mBAAkC;AACjD,MAAI,QAAQ;AACX,UAAM,aAAa,MAAM,mBAAmB,MAAM,MAAM;AACxD,QAAI,aAAa,GAAG;AACnB,aAAO;AAAA,IACR;AAAA,EACD;AACA,QAAM,WAAW,oBAAmB,UAAK,sBAAL,8BAAyB,SAAS;AACtE,MAAI,WAAW,GAAG;AACjB,WAAO;AAAA,EACR;AACA,MAAI,MAAM;AACV,aAAW,MAAM,mBAAmB,KAAK,WAAW,QAAQ,GAAG;AAC9D,UAAM,KAAK,MAAM,KAAK,cAAc,EAAE;AACtC,UAAM,IAAI,OAAO,yBAAI,GAAG;AACxB,QAAI,OAAO,SAAS,CAAC,KAAK,IAAI,KAAK;AAClC,YAAM;AAAA,IACP;AAAA,EACD;AACA,SAAO,MAAM,IAAI,KAAK,MAAM,GAAG,IAAI;AACpC;AAGO,SAAS,qBACf,SACA,UACA,SACA,QACS;AACT,MAAI,UAAU,GAAG;AAChB,WAAO;AAAA,EACR;AACA,aAAO,kDAAsB,UAAU,SAAS,MAAM;AACvD;AAGO,MAAM,2BAA2B;AAGjC,SAAS,oBAAoB,mBAA2B,YAA4B;AAC1F,MAAI,qBAAqB,GAAG;AAC3B,WAAO;AAAA,EACR;AACA,QAAM,MAAM,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,UAAU,CAAC;AACjD,SAAO,KAAK,IAAI,GAAG,KAAK,OAAQ,MAAM,OAAO,MAAO,iBAAiB,CAAC;AACvE;AAGO,SAAS,eAAe,WAAmB,gBAAgC;AACjF,QAAM,QAAQ,KAAK,IAAI,GAAG,cAAc;AACxC,MAAI,aAAa,GAAG;AACnB,WAAO;AAAA,EACR;AACA,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,KAAK,CAAC;AACjD;AAGA,eAAsB,6BACrB,MACA,UAC8B;AAtK/B;AAuKC,QAAM,gBAAe,UAAK,sBAAL,8BAAyB;AAC9C,MAAI,cAAc;AACjB,eAAW,OAAO,CAAC,mBAAmB,eAAe,WAAW,GAAY;AAC3E,YAAM,IAAI,OAAO,aAAa,GAAG,CAAC;AAClC,UAAI,OAAO,SAAS,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK;AAC7C,eAAO,KAAK,MAAM,CAAC;AAAA,MACpB;AAAA,IACD;AAAA,EACD;AAEA,QAAM,UAAS,gBAAK,oBAAL,8BAAuB,cAAvB,mBAAkC;AACjD,MAAI,QAAQ;AACX,UAAM,YAAY,MAAM,KAAK,cAAc,GAAG,KAAK,SAAS,WAAW,MAAM,0BAA0B;AACvG,UAAM,IAAI,OAAO,uCAAW,GAAG;AAC/B,QAAI,OAAO,SAAS,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK;AAC7C,aAAO,KAAK,MAAM,CAAC;AAAA,IACpB;AAAA,EACD;AAEA,QAAM,aAAa;AAAA,IAClB,GAAG,KAAK,SAAS,iBAAiB,QAAQ;AAAA,IAC1C,GAAG,KAAK,SAAS,iBAAiB,QAAQ;AAAA,IAC1C,GAAG,KAAK,SAAS,cAAc,QAAQ;AAAA,EACxC;AACA,aAAW,MAAM,YAAY;AAC5B,UAAM,KAAK,MAAM,KAAK,cAAc,EAAE;AACtC,UAAM,IAAI,OAAO,yBAAI,GAAG;AACxB,QAAI,OAAO,SAAS,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK;AAC7C,aAAO,KAAK,MAAM,CAAC;AAAA,IACpB;AAAA,EACD;AAEA,SAAO;AACR;AAGO,SAAS,qBAAqB,SAAiB,YAA4B;AACjF,MAAI,WAAW,GAAG;AACjB,WAAO;AAAA,EACR;AACA,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC;AACjE;AAEO,SAAS,4BACf,OACA,SACA,YACA,UACA,SACA,QACuC;AACvC,MAAI,UAAU,UAAU;AACvB,WAAO,EAAE,SAAS,qBAAqB,SAAS,UAAU,SAAS,MAAM,GAAG,SAAS,EAAE;AAAA,EACxF;AACA,MAAI,UAAU,UAAU;AACvB,WAAO,EAAE,SAAS,qBAAqB,SAAS,UAAU,GAAG,SAAS,WAAW;AAAA,EAClF;AACA,SAAO,EAAE,SAAS,GAAG,SAAS,EAAE;AACjC;",
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,106 +39,34 @@ 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
- async function readSocPercent(host, deviceId) {
54
- const candidates = [
55
- `${host.namespace}.solarbank.${deviceId}.sensors.state_of_charge`,
56
- `${host.namespace}.combiner_box.${deviceId}.sensors.state_of_charge`,
57
- `${host.namespace}.combiner_box.${deviceId}.sensors.battery_soc`
58
- ];
59
- for (const id of candidates) {
60
- const st = await host.getStateAsync(id);
61
- if ((st == null ? void 0 : st.val) !== null && (st == null ? void 0 : st.val) !== void 0) {
62
- const n = Number(st.val);
63
- if (!Number.isNaN(n)) {
64
- return Math.min(100, Math.max(0, n));
65
- }
66
- }
67
- }
68
- return 0;
69
- }
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) {
49
+ async function applyManualMode(host, device) {
78
50
  const ctx = host.getDeviceContext(device.deviceId);
79
51
  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
- }
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
52
  }
114
- async function applyExportLimit(host, device, exportTargetW) {
115
- const exportW = clampExportW(exportTargetW, device.role);
116
- if (exportW <= 0) {
53
+ async function applyAcOutputLimit(host, device, targetW) {
54
+ const acOutputW = clampAcOutputW(targetW, device.role);
55
+ const last = lastAppliedExportW.get(device.deviceId);
56
+ if (last === acOutputW) {
117
57
  return;
118
58
  }
119
59
  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);
60
+ await host.applyControl(device.deviceId, "ac_output_limit", acOutputW, ctx, { acOutputApiOnly: true });
61
+ lastAppliedExportW.set(device.deviceId, acOutputW);
136
62
  }
137
63
  async function applyAfterPhase(host, device, modeAfter) {
138
64
  lastAppliedExportW.delete(device.deviceId);
139
- lastAppliedHomeLoadW.delete(device.deviceId);
140
- lastAppliedChargeW.delete(device.deviceId);
141
65
  lastAppliedPhase.delete(device.deviceId);
142
66
  const ctx = host.getDeviceContext(device.deviceId);
143
67
  await host.applyControl(device.deviceId, "preset_usage_mode", modeAfter, ctx);
144
68
  }
145
- async function applyCurtailmentSetpoints(host, device, phase, exportW, chargeW, modeAfter, opts) {
69
+ async function applyCurtailmentSetpoints(host, device, phase, exportW, modeAfter, opts) {
146
70
  const prevPhase = lastAppliedPhase.get(device.deviceId);
147
71
  const phaseChanged = prevPhase !== phase;
148
72
  if (phase === "after" || phase === "idle") {
@@ -150,14 +74,13 @@ async function applyCurtailmentSetpoints(host, device, phase, exportW, chargeW,
150
74
  return;
151
75
  }
152
76
  if (phaseChanged || !(opts == null ? void 0 : opts.modeOnly)) {
153
- await applyManualAndExportSwitches(host, device);
77
+ await applyManualMode(host, device);
154
78
  lastAppliedPhase.set(device.deviceId, phase);
155
79
  }
156
80
  if (opts == null ? void 0 : opts.modeOnly) {
157
81
  return;
158
82
  }
159
- await applyChargeLimit(host, device, chargeW);
160
- await applyExportLimit(host, device, exportW);
83
+ await applyAcOutputLimit(host, device, exportW);
161
84
  }
162
85
  async function buildDeviceContext(host, device, forecast, nowHour, livePvOverride) {
163
86
  const limit = (0, import_curtailmentProfiles.acExportLimitW)(device);
@@ -165,12 +88,14 @@ async function buildDeviceContext(host, device, forecast, nowHour, livePvOverrid
165
88
  const phase = (0, import_curtailmentForecast.currentPhase)(window, nowHour);
166
89
  const livePvW = livePvOverride !== void 0 && livePvOverride >= 0 ? Math.round(livePvOverride) : window.today ? await (0, import_curtailmentPower.readLivePvPowerW)(host, device.deviceId) : 0;
167
90
  const remaining = (0, import_curtailmentForecast.remainingCurtailmentHours)(window, nowHour);
168
- 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;
91
+ const soc = window.today && phase === "active" ? await (0, import_curtailmentPower.readSocPercentForCurtailment)(host, device.deviceId) : void 0;
92
+ const missingChargeWh = window.today && phase === "active" && soc !== void 0 ? (0, import_curtailmentPower.calcMissingChargeWh)(device.batteryCapacityWh, soc) : 0;
93
+ const maxChargeW = window.today && phase === "active" && soc !== void 0 ? (0, import_curtailmentPower.calcMaxChargeW)(missingChargeWh, remaining) : 0;
170
94
  const { exportW, chargeW } = (0, import_curtailmentPower.resolveCurtailmentSetpoints)(phase, livePvW, maxChargeW, forecast, nowHour, window);
171
- return { limit, window, phase, livePvW, maxChargeW, exportW, chargeW, remaining, soc };
95
+ return { limit, window, phase, livePvW, missingChargeWh, maxChargeW, exportW, chargeW, remaining, soc };
172
96
  }
173
97
  async function publishDeviceStates(host, ctx) {
98
+ var _a;
174
99
  await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.today, ctx.window.today, true);
175
100
  await host.setState(
176
101
  import_curtailmentStates.CURTAILMENT_STATE_IDS.start,
@@ -182,6 +107,8 @@ async function publishDeviceStates(host, ctx) {
182
107
  ctx.window.today ? `${ctx.window.endHour.toString().padStart(2, "0")}:00` : "",
183
108
  true
184
109
  );
110
+ await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.missingChargeWh, ctx.missingChargeWh, true);
111
+ await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.socPercent, (_a = ctx.soc) != null ? _a : 0, true);
185
112
  await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.maxChargeW, ctx.maxChargeW, true);
186
113
  await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.exportW, ctx.exportW, true);
187
114
  await host.setState(import_curtailmentStates.CURTAILMENT_STATE_IDS.remainingHours, ctx.remaining, true);
@@ -198,18 +125,24 @@ async function runDeviceCurtailment(host, device, config, forecast, nowHour, opt
198
125
  }
199
126
  if (ctx.phase !== "before" && ctx.phase !== "active") {
200
127
  if (!(opts == null ? void 0 : opts.setpointsOnly)) {
201
- await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter);
128
+ await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter);
202
129
  }
203
130
  return;
204
131
  }
205
132
  if (!(opts == null ? void 0 : opts.setpointsOnly)) {
206
133
  const unitsHint = device.role === "combiner" && ((_a = device.units) == null ? void 0 : _a.length) ? `, units=${device.units.join("+")} (${device.units.length} banks)` : "";
134
+ const socHint = ctx.soc === void 0 ? "SOC=n/a" : `SOC=${ctx.soc}%`;
135
+ if (ctx.phase === "active" && ctx.soc === void 0) {
136
+ host.log.warn(
137
+ `Curtailment [${device.deviceId}]: no SOC sensor \u2014 missing_charge_wh and max_charge_w stay 0; check state_of_charge`
138
+ );
139
+ }
207
140
  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}%`
141
+ `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, ${socHint}`
209
142
  );
210
143
  }
211
144
  try {
212
- await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter, {
145
+ await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {
213
146
  modeOnly: (opts == null ? void 0 : opts.setpointsOnly) && lastAppliedPhase.get(device.deviceId) === ctx.phase
214
147
  });
215
148
  } catch (err) {
@@ -238,11 +171,11 @@ async function runCurtailmentOnPvChange(host, config, deviceId, livePvW) {
238
171
  }
239
172
  await publishDeviceStates(host, ctx);
240
173
  try {
241
- await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, ctx.chargeW, config.modeAfter, {
174
+ await applyCurtailmentSetpoints(host, device, ctx.phase, ctx.exportW, config.modeAfter, {
242
175
  modeOnly: lastAppliedPhase.get(device.deviceId) === ctx.phase
243
176
  });
244
177
  host.log.debug(
245
- `Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W, charge=${ctx.chargeW}W, export=${ctx.exportW}W`
178
+ `Curtailment PV follow [${device.deviceId}]: phase=${ctx.phase}, livePv=${ctx.livePvW}W, missingWh=${ctx.missingChargeWh}, maxCharge=${ctx.maxChargeW}W, export=${ctx.exportW}W`
246
179
  );
247
180
  } catch (err) {
248
181
  host.log.warn(`Curtailment PV follow failed for ${device.deviceId}: ${err.message}`);
@@ -273,7 +206,6 @@ async function runCurtailmentAvoidance(host, config) {
273
206
  }
274
207
  // Annotate the CommonJS export names for ESM import in node:
275
208
  0 && (module.exports = {
276
- COMBINER_MIN_HOME_LOAD_W,
277
209
  runCurtailmentAvoidance,
278
210
  runCurtailmentOnPvChange
279
211
  });
@@ -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": "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",
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\treadSocPercentForCurtailment,\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\topts?: { acOutputApiOnly?: boolean },\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 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, { acOutputApiOnly: true });\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 =\n\t\twindow.today && phase === \"active\" ? await readSocPercentForCurtailment(host, device.deviceId) : undefined;\n\tconst missingChargeWh =\n\t\twindow.today && phase === \"active\" && soc !== undefined\n\t\t\t? calcMissingChargeWh(device.batteryCapacityWh, soc)\n\t\t\t: 0;\n\tconst maxChargeW =\n\t\twindow.today && phase === \"active\" && soc !== undefined ? 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.socPercent, ctx.soc ?? 0, 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\tconst socHint = ctx.soc === undefined ? \"SOC=n/a\" : `SOC=${ctx.soc}%`;\n\t\tif (ctx.phase === \"active\" && ctx.soc === undefined) {\n\t\t\thost.log.warn(\n\t\t\t\t`Curtailment [${device.deviceId}]: no SOC sensor \u2014 missing_charge_wh and max_charge_w stay 0; check state_of_charge`,\n\t\t\t);\n\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, ${socHint}`,\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,8 @@ 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`,
32
+ socPercent: `${CURTAILMENT_CHANNEL}.soc_percent`,
31
33
  maxChargeW: `${CURTAILMENT_CHANNEL}.max_charge_w`,
32
34
  exportW: `${CURTAILMENT_CHANNEL}.export_w`,
33
35
  livePvW: `${CURTAILMENT_CHANNEL}.live_pv_w`,
@@ -75,10 +77,22 @@ async function setupCurtailmentStates(adapter) {
75
77
  def: ""
76
78
  }
77
79
  },
80
+ {
81
+ id: CURTAILMENT_STATE_IDS.missingChargeWh,
82
+ common: {
83
+ name: "Missing charge energy to full SOC (active phase, Wh)",
84
+ type: "number",
85
+ role: "value.energy",
86
+ unit: "Wh",
87
+ read: true,
88
+ write: false,
89
+ def: 0
90
+ }
91
+ },
78
92
  {
79
93
  id: CURTAILMENT_STATE_IDS.maxChargeW,
80
94
  common: {
81
- name: "Max AC charge power (active phase, W)",
95
+ name: "Max AC charge power (missing Wh \xF7 remaining hours, W)",
82
96
  type: "number",
83
97
  role: "value.power",
84
98
  unit: "W",
@@ -153,6 +167,7 @@ async function setupCurtailmentStates(adapter) {
153
167
  common: st.common,
154
168
  native: {}
155
169
  });
170
+ await adapter.extendObject(st.id, { common: st.common });
156
171
  }
157
172
  }
158
173
  // Annotate the CommonJS export names for ESM import in node:
@@ -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": "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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\tsocPercent: `${CURTAILMENT_CHANNEL}.soc_percent`,\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\t// Ensure new states appear after adapter upgrades (setObjectNotExists alone does not update).\n\t\tawait adapter.extendObject(st.id, { common: st.common });\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/types.ts"],
4
- "sourcesContent": ["export interface DeviceInfo {\n\tid: string;\n\ttype: string;\n\tname: string;\n\tsite_id: string;\n\tmodel: string;\n\tdevice_pn?: string;\n\tstation_sn?: string;\n\tgeneration?: number;\n}\n\n/** Cached from last poll so set-bridge can use MQTT without extra site refresh. */\nexport interface DeviceControlContext {\n\ttype: string;\n\tsite_id: string;\n\tdevice_pn: string;\n\tstation_sn: string;\n\tgeneration: number;\n}\n\nexport interface BridgeDevice {\n\tinfo: DeviceInfo;\n\tentities: Record<string, string | number | boolean | null>;\n\twritable: string[];\n\t/** Valid SolarbankUsageMode names for preset_usage_mode (HA solarbank_usage_mode_options). */\n\tusage_mode_options?: string[];\n\t/** Site/device has energy_details from API (statistics channel). */\n\thasStatistics?: boolean;\n}\n\nexport interface DeviceListEntry {\n\tid: string;\n\tname: string;\n\ttype?: string;\n\tsite_id?: string;\n\tmodel?: string;\n}\n\nexport interface BridgePollResult {\n\tok: boolean;\n\terror?: string;\n\tnickname?: string;\n\tdevices?: BridgeDevice[] | DeviceListEntry[];\n\tsites?: Array<{ id: string; name: string }>;\n\tschedule?: unknown;\n\tpath?: string;\n\tsystem?: unknown;\n\trefreshDetails?: boolean;\n\tintervalcount?: number;\n\tdeviceintervals?: number;\n}\n\nexport interface BridgeConfig {\n\tusername: string;\n\tpassword: string;\n\tcountry: string;\n\tmqttUsage: boolean;\n\tcacheDir: string;\n\texclude?: string[];\n\tenableAllDevices?: boolean;\n\tselectedSiteId?: string;\n\tselectedDeviceIds?: string[];\n\t/** HA dev_interval_mult: device details every N site polls (default 10). */\n\tdeviceDetailMultiplier?: number;\n\trequestDelay?: number;\n\trequestTimeout?: number;\n\tendpointLimit?: number;\n\tenableCoreEntities?: boolean;\n\tenableEnergyStatistics?: boolean;\n\tenableEnergyDetail?: boolean;\n\tenablePowerFlows?: boolean;\n\tenableDiagnostics?: boolean;\n\tenableBinaryIndicators?: boolean;\n\tenableAdvancedControls?: boolean;\n\tenableSystemOverview?: boolean;\n\tenableSitePrice?: boolean;\n\tenableAccountInfo?: boolean;\n\tenableSolarbankMeta?: boolean;\n\tenableSmartplug?: boolean;\n\tenablePps?: boolean;\n\tenableEvCharger?: boolean;\n\tenableVehicle?: boolean;\n\tenableHes?: boolean;\n\tenablePowerPanel?: boolean;\n\tenableInverter?: boolean;\n}\n\nexport interface BridgeSetConfig extends BridgeConfig {\n\tdeviceId: string;\n\tcontrol: string;\n\tvalue: string | number | boolean;\n\tdeviceContext?: DeviceControlContext;\n}\n\nexport interface BridgeServiceConfig extends BridgeConfig {\n\tservice: string;\n\tparams: Record<string, unknown>;\n}\n"],
4
+ "sourcesContent": ["export interface DeviceInfo {\n\tid: string;\n\ttype: string;\n\tname: string;\n\tsite_id: string;\n\tmodel: string;\n\tdevice_pn?: string;\n\tstation_sn?: string;\n\tgeneration?: number;\n}\n\n/** Cached from last poll so set-bridge can use MQTT without extra site refresh. */\nexport interface DeviceControlContext {\n\ttype: string;\n\tsite_id: string;\n\tdevice_pn: string;\n\tstation_sn: string;\n\tgeneration: number;\n}\n\nexport interface BridgeDevice {\n\tinfo: DeviceInfo;\n\tentities: Record<string, string | number | boolean | null>;\n\twritable: string[];\n\t/** Valid SolarbankUsageMode names for preset_usage_mode (HA solarbank_usage_mode_options). */\n\tusage_mode_options?: string[];\n\t/** Site/device has energy_details from API (statistics channel). */\n\thasStatistics?: boolean;\n}\n\nexport interface DeviceListEntry {\n\tid: string;\n\tname: string;\n\ttype?: string;\n\tsite_id?: string;\n\tmodel?: string;\n}\n\nexport interface BridgePollResult {\n\tok: boolean;\n\terror?: string;\n\tnickname?: string;\n\tdevices?: BridgeDevice[] | DeviceListEntry[];\n\tsites?: Array<{ id: string; name: string }>;\n\tschedule?: unknown;\n\tpath?: string;\n\tsystem?: unknown;\n\trefreshDetails?: boolean;\n\tintervalcount?: number;\n\tdeviceintervals?: number;\n}\n\nexport interface BridgeConfig {\n\tusername: string;\n\tpassword: string;\n\tcountry: string;\n\tmqttUsage: boolean;\n\tcacheDir: string;\n\texclude?: string[];\n\tenableAllDevices?: boolean;\n\tselectedSiteId?: string;\n\tselectedDeviceIds?: string[];\n\t/** HA dev_interval_mult: device details every N site polls (default 10). */\n\tdeviceDetailMultiplier?: number;\n\trequestDelay?: number;\n\trequestTimeout?: number;\n\tendpointLimit?: number;\n\tenableCoreEntities?: boolean;\n\tenableEnergyStatistics?: boolean;\n\tenableEnergyDetail?: boolean;\n\tenablePowerFlows?: boolean;\n\tenableDiagnostics?: boolean;\n\tenableBinaryIndicators?: boolean;\n\tenableAdvancedControls?: boolean;\n\tenableSystemOverview?: boolean;\n\tenableSitePrice?: boolean;\n\tenableAccountInfo?: boolean;\n\tenableSolarbankMeta?: boolean;\n\tenableSmartplug?: boolean;\n\tenablePps?: boolean;\n\tenableEvCharger?: boolean;\n\tenableVehicle?: boolean;\n\tenableHes?: boolean;\n\tenablePowerPanel?: boolean;\n\tenableInverter?: boolean;\n}\n\nexport interface BridgeSetConfig extends BridgeConfig {\n\tdeviceId: string;\n\tcontrol: string;\n\tvalue: string | number | boolean;\n\tdeviceContext?: DeviceControlContext;\n\t/** Curtailment: set AC output via API only (no MQTT / station feed-in side effects). */\n\tacOutputApiOnly?: boolean;\n}\n\nexport interface BridgeServiceConfig extends BridgeConfig {\n\tservice: string;\n\tparams: Record<string, unknown>;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -285,7 +285,7 @@ class AnkerSolix extends utils.Adapter {
285
285
  });
286
286
  }
287
287
  }
288
- async applyAdapterControl(deviceId, control, value, deviceContext) {
288
+ async applyAdapterControl(deviceId, control, value, deviceContext, opts) {
289
289
  await (0, import_pythonBridge.runBridge)(
290
290
  "set",
291
291
  {
@@ -293,7 +293,8 @@ class AnkerSolix extends utils.Adapter {
293
293
  deviceId,
294
294
  control,
295
295
  value,
296
- deviceContext
296
+ deviceContext,
297
+ acOutputApiOnly: opts == null ? void 0 : opts.acOutputApiOnly
297
298
  },
298
299
  this.config.pythonPath || "",
299
300
  this.log
@@ -340,7 +341,7 @@ class AnkerSolix extends utils.Adapter {
340
341
  await this.setState(id, val, ack != null ? ack : true);
341
342
  },
342
343
  getDeviceContext: (deviceId) => this.deviceContexts.get(deviceId),
343
- applyControl: (deviceId, control, value, deviceContext) => this.applyAdapterControl(deviceId, control, value, deviceContext)
344
+ applyControl: (deviceId, control, value, deviceContext, opts) => this.applyAdapterControl(deviceId, control, value, deviceContext, opts)
344
345
  };
345
346
  }
346
347
  refreshCurtailmentDeviceIds() {
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["/*\n * ioBroker Anker Solix adapter\n * Based on https://github.com/thomluther/ha-anker-solix (Home Assistant integration)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport * as utils from \"@iobroker/adapter-core\";\n\nimport { parseSelectedDeviceIds } from \"./lib/configHelpers\";\nimport { ControlQueue } from \"./lib/controlQueue\";\nimport { parsePvSensorStateId, parseSystemPvStateId } from \"./lib/curtailmentPower\";\nimport {\n\trunCurtailmentAvoidance,\n\trunCurtailmentOnPvChange,\n\ttype CurtailmentRunnerConfig,\n\ttype CurtailmentRunnerHost,\n} from \"./lib/curtailmentRunner\";\nimport { setupCurtailmentStates } from \"./lib/curtailmentStates\";\nimport { runPythonInstaller } from \"./lib/ensurePython\";\nimport { ensureBridgeDaemon, runBridge, stopBridgeDaemon } from \"./lib/pythonBridge\";\nimport { SERVICE_STATES, setupServiceStates } from \"./lib/services\";\nimport { resolveCurtailmentDevices } from \"./lib/curtailmentConfig\";\nimport { parseControlStateId, syncDevices, type CurtailmentPvSyncHost } from \"./lib/stateSync\";\nimport type { BridgeConfig, BridgeDevice, BridgeServiceConfig, DeviceControlContext } from \"./lib/types\";\n\nclass AnkerSolix extends utils.Adapter {\n\tprivate pollTimer: ioBroker.Interval | undefined;\n\tprivate readonly controlQueue = new ControlQueue();\n\tprivate readonly deviceContexts = new Map<string, DeviceControlContext>();\n\tprivate readonly deviceEntities = new Map<string, Record<string, unknown>>();\n\tprivate readonly deviceWritable = new Map<string, string[]>();\n\tprivate readonly lastNotifiedPvW = new Map<string, number>();\n\tprivate readonly curtailmentDeviceIds = new Set<string>();\n\tprivate pollAfterControlTimer: NodeJS.Timeout | undefined;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"anker-solix\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate getAuthCacheDir(): string {\n\t\treturn path.join(utils.getAbsoluteInstanceDataDir(this), \"authcache\");\n\t}\n\n\tprivate getAuthCacheFile(): string {\n\t\tconst email = (this.config.username || \"\").trim();\n\t\treturn path.join(this.getAuthCacheDir(), `${email}.json`);\n\t}\n\n\tprivate logAuthCacheStatus(): void {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst cacheFile = this.getAuthCacheFile();\n\t\tconst email = (this.config.username || \"\").trim();\n\t\tif (!email) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(cacheDir, { recursive: true });\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Cannot create authcache folder ${cacheDir}: ${(err as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (fs.existsSync(cacheFile)) {\n\t\t\tthis.log.debug(`Anker login cache present: ${cacheFile}`);\n\t\t\treturn;\n\t\t}\n\t\tlet other = \"\";\n\t\ttry {\n\t\t\tconst names = fs.readdirSync(cacheDir).filter(f => f.endsWith(\".json\"));\n\t\t\tif (names.length) {\n\t\t\t\tother = ` Found other file(s) in folder: ${names.join(\", \")} (username must match filename).`;\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t\tthis.log.warn(\n\t\t\t`No Anker login cache at ${cacheFile}.${other} ` +\n\t\t\t\t\"Without this file every adapter restart triggers a new API login (often captcha 100032). \" +\n\t\t\t\t\"Copy <email>.json from a working Anker/Solix integration (e.g. ha-anker-solix) into that folder, then restart.\",\n\t\t);\n\t}\n\n\tprivate getBridgeConfig(): BridgeConfig {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst selectedIds = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\n\t\treturn {\n\t\t\tusername: this.config.username,\n\t\t\tpassword: this.config.password,\n\t\t\tcountry: this.config.country || \"DE\",\n\t\t\tmqttUsage: this.config.mqttUsage !== false,\n\t\t\tcacheDir,\n\t\t\tenableAllDevices: this.config.enableAllDevices !== false,\n\t\t\tselectedSiteId: this.config.selectedSiteId || \"\",\n\t\t\tselectedDeviceIds: selectedIds,\n\t\t\tdeviceDetailMultiplier: Math.max(1, Number(this.config.deviceDetailMultiplier) || 10),\n\t\t\trequestDelay: Number(this.config.requestDelay) || 0.3,\n\t\t\trequestTimeout: Number(this.config.requestTimeout) || 10,\n\t\t\tendpointLimit: Number(this.config.endpointLimit) || 10,\n\t\t\tenableCoreEntities: this.config.enableCoreEntities !== false,\n\t\t\tenableEnergyStatistics: !!this.config.enableEnergyStatistics,\n\t\t\tenableEnergyDetail: !!this.config.enableEnergyDetail,\n\t\t\tenablePowerFlows: !!this.config.enablePowerFlows,\n\t\t\tenableDiagnostics: !!this.config.enableDiagnostics,\n\t\t\tenableBinaryIndicators: !!this.config.enableBinaryIndicators,\n\t\t\tenableAdvancedControls: !!this.config.enableAdvancedControls,\n\t\t\tenableSystemOverview: !!this.config.enableSystemOverview,\n\t\t\tenableSitePrice: !!this.config.enableSitePrice,\n\t\t\tenableAccountInfo: !!this.config.enableAccountInfo,\n\t\t\tenableSolarbankMeta: !!this.config.enableSolarbankMeta,\n\t\t\tenableSmartplug: !!this.config.enableSmartplug,\n\t\t\tenablePps: !!this.config.enablePps,\n\t\t\tenableEvCharger: !!this.config.enableEvCharger,\n\t\t\tenableVehicle: !!this.config.enableVehicle,\n\t\t\tenableHes: !!this.config.enableHes,\n\t\t\tenablePowerPanel: !!this.config.enablePowerPanel,\n\t\t\tenableInverter: !!this.config.enableInverter,\n\t\t};\n\t}\n\n\t/** Remove legacy install symlink from old GitHub repo name \"AnkerSolix\". */\n\tprivate cleanupLegacyInstallSymlink(): void {\n\t\tconst alias = path.join(this.adapterDir, \"..\", \"iobroker.AnkerSolix\");\n\t\ttry {\n\t\t\tif (fs.existsSync(alias) && fs.lstatSync(alias).isSymbolicLink()) {\n\t\t\t\tfs.unlinkSync(alias);\n\t\t\t\tthis.log.info(\"Removed legacy symlink iobroker.AnkerSolix\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tprivate async ensurePythonDeps(force = false): Promise<boolean> {\n\t\tif (!force && this.config.autoInstallPython === false) {\n\t\t\treturn true;\n\t\t}\n\t\tconst result = await runPythonInstaller(this.config.pythonPath || \"\", this.log);\n\t\tawait this.setState(\"info.pythonReady\", result.ok, true);\n\t\tif (!result.ok) {\n\t\t\tthis.log.warn(`Python setup: ${result.message}`);\n\t\t}\n\t\treturn result.ok;\n\t}\n\n\tprivate async pollOnce(): Promise<void> {\n\t\tif (!this.config.acceptTerms) {\n\t\t\tthis.log.warn(\"Please accept the usage terms in the adapter configuration.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.config.username?.trim()) {\n\t\t\tthis.log.warn(\"Anker e-mail (username) is required in adapter settings.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.config.password?.trim()) {\n\t\t\tthis.log.warn(\"Password missing \u2013 open instance config in Admin, re-enter Anker password and save.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!(await this.ensurePythonDeps())) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runBridge(\"poll\", this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (this.config.enableCurtailmentAvoidance) {\n\t\t\t\tthis.refreshCurtailmentDeviceIds();\n\t\t\t}\n\n\t\t\tconst pollDevices = result.devices as BridgeDevice[] | undefined;\n\t\t\tif (pollDevices?.length) {\n\t\t\t\tthis.rememberDeviceContexts(pollDevices);\n\t\t\t\tthis.rememberDeviceEntities(pollDevices);\n\t\t\t\tawait syncDevices(this, pollDevices);\n\t\t\t}\n\n\t\t\tif (result.nickname) {\n\t\t\t\tawait this.setState(\"account.nickname\", result.nickname, true);\n\t\t\t}\n\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t\tconst detailHint = result.refreshDetails ? \"devices+mqtt\" : \"sites\";\n\t\t\tconst intervalHint =\n\t\t\t\tresult.intervalcount !== undefined && result.deviceintervals !== undefined\n\t\t\t\t\t? `, next detail in ~${result.intervalcount} polls`\n\t\t\t\t\t: \"\";\n\t\t\tthis.log.debug(`Poll OK (${pollDevices?.length ?? 0} devices, ${detailHint}${intervalHint})`);\n\n\t\t\tawait this.runCurtailmentAvoidanceIfEnabled();\n\t\t} catch (error) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\tconst msg = (error as Error).message || String(error);\n\t\t\tif (msg.includes(\"CaptchaRequired\") || msg.includes(\"100032\") || msg.toLowerCase().includes(\"captcha\")) {\n\t\t\t\tconst cacheFile = this.getAuthCacheFile();\n\t\t\t\tconst missing = !fs.existsSync(cacheFile);\n\t\t\t\tconst hint = missing\n\t\t\t\t\t? `Erwartete Datei: ${cacheFile} \u2013 von funktionierender Anker/Solix-Integration (z. B. ha-anker-solix) dorthin kopieren, Ordner anlegen falls n\u00F6tig, Adapter neu starten.`\n\t\t\t\t\t: `Cache vorhanden aber ung\u00FCltig: ${cacheFile} \u2013 frische Datei von HA kopieren oder Passwort in Admin neu speichern.`;\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 API-Neulogin n\u00F6tig${missing ? \" (kein Login-Cache)\" : \"\"}. ${hint}`,\n\t\t\t\t);\n\t\t\t\tif (missing) {\n\t\t\t\t\tthis.logAuthCacheStatus();\n\t\t\t\t}\n\t\t\t} else if (msg.includes(\"Cached Anker login is invalid\") || msg.includes(\"invalidated by the mobile app\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Gespeicherter API-Token ung\u00FCltig (abgelaufen oder durch App ersetzt). ` +\n\t\t\t\t\t\t\"Nicht \u201ECache l\u00F6schen\u201C \u2013 stattdessen frische authcache-Datei von HA kopieren oder App kurz abmelden, dann neu starten.\",\n\t\t\t\t);\n\t\t\t} else if (msg.includes(\"InvalidCredentials\") || msg.includes(\"Authentication failed\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Check e-mail, password and country (${this.config.country || \"DE\"}). ` +\n\t\t\t\t\t\t\"In Admin use \u201CInstall Python dependencies\u201D tab or restart after saving config; \" +\n\t\t\t\t\t\t\"try country matching your Anker account region.\",\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tmsg.includes(\"26161\") ||\n\t\t\t\tmsg.includes(\"429\") ||\n\t\t\t\tmsg.includes(\"Too Many Requests\") ||\n\t\t\t\tmsg.includes(\"Failed to request\")\n\t\t\t) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`Poll failed (Anker API limit or temporary error): ${msg} \u2013 ` +\n\t\t\t\t\t\t\"adapter will retry; increase scan interval (e.g. 120 s) if this persists.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.log.error(`Poll failed: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getPrimaryDeviceId(): string {\n\t\tconst selected = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\t\tif (selected[0]) {\n\t\t\treturn selected[0];\n\t\t}\n\t\ttry {\n\t\t\tconst list = JSON.parse(this.config.deviceListJson || '{\"devices\":[]}') as {\n\t\t\t\tdevices?: Array<{ id: string }>;\n\t\t\t};\n\t\t\treturn list.devices?.[0]?.id || \"\";\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\tprivate async handleServiceTrigger(stateId: string): Promise<void> {\n\t\tconst ns = `${this.namespace}.services.`;\n\t\tif (!stateId.startsWith(ns)) {\n\t\t\treturn;\n\t\t}\n\t\tconst action = stateId.slice(ns.length);\n\n\t\tif (action === \"refresh_devices\") {\n\t\t\tawait this.pollOnce();\n\t\t\tawait this.setState(stateId, false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst serviceActions = [\"get_schedule\", \"clear_schedule\", \"export_systems\", \"get_system_info\"];\n\t\tif (!serviceActions.includes(action)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst params: Record<string, unknown> = {\n\t\t\tdeviceId: this.getPrimaryDeviceId(),\n\t\t\tsiteId: this.config.selectedSiteId || \"\",\n\t\t\tincludeMqtt: this.config.mqttUsage !== false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst serviceConfig: BridgeServiceConfig = {\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tservice: action,\n\t\t\t\tparams,\n\t\t\t};\n\t\t\tconst result = await runBridge(\"service\", serviceConfig, this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (action === \"get_schedule\" && result.schedule !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.scheduleJson, JSON.stringify(result.schedule, null, 2), true);\n\t\t\t}\n\t\t\tif (action === \"export_systems\" && result.path) {\n\t\t\t\tawait this.setState(SERVICE_STATES.exportResult, String(result.path), true);\n\t\t\t}\n\t\t\tif (action === \"get_system_info\" && result.system !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.systemInfo, JSON.stringify(result.system, null, 2), true);\n\t\t\t}\n\n\t\t\tawait this.setState(stateId, false, true);\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Service ${action} failed: ${(error as Error).message}`);\n\t\t\tawait this.setState(stateId, false, true);\n\t\t}\n\t}\n\n\tprivate rememberDeviceContexts(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tconst info = device.info;\n\t\t\tthis.deviceContexts.set(info.id, {\n\t\t\t\ttype: info.type,\n\t\t\t\tsite_id: info.site_id,\n\t\t\t\tdevice_pn: info.device_pn || info.model || \"\",\n\t\t\t\tstation_sn: info.station_sn || \"\",\n\t\t\t\tgeneration: info.generation ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async applyAdapterControl(\n\t\tdeviceId: string,\n\t\tcontrol: string,\n\t\tvalue: string | number | boolean,\n\t\tdeviceContext?: DeviceControlContext,\n\t): Promise<void> {\n\t\tawait runBridge(\n\t\t\t\"set\",\n\t\t\t{\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tdeviceId,\n\t\t\t\tcontrol,\n\t\t\t\tvalue,\n\t\t\t\tdeviceContext,\n\t\t\t},\n\t\t\tthis.config.pythonPath || \"\",\n\t\t\tthis.log,\n\t\t);\n\t}\n\n\tprivate rememberDeviceEntities(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tthis.deviceEntities.set(device.info.id, device.entities);\n\t\t}\n\t}\n\n\tprivate getCurtailmentConfig(): CurtailmentRunnerConfig {\n\t\tconst modeAfter = this.config.curtailmentModeAfter === \"smart\" ? \"smart\" : \"smartmeter\";\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\tforecastBasePath: (this.config.curtailmentForecastPath || \"solarprognose.0.forecast.00.hourly\").trim(),\n\t\t\tmodeAfter,\n\t\t\tcurtailmentHasCombiner: this.config.curtailmentHasCombiner,\n\t\t\tcurtailmentStandaloneDeviceId: this.config.curtailmentStandaloneDeviceId,\n\t\t\tcurtailmentStandaloneProfile: this.config.curtailmentStandaloneProfile,\n\t\t\tcurtailmentStandaloneBatteryWh: this.config.curtailmentStandaloneBatteryWh,\n\t\t\tcurtailmentCombinerDeviceId: this.config.curtailmentCombinerDeviceId,\n\t\t\tcurtailmentCombinerBatteryWh: this.config.curtailmentCombinerBatteryWh,\n\t\t\tcurtailmentCombinerUnit1: this.config.curtailmentCombinerUnit1,\n\t\t\tcurtailmentCombinerUnit2: this.config.curtailmentCombinerUnit2,\n\t\t\tcurtailmentCombinerUnit3: this.config.curtailmentCombinerUnit3,\n\t\t\tcurtailmentCombinerUnit4: this.config.curtailmentCombinerUnit4,\n\t\t\tcurtailmentDevicesJson: this.config.curtailmentDevicesJson,\n\t\t};\n\t}\n\n\tprivate getCurtailmentHost(): CurtailmentRunnerHost {\n\t\treturn {\n\t\t\tnamespace: this.namespace,\n\t\t\tlog: this.log,\n\t\t\tgetForeignStateAsync: id => this.getForeignStateAsync(id),\n\t\t\tgetForeignObjectAsync: id => this.getForeignObjectAsync(id),\n\t\t\tgetStateAsync: id => this.getStateAsync(id),\n\t\t\tgetDeviceEntities: deviceId => this.deviceEntities.get(deviceId),\n\t\t\tgetDeviceSiteId: deviceId => this.deviceContexts.get(deviceId)?.site_id,\n\t\t\tgetDeviceWritable: deviceId => this.deviceWritable.get(deviceId),\n\t\t\tsetState: async (id, val, ack) => {\n\t\t\t\tawait this.setState(id, val as ioBroker.StateValue, ack ?? true);\n\t\t\t},\n\t\t\tgetDeviceContext: deviceId => this.deviceContexts.get(deviceId),\n\t\t\tapplyControl: (deviceId, control, value, deviceContext) =>\n\t\t\t\tthis.applyAdapterControl(deviceId, control, value, deviceContext),\n\t\t};\n\t}\n\n\tprivate refreshCurtailmentDeviceIds(): void {\n\t\tthis.curtailmentDeviceIds.clear();\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const d of resolveCurtailmentDevices(this.getCurtailmentConfig())) {\n\t\t\tif (d.enabled) {\n\t\t\t\tthis.curtailmentDeviceIds.add(d.deviceId);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleCurtailmentPvUpdated(deviceId: string, livePvW: number): void {\n\t\tif (!this.config.enableCurtailmentAvoidance || !this.curtailmentDeviceIds.has(deviceId)) {\n\t\t\treturn;\n\t\t}\n\t\tconst rounded = Math.round(livePvW);\n\t\tif (this.lastNotifiedPvW.get(deviceId) === rounded) {\n\t\t\treturn;\n\t\t}\n\t\tthis.lastNotifiedPvW.set(deviceId, rounded);\n\t\tvoid this.runCurtailmentExportOnPvChange(deviceId, rounded);\n\t}\n\n\tprivate handleCurtailmentSystemPvUpdated(siteId: string, livePvW: number): void {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tconst rounded = Math.round(livePvW);\n\t\tfor (const deviceId of this.curtailmentDeviceIds) {\n\t\t\tconst ctx = this.deviceContexts.get(deviceId);\n\t\t\tif (ctx?.site_id !== siteId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (this.lastNotifiedPvW.get(deviceId) === rounded) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.lastNotifiedPvW.set(deviceId, rounded);\n\t\t\tvoid this.runCurtailmentExportOnPvChange(deviceId, rounded);\n\t\t}\n\t}\n\n\tprivate async runCurtailmentExportOnPvChange(deviceId: string, livePvW: number): Promise<void> {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait runCurtailmentOnPvChange(this.getCurtailmentHost(), this.getCurtailmentConfig(), deviceId, livePvW);\n\t\t} catch (err) {\n\t\t\tthis.log.debug(`Curtailment PV follow: ${(err as Error).message}`);\n\t\t}\n\t}\n\n\tprivate subscribeCurtailmentPvStates(): void {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tconst ns = this.namespace;\n\t\tthis.subscribeStates(`${ns}.system.*.sensors.total_pv_power`);\n\t\tfor (const channel of [\"solarbank\", \"combiner_box\"]) {\n\t\t\tthis.subscribeStates(`${ns}.${channel}.*.sensors.total_pv_power`);\n\t\t\tthis.subscribeStates(`${ns}.${channel}.*.sensors.input_power`);\n\t\t}\n\t}\n\n\tprivate async runCurtailmentAvoidanceIfEnabled(): Promise<void> {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tthis.refreshCurtailmentDeviceIds();\n\t\ttry {\n\t\t\tawait runCurtailmentAvoidance(this.getCurtailmentHost(), this.getCurtailmentConfig());\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Curtailment avoidance: ${(err as Error).message}`);\n\t\t}\n\t}\n\n\tprivate schedulePollAfterControl(): void {\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t}\n\t\tthis.pollAfterControlTimer = setTimeout(() => {\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t\tvoid this.pollOnce();\n\t\t}, 12_000);\n\t}\n\n\tprivate async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n\t\tif (!state) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.enableCurtailmentAvoidance) {\n\t\t\tconst n = Number(state.val);\n\t\t\tif (Number.isFinite(n) && n >= 0) {\n\t\t\t\tconst systemPv = parseSystemPvStateId(this.namespace, id);\n\t\t\t\tif (systemPv) {\n\t\t\t\t\tthis.handleCurtailmentSystemPvUpdated(systemPv.siteId, n);\n\t\t\t\t} else {\n\t\t\t\t\tconst pv = parsePvSensorStateId(this.namespace, id);\n\t\t\t\t\tif (pv) {\n\t\t\t\t\t\tthis.handleCurtailmentPvUpdated(pv.deviceId, n);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (state.ack) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (id.startsWith(`${this.namespace}.services.`) && state.val === true) {\n\t\t\tawait this.handleServiceTrigger(id);\n\t\t\treturn;\n\t\t}\n\n\t\tconst control = parseControlStateId(this.namespace, id);\n\t\tif (!control) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst current = await this.getStateAsync(id);\n\t\tif (\n\t\t\tcurrent?.ack &&\n\t\t\tcurrent.val !== null &&\n\t\t\tcurrent.val !== undefined &&\n\t\t\tString(current.val) === String(state.val)\n\t\t) {\n\t\t\tawait this.setState(id, { val: state.val, ack: true });\n\t\t\treturn;\n\t\t}\n\n\t\tconst value = state.val as string | number | boolean;\n\t\tconst deviceContext = this.deviceContexts.get(control.deviceId);\n\n\t\tthis.controlQueue.enqueue({\n\t\t\tstateId: id,\n\t\t\texecute: async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runBridge(\n\t\t\t\t\t\t\"set\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\t\t\t\tdeviceId: control.deviceId,\n\t\t\t\t\t\t\tcontrol: control.control,\n\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\tdeviceContext,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\t\tthis.log,\n\t\t\t\t\t);\n\t\t\t\t\tawait this.setState(id, { val: value, ack: true });\n\t\t\t\t\tthis.log.info(`Applied ${control.control} on ${control.deviceId}`);\n\t\t\t\t\tthis.schedulePollAfterControl();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = (error as Error).message;\n\t\t\t\t\tif (message.includes(\"429\") || message.includes(\"Too Many Requests\")) {\n\t\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t\t`Control rate-limited for ${id} \u2013 wait ~1 minute before retrying (Anker API limit).`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.error(`Control failed for ${id}: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.setState(id, { val: value, ack: false });\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj?.command) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst respond = (response: unknown): void => {\n\t\t\tif (obj.callback) {\n\t\t\t\tthis.sendTo(obj.from, obj.command, response, obj.callback);\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tif (obj.command === \"clearAuthCache\") {\n\t\t\t\tconst cacheDir = this.getAuthCacheDir();\n\t\t\t\tconst fs = await import(\"node:fs/promises\");\n\t\t\t\ttry {\n\t\t\t\t\tconst files = await fs.readdir(cacheDir);\n\t\t\t\t\tawait Promise.all(files.map(f => fs.unlink(path.join(cacheDir, f)).catch(() => undefined)));\n\t\t\t\t\tawait stopBridgeDaemon();\n\t\t\t\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache cleared (${files.length} file(s) in ${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Next poll requires a new API login; on many hosts Anker returns captcha (100032). \" +\n\t\t\t\t\t\t\t`Restore authcache/${(this.config.username || \"\").trim()}.json from HA or retry login when cloud allows it.`,\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: files.length });\n\t\t\t\t} catch {\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache clear requested but folder empty or missing (${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Adapter must complete a successful API login to create authcache/<email>.json.\",\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: 0 });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"installPython\") {\n\t\t\t\tconst ok = await this.ensurePythonDeps(true);\n\t\t\t\trespond({ ok });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"loadDevices\") {\n\t\t\t\tif (!this.config.username || !this.config.password) {\n\t\t\t\t\trespond({ error: \"Credentials required\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.ensurePythonDeps();\n\t\t\t\tconst result = await runBridge(\n\t\t\t\t\t\"list_devices\",\n\t\t\t\t\tthis.getBridgeConfig(),\n\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\tthis.log,\n\t\t\t\t);\n\t\t\t\tconst payload = {\n\t\t\t\t\tsites: result.sites || [],\n\t\t\t\t\tdevices: result.devices || [],\n\t\t\t\t};\n\t\t\t\trespond({ ok: true, deviceListJson: JSON.stringify(payload, null, 2), ...payload });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trespond({ error: `Unknown command ${obj.command}` });\n\t\t} catch (error) {\n\t\t\trespond({ error: (error as Error).message });\n\t\t}\n\t}\n\n\tprivate async onReady(): Promise<void> {\n\t\tthis.cleanupLegacyInstallSymlink();\n\n\t\tawait this.setObjectNotExistsAsync(\"account\", {\n\t\t\ttype: \"channel\",\n\t\t\tcommon: { name: \"Account\" },\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"account.nickname\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Account nickname\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"info\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"info.pythonReady\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Python dependencies ready\",\n\t\t\t\ttype: \"boolean\",\n\t\t\t\trole: \"indicator\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\n\t\tawait setupServiceStates(this);\n\t\tawait setupCurtailmentStates(this);\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentPvUpdated = (deviceId, livePvW) =>\n\t\t\tthis.handleCurtailmentPvUpdated(deviceId, livePvW);\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentSystemPvUpdated = (siteId, livePvW) =>\n\t\t\tthis.handleCurtailmentSystemPvUpdated(siteId, livePvW);\n\t\tthis.refreshCurtailmentDeviceIds();\n\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\tconst intervalSec = Math.max(30, Number(this.config.scanInterval) || 60);\n\t\tthis.log.info(\n\t\t\t`Anker Solix adapter started (poll every ${intervalSec}s, MQTT: ${this.config.mqttUsage !== false})`,\n\t\t);\n\n\t\tawait this.ensurePythonDeps();\n\n\t\tthis.logAuthCacheStatus();\n\n\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\tthis.subscribeStates(`${this.namespace}.*.control.*`);\n\t\tthis.subscribeStates(`${this.namespace}.services.*`);\n\t\tthis.subscribeCurtailmentPvStates();\n\n\t\tawait this.pollOnce();\n\t\tthis.pollTimer = this.setInterval(() => {\n\t\t\tvoid this.pollOnce();\n\t\t}, intervalSec * 1000);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tif (this.pollTimer) {\n\t\t\tthis.clearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t}\n\t\tthis.lastNotifiedPvW.clear();\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentPvUpdated = undefined;\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentSystemPvUpdated = undefined;\n\t\tvoid stopBridgeDaemon().finally(() => callback());\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AnkerSolix(options);\n} else {\n\t(() => new AnkerSolix())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/*\n * ioBroker Anker Solix adapter\n * Based on https://github.com/thomluther/ha-anker-solix (Home Assistant integration)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport * as utils from \"@iobroker/adapter-core\";\n\nimport { parseSelectedDeviceIds } from \"./lib/configHelpers\";\nimport { ControlQueue } from \"./lib/controlQueue\";\nimport { parsePvSensorStateId, parseSystemPvStateId } from \"./lib/curtailmentPower\";\nimport {\n\trunCurtailmentAvoidance,\n\trunCurtailmentOnPvChange,\n\ttype CurtailmentRunnerConfig,\n\ttype CurtailmentRunnerHost,\n} from \"./lib/curtailmentRunner\";\nimport { setupCurtailmentStates } from \"./lib/curtailmentStates\";\nimport { runPythonInstaller } from \"./lib/ensurePython\";\nimport { ensureBridgeDaemon, runBridge, stopBridgeDaemon } from \"./lib/pythonBridge\";\nimport { SERVICE_STATES, setupServiceStates } from \"./lib/services\";\nimport { resolveCurtailmentDevices } from \"./lib/curtailmentConfig\";\nimport { parseControlStateId, syncDevices, type CurtailmentPvSyncHost } from \"./lib/stateSync\";\nimport type { BridgeConfig, BridgeDevice, BridgeServiceConfig, DeviceControlContext } from \"./lib/types\";\n\nclass AnkerSolix extends utils.Adapter {\n\tprivate pollTimer: ioBroker.Interval | undefined;\n\tprivate readonly controlQueue = new ControlQueue();\n\tprivate readonly deviceContexts = new Map<string, DeviceControlContext>();\n\tprivate readonly deviceEntities = new Map<string, Record<string, unknown>>();\n\tprivate readonly deviceWritable = new Map<string, string[]>();\n\tprivate readonly lastNotifiedPvW = new Map<string, number>();\n\tprivate readonly curtailmentDeviceIds = new Set<string>();\n\tprivate pollAfterControlTimer: NodeJS.Timeout | undefined;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"anker-solix\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate getAuthCacheDir(): string {\n\t\treturn path.join(utils.getAbsoluteInstanceDataDir(this), \"authcache\");\n\t}\n\n\tprivate getAuthCacheFile(): string {\n\t\tconst email = (this.config.username || \"\").trim();\n\t\treturn path.join(this.getAuthCacheDir(), `${email}.json`);\n\t}\n\n\tprivate logAuthCacheStatus(): void {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst cacheFile = this.getAuthCacheFile();\n\t\tconst email = (this.config.username || \"\").trim();\n\t\tif (!email) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tfs.mkdirSync(cacheDir, { recursive: true });\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Cannot create authcache folder ${cacheDir}: ${(err as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (fs.existsSync(cacheFile)) {\n\t\t\tthis.log.debug(`Anker login cache present: ${cacheFile}`);\n\t\t\treturn;\n\t\t}\n\t\tlet other = \"\";\n\t\ttry {\n\t\t\tconst names = fs.readdirSync(cacheDir).filter(f => f.endsWith(\".json\"));\n\t\t\tif (names.length) {\n\t\t\t\tother = ` Found other file(s) in folder: ${names.join(\", \")} (username must match filename).`;\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t\tthis.log.warn(\n\t\t\t`No Anker login cache at ${cacheFile}.${other} ` +\n\t\t\t\t\"Without this file every adapter restart triggers a new API login (often captcha 100032). \" +\n\t\t\t\t\"Copy <email>.json from a working Anker/Solix integration (e.g. ha-anker-solix) into that folder, then restart.\",\n\t\t);\n\t}\n\n\tprivate getBridgeConfig(): BridgeConfig {\n\t\tconst cacheDir = this.getAuthCacheDir();\n\t\tconst selectedIds = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\n\t\treturn {\n\t\t\tusername: this.config.username,\n\t\t\tpassword: this.config.password,\n\t\t\tcountry: this.config.country || \"DE\",\n\t\t\tmqttUsage: this.config.mqttUsage !== false,\n\t\t\tcacheDir,\n\t\t\tenableAllDevices: this.config.enableAllDevices !== false,\n\t\t\tselectedSiteId: this.config.selectedSiteId || \"\",\n\t\t\tselectedDeviceIds: selectedIds,\n\t\t\tdeviceDetailMultiplier: Math.max(1, Number(this.config.deviceDetailMultiplier) || 10),\n\t\t\trequestDelay: Number(this.config.requestDelay) || 0.3,\n\t\t\trequestTimeout: Number(this.config.requestTimeout) || 10,\n\t\t\tendpointLimit: Number(this.config.endpointLimit) || 10,\n\t\t\tenableCoreEntities: this.config.enableCoreEntities !== false,\n\t\t\tenableEnergyStatistics: !!this.config.enableEnergyStatistics,\n\t\t\tenableEnergyDetail: !!this.config.enableEnergyDetail,\n\t\t\tenablePowerFlows: !!this.config.enablePowerFlows,\n\t\t\tenableDiagnostics: !!this.config.enableDiagnostics,\n\t\t\tenableBinaryIndicators: !!this.config.enableBinaryIndicators,\n\t\t\tenableAdvancedControls: !!this.config.enableAdvancedControls,\n\t\t\tenableSystemOverview: !!this.config.enableSystemOverview,\n\t\t\tenableSitePrice: !!this.config.enableSitePrice,\n\t\t\tenableAccountInfo: !!this.config.enableAccountInfo,\n\t\t\tenableSolarbankMeta: !!this.config.enableSolarbankMeta,\n\t\t\tenableSmartplug: !!this.config.enableSmartplug,\n\t\t\tenablePps: !!this.config.enablePps,\n\t\t\tenableEvCharger: !!this.config.enableEvCharger,\n\t\t\tenableVehicle: !!this.config.enableVehicle,\n\t\t\tenableHes: !!this.config.enableHes,\n\t\t\tenablePowerPanel: !!this.config.enablePowerPanel,\n\t\t\tenableInverter: !!this.config.enableInverter,\n\t\t};\n\t}\n\n\t/** Remove legacy install symlink from old GitHub repo name \"AnkerSolix\". */\n\tprivate cleanupLegacyInstallSymlink(): void {\n\t\tconst alias = path.join(this.adapterDir, \"..\", \"iobroker.AnkerSolix\");\n\t\ttry {\n\t\t\tif (fs.existsSync(alias) && fs.lstatSync(alias).isSymbolicLink()) {\n\t\t\t\tfs.unlinkSync(alias);\n\t\t\t\tthis.log.info(\"Removed legacy symlink iobroker.AnkerSolix\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tprivate async ensurePythonDeps(force = false): Promise<boolean> {\n\t\tif (!force && this.config.autoInstallPython === false) {\n\t\t\treturn true;\n\t\t}\n\t\tconst result = await runPythonInstaller(this.config.pythonPath || \"\", this.log);\n\t\tawait this.setState(\"info.pythonReady\", result.ok, true);\n\t\tif (!result.ok) {\n\t\t\tthis.log.warn(`Python setup: ${result.message}`);\n\t\t}\n\t\treturn result.ok;\n\t}\n\n\tprivate async pollOnce(): Promise<void> {\n\t\tif (!this.config.acceptTerms) {\n\t\t\tthis.log.warn(\"Please accept the usage terms in the adapter configuration.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.config.username?.trim()) {\n\t\t\tthis.log.warn(\"Anker e-mail (username) is required in adapter settings.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\t\tif (!this.config.password?.trim()) {\n\t\t\tthis.log.warn(\"Password missing \u2013 open instance config in Admin, re-enter Anker password and save.\");\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!(await this.ensurePythonDeps())) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await runBridge(\"poll\", this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (this.config.enableCurtailmentAvoidance) {\n\t\t\t\tthis.refreshCurtailmentDeviceIds();\n\t\t\t}\n\n\t\t\tconst pollDevices = result.devices as BridgeDevice[] | undefined;\n\t\t\tif (pollDevices?.length) {\n\t\t\t\tthis.rememberDeviceContexts(pollDevices);\n\t\t\t\tthis.rememberDeviceEntities(pollDevices);\n\t\t\t\tawait syncDevices(this, pollDevices);\n\t\t\t}\n\n\t\t\tif (result.nickname) {\n\t\t\t\tawait this.setState(\"account.nickname\", result.nickname, true);\n\t\t\t}\n\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t\tconst detailHint = result.refreshDetails ? \"devices+mqtt\" : \"sites\";\n\t\t\tconst intervalHint =\n\t\t\t\tresult.intervalcount !== undefined && result.deviceintervals !== undefined\n\t\t\t\t\t? `, next detail in ~${result.intervalcount} polls`\n\t\t\t\t\t: \"\";\n\t\t\tthis.log.debug(`Poll OK (${pollDevices?.length ?? 0} devices, ${detailHint}${intervalHint})`);\n\n\t\t\tawait this.runCurtailmentAvoidanceIfEnabled();\n\t\t} catch (error) {\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\t\t\tconst msg = (error as Error).message || String(error);\n\t\t\tif (msg.includes(\"CaptchaRequired\") || msg.includes(\"100032\") || msg.toLowerCase().includes(\"captcha\")) {\n\t\t\t\tconst cacheFile = this.getAuthCacheFile();\n\t\t\t\tconst missing = !fs.existsSync(cacheFile);\n\t\t\t\tconst hint = missing\n\t\t\t\t\t? `Erwartete Datei: ${cacheFile} \u2013 von funktionierender Anker/Solix-Integration (z. B. ha-anker-solix) dorthin kopieren, Ordner anlegen falls n\u00F6tig, Adapter neu starten.`\n\t\t\t\t\t: `Cache vorhanden aber ung\u00FCltig: ${cacheFile} \u2013 frische Datei von HA kopieren oder Passwort in Admin neu speichern.`;\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 API-Neulogin n\u00F6tig${missing ? \" (kein Login-Cache)\" : \"\"}. ${hint}`,\n\t\t\t\t);\n\t\t\t\tif (missing) {\n\t\t\t\t\tthis.logAuthCacheStatus();\n\t\t\t\t}\n\t\t\t} else if (msg.includes(\"Cached Anker login is invalid\") || msg.includes(\"invalidated by the mobile app\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Gespeicherter API-Token ung\u00FCltig (abgelaufen oder durch App ersetzt). ` +\n\t\t\t\t\t\t\"Nicht \u201ECache l\u00F6schen\u201C \u2013 stattdessen frische authcache-Datei von HA kopieren oder App kurz abmelden, dann neu starten.\",\n\t\t\t\t);\n\t\t\t} else if (msg.includes(\"InvalidCredentials\") || msg.includes(\"Authentication failed\")) {\n\t\t\t\tthis.log.error(\n\t\t\t\t\t`Poll failed: ${msg} \u2013 Check e-mail, password and country (${this.config.country || \"DE\"}). ` +\n\t\t\t\t\t\t\"In Admin use \u201CInstall Python dependencies\u201D tab or restart after saving config; \" +\n\t\t\t\t\t\t\"try country matching your Anker account region.\",\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tmsg.includes(\"26161\") ||\n\t\t\t\tmsg.includes(\"429\") ||\n\t\t\t\tmsg.includes(\"Too Many Requests\") ||\n\t\t\t\tmsg.includes(\"Failed to request\")\n\t\t\t) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`Poll failed (Anker API limit or temporary error): ${msg} \u2013 ` +\n\t\t\t\t\t\t\"adapter will retry; increase scan interval (e.g. 120 s) if this persists.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.log.error(`Poll failed: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate getPrimaryDeviceId(): string {\n\t\tconst selected = parseSelectedDeviceIds(this.config.selectedDeviceIds);\n\t\tif (selected[0]) {\n\t\t\treturn selected[0];\n\t\t}\n\t\ttry {\n\t\t\tconst list = JSON.parse(this.config.deviceListJson || '{\"devices\":[]}') as {\n\t\t\t\tdevices?: Array<{ id: string }>;\n\t\t\t};\n\t\t\treturn list.devices?.[0]?.id || \"\";\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\tprivate async handleServiceTrigger(stateId: string): Promise<void> {\n\t\tconst ns = `${this.namespace}.services.`;\n\t\tif (!stateId.startsWith(ns)) {\n\t\t\treturn;\n\t\t}\n\t\tconst action = stateId.slice(ns.length);\n\n\t\tif (action === \"refresh_devices\") {\n\t\t\tawait this.pollOnce();\n\t\t\tawait this.setState(stateId, false, true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst serviceActions = [\"get_schedule\", \"clear_schedule\", \"export_systems\", \"get_system_info\"];\n\t\tif (!serviceActions.includes(action)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst params: Record<string, unknown> = {\n\t\t\tdeviceId: this.getPrimaryDeviceId(),\n\t\t\tsiteId: this.config.selectedSiteId || \"\",\n\t\t\tincludeMqtt: this.config.mqttUsage !== false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst serviceConfig: BridgeServiceConfig = {\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tservice: action,\n\t\t\t\tparams,\n\t\t\t};\n\t\t\tconst result = await runBridge(\"service\", serviceConfig, this.config.pythonPath || \"\", this.log);\n\n\t\t\tif (action === \"get_schedule\" && result.schedule !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.scheduleJson, JSON.stringify(result.schedule, null, 2), true);\n\t\t\t}\n\t\t\tif (action === \"export_systems\" && result.path) {\n\t\t\t\tawait this.setState(SERVICE_STATES.exportResult, String(result.path), true);\n\t\t\t}\n\t\t\tif (action === \"get_system_info\" && result.system !== undefined) {\n\t\t\t\tawait this.setState(SERVICE_STATES.systemInfo, JSON.stringify(result.system, null, 2), true);\n\t\t\t}\n\n\t\t\tawait this.setState(stateId, false, true);\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Service ${action} failed: ${(error as Error).message}`);\n\t\t\tawait this.setState(stateId, false, true);\n\t\t}\n\t}\n\n\tprivate rememberDeviceContexts(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tconst info = device.info;\n\t\t\tthis.deviceContexts.set(info.id, {\n\t\t\t\ttype: info.type,\n\t\t\t\tsite_id: info.site_id,\n\t\t\t\tdevice_pn: info.device_pn || info.model || \"\",\n\t\t\t\tstation_sn: info.station_sn || \"\",\n\t\t\t\tgeneration: info.generation ?? 0,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate async applyAdapterControl(\n\t\tdeviceId: string,\n\t\tcontrol: string,\n\t\tvalue: string | number | boolean,\n\t\tdeviceContext?: DeviceControlContext,\n\t\topts?: { acOutputApiOnly?: boolean },\n\t): Promise<void> {\n\t\tawait runBridge(\n\t\t\t\"set\",\n\t\t\t{\n\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\tdeviceId,\n\t\t\t\tcontrol,\n\t\t\t\tvalue,\n\t\t\t\tdeviceContext,\n\t\t\t\tacOutputApiOnly: opts?.acOutputApiOnly,\n\t\t\t},\n\t\t\tthis.config.pythonPath || \"\",\n\t\t\tthis.log,\n\t\t);\n\t}\n\n\tprivate rememberDeviceEntities(devices: BridgeDevice[]): void {\n\t\tfor (const device of devices) {\n\t\t\tthis.deviceEntities.set(device.info.id, device.entities);\n\t\t}\n\t}\n\n\tprivate getCurtailmentConfig(): CurtailmentRunnerConfig {\n\t\tconst modeAfter = this.config.curtailmentModeAfter === \"smart\" ? \"smart\" : \"smartmeter\";\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\tforecastBasePath: (this.config.curtailmentForecastPath || \"solarprognose.0.forecast.00.hourly\").trim(),\n\t\t\tmodeAfter,\n\t\t\tcurtailmentHasCombiner: this.config.curtailmentHasCombiner,\n\t\t\tcurtailmentStandaloneDeviceId: this.config.curtailmentStandaloneDeviceId,\n\t\t\tcurtailmentStandaloneProfile: this.config.curtailmentStandaloneProfile,\n\t\t\tcurtailmentStandaloneBatteryWh: this.config.curtailmentStandaloneBatteryWh,\n\t\t\tcurtailmentCombinerDeviceId: this.config.curtailmentCombinerDeviceId,\n\t\t\tcurtailmentCombinerBatteryWh: this.config.curtailmentCombinerBatteryWh,\n\t\t\tcurtailmentCombinerUnit1: this.config.curtailmentCombinerUnit1,\n\t\t\tcurtailmentCombinerUnit2: this.config.curtailmentCombinerUnit2,\n\t\t\tcurtailmentCombinerUnit3: this.config.curtailmentCombinerUnit3,\n\t\t\tcurtailmentCombinerUnit4: this.config.curtailmentCombinerUnit4,\n\t\t\tcurtailmentDevicesJson: this.config.curtailmentDevicesJson,\n\t\t};\n\t}\n\n\tprivate getCurtailmentHost(): CurtailmentRunnerHost {\n\t\treturn {\n\t\t\tnamespace: this.namespace,\n\t\t\tlog: this.log,\n\t\t\tgetForeignStateAsync: id => this.getForeignStateAsync(id),\n\t\t\tgetForeignObjectAsync: id => this.getForeignObjectAsync(id),\n\t\t\tgetStateAsync: id => this.getStateAsync(id),\n\t\t\tgetDeviceEntities: deviceId => this.deviceEntities.get(deviceId),\n\t\t\tgetDeviceSiteId: deviceId => this.deviceContexts.get(deviceId)?.site_id,\n\t\t\tgetDeviceWritable: deviceId => this.deviceWritable.get(deviceId),\n\t\t\tsetState: async (id, val, ack) => {\n\t\t\t\tawait this.setState(id, val as ioBroker.StateValue, ack ?? true);\n\t\t\t},\n\t\t\tgetDeviceContext: deviceId => this.deviceContexts.get(deviceId),\n\t\t\tapplyControl: (deviceId, control, value, deviceContext, opts) =>\n\t\t\t\tthis.applyAdapterControl(deviceId, control, value, deviceContext, opts),\n\t\t};\n\t}\n\n\tprivate refreshCurtailmentDeviceIds(): void {\n\t\tthis.curtailmentDeviceIds.clear();\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const d of resolveCurtailmentDevices(this.getCurtailmentConfig())) {\n\t\t\tif (d.enabled) {\n\t\t\t\tthis.curtailmentDeviceIds.add(d.deviceId);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleCurtailmentPvUpdated(deviceId: string, livePvW: number): void {\n\t\tif (!this.config.enableCurtailmentAvoidance || !this.curtailmentDeviceIds.has(deviceId)) {\n\t\t\treturn;\n\t\t}\n\t\tconst rounded = Math.round(livePvW);\n\t\tif (this.lastNotifiedPvW.get(deviceId) === rounded) {\n\t\t\treturn;\n\t\t}\n\t\tthis.lastNotifiedPvW.set(deviceId, rounded);\n\t\tvoid this.runCurtailmentExportOnPvChange(deviceId, rounded);\n\t}\n\n\tprivate handleCurtailmentSystemPvUpdated(siteId: string, livePvW: number): void {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tconst rounded = Math.round(livePvW);\n\t\tfor (const deviceId of this.curtailmentDeviceIds) {\n\t\t\tconst ctx = this.deviceContexts.get(deviceId);\n\t\t\tif (ctx?.site_id !== siteId) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (this.lastNotifiedPvW.get(deviceId) === rounded) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.lastNotifiedPvW.set(deviceId, rounded);\n\t\t\tvoid this.runCurtailmentExportOnPvChange(deviceId, rounded);\n\t\t}\n\t}\n\n\tprivate async runCurtailmentExportOnPvChange(deviceId: string, livePvW: number): Promise<void> {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait runCurtailmentOnPvChange(this.getCurtailmentHost(), this.getCurtailmentConfig(), deviceId, livePvW);\n\t\t} catch (err) {\n\t\t\tthis.log.debug(`Curtailment PV follow: ${(err as Error).message}`);\n\t\t}\n\t}\n\n\tprivate subscribeCurtailmentPvStates(): void {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tconst ns = this.namespace;\n\t\tthis.subscribeStates(`${ns}.system.*.sensors.total_pv_power`);\n\t\tfor (const channel of [\"solarbank\", \"combiner_box\"]) {\n\t\t\tthis.subscribeStates(`${ns}.${channel}.*.sensors.total_pv_power`);\n\t\t\tthis.subscribeStates(`${ns}.${channel}.*.sensors.input_power`);\n\t\t}\n\t}\n\n\tprivate async runCurtailmentAvoidanceIfEnabled(): Promise<void> {\n\t\tif (!this.config.enableCurtailmentAvoidance) {\n\t\t\treturn;\n\t\t}\n\t\tthis.refreshCurtailmentDeviceIds();\n\t\ttry {\n\t\t\tawait runCurtailmentAvoidance(this.getCurtailmentHost(), this.getCurtailmentConfig());\n\t\t} catch (err) {\n\t\t\tthis.log.warn(`Curtailment avoidance: ${(err as Error).message}`);\n\t\t}\n\t}\n\n\tprivate schedulePollAfterControl(): void {\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t}\n\t\tthis.pollAfterControlTimer = setTimeout(() => {\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t\tvoid this.pollOnce();\n\t\t}, 12_000);\n\t}\n\n\tprivate async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n\t\tif (!state) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.enableCurtailmentAvoidance) {\n\t\t\tconst n = Number(state.val);\n\t\t\tif (Number.isFinite(n) && n >= 0) {\n\t\t\t\tconst systemPv = parseSystemPvStateId(this.namespace, id);\n\t\t\t\tif (systemPv) {\n\t\t\t\t\tthis.handleCurtailmentSystemPvUpdated(systemPv.siteId, n);\n\t\t\t\t} else {\n\t\t\t\t\tconst pv = parsePvSensorStateId(this.namespace, id);\n\t\t\t\t\tif (pv) {\n\t\t\t\t\t\tthis.handleCurtailmentPvUpdated(pv.deviceId, n);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (state.ack) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (id.startsWith(`${this.namespace}.services.`) && state.val === true) {\n\t\t\tawait this.handleServiceTrigger(id);\n\t\t\treturn;\n\t\t}\n\n\t\tconst control = parseControlStateId(this.namespace, id);\n\t\tif (!control) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst current = await this.getStateAsync(id);\n\t\tif (\n\t\t\tcurrent?.ack &&\n\t\t\tcurrent.val !== null &&\n\t\t\tcurrent.val !== undefined &&\n\t\t\tString(current.val) === String(state.val)\n\t\t) {\n\t\t\tawait this.setState(id, { val: state.val, ack: true });\n\t\t\treturn;\n\t\t}\n\n\t\tconst value = state.val as string | number | boolean;\n\t\tconst deviceContext = this.deviceContexts.get(control.deviceId);\n\n\t\tthis.controlQueue.enqueue({\n\t\t\tstateId: id,\n\t\t\texecute: async () => {\n\t\t\t\ttry {\n\t\t\t\t\tawait runBridge(\n\t\t\t\t\t\t\"set\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...this.getBridgeConfig(),\n\t\t\t\t\t\t\tdeviceId: control.deviceId,\n\t\t\t\t\t\t\tcontrol: control.control,\n\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\tdeviceContext,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\t\tthis.log,\n\t\t\t\t\t);\n\t\t\t\t\tawait this.setState(id, { val: value, ack: true });\n\t\t\t\t\tthis.log.info(`Applied ${control.control} on ${control.deviceId}`);\n\t\t\t\t\tthis.schedulePollAfterControl();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = (error as Error).message;\n\t\t\t\t\tif (message.includes(\"429\") || message.includes(\"Too Many Requests\")) {\n\t\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t\t`Control rate-limited for ${id} \u2013 wait ~1 minute before retrying (Anker API limit).`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.error(`Control failed for ${id}: ${message}`);\n\t\t\t\t\t}\n\t\t\t\t\tawait this.setState(id, { val: value, ack: false });\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj?.command) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst respond = (response: unknown): void => {\n\t\t\tif (obj.callback) {\n\t\t\t\tthis.sendTo(obj.from, obj.command, response, obj.callback);\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\tif (obj.command === \"clearAuthCache\") {\n\t\t\t\tconst cacheDir = this.getAuthCacheDir();\n\t\t\t\tconst fs = await import(\"node:fs/promises\");\n\t\t\t\ttry {\n\t\t\t\t\tconst files = await fs.readdir(cacheDir);\n\t\t\t\t\tawait Promise.all(files.map(f => fs.unlink(path.join(cacheDir, f)).catch(() => undefined)));\n\t\t\t\t\tawait stopBridgeDaemon();\n\t\t\t\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache cleared (${files.length} file(s) in ${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Next poll requires a new API login; on many hosts Anker returns captcha (100032). \" +\n\t\t\t\t\t\t\t`Restore authcache/${(this.config.username || \"\").trim()}.json from HA or retry login when cloud allows it.`,\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: files.length });\n\t\t\t\t} catch {\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`Anker login cache clear requested but folder empty or missing (${cacheDir}). ` +\n\t\t\t\t\t\t\t\"Adapter must complete a successful API login to create authcache/<email>.json.\",\n\t\t\t\t\t);\n\t\t\t\t\trespond({ ok: true, cleared: 0 });\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"installPython\") {\n\t\t\t\tconst ok = await this.ensurePythonDeps(true);\n\t\t\t\trespond({ ok });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (obj.command === \"loadDevices\") {\n\t\t\t\tif (!this.config.username || !this.config.password) {\n\t\t\t\t\trespond({ error: \"Credentials required\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait this.ensurePythonDeps();\n\t\t\t\tconst result = await runBridge(\n\t\t\t\t\t\"list_devices\",\n\t\t\t\t\tthis.getBridgeConfig(),\n\t\t\t\t\tthis.config.pythonPath || \"\",\n\t\t\t\t\tthis.log,\n\t\t\t\t);\n\t\t\t\tconst payload = {\n\t\t\t\t\tsites: result.sites || [],\n\t\t\t\t\tdevices: result.devices || [],\n\t\t\t\t};\n\t\t\t\trespond({ ok: true, deviceListJson: JSON.stringify(payload, null, 2), ...payload });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\trespond({ error: `Unknown command ${obj.command}` });\n\t\t} catch (error) {\n\t\t\trespond({ error: (error as Error).message });\n\t\t}\n\t}\n\n\tprivate async onReady(): Promise<void> {\n\t\tthis.cleanupLegacyInstallSymlink();\n\n\t\tawait this.setObjectNotExistsAsync(\"account\", {\n\t\t\ttype: \"channel\",\n\t\t\tcommon: { name: \"Account\" },\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"account.nickname\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Account nickname\",\n\t\t\t\ttype: \"string\",\n\t\t\t\trole: \"info\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\t\tawait this.setObjectNotExistsAsync(\"info.pythonReady\", {\n\t\t\ttype: \"state\",\n\t\t\tcommon: {\n\t\t\t\tname: \"Python dependencies ready\",\n\t\t\t\ttype: \"boolean\",\n\t\t\t\trole: \"indicator\",\n\t\t\t\tread: true,\n\t\t\t\twrite: false,\n\t\t\t\tdef: false,\n\t\t\t},\n\t\t\tnative: {},\n\t\t});\n\n\t\tawait setupServiceStates(this);\n\t\tawait setupCurtailmentStates(this);\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentPvUpdated = (deviceId, livePvW) =>\n\t\t\tthis.handleCurtailmentPvUpdated(deviceId, livePvW);\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentSystemPvUpdated = (siteId, livePvW) =>\n\t\t\tthis.handleCurtailmentSystemPvUpdated(siteId, livePvW);\n\t\tthis.refreshCurtailmentDeviceIds();\n\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\tconst intervalSec = Math.max(30, Number(this.config.scanInterval) || 60);\n\t\tthis.log.info(\n\t\t\t`Anker Solix adapter started (poll every ${intervalSec}s, MQTT: ${this.config.mqttUsage !== false})`,\n\t\t);\n\n\t\tawait this.ensurePythonDeps();\n\n\t\tthis.logAuthCacheStatus();\n\n\t\tawait ensureBridgeDaemon(this.getBridgeConfig(), this.config.pythonPath || \"\", this.log);\n\n\t\tthis.subscribeStates(`${this.namespace}.*.control.*`);\n\t\tthis.subscribeStates(`${this.namespace}.services.*`);\n\t\tthis.subscribeCurtailmentPvStates();\n\n\t\tawait this.pollOnce();\n\t\tthis.pollTimer = this.setInterval(() => {\n\t\t\tvoid this.pollOnce();\n\t\t}, intervalSec * 1000);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tif (this.pollTimer) {\n\t\t\tthis.clearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.pollAfterControlTimer) {\n\t\t\tclearTimeout(this.pollAfterControlTimer);\n\t\t\tthis.pollAfterControlTimer = undefined;\n\t\t}\n\t\tthis.lastNotifiedPvW.clear();\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentPvUpdated = undefined;\n\t\t(this as CurtailmentPvSyncHost).onCurtailmentSystemPvUpdated = undefined;\n\t\tvoid stopBridgeDaemon().finally(() => callback());\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AnkerSolix(options);\n} else {\n\t(() => new AnkerSolix())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["fs"]
7
7
  }
package/io-package.json CHANGED
@@ -1,21 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "anker-solix",
4
- "version": "0.10.13",
4
+ "version": "0.10.15",
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.15": {
8
+ "en": "Curtailment: API-only ac_output_limit; fix SOC/missing_charge_wh; no station feed-in changes",
9
+ "de": "Abregelung: ac_output_limit nur API; SOC/missing_charge_wh korrigiert; kein Einspeise-Limit",
10
+ "ru": "Abregelung: API ac_output_limit",
11
+ "pt": "Abregelung: API ac_output_limit",
12
+ "nl": "Abregelung: API ac_output_limit",
13
+ "fr": "Abregelung: API ac_output_limit",
14
+ "it": "Abregelung: API ac_output_limit",
15
+ "es": "Abregelung: API ac_output_limit",
16
+ "pl": "Abregelung: API ac_output_limit",
17
+ "uk": "Abregelung: API ac_output_limit",
18
+ "zh-cn": "限电 API ac_output_limit"
19
+ },
20
+ "0.10.14": {
21
+ "en": "Curtailment: manual + ac_output_limit only; missing_charge_wh; active export = PV − charge",
22
+ "de": "Abregelung: nur Benutzerdefiniert + ac_output_limit; missing_charge_wh; active: Einspeisung = PV − Laden",
23
+ "ru": "Abregelung: manual + ac_output_limit",
24
+ "pt": "Abregelung: manual + ac_output_limit",
25
+ "nl": "Abregelung: manual + ac_output_limit",
26
+ "fr": "Abregelung: manual + ac_output_limit",
27
+ "it": "Abregelung: manual + ac_output_limit",
28
+ "es": "Abregelung: manual + ac_output_limit",
29
+ "pl": "Abregelung: manual + ac_output_limit",
30
+ "uk": "Abregelung: manual + ac_output_limit",
31
+ "zh-cn": "限电仅 manual + ac_output_limit"
19
32
  },
20
33
  "0.10.12": {
21
34
  "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.15",
4
4
  "description": "ioBroker adapter for Anker Solix (based on ha-anker-solix)",
5
5
  "author": {
6
6
  "name": "MatthiasUlrich1",
package/python/bridge.py CHANGED
@@ -318,6 +318,7 @@ async def _set_ac_output_limit(
318
318
  device: dict,
319
319
  limit: int,
320
320
  ha_client: IoBrokerAnkerApiClient | None = None,
321
+ api_only: bool = False,
321
322
  ) -> Any:
322
323
  """HA max_load / max_load_total: MQTT first, optional API cache sync without get_power_limit."""
323
324
  dev_type = str(device.get("type") or "").lower()
@@ -325,19 +326,20 @@ async def _set_ac_output_limit(
325
326
  multisystem = dev_type == COMBINER or "all_power_limit" in device
326
327
  mqtt_ok = False
327
328
 
328
- if multisystem:
329
- mqtt_ok = await _mqtt_max_load_parallel(
330
- api, site_id, limit, control_sn, ha_client=ha_client
331
- )
332
- elif dev_type == SOLARBANK and not station_sn:
333
- mqtt_ok = await _mqtt_command(
334
- api,
335
- device_id,
336
- SolixMqttCommands.sb_max_load,
337
- limit,
338
- "set_max_load",
339
- ha_client=ha_client,
340
- )
329
+ if not api_only:
330
+ if multisystem:
331
+ mqtt_ok = await _mqtt_max_load_parallel(
332
+ api, site_id, limit, control_sn, ha_client=ha_client
333
+ )
334
+ elif dev_type == SOLARBANK and not station_sn:
335
+ mqtt_ok = await _mqtt_command(
336
+ api,
337
+ device_id,
338
+ SolixMqttCommands.sb_max_load,
339
+ limit,
340
+ "set_max_load",
341
+ ha_client=ha_client,
342
+ )
341
343
 
342
344
  api_sn = (
343
345
  control_sn
@@ -346,6 +348,8 @@ async def _set_ac_output_limit(
346
348
  )
347
349
  api_ok = await _api_set_ac_output_only(api, site_id, api_sn, limit, device)
348
350
 
351
+ if api_only and api_ok:
352
+ return {"api": True, "curtailment_api_only": True}
349
353
  if mqtt_ok:
350
354
  return {"mqtt": True, "api": api_ok}
351
355
  if api_ok:
@@ -497,7 +501,9 @@ async def apply_control(
497
501
  control: str,
498
502
  value: Any,
499
503
  ha_client: IoBrokerAnkerApiClient | None = None,
504
+ config: dict | None = None,
500
505
  ) -> None:
506
+ config = config or {}
501
507
  site_id, control_sn, device_id, device = _control_context(api, device_id)
502
508
  dev_type = str(device.get("type") or "").lower()
503
509
 
@@ -537,6 +543,7 @@ async def apply_control(
537
543
  device,
538
544
  int(value),
539
545
  ha_client=ha_client,
546
+ api_only=bool(config.get("acOutputApiOnly")),
540
547
  )
541
548
  elif control == "pv_input_limit":
542
549
  result = await api.set_power_limit(
@@ -803,6 +810,7 @@ async def run_set_with_client(client: IoBrokerAnkerApiClient, config: dict) -> d
803
810
  str(config["control"]),
804
811
  config["value"],
805
812
  ha_client=client,
813
+ config=config,
806
814
  )
807
815
  return {"ok": True, "persistent": True}
808
816