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.
- package/README.md +42 -67
- package/io-package.json +34 -30
- package/lib/helpers/consumptionHelper.js +6 -6
- package/lib/helpers/controlHelper.js +209 -50
- package/lib/helpers/controlHelper2.js +195 -0
- package/lib/helpers/debugLogHelper.js +5 -5
- package/lib/helpers/frostHelper.js +18 -2
- package/lib/helpers/pumpHelper.js +41 -8
- package/lib/helpers/runtimeHelper.js +36 -6
- package/lib/helpers/solarHelper.js +36 -12
- package/lib/helpers/speechHelper.js +68 -40
- package/lib/helpers/speechTextHelper.js +184 -0
- package/lib/helpers/statusHelper.js +2 -2
- package/lib/helpers/temperatureHelper.js +2 -2
- package/lib/helpers/timeHelper.js +10 -1
- package/lib/stateDefinitions/controlStates.js +128 -0
- package/lib/stateDefinitions/pumpStates.js +19 -0
- package/lib/stateDefinitions/runtimeStates.js +1 -1
- package/lib/stateDefinitions/speechStates.js +58 -0
- package/lib/stateDefinitions/statusStates.js +4 -0
- package/main.js +18 -0
- package/package.json +3 -2
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* controlHelper
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
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');
|
|
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.
|
|
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
|
|
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:
|
|
68
|
-
adapter.
|
|
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
|
|
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.
|
|
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 (
|
|
244
|
+
adapter.log.info(`[controlHelper] Rückspülung gestartet (${duration} Minuten).`);
|
|
94
245
|
|
|
95
246
|
if (notify) {
|
|
96
|
-
await
|
|
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
|
|
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
|
|
280
|
+
adapter.log.info(`[controlHelper] Energiezähler wird vollständig zurückgesetzt (${timestamp}).`);
|
|
129
281
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
await adapter.setStateAsync(
|
|
297
|
+
for (const sid of consStates) {
|
|
298
|
+
await adapter.setStateAsync(sid, { val: 0, ack: true });
|
|
299
|
+
}
|
|
141
300
|
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
*
|
|
160
|
-
* (setzt nur speech.last_text, Versand erfolgt dort)
|
|
315
|
+
* Sendet Text an speech.queue
|
|
161
316
|
*
|
|
162
|
-
* @param {string} text -
|
|
317
|
+
* @param {string} text - Nachricht, die an speech.queue gesendet werden soll
|
|
163
318
|
*/
|
|
164
|
-
async function
|
|
319
|
+
async function _sendSpeech(text) {
|
|
165
320
|
if (!text) {
|
|
166
321
|
return;
|
|
167
322
|
}
|
|
168
|
-
|
|
169
323
|
try {
|
|
170
|
-
await adapter.setStateAsync('speech.
|
|
171
|
-
adapter.log.debug(`[controlHelper]
|
|
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
|
|
327
|
+
adapter.log.warn(`[controlHelper] Fehler beim Senden an speech.queue: ${err.message}`);
|
|
174
328
|
}
|
|
175
329
|
}
|
|
176
330
|
|
|
177
331
|
/**
|
|
178
|
-
*
|
|
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.
|
|
45
|
+
adapter.log.debug('[debugLogHelper] Kein Bereich ausgewählt – Logger inaktiv.');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
adapter.log.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
93
|
+
this.adapter.log.debug(
|
|
78
94
|
`[frostHelper] Frostschutz → Pumpe ${shouldRun ? 'EIN' : 'AUS'} (Außen=${outside}°C, Grenze=${frostTemp}°C)`,
|
|
79
95
|
);
|
|
80
96
|
}
|