iobroker.smartloadmanager 0.2.0 → 0.4.1
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 +5 -0
- package/docs/de/README.md +9 -0
- package/docs/en/README.md +7 -0
- package/io-package.json +14 -1
- package/main.js +65 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,11 @@ It can optionally integrate `ioBroker.pvforecast` through `summary.JSONData`. Su
|
|
|
32
32
|
Placeholder for the next version (at the beginning of the line):
|
|
33
33
|
### **WORK IN PROGRESS**
|
|
34
34
|
-->
|
|
35
|
+
### 0.4.1 (2026-10-24)
|
|
36
|
+
|
|
37
|
+
- (quorle) Make forecast configuration, data imports and decision notifications visible only in the debug log; report forecast decisions for percentage and battery consumers and limit identical notifications to once per five minutes.
|
|
38
|
+
- (quorle) Clear cached forecast data and report missing, invalid or expired source data instead of silently retaining previous forecasts.
|
|
39
|
+
|
|
35
40
|
### 0.2.0 (2026-10-24)
|
|
36
41
|
|
|
37
42
|
- (quorle) Added sustained PV forecast evaluation, configurable W/kW units, safety margins, minimum on/off times and limited forecast dip bridging.
|
package/docs/de/README.md
CHANGED
|
@@ -61,6 +61,15 @@ automatisch aus. Dafür `controlMode: 0` verwenden.
|
|
|
61
61
|
|
|
62
62
|
`enableForecast` aktivieren, `forecastJsonDatapoint` etwa auf
|
|
63
63
|
`pvforecast.0.summary.JSONData` setzen und beim Verbraucher `useForecast` aktivieren.
|
|
64
|
+
Auch `pvforecast.0.plants.<Anlagenname>.JSONData` ist möglich; dann wird nur diese
|
|
65
|
+
Anlage berücksichtigt. Die globale Aktivierung und „Prognose verwenden“ beim
|
|
66
|
+
jeweiligen Verbraucher sind beide erforderlich. „Prognoseentscheidungsbenachrichtigungen“
|
|
67
|
+
aktiviert die Prognosesteuerung nicht selbst und erzeugt erst bei einer tatsächlichen
|
|
68
|
+
Prognoseentscheidung eine Nachricht. In 0.2.0 stehen Datenimporte nur im Debug-Log.
|
|
69
|
+
Version 0.4.1 zeigt Konfiguration, Datenimporte und aktivierte
|
|
70
|
+
Entscheidungsnachrichten nur im Debug-Log an. Gleiche Entscheidungsnachrichten werden
|
|
71
|
+
innerhalb von fünf Minuten zusammengefasst. Für externe Nachrichten zusätzlich den
|
|
72
|
+
Notification Manager oder einen Nachrichtendienst konfigurieren.
|
|
64
73
|
Beispiel für das erwartete Format (Zeitstempel sind Unix-Millisekunden):
|
|
65
74
|
|
|
66
75
|
```js
|
package/docs/en/README.md
CHANGED
|
@@ -52,6 +52,13 @@ its existing device state unchanged; use `controlMode: 0` to switch it off.
|
|
|
52
52
|
|
|
53
53
|
Enable `enableForecast`, select e.g. `pvforecast.0.summary.JSONData` as
|
|
54
54
|
`forecastJsonDatapoint`, and enable `useForecast` for the consumer.
|
|
55
|
+
Alternatively select `pvforecast.0.plants.<plant name>.JSONData` to use one plant.
|
|
56
|
+
Both global forecast control and the individual consumer's forecast option must be
|
|
57
|
+
enabled. Decision notifications do not enable forecast control and are emitted only
|
|
58
|
+
when a forecast actually affects control. Version 0.2.0 logs imports at debug level.
|
|
59
|
+
Version 0.4.1 logs configuration, imports and enabled decision
|
|
60
|
+
notifications only at debug level, suppressing identical decisions for five minutes.
|
|
61
|
+
External delivery additionally requires Notification Manager or a messenger service.
|
|
55
62
|
The expected JSON array uses Unix timestamps in milliseconds:
|
|
56
63
|
|
|
57
64
|
```js
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "smartloadmanager",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.4.1": {
|
|
7
|
+
"en": "Fix forecast notifications for percentage and battery consumers, limit duplicate notifications, clear invalid forecast data and log all forecast diagnostics only at debug level.",
|
|
8
|
+
"de": "Prognosemeldungen für Prozent- und Batterieverbraucher korrigiert, doppelte Meldungen begrenzt, ungültige Prognosedaten verworfen und sämtliche Prognosediagnosen ausschließlich auf Debug-Stufe protokolliert.",
|
|
9
|
+
"ru": "Исправлены уведомления прогноза для процентных потребителей и батарей, ограничены повторы, очищены недействительные данные; диагностика прогноза выводится только на уровне debug.",
|
|
10
|
+
"pt": "Corrigidas notificações de previsão para consumidores percentuais e baterias, limitadas repetições, eliminados dados inválidos e diagnóstico apenas no nível debug.",
|
|
11
|
+
"nl": "Prognosemeldingen voor procentuele verbruikers en batterijen hersteld, herhalingen beperkt, ongeldige gegevens gewist en prognosediagnose uitsluitend op debugniveau gelogd.",
|
|
12
|
+
"fr": "Correction des notifications de prévision pour consommateurs en pourcentage et batteries, limitation des doublons, suppression des données invalides et diagnostic uniquement au niveau debug.",
|
|
13
|
+
"it": "Corrette le notifiche di previsione per utenze percentuali e batterie, limitati i duplicati, eliminati i dati non validi e diagnostica solo a livello debug.",
|
|
14
|
+
"es": "Corregidas las notificaciones de previsión para consumidores porcentuales y baterías, limitados los duplicados, borrados los datos inválidos y diagnóstico solo en nivel debug.",
|
|
15
|
+
"pl": "Poprawiono powiadomienia prognozy dla odbiorników procentowych i akumulatorów, ograniczono duplikaty, usunięto nieprawidłowe dane; diagnostyka wyłącznie na poziomie debug.",
|
|
16
|
+
"uk": "Виправлено повідомлення прогнозу для відсоткових споживачів і батарей, обмежено повтори, очищено недійсні дані; діагностика прогнозу лише на рівні debug.",
|
|
17
|
+
"zh-cn": "修复百分比用电设备和电池的预测通知,限制重复通知,清除无效预测数据,所有预测诊断仅在 debug 级别记录。"
|
|
18
|
+
},
|
|
6
19
|
"0.2.0": {
|
|
7
20
|
"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
21
|
"de": "PV-Prognosesteuerung und Mindestlaufzeiten ergänzt; Verbrauchersteuerung, Zeitfenster und Timer korrigiert; Übersetzungen, Tests und Workflows aktualisiert.",
|
package/main.js
CHANGED
|
@@ -22,6 +22,8 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
22
22
|
// 🔮 Forecast (PV forecast integration)
|
|
23
23
|
this.forecastEnabled = false;
|
|
24
24
|
this.forecastJsonDatapoint = null;
|
|
25
|
+
this.forecastNotificationTimes = new Map();
|
|
26
|
+
this.forecastLogSignature = '';
|
|
25
27
|
this.forecast = {
|
|
26
28
|
power: 0,
|
|
27
29
|
surplus: 0,
|
|
@@ -86,6 +88,17 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
86
88
|
// 🔮 Forecast config (PV forecast integration)
|
|
87
89
|
this.forecastEnabled = !!this.config.enableForecast;
|
|
88
90
|
this.forecastJsonDatapoint = this.config.forecastJsonDatapoint || null;
|
|
91
|
+
this.log.debug(
|
|
92
|
+
`[Forecast] ${this.forecastEnabled ? 'Enabled' : 'Disabled'}; source: ${this.forecastJsonDatapoint || 'not configured'}; decision notifications: ${this.config.notifyForecast ? 'enabled' : 'disabled'}.`,
|
|
93
|
+
);
|
|
94
|
+
if (this.forecastEnabled && !this.forecastJsonDatapoint) {
|
|
95
|
+
this.log.debug('[Forecast] No JSON data point configured; forecast control cannot run.');
|
|
96
|
+
}
|
|
97
|
+
if (this.config.notifyForecast && !this.forecastEnabled) {
|
|
98
|
+
this.log.debug(
|
|
99
|
+
'[Forecast] Decision notifications do not enable forecast control. Enable forecast control and select a JSON data point first.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
89
102
|
if (this.forecastEnabled && this.forecastJsonDatapoint) {
|
|
90
103
|
this.forecast.unit = await this.detectForecastPowerUnit();
|
|
91
104
|
}
|
|
@@ -118,6 +131,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
this.log.info(`Loaded consumers: ${this.consumerList.map(v => v.name).join(', ')}`);
|
|
134
|
+
if (this.forecastEnabled) {
|
|
135
|
+
const forecastConsumers = this.consumerList.filter(v => v.enabled && v.useForecast);
|
|
136
|
+
this.log.debug(
|
|
137
|
+
`[Forecast] Enabled consumers using forecast: ${forecastConsumers.map(v => v.name).join(', ') || 'none; activate useForecast for the desired consumers'}.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
121
140
|
|
|
122
141
|
await this.checkAndCreateConsumerObjects();
|
|
123
142
|
await this.createConsumerStates();
|
|
@@ -504,6 +523,19 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
504
523
|
// =====================================================================
|
|
505
524
|
// ========================= Forecast JSON parse =======================
|
|
506
525
|
// =====================================================================
|
|
526
|
+
async clearForecast(reason) {
|
|
527
|
+
this.forecastLogSignature = '';
|
|
528
|
+
this.forecast.points = [];
|
|
529
|
+
this.forecast.power = 0;
|
|
530
|
+
this.forecast.surplus = 0;
|
|
531
|
+
this.forecast.minutes = 0;
|
|
532
|
+
this.forecast.lastUpdate = 0;
|
|
533
|
+
for (const field of ['power', 'surplus', 'minutes', 'lastUpdate']) {
|
|
534
|
+
await this.setStateAsync(`${this.namespace}.forecast.${field}`, { val: 0, ack: true });
|
|
535
|
+
}
|
|
536
|
+
this.log.debug(`[Forecast] ${reason}; forecast control is unavailable until valid data arrives.`);
|
|
537
|
+
}
|
|
538
|
+
|
|
507
539
|
async processPvforecastJson() {
|
|
508
540
|
try {
|
|
509
541
|
if (!this.forecastEnabled || !this.forecastJsonDatapoint) {
|
|
@@ -512,6 +544,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
512
544
|
|
|
513
545
|
const state = await this.getForeignStateAsync(this.forecastJsonDatapoint);
|
|
514
546
|
if (!state || !state.val) {
|
|
547
|
+
await this.clearForecast('JSON data point is missing or empty');
|
|
515
548
|
return;
|
|
516
549
|
}
|
|
517
550
|
|
|
@@ -521,13 +554,13 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
521
554
|
baseload: this.config.baseload || 0,
|
|
522
555
|
});
|
|
523
556
|
} catch (error) {
|
|
524
|
-
this.
|
|
525
|
-
this.log.warn(`[Forecast] Invalid pvforecast JSON: ${error.message}`);
|
|
557
|
+
await this.clearForecast(`Invalid pvforecast JSON: ${error.message}`);
|
|
526
558
|
return;
|
|
527
559
|
}
|
|
528
560
|
|
|
529
561
|
const best = this.forecast.points[0];
|
|
530
562
|
if (!best) {
|
|
563
|
+
await this.clearForecast('JSON contains no valid future forecast points');
|
|
531
564
|
return;
|
|
532
565
|
}
|
|
533
566
|
const power = best.power;
|
|
@@ -549,11 +582,15 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
549
582
|
ack: true,
|
|
550
583
|
});
|
|
551
584
|
|
|
552
|
-
this.
|
|
553
|
-
|
|
554
|
-
|
|
585
|
+
const signature = `${this.forecast.points.length}:${best.timestamp}:${power}:${best.surplus}`;
|
|
586
|
+
if (signature !== this.forecastLogSignature) {
|
|
587
|
+
this.log.debug(
|
|
588
|
+
`[Forecast] ${this.forecast.points.length} values parsed (${this.forecast.unit || 'auto'}) - next: ${power}W in ${minutes}min`,
|
|
589
|
+
);
|
|
590
|
+
this.forecastLogSignature = signature;
|
|
591
|
+
}
|
|
555
592
|
} catch (error) {
|
|
556
|
-
this.log.
|
|
593
|
+
this.log.debug(`[Forecast] Error processing pvforecast JSON: ${error.message}`);
|
|
557
594
|
}
|
|
558
595
|
}
|
|
559
596
|
|
|
@@ -656,17 +693,29 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
656
693
|
ack: true,
|
|
657
694
|
});
|
|
658
695
|
} catch (err) {
|
|
659
|
-
this.log.
|
|
696
|
+
this.log.debug(`[Forecast] Failed to set forecastStatus: ${err.message}`);
|
|
660
697
|
}
|
|
661
698
|
}
|
|
662
699
|
|
|
663
700
|
async sendForecastNotification(type, message) {
|
|
664
701
|
try {
|
|
665
702
|
if (this.config.notifyForecast) {
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
for (const [key, sentAt] of this.forecastNotificationTimes) {
|
|
705
|
+
if (now - sentAt >= 5 * 60000) {
|
|
706
|
+
this.forecastNotificationTimes.delete(key);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const key = `${type}:${message}`;
|
|
710
|
+
if (this.forecastNotificationTimes.has(key)) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
this.log.debug(`[Forecast] ${message}`);
|
|
666
714
|
await this.sendNotification('smartloadmanager', 'notify', message);
|
|
715
|
+
this.forecastNotificationTimes.set(key, now);
|
|
667
716
|
}
|
|
668
717
|
} catch (err) {
|
|
669
|
-
this.log.
|
|
718
|
+
this.log.debug(`[Forecast] Failed to send forecast notification: ${err.message}`);
|
|
670
719
|
}
|
|
671
720
|
}
|
|
672
721
|
|
|
@@ -1291,6 +1340,10 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1291
1340
|
if (newPercent < currentPercent && this.isForecastDelayingSwitchOff(v, currentSurplus, currentPower)) {
|
|
1292
1341
|
newPercent = currentPercent;
|
|
1293
1342
|
this.log.debug(`[Forecast] ${v.name}: holding ${currentPercent}% during a short forecast-backed dip`);
|
|
1343
|
+
await this.sendForecastNotification(
|
|
1344
|
+
'percent',
|
|
1345
|
+
`${v.name}: holding ${currentPercent}% during a forecast-backed dip.`,
|
|
1346
|
+
);
|
|
1294
1347
|
}
|
|
1295
1348
|
|
|
1296
1349
|
if (newPercent !== currentPercent) {
|
|
@@ -1410,6 +1463,10 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1410
1463
|
powerToSet = Math.min(oldPower, v.performance || oldPower);
|
|
1411
1464
|
modeToSet = 2;
|
|
1412
1465
|
this.log.debug(`[Forecast] ${v.name}: holding ${powerToSet}W charging power during a short dip`);
|
|
1466
|
+
await this.sendForecastNotification(
|
|
1467
|
+
'battery',
|
|
1468
|
+
`${v.name}: holding ${powerToSet}W charging power during a forecast-backed dip.`,
|
|
1469
|
+
);
|
|
1413
1470
|
}
|
|
1414
1471
|
|
|
1415
1472
|
const oldMode = this.batteryControlModeDatapoint
|