iobroker.poolcontrol 0.3.0 → 0.4.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 +32 -18
- package/admin/jsonConfig.json +4 -0
- package/io-package.json +28 -15
- package/lib/helpers/consumptionHelper.js +27 -32
- package/lib/helpers/frostHelper.js +11 -11
- package/lib/helpers/pumpHelper.js +22 -12
- package/lib/helpers/pumpHelper2.js +18 -16
- package/lib/helpers/pumpHelper3.js +52 -17
- package/lib/helpers/runtimeHelper.js +141 -43
- package/lib/helpers/statisticsHelper.js +392 -0
- package/lib/stateDefinitions/controlStates.js +0 -37
- package/lib/stateDefinitions/pumpStates3.js +63 -11
- package/lib/stateDefinitions/runtimeStates.js +20 -5
- package/lib/stateDefinitions/statisticsStates.js +138 -0
- package/main.js +10 -1
- package/package.json +1 -1
|
@@ -21,9 +21,24 @@ const runtimeHelper = {
|
|
|
21
21
|
resetTimer: null,
|
|
22
22
|
liveTimer: null, // Timer für Live-Updates
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Initialisiert den Runtime-Helper.
|
|
26
|
+
* Führt eine kurze Startverzögerung ein, um sicherzustellen,
|
|
27
|
+
* dass persistente States nach einer Überinstallation korrekt geladen werden.
|
|
28
|
+
*
|
|
29
|
+
* @param {ioBroker.Adapter} adapter - Aktive ioBroker-Adapterinstanz.
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
async init(adapter) {
|
|
25
33
|
this.adapter = adapter;
|
|
26
34
|
|
|
35
|
+
// ------------------------------------------------------
|
|
36
|
+
// NEU: Kurze Startverzögerung, damit ioBroker persistente States
|
|
37
|
+
// vollständig aus der Datenbank laden kann (Überinstallationsschutz)
|
|
38
|
+
// ------------------------------------------------------
|
|
39
|
+
this.adapter.log.debug('[runtimeHelper] Warte 3 Sekunden, um persistente States zu laden ...');
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
41
|
+
|
|
27
42
|
// Pumpenschalter überwachen
|
|
28
43
|
this.adapter.subscribeStates('pump.pump_switch');
|
|
29
44
|
|
|
@@ -52,6 +67,13 @@ const runtimeHelper = {
|
|
|
52
67
|
const seasonRaw = (await this.adapter.getStateAsync('runtime.season_total'))?.val;
|
|
53
68
|
const countRaw = (await this.adapter.getStateAsync('runtime.start_count_today'))?.val;
|
|
54
69
|
|
|
70
|
+
// FIX: Falls States leer oder neu angelegt sind, Warnhinweis ausgeben und Werte nicht überschreiben
|
|
71
|
+
if (!totalRaw && !seasonRaw) {
|
|
72
|
+
this.adapter.log.info(
|
|
73
|
+
'[runtimeHelper] Keine gespeicherten Laufzeiten gefunden – möglicherweise neue oder überinstallierte Instanz. Laufzeiten starten bei 0.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
// >>> NEU: Formatierten Text (z. B. "3h 12m 5s") in Sekunden umwandeln
|
|
56
78
|
this.runtimeTotal = this._parseFormattedTimeToSeconds(totalRaw);
|
|
57
79
|
this.runtimeToday = this._parseFormattedTimeToSeconds(todayRaw);
|
|
@@ -72,38 +94,72 @@ const runtimeHelper = {
|
|
|
72
94
|
return;
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
// FIX: Robuste Start-/Stop-Logik für pump.pump_switch
|
|
75
98
|
if (id.endsWith('pump.pump_switch')) {
|
|
76
|
-
if (state.val
|
|
77
|
-
// Pumpe
|
|
78
|
-
this.isRunning
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
99
|
+
if (state.val) {
|
|
100
|
+
// FIX: Immer starten, wenn Pumpe an ist (egal welcher Helper)
|
|
101
|
+
if (!this.isRunning || !this.lastOn) {
|
|
102
|
+
this.isRunning = true;
|
|
103
|
+
this.lastOn = Date.now();
|
|
104
|
+
this.startCountToday += 1;
|
|
105
|
+
|
|
106
|
+
// Live-Timer starten (jede Minute)
|
|
107
|
+
this._startLiveTimer();
|
|
108
|
+
|
|
109
|
+
// Start sofort in State schreiben
|
|
110
|
+
await this.adapter.setStateAsync('runtime.start_count_today', {
|
|
111
|
+
val: this.startCountToday,
|
|
112
|
+
ack: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ------------------------------------------------------
|
|
116
|
+
// Statuswerte bei Pumpenstart setzen
|
|
117
|
+
// ------------------------------------------------------
|
|
118
|
+
const nowStr = new Date().toLocaleString();
|
|
119
|
+
await this.adapter.setStateAsync('status.pump_last_start', { val: nowStr, ack: true });
|
|
120
|
+
await this.adapter.setStateAsync('status.pump_today_count', {
|
|
121
|
+
val: this.startCountToday,
|
|
122
|
+
ack: true,
|
|
123
|
+
});
|
|
124
|
+
await this.adapter.setStateAsync('status.pump_was_on_today', { val: true, ack: true });
|
|
125
|
+
// ------------------------------------------------------
|
|
126
|
+
this.adapter.log.debug('[runtimeHelper] Pumpenlaufzeit gestartet.');
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// FIX: Immer sauber stoppen, wenn Pumpe aus ist
|
|
130
|
+
if (this.isRunning && this.lastOn) {
|
|
131
|
+
const delta = Math.floor((Date.now() - this.lastOn) / 1000);
|
|
132
|
+
this.runtimeToday += delta;
|
|
133
|
+
this.runtimeTotal += delta;
|
|
134
|
+
|
|
135
|
+
// Saisonlaufzeit nur zählen, wenn aktiv
|
|
136
|
+
const seasonActive = !!(await this.adapter.getStateAsync('control.season.active'))?.val;
|
|
137
|
+
if (seasonActive) {
|
|
138
|
+
this.runtimeSeason += delta;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.isRunning = false;
|
|
142
|
+
this.lastOn = null;
|
|
143
|
+
|
|
144
|
+
// Live-Timer stoppen
|
|
145
|
+
this._stopLiveTimer();
|
|
146
|
+
|
|
147
|
+
// States final aktualisieren
|
|
148
|
+
await this._updateStates();
|
|
149
|
+
|
|
150
|
+
// ------------------------------------------------------
|
|
151
|
+
// Statuswert bei Pumpenstopp setzen
|
|
152
|
+
// ------------------------------------------------------
|
|
153
|
+
const nowStr = new Date().toLocaleString();
|
|
154
|
+
await this.adapter.setStateAsync('status.pump_last_stop', { val: nowStr, ack: true });
|
|
155
|
+
// ------------------------------------------------------
|
|
156
|
+
this.adapter.log.debug('[runtimeHelper] Pumpenlaufzeit gestoppt.');
|
|
157
|
+
} else {
|
|
158
|
+
// FIX: Falls Pumpe aus, aber kein aktiver Lauf (z. B. Neustart) → nur Timer sicher stoppen
|
|
159
|
+
this._stopLiveTimer();
|
|
160
|
+
this.isRunning = false;
|
|
161
|
+
this.lastOn = null;
|
|
97
162
|
}
|
|
98
|
-
|
|
99
|
-
this.isRunning = false;
|
|
100
|
-
this.lastOn = null;
|
|
101
|
-
|
|
102
|
-
// Live-Timer stoppen
|
|
103
|
-
this._stopLiveTimer();
|
|
104
|
-
|
|
105
|
-
// States final aktualisieren
|
|
106
|
-
await this._updateStates();
|
|
107
163
|
}
|
|
108
164
|
}
|
|
109
165
|
},
|
|
@@ -134,6 +190,16 @@ const runtimeHelper = {
|
|
|
134
190
|
await this.adapter.setStateAsync('runtime.season_total', { val: formattedSeason, ack: true });
|
|
135
191
|
await this.adapter.setStateAsync('runtime.start_count_today', { val: this.startCountToday, ack: true });
|
|
136
192
|
|
|
193
|
+
// Poolparameter laden (vor Durchflussprüfung!)
|
|
194
|
+
const poolSize = (await this.adapter.getStateAsync('general.pool_size'))?.val || 0;
|
|
195
|
+
const minCirc = (await this.adapter.getStateAsync('general.min_circulation_per_day'))?.val || 1;
|
|
196
|
+
|
|
197
|
+
// daily_required immer direkt setzen – auch ohne Durchfluss
|
|
198
|
+
const dailyRequired = Math.round(poolSize * minCirc);
|
|
199
|
+
if (dailyRequired > 0) {
|
|
200
|
+
await this.adapter.setStateAsync('circulation.daily_required', { val: dailyRequired, ack: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
137
203
|
// Umwälzmenge berechnen
|
|
138
204
|
// Reeller Durchflusswert aus pump.live.flow_current_lh
|
|
139
205
|
const liveFlowLh = (await this.adapter.getStateAsync('pump.live.flow_current_lh'))?.val || 0;
|
|
@@ -143,19 +209,26 @@ const runtimeHelper = {
|
|
|
143
209
|
return;
|
|
144
210
|
}
|
|
145
211
|
|
|
146
|
-
// Poolparameter laden
|
|
147
|
-
const poolSize = (await this.adapter.getStateAsync('general.pool_size'))?.val || 0;
|
|
148
|
-
const minCirc = (await this.adapter.getStateAsync('general.min_circulation_per_day'))?.val || 1;
|
|
149
|
-
|
|
150
212
|
// Berechnung der realen Tagesumwälzung (Liter)
|
|
151
213
|
const dailyTotal = Math.round((effectiveToday / 3600) * liveFlowLh);
|
|
152
|
-
const dailyRequired = Math.round(poolSize * minCirc);
|
|
153
214
|
const dailyRemaining = Math.max(dailyRequired - dailyTotal, 0);
|
|
154
215
|
|
|
155
|
-
// Werte
|
|
156
|
-
await this.adapter.
|
|
157
|
-
await this.adapter.
|
|
158
|
-
|
|
216
|
+
// Bestehende Werte für Total/Remaining laden
|
|
217
|
+
const oldTotal = (await this.adapter.getStateAsync('circulation.daily_total'))?.val || 0;
|
|
218
|
+
const oldRemaining = (await this.adapter.getStateAsync('circulation.daily_remaining'))?.val || 0;
|
|
219
|
+
|
|
220
|
+
// Nur schreiben, wenn tatsächlich sinnvolle Livewerte vorliegen
|
|
221
|
+
if (liveFlowLh > 0 && dailyTotal > 0) {
|
|
222
|
+
await this.adapter.setStateAsync('circulation.daily_total', { val: dailyTotal, ack: true });
|
|
223
|
+
await this.adapter.setStateAsync('circulation.daily_remaining', { val: dailyRemaining, ack: true });
|
|
224
|
+
this.adapter.log.debug(
|
|
225
|
+
`[runtimeHelper] Circulation-Werte aktualisiert (Total=${dailyTotal}, Required=${dailyRequired}, Remaining=${dailyRemaining})`,
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
this.adapter.log.debug(
|
|
229
|
+
`[runtimeHelper] Keine gültigen Live-Daten – bestehende Werte bleiben erhalten (Total=${oldTotal}, Required=${dailyRequired}, Remaining=${oldRemaining})`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
159
232
|
} catch (err) {
|
|
160
233
|
this.adapter.log.warn(`[runtimeHelper] Fehler beim Update der States: ${err.message}`);
|
|
161
234
|
}
|
|
@@ -204,12 +277,37 @@ const runtimeHelper = {
|
|
|
204
277
|
nextMidnight.setHours(24, 0, 0, 0);
|
|
205
278
|
const msUntilMidnight = nextMidnight - now;
|
|
206
279
|
|
|
207
|
-
this.resetTimer = setTimeout(() => {
|
|
280
|
+
this.resetTimer = setTimeout(async () => {
|
|
208
281
|
this.runtimeToday = 0;
|
|
209
282
|
this.startCountToday = 0;
|
|
210
283
|
this.lastOn = this.isRunning ? Date.now() : null;
|
|
284
|
+
|
|
285
|
+
// Laufzeiten zurücksetzen
|
|
211
286
|
this._updateStates();
|
|
287
|
+
|
|
288
|
+
// --- NEU: Circulation-Werte um Mitternacht zurücksetzen ---
|
|
289
|
+
await this.adapter.setStateAsync('circulation.daily_total', { val: 0, ack: true });
|
|
290
|
+
|
|
291
|
+
// daily_required neu berechnen (optional, falls sich Poolgröße geändert hat)
|
|
292
|
+
const poolSize = (await this.adapter.getStateAsync('general.pool_size'))?.val || 0;
|
|
293
|
+
const minCirc = (await this.adapter.getStateAsync('general.min_circulation_per_day'))?.val || 1;
|
|
294
|
+
const dailyRequired = Math.round(poolSize * minCirc);
|
|
295
|
+
await this.adapter.setStateAsync('circulation.daily_required', { val: dailyRequired, ack: true });
|
|
296
|
+
|
|
297
|
+
// 👉 daily_remaining neue berechnen auf Grundlage von daily_required
|
|
298
|
+
await this.adapter.setStateAsync('circulation.daily_remaining', { val: dailyRequired, ack: true });
|
|
299
|
+
|
|
300
|
+
// ------------------------------------------------------
|
|
301
|
+
// NEU: Pumpenstatuswerte um Mitternacht zurücksetzen
|
|
302
|
+
// ------------------------------------------------------
|
|
303
|
+
await this.adapter.setStateAsync('status.pump_today_count', { val: 0, ack: true });
|
|
304
|
+
await this.adapter.setStateAsync('status.pump_was_on_today', { val: false, ack: true });
|
|
305
|
+
// ------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
// Nächsten Reset planen
|
|
212
308
|
this._scheduleDailyReset();
|
|
309
|
+
|
|
310
|
+
this.adapter.log.debug('[runtimeHelper] Tagesreset (Runtime + Circulation) ausgeführt.');
|
|
213
311
|
}, msUntilMidnight);
|
|
214
312
|
},
|
|
215
313
|
|
|
@@ -217,8 +315,8 @@ const runtimeHelper = {
|
|
|
217
315
|
if (this.liveTimer) {
|
|
218
316
|
clearInterval(this.liveTimer);
|
|
219
317
|
}
|
|
220
|
-
this.liveTimer = setInterval(() => this._updateStates(),
|
|
221
|
-
this.adapter.log.debug('[runtimeHelper] Live-Timer gestartet (Updates
|
|
318
|
+
this.liveTimer = setInterval(() => this._updateStates(), 10 * 1000);
|
|
319
|
+
this.adapter.log.debug('[runtimeHelper] Live-Timer gestartet (Updates alle 10 Sekunden)');
|
|
222
320
|
},
|
|
223
321
|
|
|
224
322
|
_stopLiveTimer() {
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* statisticsHelper.js
|
|
5
|
+
* -------------------
|
|
6
|
+
* Vollständige Steuerung der Tagesstatistik (Temperatur)
|
|
7
|
+
* im Bereich analytics.statistics.temperature.today.*
|
|
8
|
+
*
|
|
9
|
+
* - Erstellt alle States (Überinstallationsschutz + Persistenz)
|
|
10
|
+
* - Erkennt aktive Sensoren anhand temperature.<sensor>.active
|
|
11
|
+
* - Reagiert eventbasiert auf Änderungen der Temperaturwerte
|
|
12
|
+
* - Berechnet laufend Min/Max/Durchschnitt
|
|
13
|
+
* - Aktualisiert JSON- und HTML-Ausgaben (pro Sensor & gesamt)
|
|
14
|
+
* - Führt automatisch täglichen Reset um Mitternacht durch
|
|
15
|
+
*
|
|
16
|
+
* @param {ioBroker.Adapter} adapter - Die aktuelle Adapterinstanz (this),
|
|
17
|
+
* über die alle ioBroker-Funktionen wie setStateAsync, getStateAsync usw. aufgerufen werden.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const statisticsHelper = {
|
|
21
|
+
adapter: null,
|
|
22
|
+
midnightTimer: null,
|
|
23
|
+
sensors: [
|
|
24
|
+
{ id: 'outside', name: 'Außentemperatur' },
|
|
25
|
+
{ id: 'ground', name: 'Bodentemperatur' },
|
|
26
|
+
{ id: 'surface', name: 'Pooloberfläche' },
|
|
27
|
+
{ id: 'flow', name: 'Vorlauf' },
|
|
28
|
+
{ id: 'return', name: 'Rücklauf' },
|
|
29
|
+
{ id: 'collector', name: 'Kollektor (Solar)' },
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
async init(adapter) {
|
|
33
|
+
this.adapter = adapter;
|
|
34
|
+
adapter.log.debug('statisticsHelper: Initialisierung gestartet.');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await this._createTemperatureStatistics();
|
|
38
|
+
await this._subscribeActiveSensors();
|
|
39
|
+
await this._scheduleMidnightReset();
|
|
40
|
+
adapter.log.debug('statisticsHelper: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
41
|
+
} catch (err) {
|
|
42
|
+
adapter.log.warn(`statisticsHelper: Fehler bei Initialisierung: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
48
|
+
*/
|
|
49
|
+
async _createTemperatureStatistics() {
|
|
50
|
+
const adapter = this.adapter;
|
|
51
|
+
|
|
52
|
+
for (const sensor of this.sensors) {
|
|
53
|
+
const basePath = `analytics.statistics.temperature.today.${sensor.id}`;
|
|
54
|
+
await adapter.setObjectNotExistsAsync(basePath, {
|
|
55
|
+
type: 'channel',
|
|
56
|
+
common: { name: `${sensor.name} (Tagesstatistik)` },
|
|
57
|
+
native: {},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
61
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
62
|
+
|
|
63
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
64
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
65
|
+
|
|
66
|
+
if (!isActive) {
|
|
67
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
68
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
69
|
+
ack: true,
|
|
70
|
+
});
|
|
71
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
72
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
73
|
+
ack: true,
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stateDefs = [
|
|
79
|
+
{ id: 'temp_min', def: null },
|
|
80
|
+
{ id: 'temp_max', def: null },
|
|
81
|
+
{ id: 'temp_min_time', def: '' },
|
|
82
|
+
{ id: 'temp_max_time', def: '' },
|
|
83
|
+
{ id: 'temp_avg', def: null },
|
|
84
|
+
{ id: 'data_points_count', def: 0 },
|
|
85
|
+
{ id: 'last_update', def: '' },
|
|
86
|
+
{ id: 'summary_json', def: '' },
|
|
87
|
+
{ id: 'summary_html', def: '' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const def of stateDefs) {
|
|
91
|
+
const fullPath = `${basePath}.${def.id}`;
|
|
92
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
93
|
+
if (!obj) {
|
|
94
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
95
|
+
type: 'state',
|
|
96
|
+
common: {
|
|
97
|
+
name: def.id,
|
|
98
|
+
type: typeof def.def === 'number' ? 'number' : 'string',
|
|
99
|
+
role: def.id.includes('time')
|
|
100
|
+
? 'value.time'
|
|
101
|
+
: def.id.includes('temp')
|
|
102
|
+
? 'value.temperature'
|
|
103
|
+
: 'value',
|
|
104
|
+
read: true,
|
|
105
|
+
write: false,
|
|
106
|
+
persist: true,
|
|
107
|
+
},
|
|
108
|
+
native: {},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
113
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
114
|
+
await adapter.setStateAsync(fullPath, { val: def.def, ack: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const outputBase = 'analytics.statistics.temperature.today.outputs';
|
|
120
|
+
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
121
|
+
type: 'channel',
|
|
122
|
+
common: { name: 'Gesamtausgaben (alle Sensoren)' },
|
|
123
|
+
native: {},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const outputStates = [
|
|
127
|
+
{ id: 'summary_all_json', def: '' },
|
|
128
|
+
{ id: 'summary_all_html', def: '' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const out of outputStates) {
|
|
132
|
+
const fullPath = `${outputBase}.${out.id}`;
|
|
133
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
134
|
+
if (!obj) {
|
|
135
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
136
|
+
type: 'state',
|
|
137
|
+
common: {
|
|
138
|
+
name: out.id,
|
|
139
|
+
type: 'string',
|
|
140
|
+
role: out.id.endsWith('json') ? 'json' : 'html',
|
|
141
|
+
read: true,
|
|
142
|
+
write: false,
|
|
143
|
+
persist: true,
|
|
144
|
+
},
|
|
145
|
+
native: {},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
150
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
151
|
+
await adapter.setStateAsync(fullPath, { val: out.id.endsWith('json') ? '{}' : '', ack: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Abonniert alle aktiven Temperatursensoren
|
|
158
|
+
*/
|
|
159
|
+
async _subscribeActiveSensors() {
|
|
160
|
+
const adapter = this.adapter;
|
|
161
|
+
for (const sensor of this.sensors) {
|
|
162
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
163
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
164
|
+
if (isActive) {
|
|
165
|
+
const stateId = `temperature.${sensor.id}.current`;
|
|
166
|
+
adapter.subscribeStates(stateId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
adapter.on('stateChange', async (id, state) => {
|
|
171
|
+
if (!state || state.ack === false) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const sensor of this.sensors) {
|
|
175
|
+
if (id.endsWith(`temperature.${sensor.id}.current`)) {
|
|
176
|
+
await this._processTemperatureChange(sensor.id, state.val);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Verarbeitung einer Temperaturänderung für einen Sensor.
|
|
185
|
+
* Aktualisiert Min-, Max- und Durchschnittswerte sowie die Zusammenfassungen.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} sensorId - Die ID des betroffenen Sensors (z. B. "outside" oder "flow").
|
|
188
|
+
* @param {number} newValue - Der neue gemessene Temperaturwert in °C.
|
|
189
|
+
*/
|
|
190
|
+
async _processTemperatureChange(sensorId, newValue) {
|
|
191
|
+
const adapter = this.adapter;
|
|
192
|
+
const basePath = `analytics.statistics.temperature.today.${sensorId}`;
|
|
193
|
+
const now = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
194
|
+
|
|
195
|
+
if (typeof newValue !== 'number') {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
200
|
+
const tempMax = (await adapter.getStateAsync(`${basePath}.temp_max`))?.val;
|
|
201
|
+
const tempAvg = (await adapter.getStateAsync(`${basePath}.temp_avg`))?.val;
|
|
202
|
+
const count = (await adapter.getStateAsync(`${basePath}.data_points_count`))?.val || 0;
|
|
203
|
+
|
|
204
|
+
let newMin = tempMin;
|
|
205
|
+
let newMax = tempMax;
|
|
206
|
+
let newAvg = tempAvg;
|
|
207
|
+
|
|
208
|
+
if (tempMin === null || newValue < tempMin) {
|
|
209
|
+
newMin = newValue;
|
|
210
|
+
await adapter.setStateAsync(`${basePath}.temp_min_time`, { val: now, ack: true });
|
|
211
|
+
}
|
|
212
|
+
if (tempMax === null || newValue > tempMax) {
|
|
213
|
+
newMax = newValue;
|
|
214
|
+
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Durchschnitt (gleitend)
|
|
218
|
+
const newCount = count + 1;
|
|
219
|
+
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
220
|
+
|
|
221
|
+
await adapter.setStateAsync(`${basePath}.temp_min`, { val: newMin, ack: true });
|
|
222
|
+
await adapter.setStateAsync(`${basePath}.temp_max`, { val: newMax, ack: true });
|
|
223
|
+
await adapter.setStateAsync(`${basePath}.temp_avg`, { val: Math.round(newAvg * 100) / 100, ack: true });
|
|
224
|
+
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
225
|
+
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
226
|
+
|
|
227
|
+
// Summary aktualisieren
|
|
228
|
+
const summary = {
|
|
229
|
+
temp_min: newMin,
|
|
230
|
+
temp_max: newMax,
|
|
231
|
+
temp_avg: Math.round(newAvg * 100) / 100,
|
|
232
|
+
updated: now,
|
|
233
|
+
};
|
|
234
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
235
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
236
|
+
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${Math.round(newAvg * 100) / 100} °C</div>`,
|
|
237
|
+
ack: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await this._updateOverallSummary();
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
245
|
+
*/
|
|
246
|
+
async _updateOverallSummary() {
|
|
247
|
+
const adapter = this.adapter;
|
|
248
|
+
const allData = [];
|
|
249
|
+
|
|
250
|
+
for (const sensor of this.sensors) {
|
|
251
|
+
const active = (await adapter.getStateAsync(`temperature.${sensor.id}.active`))?.val === true;
|
|
252
|
+
if (!active) {
|
|
253
|
+
allData.push({ name: sensor.name, status: 'kein Sensor aktiv' });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const min = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_min`))
|
|
258
|
+
?.val;
|
|
259
|
+
const max = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_max`))
|
|
260
|
+
?.val;
|
|
261
|
+
const avg = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_avg`))
|
|
262
|
+
?.val;
|
|
263
|
+
|
|
264
|
+
allData.push({ name: sensor.name, min, max, avg });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
268
|
+
val: JSON.stringify(allData),
|
|
269
|
+
ack: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
273
|
+
html += '<tr><th style="text-align:left;">Sensor</th><th>Min</th><th>Max</th><th>Ø</th></tr>';
|
|
274
|
+
for (const entry of allData) {
|
|
275
|
+
html += `<tr><td>${entry.name}</td><td>${entry.min ?? '-'}</td><td>${entry.max ?? '-'}</td><td>${entry.avg ?? '-'}</td></tr>`;
|
|
276
|
+
}
|
|
277
|
+
html += '</table>';
|
|
278
|
+
|
|
279
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
|
|
280
|
+
val: html,
|
|
281
|
+
ack: true,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Mitternacht-Reset planen
|
|
287
|
+
*/
|
|
288
|
+
async _scheduleMidnightReset() {
|
|
289
|
+
const adapter = this.adapter;
|
|
290
|
+
if (this.midnightTimer) {
|
|
291
|
+
clearTimeout(this.midnightTimer);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const now = new Date();
|
|
295
|
+
const nextMidnight = new Date(now);
|
|
296
|
+
nextMidnight.setHours(24, 0, 5, 0);
|
|
297
|
+
const msUntilMidnight = nextMidnight.getTime() - now.getTime();
|
|
298
|
+
|
|
299
|
+
this.midnightTimer = setTimeout(async () => {
|
|
300
|
+
await this._resetDailyTemperatureStats();
|
|
301
|
+
await this._scheduleMidnightReset();
|
|
302
|
+
}, msUntilMidnight);
|
|
303
|
+
|
|
304
|
+
adapter.log.debug(
|
|
305
|
+
`statisticsHelper: Mitternacht-Reset geplant in ${Math.round(msUntilMidnight / 60000)} Minuten.`,
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Tagesstatistik zurücksetzen
|
|
311
|
+
*/
|
|
312
|
+
async _resetDailyTemperatureStats() {
|
|
313
|
+
const adapter = this.adapter;
|
|
314
|
+
adapter.log.info('statisticsHelper: Tagesstatistik wird zurückgesetzt.');
|
|
315
|
+
|
|
316
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
317
|
+
|
|
318
|
+
for (const sensor of this.sensors) {
|
|
319
|
+
const basePath = `analytics.statistics.temperature.today.${sensor.id}`;
|
|
320
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
321
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
322
|
+
|
|
323
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
324
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
325
|
+
|
|
326
|
+
if (!isActive) {
|
|
327
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
328
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
329
|
+
ack: true,
|
|
330
|
+
});
|
|
331
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
332
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
333
|
+
ack: true,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const stateList = [
|
|
339
|
+
'temp_min',
|
|
340
|
+
'temp_max',
|
|
341
|
+
'temp_min_time',
|
|
342
|
+
'temp_max_time',
|
|
343
|
+
'temp_avg',
|
|
344
|
+
'data_points_count',
|
|
345
|
+
'last_update',
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
for (const state of stateList) {
|
|
349
|
+
const fullPath = `${basePath}.${state}`;
|
|
350
|
+
let defValue = null;
|
|
351
|
+
if (state.includes('time')) {
|
|
352
|
+
defValue = '';
|
|
353
|
+
}
|
|
354
|
+
if (state === 'data_points_count') {
|
|
355
|
+
defValue = 0;
|
|
356
|
+
}
|
|
357
|
+
if (state === 'last_update') {
|
|
358
|
+
defValue = resetDate;
|
|
359
|
+
}
|
|
360
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
364
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Tageswerte zurückgesetzt' }),
|
|
365
|
+
ack: true,
|
|
366
|
+
});
|
|
367
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
368
|
+
val: `<div style="color:gray;">Tageswerte zurückgesetzt (${resetDate})</div>`,
|
|
369
|
+
ack: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
374
|
+
val: '{}',
|
|
375
|
+
ack: true,
|
|
376
|
+
});
|
|
377
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
|
|
378
|
+
val: '',
|
|
379
|
+
ack: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
adapter.log.debug('statisticsHelper: Tagesstatistik zurückgesetzt.');
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
cleanup() {
|
|
386
|
+
if (this.midnightTimer) {
|
|
387
|
+
clearTimeout(this.midnightTimer);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
module.exports = statisticsHelper;
|
|
@@ -26,43 +26,6 @@ async function createControlStates(adapter) {
|
|
|
26
26
|
native: {},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
// Channel: control.season
|
|
30
|
-
await adapter.setObjectNotExistsAsync('control.season', {
|
|
31
|
-
type: 'channel',
|
|
32
|
-
common: {
|
|
33
|
-
name: 'Saisonsteuerung',
|
|
34
|
-
desc: 'Steuerung der aktiven Poolsaison und saisonabhängiger Funktionen',
|
|
35
|
-
},
|
|
36
|
-
native: {},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// State: control.season.active (mit Persist-Schutz)
|
|
40
|
-
await adapter.setObjectNotExistsAsync('control.season.active', {
|
|
41
|
-
type: 'state',
|
|
42
|
-
common: {
|
|
43
|
-
name: 'Poolsaison aktiv',
|
|
44
|
-
desc: 'Zeigt an, ob die Poolsaison aktiv ist (steuerbar über VIS oder Blockly)',
|
|
45
|
-
type: 'boolean',
|
|
46
|
-
role: 'switch',
|
|
47
|
-
read: true,
|
|
48
|
-
write: true,
|
|
49
|
-
def: false,
|
|
50
|
-
persist: true, // dauerhaft speichern
|
|
51
|
-
},
|
|
52
|
-
native: {},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const existingSeasonActive = await adapter.getStateAsync('control.season.active');
|
|
56
|
-
if (
|
|
57
|
-
existingSeasonActive === null ||
|
|
58
|
-
existingSeasonActive.val === null ||
|
|
59
|
-
existingSeasonActive.val === undefined
|
|
60
|
-
) {
|
|
61
|
-
const cfgValue = !!adapter.config.season_active;
|
|
62
|
-
await adapter.setStateAsync('control.season.active', { val: cfgValue, ack: true });
|
|
63
|
-
adapter.log.debug(`[controlStates] State control.season.active initialisiert mit Wert: ${cfgValue}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
29
|
// ---------------------------------------------------------------------
|
|
67
30
|
// Channel: control.pump
|
|
68
31
|
await adapter.setObjectNotExistsAsync('control.pump', {
|