iobroker.smartloadmanager 0.1.0 → 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.
- package/LICENSE +1 -1
- package/README.md +23 -5
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +130 -0
- package/admin/i18n/fr.json +130 -0
- package/admin/i18n/it.json +130 -0
- package/admin/i18n/nl.json +130 -0
- package/admin/i18n/pl.json +130 -0
- package/admin/i18n/pt.json +130 -0
- package/admin/i18n/ru.json +130 -0
- package/admin/i18n/uk.json +130 -0
- package/admin/i18n/zh-cn.json +130 -0
- package/admin/jsonConfig.json +311 -26
- package/docs/de/README.md +283 -0
- package/docs/de/release-0.2.0-pruefung.md +74 -0
- package/docs/en/README.md +268 -0
- package/io-package.json +114 -11
- package/lib/forecast.js +123 -0
- package/main.js +651 -83
- package/package.json +18 -15
- package/admin/i18n/de/translations.json +0 -110
- package/admin/i18n/en/translations.json +0 -108
- package/admin/i18n/es/translations.json +0 -110
- package/admin/i18n/fr/translations.json +0 -110
- package/admin/i18n/it/translations.json +0 -110
- package/admin/i18n/nl/translations.json +0 -110
- package/admin/i18n/pl/translations.json +0 -110
- package/admin/i18n/pt/translations.json +0 -110
- package/admin/i18n/ru/translations.json +0 -110
- package/admin/i18n/uk/translations.json +0 -110
- package/admin/i18n/zh-cn/translations.json +0 -110
- package/lib/adapter-config.d.ts +0 -20
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "smartloadmanager",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"news": {
|
|
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
|
+
},
|
|
19
|
+
"0.1.2": {
|
|
20
|
+
"en": "Update dependencies",
|
|
21
|
+
"de": "Abhängigkeiten aktualisieren",
|
|
22
|
+
"ru": "Обновление зависимостей",
|
|
23
|
+
"pt": "Atualizar dependências",
|
|
24
|
+
"nl": "Afhankelijkheden bijwerken",
|
|
25
|
+
"fr": "Mise à jour des dépendances",
|
|
26
|
+
"it": "Dipendenze di aggiornamento",
|
|
27
|
+
"es": "Actualizar las dependencias",
|
|
28
|
+
"pl": "Aktualizuj zależności",
|
|
29
|
+
"uk": "Оновлення залежності",
|
|
30
|
+
"zh-cn": "更新依赖关系"
|
|
31
|
+
},
|
|
6
32
|
"0.1.0": {
|
|
7
33
|
"en": "Added a checkbox for activating consumers from the settings to the object tree",
|
|
8
34
|
"de": "Hinzufügen eines Kontrollkästchens zur Aktivierung von Verbrauchern von den Einstellungen zum Objektbaum",
|
|
@@ -57,7 +83,7 @@
|
|
|
57
83
|
},
|
|
58
84
|
"0.0.1": {
|
|
59
85
|
"en": "Release",
|
|
60
|
-
"de": "
|
|
86
|
+
"de": "Veröffentlichung",
|
|
61
87
|
"ru": "Выпуск",
|
|
62
88
|
"pt": "Lançar",
|
|
63
89
|
"nl": "Loslaten",
|
|
@@ -137,7 +163,7 @@
|
|
|
137
163
|
],
|
|
138
164
|
"globalDependencies": [
|
|
139
165
|
{
|
|
140
|
-
"admin": ">=7.6.
|
|
166
|
+
"admin": ">=7.6.20"
|
|
141
167
|
}
|
|
142
168
|
]
|
|
143
169
|
},
|
|
@@ -159,19 +185,46 @@
|
|
|
159
185
|
},
|
|
160
186
|
"description": {
|
|
161
187
|
"en": "Notifications from SmartLoadManager",
|
|
162
|
-
"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 的通知"
|
|
163
198
|
},
|
|
164
199
|
"categories": [
|
|
165
200
|
{
|
|
166
201
|
"category": "info",
|
|
167
202
|
"name": {
|
|
168
203
|
"en": "Info",
|
|
169
|
-
"de": "
|
|
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": "信息"
|
|
170
214
|
},
|
|
171
215
|
"severity": "notify",
|
|
172
216
|
"description": {
|
|
173
217
|
"en": "Information about consumer switching or battery adjustments",
|
|
174
|
-
"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": "有关用电设备切换或电池调整的信息"
|
|
175
228
|
},
|
|
176
229
|
"regex": [],
|
|
177
230
|
"limit": 1
|
|
@@ -180,12 +233,30 @@
|
|
|
180
233
|
"category": "notify",
|
|
181
234
|
"name": {
|
|
182
235
|
"en": "Notify",
|
|
183
|
-
"de": "
|
|
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": "通知"
|
|
184
246
|
},
|
|
185
247
|
"severity": "notify",
|
|
186
248
|
"description": {
|
|
187
249
|
"en": "Notify about events",
|
|
188
|
-
"de": "
|
|
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": "事件通知"
|
|
189
260
|
},
|
|
190
261
|
"regex": [],
|
|
191
262
|
"limit": 1
|
|
@@ -194,12 +265,30 @@
|
|
|
194
265
|
"category": "alert",
|
|
195
266
|
"name": {
|
|
196
267
|
"en": "Alert",
|
|
197
|
-
"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": "警报"
|
|
198
278
|
},
|
|
199
279
|
"severity": "alert",
|
|
200
280
|
"description": {
|
|
201
281
|
"en": "Alarm or failures in the adapter",
|
|
202
|
-
"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": "适配器中的警报或故障"
|
|
203
292
|
},
|
|
204
293
|
"regex": [],
|
|
205
294
|
"limit": 1
|
|
@@ -235,7 +324,21 @@
|
|
|
235
324
|
"whatsappRecipients": "",
|
|
236
325
|
"signalRecipients": "",
|
|
237
326
|
"pushoverRecipients": "",
|
|
238
|
-
"enableNotificationManager": false
|
|
327
|
+
"enableNotificationManager": false,
|
|
328
|
+
"enableForecast": false,
|
|
329
|
+
"forecastDefaultMinMinutes": 30,
|
|
330
|
+
"forecastPowerUnit": "auto",
|
|
331
|
+
"forecastSafetyPercent": 80,
|
|
332
|
+
"forecastMaxStartMinutes": 30,
|
|
333
|
+
"forecastMinCurrentPercent": 50,
|
|
334
|
+
"forecastBridgeMinutes": 5,
|
|
335
|
+
"forecastMaxGridImport": 100,
|
|
336
|
+
"enableNotifications": false,
|
|
337
|
+
"notifyForecastBinary": false,
|
|
338
|
+
"notifyForecastPercent": false,
|
|
339
|
+
"notifyForecastBattery": false,
|
|
340
|
+
"forecastJsonDatapoint": "",
|
|
341
|
+
"notifyForecast": false
|
|
239
342
|
},
|
|
240
343
|
"objects": [],
|
|
241
344
|
"instanceObjects": []
|
package/lib/forecast.js
ADDED
|
@@ -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 };
|