iobroker.poolcontrol 0.0.9 → 0.1.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/LICENSE CHANGED
@@ -19,29 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
- ---
24
-
25
- MIT-Lizenz
26
-
27
- Copyright (c) 2025 DasBo1975
28
-
29
- Hiermit wird unentgeltlich jeder Person, die eine Kopie der Software und der
30
- zugehörigen Dokumentationen (die "Software") erhält, die Erlaubnis erteilt,
31
- sie uneingeschränkt zu nutzen, einschließlich und ohne Ausnahme des Rechts,
32
- sie zu verwenden, zu kopieren, zu ändern, zusammenzuführen, zu veröffentlichen,
33
- zu verbreiten, zu unterlizenzieren und/oder zu verkaufen, und Personen, denen
34
- diese Software überlassen wird, diese Rechte zu verschaffen, unter den
35
- folgenden Bedingungen:
36
-
37
- Der obige Urheberrechtsvermerk und dieser Erlaubnisvermerk sind in allen
38
- Kopien oder Teilkopien der Software beizulegen.
39
-
40
- DIE SOFTWARE WIRD OHNE JEDE AUSDRÜCKLICHE ODER STILLSCHWEIGENDE GARANTIE
41
- BEREITGESTELLT, EINSCHLIESSLICH DER GARANTIE DER MARKTREIFE, DER EIGNUNG FÜR
42
- EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG VON RECHTEN DRITTER.
43
- IN KEINEM FALL SIND DIE AUTOREN ODER URHEBERRECHTSINHABER FÜR ANSPRÜCHE,
44
- SCHÄDEN ODER SONSTIGE VERPFLICHTUNGEN HAFTBAR ZU MACHEN, OB AUS EINEM
45
- VERTRAG, EINER UNERLAUBTEN HANDLUNG ODER ANDERWEITIG ENTSTANDEN, DIE SICH AUS
46
- ODER IN VERBINDUNG MIT DER SOFTWARE ODER DER VERWENDUNG ODER SONSTIGEN
47
- HANDLUNGEN IN DER SOFTWARE ERGEBEN.
package/README.md CHANGED
@@ -94,6 +94,33 @@ Funktionen können sich ändern, bitte regelmäßig den Changelog beachten.
94
94
  ## Changelog
95
95
  Auszug, vollständige Liste siehe `io-package.json`:
96
96
 
97
+ ### 0.1.0
98
+ - Sprachausgabe über **E-Mail** hinzugefügt (konfigurierbar: Instanz, Empfänger, Betreff).
99
+ - Erweiterung der Instanz-Konfiguration im Tab „Sprachausgaben“.
100
+ **Bugfixes**
101
+ - Kleinere Korrekturen und Optimierungen in der Dokumentation (`help.md`).
102
+ - Logging in `speechHelper` verbessert.
103
+
104
+ ### 0.0.10
105
+
106
+ Statusübersicht
107
+ Ab Version 0.0.10 gibt es einen eigenen Bereich `status.*` mit folgenden Datenpunkten:
108
+
109
+ - **status.summary** → Textübersicht (Pumpe, Modus, Temperaturen, Laufzeit, Umwälzung)
110
+ - **status.overview_json** → Übersicht als JSON (maschinenlesbar)
111
+ - **status.last_summary_update** → Zeitpunkt der letzten Aktualisierung
112
+ - **status.pump_last_start** → Letzter Pumpenstart (Zeitstempel)
113
+ - **status.pump_last_stop** → Letztes Pumpenende (Zeitstempel)
114
+ - **status.pump_was_on_today** → Boolean, ob die Pumpe heute lief
115
+ - **status.pump_today_count** → Anzahl der Starts heute (Reset um Mitternacht)
116
+ - **status.system_ok** → Boolean, ob das System fehlerfrei läuft
117
+ - **status.system_warning** → Boolean, wenn eine Warnung aktiv ist
118
+ - **status.system_warning_text** → Beschreibung der aktiven Warnung
119
+ - **status.season_active** → Anzeige, ob die Poolsaison aktiv ist
120
+
121
+ Diese Datenpunkte sind besonders für **VIS/vis2, Alexa- oder Telegram-Ausgaben** gedacht, da sie eine schnelle Übersicht über den aktuellen Poolstatus bieten.
122
+
123
+
97
124
  ### 0.0.9
98
125
  - Laufzeit-, Umwälz-, Verbrauch-/Kosten- und Temperatur-Min/Max-States sind jetzt persistent
99
126
  (Werte bleiben nach Adapter-Neustart oder Stromausfall erhalten)
@@ -137,5 +164,6 @@ Der Nutzer ist für die **sichere Installation und den Betrieb seiner Hardware**
137
164
  ---
138
165
 
139
166
  ## License
140
- MIT License
167
+ Copyright (c) 2025 DasBo1975 <dasbo1975@outlook.de>
141
168
 
169
+ MIT License
@@ -49,6 +49,21 @@
49
49
  "lg": 3,
50
50
  "xl": 3,
51
51
  "newLine": true
52
+ },
53
+ "divider_season1": {
54
+ "type": "divider",
55
+ "newLine": true
56
+ },
57
+ "season_active": {
58
+ "type": "checkbox",
59
+ "label": "Poolsaison aktiv",
60
+ "default": false,
61
+ "xs": 12,
62
+ "sm": 3,
63
+ "md": 3,
64
+ "lg": 3,
65
+ "xl": 3,
66
+ "newLine": true
52
67
  }
53
68
  }
54
69
  },
@@ -831,6 +846,53 @@
831
846
  "lg": 6,
832
847
  "xl": 6
833
848
  },
849
+ "divider_speech_email": {
850
+ "type": "divider",
851
+ "newLine": true
852
+ },
853
+ "speech_email_enabled": {
854
+ "type": "checkbox",
855
+ "label": "Ausgabe über E-Mail aktivieren",
856
+ "default": false,
857
+ "xs": 12,
858
+ "sm": 3,
859
+ "md": 3,
860
+ "lg": 3,
861
+ "xl": 3,
862
+ "newLine": true
863
+ },
864
+ "speech_email_instance": {
865
+ "type": "instance",
866
+ "adapter": "email",
867
+ "label": "E-Mail Adapter-Instanz",
868
+ "default": "email.0",
869
+ "xs": 12,
870
+ "sm": 6,
871
+ "md": 6,
872
+ "lg": 6,
873
+ "xl": 6
874
+ },
875
+ "speech_email_recipient": {
876
+ "type": "text",
877
+ "label": "Empfänger-Adresse",
878
+ "default": "meineadresse@example.com",
879
+ "xs": 12,
880
+ "sm": 6,
881
+ "md": 6,
882
+ "lg": 6,
883
+ "xl": 6,
884
+ "newLine": true
885
+ },
886
+ "speech_email_subject": {
887
+ "type": "text",
888
+ "label": "E-Mail Betreff",
889
+ "default": "PoolControl Nachricht",
890
+ "xs": 12,
891
+ "sm": 6,
892
+ "md": 6,
893
+ "lg": 6,
894
+ "xl": 6
895
+ },
834
896
  "divider_speech4": {
835
897
  "type": "divider",
836
898
  "newLine": true
@@ -938,7 +1000,12 @@
938
1000
  },
939
1001
  "helpText": {
940
1002
  "type": "staticText",
941
- "text": "Hier finden Sie die vollständige Dokumentation und Hinweise zu allen Einstellungen. Weitere Hinweise zu künftigen Versionen folgen."
1003
+ "text": "Hier finden Sie die vollständige Dokumentation und Hinweise zu allen Einstellungen. Weitere Hinweise zu künftigen Versionen folgen.",
1004
+ "xs": 12,
1005
+ "sm": 12,
1006
+ "md": 12,
1007
+ "lg": 12,
1008
+ "xl": 12
942
1009
  }
943
1010
  }
944
1011
  }
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.0.9",
4
+ "version": "0.1.0",
5
5
  "news": {
6
+ "0.1.0": {
7
+ "en": "Added speech output via E-Mail (configurable: instance, recipient, subject)",
8
+ "de": "Sprachausgabe per E-Mail hinzugefügt (konfigurierbar: Instanz, Empfänger, Betreff)",
9
+ "ru": "Добавлен вывод речи по электронной почте (настраивается: экземпляр, получатель, тема)",
10
+ "pt": "Adicionada saída de fala por e-mail (configurável: instância, destinatário, assunto)",
11
+ "nl": "Spraakuitvoer via e-mail toegevoegd (configureerbaar: instantie, ontvanger, onderwerp)",
12
+ "fr": "Ajout de la sortie vocale par e-mail (configurable : instance, destinataire, sujet)",
13
+ "it": "Aggiunta uscita vocale via e-mail (configurabile: istanza, destinatario, oggetto)",
14
+ "es": "Se agregó salida de voz por correo electrónico (configurable: instancia, destinatario, asunto)",
15
+ "pl": "Dodano wyjście głosowe przez e-mail (konfigurowalne: instancja, odbiorca, temat)",
16
+ "uk": "Додано озвучення електронною поштою (налаштовується: екземпляр, одержувач, тема)",
17
+ "zh-cn": "新增通过电子邮件的语音输出(可配置:实例、收件人、主题)"
18
+ },
19
+ "0.0.10": {
20
+ "en": "Added extended status overview with pump statistics, system flags and JSON summary",
21
+ "de": "Erweiterte Statusübersicht mit Pumpenstatistiken, Systemanzeigen und JSON-Zusammenfassung hinzugefügt",
22
+ "ru": "Добавлен расширенный обзор состояния с насосной статистикой, системными флагами и JSON-резюме",
23
+ "pt": "Adicionada visão geral de status estendida com estatísticas da bomba, sinalizadores do sistema e resumo em JSON",
24
+ "nl": "Uitgebreide statusoverzicht toegevoegd met pompstatistieken, systeemindicatoren en JSON-samenvatting",
25
+ "fr": "Aperçu du statut étendu ajouté avec statistiques de la pompe, indicateurs système et résumé JSON",
26
+ "it": "Aggiunta panoramica dello stato estesa con statistiche della pompa, indicatori di sistema e riepilogo JSON",
27
+ "es": "Añadida vista general de estado extendida con estadísticas de la bomba, indicadores del sistema y resumen en JSON",
28
+ "pl": "Dodano rozszerzony przegląd statusu ze statystykami pompy, flagami systemowymi i podsumowaniem JSON",
29
+ "uk": "Додано розширений огляд статусу зі статистикою насоса, системними прапорцями та зведенням JSON",
30
+ "zh-cn": "新增扩展状态概览,包括泵统计、系统标志和 JSON 摘要"
31
+ },
6
32
  "0.0.9": {
7
33
  "en": "Made runtime, circulation, consumption/costs and temperature min/max states persistent across restarts",
8
34
  "de": "Laufzeit-, Umwälz-, Verbrauch-/Kosten- und Temperatur-Min/Max-States bleiben jetzt über Neustarts erhalten",
@@ -15,32 +41,6 @@
15
41
  "pl": "Stany czasu pracy, cyrkulacji, zużycia/kosztów i temp. min/max są teraz zachowane po restartach",
16
42
  "uk": "Стан часу роботи, циркуляції, споживання/вартості та мін/макс температури тепер зберігаються після перезапусків",
17
43
  "zh-cn": "运行时间、循环、消耗/成本和温度最小/最大状态现在在重启后保持"
18
- },
19
- "0.0.8": {
20
- "en": "Added Help tab in instance configuration with link to GitHub documentation",
21
- "de": "Hilfetab in der Instanzkonfiguration hinzugefügt mit Link zur GitHub-Dokumentation",
22
- "ru": "Добавлена вкладка помощи в настройке экземпляра со ссылкой на документацию GitHub",
23
- "pt": "Guia de ajuda adicionado na configuração da instância com link para a documentação do GitHub",
24
- "nl": "Help-tab toegevoegd in de instance-configuratie met link naar GitHub-documentatie",
25
- "fr": "Onglet d'aide ajouté dans la configuration de l'instance avec lien vers la documentation GitHub",
26
- "it": "Aggiunta scheda di aiuto nella configurazione dell'istanza con collegamento alla documentazione GitHub",
27
- "es": "Se agregó pestaña de ayuda en la configuración de la instancia con enlace a la documentación de GitHub",
28
- "pl": "Dodano kartę pomocy w konfiguracji instancji z linkiem do dokumentacji GitHub",
29
- "uk": "Додано вкладку довідки в конфігурацію екземпляра з посиланням на документацію GitHub",
30
- "zh-cn": "在实例配置中添加了帮助选项卡,并链接到 документацию GitHub"
31
- },
32
- "0.0.7": {
33
- "en": "Added help file (help.md) and first README version",
34
- "de": "Hilfedatei (help.md) und erste README-Version hinzugefügt",
35
- "ru": "Добавлен файл справки (help.md) и первая версия README",
36
- "pt": "Adicionado arquivo de ajuda (help.md) e primeira versão do README",
37
- "nl": "Helpbestand (help.md) en eerste README-versie toegevoegd",
38
- "fr": "Ajout du fichier d'aide (help.md) et première version du README",
39
- "it": "Aggiunto file di aiuto (help.md) e prima versione di README",
40
- "es": "Se agregó archivo de ayuda (help.md) y primera versión de README",
41
- "pl": "Dodano plik pomocy (help.md) i pierwszą wersję README",
42
- "uk": "Додано файл довідки (help.md) та першу версію README",
43
- "zh-cn": "添加了帮助文件 (help.md) 和第一个 README 版本"
44
44
  }
45
45
  },
46
46
  "titleLang": {
@@ -103,14 +103,11 @@
103
103
  "js-controller": ">=6.0.11"
104
104
  }
105
105
  ],
106
- "globalDependencies": {
106
+ "globalDependencies": [
107
+ {
107
108
  "admin": ">=7.6.17"
108
- },
109
- "support": {
110
- "donate": {
111
- "paypal": "https://www.paypal.com/donate?business=dirk.bertin%40t-online.de"
112
- }
113
- }
109
+ }
110
+ ]
114
111
  },
115
112
  "native": {
116
113
  "option1": true,
@@ -92,6 +92,15 @@ const pumpHelper = {
92
92
  return;
93
93
  }
94
94
 
95
+ // Saisonprüfung
96
+ const season = (await this.adapter.getStateAsync('status.season_active'))?.val;
97
+ if (!season) {
98
+ this.adapter.log.debug(
99
+ '[pumpHelper] Saison inaktiv – Pumpenlogik übersprungen (Frostschutz läuft separat)',
100
+ );
101
+ return;
102
+ }
103
+
95
104
  // 1) Leistung aus Fremd-State spiegeln
96
105
  if (this.currentPowerId && id === this.currentPowerId) {
97
106
  const val = this._parseNumber(state.val);
@@ -32,17 +32,14 @@ const solarHelper = {
32
32
 
33
33
  async _checkSolar() {
34
34
  try {
35
+ // --- NEU: Saisonstatus ---
36
+ const season = (await this.adapter.getStateAsync('status.season_active'))?.val;
37
+
35
38
  // Solarsteuerung aktiv?
36
39
  const active = (await this.adapter.getStateAsync('solar.solar_control_active'))?.val;
37
- if (!active) {
38
- return;
39
- }
40
40
 
41
- // Pumpenmodus muss AUTO sein
41
+ // Pumpenmodus
42
42
  const mode = (await this.adapter.getStateAsync('pump.mode'))?.val;
43
- if (mode !== 'auto') {
44
- return;
45
- }
46
43
 
47
44
  // Grenzwerte laden
48
45
  const tempOn = (await this.adapter.getStateAsync('solar.temp_on'))?.val;
@@ -58,32 +55,45 @@ const solarHelper = {
58
55
  return;
59
56
  }
60
57
 
61
- let shouldRun = false;
62
- const delta = collector - pool;
58
+ // --- Schaltlogik nur ausführen, wenn Saison aktiv, Solar aktiv und Modus AUTO ---
59
+ if (season && active && mode === 'auto') {
60
+ let shouldRun = false;
61
+ const delta = collector - pool;
63
62
 
64
- // Logik: Einschalten, wenn Collector > tempOn und Delta > 0
65
- if (collector >= tempOn && delta > 0) {
66
- shouldRun = true;
67
- }
63
+ // Logik: Einschalten, wenn Collector > tempOn und Delta > 0
64
+ if (collector >= tempOn && delta > 0) {
65
+ shouldRun = true;
66
+ }
68
67
 
69
- // Ausschalten, wenn Collector < tempOff oder Delta <= 0
70
- if (collector <= tempOff || delta <= 0) {
71
- shouldRun = false;
72
- }
68
+ // Ausschalten, wenn Collector < tempOff oder Delta <= 0
69
+ if (collector <= tempOff || delta <= 0) {
70
+ shouldRun = false;
71
+ }
73
72
 
74
- // Optional Hysterese (kann später erweitert werden)
75
- if (hysteresis) {
76
- // z. B. Ausschaltgrenze etwas absenken
77
- }
73
+ // Optional Hysterese (kann später erweitert werden)
74
+ if (hysteresis) {
75
+ // z. B. Ausschaltgrenze etwas absenken
76
+ }
78
77
 
79
- // ZENTRAL: Pumpe über Bool-Schalter setzen
80
- await this.adapter.setStateAsync('pump.pump_switch', {
81
- val: shouldRun,
82
- ack: false,
83
- });
84
- this.adapter.log.debug(
85
- `[solarHelper] Solarregelung → Pumpe ${shouldRun ? 'EIN' : 'AUS'} (Collector=${collector}°C, Pool=${pool}°C, Delta=${delta}°C)`,
86
- );
78
+ // ZENTRAL: Pumpe über Bool-Schalter setzen
79
+ await this.adapter.setStateAsync('pump.pump_switch', {
80
+ val: shouldRun,
81
+ ack: false,
82
+ });
83
+ this.adapter.log.debug(
84
+ `[solarHelper] Solarregelung → Pumpe ${shouldRun ? 'EIN' : 'AUS'} (Collector=${collector}°C, Pool=${pool}°C, Delta=${delta}°C)`,
85
+ );
86
+ } else {
87
+ // Keine Schaltung – Grund protokollieren
88
+ const reason = !season
89
+ ? 'Saison inaktiv'
90
+ : !active
91
+ ? 'Solarsteuerung aus'
92
+ : mode !== 'auto'
93
+ ? 'Pumpenmodus != auto'
94
+ : 'unbekannt';
95
+ this.adapter.log.debug(`[solarHelper] Solarregelung übersprungen (${reason})`);
96
+ }
87
97
 
88
98
  // --- Kollektor-Warnung ---
89
99
  const warnActive = (await this.adapter.getStateAsync('solar.warn_active'))?.val;
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * speechHelper
5
- * - Sendet Texte an Alexa und/oder Telegram
5
+ * - Sendet Texte an Alexa, Telegram und optional per E-Mail
6
6
  * - Verwendet Config (jsonConfig) + States aus speechStates.js
7
7
  */
8
8
 
@@ -95,6 +95,24 @@ const speechHelper = {
95
95
  });
96
96
  this.adapter.log.info(`[speechHelper] Telegram sendet: ${text}`);
97
97
  }
98
+
99
+ // E-Mail-Ausgabe
100
+ if (this.adapter.config.speech_email_enabled && this.adapter.config.speech_email_instance) {
101
+ const instance = this.adapter.config.speech_email_instance;
102
+ const sendState = `${instance}.mail`;
103
+
104
+ await this.adapter.setForeignStateAsync(sendState, {
105
+ val: {
106
+ to: this.adapter.config.speech_email_recipient,
107
+ subject: this.adapter.config.speech_email_subject || 'PoolControl Nachricht',
108
+ text: text,
109
+ },
110
+ ack: false,
111
+ });
112
+ this.adapter.log.info(
113
+ `[speechHelper] E-Mail gesendet an ${this.adapter.config.speech_email_recipient}: ${text}`,
114
+ );
115
+ }
98
116
  } catch (err) {
99
117
  this.adapter.log.warn(`[speechHelper] Fehler beim Sprechen: ${err.message}`);
100
118
  }
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * statusHelper
5
+ * - Erstellt Text- und JSON-Zusammenfassungen
6
+ * - Schreibt Pumpenstart/Stop-Zeiten, Zähler, Indikatoren
7
+ * - Hält systemweite OK/Warning-Flags aktuell
8
+ * - Führt Tagesreset um Mitternacht durch
9
+ */
10
+
11
+ const statusHelper = {
12
+ adapter: null,
13
+ midnightTimer: null,
14
+ pumpOn: null, // interner Merker für Pumpenstatus
15
+
16
+ init(adapter) {
17
+ this.adapter = adapter;
18
+
19
+ // Relevante States überwachen
20
+ this.adapter.subscribeStates('pump.status');
21
+ this.adapter.subscribeStates('pump.mode');
22
+ this.adapter.subscribeStates('pump.pump_switch'); // wichtig für Start/Stop
23
+ this.adapter.subscribeStates('temperature.surface.current');
24
+ this.adapter.subscribeStates('temperature.collector.current');
25
+ this.adapter.subscribeStates('temperature.outside.current');
26
+ this.adapter.subscribeStates('runtime.today');
27
+ this.adapter.subscribeStates('circulation.daily_total');
28
+ this.adapter.subscribeStates('circulation.daily_required');
29
+
30
+ // System-Warnungen überwachen
31
+ this.adapter.subscribeStates('solar.collector_warning');
32
+ this.adapter.subscribeStates('pump.error');
33
+
34
+ // Aktuellen Pumpenstatus laden
35
+ this.adapter
36
+ .getStateAsync('pump.pump_switch')
37
+ .then(s => {
38
+ this.pumpOn = !!s?.val;
39
+ })
40
+ .catch(() => {
41
+ this.pumpOn = false;
42
+ });
43
+
44
+ // Beim Start initiale Updates
45
+ this.updateSummary().catch(err =>
46
+ this.adapter.log.warn(`[statusHelper] Initial-Update fehlgeschlagen: ${err.message}`),
47
+ );
48
+ this.updateSystemStatus().catch(err =>
49
+ this.adapter.log.warn(`[statusHelper] Initial-Systemstatus fehlgeschlagen: ${err.message}`),
50
+ );
51
+
52
+ // Mitternacht-Reset einplanen
53
+ this.scheduleMidnightReset();
54
+
55
+ this.adapter.log.info('[statusHelper] initialisiert');
56
+ },
57
+
58
+ async handleStateChange(id, state) {
59
+ if (!state || state.ack !== true) {
60
+ return;
61
+ }
62
+
63
+ // Pumpenstart/-stop
64
+ if (id.endsWith('pump.pump_switch')) {
65
+ const nowOn = !!state.val;
66
+
67
+ // steigende Flanke
68
+ if (nowOn && this.pumpOn !== true) {
69
+ await this.adapter.setStateAsync('status.pump_last_start', {
70
+ val: new Date().toISOString(),
71
+ ack: true,
72
+ });
73
+ const currentCount = (await this.adapter.getStateAsync('status.pump_today_count'))?.val || 0;
74
+ await this.adapter.setStateAsync('status.pump_today_count', { val: currentCount + 1, ack: true });
75
+ await this.adapter.setStateAsync('status.pump_was_on_today', { val: true, ack: true });
76
+ this.pumpOn = true;
77
+ }
78
+
79
+ // fallende Flanke
80
+ if (!nowOn && this.pumpOn !== false) {
81
+ await this.adapter.setStateAsync('status.pump_last_stop', {
82
+ val: new Date().toISOString(),
83
+ ack: true,
84
+ });
85
+ this.pumpOn = false;
86
+ }
87
+ }
88
+
89
+ // System-Warnungen
90
+ if (id.endsWith('solar.collector_warning') || id.endsWith('pump.error')) {
91
+ await this.updateSystemStatus();
92
+ }
93
+
94
+ // Allgemeines Update
95
+ await this.updateSummary();
96
+ },
97
+
98
+ async updateSummary() {
99
+ try {
100
+ // Werte laden
101
+ const pumpStatus = (await this.adapter.getStateAsync('pump.status'))?.val || 'unbekannt';
102
+ const pumpMode = (await this.adapter.getStateAsync('pump.mode'))?.val || 'unknown';
103
+
104
+ const poolTemp = (await this.adapter.getStateAsync('temperature.surface.current'))?.val;
105
+ const collectorTemp = (await this.adapter.getStateAsync('temperature.collector.current'))?.val;
106
+ const outsideTemp = (await this.adapter.getStateAsync('temperature.outside.current'))?.val;
107
+
108
+ const runtimeToday = (await this.adapter.getStateAsync('runtime.today'))?.val || 0;
109
+ const dailyTotal = (await this.adapter.getStateAsync('circulation.daily_total'))?.val || 0;
110
+ const dailyRequired = (await this.adapter.getStateAsync('circulation.daily_required'))?.val || 0;
111
+
112
+ // Laufzeit formatieren
113
+ const h = Math.floor(runtimeToday / 3600);
114
+ const m = Math.floor((runtimeToday % 3600) / 60);
115
+ const runtimeFormatted = `${h}h ${m}m`;
116
+
117
+ // Umwälzungsquote
118
+ let circulationPct = 0;
119
+ if (dailyRequired > 0) {
120
+ circulationPct = Math.round((dailyTotal / dailyRequired) * 100);
121
+ }
122
+
123
+ // Text bauen
124
+ let text = `Pumpe: ${pumpStatus}`;
125
+ if (pumpMode && pumpMode !== 'unknown') {
126
+ text += ` (Modus: ${pumpMode})`;
127
+ }
128
+ if (poolTemp != null) {
129
+ text += `. Pool: ${poolTemp.toFixed(1)} °C`;
130
+ }
131
+ if (collectorTemp != null) {
132
+ text += `, Kollektor: ${collectorTemp.toFixed(1)} °C`;
133
+ }
134
+ if (outsideTemp != null) {
135
+ text += `, Außentemperatur: ${outsideTemp.toFixed(1)} °C`;
136
+ }
137
+ text += `. Tageslaufzeit: ${runtimeFormatted} (${circulationPct}% der Soll-Umwälzung).`;
138
+
139
+ // In States schreiben
140
+ await this.adapter.setStateAsync('status.summary', { val: text, ack: true });
141
+ await this.adapter.setStateAsync('status.last_summary_update', {
142
+ val: new Date().toISOString(),
143
+ ack: true,
144
+ });
145
+
146
+ // JSON-Übersicht bauen
147
+ const json = {
148
+ pump: pumpStatus,
149
+ mode: pumpMode,
150
+ pool: poolTemp,
151
+ collector: collectorTemp,
152
+ outside: outsideTemp,
153
+ runtime_today: runtimeToday,
154
+ runtime_formatted: runtimeFormatted,
155
+ circulation_pct: circulationPct,
156
+ };
157
+ await this.adapter.setStateAsync('status.overview_json', {
158
+ val: JSON.stringify(json),
159
+ ack: true,
160
+ });
161
+ } catch (err) {
162
+ this.adapter.log.warn(`[statusHelper] Fehler beim Update: ${err.message}`);
163
+ }
164
+ },
165
+
166
+ async updateSystemStatus() {
167
+ try {
168
+ const pumpError = (await this.adapter.getStateAsync('pump.error'))?.val;
169
+ const collectorWarning = (await this.adapter.getStateAsync('solar.collector_warning'))?.val;
170
+
171
+ let warningActive = false;
172
+ let warningText = '';
173
+
174
+ if (pumpError) {
175
+ warningActive = true;
176
+ warningText += 'Pumpenfehler ';
177
+ }
178
+ if (collectorWarning) {
179
+ warningActive = true;
180
+ warningText += 'Kollektorwarnung ';
181
+ }
182
+
183
+ await this.adapter.setStateAsync('status.system_warning', { val: warningActive, ack: true });
184
+ await this.adapter.setStateAsync('status.system_warning_text', { val: warningText.trim(), ack: true });
185
+ await this.adapter.setStateAsync('status.system_ok', { val: !warningActive, ack: true });
186
+ } catch (err) {
187
+ this.adapter.log.warn(`[statusHelper] Fehler beim Systemstatus: ${err.message}`);
188
+ }
189
+ },
190
+
191
+ scheduleMidnightReset() {
192
+ if (this.midnightTimer) {
193
+ clearTimeout(this.midnightTimer);
194
+ }
195
+
196
+ const now = new Date();
197
+ const nextMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 5, 0);
198
+ const msToMidnight = nextMidnight.getTime() - now.getTime();
199
+
200
+ this.midnightTimer = setTimeout(async () => {
201
+ await this.doMidnightReset();
202
+ this.scheduleMidnightReset(); // neu einplanen
203
+ }, msToMidnight);
204
+
205
+ this.adapter.log.debug(`[statusHelper] Tagesreset geplant in ${Math.round(msToMidnight / 1000)}s`);
206
+ },
207
+
208
+ async doMidnightReset() {
209
+ try {
210
+ await this.adapter.setStateAsync('status.pump_today_count', { val: 0, ack: true });
211
+ await this.adapter.setStateAsync('status.pump_was_on_today', { val: false, ack: true });
212
+ this.adapter.log.info('[statusHelper] Tagesreset durchgeführt');
213
+ } catch (err) {
214
+ this.adapter.log.warn(`[statusHelper] Fehler beim Tagesreset: ${err.message}`);
215
+ }
216
+ },
217
+
218
+ cleanup() {
219
+ if (this.midnightTimer) {
220
+ clearTimeout(this.midnightTimer);
221
+ this.midnightTimer = null;
222
+ }
223
+ },
224
+ };
225
+
226
+ module.exports = statusHelper;
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Legt alle States für Status-Übersichten an:
5
+ * - status.summary (string, Textzusammenfassung)
6
+ * - status.overview_json (string, JSON-Zusammenfassung)
7
+ * - status.last_summary_update (string, Zeitstempel)
8
+ * - status.pump_last_start (string, Zeitstempel)
9
+ * - status.pump_last_stop (string, Zeitstempel)
10
+ * - status.pump_was_on_today (boolean)
11
+ * - status.pump_today_count (number)
12
+ * - status.system_ok (boolean)
13
+ * - status.system_warning (boolean)
14
+ * - status.system_warning_text (string)
15
+ * - status.season_active (boolean)
16
+ *
17
+ * @param {import("iobroker").Adapter} adapter - ioBroker Adapter-Instanz
18
+ */
19
+ async function createStatusStates(adapter) {
20
+ // Root-Kanal "status"
21
+ await adapter.setObjectNotExistsAsync('status', {
22
+ type: 'channel',
23
+ common: { name: 'Statusübersicht' },
24
+ native: {},
25
+ });
26
+
27
+ // Zusammenfassung als Text
28
+ await adapter.setObjectNotExistsAsync('status.summary', {
29
+ type: 'state',
30
+ common: {
31
+ name: 'Zusammenfassung als Text',
32
+ type: 'string',
33
+ role: 'text',
34
+ read: true,
35
+ write: false,
36
+ // bewusst kein persist: true, da nur Live-Daten
37
+ },
38
+ native: {},
39
+ });
40
+ await adapter.setStateAsync('status.summary', { val: '', ack: true });
41
+
42
+ // JSON-Zusammenfassung
43
+ await adapter.setObjectNotExistsAsync('status.overview_json', {
44
+ type: 'state',
45
+ common: {
46
+ name: 'Übersicht als JSON',
47
+ type: 'string',
48
+ role: 'json',
49
+ read: true,
50
+ write: false,
51
+ },
52
+ native: {},
53
+ });
54
+ await adapter.setStateAsync('status.overview_json', { val: '{}', ack: true });
55
+
56
+ // Letzte Aktualisierung der Summary
57
+ await adapter.setObjectNotExistsAsync('status.last_summary_update', {
58
+ type: 'state',
59
+ common: {
60
+ name: 'Letzte Aktualisierung der Zusammenfassung',
61
+ type: 'string',
62
+ role: 'date',
63
+ read: true,
64
+ write: false,
65
+ },
66
+ native: {},
67
+ });
68
+ await adapter.setStateAsync('status.last_summary_update', { val: '', ack: true });
69
+
70
+ // Pumpen-Status: letzter Start
71
+ await adapter.setObjectNotExistsAsync('status.pump_last_start', {
72
+ type: 'state',
73
+ common: {
74
+ name: 'Letzter Pumpenstart',
75
+ type: 'string',
76
+ role: 'date',
77
+ read: true,
78
+ write: false,
79
+ },
80
+ native: {},
81
+ });
82
+ await adapter.setStateAsync('status.pump_last_start', { val: '', ack: true });
83
+
84
+ // Pumpen-Status: letzter Stopp
85
+ await adapter.setObjectNotExistsAsync('status.pump_last_stop', {
86
+ type: 'state',
87
+ common: {
88
+ name: 'Letztes Pumpenende',
89
+ type: 'string',
90
+ role: 'date',
91
+ read: true,
92
+ write: false,
93
+ },
94
+ native: {},
95
+ });
96
+ await adapter.setStateAsync('status.pump_last_stop', { val: '', ack: true });
97
+
98
+ // Pumpen-Status: heute eingeschaltet
99
+ await adapter.setObjectNotExistsAsync('status.pump_was_on_today', {
100
+ type: 'state',
101
+ common: {
102
+ name: 'Pumpe war heute eingeschaltet',
103
+ type: 'boolean',
104
+ role: 'indicator',
105
+ read: true,
106
+ write: false,
107
+ },
108
+ native: {},
109
+ });
110
+ await adapter.setStateAsync('status.pump_was_on_today', { val: false, ack: true });
111
+
112
+ // Pumpen-Status: Anzahl Starts heute
113
+ await adapter.setObjectNotExistsAsync('status.pump_today_count', {
114
+ type: 'state',
115
+ common: {
116
+ name: 'Pumpenstarts heute',
117
+ type: 'number',
118
+ role: 'value',
119
+ read: true,
120
+ write: false,
121
+ },
122
+ native: {},
123
+ });
124
+ await adapter.setStateAsync('status.pump_today_count', { val: 0, ack: true });
125
+
126
+ // Systemstatus: OK
127
+ await adapter.setObjectNotExistsAsync('status.system_ok', {
128
+ type: 'state',
129
+ common: {
130
+ name: 'System OK',
131
+ type: 'boolean',
132
+ role: 'indicator',
133
+ read: true,
134
+ write: false,
135
+ },
136
+ native: {},
137
+ });
138
+ await adapter.setStateAsync('status.system_ok', { val: true, ack: true });
139
+
140
+ // Systemstatus: Warnung aktiv
141
+ await adapter.setObjectNotExistsAsync('status.system_warning', {
142
+ type: 'state',
143
+ common: {
144
+ name: 'System-Warnung aktiv',
145
+ type: 'boolean',
146
+ role: 'indicator',
147
+ read: true,
148
+ write: false,
149
+ },
150
+ native: {},
151
+ });
152
+ await adapter.setStateAsync('status.system_warning', { val: false, ack: true });
153
+
154
+ // Systemstatus: Warnungstext
155
+ await adapter.setObjectNotExistsAsync('status.system_warning_text', {
156
+ type: 'state',
157
+ common: {
158
+ name: 'Beschreibung der Systemwarnung',
159
+ type: 'string',
160
+ role: 'text',
161
+ read: true,
162
+ write: false,
163
+ },
164
+ native: {},
165
+ });
166
+ await adapter.setStateAsync('status.system_warning_text', { val: '', ack: true });
167
+
168
+ // Saisonstatus
169
+ await adapter.setObjectNotExistsAsync('status.season_active', {
170
+ type: 'state',
171
+ common: {
172
+ name: 'Poolsaison aktiv',
173
+ type: 'boolean',
174
+ role: 'switch',
175
+ read: true,
176
+ write: true,
177
+ },
178
+ native: {},
179
+ });
180
+ await adapter.setStateAsync('status.season_active', { val: false, ack: true });
181
+ }
182
+
183
+ module.exports = {
184
+ createStatusStates,
185
+ };
package/main.js CHANGED
@@ -13,6 +13,7 @@ const speechHelper = require('./lib/helpers/speechHelper');
13
13
  const consumptionHelper = require('./lib/helpers/consumptionHelper');
14
14
  const solarHelper = require('./lib/helpers/solarHelper');
15
15
  const frostHelper = require('./lib/helpers/frostHelper');
16
+ const statusHelper = require('./lib/helpers/statusHelper');
16
17
  const { createTemperatureStates } = require('./lib/stateDefinitions/temperatureStates');
17
18
  const { createPumpStates } = require('./lib/stateDefinitions/pumpStates');
18
19
  const { createSolarStates } = require('./lib/stateDefinitions/solarStates');
@@ -21,6 +22,7 @@ const { createTimeStates } = require('./lib/stateDefinitions/timeStates');
21
22
  const { createRuntimeStates } = require('./lib/stateDefinitions/runtimeStates');
22
23
  const { createSpeechStates } = require('./lib/stateDefinitions/speechStates');
23
24
  const { createConsumptionStates } = require('./lib/stateDefinitions/consumptionStates');
25
+ const { createStatusStates } = require('./lib/stateDefinitions/statusStates');
24
26
 
25
27
  class Poolcontrol extends utils.Adapter {
26
28
  constructor(options) {
@@ -60,6 +62,15 @@ class Poolcontrol extends utils.Adapter {
60
62
  // --- Verbrauch & Kosten ---
61
63
  await createConsumptionStates(this);
62
64
 
65
+ // --- Statusübersicht ---
66
+ await createStatusStates(this);
67
+
68
+ // Saisonstatus aus Config übernehmen
69
+ await this.setStateAsync('status.season_active', {
70
+ val: this.config.season_active,
71
+ ack: true,
72
+ });
73
+
63
74
  // --- Helper starten ---
64
75
  temperatureHelper.init(this);
65
76
  timeHelper.init(this);
@@ -69,6 +80,7 @@ class Poolcontrol extends utils.Adapter {
69
80
  consumptionHelper.init(this);
70
81
  solarHelper.init(this);
71
82
  frostHelper.init(this);
83
+ statusHelper.init(this);
72
84
  }
73
85
 
74
86
  onUnload(callback) {
@@ -97,6 +109,9 @@ class Poolcontrol extends utils.Adapter {
97
109
  if (frostHelper.cleanup) {
98
110
  frostHelper.cleanup();
99
111
  }
112
+ if (statusHelper.cleanup) {
113
+ statusHelper.cleanup();
114
+ }
100
115
  } catch (e) {
101
116
  this.log.warn(`[onUnload] Fehler beim Cleanup: ${e.message}`);
102
117
  } finally {
@@ -104,12 +119,20 @@ class Poolcontrol extends utils.Adapter {
104
119
  }
105
120
  }
106
121
 
107
- onStateChange(id, state) {
122
+ async onStateChange(id, state) {
108
123
  if (state) {
109
124
  this.log.debug(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
110
125
  } else {
111
126
  this.log.debug(`state ${id} deleted`);
112
127
  }
128
+
129
+ // Saisonstatus manuell ändern (z.B. über VIS)
130
+ if (id.endsWith('status.season_active') && state && state.ack === false) {
131
+ this.log.info(`[main] Saisonstatus geändert: ${state.val}`);
132
+ await this.setStateAsync('status.season_active', { val: state.val, ack: true });
133
+ return; // danach keine Helper mehr aufrufen
134
+ }
135
+
113
136
  try {
114
137
  temperatureHelper.handleStateChange(id, state);
115
138
  } catch (e) {
@@ -135,6 +158,11 @@ class Poolcontrol extends utils.Adapter {
135
158
  } catch (e) {
136
159
  this.log.warn(`[consumptionHelper] Fehler in handleStateChange: ${e.message}`);
137
160
  }
161
+ try {
162
+ statusHelper.handleStateChange(id, state);
163
+ } catch (e) {
164
+ this.log.warn(`[statusHelper] Fehler in handleStateChange: ${e.message}`);
165
+ }
138
166
  }
139
167
  }
140
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.poolcontrol",
3
- "version": "0.0.9",
3
+ "version": "0.1.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",
@@ -27,14 +27,9 @@
27
27
  "@iobroker/adapter-dev": "^1.5.0",
28
28
  "@iobroker/eslint-config": "^2.2.0",
29
29
  "@iobroker/testing": "^5.1.1",
30
- "chai": "^4.5.0",
31
- "chai-as-promised": "^7.1.2",
32
30
  "eslint": "^9.36.0",
33
- "mocha": "^11.7.2",
34
31
  "prettier": "^3.6.2",
35
- "proxyquire": "^2.1.3",
36
- "sinon": "^21.0.0",
37
- "sinon-chai": "^3.7.0"
32
+ "proxyquire": "^2.1.3"
38
33
  },
39
34
  "main": "main.js",
40
35
  "files": [