iobroker.poolcontrol 0.1.2 → 0.2.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.
@@ -0,0 +1,340 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * controlHelper
5
+ * - Steuert Wartungsmodus, Rückspülung, Energie-Reset, Saison
6
+ * - Führt tägliche Umwälzprüfung (z. B. 18:00 Uhr) durch
7
+ * - Automatisches Nachpumpen, wenn Tagesziel nicht erreicht
8
+ * - Sendet Statusmeldungen über speech.queue
9
+ * - Nutzt Vorrangsteuerung über pump.mode = "controlHelper"
10
+ */
11
+
12
+ let adapter;
13
+ let backwashTimer = null;
14
+ let dailyTimer = null;
15
+ let previousPumpMode = null;
16
+
17
+ /**
18
+ * Initialisiert den Control-Helper.
19
+ *
20
+ * @param {import('iobroker').Adapter} a - ioBroker Adapterinstanz
21
+ */
22
+ function init(a) {
23
+ adapter = a;
24
+ adapter.log.info('[controlHelper] initialisiert');
25
+
26
+ // States abonnieren
27
+ adapter.subscribeStates('control.season.active');
28
+ adapter.subscribeStates('control.pump.backwash_start');
29
+ adapter.subscribeStates('control.pump.maintenance_active');
30
+ adapter.subscribeStates('control.energy.reset');
31
+ adapter.subscribeStates('control.circulation.check_time');
32
+
33
+ // Täglichen Check planen
34
+ _scheduleDailyCheck().catch(err =>
35
+ adapter.log.error(`[controlHelper] Fehler bei _scheduleDailyCheck(): ${err.message}`),
36
+ );
37
+
38
+ adapter.log.debug('[controlHelper] Überwachung der Control-States aktiviert');
39
+ }
40
+
41
+ /**
42
+ * Plant den täglichen Umwälzungscheck neu.
43
+ */
44
+ async function _scheduleDailyCheck() {
45
+ try {
46
+ if (dailyTimer) {
47
+ clearTimeout(dailyTimer);
48
+ }
49
+
50
+ const timeStr = (await adapter.getStateAsync('control.circulation.check_time'))?.val || '18:00';
51
+ const [hours, minutes] = timeStr.split(':').map(x => parseInt(x, 10));
52
+
53
+ const now = new Date();
54
+ const next = new Date();
55
+ next.setHours(hours, minutes, 0, 0);
56
+ if (next <= now) {
57
+ next.setDate(next.getDate() + 1);
58
+ }
59
+
60
+ const diffMs = next - now;
61
+ adapter.log.debug(`[controlHelper] Nächster Tages-Umwälzungscheck geplant für ${next.toLocaleTimeString()}`);
62
+
63
+ dailyTimer = setTimeout(async () => {
64
+ await _runDailyCirculationCheck();
65
+ await _scheduleDailyCheck();
66
+ }, diffMs);
67
+ } catch (err) {
68
+ adapter.log.error(`[controlHelper] Fehler bei _scheduleDailyCheck(): ${err.message}`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Führt den täglichen Umwälzungsbericht und ggf. Nachpumpen aus.
74
+ */
75
+ async function _runDailyCirculationCheck() {
76
+ try {
77
+ adapter.log.debug('[controlHelper] Starte Tages-Umwälzungscheck ...');
78
+
79
+ const seasonActive = (await adapter.getStateAsync('status.season_active'))?.val;
80
+ const mode = (await adapter.getStateAsync('control.circulation.mode'))?.val || 'off';
81
+ const dailyTotal = Math.round((await adapter.getStateAsync('circulation.daily_total'))?.val || 0);
82
+ const dailyRequired = Math.round((await adapter.getStateAsync('circulation.daily_required'))?.val || 0);
83
+ const collector = Number((await adapter.getStateAsync('temperature.collector.current'))?.val || 0);
84
+ const pool = Number((await adapter.getStateAsync('temperature.surface.current'))?.val || 0);
85
+
86
+ if (!seasonActive) {
87
+ adapter.log.debug('[controlHelper] Saison inaktiv – Tagesprüfung übersprungen.');
88
+ return;
89
+ }
90
+
91
+ if (!dailyRequired || dailyRequired <= 0) {
92
+ await _sendSpeech('Keine Zielumwälzmenge festgelegt – Tagesbericht übersprungen.');
93
+ return;
94
+ }
95
+
96
+ const percent = Math.min(100, Math.round((dailyTotal / dailyRequired) * 100));
97
+ const missing = Math.max(0, Math.round(dailyRequired - dailyTotal));
98
+ let message = '';
99
+
100
+ switch (mode) {
101
+ case 'notify':
102
+ message = `Heutige Umwälzung: ${dailyTotal} l (${percent} %). Es fehlen noch ${missing} l. Bitte ggf. manuell nachpumpen.`;
103
+ break;
104
+
105
+ case 'manual':
106
+ message = `Heutige Umwälzung: ${dailyTotal} l (${percent} %). Es fehlen noch ${missing} l. Bitte Pumpe manuell einschalten.`;
107
+ break;
108
+
109
+ case 'auto':
110
+ if (percent >= 100) {
111
+ message = `Tagesumwälzung abgeschlossen: ${dailyTotal} l (${percent} %). Kein Nachpumpen erforderlich.`;
112
+ } else if (collector > pool) {
113
+ message = `Heutige Umwälzung: ${dailyTotal} l (${percent} %). Es fehlen ${missing} l. Nachpumpen startet automatisch (Kollektor wärmer).`;
114
+ await _startAutoPumping(missing);
115
+ return;
116
+ } else {
117
+ message = `Heutige Umwälzung: ${dailyTotal} l (${percent} %). Kein automatisches Nachpumpen, Kollektor kälter als Pool.`;
118
+ }
119
+ break;
120
+
121
+ default:
122
+ adapter.log.debug(`[controlHelper] Modus '${mode}' → keine Aktion.`);
123
+ return;
124
+ }
125
+
126
+ await _sendSpeech(message);
127
+ await adapter.setStateAsync('control.circulation.last_report', { val: new Date().toISOString(), ack: true });
128
+ } catch (err) {
129
+ adapter.log.error(`[controlHelper] Fehler beim Tagescheck: ${err.message}`);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Startet automatisches Nachpumpen.
135
+ *
136
+ * @param {number} missingLiter - Fehlende Umwälzmenge in Litern
137
+ */
138
+ async function _startAutoPumping(missingLiter) {
139
+ try {
140
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
141
+
142
+ previousPumpMode = (await adapter.getStateAsync('pump.mode'))?.val || 'auto';
143
+ await adapter.setStateAsync('pump.mode', { val: 'controlHelper', ack: true });
144
+ await adapter.setStateAsync('pump.reason', { val: 'nachpumpen', ack: true });
145
+ await adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
146
+
147
+ adapter.log.info(`[controlHelper] Automatisches Nachpumpen gestartet (${missingLiter} l fehlen).`);
148
+ if (notify) {
149
+ await _sendSpeech(`Automatisches Nachpumpen gestartet. Es fehlen ${missingLiter} Liter.`);
150
+ }
151
+
152
+ const interval = setInterval(async () => {
153
+ const total = Math.round((await adapter.getStateAsync('circulation.daily_total'))?.val || 0);
154
+ const required = Math.round((await adapter.getStateAsync('circulation.daily_required'))?.val || 0);
155
+
156
+ if (total >= required) {
157
+ clearInterval(interval);
158
+ await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
159
+ await adapter.setStateAsync('pump.mode', { val: previousPumpMode, ack: true });
160
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
161
+ previousPumpMode = null;
162
+
163
+ adapter.log.info('[controlHelper] Nachpumpen abgeschlossen – Tagesziel erreicht.');
164
+ if (notify) {
165
+ await _sendSpeech('Nachpumpen abgeschlossen. Tagesziel erreicht.');
166
+ }
167
+ }
168
+ }, 60 * 1000);
169
+ } catch (err) {
170
+ adapter.log.error(`[controlHelper] Fehler beim automatischen Nachpumpen: ${err.message}`);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Reagiert auf Änderungen der States im Bereich control.*
176
+ *
177
+ * @param {string} id - Objekt-ID des geänderten States
178
+ * @param {ioBroker.State} state - Neuer State-Wert
179
+ */
180
+ async function handleStateChange(id, state) {
181
+ try {
182
+ if (!state || state.ack) {
183
+ return;
184
+ }
185
+
186
+ // === SAISONSTATUS ===
187
+ if (id.endsWith('control.season.active')) {
188
+ const newVal = !!state.val;
189
+ adapter.log.info(`[controlHelper] Poolsaison wurde ${newVal ? 'aktiviert' : 'deaktiviert'}.`);
190
+ await adapter.setStateAsync('status.season_active', { val: newVal, ack: true });
191
+ }
192
+
193
+ // === WARTUNGSMODUS ===
194
+ if (id.endsWith('control.pump.maintenance_active')) {
195
+ const active = !!state.val;
196
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
197
+
198
+ if (active) {
199
+ previousPumpMode = (await adapter.getStateAsync('pump.mode'))?.val || 'auto';
200
+ await adapter.setStateAsync('pump.mode', { val: 'controlHelper', ack: true });
201
+ await adapter.setStateAsync('pump.reason', { val: 'wartung', ack: true });
202
+ await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
203
+ adapter.log.info('[controlHelper] Wartungsmodus aktiviert. Automatik pausiert.');
204
+
205
+ if (notify) {
206
+ await _sendSpeech('Wartungsmodus aktiviert. Automatikfunktionen deaktiviert.');
207
+ }
208
+ } else {
209
+ await adapter.setStateAsync('pump.mode', { val: previousPumpMode, ack: true });
210
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
211
+ previousPumpMode = null;
212
+
213
+ adapter.log.info('[controlHelper] Wartungsmodus beendet. Automatik wieder aktiv.');
214
+ if (notify) {
215
+ await _sendSpeech('Wartungsmodus beendet. Automatikbetrieb wieder aktiv.');
216
+ }
217
+ }
218
+ }
219
+
220
+ // === RÜCKSPÜLUNG ===
221
+ if (id.endsWith('control.pump.backwash_start') && state.val === true) {
222
+ const duration = (await adapter.getStateAsync('control.pump.backwash_duration'))?.val || 1;
223
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
224
+ const prevMode = (await adapter.getStateAsync('pump.mode'))?.val || 'auto';
225
+ const active = (await adapter.getStateAsync('control.pump.backwash_active'))?.val;
226
+
227
+ if (active) {
228
+ adapter.log.warn('[controlHelper] Rückspülung bereits aktiv – neuer Start abgelehnt.');
229
+ return;
230
+ }
231
+
232
+ await adapter.setStateAsync('control.pump.backwash_active', { val: true, ack: true });
233
+ await adapter.setStateAsync('control.pump.backwash_start', { val: false, ack: true });
234
+ await adapter.setStateAsync('pump.mode', { val: 'controlHelper', ack: true });
235
+ await adapter.setStateAsync('pump.reason', { val: 'rückspülen', ack: true });
236
+ await adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
237
+
238
+ const durationText = duration === 1 ? 'eine Minute' : `${duration} Minuten`;
239
+ adapter.log.info(`[controlHelper] Rückspülung gestartet (${duration} Minuten).`);
240
+
241
+ if (notify) {
242
+ await _sendSpeech(`Rückspülung gestartet. Dauer ${durationText}.`);
243
+ }
244
+
245
+ if (backwashTimer) {
246
+ clearTimeout(backwashTimer);
247
+ }
248
+ backwashTimer = setTimeout(
249
+ async () => {
250
+ try {
251
+ await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
252
+ await adapter.setStateAsync('pump.mode', { val: prevMode, ack: true });
253
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
254
+ await adapter.setStateAsync('control.pump.backwash_active', { val: false, ack: true });
255
+
256
+ adapter.log.info('[controlHelper] Rückspülung beendet. Automatik wieder aktiv.');
257
+ if (notify) {
258
+ await _sendSpeech('Rückspülung abgeschlossen. Automatikmodus wieder aktiv.');
259
+ }
260
+ } catch (err) {
261
+ adapter.log.warn(`[controlHelper] Fehler beim Beenden der Rückspülung: ${err.message}`);
262
+ }
263
+ },
264
+ duration * 60 * 1000,
265
+ );
266
+ }
267
+
268
+ // === ENERGIEZÄHLER RESET ===
269
+ if (id.endsWith('control.energy.reset') && state.val === true) {
270
+ const now = new Date();
271
+ const timestamp = now.toLocaleString('de-DE');
272
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
273
+
274
+ adapter.log.info(`[controlHelper] Energiezähler wird vollständig zurückgesetzt (${timestamp}).`);
275
+
276
+ const consStates = [
277
+ 'consumption.total_kwh',
278
+ 'consumption.day_kwh',
279
+ 'consumption.week_kwh',
280
+ 'consumption.month_kwh',
281
+ 'consumption.year_kwh',
282
+ 'consumption.last_total_kwh',
283
+ 'consumption.offset_kwh',
284
+ 'costs.total_eur',
285
+ 'costs.day_eur',
286
+ 'costs.week_eur',
287
+ 'costs.month_eur',
288
+ 'costs.year_eur',
289
+ ];
290
+
291
+ for (const sid of consStates) {
292
+ await adapter.setStateAsync(sid, { val: 0, ack: true });
293
+ }
294
+
295
+ await adapter.setStateAsync('control.energy.reset', { val: false, ack: true });
296
+
297
+ const msg = `Energiezähler und Kosten wurden am ${timestamp} vollständig zurückgesetzt.`;
298
+ adapter.log.info(`[controlHelper] ${msg}`);
299
+ if (notify) {
300
+ await _sendSpeech(msg);
301
+ }
302
+ }
303
+ } catch (err) {
304
+ adapter.log.error(`[controlHelper] Fehler bei State-Änderung: ${err.message}`);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Sendet Text an speech.queue
310
+ *
311
+ * @param {string} text - Nachricht, die an speech.queue gesendet werden soll
312
+ */
313
+ async function _sendSpeech(text) {
314
+ if (!text) {
315
+ return;
316
+ }
317
+ try {
318
+ await adapter.setStateAsync('speech.queue', { val: text, ack: false });
319
+ adapter.log.debug(`[controlHelper] Nachricht an speech.queue: ${text}`);
320
+ } catch (err) {
321
+ adapter.log.warn(`[controlHelper] Fehler beim Senden an speech.queue: ${err.message}`);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Aufräumen
327
+ */
328
+ function cleanup() {
329
+ if (backwashTimer) {
330
+ clearTimeout(backwashTimer);
331
+ backwashTimer = null;
332
+ }
333
+ if (dailyTimer) {
334
+ clearTimeout(dailyTimer);
335
+ dailyTimer = null;
336
+ }
337
+ previousPumpMode = null;
338
+ }
339
+
340
+ module.exports = { init, handleStateChange, cleanup };
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * debugLogHelper (SystemCheck-Version)
5
+ * - Überwacht nur einen auswählbaren Bereich (z. B. pump.*, solar.*, runtime.*)
6
+ * - Loggt Änderungen fortlaufend in SystemCheck.debug_logs.log
7
+ * - Kann per SystemCheck.debug_logs.clear geleert werden
8
+ * - target_area wird automatisch erkannt (dynamisch durch States-Datei)
9
+ */
10
+
11
+ const debugLogHelper = {
12
+ adapter: null,
13
+ currentTarget: 'none',
14
+ subscribedTarget: null,
15
+ lastChange: {},
16
+ thresholdMs: 2000, // Mindestzeit zwischen Änderungen (ms)
17
+ buffer: '',
18
+ bufferTimer: null,
19
+
20
+ /**
21
+ * Initialisierung des Debug-Helpers.
22
+ *
23
+ * @param {import('iobroker').Adapter} adapter - ioBroker Adapter-Instanz
24
+ */
25
+ async init(adapter) {
26
+ this.adapter = adapter;
27
+
28
+ // --- SystemCheck-Ordner sicherstellen ---
29
+ await this.adapter.setObjectNotExistsAsync('SystemCheck', {
30
+ type: 'channel',
31
+ common: { name: 'SystemCheck (Diagnose und Tools)' },
32
+ native: {},
33
+ });
34
+
35
+ // States für clear und target_area überwachen
36
+ adapter.subscribeStates('SystemCheck.debug_logs.clear');
37
+ adapter.subscribeStates('SystemCheck.debug_logs.target_area');
38
+
39
+ // Initialwert für target_area lesen
40
+ const target = (await adapter.getStateAsync('SystemCheck.debug_logs.target_area'))?.val || 'none';
41
+ this.currentTarget = target;
42
+ if (target !== 'none') {
43
+ this._subscribeTarget(target);
44
+ } else {
45
+ adapter.log.debug('[debugLogHelper] Kein Bereich ausgewählt – Logger inaktiv.');
46
+ }
47
+
48
+ adapter.log.debug('[debugLogHelper] Initialisierung abgeschlossen');
49
+ },
50
+
51
+ /**
52
+ * Reagiert auf State-Änderungen
53
+ *
54
+ * @param {string} id - State-ID
55
+ * @param {ioBroker.State} state - State-Wert
56
+ */
57
+ async handleStateChange(id, state) {
58
+ if (!this.adapter || !state) {
59
+ return;
60
+ }
61
+
62
+ // Umschalten des überwachten Bereichs
63
+ if (id.endsWith('SystemCheck.debug_logs.target_area')) {
64
+ const newTarget = state.val || 'none';
65
+ await this._switchTarget(newTarget);
66
+ return;
67
+ }
68
+
69
+ // Clear-Button
70
+ if (id.endsWith('SystemCheck.debug_logs.clear') && state.val === true) {
71
+ await this._clearLog();
72
+ await this.adapter.setStateAsync('SystemCheck.debug_logs.clear', { val: false, ack: true });
73
+ return;
74
+ }
75
+
76
+ // Nur loggen, wenn der Bereich aktiv ist
77
+ if (!this.subscribedTarget || this.subscribedTarget === 'none') {
78
+ return;
79
+ }
80
+
81
+ // Nur Events aus dem überwachten Bereich aufnehmen
82
+ if (!id.includes(`.${this.subscribedTarget}.`)) {
83
+ return;
84
+ }
85
+
86
+ const now = Date.now();
87
+ const last = this.lastChange[id] || 0;
88
+ this.lastChange[id] = now;
89
+
90
+ if (now - last < this.thresholdMs) {
91
+ const msg = `[${new Date().toISOString()}] ${id} änderte sich zu schnell (${now - last} ms, val=${state.val}, ack=${state.ack})\n`;
92
+ await this._appendLog(msg);
93
+ }
94
+ },
95
+
96
+ /**
97
+ * Wechselt den aktiven Überwachungsbereich
98
+ *
99
+ * @param {string} newTarget - Name des neuen Bereichs (z.B. "pump", "solar", oder "none")
100
+ */
101
+ async _switchTarget(newTarget) {
102
+ if (this.subscribedTarget === newTarget) {
103
+ return;
104
+ }
105
+ if (this.subscribedTarget && this.subscribedTarget !== 'none') {
106
+ this.adapter.unsubscribeStates(`${this.subscribedTarget}.*`);
107
+ this.adapter.log.debug(`[debugLogHelper] Überwachung für Bereich "${this.subscribedTarget}" beendet.`);
108
+ }
109
+
110
+ this.subscribedTarget = newTarget;
111
+
112
+ if (newTarget === 'none') {
113
+ this.adapter.log.debug('[debugLogHelper] Kein Bereich aktiv.');
114
+ return;
115
+ }
116
+
117
+ this._subscribeTarget(newTarget);
118
+ this.adapter.log.debug(`[debugLogHelper] Überwachung für Bereich "${newTarget}" gestartet.`);
119
+ await this._appendLog(
120
+ `\n=== Debug-Log gestartet: Bereich "${newTarget}" @ ${new Date().toLocaleString()} ===\n`,
121
+ );
122
+ },
123
+
124
+ /**
125
+ * Abonniert States für den angegebenen Bereich
126
+ *
127
+ * @param {string} target - Name des zu überwachenden Bereichs
128
+ */
129
+ _subscribeTarget(target) {
130
+ this.adapter.subscribeStates(`${target}.*`);
131
+ this.subscribedTarget = target;
132
+ },
133
+
134
+ /**
135
+ * Log anhängen (fortlaufend)
136
+ *
137
+ * @param {string} message - Text, der in das fortlaufende Log geschrieben wird
138
+ */
139
+ async _appendLog(message) {
140
+ try {
141
+ this.buffer += message;
142
+
143
+ // Schreibe alle 5 Sekunden oder ab 2 KB
144
+ if (this.buffer.length > 2000) {
145
+ await this._flushBuffer();
146
+ } else if (!this.bufferTimer) {
147
+ this.bufferTimer = setTimeout(() => this._flushBuffer(), 5000);
148
+ }
149
+ } catch (err) {
150
+ this.adapter.log.warn(`[debugLogHelper] Fehler beim Anhängen an Log: ${err.message}`);
151
+ }
152
+ },
153
+
154
+ async _flushBuffer() {
155
+ try {
156
+ if (!this.buffer) {
157
+ return;
158
+ }
159
+ const current = (await this.adapter.getStateAsync('SystemCheck.debug_logs.log'))?.val || '';
160
+ const newVal = current + this.buffer;
161
+ await this.adapter.setStateAsync('SystemCheck.debug_logs.log', {
162
+ val: newVal.slice(-60000),
163
+ ack: true,
164
+ }); // max ~60k Zeichen
165
+ this.buffer = '';
166
+ this.bufferTimer = null;
167
+ } catch (err) {
168
+ this.adapter.log.warn(`[debugLogHelper] Fehler beim Schreiben in Log: ${err.message}`);
169
+ }
170
+ },
171
+
172
+ /**
173
+ * Löscht das Log komplett
174
+ */
175
+ async _clearLog() {
176
+ try {
177
+ await this.adapter.setStateAsync('SystemCheck.debug_logs.log', { val: '', ack: true });
178
+ this.buffer = '';
179
+ this.adapter.log.info('[debugLogHelper] Debug-Log gelöscht');
180
+ } catch (err) {
181
+ this.adapter.log.warn(`[debugLogHelper] Fehler beim Löschen des Logs: ${err.message}`);
182
+ }
183
+ },
184
+
185
+ cleanup() {
186
+ if (this.bufferTimer) {
187
+ clearTimeout(this.bufferTimer);
188
+ }
189
+ this.adapter.log.debug('[debugLogHelper] Cleanup ausgeführt');
190
+ },
191
+ };
192
+
193
+ module.exports = debugLogHelper;
@@ -18,7 +18,7 @@ const frostHelper = {
18
18
  // Minütlicher Check
19
19
  this._scheduleCheck();
20
20
 
21
- this.adapter.log.info('[frostHelper] initialisiert (Prüfung alle 60s)');
21
+ this.adapter.log.debug('[frostHelper] initialisiert (Prüfung alle 60s)');
22
22
  },
23
23
 
24
24
  _scheduleCheck() {
@@ -32,6 +32,13 @@ const frostHelper = {
32
32
 
33
33
  async _checkFrost() {
34
34
  try {
35
+ // --- NEU: Vorrangprüfung durch ControlHelper ---
36
+ const pumpStatus = (await this.adapter.getStateAsync('pump.status'))?.val || '';
37
+ if (pumpStatus.includes('controlHelper')) {
38
+ this.adapter.log.debug('[frostHelper] Vorrang durch ControlHelper aktiv – Frostschutz pausiert.');
39
+ return;
40
+ }
41
+
35
42
  // Nur aktiv im AUTO-Modus
36
43
  const mode = (await this.adapter.getStateAsync('pump.mode'))?.val;
37
44
  if (mode !== 'auto') {
@@ -68,13 +75,22 @@ const frostHelper = {
68
75
  shouldRun = false;
69
76
  }
70
77
 
78
+ // --- NEU: Sprachsignal für Frostschutz setzen ---
79
+ const oldVal = (await this.adapter.getStateAsync('speech.frost_active'))?.val;
80
+ if (oldVal !== shouldRun) {
81
+ await this.adapter.setStateChangedAsync('speech.frost_active', {
82
+ val: shouldRun,
83
+ ack: true,
84
+ });
85
+ }
86
+
71
87
  // Schalten nur, wenn sich etwas ändert
72
88
  if (shouldRun !== pumpActive) {
73
89
  await this.adapter.setStateAsync('pump.pump_switch', {
74
90
  val: shouldRun,
75
91
  ack: false,
76
92
  });
77
- this.adapter.log.info(
93
+ this.adapter.log.debug(
78
94
  `[frostHelper] Frostschutz → Pumpe ${shouldRun ? 'EIN' : 'AUS'} (Außen=${outside}°C, Grenze=${frostTemp}°C)`,
79
95
  );
80
96
  }