iobroker.poolcontrol 0.2.0 → 0.2.2

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.
@@ -2,14 +2,17 @@
2
2
 
3
3
  /**
4
4
  * controlHelper
5
- * - Überwacht States im Bereich "control"
6
- * - Steuert manuelle Aktionen: Rückspülung, Wartungsmodus
7
- * - Synchronisiert mit Pumpenmodus und Sprach-/Benachrichtigungssystem
8
- * - Erweiterung: automatische Sprach- und Benachrichtigungs-Ausgabe bei Start/Ende
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"
9
10
  */
10
11
 
11
12
  let adapter;
12
13
  let backwashTimer = null;
14
+ let dailyTimer = null;
15
+ let previousPumpMode = null;
13
16
 
14
17
  /**
15
18
  * Initialisiert den Control-Helper.
@@ -24,15 +27,156 @@ function init(a) {
24
27
  adapter.subscribeStates('control.season.active');
25
28
  adapter.subscribeStates('control.pump.backwash_start');
26
29
  adapter.subscribeStates('control.pump.maintenance_active');
27
- adapter.subscribeStates('control.energy.reset'); // NEU: Energie-Reset-Button
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
+ );
28
37
 
29
38
  adapter.log.debug('[controlHelper] Überwachung der Control-States aktiviert');
30
39
  }
31
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.active_helper', { val: 'controlHelper', ack: true });
145
+ await adapter.setStateAsync('pump.reason', { val: 'nachpumpen', ack: true });
146
+ await adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
147
+
148
+ adapter.log.info(`[controlHelper] Automatisches Nachpumpen gestartet (${missingLiter} l fehlen).`);
149
+ if (notify) {
150
+ await _sendSpeech(`Automatisches Nachpumpen gestartet. Es fehlen ${missingLiter} Liter.`);
151
+ }
152
+
153
+ const interval = setInterval(async () => {
154
+ const total = Math.round((await adapter.getStateAsync('circulation.daily_total'))?.val || 0);
155
+ const required = Math.round((await adapter.getStateAsync('circulation.daily_required'))?.val || 0);
156
+
157
+ if (total >= required) {
158
+ clearInterval(interval);
159
+ await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
160
+ await adapter.setStateAsync('pump.mode', { val: previousPumpMode, ack: true });
161
+ await adapter.setStateAsync('pump.active_helper', { val: '', ack: true });
162
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
163
+ previousPumpMode = null;
164
+
165
+ adapter.log.info('[controlHelper] Nachpumpen abgeschlossen – Tagesziel erreicht.');
166
+ if (notify) {
167
+ await _sendSpeech('Nachpumpen abgeschlossen. Tagesziel erreicht.');
168
+ }
169
+ }
170
+ }, 60 * 1000);
171
+ } catch (err) {
172
+ adapter.log.error(`[controlHelper] Fehler beim automatischen Nachpumpen: ${err.message}`);
173
+ }
174
+ }
175
+
32
176
  /**
33
177
  * Reagiert auf Änderungen der States im Bereich control.*
34
178
  *
35
- * @param {string} id - ID des geänderten States
179
+ * @param {string} id - Objekt-ID des geänderten States
36
180
  * @param {ioBroker.State} state - Neuer State-Wert
37
181
  */
38
182
  async function handleStateChange(id, state) {
@@ -41,39 +185,43 @@ async function handleStateChange(id, state) {
41
185
  return;
42
186
  }
43
187
 
44
- // === SAISONSTATUS =====================================================
188
+ // === SAISONSTATUS ===
45
189
  if (id.endsWith('control.season.active')) {
46
190
  const newVal = !!state.val;
47
191
  adapter.log.info(`[controlHelper] Poolsaison wurde ${newVal ? 'aktiviert' : 'deaktiviert'}.`);
48
192
  await adapter.setStateAsync('status.season_active', { val: newVal, ack: true });
49
193
  }
50
194
 
51
- // === WARTUNGSMODUS ====================================================
195
+ // === WARTUNGSMODUS ===
52
196
  if (id.endsWith('control.pump.maintenance_active')) {
53
197
  const active = !!state.val;
54
198
  const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
55
199
 
56
200
  if (active) {
57
- await adapter.setStateAsync('pump.mode', { val: 'override', ack: true });
201
+ previousPumpMode = (await adapter.getStateAsync('pump.mode'))?.val || 'auto';
202
+ await adapter.setStateAsync('pump.mode', { val: 'controlHelper', ack: true });
203
+ await adapter.setStateAsync('pump.reason', { val: 'wartung', ack: true });
204
+ await adapter.setStateAsync('pump.active_helper', { val: 'controlHelper', ack: true });
58
205
  await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
59
206
  adapter.log.info('[controlHelper] Wartungsmodus aktiviert. Automatik pausiert.');
60
207
 
61
208
  if (notify) {
62
- await sendNotification(
63
- 'Wartungsmodus aktiviert. Automatikfunktionen sind vorübergehend deaktiviert.',
64
- );
209
+ await _sendSpeech('Wartungsmodus aktiviert. Automatikfunktionen deaktiviert.');
65
210
  }
66
211
  } else {
67
- await adapter.setStateAsync('pump.mode', { val: 'auto', ack: true });
68
- adapter.log.info('[controlHelper] Wartungsmodus beendet. Automatik wieder aktiv.');
212
+ await adapter.setStateAsync('pump.mode', { val: previousPumpMode, ack: true });
213
+ await adapter.setStateAsync('pump.active_helper', { val: '', ack: true });
214
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
215
+ previousPumpMode = null;
69
216
 
217
+ adapter.log.info('[controlHelper] Wartungsmodus beendet. Automatik wieder aktiv.');
70
218
  if (notify) {
71
- await sendNotification('Wartungsmodus beendet. Automatikbetrieb ist wieder aktiv.');
219
+ await _sendSpeech('Wartungsmodus beendet. Automatikbetrieb wieder aktiv.');
72
220
  }
73
221
  }
74
222
  }
75
223
 
76
- // === RÜCKSPÜLUNG ======================================================
224
+ // === RÜCKSPÜLUNG ===
77
225
  if (id.endsWith('control.pump.backwash_start') && state.val === true) {
78
226
  const duration = (await adapter.getStateAsync('control.pump.backwash_duration'))?.val || 1;
79
227
  const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
@@ -86,14 +234,17 @@ async function handleStateChange(id, state) {
86
234
  }
87
235
 
88
236
  await adapter.setStateAsync('control.pump.backwash_active', { val: true, ack: true });
89
- await adapter.setStateAsync('pump.mode', { val: 'override', ack: true });
237
+ await adapter.setStateAsync('control.pump.backwash_start', { val: false, ack: true });
238
+ await adapter.setStateAsync('pump.mode', { val: 'controlHelper', ack: true });
239
+ await adapter.setStateAsync('pump.active_helper', { val: 'controlHelper', ack: true });
240
+ await adapter.setStateAsync('pump.reason', { val: 'rückspülen', ack: true });
90
241
  await adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
91
242
 
92
243
  const durationText = duration === 1 ? 'eine Minute' : `${duration} Minuten`;
93
- adapter.log.info(`[controlHelper] Rückspülung gestartet (Dauer: ${duration} Minuten).`);
244
+ adapter.log.info(`[controlHelper] Rückspülung gestartet (${duration} Minuten).`);
94
245
 
95
246
  if (notify) {
96
- await sendNotification(`Rückspülung gestartet. Dauer ${durationText}.`);
247
+ await _sendSpeech(`Rückspülung gestartet. Dauer ${durationText}.`);
97
248
  }
98
249
 
99
250
  if (backwashTimer) {
@@ -104,12 +255,13 @@ async function handleStateChange(id, state) {
104
255
  try {
105
256
  await adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
106
257
  await adapter.setStateAsync('pump.mode', { val: prevMode, ack: true });
258
+ await adapter.setStateAsync('pump.active_helper', { val: '', ack: true });
259
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
107
260
  await adapter.setStateAsync('control.pump.backwash_active', { val: false, ack: true });
108
261
 
109
262
  adapter.log.info('[controlHelper] Rückspülung beendet. Automatik wieder aktiv.');
110
-
111
263
  if (notify) {
112
- await sendNotification('Rückspülung abgeschlossen. Automatikmodus wieder aktiv.');
264
+ await _sendSpeech('Rückspülung abgeschlossen. Automatikmodus wieder aktiv.');
113
265
  }
114
266
  } catch (err) {
115
267
  adapter.log.warn(`[controlHelper] Fehler beim Beenden der Rückspülung: ${err.message}`);
@@ -119,35 +271,39 @@ async function handleStateChange(id, state) {
119
271
  );
120
272
  }
121
273
 
122
- // === ENERGIEZÄHLER RESET =============================================
274
+ // === ENERGIEZÄHLER RESET ===
123
275
  if (id.endsWith('control.energy.reset') && state.val === true) {
124
276
  const now = new Date();
125
277
  const timestamp = now.toLocaleString('de-DE');
126
278
  const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
127
279
 
128
- adapter.log.info(`[controlHelper] Energiezähler-Reset ausgelöst (${timestamp})`);
280
+ adapter.log.info(`[controlHelper] Energiezähler wird vollständig zurückgesetzt (${timestamp}).`);
129
281
 
130
- try {
131
- // Den eigentlichen Reset im consumptionHelper ausführen
132
- const consumptionHelper = require('../helpers/consumptionHelper');
133
- if (consumptionHelper && typeof consumptionHelper.resetAll === 'function') {
134
- await consumptionHelper.resetAll(adapter);
135
- } else {
136
- adapter.log.warn('[controlHelper] consumptionHelper.resetAll() nicht verfügbar');
137
- }
282
+ const consStates = [
283
+ 'consumption.total_kwh',
284
+ 'consumption.day_kwh',
285
+ 'consumption.week_kwh',
286
+ 'consumption.month_kwh',
287
+ 'consumption.year_kwh',
288
+ 'consumption.last_total_kwh',
289
+ 'consumption.offset_kwh',
290
+ 'costs.total_eur',
291
+ 'costs.day_eur',
292
+ 'costs.week_eur',
293
+ 'costs.month_eur',
294
+ 'costs.year_eur',
295
+ ];
138
296
 
139
- // Button wieder zurücksetzen
140
- await adapter.setStateAsync('control.energy.reset', { val: false, ack: true });
297
+ for (const sid of consStates) {
298
+ await adapter.setStateAsync(sid, { val: 0, ack: true });
299
+ }
141
300
 
142
- // Log & Sprach-/Benachrichtigungsausgabe
143
- const msg = `Energiezähler wurde am ${timestamp} vollständig zurückgesetzt.`;
144
- adapter.log.info(`[controlHelper] ${msg}`);
301
+ await adapter.setStateAsync('control.energy.reset', { val: false, ack: true });
145
302
 
146
- if (notify) {
147
- await sendNotification(msg);
148
- }
149
- } catch (err) {
150
- adapter.log.error(`[controlHelper] Fehler beim Energiezähler-Reset: ${err.message}`);
303
+ const msg = `Energiezähler und Kosten wurden am ${timestamp} vollständig zurückgesetzt.`;
304
+ adapter.log.info(`[controlHelper] ${msg}`);
305
+ if (notify) {
306
+ await _sendSpeech(msg);
151
307
  }
152
308
  }
153
309
  } catch (err) {
@@ -156,32 +312,35 @@ async function handleStateChange(id, state) {
156
312
  }
157
313
 
158
314
  /**
159
- * Leitet Benachrichtigungen an speechHelper weiter
160
- * (setzt nur speech.last_text, Versand erfolgt dort)
315
+ * Sendet Text an speech.queue
161
316
  *
162
- * @param {string} text - Nachrichtentext, der gesendet werden soll
317
+ * @param {string} text - Nachricht, die an speech.queue gesendet werden soll
163
318
  */
164
- async function sendNotification(text) {
319
+ async function _sendSpeech(text) {
165
320
  if (!text) {
166
321
  return;
167
322
  }
168
-
169
323
  try {
170
- await adapter.setStateAsync('speech.last_text', { val: text, ack: false });
171
- adapter.log.debug(`[controlHelper] Benachrichtigung an speechHelper weitergeleitet: ${text}`);
324
+ await adapter.setStateAsync('speech.queue', { val: text, ack: false });
325
+ adapter.log.debug(`[controlHelper] Nachricht an speech.queue: ${text}`);
172
326
  } catch (err) {
173
- adapter.log.warn(`[controlHelper] Fehler beim Weiterleiten der Benachrichtigung: ${err.message}`);
327
+ adapter.log.warn(`[controlHelper] Fehler beim Senden an speech.queue: ${err.message}`);
174
328
  }
175
329
  }
176
330
 
177
331
  /**
178
- * Stoppt Timer und räumt Ressourcen auf.
332
+ * Aufräumen
179
333
  */
180
334
  function cleanup() {
181
335
  if (backwashTimer) {
182
336
  clearTimeout(backwashTimer);
183
337
  backwashTimer = null;
184
338
  }
339
+ if (dailyTimer) {
340
+ clearTimeout(dailyTimer);
341
+ dailyTimer = null;
342
+ }
343
+ previousPumpMode = null;
185
344
  }
186
345
 
187
346
  module.exports = { init, handleStateChange, cleanup };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * controlHelper2
5
+ * ----------------------------------------------
6
+ * Rückspülerinnerungs-Logik (eigenständig)
7
+ * ----------------------------------------------
8
+ * - täglicher Check um 12:00 Uhr (lokale Host-Zeit)
9
+ * - prüft Intervall und letzte Rückspülung
10
+ * - erzeugt Erinnerungen (Log + speech.queue)
11
+ * - setzt Erinnerung automatisch zurück, wenn Rückspülung startet
12
+ * ----------------------------------------------
13
+ */
14
+
15
+ // NEU: Modulvariablen
16
+ let adapter;
17
+ let backwashReminderTimer = null;
18
+ let lastReminderDay = null; // verhindert doppelte tägliche Meldungen
19
+
20
+ // NEU: Initialisierung
21
+ /**
22
+ * Initialisiert den Rückspülerinnerungs-Helper
23
+ *
24
+ * @param {import('iobroker').Adapter} a – ioBroker-Adapterinstanz
25
+ */
26
+ function init(a) {
27
+ adapter = a;
28
+ adapter.log.info('[controlHelper2] Rückspülerinnerung initialisiert (täglicher Check um 12:00 Uhr).');
29
+
30
+ // Rückspülstart abonnieren, um Erinnerung zurückzusetzen
31
+ adapter.subscribeStates('control.pump.backwash_start');
32
+
33
+ // Täglichen Check planen
34
+ _scheduleBackwashReminder().catch(err =>
35
+ adapter.log.error(`[controlHelper2] Fehler bei _scheduleBackwashReminder(): ${err.message}`),
36
+ );
37
+ }
38
+
39
+ // NEU: Plant täglichen Timer um 12:00 Uhr lokale Zeit
40
+ async function _scheduleBackwashReminder() {
41
+ try {
42
+ if (backwashReminderTimer) {
43
+ clearTimeout(backwashReminderTimer);
44
+ }
45
+
46
+ const now = new Date();
47
+ const next = new Date();
48
+ next.setHours(12, 0, 0, 0); // 12:00 Uhr lokale Zeit
49
+
50
+ // Wenn 12:00 Uhr heute schon vorbei ist → morgen
51
+ if (next <= now) {
52
+ next.setDate(next.getDate() + 1);
53
+ }
54
+
55
+ const diffMs = next - now;
56
+ adapter.log.debug(`[controlHelper2] Nächster Rückspülerinnerungs-Check geplant für ${next.toLocaleString()}`);
57
+
58
+ backwashReminderTimer = setTimeout(async () => {
59
+ await _runBackwashReminderCheck();
60
+ await _scheduleBackwashReminder(); // neu planen
61
+ }, diffMs);
62
+ } catch (err) {
63
+ adapter.log.error(`[controlHelper2] Fehler bei _scheduleBackwashReminder(): ${err.message}`);
64
+ }
65
+ }
66
+
67
+ // NEU: Führt täglichen Erinnerungs-Check aus
68
+ async function _runBackwashReminderCheck() {
69
+ try {
70
+ adapter.log.debug('[controlHelper2] Starte Rückspülerinnerungs-Check ...');
71
+
72
+ const reminderActive = (await adapter.getStateAsync('control.pump.backwash_reminder_active'))?.val;
73
+ if (!reminderActive) {
74
+ adapter.log.debug('[controlHelper2] Rückspülerinnerung deaktiviert – Check übersprungen.');
75
+ return;
76
+ }
77
+
78
+ const intervalDays = Number((await adapter.getStateAsync('control.pump.backwash_interval_days'))?.val || 7);
79
+ const lastDateStr = (await adapter.getStateAsync('control.pump.backwash_last_date'))?.val || '';
80
+
81
+ const now = new Date();
82
+ const todayKey = now.toISOString().split('T')[0]; // yyyy-mm-dd
83
+
84
+ // Verhindert doppelte Erinnerungen am selben Tag
85
+ if (lastReminderDay === todayKey) {
86
+ adapter.log.debug('[controlHelper2] Erinnerung für heute bereits gesendet.');
87
+ return;
88
+ }
89
+
90
+ let daysSince = null;
91
+ if (lastDateStr) {
92
+ const lastDate = new Date(lastDateStr);
93
+ const diffMs = now - lastDate;
94
+ daysSince = Math.floor(diffMs / (1000 * 60 * 60 * 24));
95
+ }
96
+
97
+ if (daysSince === null) {
98
+ adapter.log.debug('[controlHelper2] Keine letzte Rückspülung bekannt – keine Erinnerung.');
99
+ return;
100
+ }
101
+
102
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
103
+ const speechEnabled = notify === true;
104
+
105
+ if (daysSince >= intervalDays) {
106
+ // Fällig oder überfällig
107
+ await adapter.setStateAsync('control.pump.backwash_required', { val: true, ack: true });
108
+ let text;
109
+
110
+ if (daysSince === intervalDays) {
111
+ text = 'Erinnerung: Rückspülung ist wieder fällig.';
112
+ } else {
113
+ const over = daysSince - intervalDays;
114
+ text = `Erinnerung: Rückspülung ist seit ${over} Tag${over === 1 ? '' : 'en'} überfällig.`;
115
+ }
116
+
117
+ adapter.log.info(`[controlHelper2] ${text}`);
118
+ if (speechEnabled) {
119
+ await _sendSpeech(text);
120
+ }
121
+
122
+ lastReminderDay = todayKey;
123
+ } else {
124
+ adapter.log.debug(`[controlHelper2] Rückspülung noch nicht fällig (${daysSince}/${intervalDays} Tage).`);
125
+ }
126
+ } catch (err) {
127
+ adapter.log.warn(`[controlHelper2] Fehler beim Erinnerungs-Check: ${err.message}`);
128
+ }
129
+ }
130
+
131
+ // NEU: Rücksetzung nach Rückspülstart
132
+ /**
133
+ * Reagiert auf State-Änderungen (z. B. Rückspülstart).
134
+ *
135
+ * @param {string} id – Objekt-ID des geänderten States
136
+ * @param {ioBroker.State} state – Neuer State-Wert
137
+ */
138
+ async function handleStateChange(id, state) {
139
+ try {
140
+ if (!state || state.ack) {
141
+ return;
142
+ }
143
+ if (!id.endsWith('control.pump.backwash_start') || !state.val) {
144
+ return;
145
+ }
146
+
147
+ adapter.log.debug('[controlHelper2] Rückspülstart erkannt – Erinnerung wird zurückgesetzt.');
148
+
149
+ await adapter.setStateAsync('control.pump.backwash_required', { val: false, ack: true });
150
+ await adapter.setStateAsync('control.pump.backwash_last_date', {
151
+ val: new Date().toISOString(),
152
+ ack: true,
153
+ });
154
+
155
+ const notify = (await adapter.getStateAsync('control.pump.notifications_enabled'))?.val;
156
+ if (notify) {
157
+ const text = 'Rückspülerinnerung wurde zurückgesetzt. Rückspülzyklus neu gestartet.';
158
+ adapter.log.info(`[controlHelper2] ${text}`);
159
+ await _sendSpeech(text);
160
+ }
161
+ } catch (err) {
162
+ adapter.log.warn(`[controlHelper2] Fehler bei handleStateChange(): ${err.message}`);
163
+ }
164
+ }
165
+
166
+ // NEU: Sprachausgabe
167
+ async function _sendSpeech(text) {
168
+ if (!text) {
169
+ return;
170
+ }
171
+ try {
172
+ await adapter.setStateAsync('speech.queue', { val: text, ack: false });
173
+ adapter.log.debug(`[controlHelper2] Nachricht an speech.queue: ${text}`);
174
+ } catch (err) {
175
+ adapter.log.warn(`[controlHelper2] Fehler beim Senden an speech.queue: ${err.message}`);
176
+ }
177
+ }
178
+
179
+ // NEU: Aufräumen
180
+ /**
181
+ * Stoppt den Rückspülerinnerungs-Timer und räumt Variablen auf.
182
+ *
183
+ * @returns {void}
184
+ */
185
+ function cleanup() {
186
+ if (backwashReminderTimer) {
187
+ clearTimeout(backwashReminderTimer);
188
+ backwashReminderTimer = null;
189
+ }
190
+ lastReminderDay = null;
191
+ adapter.log.debug('[controlHelper2] Cleanup ausgeführt.');
192
+ }
193
+
194
+ // NEU: Exporte
195
+ module.exports = { init, handleStateChange, cleanup };
@@ -42,10 +42,10 @@ const debugLogHelper = {
42
42
  if (target !== 'none') {
43
43
  this._subscribeTarget(target);
44
44
  } else {
45
- adapter.log.info('[debugLogHelper] Kein Bereich ausgewählt – Logger inaktiv.');
45
+ adapter.log.debug('[debugLogHelper] Kein Bereich ausgewählt – Logger inaktiv.');
46
46
  }
47
47
 
48
- adapter.log.info('[debugLogHelper] Initialisierung abgeschlossen');
48
+ adapter.log.debug('[debugLogHelper] Initialisierung abgeschlossen');
49
49
  },
50
50
 
51
51
  /**
@@ -104,18 +104,18 @@ const debugLogHelper = {
104
104
  }
105
105
  if (this.subscribedTarget && this.subscribedTarget !== 'none') {
106
106
  this.adapter.unsubscribeStates(`${this.subscribedTarget}.*`);
107
- this.adapter.log.info(`[debugLogHelper] Überwachung für Bereich "${this.subscribedTarget}" beendet.`);
107
+ this.adapter.log.debug(`[debugLogHelper] Überwachung für Bereich "${this.subscribedTarget}" beendet.`);
108
108
  }
109
109
 
110
110
  this.subscribedTarget = newTarget;
111
111
 
112
112
  if (newTarget === 'none') {
113
- this.adapter.log.info('[debugLogHelper] Kein Bereich aktiv.');
113
+ this.adapter.log.debug('[debugLogHelper] Kein Bereich aktiv.');
114
114
  return;
115
115
  }
116
116
 
117
117
  this._subscribeTarget(newTarget);
118
- this.adapter.log.info(`[debugLogHelper] Überwachung für Bereich "${newTarget}" gestartet.`);
118
+ this.adapter.log.debug(`[debugLogHelper] Überwachung für Bereich "${newTarget}" gestartet.`);
119
119
  await this._appendLog(
120
120
  `\n=== Debug-Log gestartet: Bereich "${newTarget}" @ ${new Date().toLocaleString()} ===\n`,
121
121
  );
@@ -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 activeHelper = (await this.adapter.getStateAsync('pump.active_helper'))?.val || '';
37
+ if (activeHelper === '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
  }