iobroker.poolcontrol 0.4.0 → 0.5.1

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,14 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
121
121
  ## Changelog
122
122
  ### **WORK IN PROGRESS**
123
123
 
124
+ ## v0.5.0 (2025-10-28)
125
+ - Erweiterung der Temperaturstatistik um Wochen- und Monatsauswertung
126
+ (`analytics.statistics.temperature.week` / `.month`)
127
+ - Eigenständige, eventbasierte Helper für Woche und Monat
128
+ - Persistente Datenpunkte mit automatischen JSON- und HTML-Zusammenfassungen
129
+ - Vorbereitung für zukünftige Erweiterungen (Saison- und Jahresstatistik)
130
+
131
+
124
132
  ### **0.4.0 (26.10.2025)**
125
133
 
126
134
  **Neue Funktionen**
package/io-package.json CHANGED
@@ -1,8 +1,33 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "news": {
6
+ "0.5.1": {
7
+ "en": "Extended week and month statistics with persistent data, unified JSON format, and installation protection.",
8
+ "de": "Erweiterte Wochen- und Monatsstatistik mit persistenten Daten, einheitlichem JSON-Format und Überinstallationsschutz.",
9
+ "ru": "Расширенная недельная и месячная статистика с постоянными данными, унифицированным форматом JSON и защитой при переустановке.",
10
+ "pt": "Estatísticas semanais e mensais expandidas com dados persistentes, formato JSON unificado e proteção de reinstalação.",
11
+ "nl": "Uitgebreide week- en maandstatistieken met persistente gegevens, uniforme JSON-indeling en installatiebescherming.",
12
+ "fr": "Statistiques hebdomadaires et mensuelles étendues avec données persistantes, format JSON unifié et protection à la réinstallation.",
13
+ "it": "Statistiche settimanali e mensili estese con dati persistenti, formato JSON unificato e protezione dall'installazione.",
14
+ "es": "Estadísticas semanales y mensuales ampliadas con datos persistentes, formato JSON unificado y protección de reinstalación.",
15
+ "pl": "Rozszerzone statystyki tygodniowe i miesięczne z trwałymi danymi, ujednoliconym formatem JSON i ochroną przed ponowną instalacją.",
16
+ "uk": "Розширена тижнева та місячна статистика з постійними даними, уніфікованим форматом JSON і захистом під час перевстановлення.",
17
+ "zh-cn": "扩展的周和月统计,具有持久数据、统一的 JSON 格式和重新安装保护。"
18
+ },
19
+ "0.5.0": {
20
+ "en": "Added weekly and monthly temperature statistics under analytics.statistics.temperature.week and analytics.statistics.temperature.month with automatic summaries, independent helpers and persistent data points.",
21
+ "de": "Wöchentliche und monatliche Temperaturstatistiken unter analytics.statistics.temperature.week und analytics.statistics.temperature.month hinzugefügt – mit automatischen Zusammenfassungen, unabhängigen Helpern und persistenten Datenpunkten.",
22
+ "ru": "Добавлена еженедельная и ежемесячная статистика температуры в analytics.statistics.temperature.week и analytics.statistics.temperature.month с автоматическими сводками, независимыми помощниками и постоянными точками данных.",
23
+ "fr": "Ajout des statistiques de température hebdomadaires et mensuelles sous analytics.statistics.temperature.week et analytics.statistics.temperature.month avec résumés automatiques, helpers indépendants et points de données persistants.",
24
+ "it": "Aggiunte statistiche settimanali e mensili della temperatura in analytics.statistics.temperature.week e analytics.statistics.temperature.month con riepiloghi automatici, helper indipendenti e punti dati persistenti.",
25
+ "es": "Se añadieron estadísticas semanales y mensuales de temperatura en analytics.statistics.temperature.week y analytics.statistics.temperature.month con resúmenes automáticos, ayudantes independientes y puntos de datos persistentes.",
26
+ "nl": "Wekelijkse en maandelijkse temperatuurstatistieken toegevoegd onder analytics.statistics.temperature.week en analytics.statistics.temperature.month met automatische samenvattingen, onafhankelijke helpers en persistente gegevenspunten.",
27
+ "pl": "Dodano tygodniowe i miesięczne statystyki temperatury w analytics.statistics.temperature.week i analytics.statistics.temperature.month z automatycznymi podsumowaniami, niezależnymi pomocnikami i trwałymi punktami danych.",
28
+ "uk": "Додано щотижневу та щомісячну статистику температури в analytics.statistics.temperature.week та analytics.statistics.temperature.month з автоматичними зведеннями, незалежними помічниками та постійними точками даних.",
29
+ "zh-cn": "在 analytics.statistics.temperature.week 和 analytics.statistics.temperature.month 中添加了每周和每月温度统计,具有自动摘要、独立助手和持久数据点。"
30
+ },
6
31
  "0.4.0": {
7
32
  "en": "Added daily temperature statistics under analytics.statistics.temperature.today with automatic min/max/average tracking, JSON and HTML summaries, and midnight reset logic.",
8
33
  "de": "Tägliche Temperaturstatistik unter analytics.statistics.temperature.today hinzugefügt mit automatischer Erfassung von Min-/Max-/Durchschnittswerten, JSON- und HTML-Zusammenfassungen sowie Mitternachts-Reset.",
@@ -40,31 +65,6 @@
40
65
  "es": "Se añadió el cálculo del caudal real de la bomba, la monitorización en vivo y un sistema de rango normal autoaprendente para el análisis de la bomba.",
41
66
  "pl": "Dodano obliczanie rzeczywistego przepływu pompy, monitorowanie na żywo i samouczący się system normalnego zakresu do analizy pompy.",
42
67
  "zh-cn": "添加了实际泵流量计算、实时监控以及用于泵分析的自学习正常范围系统。"
43
- },
44
- "0.2.2": {
45
- "en": "Added automatic backwash reminder with speech and log notifications.",
46
- "de": "Automatische Rückspülerinnerung mit Sprach- und Log-Benachrichtigung hinzugefügt.",
47
- "ru": "Добавлено автоматическое напоминание о обратной промывке с голосовыми и журнальными уведомлениями.",
48
- "pt": "Adicionado lembrete automático de retrolavagem com notificações de voz e registro.",
49
- "nl": "Automatische terugspoelherinnering toegevoegd met spraak- en logmeldingen.",
50
- "fr": "Ajout d’un rappel automatique de contre-lavage avec notifications vocales et journaux.",
51
- "it": "Aggiunto promemoria automatico di controlavaggio con notifiche vocali e log.",
52
- "es": "Recordatorio automático de contralavado añadido con notificaciones de voz y registro.",
53
- "pl": "Dodano automatyczne przypomnienie o płukaniu wstecznym z powiadomieniami głosowymi i dziennikiem.",
54
- "zh-cn": "添加了带语音和日志通知的自动反冲洗提醒。"
55
- },
56
- "0.2.1": {
57
- "en": "Fixed the issue with invisible states for speech control and added proper internal variable handling.",
58
- "de": "Behebung des Problems mit unsichtbaren States für die Sprachsteuerung und hinzugefügte korrekte Handhabung interner Variablen.",
59
- "ru": "Исправлена проблема с невидимыми состояниями для управления речью и добавлена правильная обработка внутренних переменных.",
60
- "pt": "Corrigido o problema com estados invisíveis para controle de fala e adicionada a manipulação adequada de variáveis internas.",
61
- "nl": "Het probleem met onzichtbare toestanden voor spraakbesturing is opgelost en de juiste verwerking van interne variabelen toegevoegd.",
62
- "fr": "Correction du problème des états invisibles pour le contrôle vocal et ajout d'un traitement correct des variables internes.",
63
- "it": "Risolto il problema con gli stati invisibili per il controllo vocale e aggiunto un corretto trattamento delle variabili interne.",
64
- "es": "Se solucionó el problema con los estados invisibles para el control de voz y se añadió el manejo adecuado de las variables internas.",
65
- "pl": "Naprawiono problem z niewidzialnymi stanami dla kontroli głosowej i dodano prawidłowe przetwarzanie zmiennych wewnętrznych.",
66
- "uk": "Виправлено проблему з невидимими станами для керування голосом та додано правильну обробку внутрішніх змінних.",
67
- "zh-cn": "修复了语音控制的隐形状态问题,并添加了正确的内部变量处理。"
68
68
  }
69
69
  },
70
70
  "titleLang": {
@@ -118,6 +118,13 @@ const pumpHelper = {
118
118
  if (this.deviceId && id === this.deviceId) {
119
119
  const val = !!state.val;
120
120
  const current = (await this.adapter.getStateAsync('pump.pump_switch'))?.val;
121
+
122
+ // NEU: Filter gegen zyklische Fremdmeldungen ohne echten Zustandswechsel
123
+ if (current === val) {
124
+ this.adapter.log.debug('[pumpHelper] Fremd-State meldet identischen Wert – ignoriert.');
125
+ return;
126
+ }
127
+
121
128
  if (current !== val) {
122
129
  await this.adapter.setStateAsync('pump.pump_switch', { val, ack: true });
123
130
  await this._updateStatus();
@@ -33,6 +33,14 @@ const statisticsHelper = {
33
33
  this.adapter = adapter;
34
34
  adapter.log.debug('statisticsHelper: Initialisierung gestartet.');
35
35
 
36
+ // --- Überinstallationsschutz ---
37
+ try {
38
+ // Prüft, ob alle States vorhanden sind, und legt fehlende still neu an
39
+ await this._verifyStructure();
40
+ } catch {
41
+ // keine Log-Ausgabe – stiller Schutz
42
+ }
43
+
36
44
  try {
37
45
  await this._createTemperatureStatistics();
38
46
  await this._subscribeActiveSensors();
@@ -64,14 +72,9 @@ const statisticsHelper = {
64
72
  const summaryHtmlPath = `${basePath}.summary_html`;
65
73
 
66
74
  if (!isActive) {
67
- await adapter.setStateAsync(summaryJsonPath, {
68
- val: JSON.stringify({ status: 'kein Sensor aktiv' }),
69
- ack: true,
70
- });
71
- await adapter.setStateAsync(summaryHtmlPath, {
72
- val: '<div style="color:gray;">kein Sensor aktiv</div>',
73
- ack: true,
74
- });
75
+ // Leere JSONs vorbereiten, damit spätere Temperaturwerte sie befüllen können
76
+ await adapter.setStateAsync(summaryJsonPath, { val: '[]', ack: true });
77
+ await adapter.setStateAsync(summaryHtmlPath, { val: '', ack: true });
75
78
  continue;
76
79
  }
77
80
 
@@ -196,6 +199,9 @@ const statisticsHelper = {
196
199
  return;
197
200
  }
198
201
 
202
+ // NEU: Rundung auf 1 Nachkommastelle
203
+ newValue = Math.round(newValue * 10) / 10;
204
+
199
205
  const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
200
206
  const tempMax = (await adapter.getStateAsync(`${basePath}.temp_max`))?.val;
201
207
  const tempAvg = (await adapter.getStateAsync(`${basePath}.temp_avg`))?.val;
@@ -218,22 +224,33 @@ const statisticsHelper = {
218
224
  const newCount = count + 1;
219
225
  newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
220
226
 
227
+ // NEU: Alle Temperaturwerte auf 1 Nachkommastelle runden
228
+ newMin = Math.round(newMin * 10) / 10;
229
+ newMax = Math.round(newMax * 10) / 10;
230
+ newAvg = Math.round(newAvg * 10) / 10;
231
+
221
232
  await adapter.setStateAsync(`${basePath}.temp_min`, { val: newMin, ack: true });
222
233
  await adapter.setStateAsync(`${basePath}.temp_max`, { val: newMax, ack: true });
223
234
  await adapter.setStateAsync(`${basePath}.temp_avg`, { val: Math.round(newAvg * 100) / 100, ack: true });
224
235
  await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
225
236
  await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
226
237
 
227
- // Summary aktualisieren
238
+ // Summary aktualisieren – erweitert um Datum, Zeitpunkte, Messanzahl, Name
228
239
  const summary = {
240
+ name: 'Tagesstatistik',
241
+ date: new Date().toISOString().slice(0, 10),
229
242
  temp_min: newMin,
243
+ temp_min_time: (await adapter.getStateAsync(`${basePath}.temp_min_time`))?.val || '',
230
244
  temp_max: newMax,
231
- temp_avg: Math.round(newAvg * 100) / 100,
245
+ temp_max_time: (await adapter.getStateAsync(`${basePath}.temp_max_time`))?.val || '',
246
+ temp_avg: newAvg,
247
+ data_points_count: newCount,
232
248
  updated: now,
233
249
  };
234
250
  await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
235
251
  await adapter.setStateAsync(`${basePath}.summary_html`, {
236
- val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${Math.round(newAvg * 100) / 100} °C</div>`,
252
+ // NEU: HTML-Ausgabe mit gerundeten Werten
253
+ val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${newAvg} °C</div>`,
237
254
  ack: true,
238
255
  });
239
256
 
@@ -241,45 +258,110 @@ const statisticsHelper = {
241
258
  },
242
259
 
243
260
  /**
244
- * Gesamt-HTML/JSON-Ausgabe aktualisieren
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.
245
264
  */
246
265
  async _updateOverallSummary() {
247
266
  const adapter = this.adapter;
248
267
  const allData = [];
249
268
 
250
- for (const sensor of this.sensors) {
251
- const active = (await adapter.getStateAsync(`temperature.${sensor.id}.active`))?.val === true;
252
- if (!active) {
253
- allData.push({ name: sensor.name, status: 'kein Sensor aktiv' });
254
- continue;
255
- }
269
+ try {
270
+ // Alle Sensoren durchlaufen
271
+ for (const sensor of this.sensors) {
272
+ const summaryState = await adapter.getStateAsync(
273
+ `analytics.statistics.temperature.today.${sensor.id}.summary_json`,
274
+ );
256
275
 
257
- const min = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_min`))
258
- ?.val;
259
- const max = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_max`))
260
- ?.val;
261
- const avg = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_avg`))
262
- ?.val;
276
+ if (!summaryState || !summaryState.val) {
277
+ continue;
278
+ }
263
279
 
264
- allData.push({ name: sensor.name, min, max, avg });
265
- }
280
+ // JSON-Inhalt parsen
281
+ let parsed;
282
+ try {
283
+ parsed = JSON.parse(summaryState.val);
284
+ } catch {
285
+ continue;
286
+ }
266
287
 
267
- await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
268
- val: JSON.stringify(allData),
269
- ack: true,
270
- });
288
+ const min = parsed.temp_min;
289
+ const max = parsed.temp_max;
290
+ const avg = parsed.temp_avg;
291
+ const date = parsed.date || '';
292
+ const minTime = parsed.temp_min_time || '';
293
+ const maxTime = parsed.temp_max_time || '';
294
+ const count = parsed.data_points_count || 0;
295
+
296
+ // Falls gar keine Werte vorhanden, überspringen
297
+ if (min == null && max == null && avg == null) {
298
+ continue;
299
+ }
271
300
 
272
- let html = '<table style="width:100%;border-collapse:collapse;">';
273
- html += '<tr><th style="text-align:left;">Sensor</th><th>Min</th><th>Max</th><th>Ø</th></tr>';
274
- for (const entry of allData) {
275
- html += `<tr><td>${entry.name}</td><td>${entry.min ?? '-'}</td><td>${entry.max ?? '-'}</td><td>${entry.avg ?? '-'}</td></tr>`;
276
- }
277
- html += '</table>';
301
+ // Werte runden
302
+ const rMin = typeof min === 'number' ? Math.round(min * 10) / 10 : min;
303
+ const rMax = typeof max === 'number' ? Math.round(max * 10) / 10 : max;
304
+ const rAvg = typeof avg === 'number' ? Math.round(avg * 10) / 10 : avg;
305
+
306
+ // Erweiterte Datensammlung
307
+ allData.push({
308
+ name: sensor.name,
309
+ date,
310
+ min: rMin,
311
+ min_time: minTime,
312
+ max: rMax,
313
+ max_time: maxTime,
314
+ avg: rAvg,
315
+ data_points_count: count,
316
+ });
317
+ }
278
318
 
279
- await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
280
- val: html,
281
- ack: true,
282
- });
319
+ // Wenn noch keine Daten vorliegen → leer lassen
320
+ if (allData.length === 0) {
321
+ await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
322
+ val: '[]',
323
+ ack: true,
324
+ });
325
+ await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
326
+ val: '',
327
+ ack: true,
328
+ });
329
+ return;
330
+ }
331
+
332
+ // JSON-Ausgabe erstellen (komplett erweitert)
333
+ const jsonOutput = JSON.stringify(allData, null, 2);
334
+
335
+ // HTML-Ausgabe erstellen (mit Datum & Anzahl Messwerte)
336
+ let html = '<table style="width:100%;border-collapse:collapse;">';
337
+ html +=
338
+ '<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>';
339
+ for (const entry of allData) {
340
+ html += `<tr>
341
+ <td>${entry.name}</td>
342
+ <td>${entry.date || '-'}</td>
343
+ <td>${entry.min ?? '-'}</td>
344
+ <td>${entry.min_time || '-'}</td>
345
+ <td>${entry.max ?? '-'}</td>
346
+ <td>${entry.max_time || '-'}</td>
347
+ <td>${entry.avg ?? '-'}</td>
348
+ <td>${entry.data_points_count ?? '-'}</td>
349
+ </tr>`;
350
+ }
351
+ html += '</table>';
352
+
353
+ // States setzen (auch bei gleichen Werten → Timestamp aktualisieren)
354
+ await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
355
+ val: jsonOutput,
356
+ ack: true,
357
+ });
358
+ await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
359
+ val: html,
360
+ ack: true,
361
+ });
362
+ } catch {
363
+ // bewusst kein Log hier – stiller Schutz
364
+ }
283
365
  },
284
366
 
285
367
  /**
@@ -382,6 +464,18 @@ const statisticsHelper = {
382
464
  adapter.log.debug('statisticsHelper: Tagesstatistik zurückgesetzt.');
383
465
  },
384
466
 
467
+ /**
468
+ * Stiller Überinstallationsschutz:
469
+ * Prüft und legt fehlende States erneut an, ohne bestehende Werte zu überschreiben.
470
+ */
471
+ async _verifyStructure() {
472
+ try {
473
+ await this._createTemperatureStatistics();
474
+ } catch {
475
+ // bewusst keine Logs – stiller Selbstschutz
476
+ }
477
+ },
478
+
385
479
  cleanup() {
386
480
  if (this.midnightTimer) {
387
481
  clearTimeout(this.midnightTimer);