iobroker.poolcontrol 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -11
- package/admin/jsonConfig.json +86 -2
- package/io-package.json +27 -27
- package/lib/helpers/aiChemistryHelpHelper.js +392 -0
- package/lib/helpers/heatHelper.js +450 -0
- package/lib/helpers/pumpHelper.js +9 -0
- package/lib/stateDefinitions/aiChemistryHelpStates.js +122 -0
- package/lib/stateDefinitions/heatStates.js +261 -0
- package/main.js +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* heatHelper
|
|
5
|
+
* -------------------------------------------------------------
|
|
6
|
+
* - Steuert Heizung/Wärmepumpe basierend auf Pooltemperatur
|
|
7
|
+
* - Respektiert:
|
|
8
|
+
* - status.season_active
|
|
9
|
+
* - control.pump.maintenance_active (Vorrang / Block)
|
|
10
|
+
* - pump.mode (nur im Automatikbetrieb)
|
|
11
|
+
* - Kann entweder:
|
|
12
|
+
* - eine schaltbare Steckdose (socket) oder
|
|
13
|
+
* - einen bool Steuer-State (boolean) bedienen
|
|
14
|
+
* - Erzeugt zusätzlich ein internes Signal:
|
|
15
|
+
* - heat.heating_request (read-only) => kann extern ausgewertet werden
|
|
16
|
+
* - Pumpen-Nachlaufzeit (min) wird berücksichtigt
|
|
17
|
+
* - Ownership-Schutz: Pumpe wird nur ausgeschaltet, wenn heatHelper sie vorher selbst eingeschaltet hat
|
|
18
|
+
* -------------------------------------------------------------
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const heatHelper = {
|
|
22
|
+
adapter: null,
|
|
23
|
+
|
|
24
|
+
// dynamische Steuer-ID (foreign)
|
|
25
|
+
_heatControlForeignId: '',
|
|
26
|
+
_afterrunTimer: null,
|
|
27
|
+
|
|
28
|
+
// Ownership / Merker
|
|
29
|
+
_ownsPump: false,
|
|
30
|
+
_desiredHeat: null,
|
|
31
|
+
_lastEval: 0,
|
|
32
|
+
|
|
33
|
+
init(adapter) {
|
|
34
|
+
this.adapter = adapter;
|
|
35
|
+
|
|
36
|
+
// lokale States überwachen
|
|
37
|
+
this.adapter.subscribeStates('heat.control_active');
|
|
38
|
+
this.adapter.subscribeStates('heat.control_type');
|
|
39
|
+
this.adapter.subscribeStates('heat.control_object_id');
|
|
40
|
+
this.adapter.subscribeStates('heat.target_temperature');
|
|
41
|
+
this.adapter.subscribeStates('heat.max_temperature');
|
|
42
|
+
this.adapter.subscribeStates('heat.pump_afterrun_minutes');
|
|
43
|
+
|
|
44
|
+
// Abhängigkeiten
|
|
45
|
+
this.adapter.subscribeStates('status.season_active');
|
|
46
|
+
this.adapter.subscribeStates('pump.mode');
|
|
47
|
+
this.adapter.subscribeStates('pump.pump_switch');
|
|
48
|
+
this.adapter.subscribeStates('temperature.surface.current');
|
|
49
|
+
|
|
50
|
+
// Vorrangschaltung / Wartung
|
|
51
|
+
this.adapter.subscribeStates('control.pump.maintenance_active');
|
|
52
|
+
|
|
53
|
+
// ggf. vorhandene Foreign-ID abonnieren
|
|
54
|
+
this._refreshForeignSubscription().catch(err =>
|
|
55
|
+
this.adapter.log.warn(`[heatHelper] Foreign-Subscription Fehler: ${err.message}`),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this._safeEvaluate('init');
|
|
59
|
+
this.adapter.log.info('[heatHelper] Initialisierung abgeschlossen.');
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async handleStateChange(id, state) {
|
|
63
|
+
if (!state) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Wenn die Ziel-Objekt-ID geändert wurde: Foreign subscription anpassen
|
|
69
|
+
if (id.endsWith('heat.control_object_id') || id.endsWith('heat.control_type')) {
|
|
70
|
+
await this._refreshForeignSubscription();
|
|
71
|
+
await this._safeEvaluate('control_target_changed');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Alles andere: neu bewerten
|
|
76
|
+
if (
|
|
77
|
+
id.endsWith('heat.control_active') ||
|
|
78
|
+
id.endsWith('heat.target_temperature') ||
|
|
79
|
+
id.endsWith('heat.max_temperature') ||
|
|
80
|
+
id.endsWith('heat.pump_afterrun_minutes') ||
|
|
81
|
+
id.endsWith('status.season_active') ||
|
|
82
|
+
id.endsWith('pump.mode') ||
|
|
83
|
+
id.endsWith('temperature.surface.current') ||
|
|
84
|
+
id.endsWith('control.pump.maintenance_active')
|
|
85
|
+
) {
|
|
86
|
+
await this._safeEvaluate('state_change');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wenn jemand die Pumpe extern schaltet: Ownership ggf. zurücknehmen
|
|
91
|
+
if (id.endsWith('pump.pump_switch')) {
|
|
92
|
+
const pumpOn = !!state.val;
|
|
93
|
+
if (!pumpOn && this._ownsPump) {
|
|
94
|
+
// wenn Pumpe aus geht obwohl wir "ownen", Ownership verlieren
|
|
95
|
+
this._ownsPump = false;
|
|
96
|
+
}
|
|
97
|
+
await this._safeEvaluate('pump_switch_changed');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
this.adapter.log.warn(`[heatHelper] Fehler in handleStateChange: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------
|
|
106
|
+
// Core
|
|
107
|
+
// -------------------------------------------------------------
|
|
108
|
+
async _evaluate(_sourceTag = '') {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (now - this._lastEval < 250) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this._lastEval = now;
|
|
114
|
+
|
|
115
|
+
const seasonActive = !!(await this.adapter.getStateAsync('status.season_active'))?.val;
|
|
116
|
+
const maintenanceActive = !!(await this.adapter.getStateAsync('control.pump.maintenance_active'))?.val;
|
|
117
|
+
|
|
118
|
+
const pumpMode = (await this.adapter.getStateAsync('pump.mode'))?.val || 'auto';
|
|
119
|
+
const heatEnabled = !!(await this.adapter.getStateAsync('heat.control_active'))?.val;
|
|
120
|
+
|
|
121
|
+
const poolTempRaw = (await this.adapter.getStateAsync('temperature.surface.current'))?.val;
|
|
122
|
+
const poolTemp = Number(poolTempRaw);
|
|
123
|
+
|
|
124
|
+
const targetTemp = Number((await this.adapter.getStateAsync('heat.target_temperature'))?.val ?? 26);
|
|
125
|
+
const maxTemp = Number((await this.adapter.getStateAsync('heat.max_temperature'))?.val ?? 30);
|
|
126
|
+
|
|
127
|
+
const afterrunMin = Math.max(
|
|
128
|
+
0,
|
|
129
|
+
Number((await this.adapter.getStateAsync('heat.pump_afterrun_minutes'))?.val ?? 0) || 0,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const controlType = (await this.adapter.getStateAsync('heat.control_type'))?.val || 'socket';
|
|
133
|
+
const controlObjectId = (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '';
|
|
134
|
+
|
|
135
|
+
// --- Hard conditions / Blocker ---
|
|
136
|
+
if (!seasonActive) {
|
|
137
|
+
return this._applyBlockedState('season_inactive', 'Poolsaison ist inaktiv', afterrunMin);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Vorrangschaltung: Wartung blockiert IMMER
|
|
141
|
+
if (maintenanceActive) {
|
|
142
|
+
return this._applyBlockedState(
|
|
143
|
+
'maintenance_active',
|
|
144
|
+
'Wartungsmodus aktiv (Control hat Vorrang)',
|
|
145
|
+
afterrunMin,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Heizungssteuerung deaktiviert
|
|
150
|
+
if (!heatEnabled) {
|
|
151
|
+
return this._applyOffState('heat_disabled', 'Heizungssteuerung deaktiviert', afterrunMin);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pumpenmodus: nur Automatik
|
|
155
|
+
if (pumpMode !== 'auto') {
|
|
156
|
+
return this._applyBlockedState(
|
|
157
|
+
'mode_not_auto',
|
|
158
|
+
`Pumpenmodus ist '${pumpMode}' (Heizung arbeitet nur in Automatik)`,
|
|
159
|
+
afterrunMin,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Sensor plausibel?
|
|
164
|
+
if (!Number.isFinite(poolTemp)) {
|
|
165
|
+
return this._applyBlockedState(
|
|
166
|
+
'no_pool_temp',
|
|
167
|
+
'Keine gültige Pooltemperatur (temperature.surface.current)',
|
|
168
|
+
afterrunMin,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sicherheitsabschaltung: MaxTemp überschritten
|
|
173
|
+
if (poolTemp >= maxTemp) {
|
|
174
|
+
return this._applyOffState(
|
|
175
|
+
'max_temp_reached',
|
|
176
|
+
`Max-Temperatur erreicht (${poolTemp.toFixed(1)} °C ≥ ${maxTemp} °C)`,
|
|
177
|
+
afterrunMin,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Heating logic (simple hysteresis-free) ---
|
|
182
|
+
// Einschalten: unter Zieltemperatur
|
|
183
|
+
// Ausschalten: bei Zieltemperatur erreicht/überschritten
|
|
184
|
+
const shouldHeat = poolTemp < targetTemp;
|
|
185
|
+
|
|
186
|
+
if (shouldHeat) {
|
|
187
|
+
return this._startHeating({
|
|
188
|
+
reason: `Heizen: Pool ${poolTemp.toFixed(1)} °C < Ziel ${targetTemp} °C`,
|
|
189
|
+
controlType,
|
|
190
|
+
controlObjectId,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this._stopHeating({
|
|
195
|
+
reason: `Ziel erreicht: Pool ${poolTemp.toFixed(1)} °C ≥ Ziel ${targetTemp} °C`,
|
|
196
|
+
controlType,
|
|
197
|
+
controlObjectId,
|
|
198
|
+
afterrunMin,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// -------------------------------------------------------------
|
|
203
|
+
// State transitions
|
|
204
|
+
// -------------------------------------------------------------
|
|
205
|
+
async _startHeating({ reason, controlType, controlObjectId }) {
|
|
206
|
+
if (this._desiredHeat === true) {
|
|
207
|
+
// nur Status/Reason ggf. aktualisieren
|
|
208
|
+
await this._setHeatStates({
|
|
209
|
+
active: true,
|
|
210
|
+
blocked: false,
|
|
211
|
+
mode: 'heating',
|
|
212
|
+
reason,
|
|
213
|
+
info: `control_type=${controlType}, target=${controlObjectId || '(leer)'}`,
|
|
214
|
+
heatingRequest: true,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._desiredHeat = true;
|
|
220
|
+
|
|
221
|
+
// Nachlauf ggf. abbrechen
|
|
222
|
+
if (this._afterrunTimer) {
|
|
223
|
+
clearTimeout(this._afterrunTimer);
|
|
224
|
+
this._afterrunTimer = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Heizung einschalten (wenn ID gesetzt)
|
|
228
|
+
await this._setHeatingDevice(true, controlObjectId);
|
|
229
|
+
|
|
230
|
+
// Pumpe einschalten (ownership)
|
|
231
|
+
await this._ensurePumpOn();
|
|
232
|
+
|
|
233
|
+
await this._setHeatStates({
|
|
234
|
+
active: true,
|
|
235
|
+
blocked: false,
|
|
236
|
+
mode: 'heating',
|
|
237
|
+
reason,
|
|
238
|
+
info: `Heizung EIN | control_type=${controlType}`,
|
|
239
|
+
heatingRequest: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.adapter.log.info(`[heatHelper] Heizung EIN (${reason})`);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async _stopHeating({ reason, controlType, controlObjectId, afterrunMin }) {
|
|
246
|
+
if (this._desiredHeat === false) {
|
|
247
|
+
// nur Status/Reason ggf. aktualisieren
|
|
248
|
+
await this._setHeatStates({
|
|
249
|
+
active: false,
|
|
250
|
+
blocked: false,
|
|
251
|
+
mode: 'off',
|
|
252
|
+
reason,
|
|
253
|
+
info: `control_type=${controlType}`,
|
|
254
|
+
heatingRequest: false,
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this._desiredHeat = false;
|
|
260
|
+
|
|
261
|
+
// Heizung aus
|
|
262
|
+
await this._setHeatingDevice(false, controlObjectId);
|
|
263
|
+
|
|
264
|
+
// Signal für andere Systeme
|
|
265
|
+
await this._setHeatStates({
|
|
266
|
+
active: false,
|
|
267
|
+
blocked: false,
|
|
268
|
+
mode: 'afterrun',
|
|
269
|
+
reason,
|
|
270
|
+
info: `Heizung AUS | Nachlauf=${afterrunMin} min | control_type=${controlType}`,
|
|
271
|
+
heatingRequest: false,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Pumpen-Nachlauf nur, wenn wir die Pumpe eingeschaltet hatten
|
|
275
|
+
await this._startAfterrunIfNeeded(afterrunMin, reason);
|
|
276
|
+
|
|
277
|
+
this.adapter.log.info(`[heatHelper] Heizung AUS (${reason})`);
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async _applyBlockedState(mode, reason, afterrunMin) {
|
|
281
|
+
// blockiert => Heizung aus, Request false
|
|
282
|
+
await this._setHeatingDevice(false, (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '');
|
|
283
|
+
|
|
284
|
+
await this._setHeatStates({
|
|
285
|
+
active: false,
|
|
286
|
+
blocked: true,
|
|
287
|
+
mode,
|
|
288
|
+
reason,
|
|
289
|
+
info: 'Heizung blockiert',
|
|
290
|
+
heatingRequest: false,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Nachlauf ggf. (nur wenn wir ownen)
|
|
294
|
+
await this._startAfterrunIfNeeded(afterrunMin, reason);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async _applyOffState(mode, reason, afterrunMin) {
|
|
298
|
+
// off => Heizung aus, Request false
|
|
299
|
+
await this._setHeatingDevice(false, (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '');
|
|
300
|
+
|
|
301
|
+
await this._setHeatStates({
|
|
302
|
+
active: false,
|
|
303
|
+
blocked: false,
|
|
304
|
+
mode,
|
|
305
|
+
reason,
|
|
306
|
+
info: 'Heizung AUS',
|
|
307
|
+
heatingRequest: false,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await this._startAfterrunIfNeeded(afterrunMin, reason);
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// -------------------------------------------------------------
|
|
314
|
+
// Pump handling (ownership protected)
|
|
315
|
+
// -------------------------------------------------------------
|
|
316
|
+
async _ensurePumpOn() {
|
|
317
|
+
try {
|
|
318
|
+
const pumpState = await this.adapter.getStateAsync('pump.pump_switch');
|
|
319
|
+
const isOn = !!pumpState?.val;
|
|
320
|
+
|
|
321
|
+
if (!isOn) {
|
|
322
|
+
// wir schalten sie ein => ownership true
|
|
323
|
+
this._ownsPump = true;
|
|
324
|
+
await this.adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
|
|
325
|
+
await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this.adapter.log.warn(`[heatHelper] Konnte Pumpe nicht einschalten: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
async _startAfterrunIfNeeded(afterrunMin, reason) {
|
|
333
|
+
// nur wenn wir die Pumpe vorher selbst eingeschaltet haben
|
|
334
|
+
if (!this._ownsPump) {
|
|
335
|
+
await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// wenn keine Nachlaufzeit: sofort aus
|
|
340
|
+
if (!afterrunMin || afterrunMin <= 0) {
|
|
341
|
+
await this._stopPumpNow('no_afterrun');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Timer neu starten
|
|
346
|
+
if (this._afterrunTimer) {
|
|
347
|
+
clearTimeout(this._afterrunTimer);
|
|
348
|
+
this._afterrunTimer = null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await this.adapter.setStateAsync('heat.afterrun_active', { val: true, ack: true });
|
|
352
|
+
|
|
353
|
+
const holdMs = Math.round(afterrunMin * 60 * 1000);
|
|
354
|
+
this.adapter.log.debug(`[heatHelper] Pumpen-Nachlauf gestartet: ${afterrunMin} min (${reason})`);
|
|
355
|
+
|
|
356
|
+
this._afterrunTimer = setTimeout(async () => {
|
|
357
|
+
// Wenn inzwischen wieder Heizbedarf aktiv ist -> Nachlauf abbrechen
|
|
358
|
+
if (this._desiredHeat === true) {
|
|
359
|
+
this.adapter.log.debug('[heatHelper] Nachlauf abgebrochen – Heizen wieder aktiv.');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await this._stopPumpNow('afterrun_done');
|
|
363
|
+
}, holdMs);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
async _stopPumpNow(tag) {
|
|
367
|
+
try {
|
|
368
|
+
await this.adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
this.adapter.log.warn(`[heatHelper] Konnte Pumpe nicht ausschalten: ${err.message}`);
|
|
371
|
+
} finally {
|
|
372
|
+
this._ownsPump = false;
|
|
373
|
+
await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
|
|
374
|
+
this.adapter.log.info(`[heatHelper] Pumpe AUS (${tag})`);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// -------------------------------------------------------------
|
|
379
|
+
// Heating device control (foreign state)
|
|
380
|
+
// -------------------------------------------------------------
|
|
381
|
+
async _setHeatingDevice(on, foreignId) {
|
|
382
|
+
const id = String(foreignId || '').trim();
|
|
383
|
+
if (!id) {
|
|
384
|
+
// Kein Ziel => nur internes heating_request als Signal
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
await this.adapter.setForeignStateAsync(id, { val: !!on, ack: false });
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.adapter.log.warn(`[heatHelper] Konnte Heizung nicht setzen (${id}): ${err.message}`);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
async _setHeatStates({ active, blocked, mode, reason, info, heatingRequest }) {
|
|
396
|
+
try {
|
|
397
|
+
await this.adapter.setStateAsync('heat.active', { val: !!active, ack: true });
|
|
398
|
+
await this.adapter.setStateAsync('heat.blocked', { val: !!blocked, ack: true });
|
|
399
|
+
await this.adapter.setStateAsync('heat.mode', { val: String(mode ?? ''), ack: true });
|
|
400
|
+
await this.adapter.setStateAsync('heat.reason', { val: String(reason ?? ''), ack: true });
|
|
401
|
+
await this.adapter.setStateAsync('heat.info', { val: String(info ?? ''), ack: true });
|
|
402
|
+
await this.adapter.setStateAsync('heat.heating_request', { val: !!heatingRequest, ack: true });
|
|
403
|
+
await this.adapter.setStateAsync('heat.last_change', { val: Date.now(), ack: true });
|
|
404
|
+
} catch (err) {
|
|
405
|
+
this.adapter.log.warn(`[heatHelper] Fehler beim Schreiben der Heat-States: ${err.message}`);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
// -------------------------------------------------------------
|
|
410
|
+
// Foreign subscribe handling
|
|
411
|
+
// -------------------------------------------------------------
|
|
412
|
+
async _refreshForeignSubscription() {
|
|
413
|
+
const id = (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '';
|
|
414
|
+
const nextId = String(id).trim();
|
|
415
|
+
|
|
416
|
+
if (nextId && nextId !== this._heatControlForeignId) {
|
|
417
|
+
// neue ID abonnieren
|
|
418
|
+
try {
|
|
419
|
+
this.adapter.subscribeForeignStates(nextId);
|
|
420
|
+
this.adapter.log.info(`[heatHelper] Subscribed Foreign-Heat-Control: "${nextId}"`);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
this.adapter.log.warn(`[heatHelper] Konnte Foreign-State nicht abonnieren (${nextId}): ${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
this._heatControlForeignId = nextId;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!nextId) {
|
|
428
|
+
this._heatControlForeignId = '';
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
async _safeEvaluate(tag) {
|
|
433
|
+
try {
|
|
434
|
+
await this._evaluate(tag);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
this.adapter.log.warn(`[heatHelper] Evaluate-Fehler (${tag}): ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
cleanup() {
|
|
441
|
+
if (this._afterrunTimer) {
|
|
442
|
+
clearTimeout(this._afterrunTimer);
|
|
443
|
+
this._afterrunTimer = null;
|
|
444
|
+
}
|
|
445
|
+
this._ownsPump = false;
|
|
446
|
+
this._desiredHeat = null;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
module.exports = heatHelper;
|
|
@@ -214,6 +214,15 @@ const pumpHelper = {
|
|
|
214
214
|
status = 'EIN (Zeitsteuerung)';
|
|
215
215
|
}
|
|
216
216
|
break;
|
|
217
|
+
|
|
218
|
+
case 'heatHelper':
|
|
219
|
+
try {
|
|
220
|
+
const reason = (await this.adapter.getStateAsync('pump.reason'))?.val || '';
|
|
221
|
+
status = reason ? `EIN (${reason})` : 'EIN (Heizung)';
|
|
222
|
+
} catch {
|
|
223
|
+
status = 'EIN (Heizung)';
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
await this.adapter.setStateChangedAsync('pump.status', {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aiChemistryHelpStates.js
|
|
5
|
+
* ----------------------------------------------------------
|
|
6
|
+
* Legt die States für die KI-Chemie-Hilfe an.
|
|
7
|
+
*
|
|
8
|
+
* Zweck:
|
|
9
|
+
* - Manuelle Auswahl eines beobachteten Pool-Problems
|
|
10
|
+
* - Ausgabe eines erklärenden Hilfetextes (ohne Dosierung)
|
|
11
|
+
*
|
|
12
|
+
* Struktur:
|
|
13
|
+
* ai.chemistry_help.*
|
|
14
|
+
*
|
|
15
|
+
* Hinweis:
|
|
16
|
+
* - Reine Informationsfunktion
|
|
17
|
+
* - Keine Steuerung, keine Automatik, keine Sprachausgabe
|
|
18
|
+
* ----------------------------------------------------------
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Erstellt alle States für die AI-Chemie-Hilfe.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('iobroker').Adapter} adapter - ioBroker Adapterinstanz
|
|
25
|
+
*/
|
|
26
|
+
async function createAiChemistryHelpStates(adapter) {
|
|
27
|
+
adapter.log.debug('[aiChemistryHelpStates] Initialisierung gestartet');
|
|
28
|
+
|
|
29
|
+
// ----------------------------------------------------------
|
|
30
|
+
// Channel: ai.chemistry_help
|
|
31
|
+
// ----------------------------------------------------------
|
|
32
|
+
await adapter.setObjectNotExistsAsync('ai.chemistry_help', {
|
|
33
|
+
type: 'channel',
|
|
34
|
+
common: {
|
|
35
|
+
name: 'Chemie-Hilfe (Erklärungen & Ursachen)',
|
|
36
|
+
},
|
|
37
|
+
native: {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ----------------------------------------------------------
|
|
41
|
+
// Auswahl des Problems (manuell)
|
|
42
|
+
// ----------------------------------------------------------
|
|
43
|
+
await adapter.setObjectNotExistsAsync('ai.chemistry_help.issue', {
|
|
44
|
+
type: 'state',
|
|
45
|
+
common: {
|
|
46
|
+
name: 'Chemie-Hilfe: Problem auswählen',
|
|
47
|
+
desc: 'Manuelle Auswahl eines beobachteten Pool-Problems zur Anzeige allgemeiner Erklärungen.',
|
|
48
|
+
type: 'string',
|
|
49
|
+
role: 'value',
|
|
50
|
+
read: true,
|
|
51
|
+
write: true,
|
|
52
|
+
def: 'none',
|
|
53
|
+
states: {
|
|
54
|
+
none: 'Kein Problem ausgewählt',
|
|
55
|
+
|
|
56
|
+
// pH-Wert
|
|
57
|
+
ph_low: 'pH-Wert ist zu niedrig',
|
|
58
|
+
ph_high: 'pH-Wert ist zu hoch',
|
|
59
|
+
|
|
60
|
+
// Chlor / Desinfektion
|
|
61
|
+
chlor_low: 'Chlorwert ist zu niedrig',
|
|
62
|
+
chlor_high: 'Chlorwert ist zu hoch',
|
|
63
|
+
chlor_no_effect: 'Chlor steigt trotz Zugabe nicht',
|
|
64
|
+
chlor_smell: 'Starker Chlorgeruch trotz Messwert',
|
|
65
|
+
|
|
66
|
+
// Wasserbild / Optik
|
|
67
|
+
water_green: 'Wasser ist grün',
|
|
68
|
+
water_cloudy: 'Wasser ist trüb / grau / milchig',
|
|
69
|
+
algae_visible: 'Algen an Wänden oder Boden sichtbar',
|
|
70
|
+
foam_on_surface: 'Schaumbildung auf der Wasseroberfläche',
|
|
71
|
+
|
|
72
|
+
// Badegefühl / Stabilität
|
|
73
|
+
skin_eye_irritation: 'Haut- oder Augenreizungen beim Baden',
|
|
74
|
+
values_unstable: 'Wasserwerte sind häufig instabil',
|
|
75
|
+
|
|
76
|
+
// Unsicherheit
|
|
77
|
+
unknown_problem: 'Problem unklar / nicht eindeutig',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
native: {},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ----------------------------------------------------------
|
|
84
|
+
// Erklärungstext (wird später vom Helper gefüllt)
|
|
85
|
+
// ----------------------------------------------------------
|
|
86
|
+
await adapter.setObjectNotExistsAsync('ai.chemistry_help.help_text', {
|
|
87
|
+
type: 'state',
|
|
88
|
+
common: {
|
|
89
|
+
name: 'Chemie-Hilfe: Erklärung',
|
|
90
|
+
desc: 'Erklärender Text zu Ursachen und allgemeinen Lösungsansätzen (keine Dosierung, keine Steuerung).',
|
|
91
|
+
type: 'string',
|
|
92
|
+
role: 'text',
|
|
93
|
+
read: true,
|
|
94
|
+
write: false,
|
|
95
|
+
def: '',
|
|
96
|
+
},
|
|
97
|
+
native: {},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ----------------------------------------------------------
|
|
101
|
+
// Zeitpunkt der letzten Auswahl
|
|
102
|
+
// ----------------------------------------------------------
|
|
103
|
+
await adapter.setObjectNotExistsAsync('ai.chemistry_help.last_issue_time', {
|
|
104
|
+
type: 'state',
|
|
105
|
+
common: {
|
|
106
|
+
name: 'Chemie-Hilfe: Letzte Auswahl',
|
|
107
|
+
desc: 'Zeitpunkt der letzten Auswahl eines Chemie-Hilfe-Problems.',
|
|
108
|
+
type: 'number',
|
|
109
|
+
role: 'value.time',
|
|
110
|
+
read: true,
|
|
111
|
+
write: false,
|
|
112
|
+
def: 0,
|
|
113
|
+
},
|
|
114
|
+
native: {},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
adapter.log.debug('[aiChemistryHelpStates] Initialisierung abgeschlossen');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
createAiChemistryHelpStates,
|
|
122
|
+
};
|