iobroker.poolcontrol 0.5.3 → 0.5.5
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 +3 -0
- package/admin/jsonConfig.json +1 -2
- package/io-package.json +27 -14
- package/lib/helpers/statisticsHelper.js +71 -26
- package/lib/helpers/statisticsHelperMonth.js +90 -54
- package/lib/helpers/statisticsHelperWeek.js +85 -53
- package/lib/helpers/temperatureHelper.js +0 -29
- package/lib/stateDefinitions/statisticsStates.js +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,9 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
|
|
|
121
121
|
## Changelog
|
|
122
122
|
### **WORK IN PROGRESS**
|
|
123
123
|
|
|
124
|
+
## v0.5.5 (2025-11-01)
|
|
125
|
+
- Endlosschleife in Statistik Woche und Monat behoben
|
|
126
|
+
|
|
124
127
|
## v0.5.3 (2025-10-30)
|
|
125
128
|
- Telegram-Benutzerwahl hinzugefügt
|
|
126
129
|
|
package/admin/jsonConfig.json
CHANGED
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "poolcontrol",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.5",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.5.5": {
|
|
7
|
+
"en": "Fixed remaining timer recursion issue in weekly and monthly statistics to fully prevent infinite reset loops. Cleaned up jsonConfig structure and improved timer handling.",
|
|
8
|
+
"de": "Verbleibende Timer-Rekursion in Wochen- und Monatsstatistik behoben, um Endlosschleifen beim Reset endgültig zu verhindern. jsonConfig-Struktur bereinigt und Timer-Handling verbessert.",
|
|
9
|
+
"ru": "Исправлена оставшаяся рекурсия таймера в еженедельной и ежемесячной статистике для полного предотвращения бесконечных циклов сброса. Упрощена структура jsonConfig и улучшено управление таймерами.",
|
|
10
|
+
"pt": "Corrigida a recursão restante do temporizador nas estatísticas semanais e mensais para evitar completamente loops de redefinição infinitos. Estrutura jsonConfig limpa e manuseio de temporizador aprimorado.",
|
|
11
|
+
"nl": "Overgebleven timerrecursie in week- en maandstatistieken opgelost om oneindige resetlussen volledig te voorkomen. jsonConfig-structuur opgeschoond en timerafhandeling verbeterd.",
|
|
12
|
+
"fr": "Correction de la récursion restante du minuteur dans les statistiques hebdomadaires et mensuelles afin d'éviter définitivement les boucles de réinitialisation infinies. Structure jsonConfig nettoyée et gestion du minuteur améliorée.",
|
|
13
|
+
"it": "Corretta la ricorsione residua del timer nelle statistiche settimanali e mensili per prevenire completamente i loop di reset infiniti. Pulita la struttura jsonConfig e migliorata la gestione dei timer.",
|
|
14
|
+
"es": "Corregida la recursión restante del temporizador en las estadísticas semanales y mensuales para evitar completamente los bucles de reinicio infinitos. Estructura jsonConfig limpiada y manejo del temporizador mejorado.",
|
|
15
|
+
"pl": "Naprawiono pozostałą rekursję timera w statystykach tygodniowych i miesięcznych, aby całkowicie zapobiec nieskończonym pętlom resetowania. Uporządkowano strukturę jsonConfig i ulepszono obsługę timera.",
|
|
16
|
+
"uk": "Виправлено залишкову рекурсію таймера у тижневій та місячній статистиці, щоб повністю запобігти нескінченним циклам скидання. Очищено структуру jsonConfig і покращено обробку таймерів.",
|
|
17
|
+
"zh-cn": "修复了每周和每月统计中的剩余计时器递归问题,以完全防止无限重置循环。清理了 jsonConfig 结构并改进了计时器处理。"
|
|
18
|
+
},
|
|
19
|
+
"0.5.4": {
|
|
20
|
+
"en": "Fixed a rare infinite loop during weekly and monthly statistics reset that could cause Redis overload. Added timer protection and improved stability.",
|
|
21
|
+
"de": "Selten auftretende Endlosschleife beim Wochen- und Monatsreset der Statistik behoben, die zu Redis-Überlastung führen konnte. Timer-Schutz und Stabilität verbessert.",
|
|
22
|
+
"ru": "Исправлена редкая бесконечная петля при сбросе еженедельной и ежемесячной статистики, вызывавшая перегрузку Redis. Добавлена защита таймера и улучшена стабильность.",
|
|
23
|
+
"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.",
|
|
24
|
+
"nl": "Zeldzame oneindige lus opgelost tijdens de reset van wekelijkse en maandelijkse statistieken die Redis kon overbelasten. Timerbescherming en stabiliteit verbeterd.",
|
|
25
|
+
"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.",
|
|
26
|
+
"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à.",
|
|
27
|
+
"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.",
|
|
28
|
+
"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ść.",
|
|
29
|
+
"uk": "Виправлено рідкісну нескінченну петлю під час скидання тижневої та місячної статистики, що могла спричинити перевантаження Redis. Додано захист таймера та покращено стабільність.",
|
|
30
|
+
"zh-cn": "修复了每周和每月统计重置期间可能导致 Redis 过载的罕见无限循环。改进了计时器保护和系统稳定性。"
|
|
31
|
+
},
|
|
6
32
|
"0.5.3": {
|
|
7
33
|
"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
34
|
"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.",
|
|
@@ -65,19 +91,6 @@
|
|
|
65
91
|
"pl": "Dodano dzienne statystyki temperatury w analytics.statistics.temperature.today z automatycznym śledzeniem wartości min/max/średnich, podsumowaniami JSON i HTML oraz resetem o północy.",
|
|
66
92
|
"uk": "Додано щоденну статистику температури в analytics.statistics.temperature.today з автоматичним відстеженням мін/макс/середніх значень, JSON і HTML зведеннями та скиданням опівночі.",
|
|
67
93
|
"zh-cn": "在 analytics.statistics.temperature.today 中添加了每日温度统计,具有自动最小/最大/平均跟踪、JSON 和 HTML 摘要以及午夜重置功能。"
|
|
68
|
-
},
|
|
69
|
-
"0.3.1": {
|
|
70
|
-
"en": "Frost protection logic stabilized: fixed hysteresis of +2 °C and rounded temperature values to avoid pump switching fluctuations around 3 °C.",
|
|
71
|
-
"de": "Frostschutz-Logik stabilisiert: feste Hysterese von +2 °C und gerundete Temperaturwerte zur Vermeidung von Pumpenschaltflattern um 3 °C.",
|
|
72
|
-
"ru": "Логика защиты от замерзания стабилизирована: фиксированная гистерезис +2 °C и округленные значения температуры для предотвращения колебаний включения насоса около 3 °C.",
|
|
73
|
-
"pt": "Lógica de proteção contra congelamento estabilizada: histerese fixa de +2 °C e valores de temperatura arredondados para evitar flutuações de comutação da bomba em torno de 3 °C.",
|
|
74
|
-
"nl": "Vorstbeschermingslogica gestabiliseerd: vaste hysterese van +2 °C en afgeronde temperatuurwaarden om pompfluctuaties rond 3 °C te voorkomen.",
|
|
75
|
-
"fr": "Logique de protection antigel stabilisée : hystérésis fixe de +2 °C et valeurs de température arrondies pour éviter les fluctuations de commutation de la pompe autour de 3 °C.",
|
|
76
|
-
"it": "Logica di protezione antigelo stabilizzata: isteresi fissa di +2 °C e valori di temperatura arrotondati per evitare fluttuazioni di commutazione della pompa intorno a 3 °C.",
|
|
77
|
-
"es": "Lógica de protección contra heladas estabilizada: histéresis fija de +2 °C y valores de temperatura redondeados para evitar fluctuaciones de conmutación de la bomba alrededor de 3 °C.",
|
|
78
|
-
"pl": "Ustabilizowano logikę ochrony przed zamarzaniem: stała histereza +2 °C i zaokrąglone wartości temperatury, aby uniknąć wahań przełączania pompy w okolicach 3 °C.",
|
|
79
|
-
"uk": "Стабілізовано логіку захисту від замерзання: фіксована гістерезис +2 °C і округлені значення температури, щоб уникнути коливань увімкнення насоса біля 3 °C.",
|
|
80
|
-
"zh-cn": "防冻逻辑稳定:固定 +2 °C 滞后并四舍五入温度值,以避免泵在 3 °C 附近频繁切换。"
|
|
81
94
|
}
|
|
82
95
|
},
|
|
83
96
|
"titleLang": {
|
|
@@ -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' },
|
|
@@ -346,11 +347,31 @@ const statisticsHelperMonth = {
|
|
|
346
347
|
|
|
347
348
|
const now = new Date();
|
|
348
349
|
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
349
|
-
|
|
350
|
+
let msUntilReset = nextReset.getTime() - now.getTime();
|
|
351
|
+
|
|
352
|
+
// Wenn heute bereits der 1. ist → keinen sofortigen Reset starten!
|
|
353
|
+
if (now.getDate() === 1) {
|
|
354
|
+
adapter.log.debug('statisticsHelperMonth: Heute ist bereits der 1. – Reset wurde übersprungen.');
|
|
355
|
+
// Plan auf nächsten Monat verschieben
|
|
356
|
+
nextReset.setMonth(nextReset.getMonth() + 1);
|
|
357
|
+
msUntilReset = nextReset.getTime() - now.getTime();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Wenn Berechnung negativ oder zu klein ist → Sicherheit
|
|
361
|
+
if (msUntilReset <= 0 || msUntilReset < 10 * 60 * 1000) {
|
|
362
|
+
adapter.log.debug('statisticsHelperMonth: msUntilReset war zu klein – korrigiere Timer.');
|
|
363
|
+
msUntilReset = 10 * 60 * 1000; // mindestens 10 Minuten warten
|
|
364
|
+
}
|
|
350
365
|
|
|
366
|
+
// Timer setzen
|
|
351
367
|
this.monthResetTimer = setTimeout(async () => {
|
|
368
|
+
if (this.isResetting) {
|
|
369
|
+
adapter.log.debug('statisticsHelperMonth: Reset läuft bereits – Timer übersprungen.');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
352
373
|
await this._resetMonthlyTemperatureStats();
|
|
353
|
-
await this._scheduleMonthReset();
|
|
374
|
+
// await this._scheduleMonthReset();
|
|
354
375
|
}, msUntilReset);
|
|
355
376
|
|
|
356
377
|
adapter.log.debug(
|
|
@@ -363,75 +384,90 @@ const statisticsHelperMonth = {
|
|
|
363
384
|
*/
|
|
364
385
|
async _resetMonthlyTemperatureStats() {
|
|
365
386
|
const adapter = this.adapter;
|
|
366
|
-
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
367
387
|
|
|
368
|
-
|
|
388
|
+
// 🟢 NEU: Schutz vor Endlosschleifen und Mehrfachausführung
|
|
389
|
+
if (this.isResetting) {
|
|
390
|
+
adapter.log.debug('statisticsHelperMonth: Reset bereits aktiv – übersprungen.');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.isResetting = true;
|
|
369
394
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const activeState = `temperature.${sensor.id}.active`;
|
|
373
|
-
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
395
|
+
try {
|
|
396
|
+
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
374
397
|
|
|
375
|
-
|
|
376
|
-
const
|
|
398
|
+
// 🟢 NEU: fehlende Zeile wieder einfügen
|
|
399
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
400
|
+
|
|
401
|
+
for (const sensor of this.sensors) {
|
|
402
|
+
const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
|
|
403
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
404
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
405
|
+
|
|
406
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
407
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
408
|
+
|
|
409
|
+
if (!isActive) {
|
|
410
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
411
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
412
|
+
ack: true,
|
|
413
|
+
});
|
|
414
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
415
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
416
|
+
ack: true,
|
|
417
|
+
});
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const stateList = [
|
|
422
|
+
'temp_min',
|
|
423
|
+
'temp_max',
|
|
424
|
+
'temp_min_time',
|
|
425
|
+
'temp_max_time',
|
|
426
|
+
'temp_avg',
|
|
427
|
+
'data_points_count',
|
|
428
|
+
'last_update',
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
for (const state of stateList) {
|
|
432
|
+
const fullPath = `${basePath}.${state}`;
|
|
433
|
+
let defValue = null;
|
|
434
|
+
if (state.includes('time')) {
|
|
435
|
+
defValue = '';
|
|
436
|
+
}
|
|
437
|
+
if (state === 'data_points_count') {
|
|
438
|
+
defValue = 0;
|
|
439
|
+
}
|
|
440
|
+
if (state === 'last_update') {
|
|
441
|
+
defValue = resetDate;
|
|
442
|
+
}
|
|
443
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
444
|
+
}
|
|
377
445
|
|
|
378
|
-
if (!isActive) {
|
|
379
446
|
await adapter.setStateAsync(summaryJsonPath, {
|
|
380
|
-
val: JSON.stringify({ status: '
|
|
447
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Monatswerte zurückgesetzt' }),
|
|
381
448
|
ack: true,
|
|
382
449
|
});
|
|
383
450
|
await adapter.setStateAsync(summaryHtmlPath, {
|
|
384
|
-
val:
|
|
451
|
+
val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
|
|
385
452
|
ack: true,
|
|
386
453
|
});
|
|
387
|
-
continue;
|
|
388
454
|
}
|
|
389
455
|
|
|
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' }),
|
|
456
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
457
|
+
val: '{}',
|
|
417
458
|
ack: true,
|
|
418
459
|
});
|
|
419
|
-
await adapter.setStateAsync(
|
|
420
|
-
val:
|
|
460
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
461
|
+
val: '',
|
|
421
462
|
ack: true,
|
|
422
463
|
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
426
|
-
val: '{}',
|
|
427
|
-
ack: true,
|
|
428
|
-
});
|
|
429
|
-
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
430
|
-
val: '',
|
|
431
|
-
ack: true,
|
|
432
|
-
});
|
|
433
464
|
|
|
434
|
-
|
|
465
|
+
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
466
|
+
} catch (err) {
|
|
467
|
+
adapter.log.warn(`statisticsHelperMonth: Fehler beim Monatsreset: ${err.message}`);
|
|
468
|
+
} finally {
|
|
469
|
+
this.isResetting = false; // 🟢 NEU: Flag wieder freigeben
|
|
470
|
+
}
|
|
435
471
|
},
|
|
436
472
|
|
|
437
473
|
/**
|
|
@@ -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,9 +369,27 @@ 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
|
-
await this._scheduleWeekReset();
|
|
392
|
+
// await this._scheduleWeekReset();
|
|
374
393
|
}, msUntilReset);
|
|
375
394
|
|
|
376
395
|
adapter.log.debug(`statisticsHelperWeek: Wochen-Reset geplant in ${Math.round(msUntilReset / 60000)} Minuten.`);
|
|
@@ -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
|
-
}
|
|
407
|
-
|
|
408
|
-
const stateList = [
|
|
409
|
-
'temp_min',
|
|
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
468
|
}
|
|
432
469
|
|
|
433
|
-
await adapter.setStateAsync(
|
|
434
|
-
val:
|
|
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
|
-
|
|
443
|
-
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
444
|
-
val: '{}',
|
|
445
|
-
ack: true,
|
|
446
|
-
});
|
|
447
|
-
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
448
|
-
val: '',
|
|
449
|
-
ack: true,
|
|
450
|
-
});
|
|
451
478
|
|
|
452
|
-
|
|
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.5",
|
|
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",
|