iobroker.poolcontrol 0.2.2 → 0.3.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,32 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
121
121
  ## Changelog
122
122
  ### **WORK IN PROGRESS**
123
123
 
124
+ ### 0.3.1 (2025-10-18)
125
+ - FrostHelper stabilisiert:
126
+ - Feste Hysterese von +2 °C (bisher +1 °C)
127
+ - Ganzzahl-Rundung eingeführt zur Vermeidung von Schaltflattern um 3 °C
128
+ - Keine Änderungen an States oder Konfiguration erforderlich
129
+
130
+ ---
131
+
132
+ ### 0.3.0 (12.10.2025)
133
+ **Neu:** Intelligentes Pumpen-Monitoring & Lernsystem
134
+
135
+ - Hinzugefügt: **Reelle Durchflussberechnung** auf Basis der tatsächlichen Leistungsaufnahme.
136
+ - Neuer Bereich **`pump.live`** zur Live-Überwachung von Leistung, Durchfluss und prozentualer Auslastung.
137
+ - Die **tägliche Umwälzberechnung** verwendet nun den realen Durchflusswert anstelle eines Fixwerts.
138
+ - Neuer Lernbereich **`pump.learning`**:
139
+ - Lernt automatisch die durchschnittlichen Leistungs- und Durchflusswerte.
140
+ - Bestimmt daraus einen dynamischen **Normalbereich (± 15 %)**.
141
+ - Berechnet prozentuale Abweichungen und erstellt **textbasierte Statusmeldungen**.
142
+ - Alle Lernwerte werden **persistent** gespeichert und bleiben auch nach Neustart erhalten.
143
+ - Vollständig **ereignisgesteuerte Logik** ohne zusätzliche Timer oder Polling-Zyklen.
144
+
145
+ > Mit dieser Version beginnt die lernfähige Phase des PoolControl-Adapters:
146
+ > Deine Pumpe weiß jetzt selbst, was für sie „normal“ ist.
147
+
148
+ ---
149
+
124
150
  ### 0.2.2 (2025-10-08)
125
151
  - Einführung einer **automatischen Rückspülerinnerung** (neuer Bereich im ControlHelper2)
126
152
  - Erinnerung mit Intervall in Tagen und automatischer Rücksetzung nach erfolgter Rückspülung
@@ -64,6 +64,10 @@
64
64
  "lg": 3,
65
65
  "xl": 3,
66
66
  "newLine": true
67
+ },
68
+ "divider_hardware1": {
69
+ "type": "divider",
70
+ "newLine": true
67
71
  }
68
72
  }
69
73
  },
package/io-package.json CHANGED
@@ -1,8 +1,33 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.2.2",
4
+ "version": "0.3.1",
5
5
  "news": {
6
+ "0.3.1": {
7
+ "en": "Frost protection logic stabilized: fixed hysteresis of +2 °C and rounded temperature values to avoid pump switching fluctuations around 3 °C.",
8
+ "de": "Frostschutz-Logik stabilisiert: feste Hysterese von +2 °C und gerundete Temperaturwerte zur Vermeidung von Pumpenschaltflattern um 3 °C.",
9
+ "ru": "Логика защиты от замерзания стабилизирована: фиксированная гистерезис +2 °C и округленные значения температуры для предотвращения колебаний включения насоса около 3 °C.",
10
+ "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.",
11
+ "nl": "Vorstbeschermingslogica gestabiliseerd: vaste hysterese van +2 °C en afgeronde temperatuurwaarden om pompfluctuaties rond 3 °C te voorkomen.",
12
+ "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.",
13
+ "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.",
14
+ "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.",
15
+ "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.",
16
+ "uk": "Стабілізовано логіку захисту від замерзання: фіксована гістерезис +2 °C і округлені значення температури, щоб уникнути коливань увімкнення насоса біля 3 °C.",
17
+ "zh-cn": "防冻逻辑稳定:固定 +2 °C 滞后并四舍五入温度值,以避免泵在 3 °C 附近频繁切换。"
18
+ },
19
+ "0.3.0": {
20
+ "en": "Added real pump flow calculation, live monitoring, and self-learning normal range system for pump analysis.",
21
+ "de": "Reelle Durchflussberechnung, Liveüberwachung und selbstlernendes Normalbereich-System zur Pumpenanalyse hinzugefügt.",
22
+ "ru": "Добавлен расчет реального расхода насоса, живой мониторинг и самообучающаяся система нормальных диапазонов для анализа насоса.",
23
+ "pt": "Adicionada cálculo de fluxo real da bomba, monitoramento ao vivo e sistema autoaprendente de faixa normal para análise da bomba.",
24
+ "nl": "Reële pompdebietberekening, livebewaking en zelflerend normaalbereiksysteem voor pompanalyse toegevoegd.",
25
+ "fr": "Ajout du calcul du débit réel de la pompe, de la surveillance en direct et d’un système de plage normale auto-apprenant pour l’analyse de la pompe.",
26
+ "it": "Aggiunto calcolo del flusso reale della pompa, monitoraggio live e sistema autoapprendente di intervallo normale per l'analisi della pompa.",
27
+ "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.",
28
+ "pl": "Dodano obliczanie rzeczywistego przepływu pompy, monitorowanie na żywo i samouczący się system normalnego zakresu do analizy pompy.",
29
+ "zh-cn": "添加了实际泵流量计算、实时监控以及用于泵分析的自学习正常范围系统。"
30
+ },
6
31
  "0.2.2": {
7
32
  "en": "Added automatic backwash reminder with speech and log notifications.",
8
33
  "de": "Automatische Rückspülerinnerung mit Sprach- und Log-Benachrichtigung hinzugefügt.",
@@ -40,33 +65,7 @@
40
65
  "pl": "Nowy obszar diagnostyczny 'SystemCheck' dla wewnętrznych dzienników debugowania i przyszłych narzędzi analitycznych.",
41
66
  "uk": "Нова діагностична область 'SystemCheck' для внутрішніх журналів налагодження та майбутніх інструментів аналізу.",
42
67
  "zh-cn": "新的诊断区域“SystemCheck”,用于内部调试日志和未来的分析工具。"
43
- },
44
- "0.1.2": {
45
- "en": "Improved: The last known temperature values of all active sensors are now read at adapter start. Sensors that report rarely are now displayed correctly.",
46
- "de": "Verbesserung: Beim Adapterstart werden nun die letzten bekannten Temperaturwerte aller aktiven Sensoren übernommen. Sensoren, die nur selten Werte senden, werden jetzt korrekt angezeigt.",
47
- "ru": "Улучшение: При запуске адаптера теперь считываются последние известные значения температуры всех активных датчиков.",
48
- "pt": "Melhoria: Os últimos valores de temperatura conhecidos de todos os sensores ativos agora são lidos ao iniciar o adaptador.",
49
- "nl": "Verbetering: Bij het starten van de adapter worden nu de laatst bekende temperatuurwaarden van alle actieve sensoren opgehaald.",
50
- "fr": "Amélioration : les dernières valeurs de température connues de tous les capteurs actifs sont désormais lues au démarrage de l’adaptateur.",
51
- "it": "Miglioramento: all'avvio dell'adattatore vengono ora letti gli ultimi valori di temperatura noti di tutti i sensori attivi.",
52
- "es": "Mejora: ahora se leen al iniciar el adaptador los últimos valores de temperatura conocidos de todos los sensores activos.",
53
- "pl": "Ulepszenie: podczas uruchamiania adaptera odczytywane są ostatnie znane wartości temperatury wszystkich aktywnych czujników.",
54
- "uk": "Покращення: під час запуску адаптера тепер зчитуються останні відомі значення температури всіх активних датчиків.",
55
- "zh-cn": "改进:适配器启动时现在会读取所有活动传感器的最后已知温度值。"
56
- },
57
- "0.1.1": {
58
- "en": "Fixed endless loop between pump_switch and deviceId for some smart sockets",
59
- "de": "Endlosschleife zwischen pump_switch und Steckdose (deviceId) behoben",
60
- "ru": "Исправлен бесконечный цикл между pump_switch и deviceId для некоторых умных розеток",
61
- "pt": "Corrigido loop infinito entre pump_switch e deviceId em algumas tomadas inteligentes",
62
- "nl": "Eindeloze lus tussen pump_switch en deviceId voor sommige slimme stopcontacten opgelost",
63
- "fr": "Correction d'une boucle infinie entre pump_switch et deviceId pour certaines prises intelligentes",
64
- "it": "Corretto loop infinito tra pump_switch e deviceId per alcune prese intelligenti",
65
- "es": "Corregido bucle infinito entre pump_switch y deviceId en algunos enchufes inteligentes",
66
- "pl": "Naprawiono nieskończoną pętlę między pump_switch a deviceId dla niektórych inteligentnych gniazdek",
67
- "uk": "Виправлено нескінченний цикл між pump_switch і deviceId для деяких розумних розеток",
68
- "zh-cn": "修复了某些智能插座中 pump_switch 与 deviceId 之间的无限循环"
69
- }
68
+ }
70
69
  },
71
70
  "titleLang": {
72
71
  "en": "PoolControl",
@@ -68,10 +68,15 @@ const frostHelper = {
68
68
  const pumpActive = (await this.adapter.getStateAsync('pump.pump_switch'))?.val;
69
69
  let shouldRun = pumpActive;
70
70
 
71
- // Logik: einschalten bei <= frostTemp, ausschalten bei >= frostTemp+1
72
- if (outside <= frostTemp) {
71
+ // FIX: Stabilere Logik mit fester Hysterese von +2 °C und Ganzzahl-Rundung
72
+ const outsideRounded = Math.round(outside);
73
+ const frostTempRounded = Math.round(frostTemp);
74
+
75
+ // Einschalten bei <= frostTempRounded
76
+ // Ausschalten erst bei >= frostTempRounded + 2 (2 K Hysterese)
77
+ if (outsideRounded <= frostTempRounded) {
73
78
  shouldRun = true;
74
- } else if (outside >= frostTemp + 1) {
79
+ } else if (outsideRounded >= frostTempRounded + 2) {
75
80
  shouldRun = false;
76
81
  }
77
82
 
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * migrationHelper
5
+ * --------------------------------------------------
6
+ * Führt nachträgliche Struktur- oder State-Anpassungen
7
+ * für bestehende Installationen durch.
8
+ *
9
+ * - Wird beim Adapterstart einmalig ausgeführt.
10
+ * - Korrigiert veraltete Definitionen (z. B. Schreibrechte, persist-Flags, etc.)
11
+ *
12
+ * Version: 1.0.1
13
+ */
14
+
15
+ const migrationHelper = {
16
+ adapter: null,
17
+
18
+ /**
19
+ * Initialisiert den Migration-Helper.
20
+ * Wird einmalig beim Adapterstart aufgerufen.
21
+ *
22
+ * @param {ioBroker.Adapter} adapter - Die aktive Adapterinstanz
23
+ */
24
+ async init(adapter) {
25
+ this.adapter = adapter;
26
+ this.adapter.log.info('[migrationHelper] Starte Migration-Check ...');
27
+
28
+ try {
29
+ // ------------------------------------------------------
30
+ // Hier alle Migrationsroutinen nacheinander aufrufen
31
+ // ------------------------------------------------------
32
+ await this._fixSpeechQueue();
33
+ await this._fixSolarWarnActivePersist();
34
+
35
+ // Weitere Routinen folgen hier später:
36
+ // await this._ensurePumpReason();
37
+ // await this._cleanupOldStates();
38
+
39
+ this.adapter.log.debug('[migrationHelper] Migration-Checks abgeschlossen.');
40
+ } catch (err) {
41
+ this.adapter.log.warn(`[migrationHelper] Fehler beim Migration-Check: ${err.message}`);
42
+ }
43
+
44
+ this.adapter.log.info('[migrationHelper] Migration-Helper beendet.');
45
+ },
46
+
47
+ // ------------------------------------------------------
48
+ // Migration: Schreibrecht für speech.queue korrigieren
49
+ // ------------------------------------------------------
50
+
51
+ /**
52
+ * Prüft und korrigiert den State "speech.queue", falls er noch write:false gesetzt hat.
53
+ * Dadurch verschwinden Warnungen beim Schreiben (Read-only state ... written without ack).
54
+ */
55
+ async _fixSpeechQueue() {
56
+ const id = 'speech.queue';
57
+ try {
58
+ const obj = await this.adapter.getObjectAsync(id);
59
+ if (!obj) {
60
+ this.adapter.log.debug(`[migrationHelper] ${id} existiert nicht – keine Anpassung nötig.`);
61
+ return;
62
+ }
63
+
64
+ const isReadOnly = obj.common?.write === false;
65
+ if (isReadOnly) {
66
+ this.adapter.log.info(`[migrationHelper] Aktualisiere Schreibrecht für ${id} → write:true`);
67
+ await this.adapter.extendObjectAsync(id, {
68
+ common: {
69
+ write: true,
70
+ desc: 'Nur intern durch den Adapter beschreibbar (nicht manuell ändern!)',
71
+ },
72
+ });
73
+ } else {
74
+ this.adapter.log.debug(`[migrationHelper] ${id} ist bereits korrekt konfiguriert.`);
75
+ }
76
+ } catch (err) {
77
+ this.adapter.log.warn(`[migrationHelper] Fehler bei Prüfung von ${id}: ${err.message}`);
78
+ }
79
+ },
80
+
81
+ // ------------------------------------------------------
82
+ // Migration: persist-Flag für solar.warn_active ergänzen
83
+ // ------------------------------------------------------
84
+
85
+ /**
86
+ * Ergänzt persist:true bei solar.warn_active,
87
+ * damit die Einstellung (Warnfunktion aktivieren/deaktivieren)
88
+ * nach einem Neustart erhalten bleibt.
89
+ */
90
+ async _fixSolarWarnActivePersist() {
91
+ const id = 'solar.warn_active';
92
+ try {
93
+ const obj = await this.adapter.getObjectAsync(id);
94
+ if (!obj) {
95
+ this.adapter.log.debug(`[migrationHelper] ${id} existiert nicht – keine Anpassung nötig.`);
96
+ return;
97
+ }
98
+
99
+ const hasPersist = obj.common?.persist === true;
100
+ if (!hasPersist) {
101
+ this.adapter.log.info(`[migrationHelper] Ergänze persist:true für ${id}`);
102
+ await this.adapter.extendObjectAsync(id, {
103
+ common: {
104
+ persist: true,
105
+ desc: `${obj.common?.desc || ''} (automatisch per Migration persistiert)`,
106
+ },
107
+ });
108
+ } else {
109
+ this.adapter.log.debug(`[migrationHelper] ${id} ist bereits mit persist:true versehen.`);
110
+ }
111
+ } catch (err) {
112
+ this.adapter.log.warn(`[migrationHelper] Fehler bei Prüfung von ${id}: ${err.message}`);
113
+ }
114
+ },
115
+ };
116
+
117
+ module.exports = migrationHelper;
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pumpHelper2.js
5
+ * ----------------------------------------------------------
6
+ * Ergänzende Berechnungslogik für Pumpen-Livewerte
7
+ * (reeller Durchfluss, Prozentleistung, letzter Durchflusswert).
8
+ *
9
+ * Verwendet vorhandene States:
10
+ * - pump.current_power
11
+ * - pump.pump_max_watt
12
+ * - pump.pump_power_lph
13
+ * - pump.pump_switch
14
+ *
15
+ * Schreibt neue Werte in:
16
+ * - pump.live.current_power_w
17
+ * - pump.live.flow_current_lh
18
+ * - pump.live.flow_percent
19
+ * - pump.live.last_flow_lh
20
+ *
21
+ * Alle Zielstates sind persistent (siehe pumpStates2.js).
22
+ * ----------------------------------------------------------
23
+ * Version: 1.0.3
24
+ */
25
+
26
+ const pumpHelper2 = {
27
+ adapter: null,
28
+ lastKnownFlow: 0, // merkt sich den letzten gültigen Durchflusswert
29
+
30
+ /**
31
+ * Initialisiert den PumpHelper2
32
+ *
33
+ * @param {ioBroker.Adapter} adapter - Aktive ioBroker Adapterinstanz
34
+ */
35
+ async init(adapter) {
36
+ this.adapter = adapter;
37
+ this.adapter.log.info('[pumpHelper2] Initialisierung gestartet');
38
+
39
+ // Relevante States überwachen
40
+ this.adapter.subscribeStates('pump.current_power');
41
+ this.adapter.subscribeStates('pump.pump_switch');
42
+
43
+ // Initialwerte berechnen
44
+ await this._updateLiveValues();
45
+
46
+ this.adapter.log.info('[pumpHelper2] Erfolgreich initialisiert');
47
+ },
48
+
49
+ /**
50
+ * StateChange-Verarbeitung
51
+ *
52
+ * @param {string} id - State-ID
53
+ * @param {ioBroker.State | null | undefined} state - Neuer Statewert
54
+ */
55
+ async handleStateChange(id, state) {
56
+ if (!state || state.ack === false) {
57
+ return;
58
+ }
59
+
60
+ // Leistungsänderung → reelle Durchflusswerte aktualisieren
61
+ if (id.endsWith('pump.current_power')) {
62
+ await this._updateLiveValues();
63
+ }
64
+
65
+ // Pumpenstatus-Änderung → letzten Durchflusswert sichern
66
+ if (id.endsWith('pump.pump_switch')) {
67
+ const pumpOn = state.val === true;
68
+ if (!pumpOn) {
69
+ // FIX: Verwende den zuletzt gemerkten Wert, statt live zu lesen (verhindert 0-Durchfluss)
70
+ const flowBeforeStop = this.lastKnownFlow;
71
+ if (flowBeforeStop > 0) {
72
+ await this._setIfChanged('pump.live.last_flow_lh', flowBeforeStop);
73
+ this.adapter.log.debug(
74
+ `[pumpHelper2] FIX: Letzter Durchflusswert gesichert: ${flowBeforeStop} l/h`,
75
+ );
76
+ } else {
77
+ this.adapter.log.debug('[pumpHelper2] Kein gespeicherter Durchflusswert vorhanden.');
78
+ }
79
+ }
80
+ }
81
+ },
82
+
83
+ /**
84
+ * Führt die Berechnung der Livewerte durch.
85
+ * (Nur wenn gültige Basiswerte vorhanden sind.)
86
+ */
87
+ async _updateLiveValues() {
88
+ try {
89
+ const [currentPower, maxPower, nominalFlow] = await Promise.all([
90
+ this._getNumber('pump.current_power'),
91
+ this._getNumber('pump.pump_max_watt'),
92
+ this._getNumber('pump.pump_power_lph'),
93
+ ]);
94
+
95
+ // Schutz gegen ungültige Werte
96
+ if (maxPower <= 0 || nominalFlow <= 0) {
97
+ this.adapter.log.debug('[pumpHelper2] Ungültige Basiswerte, Berechnung übersprungen');
98
+ return;
99
+ }
100
+
101
+ // Prozentuale Auslastung
102
+ const flowPercent = Math.min(Math.max((currentPower / maxPower) * 100, 0), 100);
103
+
104
+ // Reeller Durchfluss
105
+ const flowCurrentLh = Math.round(nominalFlow * (currentPower / maxPower) * 10) / 10; // 1 Nachkommastelle
106
+
107
+ // Letzten gültigen Wert merken
108
+ if (flowCurrentLh > 0) {
109
+ this.lastKnownFlow = flowCurrentLh;
110
+ }
111
+
112
+ // In States schreiben
113
+ await this._setIfChanged('pump.live.current_power_w', currentPower);
114
+ await this._setIfChanged('pump.live.flow_current_lh', flowCurrentLh);
115
+ await this._setIfChanged('pump.live.flow_percent', flowPercent);
116
+
117
+ this.adapter.log.debug(
118
+ `[pumpHelper2] Reeller Durchfluss aktualisiert: ${flowCurrentLh} l/h (${flowPercent.toFixed(1)}%)`,
119
+ );
120
+ } catch (err) {
121
+ this.adapter.log.warn(`[pumpHelper2] Fehler bei _updateLiveValues: ${err.message}`);
122
+ }
123
+ },
124
+
125
+ /**
126
+ * Liest einen numerischen Statewert (oder 0 bei Fehler).
127
+ *
128
+ * @param {string} id - Objekt-ID des zu lesenden States
129
+ * @returns {Promise<number>} - Aktueller numerischer Wert oder 0
130
+ */
131
+ async _getNumber(id) {
132
+ const state = await this.adapter.getStateAsync(id);
133
+ const val = Number(state?.val);
134
+ return isNaN(val) ? 0 : val;
135
+ },
136
+
137
+ /**
138
+ * Schreibt neuen Wert nur, wenn er sich geändert hat.
139
+ *
140
+ * @param {string} id - Objekt-ID des zu schreibenden States
141
+ * @param {number} newVal - Neuer Wert, des gesetzt werden soll
142
+ */
143
+ async _setIfChanged(id, newVal) {
144
+ const current = await this.adapter.getStateAsync(id);
145
+ if (current && Number(current.val) === newVal) {
146
+ return;
147
+ }
148
+ await this.adapter.setStateAsync(id, { val: newVal, ack: true });
149
+ },
150
+
151
+ /**
152
+ * Cleanup bei Adapter-Unload
153
+ * Wird aktuell nur als Platzhalter verwendet.
154
+ */
155
+ cleanup() {
156
+ // Derzeit keine Timer oder Intervalle vorhanden
157
+ // Platzhalter für zukünftige Erweiterungen
158
+ this.adapter?.log.debug('[pumpHelper2] Cleanup ausgeführt.');
159
+ },
160
+ };
161
+
162
+ module.exports = pumpHelper2;
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pumpHelper3.js
5
+ * ----------------------------------------------------------
6
+ * Lern- und Analysemodul für die Pumpe
7
+ * - Ermittelt Durchschnitts- und Normalwerte aus realen Laufzyklen
8
+ * - Berechnet Abweichungen (Leistung & Durchfluss)
9
+ * - Liefert textbasierte Statusbewertung ("im Normalbereich" etc.)
10
+ * - Keine Sprach- oder Queue-Ausgabe
11
+ *
12
+ * Verwaltet:
13
+ * pump.learning.*
14
+ *
15
+ * Abhängigkeiten:
16
+ * - pump.live.current_power_w
17
+ * - pump.live.flow_current_lh
18
+ * - pump.pump_switch
19
+ *
20
+ * Version: 1.0.0
21
+ */
22
+
23
+ const pumpHelper3 = {
24
+ adapter: null,
25
+
26
+ /** interner Speicher */
27
+ currentSessionValues: {
28
+ power: [],
29
+ flow: [],
30
+ },
31
+
32
+ /**
33
+ * Initialisiert den Helper
34
+ *
35
+ * @param {ioBroker.Adapter} adapter - aktive Adapterinstanz
36
+ */
37
+ async init(adapter) {
38
+ this.adapter = adapter;
39
+ this.adapter.log.info('[pumpHelper3] Initialisierung gestartet');
40
+
41
+ // Relevante States überwachen
42
+ this.adapter.subscribeStates('pump.pump_switch');
43
+ this.adapter.subscribeStates('pump.live.current_power_w');
44
+ this.adapter.subscribeStates('pump.live.flow_current_lh');
45
+
46
+ this.adapter.log.info('[pumpHelper3] Erfolgreich initialisiert');
47
+ },
48
+
49
+ /**
50
+ * Verarbeitet State-Änderungen
51
+ *
52
+ * @param {string} id - State-ID
53
+ * @param {ioBroker.State} state - neuer Statewert
54
+ */
55
+ async handleStateChange(id, state) {
56
+ if (!state) {
57
+ return;
58
+ }
59
+
60
+ try {
61
+ if (id.endsWith('pump.pump_switch')) {
62
+ const isOn = state.val === true;
63
+ if (isOn) {
64
+ // Pumpe startet → neue Sitzung
65
+ this.currentSessionValues.power = [];
66
+ this.currentSessionValues.flow = [];
67
+ this.adapter.log.debug('[pumpHelper3] Neue Lern-Session gestartet');
68
+ } else {
69
+ // Pumpe stoppt → Lernwerte aktualisieren
70
+ await this._finalizeLearningCycle();
71
+ }
72
+ }
73
+
74
+ // Wenn Pumpe läuft, aktuelle Werte sammeln
75
+ const pumpRunning = !!(await this.adapter.getStateAsync('pump.pump_switch'))?.val;
76
+ if (pumpRunning) {
77
+ if (id.endsWith('pump.live.current_power_w')) {
78
+ this._pushValue('power', state.val);
79
+ }
80
+ if (id.endsWith('pump.live.flow_current_lh')) {
81
+ this._pushValue('flow', state.val);
82
+ }
83
+ }
84
+
85
+ // Bei jeder Änderung der Livewerte aktuelle Abweichung bewerten
86
+ await this._updateDeviationAndStatus();
87
+ } catch (err) {
88
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei handleStateChange: ${err.message}`);
89
+ }
90
+ },
91
+
92
+ /**
93
+ * Fügt einen neuen Wert in den temporären Speicher ein.
94
+ *
95
+ * @param {"power"|"flow"} type - Art des Wertes
96
+ * @param {number} value - neuer Messwert
97
+ */
98
+ _pushValue(type, value) {
99
+ if (!this.currentSessionValues[type]) {
100
+ this.currentSessionValues[type] = [];
101
+ }
102
+ if (typeof value === 'number' && value > 0) {
103
+ this.currentSessionValues[type].push(value);
104
+ }
105
+ },
106
+
107
+ /**
108
+ * Wird aufgerufen, wenn ein Pumpenlauf endet.
109
+ * Berechnet Durchschnittswerte des Laufs und aktualisiert Lernfelder.
110
+ */
111
+ async _finalizeLearningCycle() {
112
+ try {
113
+ const { power, flow } = this.currentSessionValues;
114
+ if (power.length === 0 || flow.length === 0) {
115
+ this.adapter.log.debug('[pumpHelper3] Keine Werte zum Lernen vorhanden, Zyklus übersprungen');
116
+ return;
117
+ }
118
+
119
+ const avgPower = this._average(power);
120
+ const avgFlow = this._average(flow);
121
+
122
+ // Vorhandene Lernwerte lesen
123
+ const learnedPower = (await this._getNumber('pump.learning.learned_avg_power_w')) || 0;
124
+ const learnedFlow = (await this._getNumber('pump.learning.learned_avg_flow_lh')) || 0;
125
+ const cycles = (await this._getNumber('pump.learning.learning_cycles_total')) || 0;
126
+
127
+ // Gleitenden Durchschnitt berechnen
128
+ const newCycles = cycles + 1;
129
+ const newAvgPower = (learnedPower * cycles + avgPower) / newCycles;
130
+ const newAvgFlow = (learnedFlow * cycles + avgFlow) / newCycles;
131
+
132
+ // Normalbereiche ±15 %
133
+ const rangePowerLow = Math.round(newAvgPower * 0.85);
134
+ const rangePowerHigh = Math.round(newAvgPower * 1.15);
135
+ const rangeFlowLow = Math.round(newAvgFlow * 0.85);
136
+ const rangeFlowHigh = Math.round(newAvgFlow * 1.15);
137
+
138
+ // States schreiben
139
+ await this.adapter.setStateAsync('pump.learning.learned_avg_power_w', {
140
+ val: Math.round(newAvgPower),
141
+ ack: true,
142
+ });
143
+ await this.adapter.setStateAsync('pump.learning.learned_avg_flow_lh', {
144
+ val: Math.round(newAvgFlow),
145
+ ack: true,
146
+ });
147
+ await this.adapter.setStateAsync('pump.learning.normal_range_power_low', { val: rangePowerLow, ack: true });
148
+ await this.adapter.setStateAsync('pump.learning.normal_range_power_high', {
149
+ val: rangePowerHigh,
150
+ ack: true,
151
+ });
152
+ await this.adapter.setStateAsync('pump.learning.normal_range_flow_low', { val: rangeFlowLow, ack: true });
153
+ await this.adapter.setStateAsync('pump.learning.normal_range_flow_high', { val: rangeFlowHigh, ack: true });
154
+ await this.adapter.setStateAsync('pump.learning.learning_cycles_total', { val: newCycles, ack: true });
155
+
156
+ this.adapter.log.debug(
157
+ `[pumpHelper3] Lernzyklus #${newCycles} abgeschlossen (Power Ø${avgPower}W, Flow Ø${avgFlow}l/h)`,
158
+ );
159
+
160
+ // Speicher leeren
161
+ this.currentSessionValues.power = [];
162
+ this.currentSessionValues.flow = [];
163
+ } catch (err) {
164
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei _finalizeLearningCycle: ${err.message}`);
165
+ }
166
+ },
167
+
168
+ /**
169
+ * Bewertet aktuelle Abweichungen und schreibt Status.
170
+ */
171
+ async _updateDeviationAndStatus() {
172
+ try {
173
+ const currentPower = await this._getNumber('pump.live.current_power_w');
174
+ const currentFlow = await this._getNumber('pump.live.flow_current_lh');
175
+ const avgPower = await this._getNumber('pump.learning.learned_avg_power_w');
176
+ const avgFlow = await this._getNumber('pump.learning.learned_avg_flow_lh');
177
+
178
+ if (avgPower <= 0 || avgFlow <= 0) {
179
+ return;
180
+ } // Noch kein Lernwert vorhanden
181
+
182
+ // Prozentuale Abweichungen
183
+ const deviationPower = ((currentPower - avgPower) / avgPower) * 100;
184
+ const deviationFlow = ((currentFlow - avgFlow) / avgFlow) * 100;
185
+
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
+ });
194
+
195
+ // Bewertung
196
+ const statusText = this._getStatusText(deviationPower, deviationFlow);
197
+ await this.adapter.setStateAsync('pump.learning.status_text', { val: statusText, ack: true });
198
+ } catch (err) {
199
+ this.adapter.log.warn(`[pumpHelper3] Fehler bei _updateDeviationAndStatus: ${err.message}`);
200
+ }
201
+ },
202
+
203
+ /**
204
+ * Liefert textbasierte Bewertung anhand der Abweichung.
205
+ *
206
+ * @param devPower - Aktuelle Abweichung der Leistung (W)
207
+ * @param devFlow - Aktuelle Abweichung des Durchflusses (L/H)
208
+ */
209
+ _getStatusText(devPower, devFlow) {
210
+ const absPower = Math.abs(devPower);
211
+ const absFlow = Math.abs(devFlow);
212
+
213
+ if (absPower <= 15 && absFlow <= 15) {
214
+ return 'Pumpe läuft im Normalbereich';
215
+ }
216
+ if (devPower < -15 || devFlow < -15) {
217
+ return 'Pumpe läuft unterhalb des Normalbereichs (möglicher Filterdruck)';
218
+ }
219
+ if (devPower > 15 || devFlow > 15) {
220
+ return 'Pumpe läuft oberhalb des Normalbereichs (möglicher Luftstau)';
221
+ }
222
+ return 'Pumpe außerhalb des bekannten Bereichs';
223
+ },
224
+
225
+ /**
226
+ * Erzeugt Durchschnittswerte aus einem Array numerischer Werte.
227
+ *
228
+ * @param {number[]} arr - Wertearray
229
+ * @returns {number} - Durchschnittswert
230
+ */
231
+ _average(arr) {
232
+ if (!arr || arr.length === 0) {
233
+ return 0;
234
+ }
235
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
236
+ },
237
+
238
+ /**
239
+ * Liest einen numerischen Statewert oder 0 bei Fehler.
240
+ *
241
+ * @param {string} id - Objekt-ID
242
+ * @returns {Promise<number>} - Gelesener Wert oder 0
243
+ */
244
+ async _getNumber(id) {
245
+ const state = await this.adapter.getStateAsync(id);
246
+ const val = Number(state?.val);
247
+ return isNaN(val) ? 0 : val;
248
+ },
249
+
250
+ /**
251
+ * Cleanup bei Adapter-Unload
252
+ */
253
+ cleanup() {
254
+ this.currentSessionValues = { power: [], flow: [] };
255
+ this.adapter?.log.debug('[pumpHelper3] Cleanup ausgeführt.');
256
+ },
257
+ };
258
+
259
+ module.exports = pumpHelper3;