iobroker.poolcontrol 0.2.0 → 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.
@@ -7,6 +7,7 @@
7
7
  * - Schreibt Werte in die States (Objekte werden in runtimeStates.js angelegt)
8
8
  * - Nutzt den zentralen Boolean pump.pump_switch
9
9
  * - NEU: zählt Starts, aktuelle Laufzeit, Saisonlaufzeit
10
+ * - NEU: stellt formatiert gespeicherte Laufzeiten nach Neustart korrekt wieder her
10
11
  */
11
12
 
12
13
  const runtimeHelper = {
@@ -35,13 +36,13 @@ const runtimeHelper = {
35
36
  // Erst nach Restore einmal berechnen
36
37
  this._updateStates();
37
38
 
38
- this.adapter.log.info('[runtimeHelper] initialisiert (mit Restore)');
39
+ this.adapter.log.debug('[runtimeHelper] initialisiert (mit Restore)');
39
40
  })
40
41
  .catch(err => {
41
42
  this.adapter.log.warn(`[runtimeHelper] Restore fehlgeschlagen: ${err.message}`);
42
43
  this._scheduleDailyReset();
43
44
  this._updateStates();
44
- this.adapter.log.info('[runtimeHelper] initialisiert (ohne Restore)');
45
+ this.adapter.log.debug('[runtimeHelper] initialisiert (ohne Restore)');
45
46
  });
46
47
  },
47
48
 
@@ -51,10 +52,10 @@ const runtimeHelper = {
51
52
  const seasonRaw = (await this.adapter.getStateAsync('runtime.season_total'))?.val;
52
53
  const countRaw = (await this.adapter.getStateAsync('runtime.start_count_today'))?.val;
53
54
 
54
- // Alle formatierten Werte werden intern wieder auf 0 gesetzt, Laufzeiten starten neu
55
- this.runtimeTotal = Number(totalRaw) || 0;
56
- this.runtimeToday = Number(todayRaw) || 0;
57
- this.runtimeSeason = Number(seasonRaw) || 0;
55
+ // >>> NEU: Formatierten Text (z. B. "3h 12m 5s") in Sekunden umwandeln
56
+ this.runtimeTotal = this._parseFormattedTimeToSeconds(totalRaw);
57
+ this.runtimeToday = this._parseFormattedTimeToSeconds(todayRaw);
58
+ this.runtimeSeason = this._parseFormattedTimeToSeconds(seasonRaw);
58
59
  this.startCountToday = Number(countRaw) || 0;
59
60
 
60
61
  // Falls Pumpe gerade läuft → Status wiederherstellen
@@ -158,6 +159,35 @@ const runtimeHelper = {
158
159
  return `${h}h ${m}m ${s}s`;
159
160
  },
160
161
 
162
+ // >>> NEU: formatierten Text (z. B. "3h 12m 5s") in Sekunden zurückrechnen
163
+ _parseFormattedTimeToSeconds(value) {
164
+ if (typeof value === 'number' && Number.isFinite(value)) {
165
+ return value;
166
+ } // bereits Sekunden
167
+ const str = String(value ?? '').trim();
168
+ if (!str) {
169
+ return 0;
170
+ }
171
+
172
+ let h = 0,
173
+ m = 0,
174
+ s = 0;
175
+ const mh = str.match(/(\d+)\s*h/);
176
+ if (mh) {
177
+ h = parseInt(mh[1], 10);
178
+ }
179
+ const mm = str.match(/(\d+)\s*m/);
180
+ if (mm) {
181
+ m = parseInt(mm[1], 10);
182
+ }
183
+ const ms = str.match(/(\d+)\s*s/);
184
+ if (ms) {
185
+ s = parseInt(ms[1], 10);
186
+ }
187
+
188
+ return h * 3600 + m * 60 + s;
189
+ },
190
+
161
191
  _scheduleDailyReset() {
162
192
  const now = new Date();
163
193
  const nextMidnight = new Date(now);
@@ -18,7 +18,7 @@ const solarHelper = {
18
18
  // Minütlicher Check
19
19
  this._scheduleCheck();
20
20
 
21
- this.adapter.log.info('[solarHelper] initialisiert (Prüfung alle 60s)');
21
+ this.adapter.log.debug('[solarHelper] initialisiert (Prüfung alle 60s)');
22
22
  },
23
23
 
24
24
  _scheduleCheck() {
@@ -32,6 +32,13 @@ const solarHelper = {
32
32
 
33
33
  async _checkSolar() {
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('[solarHelper] Vorrang durch ControlHelper aktiv – Solarregelung pausiert.');
39
+ return;
40
+ }
41
+
35
42
  // --- NEU: Saisonstatus ---
36
43
  const season = (await this.adapter.getStateAsync('status.season_active'))?.val;
37
44
 
@@ -47,8 +54,8 @@ const solarHelper = {
47
54
  const hysteresis = (await this.adapter.getStateAsync('solar.hysteresis_active'))?.val;
48
55
 
49
56
  // Temperaturen laden
50
- const collector = (await this.adapter.getStateAsync('temperature.collector.current'))?.val;
51
- const pool = (await this.adapter.getStateAsync('temperature.surface.current'))?.val; // Oberfläche = Pooltemp
57
+ const collector = Number((await this.adapter.getStateAsync('temperature.collector.current'))?.val);
58
+ const pool = Number((await this.adapter.getStateAsync('temperature.surface.current'))?.val); // Oberfläche = Pooltemp
52
59
 
53
60
  if (collector == null || pool == null) {
54
61
  this.adapter.log.debug('[solarHelper] Keine gültigen Temperaturen verfügbar');
@@ -75,11 +82,21 @@ const solarHelper = {
75
82
  // z. B. Ausschaltgrenze etwas absenken
76
83
  }
77
84
 
85
+ // --- NEU: Sprachvariable für Solarsteuerung setzen ---
86
+ const oldVal = (await this.adapter.getStateAsync('speech.solar_active'))?.val;
87
+ if (oldVal !== shouldRun) {
88
+ await this.adapter.setStateChangedAsync('speech.solar_active', {
89
+ val: shouldRun,
90
+ ack: true,
91
+ });
92
+ }
93
+
78
94
  // ZENTRAL: Pumpe über Bool-Schalter setzen
79
- await this.adapter.setStateAsync('pump.pump_switch', {
95
+ await this.adapter.setStateChangedAsync('pump.pump_switch', {
80
96
  val: shouldRun,
81
97
  ack: false,
82
98
  });
99
+
83
100
  this.adapter.log.debug(
84
101
  `[solarHelper] Solarregelung → Pumpe ${shouldRun ? 'EIN' : 'AUS'} (Collector=${collector}°C, Pool=${pool}°C, Delta=${delta}°C)`,
85
102
  );
@@ -99,7 +116,10 @@ const solarHelper = {
99
116
  const warnActive = (await this.adapter.getStateAsync('solar.warn_active'))?.val;
100
117
  if (warnActive) {
101
118
  const warnTemp = Number((await this.adapter.getStateAsync('solar.warn_temp'))?.val) || 0;
119
+
120
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
102
121
  const speechEnabled = (await this.adapter.getStateAsync('solar.warn_speech'))?.val;
122
+
103
123
  const currentWarning = (await this.adapter.getStateAsync('solar.collector_warning'))?.val || false;
104
124
 
105
125
  // Neue Warnung, wenn Collector >= warnTemp
@@ -112,13 +132,17 @@ const solarHelper = {
112
132
  `[solarHelper] WARNUNG: Kollektortemperatur ${collector}°C >= ${warnTemp}°C!`,
113
133
  );
114
134
 
115
- // Sprachausgabe bei Aktivierung
116
- if (speechEnabled) {
117
- await this.adapter.setStateAsync('speech.last_text', {
118
- val: `Warnung: Kollektortemperatur ${collector} Grad erreicht.`,
119
- ack: true,
120
- });
121
- }
135
+ /*
136
+ * Deaktiviert, ersetzt durch SpeechTextHelper
137
+ *
138
+ * // Sprachausgabe bei Aktivierung
139
+ * if (speechEnabled) {
140
+ * await this.adapter.setStateAsync('speech.last_text', {
141
+ * val: `Warnung: Kollektortemperatur ${collector} Grad erreicht.`,
142
+ * ack: true,
143
+ * });
144
+ * }
145
+ */
122
146
  }
123
147
 
124
148
  // Warnung zurücksetzen, wenn Collector <= 90 % von warnTemp
@@ -127,7 +151,7 @@ const solarHelper = {
127
151
  val: false,
128
152
  ack: true,
129
153
  });
130
- this.adapter.log.info(
154
+ this.adapter.log.debug(
131
155
  `[solarHelper] Kollektorwarnung zurückgesetzt: ${collector}°C <= ${warnTemp * 0.9}°C`,
132
156
  );
133
157
  }
@@ -21,11 +21,10 @@ const speechHelper = {
21
21
  this.adapter.subscribeStates('pump.error'); // Fehleransagen
22
22
  this.adapter.subscribeStates('temperature.*.current'); // Temp-Trigger
23
23
  this.adapter.subscribeStates('pump.pump_switch'); // wichtig für Flankenerkennung
24
-
25
- // NEU: auch speech.last_text überwachen (z. B. von controlHelper gesendet)
26
24
  this.adapter.subscribeStates('speech.last_text');
25
+ this.adapter.subscribeStates('speech.queue'); // <<< NEU: zentrale Nachrichtenwarteschlange
27
26
 
28
- this.adapter.log.info('[speechHelper] initialisiert');
27
+ this.adapter.log.debug('[speechHelper] initialisiert');
29
28
  },
30
29
 
31
30
  async handleStateChange(id, state) {
@@ -39,6 +38,16 @@ const speechHelper = {
39
38
  return;
40
39
  }
41
40
 
41
+ // NEU: Nachricht aus zentraler speech.queue
42
+ if (id.endsWith('speech.queue') && state.ack === false) {
43
+ const txt = String(state.val || '').trim();
44
+ if (txt) {
45
+ await this._speak(txt);
46
+ await this.adapter.setStateAsync('speech.queue', { val: '', ack: true });
47
+ }
48
+ return;
49
+ }
50
+
42
51
  // NEU: Direktnachricht von controlHelper über speech.last_text
43
52
  if (id.endsWith('speech.last_text') && state.ack === false) {
44
53
  const txt = String(state.val || '').trim();
@@ -57,48 +66,67 @@ const speechHelper = {
57
66
  return;
58
67
  }
59
68
 
60
- // === Pumpenstart / -stop nur bei Zustandswechsel ===
61
- if (id.endsWith('pump.pump_switch')) {
62
- const newVal = !!state.val;
63
-
64
- // Nur wenn sich der Zustand wirklich geändert hat
65
- if (this.lastPumpState !== newVal) {
66
- this.lastPumpState = newVal;
67
-
68
- if (newVal) {
69
- const txt =
70
- (await this.adapter.getStateAsync('speech.start_text'))?.val ||
71
- 'Die Poolpumpe wurde gestartet.';
72
- await this._speak(txt);
73
- } else {
74
- const txt =
75
- (await this.adapter.getStateAsync('speech.end_text'))?.val || 'Die Poolpumpe wurde gestoppt.';
76
- await this._speak(txt);
77
- }
78
- } else {
79
- this.adapter.log.debug('[speechHelper] Ignoriere Pumpenmeldung kein Zustandswechsel.');
80
- }
69
+ /*
70
+ *
71
+ * Deaktiviert, ersetzt durch speechTextHelper
72
+ *
73
+ * // === Pumpenstart / -stop nur bei Zustandswechsel ===
74
+ * if (id.endsWith('pump.pump_switch')) {
75
+ * const newVal = !!state.val;
76
+ *
77
+ * // Nur wenn sich der Zustand wirklich geändert hat
78
+ * if (this.lastPumpState !== newVal) {
79
+ * this.lastPumpState = newVal;
80
+ *
81
+ * if (newVal) {
82
+ * const txt =
83
+ * (await this.adapter.getStateAsync('speech.start_text'))?.val ||
84
+ * 'Die Poolpumpe wurde gestartet.';
85
+ * await this._speak(txt);
86
+ * } else {
87
+ * const txt =
88
+ * (await this.adapter.getStateAsync('speech.end_text'))?.val || 'Die Poolpumpe wurde gestoppt.';
89
+ * await this._speak(txt);
90
+ * }
91
+ * } else {
92
+ * this.adapter.log.debug('[speechHelper] Ignoriere Pumpenmeldung – kein Zustandswechsel.');
93
+ * }
94
+ * return;
95
+ * }
96
+ */
97
+
98
+ // Nur Pool-Oberflächentemperatur berücksichtigen
99
+ if (!id.includes('temperature.surface')) {
81
100
  return;
82
101
  }
83
102
 
84
- // Temperatur-Trigger: über Config-Schwelle
85
- if (id.includes('.temperature.') && id.endsWith('.current')) {
86
- const threshold = this.adapter.config.speech_temp_threshold || 0;
87
- const val = Number(state.val);
88
- if (val >= threshold && threshold > 0) {
89
- const now = Date.now();
90
- const last = this.lastTempNotify[id] || 0;
91
-
92
- // Nur einmal pro Stunde pro Sensor
93
- if (now - last > 60 * 60 * 1000) {
94
- await this._speak(`Der Pool hat ${val} Grad erreicht.`);
95
- this.lastTempNotify[id] = now;
96
- } else {
97
- this.adapter.log.debug(`[speechHelper] Temperaturansage für ${id} unterdrückt (Cooldown aktiv).`);
98
- }
103
+ const threshold = this.adapter.config.speech_temp_threshold || 0;
104
+ const val = Number(state.val);
105
+
106
+ if (val >= threshold && threshold > 0) {
107
+ const now = Date.now();
108
+ const lastInfo = this.lastTempNotify[id] || { time: 0, temp: 0, date: null };
109
+
110
+ const lastDate = lastInfo.date;
111
+ const today = new Date().toDateString();
112
+ const tempDiff = Math.abs(val - lastInfo.temp);
113
+
114
+ // Prüfen: neuer Tag oder Temperatur mindestens +2°C höher
115
+ const isNewDay = lastDate !== today;
116
+ const significantChange = tempDiff >= 2;
117
+
118
+ if (isNewDay || significantChange) {
119
+ await this._speak(`Der Pool hat jetzt ${val} Grad erreicht.`);
120
+ this.lastTempNotify[id] = { time: now, temp: val, date: today };
121
+ } else {
122
+ this.adapter.log.debug(
123
+ `[speechHelper] Temperaturansage unterdrückt (tempDiff=${tempDiff.toFixed(
124
+ 1,
125
+ )}°C, letzter Wert=${lastInfo.temp}°C).`,
126
+ );
99
127
  }
100
- return;
101
128
  }
129
+ return;
102
130
  },
103
131
 
104
132
  async _speak(text) {
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * speechTextHelper
5
+ * - Erzeugt situationsabhängige Sprachtexte (z. B. Zeitmodus, Solar, PV, Wartung usw.)
6
+ * - Sendet fertige Texte an speech.queue (ack: false)
7
+ * - Beeinflusst keine Steuerlogik und keine anderen Helper
8
+ * - Wird nach und nach um Textbausteine erweitert (Schritt-für-Schritt pro Datei)
9
+ *
10
+ * @module speechTextHelper
11
+ * @version 1.0.3
12
+ */
13
+
14
+ const speechTextHelper = {
15
+ // @type {ioBroker.Adapter}
16
+ adapter: null,
17
+
18
+ /**
19
+ * Initialisiert den Helper und abonniert relevante States.
20
+ *
21
+ * @param {ioBroker.Adapter} adapter - ioBroker Adapterinstanz
22
+ */
23
+ init(adapter) {
24
+ this.adapter = adapter;
25
+
26
+ // Relevante States abonnieren (Pumpenlogik + Status)
27
+ this.adapter.subscribeStates('pump.pump_switch');
28
+ this.adapter.subscribeStates('pump.mode');
29
+ this.adapter.subscribeStates('pump.reason');
30
+ this.adapter.subscribeStates('pump.status'); // zentrale Statusüberwachung
31
+
32
+ // --- NEU: Solar-Warnung überwachen ---
33
+ this.adapter.subscribeStates('solar.collector_warning');
34
+
35
+ // --- NEU: Solarsteuerung überwachen ---
36
+ this.adapter.subscribeStates('speech.solar_active');
37
+
38
+ // --- NEU: Zeitsteuerung überwachen ---
39
+ this.adapter.subscribeStates('speech.time_active');
40
+
41
+ // Später erweiterbar:
42
+ // this.adapter.subscribeStates('solar.solar_control_active');
43
+ // this.adapter.subscribeStates('control.pump.backwash_active');
44
+
45
+ this.adapter.log.debug(
46
+ '[speechTextHelper] initialisiert (Grundstruktur aktiv, inkl. Solar-Warnung, keine weiteren Textlogiken)',
47
+ );
48
+ },
49
+
50
+ /**
51
+ * Reagiert auf State-Änderungen.
52
+ * Hier werden nach und nach die jeweiligen Textausgaben ergänzt.
53
+ *
54
+ * @param {string} id - Objekt-ID des geänderten States
55
+ * @param {ioBroker.State} state - Neuer Statewert
56
+ */
57
+ async handleStateChange(id, state) {
58
+ try {
59
+ if (!state) {
60
+ return;
61
+ }
62
+
63
+ // --- Pumpenstatusänderung ---
64
+ if (id.endsWith('pump.status')) {
65
+ const status = String(state.val || '').toLowerCase();
66
+ this.adapter.log.silly(`[speechTextHelper] Pumpenstatus geändert: ${status}`);
67
+ return;
68
+ }
69
+
70
+ // --- Pumpenereignisse ---
71
+ if (id.endsWith('pump.pump_switch') || id.endsWith('pump.mode') || id.endsWith('pump.reason')) {
72
+ this.adapter.log.silly(`[speechTextHelper] Pumpen-Event erkannt: ${id} = ${state.val}`);
73
+ return;
74
+ }
75
+
76
+ // --- NEU: Solar-Warnung ---
77
+ if (id.endsWith('solar.collector_warning')) {
78
+ const val = !!state.val;
79
+
80
+ if (val) {
81
+ // Neue Warnung aktiv
82
+ const collectorTemp = Number(
83
+ (await this.adapter.getStateAsync('temperature.collector.current'))?.val,
84
+ );
85
+ const warnTemp = Number((await this.adapter.getStateAsync('solar.warn_temp'))?.val);
86
+ const text = `Warnung: Kollektortemperatur ${collectorTemp} Grad erreicht (Warnschwelle ${warnTemp}°C).`;
87
+ await this._sendSpeech(text);
88
+ this.adapter.log.debug(`[speechTextHelper] Solar-Warnung gesendet: ${text}`);
89
+ } else {
90
+ // Warnung aufgehoben
91
+ const text = 'Kollektorwarnung aufgehoben.';
92
+ await this._sendSpeech(text);
93
+ this.adapter.log.debug('[speechTextHelper] Solar-Warnung aufgehoben.');
94
+ }
95
+
96
+ // ersetzt SolarHelper Textausgabe
97
+ return;
98
+ }
99
+
100
+ // --- NEU: Reaktion auf Solarsteuerung ---
101
+ if (id.endsWith('speech.solar_active')) {
102
+ const val = !!state.val;
103
+ // Pumpenstatus aktualisieren, damit auch im VIS korrekt sichtbar
104
+ if (val) {
105
+ await this.adapter.setStateAsync('pump.status', {
106
+ val: 'EIN (Solarsteuerung)',
107
+ ack: true,
108
+ });
109
+ } else {
110
+ await this.adapter.setStateAsync('pump.status', {
111
+ val: 'AUS (Solarsteuerung beendet)',
112
+ ack: true,
113
+ });
114
+ }
115
+ if (val) {
116
+ const text = 'Die Poolpumpe wurde durch die Solarsteuerung eingeschaltet.';
117
+ await this._sendSpeech(text);
118
+ this.adapter.log.debug('[speechTextHelper] Solarsteuerung aktiviert → Ansage gesendet.');
119
+ } else {
120
+ const text = 'Solarsteuerung beendet – Poolpumpe ausgeschaltet.';
121
+ await this._sendSpeech(text);
122
+ this.adapter.log.debug('[speechTextHelper] Solarsteuerung deaktiviert → Ansage gesendet.');
123
+ }
124
+ return;
125
+ }
126
+
127
+ // --- NEU: Reaktion auf Zeitsteuerung ---
128
+ if (id.endsWith('speech.time_active')) {
129
+ const val = !!state.val;
130
+
131
+ // Pumpenstatus mitpflegen
132
+ if (val) {
133
+ await this.adapter.setStateAsync('pump.status', {
134
+ val: 'EIN (Zeitsteuerung)',
135
+ ack: true,
136
+ });
137
+ const text = 'Die Poolpumpe wurde durch die Zeitsteuerung eingeschaltet.';
138
+ await this._sendSpeech(text);
139
+ this.adapter.log.debug('[speechTextHelper] Zeitsteuerung aktiviert → Ansage gesendet.');
140
+ } else {
141
+ await this.adapter.setStateAsync('pump.status', {
142
+ val: 'AUS (Zeitsteuerung beendet)',
143
+ ack: true,
144
+ });
145
+ const text = 'Zeitsteuerung beendet – Poolpumpe ausgeschaltet.';
146
+ await this._sendSpeech(text);
147
+ this.adapter.log.debug('[speechTextHelper] Zeitsteuerung deaktiviert → Ansage gesendet.');
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Weitere Blöcke (z. B. Zeitmodus, Wartung usw.) folgen später hier
153
+ } catch (err) {
154
+ this.adapter.log.warn(`[speechTextHelper] Fehler bei handleStateChange: ${err.message}`);
155
+ }
156
+ },
157
+
158
+ /**
159
+ * Sendet Text an speech.queue.
160
+ *
161
+ * @param {string} text - Der zu sendende Text
162
+ */
163
+ async _sendSpeech(text) {
164
+ if (!text) {
165
+ return;
166
+ }
167
+ try {
168
+ await this.adapter.setStateAsync('speech.queue', { val: text, ack: false });
169
+ this.adapter.log.debug(`[speechTextHelper] Text gesendet: ${text}`);
170
+ } catch (err) {
171
+ this.adapter.log.warn(`[speechTextHelper] Fehler beim Senden an speech.queue: ${err.message}`);
172
+ }
173
+ },
174
+
175
+ /**
176
+ * Aufräumen (z. B. Timer beenden)
177
+ */
178
+ cleanup() {
179
+ // Aktuell keine Ressourcen
180
+ this.adapter.log.debug('[speechTextHelper] cleanup ausgeführt');
181
+ },
182
+ };
183
+
184
+ module.exports = speechTextHelper;
@@ -52,7 +52,7 @@ const statusHelper = {
52
52
  // Mitternacht-Reset einplanen
53
53
  this.scheduleMidnightReset();
54
54
 
55
- this.adapter.log.info('[statusHelper] initialisiert');
55
+ this.adapter.log.debug('[statusHelper] initialisiert');
56
56
  },
57
57
 
58
58
  async handleStateChange(id, state) {
@@ -209,7 +209,7 @@ const statusHelper = {
209
209
  try {
210
210
  await this.adapter.setStateAsync('status.pump_today_count', { val: 0, ack: true });
211
211
  await this.adapter.setStateAsync('status.pump_was_on_today', { val: false, ack: true });
212
- this.adapter.log.info('[statusHelper] Tagesreset durchgeführt');
212
+ this.adapter.log.debug('[statusHelper] Tagesreset durchgeführt');
213
213
  } catch (err) {
214
214
  this.adapter.log.warn(`[statusHelper] Fehler beim Tagesreset: ${err.message}`);
215
215
  }
@@ -67,7 +67,7 @@ const temperatureHelper = {
67
67
  // Reset um Mitternacht
68
68
  this._scheduleDailyReset();
69
69
 
70
- adapter.log.info(
70
+ adapter.log.debug(
71
71
  `[temperatureHelper] Aktiv: ${
72
72
  Object.keys(this.sensors).length
73
73
  ? Object.entries(this.sensors)
@@ -230,7 +230,7 @@ const temperatureHelper = {
230
230
  },
231
231
 
232
232
  async _resetMinMax() {
233
- this.adapter.log.info('[temperatureHelper] Setze Tages-Min/Max zurück');
233
+ this.adapter.log.debug('[temperatureHelper] Setze Tages-Min/Max zurück');
234
234
  for (const key of Object.keys(this.sensors)) {
235
235
  // Bugfix: statt leeres Objekt → löschen, damit Neu-Init greift
236
236
  delete this.minMax[key];
@@ -17,7 +17,7 @@ const timeHelper = {
17
17
  // Minütlicher Check
18
18
  this._scheduleCheck();
19
19
 
20
- this.adapter.log.info('[timeHelper] initialisiert (Prüfung alle 60s)');
20
+ this.adapter.log.debug('[timeHelper] initialisiert (Prüfung alle 60s)');
21
21
  },
22
22
 
23
23
  _scheduleCheck() {
@@ -59,6 +59,15 @@ const timeHelper = {
59
59
  }
60
60
  }
61
61
 
62
+ // --- NEU: Sprachsignal für Zeitsteuerung setzen ---
63
+ const oldVal = (await this.adapter.getStateAsync('speech.time_active'))?.val;
64
+ if (oldVal !== shouldRun) {
65
+ await this.adapter.setStateAsync('speech.time_active', {
66
+ val: shouldRun,
67
+ ack: true,
68
+ });
69
+ }
70
+
62
71
  // Pumpe über die echte Steckdosen-ID schalten
63
72
  await this.adapter.setForeignStateAsync(pumpSwitchId, {
64
73
  val: shouldRun,
@@ -176,6 +176,70 @@ async function createControlStates(adapter) {
176
176
  native: {},
177
177
  });
178
178
  await adapter.setStateAsync('control.energy.reset', { val: false, ack: true });
179
+
180
+ // ---------------------------------------------------------------------
181
+ // Channel: control.circulation
182
+ await adapter.setObjectNotExistsAsync('control.circulation', {
183
+ type: 'channel',
184
+ common: {
185
+ name: 'Tagesumwälzungs-Steuerung',
186
+ desc: 'Automatische oder manuelle Prüfung der täglichen Umwälzmenge',
187
+ },
188
+ native: {},
189
+ });
190
+
191
+ // State: Modus (auto/manual/notify/off)
192
+ await adapter.setObjectNotExistsAsync('control.circulation.mode', {
193
+ type: 'state',
194
+ common: {
195
+ name: 'Modus der Umwälzungsprüfung',
196
+ desc: 'Legt fest, ob und wie die Tagesumwälzung geprüft und ggf. nachgepumpt wird',
197
+ type: 'string',
198
+ role: 'value',
199
+ read: true,
200
+ write: true,
201
+ def: 'notify',
202
+ states: {
203
+ auto: 'Automatik',
204
+ manual: 'Manuell',
205
+ notify: 'Nur benachrichtigen',
206
+ off: 'Aus',
207
+ },
208
+ },
209
+ native: {},
210
+ });
211
+ await adapter.setStateAsync('control.circulation.mode', { val: 'notify', ack: true });
212
+
213
+ // State: Prüfzeitpunkt
214
+ await adapter.setObjectNotExistsAsync('control.circulation.check_time', {
215
+ type: 'state',
216
+ common: {
217
+ name: 'Prüfzeitpunkt für Tagesumwälzung',
218
+ desc: 'Uhrzeit, zu der täglich die Tagesumwälzung geprüft und ggf. gemeldet wird (Format HH:MM)',
219
+ type: 'string',
220
+ role: 'value.time',
221
+ read: true,
222
+ write: true,
223
+ def: '18:00',
224
+ },
225
+ native: {},
226
+ });
227
+ await adapter.setStateAsync('control.circulation.check_time', { val: '18:00', ack: true });
228
+
229
+ // State: letzter Bericht
230
+ await adapter.setObjectNotExistsAsync('control.circulation.last_report', {
231
+ type: 'state',
232
+ common: {
233
+ name: 'Letzter Bericht zur Tagesumwälzung',
234
+ desc: 'Zeitstempel des letzten automatisch erzeugten Umwälzungs-Reports',
235
+ type: 'string',
236
+ role: 'date',
237
+ read: true,
238
+ write: false,
239
+ },
240
+ native: {},
241
+ });
242
+ await adapter.setStateAsync('control.circulation.last_report', { val: '', ack: true });
179
243
  } catch (err) {
180
244
  adapter.log.error(`[controlStates] Fehler beim Erstellen der Control-States: ${err.message}`);
181
245
  }
@@ -78,7 +78,7 @@ async function createRuntimeStates(adapter) {
78
78
  role: 'text',
79
79
  read: true,
80
80
  write: false,
81
- persist: false,
81
+ persist: true,
82
82
  },
83
83
  native: {},
84
84
  });