iobroker.poolcontrol 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ 'use strict';
2
+ /* eslint-disable jsdoc/require-param-description */
3
+
4
+ /**
5
+ * aiForecastHelper
6
+ * --------------------------------------------------------------
7
+ * Erzeugt die „Vorhersage für morgen“.
8
+ *
9
+ * Nutzt folgende States:
10
+ * ai.weather.switches.tomorrow_forecast_enabled
11
+ * ai.weather.switches.allow_speech
12
+ * ai.weather.switches.debug_mode
13
+ *
14
+ * ai.weather.schedule.tomorrow_forecast_time
15
+ *
16
+ * ai.weather.outputs.tomorrow_forecast
17
+ *
18
+ * WICHTIG:
19
+ * Die Vorhersage wird NICHT in ai.weather.outputs.last_message geschrieben,
20
+ * damit wichtige Warnmeldungen nicht überschrieben werden.
21
+ */
22
+
23
+ const https = require('https');
24
+
25
+ const aiForecastHelper = {
26
+ adapter: null,
27
+ timer: null,
28
+ _debugMode: false,
29
+
30
+ /**
31
+ * Initialisiert den Forecast-Helper.
32
+ *
33
+ * @param {import('iobroker').Adapter} adapter
34
+ */
35
+ async init(adapter) {
36
+ this.adapter = adapter;
37
+ this.adapter.log.info('[aiForecastHelper] Initialisierung gestartet');
38
+
39
+ await this._refreshTimer();
40
+
41
+ // ----------------------------------------------------------
42
+ // NEU: Sofortige Ausführung beim Adapterstart (wenn aktiviert)
43
+ // ----------------------------------------------------------
44
+ const enabled = await this._getBool('ai.weather.switches.tomorrow_forecast_enabled', false);
45
+ if (enabled) {
46
+ this.adapter.log.info('[aiForecastHelper] Starte einmalige Sofort-Vorhersage (Adapterstart)');
47
+ try {
48
+ await this._runForecast();
49
+ } catch (err) {
50
+ this.adapter.log.warn(`[aiForecastHelper] Fehler bei Sofort-Vorhersage: ${err.message}`);
51
+ }
52
+ }
53
+
54
+ this.adapter.log.info('[aiForecastHelper] Initialisierung abgeschlossen');
55
+ },
56
+
57
+ /**
58
+ * Aufräumen beim Adapter-Stop.
59
+ */
60
+ cleanup() {
61
+ if (this.timer) {
62
+ clearInterval(this.timer);
63
+ this.timer = null;
64
+ }
65
+ this.adapter && this.adapter.log.debug('[aiForecastHelper] Cleanup abgeschlossen');
66
+ },
67
+
68
+ /**
69
+ * Reagiert auf State-Änderungen an switches + schedule.
70
+ *
71
+ * @param id
72
+ * @param state
73
+ */
74
+ async handleStateChange(id, state) {
75
+ if (!state || !this.adapter) {
76
+ return;
77
+ }
78
+
79
+ // Von Adapter selbst gesetzt? → ignorieren
80
+ if (state.from && state.from.startsWith(`system.adapter.${this.adapter.name}.`)) {
81
+ return;
82
+ }
83
+
84
+ // Nur Ai-Weather-spezifische Änderungen beachten
85
+ if (!id.includes('ai.weather.switches.') && !id.includes('ai.weather.schedule.')) {
86
+ return;
87
+ }
88
+
89
+ // ----------------------------------------------------------
90
+ // NEU: Sofortige Ausführung, wenn der Forecast aktiviert wird
91
+ // ----------------------------------------------------------
92
+ if (id.endsWith('ai.weather.switches.tomorrow_forecast_enabled') && state.val === true) {
93
+ this.adapter.log.info('[aiForecastHelper] Forecast aktiviert → einmalige sofortige Ausführung');
94
+ try {
95
+ await this._runForecast();
96
+ } catch (err) {
97
+ this.adapter.log.warn(`[aiForecastHelper] Fehler bei Sofort-Ausführung: ${err.message}`);
98
+ }
99
+ }
100
+
101
+ this.adapter.log.info(`[aiForecastHelper] Änderung erkannt: ${id} = ${state.val}`);
102
+ await this._refreshTimer();
103
+ },
104
+
105
+ // ---------------------------------------------------------------------
106
+ // TIMER-VERWALTUNG
107
+ // ---------------------------------------------------------------------
108
+ async _refreshTimer() {
109
+ // alten Timer stoppen
110
+ if (this.timer) {
111
+ clearInterval(this.timer);
112
+ this.timer = null;
113
+ }
114
+
115
+ const enabled = await this._getBool('ai.weather.switches.tomorrow_forecast_enabled', false);
116
+ this._debugMode = await this._getBool('ai.weather.switches.debug_mode', false);
117
+
118
+ if (!enabled) {
119
+ this.adapter.log.info('[aiForecastHelper] Forecast deaktiviert – kein Timer aktiv');
120
+ return;
121
+ }
122
+
123
+ const time = await this._getTimeOrDefault('ai.weather.schedule.tomorrow_forecast_time', '19:00');
124
+
125
+ this.adapter.log.info(
126
+ `[aiForecastHelper] Forecast-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
127
+ );
128
+
129
+ // minütlicher Check
130
+ this.timer = setInterval(async () => {
131
+ const now = new Date();
132
+ if (now.getHours() === time.hour && now.getMinutes() === time.minute) {
133
+ try {
134
+ await this._runForecast();
135
+ } catch (err) {
136
+ this.adapter.log.warn(`[aiForecastHelper] Fehler im Timer: ${err.message}`);
137
+ }
138
+ }
139
+ }, 60 * 1000);
140
+ },
141
+
142
+ // ---------------------------------------------------------------------
143
+ // HAUPTFUNKTION – VORHERSAGE ERZEUGEN
144
+ // ---------------------------------------------------------------------
145
+ async _runForecast() {
146
+ try {
147
+ this.adapter.log.info('[aiForecastHelper] Erzeuge Vorhersage für morgen ...');
148
+
149
+ const geo = await this._loadGeoLocation();
150
+ if (!geo) {
151
+ this.adapter.log.warn('[aiForecastHelper] Abbruch – keine Geodaten verfügbar');
152
+ return;
153
+ }
154
+
155
+ const weather = await this._fetchWeather(geo.lat, geo.lon);
156
+ if (!weather) {
157
+ this.adapter.log.warn('[aiForecastHelper] Abbruch – keine Wetterdaten');
158
+ return;
159
+ }
160
+
161
+ const text = this._buildForecastText(weather);
162
+
163
+ await this._writeOutput('tomorrow_forecast', text);
164
+ await this._maybeSpeak(text);
165
+
166
+ this.adapter.log.info('[aiForecastHelper] Vorhersage für morgen erzeugt');
167
+ } catch (err) {
168
+ this.adapter.log.warn(`[aiForecastHelper] Fehler bei _runForecast(): ${err.message}`);
169
+ }
170
+ },
171
+
172
+ // ---------------------------------------------------------------------
173
+ // TEXTGENERATOR – Vorhersage für morgen (erweitert)
174
+ // ---------------------------------------------------------------------
175
+ _buildForecastText(weather) {
176
+ try {
177
+ const tmaxArr = weather?.daily?.temperature_2m_max || [];
178
+ const tminArr = weather?.daily?.temperature_2m_min || [];
179
+ const codeArr = weather?.daily?.weathercode || [];
180
+
181
+ // NEU: zusätzliche Arrays für Regen & Wind
182
+ const rainProbArr = weather?.daily?.precipitation_probability_max || []; // NEU
183
+ const windMaxArr = weather?.daily?.wind_speed_10m_max || []; // NEU
184
+
185
+ // Index 1 = morgen
186
+ const tmax = this._safeValue(tmaxArr[1]);
187
+ const tmin = this._safeValue(tminArr[1]);
188
+ const code = this._safeValue(codeArr[1]);
189
+
190
+ // NEU: Zusatzwerte für Regen & Wind
191
+ const rain = this._safeValue(rainProbArr[1]); // Regenwahrscheinlichkeit in %
192
+ const wind = this._safeValue(windMaxArr[1]); // max. Wind (km/h, laut Open-Meteo)
193
+
194
+ const desc = this._describeWeatherCode(code);
195
+
196
+ let text = 'Vorhersage für morgen: ';
197
+
198
+ // --- Temperaturteil (wie bisher, nur leicht ergänzt) ---
199
+ if (tmax != null && tmin != null) {
200
+ text += `Temperaturen zwischen ${tmin.toFixed(1)} °C und ${tmax.toFixed(1)} °C`;
201
+ } else if (tmax != null) {
202
+ text += `Temperaturen bis etwa ${tmax.toFixed(1)} °C`;
203
+ } else {
204
+ text += 'keine Temperaturdaten verfügbar';
205
+ }
206
+
207
+ if (desc) {
208
+ text += `, Wetter: ${desc}.`;
209
+ } else {
210
+ text += '.';
211
+ }
212
+
213
+ // --------------------------------------------------------
214
+ // NEU: Regenwahrscheinlichkeit
215
+ // --------------------------------------------------------
216
+ if (rain != null) {
217
+ text += ` Regenwahrscheinlichkeit: ${rain}%`;
218
+ if (rain >= 70) {
219
+ text += ' – hoher Regenanteil erwartet.';
220
+ } else if (rain >= 40) {
221
+ text += ' – zeitweise Schauer möglich.';
222
+ } else {
223
+ text += ' – überwiegend trocken.';
224
+ }
225
+ }
226
+
227
+ text += ' ';
228
+
229
+ // --------------------------------------------------------
230
+ // NEU: Windanalyse (stark / frisch / leicht)
231
+ // --------------------------------------------------------
232
+ if (wind != null) {
233
+ let windText = '';
234
+
235
+ if (wind >= 60) {
236
+ windText = 'sehr starker Wind / Sturm';
237
+ } else if (wind >= 40) {
238
+ windText = 'starker Wind';
239
+ } else if (wind >= 25) {
240
+ windText = 'frischer Wind';
241
+ } else if (wind >= 10) {
242
+ windText = 'leichter Wind';
243
+ } else {
244
+ windText = 'kaum spürbarer Wind';
245
+ }
246
+
247
+ text += `Wind: ${windText} (max. ${wind} km/h). `;
248
+
249
+ // NEU: Warnung bei starkem Wind
250
+ if (wind >= 40) {
251
+ text += '⚠️ Achtung: starker Wind vorhergesagt – Abdeckung und loses Zubehör sichern. ';
252
+ }
253
+ }
254
+
255
+ // --------------------------------------------------------
256
+ // NEU: Einschätzung „Solarwetter“
257
+ // --------------------------------------------------------
258
+ if (tmax != null) {
259
+ if (tmax >= 26) {
260
+ text +=
261
+ 'Morgen ist gutes Solarwetter – der Pool kann sich deutlich erwärmen, Abdeckung tagsüber geöffnet lassen. ';
262
+ } else if (tmax >= 20) {
263
+ text +=
264
+ 'Morgen ist moderates Solarwetter – leichte Erwärmung möglich, Abdeckung je nach Bedarf öffnen. ';
265
+ } else {
266
+ text +=
267
+ 'Nur wenig Solarwärme zu erwarten – Abdeckung möglichst geschlossen halten, um Wärmeverluste zu reduzieren. ';
268
+ }
269
+ }
270
+
271
+ // --------------------------------------------------------
272
+ // NEU: Pool-Empfehlungen für morgen
273
+ // --------------------------------------------------------
274
+ text += 'Pool-Empfehlungen für morgen: ';
275
+
276
+ if (rain != null && rain >= 60) {
277
+ text += 'Abdeckung geschlossen halten, da mit Regen zu rechnen ist. ';
278
+ } else if (wind != null && wind >= 40) {
279
+ text += 'Abdeckung gut sichern und empfindliche Gegenstände aus dem Poolbereich entfernen. ';
280
+ } else if (tmax != null && tmax >= 25) {
281
+ text += 'Gutes Badewetter – Pumpe tagsüber ausreichend laufen lassen und Abdeckung geöffnet halten. ';
282
+ } else {
283
+ text += 'Normale Poolnutzung möglich, Einstellungen können unverändert bleiben. ';
284
+ }
285
+
286
+ return text.trim();
287
+ } catch {
288
+ return 'Vorhersage: Wetterdaten konnten nicht ausgewertet werden.';
289
+ }
290
+ },
291
+
292
+ // ---------------------------------------------------------------------
293
+ // WETTER & GEO
294
+ // ---------------------------------------------------------------------
295
+ async _loadGeoLocation() {
296
+ try {
297
+ const obj = await this.adapter.getForeignObjectAsync('system.config');
298
+ if (!obj || !obj.common) {
299
+ return null;
300
+ }
301
+
302
+ const lat = Number(obj.common.latitude);
303
+ const lon = Number(obj.common.longitude);
304
+
305
+ if (Number.isNaN(lat) || Number.isNaN(lon)) {
306
+ return null;
307
+ }
308
+
309
+ if (this._debugMode) {
310
+ this.adapter.log.debug(`[aiForecastHelper] Geodaten: lat=${lat}, lon=${lon}`);
311
+ }
312
+
313
+ return { lat, lon };
314
+ } catch {
315
+ return null;
316
+ }
317
+ },
318
+
319
+ async _fetchWeather(lat, lon) {
320
+ const url =
321
+ `https://api.open-meteo.com/v1/forecast` +
322
+ `?latitude=${lat}&longitude=${lon}` +
323
+ `&current=temperature_2m,wind_speed_10m` +
324
+ // NEU: zusätzliche Daily-Parameter für Regenwahrscheinlichkeit & max. Wind
325
+ `&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_probability_max,wind_speed_10m_max` +
326
+ `&timezone=auto`;
327
+
328
+ if (this._debugMode) {
329
+ this.adapter.log.debug(`[aiForecastHelper] Abruf: ${url}`);
330
+ }
331
+
332
+ return new Promise(resolve => {
333
+ try {
334
+ https
335
+ .get(url, res => {
336
+ let data = '';
337
+ res.on('data', chunk => (data += chunk));
338
+ res.on('end', () => {
339
+ try {
340
+ resolve(JSON.parse(data));
341
+ } catch {
342
+ resolve(null);
343
+ }
344
+ });
345
+ })
346
+ .on('error', () => resolve(null));
347
+ } catch {
348
+ resolve(null);
349
+ }
350
+ });
351
+ },
352
+
353
+ // ---------------------------------------------------------------------
354
+ // AUSGABE
355
+ // ---------------------------------------------------------------------
356
+ async _writeOutput(id, text) {
357
+ try {
358
+ await this.adapter.setStateAsync(`ai.weather.outputs.${id}`, { val: text, ack: true });
359
+
360
+ if (this._debugMode) {
361
+ this.adapter.log.debug(`[aiForecastHelper] Output geschrieben: ai.weather.outputs.${id}`);
362
+ }
363
+ } catch (err) {
364
+ this.adapter.log.error(`[aiForecastHelper] Fehler beim Schreiben eines Outputs (${id}): ${err.message}`);
365
+ }
366
+ },
367
+
368
+ async _maybeSpeak(text) {
369
+ const allowed = await this._getBool('ai.weather.switches.allow_speech', false);
370
+ if (!allowed) {
371
+ return;
372
+ }
373
+
374
+ try {
375
+ await this.adapter.setStateAsync('speech.queue', { val: text, ack: false });
376
+ this.adapter.log.info('[aiForecastHelper] Sprachausgabe gestartet');
377
+ } catch (err) {
378
+ this.adapter.log.warn(`[aiForecastHelper] Fehler bei Sprachausgabe: ${err.message}`);
379
+ }
380
+ },
381
+
382
+ // ---------------------------------------------------------------------
383
+ // HILFSFUNKTIONEN
384
+ // ---------------------------------------------------------------------
385
+ _safeValue(v) {
386
+ if (v == null) {
387
+ return null;
388
+ }
389
+ const num = Number(v);
390
+ return Number.isNaN(num) ? null : num;
391
+ },
392
+
393
+ _describeWeatherCode(code) {
394
+ if (code == null) {
395
+ return '';
396
+ }
397
+
398
+ const map = {
399
+ 0: 'klarer Himmel',
400
+ 1: 'überwiegend sonnig',
401
+ 2: 'wechselhaft bewölkt',
402
+ 3: 'bedeckt',
403
+ 45: 'Nebel',
404
+ 48: 'Hochnebel',
405
+ 51: 'leichter Nieselregen',
406
+ 53: 'mäßiger Nieselregen',
407
+ 55: 'starker Nieselregen',
408
+ 61: 'leichter Regen',
409
+ 63: 'mäßiger Regen',
410
+ 65: 'starker Regen',
411
+ 80: 'Regenschauer',
412
+ 81: 'kräftige Schauer',
413
+ 82: 'Starkschauer',
414
+ 95: 'Gewitter',
415
+ 96: 'Gewitter mit Hagel',
416
+ 99: 'starkes Gewitter mit Hagel',
417
+ };
418
+
419
+ return map[code] || `Wettercode ${code}`;
420
+ },
421
+
422
+ async _getBool(id, fallback) {
423
+ try {
424
+ const st = await this.adapter.getStateAsync(id);
425
+ return st && st.val != null ? !!st.val : fallback;
426
+ } catch {
427
+ return fallback;
428
+ }
429
+ },
430
+
431
+ async _getTimeOrDefault(id, def) {
432
+ const state = await this.adapter.getStateAsync(id);
433
+ const str = state?.val ?? def;
434
+
435
+ const m = /^(\d{1,2}):(\d{2})$/.exec(String(str));
436
+ if (!m) {
437
+ const defM = /^(\d{1,2}):(\d{2})$/.exec(def);
438
+ return { hour: Number(defM[1]), minute: Number(defM[2]) };
439
+ }
440
+
441
+ return {
442
+ hour: Math.min(Math.max(Number(m[1]), 0), 23),
443
+ minute: Math.min(Math.max(Number(m[2]), 0), 59),
444
+ };
445
+ },
446
+ };
447
+
448
+ module.exports = aiForecastHelper;