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 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
 
@@ -851,8 +851,7 @@
851
851
  },
852
852
  "speech_telegram_placeholder": {
853
853
  "type": "staticText",
854
- "label": "",
855
- "text": "",
854
+ "text": " ",
856
855
  "xs": 12,
857
856
  "sm": 3,
858
857
  "md": 3,
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.5.3",
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
- * Verarbeitung einer Temperaturänderung für einen Sensor.
188
- * Aktualisiert Min-, Max- und Durchschnittswerte sowie die Zusammenfassungen.
250
+ * Verarbeitet Temperaturänderungen und aktualisiert Statistikdaten.
189
251
  *
190
- * @param {string} sensorId - Die ID des betroffenen Sensors (z. B. "outside" oder "flow").
191
- * @param {number} newValue - Der neue gemessene Temperaturwert in °C.
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 (erweiterte Version)
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
- * Stiller Überinstallationsschutz:
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
- const msUntilReset = nextReset.getTime() - now.getTime();
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
- const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
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
- for (const sensor of this.sensors) {
371
- const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
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
- const summaryJsonPath = `${basePath}.summary_json`;
376
- const summaryHtmlPath = `${basePath}.summary_html`;
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: 'kein Sensor aktiv' }),
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: '<div style="color:gray;">kein Sensor aktiv</div>',
451
+ val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
385
452
  ack: true,
386
453
  });
387
- continue;
388
454
  }
389
455
 
390
- const stateList = [
391
- 'temp_min',
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(summaryHtmlPath, {
420
- val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
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
- adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
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
- const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
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
- for (const sensor of this.sensors) {
389
- const basePath = `analytics.statistics.temperature.week.${sensor.id}`;
390
- const activeState = `temperature.${sensor.id}.active`;
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 summaryJsonPath = `${basePath}.summary_json`;
394
- const summaryHtmlPath = `${basePath}.summary_html`;
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: 'kein Sensor aktiv' }),
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: '<div style="color:gray;">kein Sensor aktiv</div>',
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(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(summaryHtmlPath, {
438
- val: `<div style="color:gray;">Wochenwerte zurückgesetzt (${resetDate})</div>`,
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
- 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",
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",