iobroker.poolcontrol 0.2.1 → 0.3.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,117 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * migrationHelper
5
+ * --------------------------------------------------
6
+ * Führt nachträgliche Struktur- oder State-Anpassungen
7
+ * für bestehende Installationen durch.
8
+ *
9
+ * - Wird beim Adapterstart einmalig ausgeführt.
10
+ * - Korrigiert veraltete Definitionen (z. B. Schreibrechte, persist-Flags, etc.)
11
+ *
12
+ * Version: 1.0.1
13
+ */
14
+
15
+ const migrationHelper = {
16
+ adapter: null,
17
+
18
+ /**
19
+ * Initialisiert den Migration-Helper.
20
+ * Wird einmalig beim Adapterstart aufgerufen.
21
+ *
22
+ * @param {ioBroker.Adapter} adapter - Die aktive Adapterinstanz
23
+ */
24
+ async init(adapter) {
25
+ this.adapter = adapter;
26
+ this.adapter.log.info('[migrationHelper] Starte Migration-Check ...');
27
+
28
+ try {
29
+ // ------------------------------------------------------
30
+ // Hier alle Migrationsroutinen nacheinander aufrufen
31
+ // ------------------------------------------------------
32
+ await this._fixSpeechQueue();
33
+ await this._fixSolarWarnActivePersist();
34
+
35
+ // Weitere Routinen folgen hier später:
36
+ // await this._ensurePumpReason();
37
+ // await this._cleanupOldStates();
38
+
39
+ this.adapter.log.debug('[migrationHelper] Migration-Checks abgeschlossen.');
40
+ } catch (err) {
41
+ this.adapter.log.warn(`[migrationHelper] Fehler beim Migration-Check: ${err.message}`);
42
+ }
43
+
44
+ this.adapter.log.info('[migrationHelper] Migration-Helper beendet.');
45
+ },
46
+
47
+ // ------------------------------------------------------
48
+ // Migration: Schreibrecht für speech.queue korrigieren
49
+ // ------------------------------------------------------
50
+
51
+ /**
52
+ * Prüft und korrigiert den State "speech.queue", falls er noch write:false gesetzt hat.
53
+ * Dadurch verschwinden Warnungen beim Schreiben (Read-only state ... written without ack).
54
+ */
55
+ async _fixSpeechQueue() {
56
+ const id = 'speech.queue';
57
+ try {
58
+ const obj = await this.adapter.getObjectAsync(id);
59
+ if (!obj) {
60
+ this.adapter.log.debug(`[migrationHelper] ${id} existiert nicht – keine Anpassung nötig.`);
61
+ return;
62
+ }
63
+
64
+ const isReadOnly = obj.common?.write === false;
65
+ if (isReadOnly) {
66
+ this.adapter.log.info(`[migrationHelper] Aktualisiere Schreibrecht für ${id} → write:true`);
67
+ await this.adapter.extendObjectAsync(id, {
68
+ common: {
69
+ write: true,
70
+ desc: 'Nur intern durch den Adapter beschreibbar (nicht manuell ändern!)',
71
+ },
72
+ });
73
+ } else {
74
+ this.adapter.log.debug(`[migrationHelper] ${id} ist bereits korrekt konfiguriert.`);
75
+ }
76
+ } catch (err) {
77
+ this.adapter.log.warn(`[migrationHelper] Fehler bei Prüfung von ${id}: ${err.message}`);
78
+ }
79
+ },
80
+
81
+ // ------------------------------------------------------
82
+ // Migration: persist-Flag für solar.warn_active ergänzen
83
+ // ------------------------------------------------------
84
+
85
+ /**
86
+ * Ergänzt persist:true bei solar.warn_active,
87
+ * damit die Einstellung (Warnfunktion aktivieren/deaktivieren)
88
+ * nach einem Neustart erhalten bleibt.
89
+ */
90
+ async _fixSolarWarnActivePersist() {
91
+ const id = 'solar.warn_active';
92
+ try {
93
+ const obj = await this.adapter.getObjectAsync(id);
94
+ if (!obj) {
95
+ this.adapter.log.debug(`[migrationHelper] ${id} existiert nicht – keine Anpassung nötig.`);
96
+ return;
97
+ }
98
+
99
+ const hasPersist = obj.common?.persist === true;
100
+ if (!hasPersist) {
101
+ this.adapter.log.info(`[migrationHelper] Ergänze persist:true für ${id}`);
102
+ await this.adapter.extendObjectAsync(id, {
103
+ common: {
104
+ persist: true,
105
+ desc: `${obj.common?.desc || ''} (automatisch per Migration persistiert)`,
106
+ },
107
+ });
108
+ } else {
109
+ this.adapter.log.debug(`[migrationHelper] ${id} ist bereits mit persist:true versehen.`);
110
+ }
111
+ } catch (err) {
112
+ this.adapter.log.warn(`[migrationHelper] Fehler bei Prüfung von ${id}: ${err.message}`);
113
+ }
114
+ },
115
+ };
116
+
117
+ module.exports = migrationHelper;
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pumpHelper2.js
5
+ * ----------------------------------------------------------
6
+ * Ergänzende Berechnungslogik für Pumpen-Livewerte
7
+ * (reeller Durchfluss, Prozentleistung, letzter Durchflusswert).
8
+ *
9
+ * Verwendet vorhandene States:
10
+ * - pump.current_power
11
+ * - pump.pump_max_watt
12
+ * - pump.pump_power_lph
13
+ * - pump.pump_switch
14
+ *
15
+ * Schreibt neue Werte in:
16
+ * - pump.live.current_power_w
17
+ * - pump.live.flow_current_lh
18
+ * - pump.live.flow_percent
19
+ * - pump.live.last_flow_lh
20
+ *
21
+ * Alle Zielstates sind persistent (siehe pumpStates2.js).
22
+ * ----------------------------------------------------------
23
+ * Version: 1.0.0
24
+ */
25
+
26
+ const pumpHelper2 = {
27
+ adapter: null,
28
+
29
+ /**
30
+ * Initialisiert den PumpHelper2
31
+ *
32
+ * @param {ioBroker.Adapter} adapter - Aktive ioBroker Adapterinstanz
33
+ */
34
+ async init(adapter) {
35
+ this.adapter = adapter;
36
+ this.adapter.log.info('[pumpHelper2] Initialisierung gestartet');
37
+
38
+ // Relevante States überwachen
39
+ this.adapter.subscribeStates('pump.current_power');
40
+ this.adapter.subscribeStates('pump.pump_switch');
41
+
42
+ // Initialwerte berechnen
43
+ await this._updateLiveValues();
44
+
45
+ this.adapter.log.info('[pumpHelper2] Erfolgreich initialisiert');
46
+ },
47
+
48
+ /**
49
+ * StateChange-Verarbeitung
50
+ *
51
+ * @param {string} id - State-ID
52
+ * @param {ioBroker.State | null | undefined} state - Neuer Statewert
53
+ */
54
+ async handleStateChange(id, state) {
55
+ if (!state || state.ack === false) {
56
+ return;
57
+ }
58
+
59
+ // Leistungsänderung → reelle Durchflusswerte aktualisieren
60
+ if (id.endsWith('pump.current_power')) {
61
+ await this._updateLiveValues();
62
+ }
63
+
64
+ // Pumpenstatus-Änderung → ggf. letzten Durchflusswert speichern
65
+ if (id.endsWith('pump.pump_switch')) {
66
+ const pumpOn = state.val === true;
67
+ if (!pumpOn) {
68
+ await this._storeLastFlowValue();
69
+ }
70
+ }
71
+ },
72
+
73
+ /**
74
+ * Führt die Berechnung der Livewerte durch.
75
+ * (Nur wenn gültige Basiswerte vorhanden sind.)
76
+ */
77
+ async _updateLiveValues() {
78
+ try {
79
+ const [currentPower, maxPower, nominalFlow] = await Promise.all([
80
+ this._getNumber('pump.current_power'),
81
+ this._getNumber('pump.pump_max_watt'),
82
+ this._getNumber('pump.pump_power_lph'),
83
+ ]);
84
+
85
+ // Schutz gegen ungültige Werte
86
+ if (maxPower <= 0 || nominalFlow <= 0) {
87
+ this.adapter.log.debug('[pumpHelper2] Ungültige Basiswerte, Berechnung übersprungen');
88
+ return;
89
+ }
90
+
91
+ // Prozentuale Auslastung
92
+ const flowPercent = Math.min(Math.max((currentPower / maxPower) * 100, 0), 100);
93
+
94
+ // Reeller Durchfluss
95
+ const flowCurrentLh = Math.round(nominalFlow * (currentPower / maxPower) * 10) / 10; // 1 Nachkommastelle
96
+
97
+ // In States schreiben
98
+ await this._setIfChanged('pump.live.current_power_w', currentPower);
99
+ await this._setIfChanged('pump.live.flow_current_lh', flowCurrentLh);
100
+ await this._setIfChanged('pump.live.flow_percent', flowPercent);
101
+
102
+ this.adapter.log.debug(
103
+ `[pumpHelper2] Reeller Durchfluss aktualisiert: ${flowCurrentLh} l/h (${flowPercent.toFixed(1)}%)`,
104
+ );
105
+ } catch (err) {
106
+ this.adapter.log.warn(`[pumpHelper2] Fehler bei _updateLiveValues: ${err.message}`);
107
+ }
108
+ },
109
+
110
+ /**
111
+ * Speichert den letzten bekannten Durchflusswert beim Pumpen-Stopp.
112
+ */
113
+ async _storeLastFlowValue() {
114
+ try {
115
+ const currentFlow = await this._getNumber('pump.live.flow_current_lh');
116
+ await this._setIfChanged('pump.live.last_flow_lh', currentFlow);
117
+ this.adapter.log.debug(`[pumpHelper2] Letzter Durchflusswert gespeichert: ${currentFlow} l/h`);
118
+ } catch (err) {
119
+ this.adapter.log.warn(`[pumpHelper2] Fehler bei _storeLastFlowValue: ${err.message}`);
120
+ }
121
+ },
122
+
123
+ /**
124
+ * Liest einen numerischen Statewert (oder 0 bei Fehler).
125
+ *
126
+ * @param {string} id - Objekt-ID des zu lesenden States
127
+ * @returns {Promise<number>} - Aktueller numerischer Wert oder 0
128
+ */
129
+ async _getNumber(id) {
130
+ const state = await this.adapter.getStateAsync(id);
131
+ const val = Number(state?.val);
132
+ return isNaN(val) ? 0 : val;
133
+ },
134
+
135
+ /**
136
+ * Schreibt neuen Wert nur, wenn er sich geändert hat.
137
+ *
138
+ * @param {string} id - Objekt-ID des zu schreibenden States
139
+ * @param {number} newVal - Neuer Wert, des gesetzt werden soll
140
+ */
141
+ async _setIfChanged(id, newVal) {
142
+ const current = await this.adapter.getStateAsync(id);
143
+ if (current && Number(current.val) === newVal) {
144
+ return;
145
+ }
146
+ await this.adapter.setStateAsync(id, { val: newVal, ack: true });
147
+ },
148
+
149
+ /**
150
+ * Cleanup bei Adapter-Unload
151
+ * Wird aktuell nur als Platzhalter verwendet.
152
+ */
153
+ cleanup() {
154
+ // Derzeit keine Timer oder Intervalle vorhanden
155
+ // Platzhalter für zukünftige Erweiterungen
156
+ this.adapter?.log.debug('[pumpHelper2] Cleanup ausgeführt.');
157
+ },
158
+ };
159
+
160
+ module.exports = pumpHelper2;
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pumpHelper3.js
5
+ * ----------------------------------------------------------
6
+ * Lern- und Analysemodul für die Pumpe
7
+ * - Ermittelt Durchschnitts- und Normalwerte aus realen Laufzyklen
8
+ * - Berechnet Abweichungen (Leistung & Durchfluss)
9
+ * - Liefert textbasierte Statusbewertung ("im Normalbereich" etc.)
10
+ * - Keine Sprach- oder Queue-Ausgabe
11
+ *
12
+ * Verwaltet:
13
+ * pump.learning.*
14
+ *
15
+ * Abhängigkeiten:
16
+ * - pump.live.current_power_w
17
+ * - pump.live.flow_current_lh
18
+ * - pump.pump_switch
19
+ *
20
+ * Version: 1.0.0
21
+ */
22
+
23
+ const pumpHelper3 = {
24
+ adapter: null,
25
+
26
+ /** interner Speicher */
27
+ currentSessionValues: {
28
+ power: [],
29
+ flow: [],
30
+ },
31
+
32
+ /**
33
+ * Initialisiert den Helper
34
+ *
35
+ * @param {ioBroker.Adapter} adapter - aktive Adapterinstanz
36
+ */
37
+ async init(adapter) {
38
+ this.adapter = adapter;
39
+ this.adapter.log.info('[pumpHelper3] Initialisierung gestartet');
40
+
41
+ // Relevante States überwachen
42
+ this.adapter.subscribeStates('pump.pump_switch');
43
+ this.adapter.subscribeStates('pump.live.current_power_w');
44
+ this.adapter.subscribeStates('pump.live.flow_current_lh');
45
+
46
+ this.adapter.log.info('[pumpHelper3] Erfolgreich initialisiert');
47
+ },
48
+
49
+ /**
50
+ * Verarbeitet State-Änderungen
51
+ *
52
+ * @param {string} id - State-ID
53
+ * @param {ioBroker.State} state - neuer Statewert
54
+ */
55
+ async handleStateChange(id, state) {
56
+ if (!state || state.ack === false) {
57
+ return;
58
+ }
59
+
60
+ try {
61
+ if (id.endsWith('pump.pump_switch')) {
62
+ const isOn = state.val === true;
63
+ if (isOn) {
64
+ // Pumpe startet → neue Sitzung
65
+ this.currentSessionValues.power = [];
66
+ this.currentSessionValues.flow = [];
67
+ this.adapter.log.debug('[pumpHelper3] Neue Lern-Session gestartet');
68
+ } else {
69
+ // Pumpe stoppt → Lernwerte aktualisieren
70
+ await this._finalizeLearningCycle();
71
+ }
72
+ }
73
+
74
+ // Wenn Pumpe läuft, aktuelle Werte sammeln
75
+ const pumpRunning = !!(await this.adapter.getStateAsync('pump.pump_switch'))?.val;
76
+ if (pumpRunning) {
77
+ if (id.endsWith('pump.live.current_power_w')) {
78
+ this._pushValue('power', state.val);
79
+ }
80
+ if (id.endsWith('pump.live.flow_current_lh')) {
81
+ this._pushValue('flow', state.val);
82
+ }
83
+ }
84
+
85
+ // Bei jeder Änderung der Livewerte aktuelle Abweichung bewerten
86
+ await this._updateDeviationAndStatus();
87
+ } catch (err) {
88
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei handleStateChange: ${err.message}`);
89
+ }
90
+ },
91
+
92
+ /**
93
+ * Fügt einen neuen Wert in den temporären Speicher ein.
94
+ *
95
+ * @param {"power"|"flow"} type - Art des Wertes
96
+ * @param {number} value - neuer Messwert
97
+ */
98
+ _pushValue(type, value) {
99
+ if (!this.currentSessionValues[type]) {
100
+ this.currentSessionValues[type] = [];
101
+ }
102
+ if (typeof value === 'number' && value > 0) {
103
+ this.currentSessionValues[type].push(value);
104
+ }
105
+ },
106
+
107
+ /**
108
+ * Wird aufgerufen, wenn ein Pumpenlauf endet.
109
+ * Berechnet Durchschnittswerte des Laufs und aktualisiert Lernfelder.
110
+ */
111
+ async _finalizeLearningCycle() {
112
+ try {
113
+ const { power, flow } = this.currentSessionValues;
114
+ if (power.length === 0 || flow.length === 0) {
115
+ this.adapter.log.debug('[pumpHelper3] Keine Werte zum Lernen vorhanden, Zyklus übersprungen');
116
+ return;
117
+ }
118
+
119
+ const avgPower = this._average(power);
120
+ const avgFlow = this._average(flow);
121
+
122
+ // Vorhandene Lernwerte lesen
123
+ const learnedPower = (await this._getNumber('pump.learning.learned_avg_power_w')) || 0;
124
+ const learnedFlow = (await this._getNumber('pump.learning.learned_avg_flow_lh')) || 0;
125
+ const cycles = (await this._getNumber('pump.learning.learning_cycles_total')) || 0;
126
+
127
+ // Gleitenden Durchschnitt berechnen
128
+ const newCycles = cycles + 1;
129
+ const newAvgPower = (learnedPower * cycles + avgPower) / newCycles;
130
+ const newAvgFlow = (learnedFlow * cycles + avgFlow) / newCycles;
131
+
132
+ // Normalbereiche ±15 %
133
+ const rangePowerLow = Math.round(newAvgPower * 0.85);
134
+ const rangePowerHigh = Math.round(newAvgPower * 1.15);
135
+ const rangeFlowLow = Math.round(newAvgFlow * 0.85);
136
+ const rangeFlowHigh = Math.round(newAvgFlow * 1.15);
137
+
138
+ // States schreiben
139
+ await this.adapter.setStateAsync('pump.learning.learned_avg_power_w', {
140
+ val: Math.round(newAvgPower),
141
+ ack: true,
142
+ });
143
+ await this.adapter.setStateAsync('pump.learning.learned_avg_flow_lh', {
144
+ val: Math.round(newAvgFlow),
145
+ ack: true,
146
+ });
147
+ await this.adapter.setStateAsync('pump.learning.normal_range_power_low', { val: rangePowerLow, ack: true });
148
+ await this.adapter.setStateAsync('pump.learning.normal_range_power_high', {
149
+ val: rangePowerHigh,
150
+ ack: true,
151
+ });
152
+ await this.adapter.setStateAsync('pump.learning.normal_range_flow_low', { val: rangeFlowLow, ack: true });
153
+ await this.adapter.setStateAsync('pump.learning.normal_range_flow_high', { val: rangeFlowHigh, ack: true });
154
+ await this.adapter.setStateAsync('pump.learning.learning_cycles_total', { val: newCycles, ack: true });
155
+
156
+ this.adapter.log.debug(
157
+ `[pumpHelper3] Lernzyklus #${newCycles} abgeschlossen (Power Ø${avgPower}W, Flow Ø${avgFlow}l/h)`,
158
+ );
159
+
160
+ // Speicher leeren
161
+ this.currentSessionValues.power = [];
162
+ this.currentSessionValues.flow = [];
163
+ } catch (err) {
164
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei _finalizeLearningCycle: ${err.message}`);
165
+ }
166
+ },
167
+
168
+ /**
169
+ * Bewertet aktuelle Abweichungen und schreibt Status.
170
+ */
171
+ async _updateDeviationAndStatus() {
172
+ try {
173
+ const currentPower = await this._getNumber('pump.live.current_power_w');
174
+ const currentFlow = await this._getNumber('pump.live.flow_current_lh');
175
+ const avgPower = await this._getNumber('pump.learning.learned_avg_power_w');
176
+ const avgFlow = await this._getNumber('pump.learning.learned_avg_flow_lh');
177
+
178
+ if (avgPower <= 0 || avgFlow <= 0) {
179
+ return;
180
+ } // Noch kein Lernwert vorhanden
181
+
182
+ // Prozentuale Abweichungen
183
+ const deviationPower = ((currentPower - avgPower) / avgPower) * 100;
184
+ const deviationFlow = ((currentFlow - avgFlow) / avgFlow) * 100;
185
+
186
+ await this.adapter.setStateAsync('pump.learning.deviation_power_percent', {
187
+ val: Math.round(deviationPower * 10) / 10,
188
+ ack: true,
189
+ });
190
+ await this.adapter.setStateAsync('pump.learning.deviation_flow_percent', {
191
+ val: Math.round(deviationFlow * 10) / 10,
192
+ ack: true,
193
+ });
194
+
195
+ // Bewertung
196
+ const statusText = this._getStatusText(deviationPower, deviationFlow);
197
+ await this.adapter.setStateAsync('pump.learning.status_text', { val: statusText, ack: true });
198
+ } catch (err) {
199
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei _updateDeviationAndStatus: ${err.message}`);
200
+ }
201
+ },
202
+
203
+ /**
204
+ * Liefert textbasierte Bewertung anhand der Abweichung.
205
+ *
206
+ * @param devPower - Aktuelle Abweichung der Leistung (W)
207
+ * @param devFlow - Aktuelle Abweichung des Durchflusses (L/H)
208
+ */
209
+ _getStatusText(devPower, devFlow) {
210
+ const absPower = Math.abs(devPower);
211
+ const absFlow = Math.abs(devFlow);
212
+
213
+ if (absPower <= 15 && absFlow <= 15) {
214
+ return 'Pumpe läuft im Normalbereich';
215
+ }
216
+ if (devPower < -15 || devFlow < -15) {
217
+ return 'Pumpe läuft unterhalb des Normalbereichs (möglicher Filterdruck)';
218
+ }
219
+ if (devPower > 15 || devFlow > 15) {
220
+ return 'Pumpe läuft oberhalb des Normalbereichs (möglicher Luftstau)';
221
+ }
222
+ return 'Pumpe außerhalb des bekannten Bereichs';
223
+ },
224
+
225
+ /**
226
+ * Erzeugt Durchschnittswerte aus einem Array numerischer Werte.
227
+ *
228
+ * @param {number[]} arr - Wertearray
229
+ * @returns {number} - Durchschnittswert
230
+ */
231
+ _average(arr) {
232
+ if (!arr || arr.length === 0) {
233
+ return 0;
234
+ }
235
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
236
+ },
237
+
238
+ /**
239
+ * Liest einen numerischen Statewert oder 0 bei Fehler.
240
+ *
241
+ * @param {string} id - Objekt-ID
242
+ * @returns {Promise<number>} - Gelesener Wert oder 0
243
+ */
244
+ async _getNumber(id) {
245
+ const state = await this.adapter.getStateAsync(id);
246
+ const val = Number(state?.val);
247
+ return isNaN(val) ? 0 : val;
248
+ },
249
+
250
+ /**
251
+ * Cleanup bei Adapter-Unload
252
+ */
253
+ cleanup() {
254
+ this.currentSessionValues = { power: [], flow: [] };
255
+ this.adapter?.log.debug('[pumpHelper3] Cleanup ausgeführt.');
256
+ },
257
+ };
258
+
259
+ module.exports = pumpHelper3;
@@ -135,14 +135,24 @@ const runtimeHelper = {
135
135
  await this.adapter.setStateAsync('runtime.start_count_today', { val: this.startCountToday, ack: true });
136
136
 
137
137
  // Umwälzmenge berechnen
138
- const pumpLph = (await this.adapter.getStateAsync('pump.pump_power_lph'))?.val || 0;
138
+ // Reeller Durchflusswert aus pump.live.flow_current_lh
139
+ const liveFlowLh = (await this.adapter.getStateAsync('pump.live.flow_current_lh'))?.val || 0;
140
+
141
+ if (liveFlowLh <= 0) {
142
+ this.adapter.log.debug('[runtimeHelper] Kein Live-Durchflusswert vorhanden, Berechnung übersprungen');
143
+ return;
144
+ }
145
+
146
+ // Poolparameter laden
139
147
  const poolSize = (await this.adapter.getStateAsync('general.pool_size'))?.val || 0;
140
148
  const minCirc = (await this.adapter.getStateAsync('general.min_circulation_per_day'))?.val || 1;
141
149
 
142
- const dailyTotal = Math.round((effectiveToday / 3600) * pumpLph);
150
+ // Berechnung der realen Tagesumwälzung (Liter)
151
+ const dailyTotal = Math.round((effectiveToday / 3600) * liveFlowLh);
143
152
  const dailyRequired = Math.round(poolSize * minCirc);
144
153
  const dailyRemaining = Math.max(dailyRequired - dailyTotal, 0);
145
154
 
155
+ // Werte schreiben
146
156
  await this.adapter.setStateAsync('circulation.daily_total', { val: dailyTotal, ack: true });
147
157
  await this.adapter.setStateAsync('circulation.daily_required', { val: dailyRequired, ack: true });
148
158
  await this.adapter.setStateAsync('circulation.daily_remaining', { val: dailyRemaining, ack: true });
@@ -33,8 +33,8 @@ const solarHelper = {
33
33
  async _checkSolar() {
34
34
  try {
35
35
  // --- NEU: Vorrangprüfung durch ControlHelper ---
36
- const pumpStatus = (await this.adapter.getStateAsync('pump.status'))?.val || '';
37
- if (pumpStatus.includes('controlHelper')) {
36
+ const activeHelper = (await this.adapter.getStateAsync('pump.active_helper'))?.val || '';
37
+ if (activeHelper === 'controlHelper') {
38
38
  this.adapter.log.debug('[solarHelper] Vorrang durch ControlHelper aktiv – Solarregelung pausiert.');
39
39
  return;
40
40
  }
@@ -12,6 +12,7 @@ const statusHelper = {
12
12
  adapter: null,
13
13
  midnightTimer: null,
14
14
  pumpOn: null, // interner Merker für Pumpenstatus
15
+ _lastSummaryUpdate: 0, // FIX: Zeitstempel für Throttle-Schutz
15
16
 
16
17
  init(adapter) {
17
18
  this.adapter = adapter;
@@ -95,8 +96,23 @@ const statusHelper = {
95
96
  await this.updateSummary();
96
97
  },
97
98
 
99
+ // FIX: Hilfsfunktion zur sicheren Formatierung
100
+ safeValue(v, digits = 1) {
101
+ if (v == null || isNaN(v)) {
102
+ return '–';
103
+ }
104
+ return Number(v).toFixed(digits);
105
+ },
106
+
98
107
  async updateSummary() {
99
108
  try {
109
+ // FIX: Throttle - Mehrfachupdates innerhalb 1 Sekunde vermeiden
110
+ if (Date.now() - this._lastSummaryUpdate < 1000) {
111
+ this.adapter.log.debug('[statusHelper] UpdateSummary übersprungen (Throttle)');
112
+ return;
113
+ }
114
+ this._lastSummaryUpdate = Date.now();
115
+
100
116
  // Werte laden
101
117
  const pumpStatus = (await this.adapter.getStateAsync('pump.status'))?.val || 'unbekannt';
102
118
  const pumpMode = (await this.adapter.getStateAsync('pump.mode'))?.val || 'unknown';
@@ -112,32 +128,41 @@ const statusHelper = {
112
128
  // Laufzeit formatieren
113
129
  const h = Math.floor(runtimeToday / 3600);
114
130
  const m = Math.floor((runtimeToday % 3600) / 60);
115
- const runtimeFormatted = `${h}h ${m}m`;
131
+ const runtimeFormatted = isNaN(h) || isNaN(m) ? '0h 00m' : `${h}h ${m}m`; // FIX: Schutz gegen NaN
116
132
 
117
133
  // Umwälzungsquote
118
134
  let circulationPct = 0;
119
135
  if (dailyRequired > 0) {
120
136
  circulationPct = Math.round((dailyTotal / dailyRequired) * 100);
121
137
  }
138
+ if (isNaN(circulationPct)) {
139
+ circulationPct = 0;
140
+ } // FIX: NaN-Absicherung
122
141
 
123
142
  // Text bauen
124
143
  let text = `Pumpe: ${pumpStatus}`;
125
144
  if (pumpMode && pumpMode !== 'unknown') {
126
145
  text += ` (Modus: ${pumpMode})`;
127
146
  }
147
+
148
+ const safe = this.safeValue.bind(this); // FIX: Kurzreferenz
128
149
  if (poolTemp != null) {
129
- text += `. Pool: ${poolTemp.toFixed(1)} °C`;
150
+ text += `. Pool: ${safe(poolTemp)} °C`;
130
151
  }
131
152
  if (collectorTemp != null) {
132
- text += `, Kollektor: ${collectorTemp.toFixed(1)} °C`;
153
+ text += `, Kollektor: ${safe(collectorTemp)} °C`;
133
154
  }
134
155
  if (outsideTemp != null) {
135
- text += `, Außentemperatur: ${outsideTemp.toFixed(1)} °C`;
156
+ text += `, Außentemperatur: ${safe(outsideTemp)} °C`;
136
157
  }
137
158
  text += `. Tageslaufzeit: ${runtimeFormatted} (${circulationPct}% der Soll-Umwälzung).`;
138
159
 
139
- // In States schreiben
140
- await this.adapter.setStateAsync('status.summary', { val: text, ack: true });
160
+ // In States schreiben (nur bei Änderung)
161
+ const current = (await this.adapter.getStateAsync('status.summary'))?.val;
162
+ if (current !== text) {
163
+ await this.adapter.setStateAsync('status.summary', { val: text, ack: true });
164
+ }
165
+
141
166
  await this.adapter.setStateAsync('status.last_summary_update', {
142
167
  val: new Date().toISOString(),
143
168
  ack: true,
@@ -147,10 +172,10 @@ const statusHelper = {
147
172
  const json = {
148
173
  pump: pumpStatus,
149
174
  mode: pumpMode,
150
- pool: poolTemp,
151
- collector: collectorTemp,
152
- outside: outsideTemp,
153
- runtime_today: runtimeToday,
175
+ pool: poolTemp ?? null,
176
+ collector: collectorTemp ?? null,
177
+ outside: outsideTemp ?? null,
178
+ runtime_today: runtimeToday ?? 0,
154
179
  runtime_formatted: runtimeFormatted,
155
180
  circulation_pct: circulationPct,
156
181
  };