iobroker.poolcontrol 0.8.2 → 0.9.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
@@ -35,6 +35,29 @@ Er ermöglicht die Automatisierung von Pumpen-, Temperatur- und Solarsteuerung s
35
35
  - Kollektor-Warnung (mit automatischer Rücksetzung bei 10 % unter der Schwelle)
36
36
  - Optionale Sprachausgabe bei Warnung
37
37
 
38
+ - **Heizungs- / Wärmepumpensteuerung (neu, Testphase)**
39
+ - Automatische Steuerung von Heizstab oder Wärmepumpe auf Basis der Pooltemperatur
40
+ - Zieltemperatur und maximale Sicherheitstemperatur konfigurierbar
41
+ - Aktiv nur bei:
42
+ - aktiver Poolsaison
43
+ - Pumpenmodus **Automatik**
44
+ - nicht aktivem Wartungsmodus
45
+ - Vorranglogik:
46
+ - Wartungsmodus blockiert die Heizungssteuerung vollständig
47
+ - Heizung greift nicht in manuelle oder zeitgesteuerte Pumpenmodi ein
48
+ - Pumpen-Nachlaufzeit nach Heizende konfigurierbar
49
+ - Ownership-Schutz:
50
+ - Die Pumpe wird nur dann ausgeschaltet, wenn sie zuvor vom heatHelper selbst eingeschaltet wurde
51
+ - Unterstützt:
52
+ - schaltbare Steckdosen **oder**
53
+ - boolesche Steuer-States externer Heizungen
54
+ - Interner Status- und Diagnosebereich unter `heat.*`
55
+ - Rein steuernd, **keine Chemie- oder Solarlogik**
56
+
57
+ **Hinweis:**
58
+ Diese Funktion befindet sich aktuell in einer **Testphase**.
59
+ Die Logik ist vollständig implementiert, sollte aber zunächst nur mit interessierten Test-Usern eingesetzt werden.
60
+
38
61
  - **Photovoltaiksteuerung (seit v0.6.0)**
39
62
  - Automatische Pumpensteuerung auf Basis von PV-Erzeugung und Hausverbrauchs
40
63
  - Einschaltlogik: Überschuss ≥ (Pumpen-Nennleistung + Sicherheitsaufschlag)
@@ -151,8 +174,6 @@ Die Konfiguration erfolgt über Tabs im Admin-Interface:
151
174
  - Erweiterte PV- und Solar-Effizienzanalyse (COP-Berechnung, Tagesnutzen, Wetterintegration)
152
175
  - Statistik-Exportfunktion (CSV/Excel)
153
176
  - Diagnostic-Helper zur automatischen Systemprüfung
154
- - Erweiterung der Heizungs-/Wärmepumpenlogik (`heatHelper`)
155
- - Zweite Pumpe (z. B. Wärmetauscher oder Wärmepumpe)
156
177
  - Eigene Widgets für VIS/VIS2 (grafische Pool- und Solarvisualisierung)
157
178
  - Steuerung von Poolbeleuchtung, Ventilen und Gegenstromanlagen
158
179
  - Integration zusätzlicher Sensorboxen (z. B. TempBox, PressureBox, LevelBox)
@@ -176,6 +197,22 @@ Neue Funktionen werden regelmäßig ergänzt – bitte den Changelog beachten.
176
197
  ## Changelog
177
198
  ### **WORK IN PROGRESS**
178
199
 
200
+ ## v0.9.0 (WORK IN PROGRESS)
201
+ - Einführung der Heizungs- / Wärmepumpensteuerung (`heatHelper`)
202
+ - Automatische Heizanforderung basierend auf Pooltemperatur
203
+ - Ziel- und Maximaltemperatur konfigurierbar
204
+ - Unterstützung von:
205
+ - schaltbaren Steckdosen
206
+ - booleschen Steuer-States
207
+ - Pumpen-Nachlaufzeit nach Heizende
208
+ - Vorrangsystem:
209
+ - Wartungsmodus blockiert Heizungssteuerung
210
+ - Aktiv nur im Automatikmodus
211
+ - Berücksichtigung des Saisonstatus
212
+ - Ownership-Schutz für Pumpensteuerung
213
+ - Neuer interner State `heat.heating_request` für externe Auswertung
214
+
215
+
179
216
  ## v0.8.2 (2025-12-25)
180
217
  - Neues KI-Modul **Chemie-Hilfe** (`aiChemistryHelpHelper`)
181
218
  - Rein informatives Hilfesystem zur Poolwasserchemie
@@ -242,17 +279,8 @@ Neue Funktionen werden regelmäßig ergänzt – bitte den Changelog beachten.
242
279
  - Telegram-Benutzerwahl hinzugefügt
243
280
 
244
281
  ## v0.5.2 (2025-10-30)
245
- - Erweitertes Helper-Vorrangssystem: Konflikte zwischen Zeit- und Solarsteuerung behoben
246
- - Frostschutz pausiert während Zeitfenster. Nun stabiles Pumpenverhalten und Verbesserte
247
- Koordination zwischen den Helpern
248
282
 
249
283
  ## v0.5.0 (2025-10-28)
250
- - Erweiterung der Temperaturstatistik um Wochen- und Monatsauswertung
251
- (`analytics.statistics.temperature.week` / `.month`)
252
- - Eigenständige, eventbasierte Helper für Woche und Monat
253
- - Persistente Datenpunkte mit automatischen JSON- und HTML-Zusammenfassungen
254
- - Vorbereitung für zukünftige Erweiterungen (Saison- und Jahresstatistik)
255
-
256
284
 
257
285
  ### **0.4.0 (26.10.2025)**
258
286
 
@@ -421,7 +421,7 @@
421
421
  },
422
422
  "solar": {
423
423
  "type": "panel",
424
- "label": "Solarverwaltung",
424
+ "label": "Solar/Heizungsverwaltung",
425
425
  "items": {
426
426
  "solar_control_active_header": {
427
427
  "type": "header",
@@ -539,7 +539,91 @@
539
539
  "md": 12,
540
540
  "lg": 12,
541
541
  "xl": 12
542
- }
542
+ },
543
+ "heat_control_header": {
544
+ "type": "header",
545
+ "text": "Heizung / Wärmepumpe",
546
+ "size": 4,
547
+ "newLine": true,
548
+ "xs": 12,
549
+ "sm": 12,
550
+ "md": 12,
551
+ "lg": 12,
552
+ "xl": 12
553
+ },
554
+
555
+ "heat_control_active": {
556
+ "type": "checkbox",
557
+ "label": "Heizungssteuerung aktivieren",
558
+ "default": false,
559
+ "xs": 12,
560
+ "sm": 3,
561
+ "md": 3,
562
+ "lg": 3,
563
+ "xl": 3,
564
+ "newLine": true
565
+ },
566
+
567
+ "heat_control_type": {
568
+ "type": "select",
569
+ "label": "Art der Heizungssteuerung",
570
+ "options": [
571
+ { "label": "Messsteckdose schalten (Ein / Aus)", "value": "socket" },
572
+ { "label": "Logischer Steuer-State (true / false)", "value": "boolean" }
573
+ ],
574
+ "default": "socket",
575
+ "xs": 12,
576
+ "sm": 4,
577
+ "md": 4,
578
+ "lg": 4,
579
+ "xl": 4,
580
+ "newLine": true
581
+ },
582
+
583
+ "heat_control_object_id": {
584
+ "type": "objectId",
585
+ "label": "Objekt-ID Heizung / Wärmepumpe",
586
+ "default": "",
587
+ "xs": 12,
588
+ "sm": 6,
589
+ "md": 6,
590
+ "lg": 6,
591
+ "xl": 6
592
+ },
593
+
594
+ "heat_temp_target": {
595
+ "type": "number",
596
+ "label": "Zieltemperatur Pool (°C)",
597
+ "default": 26,
598
+ "xs": 12,
599
+ "sm": 3,
600
+ "md": 3,
601
+ "lg": 3,
602
+ "xl": 3,
603
+ "newLine": true
604
+ },
605
+
606
+ "heat_temp_max": {
607
+ "type": "number",
608
+ "label": "Maximale Pooltemperatur (°C – Sicherheitsabschaltung)",
609
+ "default": 30,
610
+ "xs": 12,
611
+ "sm": 3,
612
+ "md": 3,
613
+ "lg": 3,
614
+ "xl": 3
615
+ },
616
+
617
+ "heat_control_hint": {
618
+ "type": "staticText",
619
+ "text": "Hinweis: Die Heizungssteuerung arbeitet temperaturgeführt und nur, wenn die Poolsaison aktiv ist. Je nach ausgewählter Steuerungsart wird entweder eine Messsteckdose geschaltet oder ein logischer Steuer-State (true/false) gesetzt.",
620
+ "newLine": true,
621
+ "xs": 12,
622
+ "sm": 12,
623
+ "md": 12,
624
+ "lg": 12,
625
+ "xl": 12
626
+ }
543
627
  }
544
628
  },
545
629
  "timecontrol": {
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "news": {
6
+ "0.9.0": {
7
+ "en": "New heating / heat pump control (heatHelper). Automatic control based on pool temperature with configurable target and maximum temperature, pump after-run time, and full priority handling (e.g. maintenance mode). Works only in automatic pump mode and respects season status. Supports both switchable sockets and boolean control states. Includes ownership protection to avoid interfering with other pump controllers.",
8
+ "de": "Neue Heizungs- / Wärmepumpensteuerung (heatHelper). Automatische Regelung basierend auf der Pooltemperatur mit konfigurierbarer Ziel- und Maximaltemperatur, Pumpen-Nachlaufzeit sowie vollständiger Vorranglogik (z. B. Wartungsmodus). Arbeitet ausschließlich im Automatikmodus der Pumpe und berücksichtigt den Saisonstatus. Unterstützt schaltbare Steckdosen und boolesche Steuer-States. Enthält Ownership-Schutz, um Konflikte mit anderen Pumpensteuerungen zu vermeiden.",
9
+ "ru": "Новая система управления отоплением / тепловым насосом (heatHelper). Автоматическое управление на основе температуры воды с настраиваемой целевой и максимальной температурой, временем добега насоса и полной системой приоритетов (например, режим обслуживания). Работает только в автоматическом режиме насоса и учитывает сезонный статус. Поддерживает как переключаемые розетки, так и булевы управляющие состояния. Включает защиту владения для предотвращения конфликтов с другими контроллерами насоса.",
10
+ "pt": "Novo controlo de aquecimento / bomba de calor (heatHelper). Controlo automático baseado na temperatura da piscina com temperatura alvo e máxima configuráveis, tempo de pós-funcionamento da bomba e lógica completa de prioridades (por exemplo, modo de manutenção). Funciona apenas no modo automático da bomba e respeita o estado da época. Suporta tomadas comutáveis e estados de controlo booleanos. Inclui proteção de propriedade para evitar conflitos com outros controladores da bomba.",
11
+ "nl": "Nieuwe verwarmings- / warmtepompregeling (heatHelper). Automatische regeling op basis van de zwembadtemperatuur met instelbare doel- en maximumtemperatuur, pomp-nalooptijd en volledige prioriteitslogica (bijv. onderhoudsmodus). Werkt uitsluitend in de automatische pompmodus en houdt rekening met de seizoensstatus. Ondersteunt zowel schakelbare stopcontacten als booleaanse besturingsstates. Bevat eigendomsbescherming om conflicten met andere pompcontrollers te voorkomen.",
12
+ "fr": "Nouveau contrôle de chauffage / pompe à chaleur (heatHelper). Régulation automatique basée sur la température de la piscine avec températures cible et maximale configurables, temps de post-fonctionnement de la pompe et gestion complète des priorités (par ex. mode maintenance). Fonctionne uniquement en mode automatique de la pompe et respecte le statut de saison. Prend en charge les prises commutables et les états de commande booléens. Inclut une protection de propriété afin d’éviter les conflits avec d’autres contrôleurs de pompe.",
13
+ "it": "Nuovo controllo di riscaldamento / pompa di calore (heatHelper). Controllo automatico basato sulla temperatura della piscina con temperatura obiettivo e massima configurabili, tempo di post-funzionamento della pompa e gestione completa delle priorità (ad es. modalità manutenzione). Funziona solo in modalità automatica della pompa e tiene conto dello stato stagionale. Supporta prese commutabili e stati di controllo booleani. Include una protezione di proprietà per evitare conflitti con altri controllori della pompa.",
14
+ "es": "Nuevo control de calefacción / bomba de calor (heatHelper). Control automático basado en la temperatura de la piscina con temperatura objetivo y máxima configurables, tiempo de posfuncionamiento de la bomba y gestión completa de prioridades (p. ej., modo de mantenimiento). Funciona únicamente en modo automático de la bomba y respeta el estado de la temporada. Compatible con enchufes conmutables y estados de control booleanos. Incluye protección de propiedad para evitar conflictos con otros controladores de la bomba.",
15
+ "pl": "Nowy system sterowania ogrzewaniem / pompą ciepła (heatHelper). Automatyczne sterowanie na podstawie temperatury basenu z konfigurowalną temperaturą docelową i maksymalną, czasem dobiegu pompy oraz pełną obsługą priorytetów (np. tryb konserwacji). Działa wyłącznie w trybie automatycznym pompy i uwzględnia status sezonu. Obsługuje przełączalne gniazda oraz binarne stany sterujące. Zawiera ochronę własności, aby zapobiec konfliktom z innymi kontrolerami pompy.",
16
+ "uk": "Нова система керування опаленням / тепловим насосом (heatHelper). Автоматичне керування на основі температури басейну з налаштовуваною цільовою та максимальною температурою, часом післяроботи насоса та повною системою пріоритетів (наприклад, режим обслуговування). Працює лише в автоматичному режимі насоса та враховує сезонний статус. Підтримує комутовані розетки та булеві стани керування. Містить захист володіння для запобігання конфліктам з іншими контролерами насоса.",
17
+ "zh-cn": "新增加热 / 热泵控制功能(heatHelper)。基于泳池水温的自动控制,支持可配置的目标温度和最高温度、泵后运行时间以及完整的优先级处理(例如维护模式)。仅在泵的自动模式下工作,并考虑季节状态。支持可开关插座和布尔控制状态。包含所有权保护机制,避免与其他泵控制逻辑发生冲突。"
18
+ },
6
19
  "0.8.2": {
7
20
  "en": "New AI chemistry help module. Provides an interactive, purely informational help system for common pool water issues (e.g. pH too high/low, chlorine ineffective, green or cloudy water). Generates understandable explanations and solution directions without automatic dosing, product recommendations, device control or speech output.",
8
21
  "de": "Neues KI-Chemie-Hilfesystem. Bietet eine interaktive, rein informative Unterstützung bei typischen Poolwasserproblemen (z. B. pH zu hoch/niedrig, Chlor wirkt nicht, grünes oder trübes Wasser). Erzeugt verständliche Ursachen- und Lösungshinweise ohne automatische Dosierung, Produktempfehlungen, Geräte­steuerung oder Sprachausgabe.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Dodano nowy system informacji: adapter zapisuje teraz sezonowe pozdrowienia oraz zainstalowaną wersję adaptera w stanach info.*. Zawiera automatyczne codzienne odświeżanie o 00:01 oraz pełne wyliczenie daty Wielkanocy.",
81
94
  "uk": "Додано нову інформаційну систему: адаптер тепер записує сезонні привітання та встановлену версію адаптера в стани info.*. Містить щоденне автоматичне оновлення о 00:01 і повний розрахунок дати Великодня.",
82
95
  "zh-cn": "新增信息系统:适配器现在会将季节性问候和已安装的适配器版本写入 info.* 状态。包括每天 00:01 的自动刷新和完整的复活节日期计算。"
83
- },
84
- "0.7.1": {
85
- "en": "Fix for pressure sensor initialization: The adapter now correctly recognizes the configured sensor ID in pumpHelper4. Additionally improved handling of missing / invalid values.",
86
- "de": "Fehlerbehebung bei der Drucksensor-Initialisierung: Der Adapter erkennt nun die konfigurierte Sensor-ID in pumpHelper4 korrekt. Außerdem wurde die Verarbeitung fehlender oder ungültiger Werte verbessert.",
87
- "ru": "Исправление инициализации датчика давления: адаптер теперь корректно распознаёт настроенный идентификатор датчика в pumpHelper4. Улучшена обработка отсутствующих и недопустимых значений.",
88
- "pt": "Correção na inicialização do sensor de pressão: o adaptador agora reconhece corretamente o ID configurado do sensor em pumpHelper4. Manipulação aprimorada de valores ausentes ou inválidos.",
89
- "nl": "Foutoplossing voor de druksensor-initialisatie: de adapter herkent nu correct de geconfigureerde sensor-ID in pumpHelper4. Verbeterde verwerking van ontbrekende of ongeldige waarden.",
90
- "fr": "Correction de l'initialisation du capteur de pression : l'adaptateur reconnaît désormais correctement l'ID du capteur configuré dans pumpHelper4. Amélioration du traitement des valeurs manquantes ou invalides.",
91
- "it": "Correzione dell'inizializzazione del sensore di pressione: l'adattatore ora riconosce correttamente l'ID configurato del sensore in pumpHelper4. Migliorata anche la gestione dei valori mancanti o non validi.",
92
- "es": "Corrección en la inicialización del sensor de presión: el adaptador ahora reconoce correctamente la ID del sensor configurado en pumpHelper4. Manejo mejorado de valores faltantes o inválidos.",
93
- "pl": "Poprawka inicjalizacji czujnika ciśnienia: adapter poprawnie rozpoznaje teraz skonfigurowany identyfikator czujnika w pumpHelper4. Ulepszono również obsługę brakujących lub nieprawidłowych wartości.",
94
- "uk": "Виправлення ініціалізації датчика тиску: адаптер тепер правильно розпізнає налаштований ID датчика в pumpHelper4. Покращено обробку відсутніх або некоректних значень.",
95
- "zh-cn": "修复压力传感器初始化:适配器现在可以正确识别 pumpHelper4 中配置的传感器 ID,并改进了对缺失或无效数值的处理。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -0,0 +1,450 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * heatHelper
5
+ * -------------------------------------------------------------
6
+ * - Steuert Heizung/Wärmepumpe basierend auf Pooltemperatur
7
+ * - Respektiert:
8
+ * - status.season_active
9
+ * - control.pump.maintenance_active (Vorrang / Block)
10
+ * - pump.mode (nur im Automatikbetrieb)
11
+ * - Kann entweder:
12
+ * - eine schaltbare Steckdose (socket) oder
13
+ * - einen bool Steuer-State (boolean) bedienen
14
+ * - Erzeugt zusätzlich ein internes Signal:
15
+ * - heat.heating_request (read-only) => kann extern ausgewertet werden
16
+ * - Pumpen-Nachlaufzeit (min) wird berücksichtigt
17
+ * - Ownership-Schutz: Pumpe wird nur ausgeschaltet, wenn heatHelper sie vorher selbst eingeschaltet hat
18
+ * -------------------------------------------------------------
19
+ */
20
+
21
+ const heatHelper = {
22
+ adapter: null,
23
+
24
+ // dynamische Steuer-ID (foreign)
25
+ _heatControlForeignId: '',
26
+ _afterrunTimer: null,
27
+
28
+ // Ownership / Merker
29
+ _ownsPump: false,
30
+ _desiredHeat: null,
31
+ _lastEval: 0,
32
+
33
+ init(adapter) {
34
+ this.adapter = adapter;
35
+
36
+ // lokale States überwachen
37
+ this.adapter.subscribeStates('heat.control_active');
38
+ this.adapter.subscribeStates('heat.control_type');
39
+ this.adapter.subscribeStates('heat.control_object_id');
40
+ this.adapter.subscribeStates('heat.target_temperature');
41
+ this.adapter.subscribeStates('heat.max_temperature');
42
+ this.adapter.subscribeStates('heat.pump_afterrun_minutes');
43
+
44
+ // Abhängigkeiten
45
+ this.adapter.subscribeStates('status.season_active');
46
+ this.adapter.subscribeStates('pump.mode');
47
+ this.adapter.subscribeStates('pump.pump_switch');
48
+ this.adapter.subscribeStates('temperature.surface.current');
49
+
50
+ // Vorrangschaltung / Wartung
51
+ this.adapter.subscribeStates('control.pump.maintenance_active');
52
+
53
+ // ggf. vorhandene Foreign-ID abonnieren
54
+ this._refreshForeignSubscription().catch(err =>
55
+ this.adapter.log.warn(`[heatHelper] Foreign-Subscription Fehler: ${err.message}`),
56
+ );
57
+
58
+ this._safeEvaluate('init');
59
+ this.adapter.log.info('[heatHelper] Initialisierung abgeschlossen.');
60
+ },
61
+
62
+ async handleStateChange(id, state) {
63
+ if (!state) {
64
+ return;
65
+ }
66
+
67
+ try {
68
+ // Wenn die Ziel-Objekt-ID geändert wurde: Foreign subscription anpassen
69
+ if (id.endsWith('heat.control_object_id') || id.endsWith('heat.control_type')) {
70
+ await this._refreshForeignSubscription();
71
+ await this._safeEvaluate('control_target_changed');
72
+ return;
73
+ }
74
+
75
+ // Alles andere: neu bewerten
76
+ if (
77
+ id.endsWith('heat.control_active') ||
78
+ id.endsWith('heat.target_temperature') ||
79
+ id.endsWith('heat.max_temperature') ||
80
+ id.endsWith('heat.pump_afterrun_minutes') ||
81
+ id.endsWith('status.season_active') ||
82
+ id.endsWith('pump.mode') ||
83
+ id.endsWith('temperature.surface.current') ||
84
+ id.endsWith('control.pump.maintenance_active')
85
+ ) {
86
+ await this._safeEvaluate('state_change');
87
+ return;
88
+ }
89
+
90
+ // Wenn jemand die Pumpe extern schaltet: Ownership ggf. zurücknehmen
91
+ if (id.endsWith('pump.pump_switch')) {
92
+ const pumpOn = !!state.val;
93
+ if (!pumpOn && this._ownsPump) {
94
+ // wenn Pumpe aus geht obwohl wir "ownen", Ownership verlieren
95
+ this._ownsPump = false;
96
+ }
97
+ await this._safeEvaluate('pump_switch_changed');
98
+ return;
99
+ }
100
+ } catch (err) {
101
+ this.adapter.log.warn(`[heatHelper] Fehler in handleStateChange: ${err.message}`);
102
+ }
103
+ },
104
+
105
+ // -------------------------------------------------------------
106
+ // Core
107
+ // -------------------------------------------------------------
108
+ async _evaluate(_sourceTag = '') {
109
+ const now = Date.now();
110
+ if (now - this._lastEval < 250) {
111
+ return;
112
+ }
113
+ this._lastEval = now;
114
+
115
+ const seasonActive = !!(await this.adapter.getStateAsync('status.season_active'))?.val;
116
+ const maintenanceActive = !!(await this.adapter.getStateAsync('control.pump.maintenance_active'))?.val;
117
+
118
+ const pumpMode = (await this.adapter.getStateAsync('pump.mode'))?.val || 'auto';
119
+ const heatEnabled = !!(await this.adapter.getStateAsync('heat.control_active'))?.val;
120
+
121
+ const poolTempRaw = (await this.adapter.getStateAsync('temperature.surface.current'))?.val;
122
+ const poolTemp = Number(poolTempRaw);
123
+
124
+ const targetTemp = Number((await this.adapter.getStateAsync('heat.target_temperature'))?.val ?? 26);
125
+ const maxTemp = Number((await this.adapter.getStateAsync('heat.max_temperature'))?.val ?? 30);
126
+
127
+ const afterrunMin = Math.max(
128
+ 0,
129
+ Number((await this.adapter.getStateAsync('heat.pump_afterrun_minutes'))?.val ?? 0) || 0,
130
+ );
131
+
132
+ const controlType = (await this.adapter.getStateAsync('heat.control_type'))?.val || 'socket';
133
+ const controlObjectId = (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '';
134
+
135
+ // --- Hard conditions / Blocker ---
136
+ if (!seasonActive) {
137
+ return this._applyBlockedState('season_inactive', 'Poolsaison ist inaktiv', afterrunMin);
138
+ }
139
+
140
+ // Vorrangschaltung: Wartung blockiert IMMER
141
+ if (maintenanceActive) {
142
+ return this._applyBlockedState(
143
+ 'maintenance_active',
144
+ 'Wartungsmodus aktiv (Control hat Vorrang)',
145
+ afterrunMin,
146
+ );
147
+ }
148
+
149
+ // Heizungssteuerung deaktiviert
150
+ if (!heatEnabled) {
151
+ return this._applyOffState('heat_disabled', 'Heizungssteuerung deaktiviert', afterrunMin);
152
+ }
153
+
154
+ // Pumpenmodus: nur Automatik
155
+ if (pumpMode !== 'auto') {
156
+ return this._applyBlockedState(
157
+ 'mode_not_auto',
158
+ `Pumpenmodus ist '${pumpMode}' (Heizung arbeitet nur in Automatik)`,
159
+ afterrunMin,
160
+ );
161
+ }
162
+
163
+ // Sensor plausibel?
164
+ if (!Number.isFinite(poolTemp)) {
165
+ return this._applyBlockedState(
166
+ 'no_pool_temp',
167
+ 'Keine gültige Pooltemperatur (temperature.surface.current)',
168
+ afterrunMin,
169
+ );
170
+ }
171
+
172
+ // Sicherheitsabschaltung: MaxTemp überschritten
173
+ if (poolTemp >= maxTemp) {
174
+ return this._applyOffState(
175
+ 'max_temp_reached',
176
+ `Max-Temperatur erreicht (${poolTemp.toFixed(1)} °C ≥ ${maxTemp} °C)`,
177
+ afterrunMin,
178
+ );
179
+ }
180
+
181
+ // --- Heating logic (simple hysteresis-free) ---
182
+ // Einschalten: unter Zieltemperatur
183
+ // Ausschalten: bei Zieltemperatur erreicht/überschritten
184
+ const shouldHeat = poolTemp < targetTemp;
185
+
186
+ if (shouldHeat) {
187
+ return this._startHeating({
188
+ reason: `Heizen: Pool ${poolTemp.toFixed(1)} °C < Ziel ${targetTemp} °C`,
189
+ controlType,
190
+ controlObjectId,
191
+ });
192
+ }
193
+
194
+ return this._stopHeating({
195
+ reason: `Ziel erreicht: Pool ${poolTemp.toFixed(1)} °C ≥ Ziel ${targetTemp} °C`,
196
+ controlType,
197
+ controlObjectId,
198
+ afterrunMin,
199
+ });
200
+ },
201
+
202
+ // -------------------------------------------------------------
203
+ // State transitions
204
+ // -------------------------------------------------------------
205
+ async _startHeating({ reason, controlType, controlObjectId }) {
206
+ if (this._desiredHeat === true) {
207
+ // nur Status/Reason ggf. aktualisieren
208
+ await this._setHeatStates({
209
+ active: true,
210
+ blocked: false,
211
+ mode: 'heating',
212
+ reason,
213
+ info: `control_type=${controlType}, target=${controlObjectId || '(leer)'}`,
214
+ heatingRequest: true,
215
+ });
216
+ return;
217
+ }
218
+
219
+ this._desiredHeat = true;
220
+
221
+ // Nachlauf ggf. abbrechen
222
+ if (this._afterrunTimer) {
223
+ clearTimeout(this._afterrunTimer);
224
+ this._afterrunTimer = null;
225
+ }
226
+
227
+ // Heizung einschalten (wenn ID gesetzt)
228
+ await this._setHeatingDevice(true, controlObjectId);
229
+
230
+ // Pumpe einschalten (ownership)
231
+ await this._ensurePumpOn();
232
+
233
+ await this._setHeatStates({
234
+ active: true,
235
+ blocked: false,
236
+ mode: 'heating',
237
+ reason,
238
+ info: `Heizung EIN | control_type=${controlType}`,
239
+ heatingRequest: true,
240
+ });
241
+
242
+ this.adapter.log.info(`[heatHelper] Heizung EIN (${reason})`);
243
+ },
244
+
245
+ async _stopHeating({ reason, controlType, controlObjectId, afterrunMin }) {
246
+ if (this._desiredHeat === false) {
247
+ // nur Status/Reason ggf. aktualisieren
248
+ await this._setHeatStates({
249
+ active: false,
250
+ blocked: false,
251
+ mode: 'off',
252
+ reason,
253
+ info: `control_type=${controlType}`,
254
+ heatingRequest: false,
255
+ });
256
+ return;
257
+ }
258
+
259
+ this._desiredHeat = false;
260
+
261
+ // Heizung aus
262
+ await this._setHeatingDevice(false, controlObjectId);
263
+
264
+ // Signal für andere Systeme
265
+ await this._setHeatStates({
266
+ active: false,
267
+ blocked: false,
268
+ mode: 'afterrun',
269
+ reason,
270
+ info: `Heizung AUS | Nachlauf=${afterrunMin} min | control_type=${controlType}`,
271
+ heatingRequest: false,
272
+ });
273
+
274
+ // Pumpen-Nachlauf nur, wenn wir die Pumpe eingeschaltet hatten
275
+ await this._startAfterrunIfNeeded(afterrunMin, reason);
276
+
277
+ this.adapter.log.info(`[heatHelper] Heizung AUS (${reason})`);
278
+ },
279
+
280
+ async _applyBlockedState(mode, reason, afterrunMin) {
281
+ // blockiert => Heizung aus, Request false
282
+ await this._setHeatingDevice(false, (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '');
283
+
284
+ await this._setHeatStates({
285
+ active: false,
286
+ blocked: true,
287
+ mode,
288
+ reason,
289
+ info: 'Heizung blockiert',
290
+ heatingRequest: false,
291
+ });
292
+
293
+ // Nachlauf ggf. (nur wenn wir ownen)
294
+ await this._startAfterrunIfNeeded(afterrunMin, reason);
295
+ },
296
+
297
+ async _applyOffState(mode, reason, afterrunMin) {
298
+ // off => Heizung aus, Request false
299
+ await this._setHeatingDevice(false, (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '');
300
+
301
+ await this._setHeatStates({
302
+ active: false,
303
+ blocked: false,
304
+ mode,
305
+ reason,
306
+ info: 'Heizung AUS',
307
+ heatingRequest: false,
308
+ });
309
+
310
+ await this._startAfterrunIfNeeded(afterrunMin, reason);
311
+ },
312
+
313
+ // -------------------------------------------------------------
314
+ // Pump handling (ownership protected)
315
+ // -------------------------------------------------------------
316
+ async _ensurePumpOn() {
317
+ try {
318
+ const pumpState = await this.adapter.getStateAsync('pump.pump_switch');
319
+ const isOn = !!pumpState?.val;
320
+
321
+ if (!isOn) {
322
+ // wir schalten sie ein => ownership true
323
+ this._ownsPump = true;
324
+ await this.adapter.setStateAsync('pump.pump_switch', { val: true, ack: false });
325
+ await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
326
+ }
327
+ } catch (err) {
328
+ this.adapter.log.warn(`[heatHelper] Konnte Pumpe nicht einschalten: ${err.message}`);
329
+ }
330
+ },
331
+
332
+ async _startAfterrunIfNeeded(afterrunMin, reason) {
333
+ // nur wenn wir die Pumpe vorher selbst eingeschaltet haben
334
+ if (!this._ownsPump) {
335
+ await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
336
+ return;
337
+ }
338
+
339
+ // wenn keine Nachlaufzeit: sofort aus
340
+ if (!afterrunMin || afterrunMin <= 0) {
341
+ await this._stopPumpNow('no_afterrun');
342
+ return;
343
+ }
344
+
345
+ // Timer neu starten
346
+ if (this._afterrunTimer) {
347
+ clearTimeout(this._afterrunTimer);
348
+ this._afterrunTimer = null;
349
+ }
350
+
351
+ await this.adapter.setStateAsync('heat.afterrun_active', { val: true, ack: true });
352
+
353
+ const holdMs = Math.round(afterrunMin * 60 * 1000);
354
+ this.adapter.log.debug(`[heatHelper] Pumpen-Nachlauf gestartet: ${afterrunMin} min (${reason})`);
355
+
356
+ this._afterrunTimer = setTimeout(async () => {
357
+ // Wenn inzwischen wieder Heizbedarf aktiv ist -> Nachlauf abbrechen
358
+ if (this._desiredHeat === true) {
359
+ this.adapter.log.debug('[heatHelper] Nachlauf abgebrochen – Heizen wieder aktiv.');
360
+ return;
361
+ }
362
+ await this._stopPumpNow('afterrun_done');
363
+ }, holdMs);
364
+ },
365
+
366
+ async _stopPumpNow(tag) {
367
+ try {
368
+ await this.adapter.setStateAsync('pump.pump_switch', { val: false, ack: false });
369
+ } catch (err) {
370
+ this.adapter.log.warn(`[heatHelper] Konnte Pumpe nicht ausschalten: ${err.message}`);
371
+ } finally {
372
+ this._ownsPump = false;
373
+ await this.adapter.setStateAsync('heat.afterrun_active', { val: false, ack: true });
374
+ this.adapter.log.info(`[heatHelper] Pumpe AUS (${tag})`);
375
+ }
376
+ },
377
+
378
+ // -------------------------------------------------------------
379
+ // Heating device control (foreign state)
380
+ // -------------------------------------------------------------
381
+ async _setHeatingDevice(on, foreignId) {
382
+ const id = String(foreignId || '').trim();
383
+ if (!id) {
384
+ // Kein Ziel => nur internes heating_request als Signal
385
+ return;
386
+ }
387
+
388
+ try {
389
+ await this.adapter.setForeignStateAsync(id, { val: !!on, ack: false });
390
+ } catch (err) {
391
+ this.adapter.log.warn(`[heatHelper] Konnte Heizung nicht setzen (${id}): ${err.message}`);
392
+ }
393
+ },
394
+
395
+ async _setHeatStates({ active, blocked, mode, reason, info, heatingRequest }) {
396
+ try {
397
+ await this.adapter.setStateAsync('heat.active', { val: !!active, ack: true });
398
+ await this.adapter.setStateAsync('heat.blocked', { val: !!blocked, ack: true });
399
+ await this.adapter.setStateAsync('heat.mode', { val: String(mode ?? ''), ack: true });
400
+ await this.adapter.setStateAsync('heat.reason', { val: String(reason ?? ''), ack: true });
401
+ await this.adapter.setStateAsync('heat.info', { val: String(info ?? ''), ack: true });
402
+ await this.adapter.setStateAsync('heat.heating_request', { val: !!heatingRequest, ack: true });
403
+ await this.adapter.setStateAsync('heat.last_change', { val: Date.now(), ack: true });
404
+ } catch (err) {
405
+ this.adapter.log.warn(`[heatHelper] Fehler beim Schreiben der Heat-States: ${err.message}`);
406
+ }
407
+ },
408
+
409
+ // -------------------------------------------------------------
410
+ // Foreign subscribe handling
411
+ // -------------------------------------------------------------
412
+ async _refreshForeignSubscription() {
413
+ const id = (await this.adapter.getStateAsync('heat.control_object_id'))?.val || '';
414
+ const nextId = String(id).trim();
415
+
416
+ if (nextId && nextId !== this._heatControlForeignId) {
417
+ // neue ID abonnieren
418
+ try {
419
+ this.adapter.subscribeForeignStates(nextId);
420
+ this.adapter.log.info(`[heatHelper] Subscribed Foreign-Heat-Control: "${nextId}"`);
421
+ } catch (err) {
422
+ this.adapter.log.warn(`[heatHelper] Konnte Foreign-State nicht abonnieren (${nextId}): ${err.message}`);
423
+ }
424
+ this._heatControlForeignId = nextId;
425
+ }
426
+
427
+ if (!nextId) {
428
+ this._heatControlForeignId = '';
429
+ }
430
+ },
431
+
432
+ async _safeEvaluate(tag) {
433
+ try {
434
+ await this._evaluate(tag);
435
+ } catch (err) {
436
+ this.adapter.log.warn(`[heatHelper] Evaluate-Fehler (${tag}): ${err.message}`);
437
+ }
438
+ },
439
+
440
+ cleanup() {
441
+ if (this._afterrunTimer) {
442
+ clearTimeout(this._afterrunTimer);
443
+ this._afterrunTimer = null;
444
+ }
445
+ this._ownsPump = false;
446
+ this._desiredHeat = null;
447
+ },
448
+ };
449
+
450
+ module.exports = heatHelper;
@@ -214,6 +214,15 @@ const pumpHelper = {
214
214
  status = 'EIN (Zeitsteuerung)';
215
215
  }
216
216
  break;
217
+
218
+ case 'heatHelper':
219
+ try {
220
+ const reason = (await this.adapter.getStateAsync('pump.reason'))?.val || '';
221
+ status = reason ? `EIN (${reason})` : 'EIN (Heizung)';
222
+ } catch {
223
+ status = 'EIN (Heizung)';
224
+ }
225
+ break;
217
226
  }
218
227
  }
219
228
  await this.adapter.setStateChangedAsync('pump.status', {
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Legt alle States für die Heizungs- / Wärmepumpensteuerung an
5
+ *
6
+ * @param {import("iobroker").Adapter} adapter - ioBroker Adapter-Instanz
7
+ */
8
+ async function createHeatStates(adapter) {
9
+ const channelId = 'heat';
10
+
11
+ // ---------------------------------------------------------
12
+ // Channel
13
+ // ---------------------------------------------------------
14
+ await adapter.setObjectNotExistsAsync(channelId, {
15
+ type: 'channel',
16
+ common: {
17
+ name: 'Heizung / Wärmepumpe',
18
+ },
19
+ native: {},
20
+ });
21
+
22
+ // ---------------------------------------------------------
23
+ // Steuerung / Konfiguration (write = true, persistent)
24
+ // ---------------------------------------------------------
25
+
26
+ await adapter.setObjectNotExistsAsync(`${channelId}.control_active`, {
27
+ type: 'state',
28
+ common: {
29
+ name: 'Heizungssteuerung aktiv',
30
+ type: 'boolean',
31
+ role: 'switch',
32
+ read: true,
33
+ write: true,
34
+ persist: true,
35
+ },
36
+ native: {},
37
+ });
38
+ const existingControlActive = await adapter.getStateAsync(`${channelId}.control_active`);
39
+ if (
40
+ existingControlActive === null ||
41
+ existingControlActive.val === null ||
42
+ existingControlActive.val === undefined
43
+ ) {
44
+ await adapter.setStateAsync(`${channelId}.control_active`, {
45
+ val: adapter.config.heat_control_active ?? false,
46
+ ack: true,
47
+ });
48
+ }
49
+
50
+ await adapter.setObjectNotExistsAsync(`${channelId}.control_type`, {
51
+ type: 'state',
52
+ common: {
53
+ name: 'Art der Heizungssteuerung',
54
+ type: 'string',
55
+ role: 'text',
56
+ read: true,
57
+ write: true,
58
+ persist: true,
59
+ },
60
+ native: {},
61
+ });
62
+ const existingControlType = await adapter.getStateAsync(`${channelId}.control_type`);
63
+ if (existingControlType === null || existingControlType.val === null || existingControlType.val === undefined) {
64
+ await adapter.setStateAsync(`${channelId}.control_type`, {
65
+ val: adapter.config.heat_control_type ?? 'socket',
66
+ ack: true,
67
+ });
68
+ }
69
+
70
+ await adapter.setObjectNotExistsAsync(`${channelId}.control_object_id`, {
71
+ type: 'state',
72
+ common: {
73
+ name: 'Steuer-Objekt-ID Heizung',
74
+ type: 'string',
75
+ role: 'text',
76
+ read: true,
77
+ write: true,
78
+ persist: true,
79
+ },
80
+ native: {},
81
+ });
82
+ const existingObjectId = await adapter.getStateAsync(`${channelId}.control_object_id`);
83
+ if (existingObjectId === null || existingObjectId.val === null || existingObjectId.val === undefined) {
84
+ await adapter.setStateAsync(`${channelId}.control_object_id`, {
85
+ val: adapter.config.heat_control_object_id ?? '',
86
+ ack: true,
87
+ });
88
+ }
89
+
90
+ await adapter.setObjectNotExistsAsync(`${channelId}.target_temperature`, {
91
+ type: 'state',
92
+ common: {
93
+ name: 'Zieltemperatur Pool',
94
+ type: 'number',
95
+ role: 'value.temperature',
96
+ unit: '°C',
97
+ read: true,
98
+ write: true,
99
+ persist: true,
100
+ },
101
+ native: {},
102
+ });
103
+ const existingTargetTemp = await adapter.getStateAsync(`${channelId}.target_temperature`);
104
+ if (existingTargetTemp === null || existingTargetTemp.val === null || existingTargetTemp.val === undefined) {
105
+ await adapter.setStateAsync(`${channelId}.target_temperature`, {
106
+ val: adapter.config.heat_temp_target ?? 26,
107
+ ack: true,
108
+ });
109
+ }
110
+
111
+ await adapter.setObjectNotExistsAsync(`${channelId}.max_temperature`, {
112
+ type: 'state',
113
+ common: {
114
+ name: 'Maximale Pooltemperatur (Sicherheit)',
115
+ type: 'number',
116
+ role: 'value.temperature',
117
+ unit: '°C',
118
+ read: true,
119
+ write: true,
120
+ persist: true,
121
+ },
122
+ native: {},
123
+ });
124
+ const existingMaxTemp = await adapter.getStateAsync(`${channelId}.max_temperature`);
125
+ if (existingMaxTemp === null || existingMaxTemp.val === null || existingMaxTemp.val === undefined) {
126
+ await adapter.setStateAsync(`${channelId}.max_temperature`, {
127
+ val: adapter.config.heat_temp_max ?? 30,
128
+ ack: true,
129
+ });
130
+ }
131
+
132
+ await adapter.setObjectNotExistsAsync(`${channelId}.pump_afterrun_minutes`, {
133
+ type: 'state',
134
+ common: {
135
+ name: 'Pumpen-Nachlaufzeit nach Heizung',
136
+ type: 'number',
137
+ role: 'value.interval',
138
+ unit: 'min',
139
+ read: true,
140
+ write: true,
141
+ persist: true,
142
+ },
143
+ native: {},
144
+ });
145
+ const existingAfterrun = await adapter.getStateAsync(`${channelId}.pump_afterrun_minutes`);
146
+ if (existingAfterrun === null || existingAfterrun.val === null || existingAfterrun.val === undefined) {
147
+ await adapter.setStateAsync(`${channelId}.pump_afterrun_minutes`, {
148
+ val: 5,
149
+ ack: true,
150
+ });
151
+ }
152
+
153
+ // ---------------------------------------------------------
154
+ // Betriebsstatus (write = false)
155
+ // ---------------------------------------------------------
156
+
157
+ await adapter.setObjectNotExistsAsync(`${channelId}.active`, {
158
+ type: 'state',
159
+ common: {
160
+ name: 'Heizung aktiv',
161
+ type: 'boolean',
162
+ role: 'indicator.working',
163
+ read: true,
164
+ write: false,
165
+ },
166
+ native: {},
167
+ });
168
+
169
+ await adapter.setObjectNotExistsAsync(`${channelId}.mode`, {
170
+ type: 'state',
171
+ common: {
172
+ name: 'Heizungsmodus',
173
+ type: 'string',
174
+ role: 'text',
175
+ read: true,
176
+ write: false,
177
+ },
178
+ native: {},
179
+ });
180
+
181
+ await adapter.setObjectNotExistsAsync(`${channelId}.blocked`, {
182
+ type: 'state',
183
+ common: {
184
+ name: 'Heizung blockiert',
185
+ type: 'boolean',
186
+ role: 'indicator.blocked',
187
+ read: true,
188
+ write: false,
189
+ },
190
+ native: {},
191
+ });
192
+
193
+ await adapter.setObjectNotExistsAsync(`${channelId}.afterrun_active`, {
194
+ type: 'state',
195
+ common: {
196
+ name: 'Pumpen-Nachlauf aktiv',
197
+ type: 'boolean',
198
+ role: 'indicator.working',
199
+ read: true,
200
+ write: false,
201
+ },
202
+ native: {},
203
+ });
204
+
205
+ await adapter.setObjectNotExistsAsync(`${channelId}.last_change`, {
206
+ type: 'state',
207
+ common: {
208
+ name: 'Letzte Statusänderung Heizung',
209
+ type: 'number',
210
+ role: 'value.time',
211
+ read: true,
212
+ write: false,
213
+ },
214
+ native: {},
215
+ });
216
+
217
+ await adapter.setObjectNotExistsAsync(`${channelId}.heating_request`, {
218
+ type: 'state',
219
+ common: {
220
+ name: 'Heizanforderung (PoolControl)',
221
+ type: 'boolean',
222
+ role: 'indicator.request',
223
+ read: true,
224
+ write: false,
225
+ def: false,
226
+ },
227
+ native: {},
228
+ });
229
+
230
+ // ---------------------------------------------------------
231
+ // Transparenz / Diagnose
232
+ // ---------------------------------------------------------
233
+
234
+ await adapter.setObjectNotExistsAsync(`${channelId}.reason`, {
235
+ type: 'state',
236
+ common: {
237
+ name: 'Grund für Heizungsstatus',
238
+ type: 'string',
239
+ role: 'text',
240
+ read: true,
241
+ write: false,
242
+ },
243
+ native: {},
244
+ });
245
+
246
+ await adapter.setObjectNotExistsAsync(`${channelId}.info`, {
247
+ type: 'state',
248
+ common: {
249
+ name: 'Heizungs-Info',
250
+ type: 'string',
251
+ role: 'text',
252
+ read: true,
253
+ write: false,
254
+ },
255
+ native: {},
256
+ });
257
+ }
258
+
259
+ module.exports = {
260
+ createHeatStates,
261
+ };
package/main.js CHANGED
@@ -30,6 +30,7 @@ const debugLogHelper = require('./lib/helpers/debugLogHelper');
30
30
  const speechTextHelper = require('./lib/helpers/speechTextHelper');
31
31
  const migrationHelper = require('./lib/helpers/migrationHelper');
32
32
  const infoHelper = require('./lib/helpers/infoHelper');
33
+ const heatHelper = require('./lib/helpers/heatHelper');
33
34
  const { createTemperatureStates } = require('./lib/stateDefinitions/temperatureStates');
34
35
  const { createPumpStates } = require('./lib/stateDefinitions/pumpStates');
35
36
  const { createPumpStates2 } = require('./lib/stateDefinitions/pumpStates2');
@@ -49,6 +50,7 @@ const { createDebugLogStates } = require('./lib/stateDefinitions/debugLogStates'
49
50
  const { createInfoStates } = require('./lib/stateDefinitions/infoStates');
50
51
  const { createAiStates } = require('./lib/stateDefinitions/aiStates'); // NEU: KI-States
51
52
  const { createAiChemistryHelpStates } = require('./lib/stateDefinitions/aiChemistryHelpStates'); // NEU: KI-Chemie-Hilfe
53
+ const { createHeatStates } = require('./lib/stateDefinitions/heatStates');
52
54
 
53
55
  class Poolcontrol extends utils.Adapter {
54
56
  constructor(options) {
@@ -79,6 +81,9 @@ class Poolcontrol extends utils.Adapter {
79
81
  // --- Solarverwaltung ---
80
82
  await createSolarStates(this);
81
83
 
84
+ // --- Heizung / Wärmepumpe ---
85
+ await createHeatStates(this);
86
+
82
87
  // --- Photovoltaik ---
83
88
  await createPhotovoltaicStates(this);
84
89
 
@@ -136,6 +141,7 @@ class Poolcontrol extends utils.Adapter {
136
141
  speechHelper.init(this);
137
142
  consumptionHelper.init(this);
138
143
  solarHelper.init(this);
144
+ heatHelper.init(this); // ← NEU
139
145
  photovoltaicHelper.init(this);
140
146
  aiHelper.init(this);
141
147
  aiForecastHelper.init(this);
@@ -196,6 +202,9 @@ class Poolcontrol extends utils.Adapter {
196
202
  if (controlHelper2.cleanup) {
197
203
  controlHelper2.cleanup();
198
204
  }
205
+ if (heatHelper.cleanup) {
206
+ heatHelper.cleanup();
207
+ }
199
208
  if (speechTextHelper.cleanup) {
200
209
  speechTextHelper.cleanup();
201
210
  }
@@ -279,6 +288,11 @@ class Poolcontrol extends utils.Adapter {
279
288
  } catch (e) {
280
289
  this.log.warn(`[photovoltaicHelper] Fehler in handleStateChange: ${e.message}`);
281
290
  }
291
+ try {
292
+ heatHelper.handleStateChange(id, state);
293
+ } catch (e) {
294
+ this.log.warn(`[heatHelper] Fehler in handleStateChange: ${e.message}`);
295
+ }
282
296
  // --- AI-Helper ---
283
297
  try {
284
298
  aiHelper.handleStateChange(id, state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.poolcontrol",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
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",