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 CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "0.7.2",
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
+ `&current=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 ((month === 12 && day >= 20) || (month === 12 && day <= 27)) {
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.2",
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",