iobroker.poolcontrol 0.7.3 → 0.8.1

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.
@@ -8,35 +8,35 @@
8
8
  * Zentraler KI-Helper für PoolControl.
9
9
  *
10
10
  * Nutzt die States aus aiStates.js:
11
- * ai.switches.*
12
- * ai.schedule.*
13
- * ai.outputs.*
11
+ * ai.weather.switches.*
12
+ * ai.weather.schedule.*
13
+ * ai.weather.outputs.*
14
14
  *
15
15
  * Funktionen:
16
16
  * - Liest Geodaten aus system.config (Latitude/Longitude)
17
17
  * - Ruft Wetterdaten von Open-Meteo ab (bei Bedarf, max. 4x/Tag – je Modul)
18
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
19
+ * ai.weather.outputs.weather_advice
20
+ * ai.weather.outputs.daily_summary
21
+ * ai.weather.outputs.pool_tips
22
+ * ai.weather.outputs.weekend_summary
23
+ * ai.weather.outputs.last_message
24
24
  * - Optional: legt Texte in speech.queue für Sprachausgabe
25
25
  *
26
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
27
+ * - ai.enabled → globaler KI-Schalter
28
+ * - ai.weather.switches.allow_speech → Sprachausgabe erlaubt
29
+ * - ai.weather.switches.weather_advice_enabled
30
+ * - ai.weather.switches.daily_summary_enabled
31
+ * - ai.weather.switches.daily_pool_tips_enabled
32
+ * - ai.weather.switches.weekend_summary_enabled
33
+ * - ai.weather.switches.debug_mode
34
34
  *
35
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
36
+ * - ai.weather.schedule.weather_advice_time
37
+ * - ai.weather.schedule.daily_summary_time
38
+ * - ai.weather.schedule.daily_pool_tips_time
39
+ * - ai.weather.schedule.weekend_summary_time
40
40
  */
41
41
 
42
42
  const https = require('https');
@@ -47,6 +47,13 @@ const aiHelper = {
47
47
  _lastScheduleValues: {}, // NEU: merkt sich letzte Zeitwerte
48
48
  _debugMode: false,
49
49
 
50
+ _adapterStartedAt: Date.now(), // FIX: Zeitpunkt des Adapterstarts
51
+
52
+ // Anti-Spam-Level (merkt sich letzte Warnungen)
53
+ _lastPoolTipCode: null,
54
+ _lastPoolTipWindLevel: null,
55
+ _lastPoolTipTimestamp: 0,
56
+
50
57
  /**
51
58
  * Initialisiert den AI-Helper (Timer + Grundkonfiguration).
52
59
  *
@@ -101,7 +108,7 @@ const aiHelper = {
101
108
  // ---------------------------------------------------------
102
109
  // FIX 2: Uhrzeitänderungen IMMER erkennen und loggen
103
110
  // ---------------------------------------------------------
104
- if (id.includes('.ai.schedule.')) {
111
+ if (id.includes('.ai.weather.schedule.')) {
105
112
  const oldVal = this._lastScheduleValues[id];
106
113
  const newVal = state.val;
107
114
 
@@ -114,13 +121,54 @@ const aiHelper = {
114
121
 
115
122
  // Timer neu aufbauen
116
123
  await this._refreshTimers();
124
+
125
+ // ----------------------------------------------------------
126
+ // NEU: Delay, damit ioBroker alle States laden kann
127
+ // ----------------------------------------------------------
128
+ await new Promise(res => setTimeout(res, 1500));
129
+
130
+ // NEU: Wenn Uhrzeit heute noch in der Zukunft liegt → sofort ausführen
131
+ try {
132
+ const now = new Date();
133
+ const [hourStr, minuteStr] = String(newVal).split(':');
134
+ const hour = Number(hourStr);
135
+ const minute = Number(minuteStr);
136
+
137
+ if (!Number.isNaN(hour) && !Number.isNaN(minute)) {
138
+ const target = new Date();
139
+ target.setHours(hour, minute, 0, 0);
140
+
141
+ // Nur wenn Zielzeit HEUTE noch bevorsteht
142
+ if (target > now) {
143
+ this.adapter.log.info(
144
+ `[aiHelper] Neue Uhrzeit liegt heute noch in der Zukunft → führe Modul sofort aus (${id})`,
145
+ );
146
+
147
+ if (id.endsWith('weather_advice_time')) {
148
+ await this._runWeatherAdvice();
149
+ }
150
+ if (id.endsWith('daily_summary_time')) {
151
+ await this._runDailySummary();
152
+ }
153
+ if (id.endsWith('daily_pool_tips_time')) {
154
+ await this._runDailyPoolTips();
155
+ }
156
+ if (id.endsWith('weekend_summary_time')) {
157
+ await this._runWeekendSummary();
158
+ }
159
+ }
160
+ }
161
+ } catch (e) {
162
+ this.adapter.log.warn(`[aiHelper] Fehler bei Sofortausführung nach Zeitänderung: ${e.message}`);
163
+ }
164
+
117
165
  return;
118
166
  }
119
167
 
120
168
  // ---------------------------------------------------------
121
169
  // FIX 3: Schalteränderungen immer melden
122
170
  // ---------------------------------------------------------
123
- if (id.includes('.ai.switches.')) {
171
+ if (id.includes('.ai.weather.switches.')) {
124
172
  this.adapter.log.info(`[aiHelper] Schalter geändert: ${id} = ${state.val} – Timer werden neu aufgebaut`);
125
173
 
126
174
  await this._refreshTimers();
@@ -148,63 +196,106 @@ const aiHelper = {
148
196
  async _refreshTimers() {
149
197
  this._clearTimers();
150
198
 
151
- const aiEnabled = await this._getBool('ai.switches.enabled', false);
152
- this._debugMode = await this._getBool('ai.switches.debug_mode', false);
199
+ const aiEnabled = await this._getBool('ai.enabled', false);
200
+ this._debugMode = await this._getBool('ai.weather.switches.debug_mode', false);
153
201
 
154
202
  if (!aiEnabled) {
155
- this.adapter.log.info('[aiHelper] KI ist deaktiviert (ai.switches.enabled = false) – keine Timer aktiv');
203
+ this.adapter.log.info('[aiHelper] KI ist deaktiviert (ai.enabled = false) – keine Timer aktiv');
156
204
  return;
157
205
  }
158
206
 
159
207
  this.adapter.log.info('[aiHelper] KI ist aktiv – Timer werden gesetzt');
160
208
 
161
209
  // --- Wetterhinweise ---
162
- const weatherEnabled = await this._getBool('ai.switches.weather_advice_enabled', false);
210
+ const weatherEnabled = await this._getBool('ai.weather.switches.weather_advice_enabled', false);
163
211
  if (weatherEnabled) {
164
- const time = await this._getTimeOrDefault('ai.schedule.weather_advice_time', '08:00');
212
+ const time = await this._getTimeOrDefault('ai.weather.schedule.weather_advice_time', '08:00');
165
213
  this._createDailyTimer(time, async () => {
166
214
  await this._runWeatherAdvice();
167
215
  });
168
- this.adapter.log.info(
216
+ this.adapter.log.debug(
169
217
  `[aiHelper] Wetterhinweis-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
170
218
  );
171
219
  }
172
220
 
173
221
  // --- Tägliche Zusammenfassung ---
174
- const summaryEnabled = await this._getBool('ai.switches.daily_summary_enabled', false);
222
+ const summaryEnabled = await this._getBool('ai.weather.switches.daily_summary_enabled', false);
175
223
  if (summaryEnabled) {
176
- const time = await this._getTimeOrDefault('ai.schedule.daily_summary_time', '09:00');
224
+ const time = await this._getTimeOrDefault('ai.weather.schedule.daily_summary_time', '09:00');
177
225
  this._createDailyTimer(time, async () => {
178
226
  await this._runDailySummary();
179
227
  });
180
- this.adapter.log.info(
228
+ this.adapter.log.debug(
181
229
  `[aiHelper] Daily-Summary-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
182
230
  );
183
231
  }
184
232
 
185
233
  // --- Tägliche Pool-Tipps ---
186
- const tipsEnabled = await this._getBool('ai.switches.daily_pool_tips_enabled', false);
234
+ const tipsEnabled = await this._getBool('ai.weather.switches.daily_pool_tips_enabled', false);
187
235
  if (tipsEnabled) {
188
- const time = await this._getTimeOrDefault('ai.schedule.daily_pool_tips_time', '10:00');
236
+ const time = await this._getTimeOrDefault('ai.weather.schedule.daily_pool_tips_time', '10:00');
189
237
  this._createDailyTimer(time, async () => {
190
238
  await this._runDailyPoolTips();
191
239
  });
192
- this.adapter.log.info(
240
+ this.adapter.log.debug(
193
241
  `[aiHelper] Pool-Tipps-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
194
242
  );
195
243
  }
196
244
 
197
245
  // --- Wochenend-Zusammenfassung ---
198
- const weekendEnabled = await this._getBool('ai.switches.weekend_summary_enabled', false);
246
+ const weekendEnabled = await this._getBool('ai.weather.switches.weekend_summary_enabled', false);
199
247
  if (weekendEnabled) {
200
- const time = await this._getTimeOrDefault('ai.schedule.weekend_summary_time', '18:00');
248
+ const time = await this._getTimeOrDefault('ai.weather.schedule.weekend_summary_time', '18:00');
201
249
  this._createDailyTimer(time, async () => {
202
250
  await this._runWeekendSummary();
203
251
  });
204
- this.adapter.log.info(
252
+ this.adapter.log.debug(
205
253
  `[aiHelper] Wochenend-Timer gesetzt für ${time.hour}:${String(time.minute).padStart(2, '0')}`,
206
254
  );
207
255
  }
256
+
257
+ //--------------------------------------------------------
258
+ // NEU: Stündlicher Wetter-Update-Timer
259
+ //--------------------------------------------------------
260
+ this.adapter.log.debug('[aiHelper] Stündlicher Wetter-Update-Timer wird gesetzt');
261
+
262
+ const hourlyTimer = setInterval(
263
+ async () => {
264
+ try {
265
+ const geo = await this._loadGeoLocation();
266
+ if (!geo) {
267
+ this.adapter.log.info('[aiHelper] Wetter-Update abgebrochen – keine Geodaten verfügbar');
268
+ return;
269
+ }
270
+
271
+ const weather = await this._fetchWeather(geo.lat, geo.lon);
272
+ if (!weather) {
273
+ this.adapter.log.info('[aiHelper] Wetter-Update abgebrochen – keine Wetterdaten verfügbar');
274
+ return;
275
+ }
276
+
277
+ // WeatherAdvice aktualisieren (heutiges Wetter)
278
+ const weatherText = this._buildWeatherAdviceText(weather);
279
+ await this._writeOutput('weather_advice', weatherText);
280
+
281
+ // NEU: Pool-Tipps automatisch mit aktualisieren
282
+ const seasonActive = await this._getBool('status.season_active', false);
283
+ const poolTipsText = this._buildPoolTipsText(weather, seasonActive);
284
+ await this._writeOutput('pool_tips', poolTipsText);
285
+
286
+ if (this._debugMode) {
287
+ this.adapter.log.debug(
288
+ '[aiHelper] Stündliches Wetter-Update durchgeführt (Weather + Pool-Tipps)',
289
+ );
290
+ }
291
+ } catch (err) {
292
+ this.adapter.log.warn(`[aiHelper] Fehler beim stündlichen Wetter-Update: ${err.message}`);
293
+ }
294
+ },
295
+ 60 * 60 * 1000,
296
+ ); // 1 Stunde
297
+
298
+ this.timers.push(hourlyTimer);
208
299
  },
209
300
 
210
301
  /**
@@ -219,20 +310,25 @@ const aiHelper = {
219
310
  const timer = setInterval(async () => {
220
311
  const now = new Date();
221
312
 
222
- // --- NEU: Nachträgliche Ausführung, falls Zeit knapp verpasst wurde ---
313
+ // --- FIX: Nachholen nur in den ersten 3 Minuten nach Adapterstart ---
223
314
  const diffMinutes = now.getHours() * 60 + now.getMinutes() - (hour * 60 + minute);
224
315
 
225
- if (diffMinutes > 0 && diffMinutes <= 2) {
316
+ const adapterUptimeMs = Date.now() - this._adapterStartedAt;
317
+ const withinStartupWindow = adapterUptimeMs <= 3 * 60 * 1000; // 3 Minuten
318
+
319
+ if (withinStartupWindow && diffMinutes > 0 && diffMinutes <= 2) {
226
320
  try {
227
321
  await callback();
228
- this.adapter.log.info('[aiHelper] Zeit knapp verpasst – nachträgliche Ausführung durchgeführt');
322
+ this.adapter.log.info(
323
+ '[aiHelper] Nachholung ausgeführt (innerhalb der ersten 3 Minuten nach Start)',
324
+ );
229
325
  } catch (err) {
230
- this.adapter.log.warn(`[aiHelper] Fehler bei nachträglicher Ausführung: ${err.message}`);
326
+ this.adapter.log.warn(`[aiHelper] Fehler bei Nachholung: ${err.message}`);
231
327
  }
232
328
  }
233
329
 
234
330
  if (now.getHours() === hour && now.getMinutes() === minute) {
235
- this.adapter.log.info(
331
+ this.adapter.log.debug(
236
332
  `[aiHelper] Timer ausgelöst: ${hour}:${String(minute).padStart(2, '0')} → Callback wird ausgeführt`,
237
333
  ); // NEU
238
334
 
@@ -359,32 +455,57 @@ const aiHelper = {
359
455
  }
360
456
  },
361
457
 
458
+ //--------------------------------------------------------
459
+ // NEU: Stündliches Wetter-Update als eigene Funktion
460
+ //--------------------------------------------------------
461
+ async _runWeatherAutoUpdate() {
462
+ try {
463
+ const geo = await this._loadGeoLocation();
464
+ if (!geo) {
465
+ this.adapter.log.info('[aiHelper] Auto-Wetterupdate abgebrochen – keine Geodaten verfügbar');
466
+ return;
467
+ }
468
+
469
+ const weather = await this._fetchWeather(geo.lat, geo.lon);
470
+ if (!weather) {
471
+ this.adapter.log.info('[aiHelper] Auto-Wetterupdate abgebrochen – keine Wetterdaten verfügbar');
472
+ return;
473
+ }
474
+
475
+ // WeatherAdvice aktualisieren
476
+ const text = this._buildWeatherAdviceText(weather);
477
+ await this._writeOutput('weather_advice', text);
478
+
479
+ if (this._debugMode) {
480
+ this.adapter.log.debug('[aiHelper] Auto-Wetterupdate erfolgreich durchgeführt');
481
+ }
482
+ } catch (err) {
483
+ this.adapter.log.warn(`[aiHelper] Fehler bei Auto-Wetterupdate: ${err.message}`);
484
+ }
485
+ },
486
+
362
487
  // ---------------------------------------------------------------------
363
488
  // Geodaten + Wetter
364
489
  // ---------------------------------------------------------------------
365
490
 
366
491
  /**
367
- * Lädt Geokoordinaten aus system.config.
492
+ * Lädt Geokoordinaten korrekt aus system.config.
368
493
  *
369
494
  * @returns {{lat:number,lon:number}|null}
370
495
  */
371
496
  async _loadGeoLocation() {
372
497
  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
- );
498
+ const obj = await this.adapter.getForeignObjectAsync('system.config');
499
+ if (!obj || !obj.common) {
500
+ this.adapter.log.warn('[aiHelper] Konnte system.config nicht laden');
380
501
  return null;
381
502
  }
382
503
 
383
- const lat = Number(latState.val);
384
- const lon = Number(lonState.val);
504
+ const lat = Number(obj.common.latitude);
505
+ const lon = Number(obj.common.longitude);
385
506
 
386
507
  if (Number.isNaN(lat) || Number.isNaN(lon)) {
387
- this.adapter.log.warn('[aiHelper] Geodaten ungültig – latitude/longitude sind keine Zahlen');
508
+ this.adapter.log.warn('[aiHelper] Geodaten ungültig – bitte in Admin unter System/Standort eintragen');
388
509
  return null;
389
510
  }
390
511
 
@@ -567,6 +688,54 @@ const aiHelper = {
567
688
 
568
689
  let text = 'Pool-Tipp für heute: ';
569
690
 
691
+ //--------------------------------------------------------
692
+ // NEU: Erweiterte Analyse für Pool-Tipps
693
+ //--------------------------------------------------------
694
+
695
+ // 1) Wind / Sturm
696
+ const wind = weather?.current?.wind_speed_10m ?? null;
697
+ if (wind != null) {
698
+ if (wind >= 60) {
699
+ text +=
700
+ '⚠️ Extrem starker Sturm erwartet! Bitte unbedingt die Abdeckung sichern und alle Gegenstände im Poolbereich fest verankern. ';
701
+ } else if (wind >= 45) {
702
+ text += 'Achtung: Starke Windböen treten auf. Bitte Abdeckung fixieren und lose Gegenstände sichern. ';
703
+ } else if (wind >= 30) {
704
+ text += 'Es wird windig – Abdeckung gut verschließen und empfindliches Zubehör schützen. ';
705
+ }
706
+ }
707
+
708
+ // 2) Regen / Starkregen
709
+ if (code != null) {
710
+ if ([65, 82].includes(code)) {
711
+ text += 'Kräftige Regenschauer erwartet – Abdeckung geschlossen halten. ';
712
+ } else if ([61, 63, 80, 81].includes(code)) {
713
+ text += 'Es wird regnerisch – Abdeckung eher geschlossen lassen. ';
714
+ }
715
+ }
716
+
717
+ // 3) Gewitter / Hagel
718
+ if (code === 95) {
719
+ text += '⚡ Gewitterwarnung! Bitte Solarfolie sichern und Technik vor Feuchtigkeit schützen. ';
720
+ }
721
+ if (code === 96 || code === 99) {
722
+ text += '⚠️ Hagelgefahr! Bitte empfindliche Geräte schützen und Poolbereich räumen. ';
723
+ }
724
+
725
+ // 4) Temperatur / Hitze
726
+ if (tmax >= 28) {
727
+ text += 'Sehr warmes Badewetter – Abdeckung tagsüber offen lassen. Chlorverbrauch steigt. ';
728
+ } else if (tmax >= 22) {
729
+ text += 'Angenehme Temperaturen – normaler Poolbetrieb empfohlen. ';
730
+ } else if (tmax <= 16) {
731
+ text += 'Kühle Temperaturen – Abdeckung geschlossen halten, um Wärmeverluste zu reduzieren. ';
732
+ }
733
+
734
+ // Fallback falls noch nichts geschrieben wurde
735
+ if (text.trim() === 'Pool-Tipp für heute:') {
736
+ text += desc ? `${desc}. ` : 'Keine besonderen Hinweise für den heutigen Tag. ';
737
+ }
738
+
570
739
  if (tmax >= 26) {
571
740
  text += 'Es wird warm bis sehr warm – gutes Badewetter. ';
572
741
  text +=
@@ -587,6 +756,47 @@ const aiHelper = {
587
756
  text += 'Bei sonniger Witterung steigt der Chlorverbrauch – Wasserwerte im Auge behalten.';
588
757
  }
589
758
 
759
+ //--------------------------------------------------------
760
+ // NEU: Anti-Spam-Logik
761
+ //--------------------------------------------------------
762
+
763
+ // // Wettercode vergleichen
764
+ // if (code != null) {
765
+ // if (this._lastPoolTipCode === code) {
766
+ // // gleiches Wetter wie vorher → eventuell abbrechen
767
+ // const nowTs = Date.now();
768
+ // // nur jede 3 Stunden dieselbe Warnung erneut ausgeben
769
+ // if (nowTs - this._lastPoolTipTimestamp < 3 * 60 * 60 * 1000) {
770
+ // return 'Pool-Tipp: Keine neuen Hinweise – Bedingungen unverändert.';
771
+ // }
772
+ // }
773
+ // }
774
+
775
+ // Windlevel kategorisieren: 0 = ruhig, 1 = windig, 2 = stark, 3 = Sturm
776
+ let windLevel = 0;
777
+ if (wind != null) {
778
+ if (wind >= 60) {
779
+ windLevel = 3;
780
+ } else if (wind >= 45) {
781
+ windLevel = 2;
782
+ } else if (wind >= 30) {
783
+ windLevel = 1;
784
+ }
785
+ }
786
+
787
+ // // prüfen, ob derselbe Windlevel schon gemeldet wurde
788
+ // if (windLevel === this._lastPoolTipWindLevel) {
789
+ // const nowTs = Date.now();
790
+ // if (nowTs - this._lastPoolTipTimestamp < 3 * 60 * 60 * 1000) {
791
+ // return 'Pool-Tipp: Keine neuen Informationen – Wetter gleich geblieben.';
792
+ // }
793
+ // }
794
+
795
+ // Wenn wir hier sind → neuer Hinweis → Werte speichern
796
+ this._lastPoolTipCode = code;
797
+ this._lastPoolTipWindLevel = windLevel;
798
+ this._lastPoolTipTimestamp = Date.now();
799
+
590
800
  return text;
591
801
  },
592
802
 
@@ -761,11 +971,11 @@ const aiHelper = {
761
971
  }
762
972
 
763
973
  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 });
974
+ await this.adapter.setStateAsync(`ai.weather.outputs.${id}`, { val: text, ack: true });
975
+ await this.adapter.setStateAsync('ai.weather.outputs.last_message', { val: text, ack: true });
766
976
 
767
977
  if (this._debugMode) {
768
- this.adapter.log.info(`[aiHelper] Output geschrieben → ai.outputs.${id}: ${text}`);
978
+ this.adapter.log.debug(`[aiHelper] Output geschrieben → ai.weather.outputs.${id}: ${text}`);
769
979
  }
770
980
  } catch (err) {
771
981
  this.adapter.log.error(`[aiHelper] Fehler beim Schreiben eines Outputs (${id}): ${err.message}`);
@@ -785,10 +995,12 @@ const aiHelper = {
785
995
  return;
786
996
  }
787
997
 
788
- const allowSpeech = await this._getBool('ai.switches.allow_speech', false);
998
+ const allowSpeech = await this._getBool('ai.weather.switches.allow_speech', false);
789
999
  if (!allowSpeech) {
790
1000
  if (this._debugMode) {
791
- this.adapter.log.debug('[aiHelper] Sprachausgabe deaktiviert (ai.switches.allow_speech = false)');
1001
+ this.adapter.log.debug(
1002
+ '[aiHelper] Sprachausgabe deaktiviert (ai.weather.switches.allow_speech = false)',
1003
+ );
792
1004
  }
793
1005
  return;
794
1006
  }
@@ -870,7 +1082,7 @@ const aiHelper = {
870
1082
  const str = await this._getString(id, def);
871
1083
 
872
1084
  // NEU: Log, welche Uhrzeit der Helper tatsächlich verwendet
873
- this.adapter.log.info(`[aiHelper] Uhrzeit geladen: ${id} = "${str}" (Default: ${def})`);
1085
+ this.adapter.log.debug(`[aiHelper] Uhrzeit geladen: ${id} = "${str}" (Default: ${def})`);
874
1086
 
875
1087
  const match = /^(\d{1,2}):(\d{2})$/.exec(str || '');
876
1088
  let hour = 0;