iobroker.poolcontrol 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -121,6 +121,30 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
121
121
  ## Changelog
122
122
  ### **WORK IN PROGRESS**
123
123
 
124
+ ### **0.4.0 (26.10.2025)**
125
+
126
+ **Neue Funktionen**
127
+ - Einführung des neuen Statistik-Systems unter `analytics.statistics.temperature.today`
128
+ - Automatische Erfassung von **Min-, Max- und Durchschnittswerten** aller aktiven Temperatursensoren
129
+ - Pro Sensor: JSON- und HTML-Zusammenfassungen mit laufender Aktualisierung
130
+ - Gesamtausgabe aller Sensoren (Tabelle) unter
131
+ `analytics.statistics.temperature.today.outputs.summary_all_html`
132
+ - Vollständig **persistente Datenpunkte** mit Überinstallationsschutz
133
+ - **Automatischer Mitternachts-Reset** zur Tagesrücksetzung inkl. Zeitstempel
134
+ - Vorbereitung für zukünftige Wochen-, Monats- und Saisonstatistiken
135
+
136
+ **Verbesserungen**
137
+ - Einheitliche Struktur durch neuen Hauptordner `analytics`
138
+ - Keine dauerhaften Loops oder Timerbelastungen – reine Eventverarbeitung
139
+ - Verbesserte Performance und Speicherstabilität
140
+ - Überarbeitete Initialisierung aller Statistik-States beim Start
141
+
142
+ **Hinweis**
143
+ Diese Version bildet die stabile Basis für alle folgenden Statistik- und Analysefunktionen
144
+ (z. B. Wochen- und Monatsstatistik, Historien- und Effizienz-Auswertungen).
145
+
146
+ ---
147
+
124
148
  ### 0.3.1 (2025-10-18)
125
149
  - FrostHelper stabilisiert:
126
150
  - Feste Hysterese von +2 °C (bisher +1 °C)
@@ -145,24 +169,6 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
145
169
  > Mit dieser Version beginnt die lernfähige Phase des PoolControl-Adapters:
146
170
  > Deine Pumpe weiß jetzt selbst, was für sie „normal“ ist.
147
171
 
148
- ---
149
-
150
- ### 0.2.2 (2025-10-08)
151
- - Einführung einer **automatischen Rückspülerinnerung** (neuer Bereich im ControlHelper2)
152
- - Erinnerung mit Intervall in Tagen und automatischer Rücksetzung nach erfolgter Rückspülung
153
- - Log- und Sprachausgabe bei Fälligkeit oder Überfälligkeit
154
- - Erweiterung des Control-Bereichs um zusätzliche States (backwash_reminder_active, interval_days, last_date, required)
155
- - Anpassung der main.js und controlStates zur Integration
156
- - Vorbereitung weiterer Wartungs- und Steuerungsfunktionen (Beleuchtung, Roboter, Ventile, Gegenstromanlage, Kesseldruck-Überwachung).
157
-
158
- ---
159
-
160
- ### 0.2.0 (2025-10-06)
161
- - Neuer Diagnosebereich **SystemCheck** für interne Debug-Logs und Analysen.
162
- - Möglichkeit, einzelne Adapterbereiche (z. B. Pumpe, Solar, Temperatur) gezielt zu überwachen.
163
- - Fortlaufendes Textprotokoll mit manueller Löschfunktion.
164
- - Alle bisherigen Debug-Funktionen aus `zz_debuglogs` in `SystemCheck.debug_logs` integriert.
165
- - Vorbereitung für zukünftige Diagnose-Erweiterungen (Export, Systemprüfung, Plausibilitäts-Checks).
166
172
 
167
173
  *(ältere Versionen siehe [io-package.json](./io-package.json))*
168
174
 
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "news": {
6
+ "0.4.0": {
7
+ "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
+ "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.",
9
+ "ru": "Добавлена ежедневная статистика температуры в analytics.statistics.temperature.today с автоматическим отслеживанием мин./макс./средних значений, JSON и HTML сводками и логикой сброса в полночь.",
10
+ "pt": "Adicionadas estatísticas diárias de temperatura em analytics.statistics.temperature.today com rastreamento automático de mínimo/máximo/média, resumos em JSON e HTML e redefinição automática à meia-noite.",
11
+ "nl": "Dagelijkse temperatuurstatistieken toegevoegd onder analytics.statistics.temperature.today met automatische min/max/gemiddelde tracking, JSON- en HTML-samenvattingen en middernachtreset.",
12
+ "fr": "Ajout de statistiques quotidiennes de température sous analytics.statistics.temperature.today avec suivi automatique min/max/moyenne, résumés JSON et HTML et réinitialisation automatique à minuit.",
13
+ "it": "Aggiunte statistiche giornaliere della temperatura in analytics.statistics.temperature.today con monitoraggio automatico di min/max/media, riepiloghi JSON e HTML e reset automatico a mezzanotte.",
14
+ "es": "Se añadieron estadísticas diarias de temperatura en analytics.statistics.temperature.today con seguimiento automático de mínimos/máximos/promedios, resúmenes en JSON y HTML y reinicio automático a medianoche.",
15
+ "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.",
16
+ "uk": "Додано щоденну статистику температури в analytics.statistics.temperature.today з автоматичним відстеженням мін/макс/середніх значень, JSON і HTML зведеннями та скиданням опівночі.",
17
+ "zh-cn": "在 analytics.statistics.temperature.today 中添加了每日温度统计,具有自动最小/最大/平均跟踪、JSON 和 HTML 摘要以及午夜重置功能。"
18
+ },
6
19
  "0.3.1": {
7
20
  "en": "Frost protection logic stabilized: fixed hysteresis of +2 °C and rounded temperature values to avoid pump switching fluctuations around 3 °C.",
8
21
  "de": "Frostschutz-Logik stabilisiert: feste Hysterese von +2 °C und gerundete Temperaturwerte zur Vermeidung von Pumpenschaltflattern um 3 °C.",
@@ -52,19 +65,6 @@
52
65
  "pl": "Naprawiono problem z niewidzialnymi stanami dla kontroli głosowej i dodano prawidłowe przetwarzanie zmiennych wewnętrznych.",
53
66
  "uk": "Виправлено проблему з невидимими станами для керування голосом та додано правильну обробку внутрішніх змінних.",
54
67
  "zh-cn": "修复了语音控制的隐形状态问题,并添加了正确的内部变量处理。"
55
- },
56
- "0.2.0": {
57
- "en": "New diagnostic area 'SystemCheck' for internal debug logs and future analysis tools.",
58
- "de": "Neuer Diagnosebereich 'SystemCheck' für interne Debug-Logs und künftige Analysefunktionen.",
59
- "ru": "Новая диагностическая область 'SystemCheck' для внутренних журналов отладки и будущих инструментов анализа.",
60
- "pt": "Nova área de diagnóstico 'SystemCheck' para logs de depuração internos e futuras ferramentas de análise.",
61
- "nl": "Nieuw diagnostisch gebied 'SystemCheck' voor interne foutopsporingslogboeken en toekomstige analysetools.",
62
- "fr": "Nouvelle zone de diagnostic 'SystemCheck' pour les journaux de débogage internes et les futurs outils d'analyse.",
63
- "it": "Nuova area diagnostica 'SystemCheck' per i log di debug interni e futuri strumenti di analisi.",
64
- "es": "Nueva área de diagnóstico 'SystemCheck' para registros internos de depuración y futuras herramientas de análisis.",
65
- "pl": "Nowy obszar diagnostyczny 'SystemCheck' dla wewnętrznych dzienników debugowania i przyszłych narzędzi analitycznych.",
66
- "uk": "Нова діагностична область 'SystemCheck' для внутрішніх журналів налагодження та майбутніх інструментів аналізу.",
67
- "zh-cn": "新的诊断区域“SystemCheck”,用于内部调试日志和未来的分析工具。"
68
68
  }
69
69
  },
70
70
  "titleLang": {
@@ -4,9 +4,9 @@
4
4
  * consumptionHelper
5
5
  * - Nutzt externen kWh-Zähler (objectId aus Config)
6
6
  * - Berechnet Periodenwerte (Tag/Woche/Monat/Jahr)
7
- * - Berechnet Kosten anhand Strompreis (€/kWh) mit additiver Logik über Neustarts
7
+ * - Berechnet Kosten anhand Strompreis (€/kWh)
8
8
  * - Offset-Mechanismus: summiert alte Werte bei Zählerwechsel/Reset auf
9
- * - NEU: Erhält Tages-/Wochen-/Monats-/Jahreswerte über Neustarts (keine Datenpunkte neu!)
9
+ * - Erhält Tages-/Wochen-/Monats-/Jahreswerte über Neustarts
10
10
  */
11
11
 
12
12
  const consumptionHelper = {
@@ -16,7 +16,6 @@ const consumptionHelper = {
16
16
  baselines: {},
17
17
  resetTimer: null,
18
18
 
19
- // interne Speicher für additive Berechnung
20
19
  lastKnownPrice: 0,
21
20
  baseTotalKwh: 0,
22
21
  baseTotalEur: 0,
@@ -25,7 +24,6 @@ const consumptionHelper = {
25
24
  this.adapter = adapter;
26
25
  this.energyId = adapter.config.external_energy_total_id || null;
27
26
 
28
- // Komma/Punkt tolerant interpretieren
29
27
  this.price = parseFloat(String(adapter.config.energy_price_eur_kwh).replace(',', '.')) || 0;
30
28
  this.lastKnownPrice = this.price;
31
29
 
@@ -39,18 +37,16 @@ const consumptionHelper = {
39
37
  }
40
38
 
41
39
  this._scheduleDailyReset();
42
- this._loadCostBaselines(); // Basiswerte laden
43
- this._restoreBaselinesFromStates(); // <-- NEU: Baselines aus bestehenden States wiederherstellen
40
+ this._loadCostBaselines();
41
+ this._restoreBaselinesFromStates();
44
42
  },
45
43
 
46
44
  async _loadCostBaselines() {
47
45
  try {
48
46
  const totalKwh = (await this.adapter.getStateAsync('consumption.total_kwh'))?.val || 0;
49
47
  const totalEur = (await this.adapter.getStateAsync('costs.total_eur'))?.val || 0;
50
-
51
48
  this.baseTotalKwh = totalKwh;
52
49
  this.baseTotalEur = totalEur;
53
-
54
50
  this.adapter.log.debug(
55
51
  `[consumptionHelper] Kosten-Basis geladen → ${this.baseTotalEur.toFixed(
56
52
  2,
@@ -65,12 +61,10 @@ const consumptionHelper = {
65
61
  if (!state || id !== this.energyId) {
66
62
  return;
67
63
  }
68
-
69
64
  const totalNowRaw = Number(state.val);
70
65
  if (!Number.isFinite(totalNowRaw)) {
71
66
  return;
72
67
  }
73
-
74
68
  await this._updateConsumption(totalNowRaw);
75
69
  },
76
70
 
@@ -80,19 +74,25 @@ const consumptionHelper = {
80
74
  const last = (await this.adapter.getStateAsync('consumption.last_total_kwh'))?.val || 0;
81
75
  let totalNow = totalNowRaw;
82
76
 
83
- // Reset-Erkennung
77
+ // FIX: Schutz gegen Überinstallations-Fehler und unplausible Sprünge
84
78
  if (totalNowRaw < last) {
85
- this.adapter.log.warn('[consumptionHelper] Zähler-Reset erkannt Offset wird angepasst');
86
- const newOffset = offset + last;
87
- await this.adapter.setStateAsync('consumption.offset_kwh', { val: newOffset, ack: true });
88
- totalNow = newOffset + totalNowRaw;
79
+ if (offset === 0 && totalNowRaw < 10 && last > 10) {
80
+ this.adapter.log.warn(
81
+ '[consumptionHelper] Überinstallationsschutz aktiv – Zählerstand kleiner, Offset bleibt unverändert.',
82
+ );
83
+ totalNow = last; // kein Offset addieren
84
+ } else {
85
+ this.adapter.log.warn('[consumptionHelper] Zähler-Reset erkannt → Offset wird angepasst');
86
+ const newOffset = offset + last;
87
+ await this.adapter.setStateAsync('consumption.offset_kwh', { val: newOffset, ack: true });
88
+ totalNow = newOffset + totalNowRaw;
89
+ }
89
90
  } else {
90
91
  totalNow = offset + totalNowRaw;
91
92
  }
92
93
 
93
94
  await this.adapter.setStateAsync('consumption.total_kwh', { val: totalNow, ack: true });
94
95
 
95
- // Baselines laden (falls leer)
96
96
  if (Object.keys(this.baselines).length === 0) {
97
97
  await this._loadBaselines(totalNow);
98
98
  }
@@ -112,18 +112,15 @@ const consumptionHelper = {
112
112
  }
113
113
  }
114
114
 
115
- // === Additive Kostenberechnung über Neustarts ===
116
115
  const deltaKwh = Math.max(0, totalNow - this.baseTotalKwh);
117
116
  const deltaEur = deltaKwh * this.price;
118
117
  const totalCost = this.baseTotalEur + deltaEur;
119
118
 
120
- // Tages-/Wochen-/Monats-/Jahreskosten berechnen (nur Momentansicht)
121
119
  const dayCost = values.day * this.price;
122
120
  const weekCost = values.week * this.price;
123
121
  const monthCost = values.month * this.price;
124
122
  const yearCost = values.year * this.price;
125
123
 
126
- // States schreiben
127
124
  await this.adapter.setStateAsync('consumption.day_kwh', { val: Number(values.day.toFixed(3)), ack: true });
128
125
  await this.adapter.setStateAsync('consumption.week_kwh', {
129
126
  val: Number(values.week.toFixed(3)),
@@ -146,17 +143,13 @@ const consumptionHelper = {
146
143
  await this.adapter.setStateAsync('costs.total_eur', { val: Number(totalCost.toFixed(2)), ack: true });
147
144
  }
148
145
 
149
- // Letzten Zählerwert speichern
150
146
  await this.adapter.setStateAsync('consumption.last_total_kwh', { val: totalNowRaw, ack: true });
151
-
152
- // NEU: aktuelle Baselines persistent sichern
153
147
  await this._saveBaselines();
154
148
  } catch (err) {
155
149
  this.adapter.log.warn(`[consumptionHelper] Fehler bei Verbrauchsupdate: ${err.message}`);
156
150
  }
157
151
  },
158
152
 
159
- // Lädt Baselines beim Start, wenn im Speicher leer
160
153
  async _loadBaselines(totalNow) {
161
154
  this.baselines = {};
162
155
  const day = (await this.adapter.getStateAsync('consumption.day_kwh'))?.val;
@@ -172,7 +165,6 @@ const consumptionHelper = {
172
165
  this.adapter.log.debug(`[consumptionHelper] Baselines geladen: ${JSON.stringify(this.baselines)}`);
173
166
  },
174
167
 
175
- // NEU: Baselines aus States rekonstruieren (beim Adapterstart)
176
168
  async _restoreBaselinesFromStates() {
177
169
  try {
178
170
  const totalNow = (await this.adapter.getStateAsync('consumption.total_kwh'))?.val || 0;
@@ -194,7 +186,6 @@ const consumptionHelper = {
194
186
  }
195
187
  },
196
188
 
197
- // NEU: Aktuelle Baselines speichern (persistente Sicherung)
198
189
  async _saveBaselines() {
199
190
  try {
200
191
  await this.adapter.setStateAsync('consumption.day_kwh', {
@@ -218,13 +209,11 @@ const consumptionHelper = {
218
209
  }
219
210
  },
220
211
 
221
- // === NEU: Manueller Gesamreset für Verbrauch & Kosten ====================
222
212
  async resetAll(adapter) {
223
213
  try {
224
214
  this.adapter = adapter;
225
215
  adapter.log.warn('[consumptionHelper] Manueller Reset aller Verbrauchs- und Kostendaten');
226
216
 
227
- // Verbrauchswerte nullen
228
217
  const consumptionKeys = [
229
218
  'day_kwh',
230
219
  'week_kwh',
@@ -238,13 +227,11 @@ const consumptionHelper = {
238
227
  await adapter.setStateAsync(`consumption.${key}`, { val: 0, ack: true });
239
228
  }
240
229
 
241
- // Kostenwerte nullen
242
230
  const costKeys = ['day_eur', 'week_eur', 'month_eur', 'year_eur', 'total_eur'];
243
231
  for (const key of costKeys) {
244
232
  await adapter.setStateAsync(`costs.${key}`, { val: 0, ack: true });
245
233
  }
246
234
 
247
- // interne Speicher ebenfalls zurücksetzen
248
235
  this.baselines = {};
249
236
  this.baseTotalKwh = 0;
250
237
  this.baseTotalEur = 0;
@@ -255,15 +242,23 @@ const consumptionHelper = {
255
242
  }
256
243
  },
257
244
 
245
+ // FIX: täglicher Reset um Mitternacht für Tagesverbrauch
258
246
  _scheduleDailyReset() {
259
247
  const now = new Date();
260
248
  const nextMidnight = new Date(now);
261
249
  nextMidnight.setHours(24, 0, 0, 0);
262
250
  const msUntilMidnight = nextMidnight - now;
263
251
 
264
- this.resetTimer = setTimeout(() => {
265
- this.baselines = {};
266
- this._scheduleDailyReset();
252
+ this.resetTimer = setTimeout(async () => {
253
+ try {
254
+ this.adapter.log.info('[consumptionHelper] Tageszähler-Reset (Mitternacht)');
255
+ await this.adapter.setStateAsync('consumption.day_kwh', { val: 0, ack: true });
256
+ await this.adapter.setStateAsync('costs.day_eur', { val: 0, ack: true });
257
+ this.baselines.day = (await this.adapter.getStateAsync('consumption.total_kwh'))?.val || 0;
258
+ } catch (err) {
259
+ this.adapter.log.warn(`[consumptionHelper] Fehler beim Mitternachtsreset: ${err.message}`);
260
+ }
261
+ this._scheduleDailyReset(); // Timer erneut setzen
267
262
  }, msUntilMidnight);
268
263
  },
269
264
 
@@ -3,8 +3,9 @@
3
3
  /**
4
4
  * frostHelper
5
5
  * - Prüft Außentemperatur gegen Frostschutz-Grenze
6
- * - Schaltet Pumpe bei Frost ein (nur im Modus "auto")
7
- * - Kleine Hysterese: +1°C zum Ausschalten
6
+ * - Frostschutz arbeitet unabhängig vom Pumpenmodus
7
+ * -(Sicherheitsfunktion: greift immer, wenn frost_protection_active = true)
8
+ * - Kleine Hysterese: +2°C zum Ausschalten
8
9
  * - Schaltet über den zentralen Bool-State pump.pump_switch
9
10
  */
10
11
 
@@ -39,12 +40,6 @@ const frostHelper = {
39
40
  return;
40
41
  }
41
42
 
42
- // Nur aktiv im AUTO-Modus
43
- const mode = (await this.adapter.getStateAsync('pump.mode'))?.val;
44
- if (mode !== 'auto') {
45
- return;
46
- }
47
-
48
43
  // Frostschutz aktiviert?
49
44
  const frostActive = (await this.adapter.getStateAsync('pump.frost_protection_active'))?.val;
50
45
  if (!frostActive) {
@@ -255,31 +255,41 @@ const pumpHelper = {
255
255
  let error = false;
256
256
  let errorMsg = '';
257
257
 
258
+ // Pumpe EIN, aber keine Leistung
258
259
  if (active === true && power < 5) {
259
260
  error = true;
260
261
  errorMsg = 'Fehler: Pumpe EIN, aber keine Leistung!';
261
262
  }
262
263
 
264
+ // Pumpe AUS, aber Leistung vorhanden
263
265
  if (active === false && power > 10) {
264
266
  error = true;
265
267
  errorMsg = 'Fehler: Pumpe AUS, aber Leistung vorhanden!';
266
268
  }
267
269
 
268
- if (active === true && maxWatt > 0 && power > maxWatt) {
269
- error = true;
270
- errorMsg = `Überlast: ${power} W > Maximalwert ${maxWatt} W → Pumpe wird abgeschaltet!`;
270
+ // --- Überlastschutz (mit fester 10%-Toleranz) ---
271
+ if (active === true && maxWatt > 0) {
272
+ const overloadTolerance = 1.1; // 10 % Sicherheitsfenster
273
+ const overloadLimit = maxWatt * overloadTolerance;
274
+
275
+ if (power > overloadLimit) {
276
+ error = true;
277
+ errorMsg = `Überlast: ${power.toFixed(1)} W > Sicherheitsgrenze ${overloadLimit.toFixed(
278
+ 1,
279
+ )} W (Maxwert ${maxWatt} W) → Pumpe wird abgeschaltet!`;
280
+
281
+ if (pumpSwitchId) {
282
+ await this.adapter.setForeignStateAsync(pumpSwitchId, {
283
+ val: false,
284
+ ack: false,
285
+ });
286
+ }
271
287
 
272
- if (pumpSwitchId) {
273
- await this.adapter.setForeignStateAsync(pumpSwitchId, {
274
- val: false,
275
- ack: false,
288
+ await this.adapter.setStateAsync('pump.mode', {
289
+ val: 'off',
290
+ ack: true,
276
291
  });
277
292
  }
278
-
279
- await this.adapter.setStateAsync('pump.mode', {
280
- val: 'off',
281
- ack: true,
282
- });
283
293
  }
284
294
 
285
295
  if (error !== errorOld) {
@@ -17,7 +17,7 @@
17
17
  * - pump.live.flow_current_lh
18
18
  * - pump.pump_switch
19
19
  *
20
- * Version: 1.0.0
20
+ * Version: 1.0.2 (zusätzliche Throttle-Logik für Deviation-Werte)
21
21
  */
22
22
 
23
23
  const pumpHelper3 = {
@@ -29,6 +29,14 @@ const pumpHelper3 = {
29
29
  flow: [],
30
30
  },
31
31
 
32
+ // NEU: interne Variablen für Schreibschutz
33
+ _lastLearningWrite: 0,
34
+ _lastLearningStatus: '',
35
+ // NEU: Drosselung für Deviation-Werte
36
+ _lastDevWrite: 0,
37
+ _lastDevPower: 0,
38
+ _lastDevFlow: 0,
39
+
32
40
  /**
33
41
  * Initialisiert den Helper
34
42
  *
@@ -167,6 +175,7 @@ const pumpHelper3 = {
167
175
 
168
176
  /**
169
177
  * Bewertet aktuelle Abweichungen und schreibt Status.
178
+ * (jetzt mit Throttle- und Change-Detection)
170
179
  */
171
180
  async _updateDeviationAndStatus() {
172
181
  try {
@@ -183,18 +192,40 @@ const pumpHelper3 = {
183
192
  const deviationPower = ((currentPower - avgPower) / avgPower) * 100;
184
193
  const deviationFlow = ((currentFlow - avgFlow) / avgFlow) * 100;
185
194
 
186
- await this.adapter.setStateAsync('pump.learning.deviation_power_percent', {
187
- val: Math.round(deviationPower * 10) / 10,
188
- ack: true,
189
- });
190
- await this.adapter.setStateAsync('pump.learning.deviation_flow_percent', {
191
- val: Math.round(deviationFlow * 10) / 10,
192
- ack: true,
193
- });
195
+ // --- NEU: Throttle + Change Detection für Deviation-Werte ---
196
+ const roundedPower = Math.round(deviationPower * 10) / 10;
197
+ const roundedFlow = Math.round(deviationFlow * 10) / 10;
198
+
199
+ if (
200
+ (this._lastDevPower !== roundedPower || this._lastDevFlow !== roundedFlow) &&
201
+ Date.now() - this._lastDevWrite > 1000
202
+ ) {
203
+ this._lastDevPower = roundedPower;
204
+ this._lastDevFlow = roundedFlow;
205
+ this._lastDevWrite = Date.now();
194
206
 
195
- // Bewertung
196
- const statusText = this._getStatusText(deviationPower, deviationFlow);
197
- await this.adapter.setStateAsync('pump.learning.status_text', { val: statusText, ack: true });
207
+ await this.adapter.setStateAsync('pump.learning.deviation_power_percent', {
208
+ val: roundedPower,
209
+ ack: true,
210
+ });
211
+ await this.adapter.setStateAsync('pump.learning.deviation_flow_percent', {
212
+ val: roundedFlow,
213
+ ack: true,
214
+ });
215
+ }
216
+
217
+ // Bewertungstext ermitteln
218
+ const statusText = await this._getStatusText(deviationPower, deviationFlow);
219
+
220
+ // --- NEU: Schreibsperre + Change-Detection für Status ---
221
+ if (statusText !== this._lastLearningStatus && Date.now() - this._lastLearningWrite > 1000) {
222
+ this._lastLearningStatus = statusText;
223
+ this._lastLearningWrite = Date.now();
224
+ await this.adapter.setStateAsync('pump.learning.status_text', {
225
+ val: statusText,
226
+ ack: true,
227
+ });
228
+ }
198
229
  } catch (err) {
199
230
  this.adapter.log.warn(`[pumpHelper3] Fehler bei _updateDeviationAndStatus: ${err.message}`);
200
231
  }
@@ -206,17 +237,21 @@ const pumpHelper3 = {
206
237
  * @param devPower - Aktuelle Abweichung der Leistung (W)
207
238
  * @param devFlow - Aktuelle Abweichung des Durchflusses (L/H)
208
239
  */
209
- _getStatusText(devPower, devFlow) {
240
+ async _getStatusText(devPower, devFlow) {
210
241
  const absPower = Math.abs(devPower);
211
242
  const absFlow = Math.abs(devFlow);
212
243
 
213
- if (absPower <= 15 && absFlow <= 15) {
244
+ // NEU: Dynamischen Toleranzwert lesen (Standard 15%)
245
+ const toleranceState = await this.adapter.getStateAsync('pump.learning.tolerance_percent');
246
+ const tolerance = Number(toleranceState?.val) || 15;
247
+
248
+ if (absPower <= tolerance && absFlow <= tolerance) {
214
249
  return 'Pumpe läuft im Normalbereich';
215
250
  }
216
- if (devPower < -15 || devFlow < -15) {
251
+ if (devPower < -tolerance || devFlow < -tolerance) {
217
252
  return 'Pumpe läuft unterhalb des Normalbereichs (möglicher Filterdruck)';
218
253
  }
219
- if (devPower > 15 || devFlow > 15) {
254
+ if (devPower > tolerance || devFlow > tolerance) {
220
255
  return 'Pumpe läuft oberhalb des Normalbereichs (möglicher Luftstau)';
221
256
  }
222
257
  return 'Pumpe außerhalb des bekannten Bereichs';
@@ -94,38 +94,72 @@ const runtimeHelper = {
94
94
  return;
95
95
  }
96
96
 
97
+ // FIX: Robuste Start-/Stop-Logik für pump.pump_switch
97
98
  if (id.endsWith('pump.pump_switch')) {
98
- if (state.val && !this.isRunning) {
99
- // Pumpe startet
100
- this.isRunning = true;
101
- this.lastOn = Date.now();
102
- this.startCountToday += 1;
103
-
104
- // Live-Timer starten (jede Minute)
105
- this._startLiveTimer();
106
-
107
- // Start sofort in State schreiben
108
- await this.adapter.setStateAsync('runtime.start_count_today', { val: this.startCountToday, ack: true });
109
- } else if (!state.val && this.isRunning) {
110
- // Pumpe stoppt
111
- const delta = Math.floor((Date.now() - this.lastOn) / 1000);
112
- this.runtimeToday += delta;
113
- this.runtimeTotal += delta;
114
-
115
- // Saisonlaufzeit nur zählen, wenn aktiv
116
- const seasonActive = !!(await this.adapter.getStateAsync('control.season.active'))?.val;
117
- if (seasonActive) {
118
- this.runtimeSeason += delta;
99
+ if (state.val) {
100
+ // FIX: Immer starten, wenn Pumpe an ist (egal welcher Helper)
101
+ if (!this.isRunning || !this.lastOn) {
102
+ this.isRunning = true;
103
+ this.lastOn = Date.now();
104
+ this.startCountToday += 1;
105
+
106
+ // Live-Timer starten (jede Minute)
107
+ this._startLiveTimer();
108
+
109
+ // Start sofort in State schreiben
110
+ await this.adapter.setStateAsync('runtime.start_count_today', {
111
+ val: this.startCountToday,
112
+ ack: true,
113
+ });
114
+
115
+ // ------------------------------------------------------
116
+ // Statuswerte bei Pumpenstart setzen
117
+ // ------------------------------------------------------
118
+ const nowStr = new Date().toLocaleString();
119
+ await this.adapter.setStateAsync('status.pump_last_start', { val: nowStr, ack: true });
120
+ await this.adapter.setStateAsync('status.pump_today_count', {
121
+ val: this.startCountToday,
122
+ ack: true,
123
+ });
124
+ await this.adapter.setStateAsync('status.pump_was_on_today', { val: true, ack: true });
125
+ // ------------------------------------------------------
126
+ this.adapter.log.debug('[runtimeHelper] Pumpenlaufzeit gestartet.');
127
+ }
128
+ } else {
129
+ // FIX: Immer sauber stoppen, wenn Pumpe aus ist
130
+ if (this.isRunning && this.lastOn) {
131
+ const delta = Math.floor((Date.now() - this.lastOn) / 1000);
132
+ this.runtimeToday += delta;
133
+ this.runtimeTotal += delta;
134
+
135
+ // Saisonlaufzeit nur zählen, wenn aktiv
136
+ const seasonActive = !!(await this.adapter.getStateAsync('control.season.active'))?.val;
137
+ if (seasonActive) {
138
+ this.runtimeSeason += delta;
139
+ }
140
+
141
+ this.isRunning = false;
142
+ this.lastOn = null;
143
+
144
+ // Live-Timer stoppen
145
+ this._stopLiveTimer();
146
+
147
+ // States final aktualisieren
148
+ await this._updateStates();
149
+
150
+ // ------------------------------------------------------
151
+ // Statuswert bei Pumpenstopp setzen
152
+ // ------------------------------------------------------
153
+ const nowStr = new Date().toLocaleString();
154
+ await this.adapter.setStateAsync('status.pump_last_stop', { val: nowStr, ack: true });
155
+ // ------------------------------------------------------
156
+ this.adapter.log.debug('[runtimeHelper] Pumpenlaufzeit gestoppt.');
157
+ } else {
158
+ // FIX: Falls Pumpe aus, aber kein aktiver Lauf (z. B. Neustart) → nur Timer sicher stoppen
159
+ this._stopLiveTimer();
160
+ this.isRunning = false;
161
+ this.lastOn = null;
119
162
  }
120
-
121
- this.isRunning = false;
122
- this.lastOn = null;
123
-
124
- // Live-Timer stoppen
125
- this._stopLiveTimer();
126
-
127
- // States final aktualisieren
128
- await this._updateStates();
129
163
  }
130
164
  }
131
165
  },
@@ -263,6 +297,13 @@ const runtimeHelper = {
263
297
  // 👉 daily_remaining neue berechnen auf Grundlage von daily_required
264
298
  await this.adapter.setStateAsync('circulation.daily_remaining', { val: dailyRequired, ack: true });
265
299
 
300
+ // ------------------------------------------------------
301
+ // NEU: Pumpenstatuswerte um Mitternacht zurücksetzen
302
+ // ------------------------------------------------------
303
+ await this.adapter.setStateAsync('status.pump_today_count', { val: 0, ack: true });
304
+ await this.adapter.setStateAsync('status.pump_was_on_today', { val: false, ack: true });
305
+ // ------------------------------------------------------
306
+
266
307
  // Nächsten Reset planen
267
308
  this._scheduleDailyReset();
268
309
 
@@ -274,8 +315,8 @@ const runtimeHelper = {
274
315
  if (this.liveTimer) {
275
316
  clearInterval(this.liveTimer);
276
317
  }
277
- this.liveTimer = setInterval(() => this._updateStates(), 60 * 1000);
278
- this.adapter.log.debug('[runtimeHelper] Live-Timer gestartet (Updates jede Minute)');
318
+ this.liveTimer = setInterval(() => this._updateStates(), 10 * 1000);
319
+ this.adapter.log.debug('[runtimeHelper] Live-Timer gestartet (Updates alle 10 Sekunden)');
279
320
  },
280
321
 
281
322
  _stopLiveTimer() {