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 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.2.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.forecast.points = [];
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.log.debug(
553
- `[Forecast] ${this.forecast.points.length} values parsed (${this.forecast.unit || 'auto'}) - next: ${power}W in ${minutes}min`,
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.error(`[Forecast] Error processing pvforecast JSON: ${error.message}`);
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.warn(`[Forecast] Failed to set forecastStatus: ${err.message}`);
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.warn(`[Forecast] Failed to send forecast notification: ${err.message}`);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.smartloadmanager",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "This adapter allows devices to be controlled via PV surplus and thus achieve zero feed-in.",
5
5
  "author": {
6
6
  "name": "quorle",