iobroker.smartloadmanager 0.1.2 → 0.2.0

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.
@@ -0,0 +1,268 @@
1
+ ![Logo](../../admin/smartloadmanager.png)
2
+
3
+ # ioBroker.smartloadmanager
4
+
5
+ ## Version 0.2.0 examples
6
+
7
+ Requires Node.js >=22.19.0. Device commands use `ack: false`; the device adapter
8
+ provides the acknowledgement. Feed-in must represent grid connection power in watts.
9
+ Enable `feedinNegativ` when export is negative. Baseload is an additional reserve;
10
+ do not subtract household consumption twice if it is already included in the meter reading.
11
+
12
+ ### Binary loads and heating
13
+
14
+ Example: `performance: 1000`, `switchOnPoint: 1200`, `switchOffPoint: 0`,
15
+ `switchOnDelay: 20`, `switchOffDelay: 10`. A 1200 W surplus after baseload starts
16
+ the on timer. Import or surplus below the off threshold starts the off timer.
17
+ `minimumOnMinutes: 10` and `minimumOffMinutes: 5` reduce cycling; import above
18
+ `forecastMaxGridImport` overrides minimum runtime. Set minimum times to 0 to disable them.
19
+
20
+ Use `datapoint` for boolean switching and `numericDatapoint` for numeric 0/1.
21
+ Heating consumers additionally support temperature, enable and heartbeat inputs.
22
+ Maximum temperature and missing enable/heartbeat prevent activation.
23
+ Heartbeat checks the input's truth value, not the age of its last update.
24
+ A missing or nonfinite configured temperature sensor prevents activation and
25
+ stops automatic heating. “Switch off only at switch-off time” intentionally
26
+ disables normal surplus shutdown until the time window ends; heating protection remains active.
27
+
28
+ ### Percentage and battery control
29
+
30
+ A 2000 W percent consumer at 50% consumes 1000 W. With 400 W additional export,
31
+ the target becomes 70%. With 200 W import it becomes 40%. Below `minPercentStart`,
32
+ the target becomes 0%. Disabled consumers are not regulated. This calculation
33
+ assumes the load is included in the grid meter reading.
34
+
35
+ Configure a battery's `batterySetpoint`, `performance`, and optional `batterySOC`
36
+ and `batteryTargetSOC`. With 800 W surplus and a 1500 W rating, the charging command
37
+ is 800 W. Reaching target SOC sets charging power to 0. The optional global battery
38
+ mode datapoint uses 0 (off), 1 (target reached), and 2 (charging); verify that these
39
+ values match the device adapter.
40
+
41
+ `controlMode`: 0 = off, 1 = manual on, 2 = automatic. Manual on sets a percent load
42
+ to 100% or battery charging power to its configured rating. Automatic regulators
43
+ respect manual modes.
44
+ Battery control includes already requested charging power and corrects it by the
45
+ measured grid surplus. `batteryDelaySeconds` and `delaySecondsProzent` remain
46
+ effective during frequent meter updates; 0 means immediate regulation.
47
+ Missing configured SOC/target SOC values stop charging. Time windows are also
48
+ checked every 15 seconds. Disabling a consumer cancels pending switches but leaves
49
+ its existing device state unchanged; use `controlMode: 0` to switch it off.
50
+
51
+ ### PV forecast example
52
+
53
+ Enable `enableForecast`, select e.g. `pvforecast.0.summary.JSONData` as
54
+ `forecastJsonDatapoint`, and enable `useForecast` for the consumer.
55
+ The expected JSON array uses Unix timestamps in milliseconds:
56
+
57
+ ```js
58
+ const now = Date.now();
59
+ const forecast = [0, 15, 30, 45].map(minutes => ({
60
+ t: now + minutes * 60000,
61
+ y: 3.5,
62
+ }));
63
+ // JSON.stringify(forecast), with forecastPowerUnit = "kW"
64
+ ```
65
+
66
+ With 500 W baseload this gives 3000 W forecast surplus, reduced to 2400 W at
67
+ `forecastSafetyPercent: 80`. A 2000 W consumer requires a sustained
68
+ `forecastMinMinutes: 30` window starting within `forecastMaxStartMinutes: 30`.
69
+ At `forecastMinCurrentPercent: 50`, at least 1000 W must already be available.
70
+
71
+ `forecastBridgeMinutes: 5` limits dip bridging; `forecastMaxGridImport: 100`
72
+ prevents bridging above 100 W import. Past forecast points are ignored.
73
+ Select `W` explicitly for small watt readings: automatic detection treats values
74
+ up to 100 as kW. Inspect `forecast.*` and each consumer's forecast status states.
75
+
76
+ Time windows may cross midnight (22:00–06:00). Equal start/end times mean all day.
77
+ Automated tests use simulated devices; no physical device installation was available.
78
+
79
+ ## 🔧 Description
80
+
81
+ The **smartloadmanager** adapter dynamically controls consumers based on a PV feed-in value.
82
+ The goal is to consume surplus power locally and minimize or completely avoid feeding into the public grid.
83
+ It supports binary consumers, percentage-controllable devices, and battery storage systems.
84
+
85
+ ---
86
+
87
+ ## 🚀 Features
88
+
89
+ - ✅ Monitoring of a configurable feed-in datapoint
90
+ - ✅ Dynamic switching on of consumers in case of surplus
91
+ - ✅ Dynamic switching off in case of deficit or grid consumption
92
+ - ✅ Supports **binary**, **percent**, **heating** and **battery** consumers
93
+ - ✅ Percentage control with delay (smooth down-regulation), optionally configurable per consumer
94
+ - ✅ Dynamic charging power for battery storage with target SOC
95
+ - ✅ Time windows for on/off logic per consumer (incl. "only at off-time")
96
+ - ✅ Automatic object creation with extended information per consumer
97
+ - ✅ Control mode: Off / Manual On / Auto per consumer
98
+ - ✅ Hysteresis control via separate on/off thresholds
99
+ - ✅ Consumer-specific switching delay (override of global setting)
100
+ - ✅ Global battery switching delay (`batteryDelaySeconds`)
101
+ - ✅ Write check for battery control mode (debug output in log)
102
+ - ✅ **Delayed switching via timers** for on and off
103
+ - ✅ **Display of active timers in debug log** with remaining time
104
+ - ✅ Optional **notifications** for binary, percent, battery and heating (Telegram / Gotify)
105
+ - ✅ Optimization for 0/1 datapoints (boolean or numeric 0/1 control)
106
+ - ✅ Detailed logging of all timers, surpluses and switching operations
107
+ - ✅ Notifications per consumer type (Binary, Percent, Battery, Heating), optionally enabled
108
+ - ✅ Heating/immersion heater control with temperature, enable and heartbeat checks
109
+
110
+ ---
111
+
112
+ ## ⚙️ Configuration
113
+
114
+ ### 🔹 Main settings
115
+
116
+ | Setting | Description |
117
+ | ---------------------------------- | ------------------------------------------------------------------------------- |
118
+ | **Feed-in datapoint** | Object ID of the feed-in value (e.g. PV surplus) |
119
+ | **Base load** | Always subtracted from feed-in value (e.g. standby consumption) |
120
+ | **Switch-on threshold** | Threshold in watts at which consumers are switched on |
121
+ | **Switch-off threshold** | Threshold in watts at which consumers are switched off |
122
+ | **Delay (seconds)** | Delay for binary switching (hysteresis smoothing) |
123
+ | **Delay percent (s)** | Global delay for down-regulation of percent consumers (e.g. wallbox) |
124
+ | **Delay battery (s)** | Global delay for battery control |
125
+ | **Feed-in negative** | If active: negative = feed-in / positive = grid consumption |
126
+ | **Battery control mode datapoint** | Optional datapoint for battery mode switching (Auto/Manual/Off) |
127
+ | **Notifications** | Enable notifications for Binary / Percent / Battery / Heating (Telegram/Gotify) |
128
+
129
+ ---
130
+
131
+ ### 🔹 Consumers
132
+
133
+ | Field | Description |
134
+ | ----------------------------------- | --------------------------------------------------------------------- |
135
+ | **Name** | Display name |
136
+ | **Active** | Enables control for this consumer |
137
+ | **Rule type** | `"binary"`, `"percent"`, `"battery"`, `"heating"` |
138
+ | **Control datapoint** | ID to switch or regulate, supports 0/1 datapoints |
139
+ | **Power [W]** | Realistic electrical power of the consumer |
140
+ | **Switch-on threshold [W]** | Required surplus for activation |
141
+ | **Switch-off threshold [W]** | Lower limit for deactivation |
142
+ | **Max power [W]** | Reference value for percentage control |
143
+ | **Delay percent [s]** | Optional delay for down-regulation for this consumer only |
144
+ | **Delay override [s]** | Consumer-specific switching delay (e.g. immediate switching possible) |
145
+ | **Switch-on time (HH:MM)** | Time when control may start |
146
+ | **Switch-off time (HH:MM)** | Time when control must end |
147
+ | **Only off at time** | Checkbox: switch off only at configured time |
148
+ | **batterySetpoint (battery only)** | Datapoint where desired charging power is written |
149
+ | **batterySOC / targetSOC** | Optional: SOC & target SOC to stop charging at target |
150
+ | **Timer / TimerEnd** | Internal timer for delayed switching, remaining time visible in log |
151
+ | **minTemperature / maxTemperature** | Lower/upper threshold for heating operation |
152
+ | **temperatureDatapoint (heating)** | Temperature datapoint for min/max checks |
153
+ | **enableDatapoint (heating)** | Optional datapoint: heating only allowed if true |
154
+ | **heartbeatDatapoint (heating)** | Optional: must cyclically deliver true, otherwise switched off |
155
+
156
+ ---
157
+
158
+ ## 🔋 Battery storage support
159
+
160
+ - Consumers with `"ruletype": "battery"` control the charging setpoint based on current surplus.
161
+ - If `batterySOC` and `batteryTargetSOC` are set, charging stops at target SOC.
162
+ - Optional `batteryControlModeDatapoint`:
163
+ - `0 = Off`, `1 = Manual`, `2 = Auto`
164
+ - Control is only active within configured time windows.
165
+ - Control runs on every feed-in update, after configured delay (`batteryDelaySeconds`).
166
+ - Delays for all consumer types (binary, percent, battery) can be set globally or per consumer.
167
+ - Detailed logging and optional notifications for battery switching.
168
+
169
+ ---
170
+
171
+ ## 🌡️ Heating support
172
+
173
+ - Consumers with `"ruletype": "heating"` are intended for immersion heaters or similar devices.
174
+ - Control only occurs when:
175
+ - Current temperature is below `maxTemperature`
176
+ - Optional: above `minTemperature`
177
+ - Optional: `enableDatapoint` is active
178
+ - Optional: `heartbeatDatapoint` is valid
179
+ - If conditions are not met, switching is stopped immediately and timers are canceled.
180
+ - Supports on/off timers (`onTimerRemaining` / `offTimerRemaining`) with remaining time display.
181
+ - Detailed logging and optional notifications for heating consumers.
182
+
183
+ ---
184
+
185
+ ## 🧠 Control logic
186
+
187
+ 1. **Feed-in datapoint value is interpreted** (positive = grid consumption or feed-in, depending on config)
188
+ 2. **Surplus > base load + switch-on threshold**:
189
+ → Consumers are switched on (binary) in ascending order of power
190
+ 3. **Deficit < base load - switch-off threshold**:
191
+ → Consumers are switched off in reverse order
192
+ 4. **Percent consumers**:
193
+ → % = surplus / max power
194
+ → Adjusted after delay (global or per consumer)
195
+ → Optional overrides possible
196
+ 5. **Battery consumers**:
197
+ → Charging power = min(surplus, max power), if target SOC not reached
198
+ → Delay via `batteryDelaySeconds` or per consumer
199
+ 6. **Time window check for all consumers**
200
+ 7. **Control only in mode Auto (2)**
201
+ 8. **Delayed switching via timers** (cancel/restart on updates, remaining time visible in log)
202
+ 9. **Optional notifications for all switching/regulation types**
203
+ 10. **0/1 datapoint optimization** (boolean or numeric 0/1 supported)
204
+ 11. **Detailed logging of all switching and surplus values**
205
+ 12. **Heating logic**: switch only within temp ranges and with valid enable/heartbeat datapoints
206
+
207
+ ---
208
+
209
+ ## 💡 Example: Wallbox
210
+
211
+ | Parameter | Value |
212
+ | --------- | ------- |
213
+ | Feed-in | 1000 W |
214
+ | Base load | 100 W |
215
+ | Max power | 11000 W |
216
+
217
+ **Calculation:**
218
+
219
+ - Surplus: 1000 – 100 = 900 W
220
+ - Percent: 900 / 11000 ≈ 8.2 % → Wallbox set to 8 %
221
+
222
+ ---
223
+
224
+ ## 📋 Object structure
225
+
226
+ Each consumer gets its own channel with the following states:
227
+
228
+ - `.controlMode` → 0 = Off, 1 = Manual, 2 = Auto
229
+ - `.switchOnTime` / `.switchOffTime`
230
+ - `.alwaysOffAtTime` → true/false
231
+ - `.performance`, `.switchOnPoint`, `.switchOffPoint`
232
+ - `.batterySetpoint` (battery only)
233
+ - `.temperatureDatapoint` (heating only)
234
+ - `.onTimerRemaining / .offTimerRemaining` → Remaining time for timers
235
+ - `.timer` → internal timer for delayed switching
236
+ - `.timerEnd` → timestamp when timer expires (for log)
237
+
238
+ ---
239
+
240
+ ## 🚫 Limitations
241
+
242
+ - No SOC history, no long-term logic
243
+ - No multiple use of identical datapoints
244
+ - Battery control based only on feed-in, not discharge power
245
+ - Heating without direct power measurement – only thresholds and enable checks
246
+
247
+ ---
248
+
249
+ ## 🛣️ Future features
250
+
251
+ - ~~PV forecast-based control (beta)~~
252
+ - Combined consumer support
253
+ - ~~Prioritization profiles~~
254
+ - Overheating or fault handling per device
255
+ - ~~Min/max percentage limits~~
256
+ - Configurable group or room logic
257
+
258
+ ---
259
+
260
+ ## PV forecast control
261
+
262
+ SmartLoadManager can use `pvforecast.<instance>.summary.JSONData`. Enable forecast control, select the JSON data point and enable it for the desired consumers.
263
+
264
+ The adapter evaluates a continuous forecast window instead of a single value. A configurable safety factor reduces optimistic forecasts. Forecast-assisted switch-on additionally requires part of the required power to be present as real surplus. Short drops can be bridged for a limited time, but only up to the configured maximum grid import.
265
+
266
+ Minimum run and off times prevent rapid cycling. Forecast decisions never override time windows, heating temperature limits, hardware enable/heartbeat signals or battery SOC limits.
267
+
268
+ Recommended starting values: 80% safety factor, 30-minute forecast window, 30-minute maximum lead time, 50% current-surplus requirement, 5-minute dip bridging, 100 W maximum grid import and 10/5-minute minimum run/off times.
package/io-package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "smartloadmanager",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "news": {
6
- "0.1.2": {
7
- "en": "Update dependencies",
8
- "de": "Abhängigkeiten aktualisieren",
9
- "ru": "Обновление зависимостей",
10
- "pt": "Atualizar dependências",
11
- "nl": "Afhankelijkheden bijwerken",
12
- "fr": "Mise à jour des dépendances",
13
- "it": "Dipendenze di aggiornamento",
14
- "es": "Actualizar las dependencias",
15
- "pl": "Aktualizuj zależności",
16
- "uk": "Оновлення залежності",
17
- "zh-cn": "更新依赖关系"
6
+ "0.2.0": {
7
+ "en": "Added sustained PV forecast control and minimum run/off times; fixed consumer commands, percent regulation, battery control, time windows and timers; updated translations, tests and workflows.",
8
+ "de": "PV-Prognosesteuerung und Mindestlaufzeiten ergänzt; Verbrauchersteuerung, Zeitfenster und Timer korrigiert; Übersetzungen, Tests und Workflows aktualisiert.",
9
+ "ru": "Добавлены управление по прогнозу ФЭ и минимальные времена работы; исправлены управление потребителями, временные окна и таймеры; обновлены переводы, тесты и рабочие процессы.",
10
+ "pt": "Adicionado controlo por previsão solar e tempos mínimos de funcionamento; corrigidos consumidores, horários e temporizadores; atualizados traduções, testes e fluxos de trabalho.",
11
+ "nl": "PV-prognosesturing en minimale aan/uit-tijden toegevoegd; schakelopdrachten, procentregeling, batterij, tijdvensters en timers hersteld; vertalingen, tests en workflows bijgewerkt.",
12
+ "fr": "Ajout des prévisions solaires et durées minimales ; correction des commandes, plages horaires et minuteries ; mise à jour des traductions, tests et workflows.",
13
+ "it": "Aggiunti controllo tramite previsioni FV e tempi minimi di accensione/spegnimento; corretti comandi, regolazione percentuale, batteria, fasce orarie e timer; aggiornati traduzioni, test e workflow.",
14
+ "es": "Añadidos control por previsión FV y tiempos mínimos; corregidos consumidores, horarios y temporizadores; actualizados traducciones, pruebas y flujos de trabajo.",
15
+ "pl": "Dodano prognozę PV i minimalne czasy pracy; poprawiono sterowanie odbiornikami, okna czasowe i zegary; zaktualizowano tłumaczenia, testy i przepływy pracy.",
16
+ "uk": "Додано керування за прогнозом ФЕ та мінімальні часи роботи; виправлено керування споживачами, часові вікна та таймери; оновлено переклади, тести та робочі процеси.",
17
+ "zh-cn": "新增光伏预测控制和最短运行时间;修复用电设备控制、时间窗口和定时器;更新翻译、测试和工作流。"
18
18
  },
19
- "0.1.1": {
19
+ "0.1.2": {
20
20
  "en": "Update dependencies",
21
21
  "de": "Abhängigkeiten aktualisieren",
22
22
  "ru": "Обновление зависимостей",
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "0.0.1": {
85
85
  "en": "Release",
86
- "de": "Release",
86
+ "de": "Veröffentlichung",
87
87
  "ru": "Выпуск",
88
88
  "pt": "Lançar",
89
89
  "nl": "Loslaten",
@@ -163,7 +163,7 @@
163
163
  ],
164
164
  "globalDependencies": [
165
165
  {
166
- "admin": ">=7.6.17"
166
+ "admin": ">=7.6.20"
167
167
  }
168
168
  ]
169
169
  },
@@ -185,19 +185,46 @@
185
185
  },
186
186
  "description": {
187
187
  "en": "Notifications from SmartLoadManager",
188
- "de": "Benachrichtigungen vom SmartLoadManager"
188
+ "de": "Benachrichtigungen vom SmartLoadManager",
189
+ "ru": "Уведомления от SmartLoadManager",
190
+ "pt": "Notificações do SmartLoadManager",
191
+ "nl": "Meldingen van SmartLoadManager",
192
+ "fr": "Notifications de SmartLoadManager",
193
+ "it": "Notifiche da SmartLoadManager",
194
+ "es": "Notificaciones de SmartLoadManager",
195
+ "pl": "Powiadomienia z SmartLoadManager",
196
+ "uk": "Сповіщення від SmartLoadManager",
197
+ "zh-cn": "来自 SmartLoadManager 的通知"
189
198
  },
190
199
  "categories": [
191
200
  {
192
201
  "category": "info",
193
202
  "name": {
194
203
  "en": "Info",
195
- "de": "Info"
204
+ "de": "Information",
205
+ "ru": "Информация",
206
+ "pt": "Informação",
207
+ "nl": "Informatie",
208
+ "fr": "Information",
209
+ "it": "Informazioni",
210
+ "es": "Información",
211
+ "pl": "Informacja",
212
+ "uk": "Інформація",
213
+ "zh-cn": "信息"
196
214
  },
197
215
  "severity": "notify",
198
216
  "description": {
199
217
  "en": "Information about consumer switching or battery adjustments",
200
- "de": "Informationen zu Verbraucherschaltungen oder Batterieanpassungen"
218
+ "de": "Informationen zu Verbraucherschaltungen oder Batterieanpassungen",
219
+ "ru": "Информация о переключении потребителей или настройке батареи",
220
+ "pt": "Informações sobre comutação de consumidores ou ajustes da bateria",
221
+ "nl": "Informatie over het schakelen van verbruikers of batterijaanpassingen",
222
+ "fr": "Informations sur la commutation des consommateurs ou les réglages de batterie",
223
+ "it": "Informazioni sulla commutazione delle utenze o sulle regolazioni della batteria",
224
+ "es": "Información sobre la conmutación de consumidores o los ajustes de batería",
225
+ "pl": "Informacje o przełączaniu odbiorników lub regulacji baterii",
226
+ "uk": "Інформація про перемикання споживачів або налаштування батареї",
227
+ "zh-cn": "有关用电设备切换或电池调整的信息"
201
228
  },
202
229
  "regex": [],
203
230
  "limit": 1
@@ -206,12 +233,30 @@
206
233
  "category": "notify",
207
234
  "name": {
208
235
  "en": "Notify",
209
- "de": "Benachrichtigen"
236
+ "de": "Benachrichtigung",
237
+ "ru": "Уведомление",
238
+ "pt": "Notificação",
239
+ "nl": "Melding",
240
+ "fr": "Notification",
241
+ "it": "Notifica",
242
+ "es": "Notificación",
243
+ "pl": "Powiadomienie",
244
+ "uk": "Сповіщення",
245
+ "zh-cn": "通知"
210
246
  },
211
247
  "severity": "notify",
212
248
  "description": {
213
249
  "en": "Notify about events",
214
- "de": "Benachrichtigen über Ereignisse"
250
+ "de": "Über Ereignisse benachrichtigen",
251
+ "ru": "Уведомлять о событиях",
252
+ "pt": "Notificar sobre eventos",
253
+ "nl": "Melden over gebeurtenissen",
254
+ "fr": "Notifier les événements",
255
+ "it": "Notifica gli eventi",
256
+ "es": "Notificar sobre eventos",
257
+ "pl": "Powiadamiaj o zdarzeniach",
258
+ "uk": "Сповіщати про події",
259
+ "zh-cn": "事件通知"
215
260
  },
216
261
  "regex": [],
217
262
  "limit": 1
@@ -220,12 +265,30 @@
220
265
  "category": "alert",
221
266
  "name": {
222
267
  "en": "Alert",
223
- "de": "Alarm"
268
+ "de": "Alarm",
269
+ "ru": "Тревога",
270
+ "pt": "Alerta",
271
+ "nl": "Alarm",
272
+ "fr": "Alerte",
273
+ "it": "Allarme",
274
+ "es": "Alerta",
275
+ "pl": "Alarm",
276
+ "uk": "Тривога",
277
+ "zh-cn": "警报"
224
278
  },
225
279
  "severity": "alert",
226
280
  "description": {
227
281
  "en": "Alarm or failures in the adapter",
228
- "de": "Alarm oder Ausfälle im Adapter"
282
+ "de": "Alarm oder Ausfälle im Adapter",
283
+ "ru": "Тревоги или сбои адаптера",
284
+ "pt": "Alarmes ou falhas no adaptador",
285
+ "nl": "Alarmen of storingen in de adapter",
286
+ "fr": "Alarmes ou défaillances de l’adaptateur",
287
+ "it": "Allarmi o guasti nell'adattatore",
288
+ "es": "Alarmas o fallos en el adaptador",
289
+ "pl": "Alarmy lub awarie adaptera",
290
+ "uk": "Тривоги або збої адаптера",
291
+ "zh-cn": "适配器中的警报或故障"
229
292
  },
230
293
  "regex": [],
231
294
  "limit": 1
@@ -263,13 +326,19 @@
263
326
  "pushoverRecipients": "",
264
327
  "enableNotificationManager": false,
265
328
  "enableForecast": false,
266
- "forecastDefaultMinMinutes": 0,
329
+ "forecastDefaultMinMinutes": 30,
330
+ "forecastPowerUnit": "auto",
331
+ "forecastSafetyPercent": 80,
332
+ "forecastMaxStartMinutes": 30,
333
+ "forecastMinCurrentPercent": 50,
334
+ "forecastBridgeMinutes": 5,
335
+ "forecastMaxGridImport": 100,
267
336
  "enableNotifications": false,
268
337
  "notifyForecastBinary": false,
269
338
  "notifyForecastPercent": false,
270
339
  "notifyForecastBattery": false,
271
340
  "forecastJsonDatapoint": "",
272
- "notifyForecast": ""
341
+ "notifyForecast": false
273
342
  },
274
343
  "objects": [],
275
344
  "instanceObjects": []
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Determine the conversion factor for forecast power values.
5
+ *
6
+ * @param {'W'|'kW'|null|undefined} unit Configured or detected power unit.
7
+ * @param {number[]} values Raw forecast values used for automatic detection.
8
+ * @returns {number} Multiplier to convert forecast values to watts.
9
+ */
10
+ function powerFactor(unit, values = []) {
11
+ if (unit === 'W') {
12
+ return 1;
13
+ }
14
+ if (unit === 'kW') {
15
+ return 1000;
16
+ }
17
+ const finite = values.filter(Number.isFinite).map(Math.abs);
18
+ return finite.length && Math.max(...finite) <= 100 ? 1000 : 1;
19
+ }
20
+
21
+ /**
22
+ * Parse pvforecast JSONData into normalized future data points.
23
+ *
24
+ * @param {string|Array<{t: number, y: number}>} value JSON string or decoded pvforecast array.
25
+ * @param {{now?: number, unit?: 'W'|'kW'|null, baseload?: number}} options Parser options.
26
+ * @returns {Array<{timestamp: number, power: number, surplus: number}>} Normalized forecast points.
27
+ */
28
+ function parseForecast(value, options = {}) {
29
+ const now = options.now || Date.now();
30
+ let data = value;
31
+ if (typeof data === 'string') {
32
+ data = JSON.parse(data);
33
+ }
34
+ if (!Array.isArray(data)) {
35
+ throw new Error('Forecast must be an array');
36
+ }
37
+
38
+ const raw = data
39
+ .map(entry => ({ timestamp: Number(entry?.t), value: Number(entry?.y) }))
40
+ .filter(entry => Number.isFinite(entry.timestamp) && Number.isFinite(entry.value) && entry.timestamp >= now)
41
+ .sort((a, b) => a.timestamp - b.timestamp);
42
+ const factor = powerFactor(
43
+ options.unit,
44
+ raw.map(entry => entry.value),
45
+ );
46
+ const baseload = Math.max(0, Number(options.baseload) || 0);
47
+ return raw.map(entry => ({
48
+ timestamp: entry.timestamp,
49
+ power: Math.max(0, Math.round(entry.value * factor)),
50
+ surplus: Math.max(0, Math.round(entry.value * factor - baseload)),
51
+ }));
52
+ }
53
+
54
+ /**
55
+ * Find a sustained forecast window with enough conservative surplus.
56
+ *
57
+ * @param {Array<{timestamp: number, surplus: number}>} points Normalized forecast points.
58
+ * @param {{now?: number, requiredPower?: number, safetyFactor?: number, minDurationMinutes?: number, maxStartMinutes?: number}} options Evaluation limits.
59
+ * @returns {{available: boolean, startMinutes: number|null, durationMinutes: number, minimumSurplus: number, averageSurplus: number}} Forecast decision.
60
+ */
61
+ function evaluateForecast(points, options = {}) {
62
+ const now = options.now || Date.now();
63
+ points = points
64
+ .filter(point => Number.isFinite(point.timestamp) && Number.isFinite(point.surplus) && point.timestamp >= now)
65
+ .sort((a, b) => a.timestamp - b.timestamp);
66
+ const requiredPower = Math.max(0, Number(options.requiredPower) || 0);
67
+ const safetyFactor = Math.min(1, Math.max(0.1, Number(options.safetyFactor) || 0.8));
68
+ const minDurationMinutes = Math.max(1, Number(options.minDurationMinutes) || 30);
69
+ const maxStartMinutes = Math.max(
70
+ 0,
71
+ options.maxStartMinutes === undefined ? 30 : Number(options.maxStartMinutes) || 0,
72
+ );
73
+ const threshold = requiredPower / safetyFactor;
74
+ const maxStart = now + maxStartMinutes * 60000;
75
+ const fallbackStep = 15 * 60000;
76
+
77
+ for (let start = 0; start < points.length; start++) {
78
+ if (points[start].timestamp > maxStart || points[start].surplus < threshold) {
79
+ continue;
80
+ }
81
+ let end = start;
82
+ let minimum = points[start].surplus;
83
+ let sum = 0;
84
+ while (end < points.length && points[end].surplus >= threshold) {
85
+ if (end > start) {
86
+ const gap = points[end].timestamp - points[end - 1].timestamp;
87
+ if (gap > 90 * 60000) {
88
+ break;
89
+ }
90
+ }
91
+ minimum = Math.min(minimum, points[end].surplus);
92
+ sum += points[end].surplus;
93
+ end++;
94
+ }
95
+ const step = end - start > 1 ? points[start + 1].timestamp - points[start].timestamp : fallbackStep;
96
+ const durationMinutes = Math.round((points[end - 1].timestamp - points[start].timestamp + step) / 60000);
97
+ if (durationMinutes >= minDurationMinutes) {
98
+ return {
99
+ available: true,
100
+ startMinutes: Math.max(0, Math.round((points[start].timestamp - now) / 60000)),
101
+ durationMinutes,
102
+ minimumSurplus: minimum,
103
+ averageSurplus: Math.round(sum / (end - start)),
104
+ };
105
+ }
106
+ }
107
+ return { available: false, startMinutes: null, durationMinutes: 0, minimumSurplus: 0, averageSurplus: 0 };
108
+ }
109
+
110
+ /**
111
+ * Check whether a minimum on/off period has elapsed.
112
+ *
113
+ * @param {number} lastChange Timestamp of the last state change.
114
+ * @param {number} minimumMinutes Required state duration in minutes.
115
+ * @param {number} now Current timestamp.
116
+ * @returns {boolean} Whether a new state change is allowed.
117
+ */
118
+ function minimumStateTimeSatisfied(lastChange, minimumMinutes, now = Date.now()) {
119
+ const minimumMs = Math.max(0, Number(minimumMinutes) || 0) * 60000;
120
+ return !lastChange || now - lastChange >= minimumMs;
121
+ }
122
+
123
+ module.exports = { evaluateForecast, minimumStateTimeSatisfied, parseForecast, powerFactor };