iobroker.poolcontrol 0.5.3 → 0.5.4
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/io-package.json +14 -1
- package/lib/helpers/statisticsHelper.js +71 -26
- package/lib/helpers/statisticsHelperMonth.js +83 -52
- package/lib/helpers/statisticsHelperWeek.js +84 -52
- package/lib/helpers/temperatureHelper.js +0 -29
- package/lib/stateDefinitions/statisticsStates.js +4 -3
- package/package.json +1 -1
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "poolcontrol",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.5.4": {
|
|
7
|
+
"en": "Fixed a rare infinite loop during weekly and monthly statistics reset that could cause Redis overload. Added timer protection and improved stability.",
|
|
8
|
+
"de": "Selten auftretende Endlosschleife beim Wochen- und Monatsreset der Statistik behoben, die zu Redis-Überlastung führen konnte. Timer-Schutz und Stabilität verbessert.",
|
|
9
|
+
"ru": "Исправлена редкая бесконечная петля при сбросе еженедельной и ежемесячной статистики, вызывавшая перегрузку Redis. Добавлена защита таймера и улучшена стабильность.",
|
|
10
|
+
"pt": "Corrigido um loop infinito raro durante a redefinição das estatísticas semanais e mensais que podia causar sobrecarga do Redis. Adicionada proteção de temporizador e melhorada a estabilidade.",
|
|
11
|
+
"nl": "Zeldzame oneindige lus opgelost tijdens de reset van wekelijkse en maandelijkse statistieken die Redis kon overbelasten. Timerbescherming en stabiliteit verbeterd.",
|
|
12
|
+
"fr": "Correction d'une boucle infinie rare lors de la réinitialisation des statistiques hebdomadaires et mensuelles pouvant provoquer une surcharge de Redis. Protection du minuteur et stabilité améliorées.",
|
|
13
|
+
"it": "Corretto un raro ciclo infinito durante il reset delle statistiche settimanali e mensili che poteva causare un sovraccarico di Redis. Migliorata la protezione del timer e la stabilità.",
|
|
14
|
+
"es": "Se corrigió un bucle infinito poco frecuente durante el reinicio de las estadísticas semanales y mensuales que podía causar sobrecarga de Redis. Protección de temporizador y estabilidad mejoradas.",
|
|
15
|
+
"pl": "Naprawiono rzadką nieskończoną pętlę podczas resetowania statystyk tygodniowych i miesięcznych, która mogła powodować przeciążenie Redis. Dodano ochronę timera i poprawiono stabilność.",
|
|
16
|
+
"uk": "Виправлено рідкісну нескінченну петлю під час скидання тижневої та місячної статистики, що могла спричинити перевантаження Redis. Додано захист таймера та покращено стабільність.",
|
|
17
|
+
"zh-cn": "修复了每周和每月统计重置期间可能导致 Redis 过载的罕见无限循环。改进了计时器保护和系统稳定性。"
|
|
18
|
+
},
|
|
6
19
|
"0.5.3": {
|
|
7
20
|
"en": "Added user selection for Telegram notifications. If no user is selected, messages are sent globally as before; if one or more usernames are specified, only those users receive the messages. Admin UI visually improved (recipient field indented under the instance). speechHelper and jsonConfig.json updated.",
|
|
8
21
|
"de": "Benutzerauswahl für Telegram-Benachrichtigungen hinzugefügt. Wenn kein Benutzer ausgewählt ist, werden Nachrichten wie bisher global gesendet; bei einem oder mehreren Benutzernamen erhalten nur diese die Nachrichten. Admin-UI optisch verbessert (Empfängerfeld unter der Instanz eingerückt). speechHelper und jsonConfig.json aktualisiert.",
|
|
@@ -35,7 +35,6 @@ const statisticsHelper = {
|
|
|
35
35
|
|
|
36
36
|
// --- Überinstallationsschutz ---
|
|
37
37
|
try {
|
|
38
|
-
// Prüft, ob alle States vorhanden sind, und legt fehlende still neu an
|
|
39
38
|
await this._verifyStructure();
|
|
40
39
|
} catch {
|
|
41
40
|
// keine Log-Ausgabe – stiller Schutz
|
|
@@ -44,6 +43,22 @@ const statisticsHelper = {
|
|
|
44
43
|
try {
|
|
45
44
|
await this._createTemperatureStatistics();
|
|
46
45
|
await this._subscribeActiveSensors();
|
|
46
|
+
|
|
47
|
+
// 🟢 NEU: Listener für Reset-Button (Einzelsensor)
|
|
48
|
+
adapter.subscribeStates('analytics.statistics.temperature.today.*.reset_today');
|
|
49
|
+
adapter.on('stateChange', async (id, state) => {
|
|
50
|
+
if (!state || state.ack === true) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (id.includes('analytics.statistics.temperature.today.') && id.endsWith('.reset_today')) {
|
|
55
|
+
const sensorId = id.split('.').slice(-2, -1)[0];
|
|
56
|
+
adapter.log.info(`[statisticsHelper] Manueller Reset für Sensor "${sensorId}" ausgelöst.`);
|
|
57
|
+
await this._resetSingleSensor(sensorId);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// 🔵 ENDE NEU
|
|
61
|
+
|
|
47
62
|
await this._scheduleMidnightReset();
|
|
48
63
|
adapter.log.debug('statisticsHelper: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
49
64
|
} catch (err) {
|
|
@@ -51,6 +66,55 @@ const statisticsHelper = {
|
|
|
51
66
|
}
|
|
52
67
|
},
|
|
53
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Setzt alle Statistikwerte eines einzelnen Sensors (manueller Reset-Button).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} sensorId - ID des Sensors, z. B. "surface" oder "flow".
|
|
73
|
+
*/
|
|
74
|
+
async _resetSingleSensor(sensorId) {
|
|
75
|
+
const adapter = this.adapter;
|
|
76
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
77
|
+
const basePath = `analytics.statistics.temperature.today.${sensorId}`;
|
|
78
|
+
adapter.log.debug(`[statisticsHelper] Starte Einzelreset für ${sensorId}.`);
|
|
79
|
+
|
|
80
|
+
const stateList = [
|
|
81
|
+
'temp_min',
|
|
82
|
+
'temp_max',
|
|
83
|
+
'temp_min_time',
|
|
84
|
+
'temp_max_time',
|
|
85
|
+
'temp_avg',
|
|
86
|
+
'data_points_count',
|
|
87
|
+
'last_update',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const state of stateList) {
|
|
91
|
+
let defValue = null;
|
|
92
|
+
if (state.includes('time')) {
|
|
93
|
+
defValue = '';
|
|
94
|
+
}
|
|
95
|
+
if (state === 'data_points_count') {
|
|
96
|
+
defValue = 0;
|
|
97
|
+
}
|
|
98
|
+
if (state === 'last_update') {
|
|
99
|
+
defValue = resetDate;
|
|
100
|
+
}
|
|
101
|
+
await adapter.setStateAsync(`${basePath}.${state}`, { val: defValue, ack: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, {
|
|
105
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Tageswerte zurückgesetzt' }),
|
|
106
|
+
ack: true,
|
|
107
|
+
});
|
|
108
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
109
|
+
val: `<div style="color:gray;">Tageswerte zurückgesetzt (${resetDate})</div>`,
|
|
110
|
+
ack: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this._updateOverallSummary();
|
|
114
|
+
adapter.log.info(`[statisticsHelper] Einzelreset für Sensor "${sensorId}" abgeschlossen.`);
|
|
115
|
+
},
|
|
116
|
+
// 🔵 ENDE NEU
|
|
117
|
+
|
|
54
118
|
/**
|
|
55
119
|
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
56
120
|
*/
|
|
@@ -72,7 +136,6 @@ const statisticsHelper = {
|
|
|
72
136
|
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
73
137
|
|
|
74
138
|
if (!isActive) {
|
|
75
|
-
// Leere JSONs vorbereiten, damit spätere Temperaturwerte sie befüllen können
|
|
76
139
|
await adapter.setStateAsync(summaryJsonPath, { val: '[]', ack: true });
|
|
77
140
|
await adapter.setStateAsync(summaryHtmlPath, { val: '', ack: true });
|
|
78
141
|
continue;
|
|
@@ -184,11 +247,10 @@ const statisticsHelper = {
|
|
|
184
247
|
},
|
|
185
248
|
|
|
186
249
|
/**
|
|
187
|
-
*
|
|
188
|
-
* Aktualisiert Min-, Max- und Durchschnittswerte sowie die Zusammenfassungen.
|
|
250
|
+
* Verarbeitet Temperaturänderungen und aktualisiert Statistikdaten.
|
|
189
251
|
*
|
|
190
|
-
* @param {string} sensorId -
|
|
191
|
-
* @param {number} newValue -
|
|
252
|
+
* @param {string} sensorId - ID des betroffenen Sensors.
|
|
253
|
+
* @param {number} newValue - Neuer gemessener Temperaturwert in °C.
|
|
192
254
|
*/
|
|
193
255
|
async _processTemperatureChange(sensorId, newValue) {
|
|
194
256
|
const adapter = this.adapter;
|
|
@@ -199,7 +261,6 @@ const statisticsHelper = {
|
|
|
199
261
|
return;
|
|
200
262
|
}
|
|
201
263
|
|
|
202
|
-
// NEU: Rundung auf 1 Nachkommastelle
|
|
203
264
|
newValue = Math.round(newValue * 10) / 10;
|
|
204
265
|
|
|
205
266
|
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
@@ -220,11 +281,9 @@ const statisticsHelper = {
|
|
|
220
281
|
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
221
282
|
}
|
|
222
283
|
|
|
223
|
-
// Durchschnitt (gleitend)
|
|
224
284
|
const newCount = count + 1;
|
|
225
285
|
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
226
286
|
|
|
227
|
-
// NEU: Alle Temperaturwerte auf 1 Nachkommastelle runden
|
|
228
287
|
newMin = Math.round(newMin * 10) / 10;
|
|
229
288
|
newMax = Math.round(newMax * 10) / 10;
|
|
230
289
|
newAvg = Math.round(newAvg * 10) / 10;
|
|
@@ -235,7 +294,6 @@ const statisticsHelper = {
|
|
|
235
294
|
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
236
295
|
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
237
296
|
|
|
238
|
-
// Summary aktualisieren – erweitert um Datum, Zeitpunkte, Messanzahl, Name
|
|
239
297
|
const summary = {
|
|
240
298
|
name: 'Tagesstatistik',
|
|
241
299
|
date: new Date().toISOString().slice(0, 10),
|
|
@@ -249,7 +307,6 @@ const statisticsHelper = {
|
|
|
249
307
|
};
|
|
250
308
|
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
251
309
|
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
252
|
-
// NEU: HTML-Ausgabe mit gerundeten Werten
|
|
253
310
|
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${newAvg} °C</div>`,
|
|
254
311
|
ack: true,
|
|
255
312
|
});
|
|
@@ -258,16 +315,13 @@ const statisticsHelper = {
|
|
|
258
315
|
},
|
|
259
316
|
|
|
260
317
|
/**
|
|
261
|
-
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
262
|
-
* Liest die fertigen summary_json-Werte aller aktiven Sensoren aus
|
|
263
|
-
* und erstellt daraus eine zusammengefasste HTML- und JSON-Ausgabe.
|
|
318
|
+
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
264
319
|
*/
|
|
265
320
|
async _updateOverallSummary() {
|
|
266
321
|
const adapter = this.adapter;
|
|
267
322
|
const allData = [];
|
|
268
323
|
|
|
269
324
|
try {
|
|
270
|
-
// Alle Sensoren durchlaufen
|
|
271
325
|
for (const sensor of this.sensors) {
|
|
272
326
|
const summaryState = await adapter.getStateAsync(
|
|
273
327
|
`analytics.statistics.temperature.today.${sensor.id}.summary_json`,
|
|
@@ -277,7 +331,6 @@ const statisticsHelper = {
|
|
|
277
331
|
continue;
|
|
278
332
|
}
|
|
279
333
|
|
|
280
|
-
// JSON-Inhalt parsen
|
|
281
334
|
let parsed;
|
|
282
335
|
try {
|
|
283
336
|
parsed = JSON.parse(summaryState.val);
|
|
@@ -293,17 +346,14 @@ const statisticsHelper = {
|
|
|
293
346
|
const maxTime = parsed.temp_max_time || '';
|
|
294
347
|
const count = parsed.data_points_count || 0;
|
|
295
348
|
|
|
296
|
-
// Falls gar keine Werte vorhanden, überspringen
|
|
297
349
|
if (min == null && max == null && avg == null) {
|
|
298
350
|
continue;
|
|
299
351
|
}
|
|
300
352
|
|
|
301
|
-
// Werte runden
|
|
302
353
|
const rMin = typeof min === 'number' ? Math.round(min * 10) / 10 : min;
|
|
303
354
|
const rMax = typeof max === 'number' ? Math.round(max * 10) / 10 : max;
|
|
304
355
|
const rAvg = typeof avg === 'number' ? Math.round(avg * 10) / 10 : avg;
|
|
305
356
|
|
|
306
|
-
// Erweiterte Datensammlung
|
|
307
357
|
allData.push({
|
|
308
358
|
name: sensor.name,
|
|
309
359
|
date,
|
|
@@ -316,7 +366,6 @@ const statisticsHelper = {
|
|
|
316
366
|
});
|
|
317
367
|
}
|
|
318
368
|
|
|
319
|
-
// Wenn noch keine Daten vorliegen → leer lassen
|
|
320
369
|
if (allData.length === 0) {
|
|
321
370
|
await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
322
371
|
val: '[]',
|
|
@@ -329,10 +378,8 @@ const statisticsHelper = {
|
|
|
329
378
|
return;
|
|
330
379
|
}
|
|
331
380
|
|
|
332
|
-
// JSON-Ausgabe erstellen (komplett erweitert)
|
|
333
381
|
const jsonOutput = JSON.stringify(allData, null, 2);
|
|
334
382
|
|
|
335
|
-
// HTML-Ausgabe erstellen (mit Datum & Anzahl Messwerte)
|
|
336
383
|
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
337
384
|
html +=
|
|
338
385
|
'<tr><th style="text-align:left;">Sensor</th><th>Datum</th><th>Min</th><th>Zeit</th><th>Max</th><th>Zeit</th><th>Ø</th><th>Anz.</th></tr>';
|
|
@@ -350,7 +397,6 @@ const statisticsHelper = {
|
|
|
350
397
|
}
|
|
351
398
|
html += '</table>';
|
|
352
399
|
|
|
353
|
-
// States setzen (auch bei gleichen Werten → Timestamp aktualisieren)
|
|
354
400
|
await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
355
401
|
val: jsonOutput,
|
|
356
402
|
ack: true,
|
|
@@ -389,7 +435,7 @@ const statisticsHelper = {
|
|
|
389
435
|
},
|
|
390
436
|
|
|
391
437
|
/**
|
|
392
|
-
* Tagesstatistik zurücksetzen
|
|
438
|
+
* Tagesstatistik zurücksetzen (automatischer Reset)
|
|
393
439
|
*/
|
|
394
440
|
async _resetDailyTemperatureStats() {
|
|
395
441
|
const adapter = this.adapter;
|
|
@@ -465,8 +511,7 @@ const statisticsHelper = {
|
|
|
465
511
|
},
|
|
466
512
|
|
|
467
513
|
/**
|
|
468
|
-
*
|
|
469
|
-
* Prüft und legt fehlende States erneut an, ohne bestehende Werte zu überschreiben.
|
|
514
|
+
* Überinstallationsschutz
|
|
470
515
|
*/
|
|
471
516
|
async _verifyStructure() {
|
|
472
517
|
try {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const statisticsHelperMonth = {
|
|
19
19
|
adapter: null,
|
|
20
20
|
monthResetTimer: null,
|
|
21
|
+
isResetting: false,
|
|
21
22
|
sensors: [
|
|
22
23
|
{ id: 'outside', name: 'Außentemperatur' },
|
|
23
24
|
{ id: 'ground', name: 'Bodentemperatur' },
|
|
@@ -348,6 +349,21 @@ const statisticsHelperMonth = {
|
|
|
348
349
|
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
349
350
|
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
350
351
|
|
|
352
|
+
// 🟢 NEU: Schutz – falls Resetzeitpunkt in der Vergangenheit liegt
|
|
353
|
+
if (msUntilReset < 60 * 1000) {
|
|
354
|
+
// unter 1 Minute Differenz
|
|
355
|
+
adapter.log.warn(
|
|
356
|
+
'statisticsHelperMonth: Berechneter Resetzeitpunkt liegt in der Vergangenheit – Korrigiere auf nächsten Monat.',
|
|
357
|
+
);
|
|
358
|
+
const corrected = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
359
|
+
const diff = corrected.getTime() - now.getTime();
|
|
360
|
+
this.monthResetTimer = setTimeout(async () => {
|
|
361
|
+
await this._resetMonthlyTemperatureStats();
|
|
362
|
+
await this._scheduleMonthReset();
|
|
363
|
+
}, diff);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
351
367
|
this.monthResetTimer = setTimeout(async () => {
|
|
352
368
|
await this._resetMonthlyTemperatureStats();
|
|
353
369
|
await this._scheduleMonthReset();
|
|
@@ -363,75 +379,90 @@ const statisticsHelperMonth = {
|
|
|
363
379
|
*/
|
|
364
380
|
async _resetMonthlyTemperatureStats() {
|
|
365
381
|
const adapter = this.adapter;
|
|
366
|
-
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
367
382
|
|
|
368
|
-
|
|
383
|
+
// 🟢 NEU: Schutz vor Endlosschleifen und Mehrfachausführung
|
|
384
|
+
if (this.isResetting) {
|
|
385
|
+
adapter.log.debug('statisticsHelperMonth: Reset bereits aktiv – übersprungen.');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.isResetting = true;
|
|
369
389
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const activeState = `temperature.${sensor.id}.active`;
|
|
373
|
-
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
390
|
+
try {
|
|
391
|
+
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
374
392
|
|
|
375
|
-
|
|
376
|
-
const
|
|
393
|
+
// 🟢 NEU: fehlende Zeile wieder einfügen
|
|
394
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
395
|
+
|
|
396
|
+
for (const sensor of this.sensors) {
|
|
397
|
+
const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
|
|
398
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
399
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
400
|
+
|
|
401
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
402
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
403
|
+
|
|
404
|
+
if (!isActive) {
|
|
405
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
406
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
407
|
+
ack: true,
|
|
408
|
+
});
|
|
409
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
410
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
411
|
+
ack: true,
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const stateList = [
|
|
417
|
+
'temp_min',
|
|
418
|
+
'temp_max',
|
|
419
|
+
'temp_min_time',
|
|
420
|
+
'temp_max_time',
|
|
421
|
+
'temp_avg',
|
|
422
|
+
'data_points_count',
|
|
423
|
+
'last_update',
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
for (const state of stateList) {
|
|
427
|
+
const fullPath = `${basePath}.${state}`;
|
|
428
|
+
let defValue = null;
|
|
429
|
+
if (state.includes('time')) {
|
|
430
|
+
defValue = '';
|
|
431
|
+
}
|
|
432
|
+
if (state === 'data_points_count') {
|
|
433
|
+
defValue = 0;
|
|
434
|
+
}
|
|
435
|
+
if (state === 'last_update') {
|
|
436
|
+
defValue = resetDate;
|
|
437
|
+
}
|
|
438
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
439
|
+
}
|
|
377
440
|
|
|
378
|
-
if (!isActive) {
|
|
379
441
|
await adapter.setStateAsync(summaryJsonPath, {
|
|
380
|
-
val: JSON.stringify({ status: '
|
|
442
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Monatswerte zurückgesetzt' }),
|
|
381
443
|
ack: true,
|
|
382
444
|
});
|
|
383
445
|
await adapter.setStateAsync(summaryHtmlPath, {
|
|
384
|
-
val:
|
|
446
|
+
val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
|
|
385
447
|
ack: true,
|
|
386
448
|
});
|
|
387
|
-
continue;
|
|
388
449
|
}
|
|
389
450
|
|
|
390
|
-
|
|
391
|
-
'
|
|
392
|
-
'temp_max',
|
|
393
|
-
'temp_min_time',
|
|
394
|
-
'temp_max_time',
|
|
395
|
-
'temp_avg',
|
|
396
|
-
'data_points_count',
|
|
397
|
-
'last_update',
|
|
398
|
-
];
|
|
399
|
-
|
|
400
|
-
for (const state of stateList) {
|
|
401
|
-
const fullPath = `${basePath}.${state}`;
|
|
402
|
-
let defValue = null;
|
|
403
|
-
if (state.includes('time')) {
|
|
404
|
-
defValue = '';
|
|
405
|
-
}
|
|
406
|
-
if (state === 'data_points_count') {
|
|
407
|
-
defValue = 0;
|
|
408
|
-
}
|
|
409
|
-
if (state === 'last_update') {
|
|
410
|
-
defValue = resetDate;
|
|
411
|
-
}
|
|
412
|
-
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await adapter.setStateAsync(summaryJsonPath, {
|
|
416
|
-
val: JSON.stringify({ date_reset: resetDate, status: 'Monatswerte zurückgesetzt' }),
|
|
451
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
452
|
+
val: '{}',
|
|
417
453
|
ack: true,
|
|
418
454
|
});
|
|
419
|
-
await adapter.setStateAsync(
|
|
420
|
-
val:
|
|
455
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
456
|
+
val: '',
|
|
421
457
|
ack: true,
|
|
422
458
|
});
|
|
423
|
-
}
|
|
424
459
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ack: true,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
460
|
+
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
461
|
+
} catch (err) {
|
|
462
|
+
adapter.log.warn(`statisticsHelperMonth: Fehler beim Monatsreset: ${err.message}`);
|
|
463
|
+
} finally {
|
|
464
|
+
this.isResetting = false; // 🟢 NEU: Flag wieder freigeben
|
|
465
|
+
}
|
|
435
466
|
},
|
|
436
467
|
|
|
437
468
|
/**
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const statisticsHelperWeek = {
|
|
19
19
|
adapter: null,
|
|
20
20
|
weekResetTimer: null,
|
|
21
|
+
isResetting: false, // 🟢 NEU
|
|
21
22
|
sensors: [
|
|
22
23
|
{ id: 'outside', name: 'Außentemperatur' },
|
|
23
24
|
{ id: 'ground', name: 'Bodentemperatur' },
|
|
@@ -368,6 +369,24 @@ const statisticsHelperWeek = {
|
|
|
368
369
|
nextReset.setHours(0, 5, 0, 0);
|
|
369
370
|
|
|
370
371
|
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
372
|
+
|
|
373
|
+
// 🟢 NEU: Schutz – falls Resetzeitpunkt in der Vergangenheit liegt
|
|
374
|
+
if (msUntilReset < 60 * 1000) {
|
|
375
|
+
// unter 1 Minute Differenz
|
|
376
|
+
adapter.log.warn(
|
|
377
|
+
'statisticsHelperWeek: Berechneter Resetzeitpunkt liegt in der Vergangenheit – Korrigiere auf nächste Woche.',
|
|
378
|
+
);
|
|
379
|
+
const corrected = new Date(now);
|
|
380
|
+
corrected.setDate(now.getDate() + 7);
|
|
381
|
+
corrected.setHours(0, 5, 0, 0);
|
|
382
|
+
const diff = corrected.getTime() - now.getTime();
|
|
383
|
+
this.weekResetTimer = setTimeout(async () => {
|
|
384
|
+
await this._resetWeeklyTemperatureStats();
|
|
385
|
+
await this._scheduleWeekReset();
|
|
386
|
+
}, diff);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
371
390
|
this.weekResetTimer = setTimeout(async () => {
|
|
372
391
|
await this._resetWeeklyTemperatureStats();
|
|
373
392
|
await this._scheduleWeekReset();
|
|
@@ -381,75 +400,88 @@ const statisticsHelperWeek = {
|
|
|
381
400
|
*/
|
|
382
401
|
async _resetWeeklyTemperatureStats() {
|
|
383
402
|
const adapter = this.adapter;
|
|
384
|
-
adapter.log.info('statisticsHelperWeek: Wochenstatistik wird zurückgesetzt.');
|
|
385
403
|
|
|
386
|
-
|
|
404
|
+
// 🟢 NEU: Schutz vor Endlosschleifen und Mehrfachausführung
|
|
405
|
+
if (this.isResetting) {
|
|
406
|
+
adapter.log.debug('statisticsHelperWeek: Reset bereits aktiv – übersprungen.');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.isResetting = true;
|
|
387
410
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
411
|
+
try {
|
|
412
|
+
adapter.log.info('statisticsHelperWeek: Wochenstatistik wird zurückgesetzt.');
|
|
413
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
392
414
|
|
|
393
|
-
const
|
|
394
|
-
|
|
415
|
+
for (const sensor of this.sensors) {
|
|
416
|
+
const basePath = `analytics.statistics.temperature.week.${sensor.id}`;
|
|
417
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
418
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
419
|
+
|
|
420
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
421
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
422
|
+
|
|
423
|
+
if (!isActive) {
|
|
424
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
425
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
426
|
+
ack: true,
|
|
427
|
+
});
|
|
428
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
429
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
430
|
+
ack: true,
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const stateList = [
|
|
436
|
+
'temp_min',
|
|
437
|
+
'temp_max',
|
|
438
|
+
'temp_min_time',
|
|
439
|
+
'temp_max_time',
|
|
440
|
+
'temp_avg',
|
|
441
|
+
'data_points_count',
|
|
442
|
+
'last_update',
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
for (const state of stateList) {
|
|
446
|
+
const fullPath = `${basePath}.${state}`;
|
|
447
|
+
let defValue = null;
|
|
448
|
+
if (state.includes('time')) {
|
|
449
|
+
defValue = '';
|
|
450
|
+
}
|
|
451
|
+
if (state === 'data_points_count') {
|
|
452
|
+
defValue = 0;
|
|
453
|
+
}
|
|
454
|
+
if (state === 'last_update') {
|
|
455
|
+
defValue = resetDate;
|
|
456
|
+
}
|
|
457
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
458
|
+
}
|
|
395
459
|
|
|
396
|
-
if (!isActive) {
|
|
397
460
|
await adapter.setStateAsync(summaryJsonPath, {
|
|
398
|
-
val: JSON.stringify({ status: '
|
|
461
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Wochenwerte zurückgesetzt' }),
|
|
399
462
|
ack: true,
|
|
400
463
|
});
|
|
401
464
|
await adapter.setStateAsync(summaryHtmlPath, {
|
|
402
|
-
val:
|
|
465
|
+
val: `<div style="color:gray;">Wochenwerte zurückgesetzt (${resetDate})</div>`,
|
|
403
466
|
ack: true,
|
|
404
467
|
});
|
|
405
|
-
continue;
|
|
406
468
|
}
|
|
407
469
|
|
|
408
|
-
|
|
409
|
-
'
|
|
410
|
-
'temp_max',
|
|
411
|
-
'temp_min_time',
|
|
412
|
-
'temp_max_time',
|
|
413
|
-
'temp_avg',
|
|
414
|
-
'data_points_count',
|
|
415
|
-
'last_update',
|
|
416
|
-
];
|
|
417
|
-
|
|
418
|
-
for (const state of stateList) {
|
|
419
|
-
const fullPath = `${basePath}.${state}`;
|
|
420
|
-
let defValue = null;
|
|
421
|
-
if (state.includes('time')) {
|
|
422
|
-
defValue = '';
|
|
423
|
-
}
|
|
424
|
-
if (state === 'data_points_count') {
|
|
425
|
-
defValue = 0;
|
|
426
|
-
}
|
|
427
|
-
if (state === 'last_update') {
|
|
428
|
-
defValue = resetDate;
|
|
429
|
-
}
|
|
430
|
-
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
await adapter.setStateAsync(summaryJsonPath, {
|
|
434
|
-
val: JSON.stringify({ date_reset: resetDate, status: 'Wochenwerte zurückgesetzt' }),
|
|
470
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
471
|
+
val: '{}',
|
|
435
472
|
ack: true,
|
|
436
473
|
});
|
|
437
|
-
await adapter.setStateAsync(
|
|
438
|
-
val:
|
|
474
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
475
|
+
val: '',
|
|
439
476
|
ack: true,
|
|
440
477
|
});
|
|
441
|
-
}
|
|
442
478
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
ack: true,
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
adapter.log.debug('statisticsHelperWeek: Wochenstatistik zurückgesetzt.');
|
|
479
|
+
adapter.log.debug('statisticsHelperWeek: Wochenstatistik zurückgesetzt.');
|
|
480
|
+
} catch (err) {
|
|
481
|
+
adapter.log.warn(`statisticsHelperWeek: Fehler beim Wochenreset: ${err.message}`);
|
|
482
|
+
} finally {
|
|
483
|
+
this.isResetting = false; // 🟢 NEU: Flag wieder freigeben
|
|
484
|
+
}
|
|
453
485
|
},
|
|
454
486
|
|
|
455
487
|
/**
|
|
@@ -76,35 +76,6 @@ const temperatureHelper = {
|
|
|
76
76
|
: 'keine Sensoren konfiguriert'
|
|
77
77
|
}`,
|
|
78
78
|
);
|
|
79
|
-
// ------------------------------------------------------
|
|
80
|
-
// NEU: Re-Subscribe-Überwachung für hängende Sensoren
|
|
81
|
-
// ------------------------------------------------------
|
|
82
|
-
setInterval(
|
|
83
|
-
async () => {
|
|
84
|
-
for (const [key, id] of Object.entries(this.sensors)) {
|
|
85
|
-
try {
|
|
86
|
-
const state = await this.adapter.getStateAsync(`temperature.${key}.current`);
|
|
87
|
-
const lastTs = state?.ts || 0;
|
|
88
|
-
const minutes = (Date.now() - lastTs) / 60000;
|
|
89
|
-
|
|
90
|
-
// Wenn länger als 10 Minuten keine Aktualisierung, Subscribe erneuern
|
|
91
|
-
if (minutes > 10) {
|
|
92
|
-
this.adapter.log.warn(
|
|
93
|
-
`[temperatureHelper] Keine Aktualisierung für ${key} seit ${Math.round(
|
|
94
|
-
minutes,
|
|
95
|
-
)} Min – re-subscribing ${id}`,
|
|
96
|
-
);
|
|
97
|
-
this.adapter.subscribeForeignStates(id);
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
this.adapter.log.debug(
|
|
101
|
-
`[temperatureHelper] Re-Subscribe-Prüfung für ${key} fehlgeschlagen: ${err.message}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
10 * 60 * 1000,
|
|
107
|
-
); // alle 10 Minuten prüfen
|
|
108
79
|
},
|
|
109
80
|
|
|
110
81
|
_collectActiveSensors(adapter) {
|
|
@@ -100,6 +100,7 @@ async function _createTemperatureStatsGroup(adapter, periodId, displayName) {
|
|
|
100
100
|
{ id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
101
101
|
{ id: 'data_points_count', name: 'Anzahl Messwerte', type: 'number', role: 'value' },
|
|
102
102
|
{ id: 'last_update', name: 'Letzte Aktualisierung', type: 'string', role: 'value.time' },
|
|
103
|
+
{ id: 'reset_today', name: 'Tagesstatistik zurücksetzen', type: 'boolean', role: 'button' },
|
|
103
104
|
{
|
|
104
105
|
id: 'summary_json',
|
|
105
106
|
name: `${displayName} (JSON)`,
|
|
@@ -122,9 +123,9 @@ async function _createTemperatureStatsGroup(adapter, periodId, displayName) {
|
|
|
122
123
|
type: def.type,
|
|
123
124
|
role: def.role,
|
|
124
125
|
unit: def.unit || undefined,
|
|
125
|
-
read: true,
|
|
126
|
-
write: false,
|
|
127
|
-
def: def.type === 'number' ? null : '',
|
|
126
|
+
read: def.type === 'boolean' && def.role === 'button' ? false : true,
|
|
127
|
+
write: def.type === 'boolean' && def.role === 'button' ? true : false,
|
|
128
|
+
def: def.type === 'number' ? null : def.type === 'boolean' ? false : '',
|
|
128
129
|
persist: true,
|
|
129
130
|
},
|
|
130
131
|
native: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.poolcontrol",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Steuerung & Automatisierung für den Pool (Pumpe, Heizung, Ventile, Sensoren).",
|
|
5
5
|
"author": "DasBo1975 <dasbo1975@outlook.de>",
|
|
6
6
|
"homepage": "https://github.com/DasBo1975/ioBroker.poolcontrol",
|