iobroker.poolcontrol 0.7.2 → 0.7.3
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/io-package.json +14 -13
- package/lib/helpers/aiHelper.js +916 -0
- package/lib/helpers/infoHelper.js +1 -1
- package/lib/stateDefinitions/aiStates.js +135 -0
- package/main.js +15 -0
- package/package.json +2 -1
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "poolcontrol",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.7.3": {
|
|
7
|
+
"en": "Fix in seasonal greetings: Christmas greeting now only appears between December 20 and 27. Incorrect early activation has been corrected.",
|
|
8
|
+
"de": "Fix bei den saisonalen Grüßen: Weihnachtsgruß erscheint nun korrekt nur vom 20. bis 27. Dezember. Fehlerhafte frühzeitige Aktivierung wurde behoben.",
|
|
9
|
+
"ru": "Исправление сезонных поздравлений: Рождественское поздравление теперь отображается только с 20 по 27 декабря. Ошибочное раннее срабатывание исправлено.",
|
|
10
|
+
"pt": "Correção nos cumprimentos sazonais: A saudação de Natal agora aparece corretamente apenas entre 20 e 27 de dezembro. Ativação incorreta antecipada foi corrigida.",
|
|
11
|
+
"nl": "Fix in seizoensgroeten: Kerstgroet verschijnt nu correct alleen van 20 t/m 27 december. Onjuiste vroege activering is verholpen.",
|
|
12
|
+
"fr": "Correction des messages saisonniers : le message de Noël apparaît désormais uniquement du 20 au 27 décembre. L’activation prématurée incorrecte a été corrigée.",
|
|
13
|
+
"it": "Correzione nei saluti stagionali: l’augurio di Natale ora appare correttamente solo dal 20 al 27 dicembre. Corretto il problema dell'attivazione anticipata.",
|
|
14
|
+
"es": "Corrección en los saludos estacionales: el mensaje de Navidad ahora aparece correctamente solo del 20 al 27 de diciembre. Se corrigió la activación anticipada incorrecta.",
|
|
15
|
+
"pl": "Poprawka pozdrowień sezonowych: życzenia świąteczne pojawiają się teraz prawidłowo tylko między 20 a 27 grudnia. Błędna przedwczesna aktywacja została naprawiona.",
|
|
16
|
+
"uk": "Виправлено сезонні вітання: Різдвяне привітання тепер з’являється лише з 20 по 27 грудня. Помилкове передчасне спрацьовування виправлено.",
|
|
17
|
+
"zh-cn": "修复季节性问候:圣诞问候现在仅在 12 月 20 日至 27 日之间显示。已修复错误的提前触发问题。"
|
|
18
|
+
},
|
|
6
19
|
"0.7.2": {
|
|
7
20
|
"en": "Added new info system: The adapter now writes seasonal greetings and the installed adapter version to info.* states. Includes automatic daily refresh at 00:01 and full Easter date calculation.",
|
|
8
21
|
"de": "Neues Info-System hinzugefügt: Der Adapter schreibt nun saisonale Grüße und die installierte Adapterversion in die info.* States. Enthält eine automatische tägliche Aktualisierung um 00:01 sowie eine vollständige Oster-Berechnung.",
|
|
@@ -80,18 +93,6 @@
|
|
|
80
93
|
"pl": "Poprawka cotygodniowego, comiesięcznego i corocznego resetu w consumptionHelper",
|
|
81
94
|
"uk": "Виправлення щотижневого, щомісячного та щорічного скидання в consumptionHelper",
|
|
82
95
|
"zh-cn": "修复 consumptionHelper 中每周、每月和每年重置的问题"
|
|
83
|
-
},
|
|
84
|
-
"0.6.2": {
|
|
85
|
-
"en": "UI refinement and admin interface improvements. Added image integration ('Egon in blue overalls') for visual identification. Speech system extended with Alexa output time configuration. Cleaned and optimized jsonConfig with section headers for improved clarity.",
|
|
86
|
-
"de": "Oberflächenüberarbeitung und Verbesserungen der Admin-Ansicht. Bildintegration ('Egon im Blaumann') zur visuellen Wiedererkennung hinzugefügt. Sprachsystem um konfigurierbare Alexa-Ausgabezeiten erweitert. jsonConfig mit Abschnittsüberschriften bereinigt und übersichtlicher gestaltet.",
|
|
87
|
-
"ru": "Улучшение интерфейса и доработка административной панели. Добавлено изображение ('Эгон в синем комбинезоне') для визуальной идентификации. Расширена система речи с настройкой времени вывода Alexa. Очищена и оптимизирована jsonConfig с заголовками разделов для лучшей читаемости.",
|
|
88
|
-
"pt": "Aprimoramento da interface e melhorias na administração. Adicionada imagem ('Egon de macacão azul') para identificação visual. Sistema de fala ampliado com configuração de horários de saída Alexa. jsonConfig limpa e otimizada com cabeçalhos de seção para melhor clareza.",
|
|
89
|
-
"nl": "UI verfijnd en verbeteringen in het beheerpaneel. Afbeelding ('Egon in blauwe overall') toegevoegd voor visuele herkenning. Spraaksysteem uitgebreid met Alexa-uitvoertijdaanpassing. jsonConfig opgeschoond en overzichtelijker gemaakt met sectiekoppen.",
|
|
90
|
-
"fr": "Amélioration de l'interface utilisateur et du panneau d'administration. Image ajoutée ('Egon en bleu de travail') pour identification visuelle. Système vocal étendu avec configuration des heures de sortie Alexa. jsonConfig nettoyé et optimisé avec en-têtes de section pour plus de clarté.",
|
|
91
|
-
"it": "Raffinamento dell'interfaccia e miglioramenti al pannello di amministrazione. Aggiunta l'immagine ('Egon in tuta blu') per l'identificazione visiva. Sistema vocale ampliato con configurazione degli orari di uscita Alexa. jsonConfig ripulito e ottimizzato con intestazioni di sezione per maggiore chiarezza.",
|
|
92
|
-
"es": "Refinamiento de la interfaz y mejoras en el panel de administración. Se agregó imagen ('Egon con mono azul') para identificación visual. Sistema de voz ampliado con configuración de horarios de salida de Alexa. jsonConfig limpiado y optimizado con encabezados de sección para mayor claridad.",
|
|
93
|
-
"pl": "Udoskonalenie interfejsu i panelu administracyjnego. Dodano obraz ('Egon w niebieskim kombinezonie') dla wizualnej identyfikacji. Rozszerzono system mowy o konfigurację czasu wyjścia Alexa. Wyczyczono i zoptymalizowano jsonConfig z nagłówkami sekcji dla lepszej przejrzystości.",
|
|
94
|
-
"zh-cn": "改进了界面和管理界面。添加了图像('蓝色工作服的Egon')以实现视觉识别。语音系统扩展了Alexa输出时间配置。清理并优化了带有章节标题的jsonConfig,使其更清晰。"
|
|
95
96
|
}
|
|
96
97
|
},
|
|
97
98
|
"titleLang": {
|
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* eslint-disable jsdoc/require-param-description */
|
|
3
|
+
/* eslint-disable jsdoc/require-returns-description */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* aiHelper
|
|
7
|
+
* --------------------------------------------------------------
|
|
8
|
+
* Zentraler KI-Helper für PoolControl.
|
|
9
|
+
*
|
|
10
|
+
* Nutzt die States aus aiStates.js:
|
|
11
|
+
* ai.switches.*
|
|
12
|
+
* ai.schedule.*
|
|
13
|
+
* ai.outputs.*
|
|
14
|
+
*
|
|
15
|
+
* Funktionen:
|
|
16
|
+
* - Liest Geodaten aus system.config (Latitude/Longitude)
|
|
17
|
+
* - Ruft Wetterdaten von Open-Meteo ab (bei Bedarf, max. 4x/Tag – je Modul)
|
|
18
|
+
* - Erzeugt Textausgaben:
|
|
19
|
+
* ai.outputs.weather_advice
|
|
20
|
+
* ai.outputs.daily_summary
|
|
21
|
+
* ai.outputs.pool_tips
|
|
22
|
+
* ai.outputs.weekend_summary
|
|
23
|
+
* ai.outputs.last_message
|
|
24
|
+
* - Optional: legt Texte in speech.queue für Sprachausgabe
|
|
25
|
+
*
|
|
26
|
+
* Wichtige Schalter:
|
|
27
|
+
* - ai.switches.enabled → globaler KI-Schalter
|
|
28
|
+
* - ai.switches.allow_speech → Sprachausgabe erlaubt
|
|
29
|
+
* - ai.switches.weather_advice_enabled
|
|
30
|
+
* - ai.switches.daily_summary_enabled
|
|
31
|
+
* - ai.switches.daily_pool_tips_enabled
|
|
32
|
+
* - ai.switches.weekend_summary_enabled
|
|
33
|
+
* - ai.switches.debug_mode
|
|
34
|
+
*
|
|
35
|
+
* Zeitsteuerung (HH:MM, lokal):
|
|
36
|
+
* - ai.schedule.weather_advice_time
|
|
37
|
+
* - ai.schedule.daily_summary_time
|
|
38
|
+
* - ai.schedule.daily_pool_tips_time
|
|
39
|
+
* - ai.schedule.weekend_summary_time
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const https = require('https');
|
|
43
|
+
|
|
44
|
+
const aiHelper = {
|
|
45
|
+
adapter: null,
|
|
46
|
+
timers: [],
|
|
47
|
+
_lastScheduleValues: {}, // NEU: merkt sich letzte Zeitwerte
|
|
48
|
+
_debugMode: false,
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialisiert den AI-Helper (Timer + Grundkonfiguration).
|
|
52
|
+
*
|
|
53
|
+
* @param {import('iobroker').Adapter} adapter
|
|
54
|
+
*/
|
|
55
|
+
async init(adapter) {
|
|
56
|
+
this.adapter = adapter;
|
|
57
|
+
this.adapter.log.info('[aiHelper] Initialisierung gestartet');
|
|
58
|
+
|
|
59
|
+
// Ersten Settings-Load + Timeraufbau
|
|
60
|
+
await this._refreshTimers();
|
|
61
|
+
|
|
62
|
+
this.adapter.log.info('[aiHelper] Initialisierung abgeschlossen');
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Aufräumen beim Adapter-Stop.
|
|
67
|
+
*/
|
|
68
|
+
cleanup() {
|
|
69
|
+
this._clearTimers();
|
|
70
|
+
this.adapter && this.adapter.log.debug('[aiHelper] Cleanup abgeschlossen (Timer gestoppt)');
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reagiert auf State-Änderungen (ai.switches.*, ai.schedule.*),
|
|
75
|
+
* damit Schalter und Zeiten ohne Neustart wirksam werden.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} id
|
|
78
|
+
* @param {ioBroker.State | null} state
|
|
79
|
+
*/
|
|
80
|
+
async handleStateChange(id, state) {
|
|
81
|
+
if (!this.adapter) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!state) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Änderungen vom Adapter selbst ignorieren
|
|
89
|
+
if (state.from && state.from.startsWith(`system.adapter.${this.adapter.name}.`)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------
|
|
94
|
+
// FIX 1: Nur AI-States weiterverarbeiten
|
|
95
|
+
// ---------------------------------------------------------
|
|
96
|
+
if (!id.includes('.ai.')) {
|
|
97
|
+
// Alle Nicht-AI-States ignorieren → verhindert Log-Flut
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------
|
|
102
|
+
// FIX 2: Uhrzeitänderungen IMMER erkennen und loggen
|
|
103
|
+
// ---------------------------------------------------------
|
|
104
|
+
if (id.includes('.ai.schedule.')) {
|
|
105
|
+
const oldVal = this._lastScheduleValues[id];
|
|
106
|
+
const newVal = state.val;
|
|
107
|
+
|
|
108
|
+
this.adapter.log.info(
|
|
109
|
+
`[aiHelper] Uhrzeit geändert: ${id}: ${oldVal || '(kein vorheriger Wert)'} → ${newVal}`,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// neuen Wert speichern
|
|
113
|
+
this._lastScheduleValues[id] = newVal;
|
|
114
|
+
|
|
115
|
+
// Timer neu aufbauen
|
|
116
|
+
await this._refreshTimers();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------
|
|
121
|
+
// FIX 3: Schalteränderungen immer melden
|
|
122
|
+
// ---------------------------------------------------------
|
|
123
|
+
if (id.includes('.ai.switches.')) {
|
|
124
|
+
this.adapter.log.info(`[aiHelper] Schalter geändert: ${id} = ${state.val} – Timer werden neu aufgebaut`);
|
|
125
|
+
|
|
126
|
+
await this._refreshTimers();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------
|
|
132
|
+
// Timer-Verwaltung
|
|
133
|
+
// ---------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Bestehende Timer stoppen.
|
|
137
|
+
*/
|
|
138
|
+
_clearTimers() {
|
|
139
|
+
for (const t of this.timers) {
|
|
140
|
+
clearInterval(t);
|
|
141
|
+
}
|
|
142
|
+
this.timers = [];
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Liest alle relevanten States und baut die Timer neu auf.
|
|
147
|
+
*/
|
|
148
|
+
async _refreshTimers() {
|
|
149
|
+
this._clearTimers();
|
|
150
|
+
|
|
151
|
+
const aiEnabled = await this._getBool('ai.switches.enabled', false);
|
|
152
|
+
this._debugMode = await this._getBool('ai.switches.debug_mode', false);
|
|
153
|
+
|
|
154
|
+
if (!aiEnabled) {
|
|
155
|
+
this.adapter.log.info('[aiHelper] KI ist deaktiviert (ai.switches.enabled = false) – keine Timer aktiv');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.adapter.log.info('[aiHelper] KI ist aktiv – Timer werden gesetzt');
|
|
160
|
+
|
|
161
|
+
// --- Wetterhinweise ---
|
|
162
|
+
const weatherEnabled = await this._getBool('ai.switches.weather_advice_enabled', false);
|
|
163
|
+
if (weatherEnabled) {
|
|
164
|
+
const time = await this._getTimeOrDefault('ai.schedule.weather_advice_time', '08:00');
|
|
165
|
+
this._createDailyTimer(time, async () => {
|
|
166
|
+
await this._runWeatherAdvice();
|
|
167
|
+
});
|
|
168
|
+
this.adapter.log.info(
|
|
169
|
+
`[aiHelper] Wetterhinweis-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Tägliche Zusammenfassung ---
|
|
174
|
+
const summaryEnabled = await this._getBool('ai.switches.daily_summary_enabled', false);
|
|
175
|
+
if (summaryEnabled) {
|
|
176
|
+
const time = await this._getTimeOrDefault('ai.schedule.daily_summary_time', '09:00');
|
|
177
|
+
this._createDailyTimer(time, async () => {
|
|
178
|
+
await this._runDailySummary();
|
|
179
|
+
});
|
|
180
|
+
this.adapter.log.info(
|
|
181
|
+
`[aiHelper] Daily-Summary-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Tägliche Pool-Tipps ---
|
|
186
|
+
const tipsEnabled = await this._getBool('ai.switches.daily_pool_tips_enabled', false);
|
|
187
|
+
if (tipsEnabled) {
|
|
188
|
+
const time = await this._getTimeOrDefault('ai.schedule.daily_pool_tips_time', '10:00');
|
|
189
|
+
this._createDailyTimer(time, async () => {
|
|
190
|
+
await this._runDailyPoolTips();
|
|
191
|
+
});
|
|
192
|
+
this.adapter.log.info(
|
|
193
|
+
`[aiHelper] Pool-Tipps-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Wochenend-Zusammenfassung ---
|
|
198
|
+
const weekendEnabled = await this._getBool('ai.switches.weekend_summary_enabled', false);
|
|
199
|
+
if (weekendEnabled) {
|
|
200
|
+
const time = await this._getTimeOrDefault('ai.schedule.weekend_summary_time', '18:00');
|
|
201
|
+
this._createDailyTimer(time, async () => {
|
|
202
|
+
await this._runWeekendSummary();
|
|
203
|
+
});
|
|
204
|
+
this.adapter.log.info(
|
|
205
|
+
`[aiHelper] Wochenend-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Erzeugt einen täglichen Timer für HH:MM (lokale Zeit).
|
|
212
|
+
* Prüft minütlich, ob die Zeit erreicht ist.
|
|
213
|
+
*
|
|
214
|
+
* @param {{hour:number,minute:number}} timeObj
|
|
215
|
+
* @param {() => Promise<void>} callback
|
|
216
|
+
*/
|
|
217
|
+
_createDailyTimer(timeObj, callback) {
|
|
218
|
+
const { hour, minute } = timeObj;
|
|
219
|
+
const timer = setInterval(async () => {
|
|
220
|
+
const now = new Date();
|
|
221
|
+
|
|
222
|
+
// --- NEU: Nachträgliche Ausführung, falls Zeit knapp verpasst wurde ---
|
|
223
|
+
const diffMinutes = now.getHours() * 60 + now.getMinutes() - (hour * 60 + minute);
|
|
224
|
+
|
|
225
|
+
if (diffMinutes > 0 && diffMinutes <= 2) {
|
|
226
|
+
try {
|
|
227
|
+
await callback();
|
|
228
|
+
this.adapter.log.info('[aiHelper] Zeit knapp verpasst – nachträgliche Ausführung durchgeführt');
|
|
229
|
+
} catch (err) {
|
|
230
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei nachträglicher Ausführung: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (now.getHours() === hour && now.getMinutes() === minute) {
|
|
235
|
+
this.adapter.log.info(
|
|
236
|
+
`[aiHelper] Timer ausgelöst: ${hour}:${String(minute).padStart(2, '0')} → Callback wird ausgeführt`,
|
|
237
|
+
); // NEU
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await callback();
|
|
241
|
+
} catch (err) {
|
|
242
|
+
this.adapter.log.warn(`[aiHelper] Fehler im Timer-Callback: ${err.message}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}, 60 * 1000); // jede Minute prüfen
|
|
246
|
+
|
|
247
|
+
this.timers.push(timer);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------
|
|
251
|
+
// Hauptfunktionen – Module
|
|
252
|
+
// ---------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 1) Wetterhinweise (ai.outputs.weather_advice)
|
|
256
|
+
*/
|
|
257
|
+
async _runWeatherAdvice() {
|
|
258
|
+
try {
|
|
259
|
+
const geo = await this._loadGeoLocation();
|
|
260
|
+
if (!geo) {
|
|
261
|
+
this.adapter.log.info('[aiHelper] Wetterhinweis abgebrochen – keine Geodaten verfügbar');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const weather = await this._fetchWeather(geo.lat, geo.lon);
|
|
266
|
+
if (!weather) {
|
|
267
|
+
this.adapter.log.info('[aiHelper] Wetterhinweis abgebrochen – keine Wetterdaten');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const text = this._buildWeatherAdviceText(weather);
|
|
272
|
+
await this._writeOutput('weather_advice', text);
|
|
273
|
+
await this._maybeSpeak(text);
|
|
274
|
+
|
|
275
|
+
this.adapter.log.info('[aiHelper] Neuer Wetterhinweis erzeugt');
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei _runWeatherAdvice(): ${err.message}`);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 2) Tägliche Zusammenfassung (ai.outputs.daily_summary)
|
|
283
|
+
*/
|
|
284
|
+
async _runDailySummary() {
|
|
285
|
+
try {
|
|
286
|
+
const geo = await this._loadGeoLocation();
|
|
287
|
+
const weather = geo ? await this._fetchWeather(geo.lat, geo.lon) : null;
|
|
288
|
+
|
|
289
|
+
const seasonActive = await this._getBool('status.season_active', false);
|
|
290
|
+
const pumpOn = await this._getBool('pump.pump_switch', false);
|
|
291
|
+
const pumpMode = await this._getString('pump.mode', 'auto');
|
|
292
|
+
const surfaceTemp = await this._getNumber('temperature.surface.current', null);
|
|
293
|
+
|
|
294
|
+
const text = this._buildDailySummaryText({
|
|
295
|
+
weather,
|
|
296
|
+
seasonActive,
|
|
297
|
+
pumpOn,
|
|
298
|
+
pumpMode,
|
|
299
|
+
surfaceTemp,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await this._writeOutput('daily_summary', text);
|
|
303
|
+
await this._maybeSpeak(text);
|
|
304
|
+
|
|
305
|
+
this.adapter.log.info('[aiHelper] Neue Tageszusammenfassung erzeugt');
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei _runDailySummary(): ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 3) Tägliche Pool-Tipps (ai.outputs.pool_tips)
|
|
313
|
+
*/
|
|
314
|
+
async _runDailyPoolTips() {
|
|
315
|
+
try {
|
|
316
|
+
const geo = await this._loadGeoLocation();
|
|
317
|
+
const weather = geo ? await this._fetchWeather(geo.lat, geo.lon) : null;
|
|
318
|
+
const seasonActive = await this._getBool('status.season_active', false);
|
|
319
|
+
|
|
320
|
+
const text = this._buildPoolTipsText(weather, seasonActive);
|
|
321
|
+
|
|
322
|
+
await this._writeOutput('pool_tips', text);
|
|
323
|
+
await this._maybeSpeak(text);
|
|
324
|
+
|
|
325
|
+
this.adapter.log.info('[aiHelper] Neue Pool-Tipps erzeugt');
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei _runDailyPoolTips(): ${err.message}`);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 4) Wochenend-Zusammenfassung (ai.outputs.weekend_summary)
|
|
333
|
+
*/
|
|
334
|
+
async _runWeekendSummary() {
|
|
335
|
+
try {
|
|
336
|
+
const now = new Date();
|
|
337
|
+
const weekday = now.getDay(); // 0=So, 1=Mo, ..., 5=Fr, 6=Sa
|
|
338
|
+
|
|
339
|
+
// Nur Freitag oder Samstag sinnvoll
|
|
340
|
+
if (weekday !== 5 && weekday !== 6) {
|
|
341
|
+
this.adapter.log.info(
|
|
342
|
+
'[aiHelper] Wochenend-Zusammenfassung übersprungen – heute ist weder Freitag noch Samstag',
|
|
343
|
+
);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const geo = await this._loadGeoLocation();
|
|
348
|
+
const weather = geo ? await this._fetchWeather(geo.lat, geo.lon) : null;
|
|
349
|
+
const seasonActive = await this._getBool('status.season_active', false);
|
|
350
|
+
|
|
351
|
+
const text = this._buildWeekendSummaryText(weather, seasonActive, weekday);
|
|
352
|
+
|
|
353
|
+
await this._writeOutput('weekend_summary', text);
|
|
354
|
+
await this._maybeSpeak(text);
|
|
355
|
+
|
|
356
|
+
this.adapter.log.info('[aiHelper] Neue Wochenend-Zusammenfassung erzeugt');
|
|
357
|
+
} catch (err) {
|
|
358
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei _runWeekendSummary(): ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------
|
|
363
|
+
// Geodaten + Wetter
|
|
364
|
+
// ---------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Lädt Geokoordinaten aus system.config.
|
|
368
|
+
*
|
|
369
|
+
* @returns {{lat:number,lon:number}|null}
|
|
370
|
+
*/
|
|
371
|
+
async _loadGeoLocation() {
|
|
372
|
+
try {
|
|
373
|
+
const latState = await this.adapter.getForeignStateAsync('system.config.latitude');
|
|
374
|
+
const lonState = await this.adapter.getForeignStateAsync('system.config.longitude');
|
|
375
|
+
|
|
376
|
+
if (!latState || !lonState || latState.val == null || lonState.val == null) {
|
|
377
|
+
this.adapter.log.warn(
|
|
378
|
+
'[aiHelper] Geodaten in system.config nicht gesetzt – bitte im Admin konfigurieren',
|
|
379
|
+
);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const lat = Number(latState.val);
|
|
384
|
+
const lon = Number(lonState.val);
|
|
385
|
+
|
|
386
|
+
if (Number.isNaN(lat) || Number.isNaN(lon)) {
|
|
387
|
+
this.adapter.log.warn('[aiHelper] Geodaten ungültig – latitude/longitude sind keine Zahlen');
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (this._debugMode) {
|
|
392
|
+
this.adapter.log.debug(`[aiHelper] Geodaten geladen: lat=${lat}, lon=${lon}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { lat, lon };
|
|
396
|
+
} catch (err) {
|
|
397
|
+
this.adapter.log.error(`[aiHelper] Fehler beim Laden der Geodaten: ${err.message}`);
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Ruft Wetterdaten von Open-Meteo ab.
|
|
404
|
+
*
|
|
405
|
+
* @param {number} lat
|
|
406
|
+
* @param {number} lon
|
|
407
|
+
* @returns {Promise<any|null>}
|
|
408
|
+
*/
|
|
409
|
+
async _fetchWeather(lat, lon) {
|
|
410
|
+
const url =
|
|
411
|
+
`https://api.open-meteo.com/v1/forecast` +
|
|
412
|
+
`?latitude=${encodeURIComponent(lat)}` +
|
|
413
|
+
`&longitude=${encodeURIComponent(lon)}` +
|
|
414
|
+
`¤t=temperature_2m,wind_speed_10m` +
|
|
415
|
+
`&daily=temperature_2m_max,temperature_2m_min,weathercode` +
|
|
416
|
+
`&timezone=auto`;
|
|
417
|
+
|
|
418
|
+
if (this._debugMode) {
|
|
419
|
+
this.adapter.log.debug(`[aiHelper] Rufe Wetterdaten ab: ${url}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return new Promise(resolve => {
|
|
423
|
+
try {
|
|
424
|
+
https
|
|
425
|
+
.get(url, res => {
|
|
426
|
+
let data = '';
|
|
427
|
+
res.on('data', chunk => {
|
|
428
|
+
data += chunk;
|
|
429
|
+
});
|
|
430
|
+
res.on('end', () => {
|
|
431
|
+
try {
|
|
432
|
+
if (!data) {
|
|
433
|
+
this.adapter.log.warn('[aiHelper] Wetterabfrage: leere Antwort erhalten');
|
|
434
|
+
return resolve(null);
|
|
435
|
+
}
|
|
436
|
+
const json = JSON.parse(data);
|
|
437
|
+
resolve(json);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
this.adapter.log.warn(`[aiHelper] Fehler beim Parsen der Wetterdaten: ${err.message}`);
|
|
440
|
+
resolve(null);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
})
|
|
444
|
+
.on('error', err => {
|
|
445
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei Wetterabfrage: ${err.message}`);
|
|
446
|
+
resolve(null);
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
this.adapter.log.warn(`[aiHelper] Unerwarteter Fehler bei Wetterabfrage: ${err.message}`);
|
|
450
|
+
resolve(null);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// ---------------------------------------------------------------------
|
|
456
|
+
// Textgeneratoren
|
|
457
|
+
// ---------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Erzeugt einen gut lesbaren Wetterhinweis-Text.
|
|
461
|
+
*
|
|
462
|
+
* @param {any} weather
|
|
463
|
+
* @returns {string}
|
|
464
|
+
*/
|
|
465
|
+
_buildWeatherAdviceText(weather) {
|
|
466
|
+
try {
|
|
467
|
+
const tmax = this._safeArrayValue(weather?.daily?.temperature_2m_max, 0);
|
|
468
|
+
const tmin = this._safeArrayValue(weather?.daily?.temperature_2m_min, 0);
|
|
469
|
+
const code = this._safeArrayValue(weather?.daily?.weathercode, 0);
|
|
470
|
+
const desc = this._describeWeatherCode(code);
|
|
471
|
+
|
|
472
|
+
let text = 'Wetterhinweis für heute: ';
|
|
473
|
+
|
|
474
|
+
if (tmax != null && tmin != null) {
|
|
475
|
+
text += `zwischen ${tmin.toFixed(1)} °C und ${tmax.toFixed(1)} °C, `;
|
|
476
|
+
} else if (tmax != null) {
|
|
477
|
+
text += `bis maximal ${tmax.toFixed(1)} °C, `;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
text += desc ? `${desc}.` : 'genaue Wetterlage konnte nicht bestimmt werden.';
|
|
481
|
+
|
|
482
|
+
return text;
|
|
483
|
+
} catch {
|
|
484
|
+
return 'Wetterhinweis: Die aktuellen Wetterdaten konnten nicht ausgewertet werden.';
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Erzeugt die Tageszusammenfassung.
|
|
490
|
+
*
|
|
491
|
+
* @param {{weather:any,seasonActive:boolean,pumpOn:boolean,pumpMode:string,surfaceTemp:number|null}} ctx
|
|
492
|
+
* @returns {string}
|
|
493
|
+
*/
|
|
494
|
+
_buildDailySummaryText(ctx) {
|
|
495
|
+
const { weather, seasonActive, pumpOn, pumpMode, surfaceTemp } = ctx || {};
|
|
496
|
+
|
|
497
|
+
let parts = [];
|
|
498
|
+
|
|
499
|
+
// Saisonstatus
|
|
500
|
+
if (seasonActive) {
|
|
501
|
+
parts.push('Die Poolsaison ist aktuell AKTIV.');
|
|
502
|
+
} else {
|
|
503
|
+
parts.push('Die Poolsaison ist aktuell NICHT aktiv.');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Pumpenstatus
|
|
507
|
+
if (pumpOn) {
|
|
508
|
+
parts.push(`Die Pumpe ist derzeit EIN (Modus: ${pumpMode || 'unbekannt'}).`);
|
|
509
|
+
} else {
|
|
510
|
+
parts.push(`Die Pumpe ist derzeit AUS (Modus: ${pumpMode || 'unbekannt'}).`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Temperatur
|
|
514
|
+
if (surfaceTemp != null && !Number.isNaN(surfaceTemp)) {
|
|
515
|
+
parts.push(`Die gemessene Wassertemperatur an der Oberfläche beträgt etwa ${surfaceTemp.toFixed(1)} °C.`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Wetterteil
|
|
519
|
+
if (weather) {
|
|
520
|
+
const tmax = this._safeArrayValue(weather?.daily?.temperature_2m_max, 0);
|
|
521
|
+
const tmin = this._safeArrayValue(weather?.daily?.temperature_2m_min, 0);
|
|
522
|
+
const code = this._safeArrayValue(weather?.daily?.weathercode, 0);
|
|
523
|
+
const desc = this._describeWeatherCode(code);
|
|
524
|
+
|
|
525
|
+
let w = 'Für heute sind ';
|
|
526
|
+
if (tmax != null && tmin != null) {
|
|
527
|
+
w += `Temperaturen zwischen ${tmin.toFixed(1)} °C und ${tmax.toFixed(1)} °C vorhergesagt`;
|
|
528
|
+
} else if (tmax != null) {
|
|
529
|
+
w += `Temperaturen bis etwa ${tmax.toFixed(1)} °C vorhergesagt`;
|
|
530
|
+
} else {
|
|
531
|
+
w += 'keine genauen Temperaturdaten verfügbar';
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (desc) {
|
|
535
|
+
w += `, bei einer Wetterlage: ${desc}.`;
|
|
536
|
+
} else {
|
|
537
|
+
w += '.';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
parts.push(w);
|
|
541
|
+
} else {
|
|
542
|
+
parts.push('Aktuelle Wetterdaten stehen derzeit nicht zur Verfügung.');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return parts.join(' ');
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Erzeugt tägliche Pool-Tipps abhängig von Wetter & Saison.
|
|
550
|
+
*
|
|
551
|
+
* @param {any} weather
|
|
552
|
+
* @param {boolean} seasonActive
|
|
553
|
+
* @returns {string}
|
|
554
|
+
*/
|
|
555
|
+
_buildPoolTipsText(weather, seasonActive) {
|
|
556
|
+
if (!seasonActive) {
|
|
557
|
+
return 'Poolsaison ist aktuell nicht aktiv. Es sind keine speziellen Pool-Tipps notwendig.';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const tmax = this._safeArrayValue(weather?.daily?.temperature_2m_max, 0);
|
|
561
|
+
const code = this._safeArrayValue(weather?.daily?.weathercode, 0);
|
|
562
|
+
const desc = this._describeWeatherCode(code);
|
|
563
|
+
|
|
564
|
+
if (tmax == null) {
|
|
565
|
+
return 'Pool-Tipp: Es liegen keine Temperaturdaten vor. Bitte Poolbetrieb nach eigenem Gefühl planen.';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let text = 'Pool-Tipp für heute: ';
|
|
569
|
+
|
|
570
|
+
if (tmax >= 26) {
|
|
571
|
+
text += 'Es wird warm bis sehr warm – gutes Badewetter. ';
|
|
572
|
+
text +=
|
|
573
|
+
'Die Pumpe kann tagsüber etwas länger laufen, und die Abdeckung sollte bei Sonnenschein geöffnet werden. ';
|
|
574
|
+
} else if (tmax >= 20) {
|
|
575
|
+
text += 'Es wird mild bis angenehm. ';
|
|
576
|
+
text += 'Eine normale Umwälzzeit reicht meist aus. Abdeckung nur bei Bedarf schließen. ';
|
|
577
|
+
} else {
|
|
578
|
+
text += 'Es bleibt eher kühl. ';
|
|
579
|
+
text +=
|
|
580
|
+
'Die Umwälzzeit kann auf das Minimum reduziert werden, und eine Abdeckung hilft, Wärmeverluste zu vermeiden. ';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (desc && /regen|schauer|gewitter|sturm/i.test(desc)) {
|
|
584
|
+
text +=
|
|
585
|
+
'Achtung: Es ist mit Regen oder stärkerem Wind zu rechnen – Abdeckung bereit halten und Zubehör sichern.';
|
|
586
|
+
} else if (desc && /sonnig|klar/i.test(desc)) {
|
|
587
|
+
text += 'Bei sonniger Witterung steigt der Chlorverbrauch – Wasserwerte im Auge behalten.';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return text;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Erzeugt Wochenend-Zusammenfassung (Samstag/Sonntag).
|
|
595
|
+
*
|
|
596
|
+
* @param {any} weather
|
|
597
|
+
* @param {boolean} seasonActive
|
|
598
|
+
* @param {number} weekday JS-Tag (0=So..6=Sa)
|
|
599
|
+
* @returns {string}
|
|
600
|
+
*/
|
|
601
|
+
_buildWeekendSummaryText(weather, seasonActive, weekday) {
|
|
602
|
+
if (!weather) {
|
|
603
|
+
return 'Wochenendübersicht: Es stehen keine Wetterdaten zur Verfügung.';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const tmaxArr = weather?.daily?.temperature_2m_max || [];
|
|
607
|
+
const tminArr = weather?.daily?.temperature_2m_min || [];
|
|
608
|
+
const codeArr = weather?.daily?.weathercode || [];
|
|
609
|
+
|
|
610
|
+
// Indizes für Samstag/Sonntag bestimmen
|
|
611
|
+
let idxSat = null;
|
|
612
|
+
let idxSun = null;
|
|
613
|
+
|
|
614
|
+
if (weekday === 5) {
|
|
615
|
+
// Freitag → morgen Samstag, übermorgen Sonntag
|
|
616
|
+
idxSat = 1;
|
|
617
|
+
idxSun = 2;
|
|
618
|
+
} else if (weekday === 6) {
|
|
619
|
+
// Samstag → heute Samstag, morgen Sonntag
|
|
620
|
+
idxSat = 0;
|
|
621
|
+
idxSun = 1;
|
|
622
|
+
} else {
|
|
623
|
+
// Fallback: nächste zwei Tage
|
|
624
|
+
idxSat = 1;
|
|
625
|
+
idxSun = 2;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const satMax = this._safeArrayValue(tmaxArr, idxSat);
|
|
629
|
+
const satMin = this._safeArrayValue(tminArr, idxSat);
|
|
630
|
+
const satCode = this._safeArrayValue(codeArr, idxSat);
|
|
631
|
+
const satDesc = this._describeWeatherCode(satCode);
|
|
632
|
+
|
|
633
|
+
const sunMax = this._safeArrayValue(tmaxArr, idxSun);
|
|
634
|
+
const sunMin = this._safeArrayValue(tminArr, idxSun);
|
|
635
|
+
const sunCode = this._safeArrayValue(codeArr, idxSun);
|
|
636
|
+
const sunDesc = this._describeWeatherCode(sunCode);
|
|
637
|
+
|
|
638
|
+
let text = 'Wochenendübersicht: ';
|
|
639
|
+
|
|
640
|
+
text += 'Samstag: ';
|
|
641
|
+
if (satMax != null && satMin != null) {
|
|
642
|
+
text += `zwischen ${satMin.toFixed(1)} °C und ${satMax.toFixed(1)} °C`;
|
|
643
|
+
} else if (satMax != null) {
|
|
644
|
+
text += `bis etwa ${satMax.toFixed(1)} °C`;
|
|
645
|
+
} else {
|
|
646
|
+
text += 'keine Temperaturdaten';
|
|
647
|
+
}
|
|
648
|
+
if (satDesc) {
|
|
649
|
+
text += `, Wetter: ${satDesc}. `;
|
|
650
|
+
} else {
|
|
651
|
+
text += '. ';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
text += 'Sonntag: ';
|
|
655
|
+
if (sunMax != null && sunMin != null) {
|
|
656
|
+
text += `zwischen ${sunMin.toFixed(1)} °C und ${sunMax.toFixed(1)} °C`;
|
|
657
|
+
} else if (sunMax != null) {
|
|
658
|
+
text += `bis etwa ${sunMax.toFixed(1)} °C`;
|
|
659
|
+
} else {
|
|
660
|
+
text += 'keine Temperaturdaten';
|
|
661
|
+
}
|
|
662
|
+
if (sunDesc) {
|
|
663
|
+
text += `, Wetter: ${sunDesc}. `;
|
|
664
|
+
} else {
|
|
665
|
+
text += '. ';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (seasonActive) {
|
|
669
|
+
text += 'Für das Wochenende bietet sich je nach Temperaturentwicklung ein angepasster Poolbetrieb an.';
|
|
670
|
+
} else {
|
|
671
|
+
text +=
|
|
672
|
+
'Die Poolsaison ist aktuell nicht aktiv – das Wochenende eignet sich eher zur Planung oder Wartung.';
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return text;
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Konvertiert Open-Meteo weathercode in eine deutsche Beschreibung.
|
|
680
|
+
*
|
|
681
|
+
* @param {number|null} code
|
|
682
|
+
* @returns {string}
|
|
683
|
+
*/
|
|
684
|
+
_describeWeatherCode(code) {
|
|
685
|
+
if (code == null || Number.isNaN(code)) {
|
|
686
|
+
return '';
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Quelle: Open-Meteo Wettercodes (vereinfachte Gruppierung)
|
|
690
|
+
if (code === 0) {
|
|
691
|
+
return 'klarer, sonniger Himmel';
|
|
692
|
+
}
|
|
693
|
+
if (code === 1) {
|
|
694
|
+
return 'überwiegend sonnig mit wenigen Wolken';
|
|
695
|
+
}
|
|
696
|
+
if (code === 2) {
|
|
697
|
+
return 'wechselhaft bewölkt';
|
|
698
|
+
}
|
|
699
|
+
if (code === 3) {
|
|
700
|
+
return 'bedeckter Himmel';
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (code === 45 || code === 48) {
|
|
704
|
+
return 'Nebel oder Hochnebel';
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (code === 51 || code === 53 || code === 55) {
|
|
708
|
+
return 'leichter bis mäßiger Nieselregen';
|
|
709
|
+
}
|
|
710
|
+
if (code === 56 || code === 57) {
|
|
711
|
+
return 'gefrierender Nieselregen';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (code === 61 || code === 63 || code === 65) {
|
|
715
|
+
return 'leichter bis kräftiger Regen';
|
|
716
|
+
}
|
|
717
|
+
if (code === 66 || code === 67) {
|
|
718
|
+
return 'gefrierender Regen';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (code === 71 || code === 73 || code === 75) {
|
|
722
|
+
return 'leichter bis starker Schneefall';
|
|
723
|
+
}
|
|
724
|
+
if (code === 77) {
|
|
725
|
+
return 'Schneekörner';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (code === 80 || code === 81 || code === 82) {
|
|
729
|
+
return 'Regenschauer';
|
|
730
|
+
}
|
|
731
|
+
if (code === 85 || code === 86) {
|
|
732
|
+
return 'Schneeschauer';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (code === 95) {
|
|
736
|
+
return 'Gewitter';
|
|
737
|
+
}
|
|
738
|
+
if (code === 96 || code === 99) {
|
|
739
|
+
return 'Gewitter mit Hagel';
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return `Wettercode ${code}`;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
// ---------------------------------------------------------------------
|
|
746
|
+
// State-/Hilfsfunktionen
|
|
747
|
+
// ---------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Schreibt einen AI-Output-Text.
|
|
751
|
+
*
|
|
752
|
+
* @param {string} id
|
|
753
|
+
* @param {string} text
|
|
754
|
+
*/
|
|
755
|
+
async _writeOutput(id, text) {
|
|
756
|
+
if (!this.adapter) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (!text) {
|
|
760
|
+
text = 'Keine Textausgabe verfügbar.';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
await this.adapter.setStateAsync(`ai.outputs.${id}`, { val: text, ack: true });
|
|
765
|
+
await this.adapter.setStateAsync('ai.outputs.last_message', { val: text, ack: true });
|
|
766
|
+
|
|
767
|
+
if (this._debugMode) {
|
|
768
|
+
this.adapter.log.info(`[aiHelper] Output geschrieben → ai.outputs.${id}: ${text}`);
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
this.adapter.log.error(`[aiHelper] Fehler beim Schreiben eines Outputs (${id}): ${err.message}`);
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Optional: sendet Text an speech.queue, wenn erlaubt.
|
|
777
|
+
*
|
|
778
|
+
* @param {string} text
|
|
779
|
+
*/
|
|
780
|
+
async _maybeSpeak(text) {
|
|
781
|
+
if (!this.adapter) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (!text) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const allowSpeech = await this._getBool('ai.switches.allow_speech', false);
|
|
789
|
+
if (!allowSpeech) {
|
|
790
|
+
if (this._debugMode) {
|
|
791
|
+
this.adapter.log.debug('[aiHelper] Sprachausgabe deaktiviert (ai.switches.allow_speech = false)');
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
await this.adapter.setStateAsync('speech.queue', { val: text, ack: false });
|
|
798
|
+
this.adapter.log.info('[aiHelper] Text an speech.queue übergeben');
|
|
799
|
+
} catch (err) {
|
|
800
|
+
this.adapter.log.warn(`[aiHelper] Fehler bei Sprachausgabe: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Liest einen Bool-State.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} id
|
|
808
|
+
* @param {boolean} fallback
|
|
809
|
+
* @returns {Promise<boolean>}
|
|
810
|
+
*/
|
|
811
|
+
async _getBool(id, fallback) {
|
|
812
|
+
try {
|
|
813
|
+
const state = await this.adapter.getStateAsync(id);
|
|
814
|
+
if (!state || state.val == null) {
|
|
815
|
+
return fallback;
|
|
816
|
+
}
|
|
817
|
+
return !!state.val;
|
|
818
|
+
} catch {
|
|
819
|
+
return fallback;
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Liest einen String-State.
|
|
825
|
+
*
|
|
826
|
+
* @param {string} id
|
|
827
|
+
* @param {string} fallback
|
|
828
|
+
* @returns {Promise<string>}
|
|
829
|
+
*/
|
|
830
|
+
async _getString(id, fallback) {
|
|
831
|
+
try {
|
|
832
|
+
const state = await this.adapter.getStateAsync(id);
|
|
833
|
+
if (!state || state.val == null) {
|
|
834
|
+
return fallback;
|
|
835
|
+
}
|
|
836
|
+
return String(state.val);
|
|
837
|
+
} catch {
|
|
838
|
+
return fallback;
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Liest einen Zahlen-State.
|
|
844
|
+
*
|
|
845
|
+
* @param {string} id
|
|
846
|
+
* @param {number|null} fallback
|
|
847
|
+
* @returns {Promise<number|null>}
|
|
848
|
+
*/
|
|
849
|
+
async _getNumber(id, fallback) {
|
|
850
|
+
try {
|
|
851
|
+
const state = await this.adapter.getStateAsync(id);
|
|
852
|
+
if (!state || state.val == null) {
|
|
853
|
+
return fallback;
|
|
854
|
+
}
|
|
855
|
+
const num = Number(state.val);
|
|
856
|
+
return Number.isNaN(num) ? fallback : num;
|
|
857
|
+
} catch {
|
|
858
|
+
return fallback;
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Liest Zeit-String HH:MM und liefert Objekt {hour,minute}.
|
|
864
|
+
*
|
|
865
|
+
* @param {string} id
|
|
866
|
+
* @param {string} def
|
|
867
|
+
* @returns {Promise<{hour:number,minute:number}>}
|
|
868
|
+
*/
|
|
869
|
+
async _getTimeOrDefault(id, def) {
|
|
870
|
+
const str = await this._getString(id, def);
|
|
871
|
+
|
|
872
|
+
// NEU: Log, welche Uhrzeit der Helper tatsächlich verwendet
|
|
873
|
+
this.adapter.log.info(`[aiHelper] Uhrzeit geladen: ${id} = "${str}" (Default: ${def})`);
|
|
874
|
+
|
|
875
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(str || '');
|
|
876
|
+
let hour = 0;
|
|
877
|
+
let minute = 0;
|
|
878
|
+
|
|
879
|
+
if (!match) {
|
|
880
|
+
this.adapter.log.warn(`[aiHelper] Ungültiges Zeitformat in ${id}: "${str}" – verwende Default ${def}`);
|
|
881
|
+
const defMatch = /^(\d{1,2}):(\d{2})$/.exec(def);
|
|
882
|
+
if (defMatch) {
|
|
883
|
+
hour = Number(defMatch[1]);
|
|
884
|
+
minute = Number(defMatch[2]);
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
hour = Math.min(Math.max(Number(match[1]), 0), 23);
|
|
888
|
+
minute = Math.min(Math.max(Number(match[2]), 0), 59);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return { hour, minute };
|
|
892
|
+
},
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Sicherer Zugriff auf ein Array-Element.
|
|
896
|
+
*
|
|
897
|
+
* @param {Array<number>|undefined} arr
|
|
898
|
+
* @param {number} idx
|
|
899
|
+
* @returns {number|null}
|
|
900
|
+
*/
|
|
901
|
+
_safeArrayValue(arr, idx) {
|
|
902
|
+
if (!Array.isArray(arr)) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
if (idx < 0 || idx >= arr.length) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const v = arr[idx];
|
|
909
|
+
if (v == null || Number.isNaN(Number(v))) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
return Number(v);
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
module.exports = aiHelper;
|
|
@@ -56,7 +56,7 @@ const infoHelper = {
|
|
|
56
56
|
let greeting = '';
|
|
57
57
|
|
|
58
58
|
// Weihnachten: 20.12. – 27.12.
|
|
59
|
-
if (
|
|
59
|
+
if (month === 12 && day >= 20 && day <= 27) {
|
|
60
60
|
greeting = '🎄 PoolControl wünscht frohe Weihnachten! 🎄';
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aiStates.js
|
|
5
|
+
* ----------------------------------------------------------
|
|
6
|
+
* Definiert alle States für den KI-Bereich des PoolControl-Adapters.
|
|
7
|
+
* Struktur:
|
|
8
|
+
* ai.switches.*
|
|
9
|
+
* ai.schedule.*
|
|
10
|
+
* ai.outputs.*
|
|
11
|
+
* ----------------------------------------------------------
|
|
12
|
+
*
|
|
13
|
+
* @param {import('iobroker').Adapter} adapter - ioBroker Adapterinstanz
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Erstellt alle KI-bezogenen States (ai.*)
|
|
18
|
+
*
|
|
19
|
+
* @param {import('iobroker').Adapter} adapter - ioBroker Adapterinstanz
|
|
20
|
+
*/
|
|
21
|
+
async function createAiStates(adapter) {
|
|
22
|
+
adapter.log.debug('[aiStates] Initialisierung gestartet');
|
|
23
|
+
|
|
24
|
+
// Hauptordner
|
|
25
|
+
await adapter.setObjectNotExistsAsync('ai', {
|
|
26
|
+
type: 'channel',
|
|
27
|
+
common: { name: 'KI / AI-Funktionen' },
|
|
28
|
+
native: {},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ------------------------------------------------------
|
|
32
|
+
// Unterordner: switches
|
|
33
|
+
// ------------------------------------------------------
|
|
34
|
+
await adapter.setObjectNotExistsAsync('ai.switches', {
|
|
35
|
+
type: 'channel',
|
|
36
|
+
common: { name: 'Schalter (KI-Steuerung)' },
|
|
37
|
+
native: {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const switches = [
|
|
41
|
+
{ id: 'enabled', name: 'KI aktivieren', def: false },
|
|
42
|
+
{ id: 'allow_speech', name: 'Sprachausgabe durch KI erlauben', def: false },
|
|
43
|
+
{ id: 'daily_summary_enabled', name: 'Tägliche Zusammenfassung aktiv', def: false },
|
|
44
|
+
{ id: 'daily_pool_tips_enabled', name: 'Tägliche Pool-Tipps aktiv', def: false },
|
|
45
|
+
{ id: 'weather_advice_enabled', name: 'Wetterhinweise aktiv', def: false },
|
|
46
|
+
{ id: 'weekend_summary_enabled', name: 'Wochenende-Zusammenfassung aktiv', def: false },
|
|
47
|
+
{ id: 'debug_mode', name: 'KI-Debugmodus', def: false },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const s of switches) {
|
|
51
|
+
await adapter.setObjectNotExistsAsync(`ai.switches.${s.id}`, {
|
|
52
|
+
type: 'state',
|
|
53
|
+
common: {
|
|
54
|
+
name: s.name,
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
role: 'switch',
|
|
57
|
+
read: true,
|
|
58
|
+
write: true,
|
|
59
|
+
def: s.def,
|
|
60
|
+
persist: true,
|
|
61
|
+
},
|
|
62
|
+
native: {},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------
|
|
67
|
+
// Unterordner: schedule
|
|
68
|
+
// ------------------------------------------------------
|
|
69
|
+
await adapter.setObjectNotExistsAsync('ai.schedule', {
|
|
70
|
+
type: 'channel',
|
|
71
|
+
common: { name: 'Zeitpläne (KI-Ausgaben)' },
|
|
72
|
+
native: {},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const schedule = [
|
|
76
|
+
{ id: 'daily_summary_time', name: 'Zeit für tägliche Zusammenfassung', def: '09:00' },
|
|
77
|
+
{ id: 'daily_pool_tips_time', name: 'Zeit für tägliche Pool-Tipps', def: '10:00' },
|
|
78
|
+
{ id: 'weather_advice_time', name: 'Zeit für Wetterhinweise', def: '08:00' },
|
|
79
|
+
{ id: 'weekend_summary_time', name: 'Zeit für Wochenend-Zusammenfassung', def: '18:00' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const t of schedule) {
|
|
83
|
+
await adapter.setObjectNotExistsAsync(`ai.schedule.${t.id}`, {
|
|
84
|
+
type: 'state',
|
|
85
|
+
common: {
|
|
86
|
+
name: t.name,
|
|
87
|
+
type: 'string',
|
|
88
|
+
role: 'text',
|
|
89
|
+
read: true,
|
|
90
|
+
write: true,
|
|
91
|
+
def: t.def,
|
|
92
|
+
persist: true,
|
|
93
|
+
},
|
|
94
|
+
native: {},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ------------------------------------------------------
|
|
99
|
+
// Unterordner: outputs
|
|
100
|
+
// ------------------------------------------------------
|
|
101
|
+
await adapter.setObjectNotExistsAsync('ai.outputs', {
|
|
102
|
+
type: 'channel',
|
|
103
|
+
common: { name: 'KI-Ausgaben (Texte)' },
|
|
104
|
+
native: {},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const outputs = [
|
|
108
|
+
{ id: 'daily_summary', name: 'Tägliche Zusammenfassung' },
|
|
109
|
+
{ id: 'pool_tips', name: 'Pool-Tipps' },
|
|
110
|
+
{ id: 'weather_advice', name: 'Wetterhinweise' },
|
|
111
|
+
{ id: 'weekend_summary', name: 'Wochenende-Zusammenfassung' },
|
|
112
|
+
{ id: 'last_message', name: 'Letzte KI-Meldung' },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const o of outputs) {
|
|
116
|
+
await adapter.setObjectNotExistsAsync(`ai.outputs.${o.id}`, {
|
|
117
|
+
type: 'state',
|
|
118
|
+
common: {
|
|
119
|
+
name: o.name,
|
|
120
|
+
type: 'string',
|
|
121
|
+
role: 'text',
|
|
122
|
+
read: true,
|
|
123
|
+
write: false,
|
|
124
|
+
persist: false,
|
|
125
|
+
},
|
|
126
|
+
native: {},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
adapter.log.debug('[aiStates] Initialisierung abgeschlossen');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
createAiStates,
|
|
135
|
+
};
|
package/main.js
CHANGED
|
@@ -21,6 +21,7 @@ const solarHelper = require('./lib/helpers/solarHelper');
|
|
|
21
21
|
const frostHelper = require('./lib/helpers/frostHelper');
|
|
22
22
|
const statusHelper = require('./lib/helpers/statusHelper');
|
|
23
23
|
const photovoltaicHelper = require('./lib/helpers/photovoltaicHelper');
|
|
24
|
+
const aiHelper = require('./lib/helpers/aiHelper');
|
|
24
25
|
const controlHelper = require('./lib/helpers/controlHelper');
|
|
25
26
|
const controlHelper2 = require('./lib/helpers/controlHelper2');
|
|
26
27
|
const debugLogHelper = require('./lib/helpers/debugLogHelper');
|
|
@@ -44,6 +45,7 @@ const { createStatusStates } = require('./lib/stateDefinitions/statusStates');
|
|
|
44
45
|
const { createControlStates } = require('./lib/stateDefinitions/controlStates');
|
|
45
46
|
const { createDebugLogStates } = require('./lib/stateDefinitions/debugLogStates');
|
|
46
47
|
const { createInfoStates } = require('./lib/stateDefinitions/infoStates');
|
|
48
|
+
const { createAiStates } = require('./lib/stateDefinitions/aiStates'); // NEU: KI-States
|
|
47
49
|
|
|
48
50
|
class Poolcontrol extends utils.Adapter {
|
|
49
51
|
constructor(options) {
|
|
@@ -110,6 +112,9 @@ class Poolcontrol extends utils.Adapter {
|
|
|
110
112
|
// --- Info States ---
|
|
111
113
|
await createInfoStates(this);
|
|
112
114
|
|
|
115
|
+
// --- AI States ---
|
|
116
|
+
await createAiStates(this); // NEU: KI-States anlegen
|
|
117
|
+
|
|
113
118
|
// --- Migration Helper zuletzt starten ---
|
|
114
119
|
await migrationHelper.init(this);
|
|
115
120
|
|
|
@@ -128,6 +133,7 @@ class Poolcontrol extends utils.Adapter {
|
|
|
128
133
|
consumptionHelper.init(this);
|
|
129
134
|
solarHelper.init(this);
|
|
130
135
|
photovoltaicHelper.init(this);
|
|
136
|
+
aiHelper.init(this);
|
|
131
137
|
frostHelper.init(this);
|
|
132
138
|
statusHelper.init(this);
|
|
133
139
|
infoHelper.init(this);
|
|
@@ -186,6 +192,9 @@ class Poolcontrol extends utils.Adapter {
|
|
|
186
192
|
}
|
|
187
193
|
if (speechTextHelper.cleanup) {
|
|
188
194
|
speechTextHelper.cleanup();
|
|
195
|
+
}
|
|
196
|
+
if (aiHelper.cleanup) {
|
|
197
|
+
aiHelper.cleanup();
|
|
189
198
|
}
|
|
190
199
|
if (infoHelper.cleanup) {
|
|
191
200
|
infoHelper.cleanup();
|
|
@@ -257,6 +266,12 @@ class Poolcontrol extends utils.Adapter {
|
|
|
257
266
|
photovoltaicHelper.handleStateChange(id, state);
|
|
258
267
|
} catch (e) {
|
|
259
268
|
this.log.warn(`[photovoltaicHelper] Fehler in handleStateChange: ${e.message}`);
|
|
269
|
+
}
|
|
270
|
+
// --- AI-Helper ---
|
|
271
|
+
try {
|
|
272
|
+
aiHelper.handleStateChange(id, state);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
this.log.warn(`[main] Fehler in aiHelper.handleStateChange: ${e.message}`);
|
|
260
275
|
}
|
|
261
276
|
try {
|
|
262
277
|
statusHelper.handleStateChange(id, state);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.poolcontrol",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
29
29
|
"@iobroker/eslint-config": "^2.2.0",
|
|
30
30
|
"@iobroker/testing": "^5.1.1",
|
|
31
|
+
"baseline-browser-mapping": "^2.8.32",
|
|
31
32
|
"eslint": "^9.36.0",
|
|
32
33
|
"prettier": "^3.6.2",
|
|
33
34
|
"proxyquire": "^2.1.3",
|