iobroker.poolcontrol 1.3.27 → 1.3.29

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.
Files changed (40) hide show
  1. package/README.md +22 -18
  2. package/admin/i18n/de/translations.json +4 -0
  3. package/admin/i18n/en/translations.json +8 -2
  4. package/admin/i18n/es/translations.json +4 -0
  5. package/admin/i18n/fr/translations.json +4 -0
  6. package/admin/i18n/it/translations.json +4 -0
  7. package/admin/i18n/nl/translations.json +4 -0
  8. package/admin/i18n/pl/translations.json +4 -0
  9. package/admin/i18n/pt/translations.json +4 -0
  10. package/admin/i18n/ru/translations.json +4 -0
  11. package/admin/i18n/uk/translations.json +4 -0
  12. package/admin/i18n/zh-cn/translations.json +4 -0
  13. package/admin/jsonConfig.json +31 -1
  14. package/io-package.json +27 -27
  15. package/lib/helpers/aiHelper.js +5 -5
  16. package/lib/helpers/consumptionHelper.js +47 -5
  17. package/lib/helpers/heatHelper.js +12 -9
  18. package/lib/helpers/infoHelper.js +11 -11
  19. package/lib/helpers/poolInsightsHelper.js +620 -0
  20. package/lib/helpers/pumpHelper.js +15 -7
  21. package/lib/helpers/statisticsHelper.js +7 -3
  22. package/lib/helpers/statisticsHelperMonth.js +19 -15
  23. package/lib/helpers/statisticsHelperWeek.js +11 -5
  24. package/lib/i18n/de.json +31 -1
  25. package/lib/i18n/en.json +34 -4
  26. package/lib/i18n/es.json +31 -1
  27. package/lib/i18n/fr.json +31 -1
  28. package/lib/i18n/it.json +31 -1
  29. package/lib/i18n/nl.json +31 -1
  30. package/lib/i18n/pl.json +31 -1
  31. package/lib/i18n/pt.json +31 -1
  32. package/lib/i18n/ru.json +31 -1
  33. package/lib/i18n/uk.json +31 -1
  34. package/lib/i18n/zh-cn.json +31 -1
  35. package/lib/stateDefinitions/poolInsightsStates.js +289 -0
  36. package/lib/stateDefinitions/pumpStates.js +39 -0
  37. package/lib/stateDefinitions/solarStates.js +12 -3
  38. package/lib/stateDefinitions/statusStates.js +1 -1
  39. package/main.js +20 -0
  40. package/package.json +1 -1
@@ -0,0 +1,620 @@
1
+ 'use strict';
2
+
3
+ const { I18n } = require('@iobroker/adapter-core');
4
+
5
+ const POOL_INSIGHTS_PREFIX = 'analytics.insights.pool';
6
+ const DAILY_ANALYSIS_HOUR = 20;
7
+ const SPEECH_COOLDOWN_MS = 6 * 60 * 60 * 1000;
8
+
9
+ const poolInsightsHelper = {
10
+ adapter: null,
11
+ dailyTimer: null,
12
+ running: false,
13
+
14
+ init(adapter) {
15
+ this.adapter = adapter;
16
+
17
+ this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.enabled`);
18
+ this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.manual_trigger`);
19
+ this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.send_to_speech_queue`);
20
+
21
+ void this._refreshSchedule();
22
+ this.adapter.log.debug('[poolInsightsHelper] Initialized');
23
+ },
24
+
25
+ handleStateChange(id, state) {
26
+ if (!state || state.ack === true) {
27
+ return;
28
+ }
29
+
30
+ if (id.endsWith(`${POOL_INSIGHTS_PREFIX}.manual_trigger`) && state.val === true) {
31
+ void this._handleManualTrigger();
32
+ return;
33
+ }
34
+
35
+ if (id.endsWith(`${POOL_INSIGHTS_PREFIX}.enabled`)) {
36
+ void this._refreshSchedule();
37
+ }
38
+ },
39
+
40
+ async _handleManualTrigger() {
41
+ try {
42
+ await this._runAnalysis('manual', true);
43
+ } finally {
44
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.manual_trigger`, false);
45
+ }
46
+ },
47
+
48
+ async _refreshSchedule() {
49
+ if (this.dailyTimer) {
50
+ this.adapter.clearTimeout(this.dailyTimer);
51
+ this.dailyTimer = null;
52
+ }
53
+
54
+ const enabled = await this._readBoolean(`${POOL_INSIGHTS_PREFIX}.enabled`);
55
+ if (!enabled) {
56
+ await this._writeDisabled('disabled');
57
+ return;
58
+ }
59
+
60
+ await this._scheduleDailyAnalysis();
61
+ },
62
+
63
+ async _scheduleDailyAnalysis() {
64
+ if (this.dailyTimer) {
65
+ this.adapter.clearTimeout(this.dailyTimer);
66
+ this.dailyTimer = null;
67
+ }
68
+
69
+ const now = new Date();
70
+ const next = new Date(now);
71
+ next.setHours(DAILY_ANALYSIS_HOUR, 0, 0, 0);
72
+ if (next <= now) {
73
+ next.setDate(next.getDate() + 1);
74
+ }
75
+
76
+ const delay = Math.max(1000, next.getTime() - now.getTime());
77
+ this.dailyTimer = this.adapter.setTimeout(async () => {
78
+ this.dailyTimer = null;
79
+ await this._runAnalysis('daily', true);
80
+ await this._refreshSchedule();
81
+ }, delay);
82
+
83
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'scheduled');
84
+ this.adapter.log.debug(`[poolInsightsHelper] Daily analysis scheduled for ${next.toISOString()}`);
85
+ },
86
+
87
+ async _runAnalysis(reason, allowSpeech) {
88
+ if (this.running) {
89
+ this.adapter.log.debug('[poolInsightsHelper] Analysis already running - skipped');
90
+ return;
91
+ }
92
+
93
+ this.running = true;
94
+ try {
95
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'running');
96
+
97
+ const snapshot = await this._readSnapshot();
98
+ const result = this._buildResult(snapshot, reason);
99
+
100
+ await this._writeResult(result);
101
+
102
+ if (allowSpeech) {
103
+ await this._sendSpeechIfAllowed(result, reason);
104
+ }
105
+
106
+ this.adapter.log.debug(`[poolInsightsHelper] Analysis completed (${reason})`);
107
+ } catch (err) {
108
+ await this._writeError(reason, err);
109
+ this.adapter.log.warn(`[poolInsightsHelper] Analysis failed: ${err.message}`);
110
+ } finally {
111
+ this.running = false;
112
+ }
113
+ },
114
+
115
+ async _readSnapshot() {
116
+ return {
117
+ temperature: {
118
+ surfaceCurrent: await this._readNumber('temperature.surface.current'),
119
+ surfaceMinToday: await this._readNumber('temperature.surface.min_today'),
120
+ surfaceMaxToday: await this._readNumber('temperature.surface.max_today'),
121
+ surfaceSummaryJson: await this._readString(
122
+ 'analytics.statistics.temperature.today.surface.summary_json',
123
+ ),
124
+ },
125
+ pump: {
126
+ runtimeTodaySeconds: await this._readNumber('runtime.today_seconds'),
127
+ startCountToday: await this._readNumber('runtime.start_count_today'),
128
+ circulationRequired: await this._readNumber('circulation.daily_required'),
129
+ circulationRemaining: await this._readNumber('circulation.daily_remaining'),
130
+ mode: await this._readString('pump.mode'),
131
+ status: await this._readString('pump.status'),
132
+ error: await this._readBoolean('pump.error'),
133
+ activeHelper: await this._readString('pump.active_helper'),
134
+ },
135
+ solar: {
136
+ ranToday: await this._readBoolean('analytics.insights.solar.results.solar_ran_today'),
137
+ estimatedGainTodayKwh: await this._readNumber(
138
+ 'analytics.insights.solar.results.estimated_gain_today_kwh',
139
+ ),
140
+ evaluationAvailable:
141
+ (await this._readString('analytics.insights.solar.results.summary_json')) !== '' ||
142
+ (await this._readString('analytics.insights.solar.results.summary_html')) !== '',
143
+ },
144
+ photovoltaic: {
145
+ activeToday: await this._readBoolean('analytics.insights.photovoltaic.results.active_today'),
146
+ startsToday: await this._readNumber('analytics.insights.photovoltaic.results.starts_today'),
147
+ runtimeTodayMin: await this._readNumber('analytics.insights.photovoltaic.results.runtime_today_min'),
148
+ evaluationAvailable:
149
+ (await this._readString('analytics.insights.photovoltaic.results.summary_json')) !== '' ||
150
+ (await this._readString('analytics.insights.photovoltaic.results.summary_text')) !== '',
151
+ },
152
+ consumption: {
153
+ dayKwh: await this._readNumber('consumption.day_kwh'),
154
+ dayEur: await this._readNumber('costs.day_eur'),
155
+ },
156
+ chemistry: {
157
+ phAvailable: await this._hasValue('chemistry.ph.outputs.summary_text'),
158
+ tdsAvailable: await this._hasValue('chemistry.tds.outputs.summary_text'),
159
+ orpAvailable: await this._hasValue('chemistry.orp.outputs.summary_text'),
160
+ },
161
+ };
162
+ },
163
+
164
+ _buildResult(snapshot, reason) {
165
+ const observations = [];
166
+ const recommendations = [];
167
+ let level = 'ok';
168
+
169
+ const tempDelta = this._getTemperatureDelta(snapshot.temperature);
170
+ if (tempDelta !== null) {
171
+ const rounded = this._round(tempDelta, 1);
172
+ observations.push({
173
+ area: 'temperature',
174
+ level: tempDelta >= 1 ? 'ok' : 'info',
175
+ text: this._translate('pool_insights_observation_temperature_delta', { delta: rounded }),
176
+ });
177
+
178
+ if (tempDelta <= 0.2) {
179
+ level = this._raiseLevel(level, 'info');
180
+ recommendations.push({
181
+ area: 'temperature',
182
+ level: 'info',
183
+ text: this._translate('pool_insights_recommendation_temperature_low_change'),
184
+ });
185
+ }
186
+ }
187
+
188
+ if (snapshot.pump.runtimeTodaySeconds !== null) {
189
+ const runtimeLevel = snapshot.pump.runtimeTodaySeconds > 0 ? 'ok' : 'info';
190
+ level = this._raiseLevel(level, runtimeLevel);
191
+ observations.push({
192
+ area: 'pump',
193
+ level: runtimeLevel,
194
+ text: this._translate('pool_insights_observation_pump_runtime', {
195
+ runtime: this._formatRuntime(snapshot.pump.runtimeTodaySeconds),
196
+ }),
197
+ });
198
+ }
199
+
200
+ if (snapshot.pump.startCountToday !== null) {
201
+ observations.push({
202
+ area: 'pump',
203
+ level: snapshot.pump.startCountToday > 12 ? 'info' : 'ok',
204
+ text: this._translate('pool_insights_observation_pump_starts', {
205
+ count: snapshot.pump.startCountToday,
206
+ }),
207
+ });
208
+
209
+ if (snapshot.pump.startCountToday > 12) {
210
+ level = this._raiseLevel(level, 'info');
211
+ recommendations.push({
212
+ area: 'pump',
213
+ level: 'info',
214
+ text: this._translate('pool_insights_recommendation_many_pump_starts'),
215
+ });
216
+ }
217
+ }
218
+
219
+ if (snapshot.pump.error === true) {
220
+ level = 'warning';
221
+ observations.push({
222
+ area: 'pump',
223
+ level: 'warning',
224
+ text: this._translate('pool_insights_observation_pump_error'),
225
+ });
226
+ recommendations.push({
227
+ area: 'pump',
228
+ level: 'warning',
229
+ text: this._translate('pool_insights_recommendation_pump_error'),
230
+ });
231
+ }
232
+
233
+ this._appendSolarObservations(snapshot.solar, observations);
234
+ this._appendPhotovoltaicObservations(snapshot.photovoltaic, observations);
235
+
236
+ if (snapshot.photovoltaic.startsToday !== null && snapshot.photovoltaic.startsToday > 10) {
237
+ level = this._raiseLevel(level, 'info');
238
+ recommendations.push({
239
+ area: 'photovoltaic',
240
+ level: 'info',
241
+ text: this._translate('pool_insights_recommendation_many_pv_starts'),
242
+ });
243
+ }
244
+
245
+ this._appendConsumptionObservations(snapshot.consumption, observations);
246
+ this._appendChemistryObservations(snapshot.chemistry, observations);
247
+
248
+ if (this._hasLimitedCoreData(snapshot)) {
249
+ level = this._raiseLevel(level, 'info');
250
+ observations.push({
251
+ area: 'pool',
252
+ level: 'info',
253
+ text: this._translate('pool_insights_observation_not_enough_data'),
254
+ });
255
+ }
256
+
257
+ if (observations.length === 0) {
258
+ observations.push({
259
+ area: 'pool',
260
+ level: 'ok',
261
+ text: this._translate('pool_insights_observation_not_enough_data'),
262
+ });
263
+ level = 'info';
264
+ }
265
+
266
+ level = observations.reduce((current, entry) => this._raiseLevel(current, entry.level), level);
267
+
268
+ const summaryText = this._buildSummaryText(level, observations, recommendations);
269
+ const now = new Date().toISOString();
270
+
271
+ return {
272
+ status: 'completed',
273
+ level,
274
+ summaryText,
275
+ summaryHtml: this._buildSummaryHtml(level, observations, recommendations),
276
+ summaryJson: {
277
+ status: 'completed',
278
+ level,
279
+ reason,
280
+ last_update: now,
281
+ inputs: snapshot,
282
+ observations,
283
+ recommendations,
284
+ },
285
+ observations,
286
+ recommendations,
287
+ lastUpdate: now,
288
+ reason,
289
+ };
290
+ },
291
+
292
+ _getTemperatureDelta(temperature) {
293
+ if (temperature.surfaceMaxToday === null || temperature.surfaceMinToday === null) {
294
+ return null;
295
+ }
296
+
297
+ return Math.max(0, temperature.surfaceMaxToday - temperature.surfaceMinToday);
298
+ },
299
+
300
+ _appendSolarObservations(solar, observations) {
301
+ if (solar.ranToday === true) {
302
+ observations.push({
303
+ area: 'solar',
304
+ level: 'ok',
305
+ text: this._translate('pool_insights_observation_solar_ran_today'),
306
+ });
307
+ } else if (solar.evaluationAvailable) {
308
+ observations.push({
309
+ area: 'solar',
310
+ level: 'info',
311
+ text: this._translate('pool_insights_observation_solar_not_ran_today'),
312
+ });
313
+ }
314
+
315
+ if (solar.estimatedGainTodayKwh !== null && solar.estimatedGainTodayKwh > 0) {
316
+ observations.push({
317
+ area: 'solar',
318
+ level: 'ok',
319
+ text: this._translate('pool_insights_observation_solar_gain_today', {
320
+ kwh: this._round(solar.estimatedGainTodayKwh, 2),
321
+ }),
322
+ });
323
+ } else if (solar.evaluationAvailable && solar.ranToday !== true) {
324
+ observations.push({
325
+ area: 'solar',
326
+ level: 'info',
327
+ text: this._translate('pool_insights_observation_solar_evaluation_available'),
328
+ });
329
+ }
330
+ },
331
+
332
+ _appendPhotovoltaicObservations(photovoltaic, observations) {
333
+ if (photovoltaic.activeToday === true) {
334
+ observations.push({
335
+ area: 'photovoltaic',
336
+ level: 'ok',
337
+ text: this._translate('pool_insights_observation_pv_used_today'),
338
+ });
339
+ } else if (photovoltaic.evaluationAvailable) {
340
+ observations.push({
341
+ area: 'photovoltaic',
342
+ level: 'info',
343
+ text: this._translate('pool_insights_observation_pv_evaluation_available'),
344
+ });
345
+ }
346
+
347
+ if (photovoltaic.runtimeTodayMin !== null && photovoltaic.runtimeTodayMin > 0) {
348
+ observations.push({
349
+ area: 'photovoltaic',
350
+ level: 'ok',
351
+ text: this._translate('pool_insights_observation_pv_runtime_today', {
352
+ minutes: this._round(photovoltaic.runtimeTodayMin, 0),
353
+ }),
354
+ });
355
+ }
356
+ },
357
+
358
+ _appendConsumptionObservations(consumption, observations) {
359
+ if (consumption.dayKwh !== null) {
360
+ observations.push({
361
+ area: 'energy',
362
+ level: 'ok',
363
+ text: this._translate('pool_insights_observation_consumption_today', {
364
+ kwh: this._round(consumption.dayKwh, 2),
365
+ }),
366
+ });
367
+ }
368
+
369
+ if (consumption.dayEur !== null) {
370
+ observations.push({
371
+ area: 'energy',
372
+ level: 'ok',
373
+ text: this._translate('pool_insights_observation_costs_today', {
374
+ eur: this._round(consumption.dayEur, 2),
375
+ }),
376
+ });
377
+ }
378
+ },
379
+
380
+ _appendChemistryObservations(chemistry, observations) {
381
+ const chemistryEvaluations = [
382
+ {
383
+ area: 'chemistry_ph',
384
+ available: chemistry.phAvailable,
385
+ key: 'pool_insights_observation_ph_evaluation_available',
386
+ },
387
+ {
388
+ area: 'chemistry_tds',
389
+ available: chemistry.tdsAvailable,
390
+ key: 'pool_insights_observation_tds_evaluation_available',
391
+ },
392
+ {
393
+ area: 'chemistry_orp',
394
+ available: chemistry.orpAvailable,
395
+ key: 'pool_insights_observation_orp_evaluation_available',
396
+ },
397
+ ];
398
+
399
+ for (const entry of chemistryEvaluations) {
400
+ if (!entry.available) {
401
+ continue;
402
+ }
403
+
404
+ observations.push({
405
+ area: entry.area,
406
+ level: 'info',
407
+ text: this._translate(entry.key),
408
+ });
409
+ }
410
+ },
411
+
412
+ _hasLimitedCoreData(snapshot) {
413
+ return (
414
+ this._getTemperatureDelta(snapshot.temperature) === null ||
415
+ snapshot.pump.runtimeTodaySeconds === null ||
416
+ snapshot.pump.startCountToday === null ||
417
+ snapshot.consumption.dayKwh === null
418
+ );
419
+ },
420
+
421
+ _buildSummaryText(level, observations, recommendations) {
422
+ const lead = {
423
+ ok: this._translate('pool_insights_summary_ok'),
424
+ info: this._translate('pool_insights_summary_info'),
425
+ warning: this._translate('pool_insights_summary_warning'),
426
+ }[level];
427
+
428
+ const parts = [lead || this._translate('pool_insights_summary_completed')];
429
+ for (const entry of observations.slice(0, 4)) {
430
+ parts.push(entry.text);
431
+ }
432
+ for (const entry of recommendations.slice(0, 2)) {
433
+ parts.push(entry.text);
434
+ }
435
+
436
+ return parts.join(' ');
437
+ },
438
+
439
+ _buildSummaryHtml(level, observations, recommendations) {
440
+ const items = observations.map(entry => `<li>${this._escapeHtml(entry.text)}</li>`).join('');
441
+ const recommendationItems = recommendations.map(entry => `<li>${this._escapeHtml(entry.text)}</li>`).join('');
442
+
443
+ return [
444
+ `<div class="pool-insights pool-insights-${this._escapeHtml(level)}">`,
445
+ `<strong>${this._escapeHtml(level)}</strong>`,
446
+ items ? `<ul>${items}</ul>` : '',
447
+ recommendationItems
448
+ ? `<p>${this._translate('pool_insights_label_recommendations')}:</p><ul>${recommendationItems}</ul>`
449
+ : '',
450
+ '</div>',
451
+ ].join('');
452
+ },
453
+
454
+ async _writeResult(result) {
455
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, result.status);
456
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, result.level);
457
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, result.summaryText);
458
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_html`, result.summaryHtml);
459
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_json`, JSON.stringify(result.summaryJson));
460
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, JSON.stringify(result.observations));
461
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.recommendations_json`, JSON.stringify(result.recommendations));
462
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, result.lastUpdate);
463
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, result.reason);
464
+ },
465
+
466
+ async _writeDisabled(reason) {
467
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'disabled');
468
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, 'none');
469
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, '');
470
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_html`, '');
471
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_json`, '{}');
472
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, '[]');
473
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.recommendations_json`, '[]');
474
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, new Date().toISOString());
475
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, reason);
476
+ },
477
+
478
+ async _writeError(reason, err) {
479
+ const now = new Date().toISOString();
480
+ const text = this._translate('pool_insights_error_analysis_failed');
481
+
482
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'error');
483
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, 'warning');
484
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, text);
485
+ await this._setState(
486
+ `${POOL_INSIGHTS_PREFIX}.summary_html`,
487
+ `<div class="pool-insights-error">${this._escapeHtml(text)}</div>`,
488
+ );
489
+ await this._setState(
490
+ `${POOL_INSIGHTS_PREFIX}.summary_json`,
491
+ JSON.stringify({
492
+ status: 'error',
493
+ level: 'warning',
494
+ reason,
495
+ last_update: now,
496
+ error: err?.message || 'unknown',
497
+ }),
498
+ );
499
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, '[]');
500
+ await this._setState(
501
+ `${POOL_INSIGHTS_PREFIX}.recommendations_json`,
502
+ JSON.stringify([
503
+ {
504
+ area: 'pool',
505
+ level: 'warning',
506
+ text: this._translate('pool_insights_error_check_log'),
507
+ },
508
+ ]),
509
+ );
510
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, now);
511
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, reason);
512
+ },
513
+
514
+ async _sendSpeechIfAllowed(result, reason) {
515
+ const sendToSpeech = await this._readBoolean(`${POOL_INSIGHTS_PREFIX}.send_to_speech_queue`);
516
+ if (!sendToSpeech || !result.summaryText) {
517
+ return;
518
+ }
519
+
520
+ if (reason === 'daily') {
521
+ const lastSpeechAt = await this._readString(`${POOL_INSIGHTS_PREFIX}.last_speech_at`);
522
+ const lastSpeechTime = Date.parse(lastSpeechAt || '');
523
+ if (Number.isFinite(lastSpeechTime) && Date.now() - lastSpeechTime < SPEECH_COOLDOWN_MS) {
524
+ this.adapter.log.debug('[poolInsightsHelper] Automatic speech skipped due to cooldown');
525
+ return;
526
+ }
527
+ }
528
+
529
+ // Use the relative state id intentionally so multiple adapter instances write to their own speech.queue.
530
+ await this._setState('speech.queue', result.summaryText, false);
531
+ await this._setState(`${POOL_INSIGHTS_PREFIX}.last_speech_at`, new Date().toISOString());
532
+ },
533
+
534
+ async _readState(id) {
535
+ try {
536
+ return await this.adapter.getStateAsync(id);
537
+ } catch {
538
+ return null;
539
+ }
540
+ },
541
+
542
+ async _readString(id) {
543
+ const state = await this._readState(id);
544
+ if (state === null || state.val === null || state.val === undefined || state.val === '') {
545
+ return '';
546
+ }
547
+ return String(state.val);
548
+ },
549
+
550
+ async _readNumber(id) {
551
+ const state = await this._readState(id);
552
+ if (state === null || state.val === null || state.val === undefined || state.val === '') {
553
+ return null;
554
+ }
555
+
556
+ const value = Number(state.val);
557
+ return Number.isFinite(value) ? value : null;
558
+ },
559
+
560
+ async _readBoolean(id) {
561
+ const state = await this._readState(id);
562
+ return state?.val === true;
563
+ },
564
+
565
+ async _hasValue(id) {
566
+ return (await this._readString(id)) !== '';
567
+ },
568
+
569
+ async _setState(id, val, ack = true) {
570
+ await this.adapter.setStateChangedAsync(id, { val, ack });
571
+ },
572
+
573
+ _formatRuntime(seconds) {
574
+ const totalMinutes = Math.max(0, Math.round(seconds / 60));
575
+ const hours = Math.floor(totalMinutes / 60);
576
+ const minutes = totalMinutes % 60;
577
+ if (hours > 0) {
578
+ return `${hours} h ${minutes} min`;
579
+ }
580
+ return `${minutes} min`;
581
+ },
582
+
583
+ _raiseLevel(current, candidate) {
584
+ const order = { ok: 0, info: 1, warning: 2 };
585
+ return order[candidate] > order[current] ? candidate : current;
586
+ },
587
+
588
+ _round(value, digits) {
589
+ const factor = 10 ** digits;
590
+ return Math.round(value * factor) / factor;
591
+ },
592
+
593
+ _translate(key, replacements = {}) {
594
+ let text = I18n.translate(key);
595
+ for (const [name, value] of Object.entries(replacements)) {
596
+ text = text.replace(`{${name}}`, String(value));
597
+ }
598
+ return text;
599
+ },
600
+
601
+ _escapeHtml(text) {
602
+ return String(text)
603
+ .replace(/&/g, '&amp;')
604
+ .replace(/</g, '&lt;')
605
+ .replace(/>/g, '&gt;')
606
+ .replace(/"/g, '&quot;')
607
+ .replace(/'/g, '&#39;');
608
+ },
609
+
610
+ cleanup() {
611
+ if (this.dailyTimer) {
612
+ this.adapter.clearTimeout(this.dailyTimer);
613
+ this.dailyTimer = null;
614
+ }
615
+ this.running = false;
616
+ this.adapter && this.adapter.log.debug('[poolInsightsHelper] Cleanup completed');
617
+ },
618
+ };
619
+
620
+ module.exports = poolInsightsHelper;
@@ -250,13 +250,24 @@ const pumpHelper = {
250
250
  const power = this._parseNumber((await this.adapter.getStateAsync('pump.current_power'))?.val);
251
251
  const maxWatt = this._parseNumber((await this.adapter.getStateAsync('pump.pump_max_watt'))?.val);
252
252
 
253
- // --- NEU: Kulanzzeiten für Start/Stop ---
254
- const graceOnMs = 5000; // 5 Sekunden nach Start ignorieren
253
+ // NEU: Konfigurierbarer Sicherheits-Timeout für Leistungsprüfung nach Pumpenstart
254
+ const startupPowerCheckTimeoutState = await this.adapter.getStateAsync(
255
+ 'pump.startup_power_check_timeout_sec',
256
+ );
257
+
258
+ const startupPowerCheckTimeoutSec = Math.min(
259
+ 10,
260
+ Math.max(5, this._parseNumber(startupPowerCheckTimeoutState?.val) || 5),
261
+ );
262
+
263
+ const graceOnMs = startupPowerCheckTimeoutSec * 1000;
255
264
  const graceOffMs = 5000; // 5 Sekunden nach Stop ignorieren
256
265
  const now = Date.now();
257
266
 
258
267
  if (active === true && this._lastPumpStart && now - this._lastPumpStart < graceOnMs) {
259
- this.adapter.log.debug('[pumpHelper] Within start grace period – skipping error check');
268
+ this.adapter.log.debug(
269
+ `[pumpHelper] Within start grace period (${startupPowerCheckTimeoutSec}s) – skipping error check`,
270
+ );
260
271
  return;
261
272
  }
262
273
 
@@ -325,10 +336,7 @@ const pumpHelper = {
325
336
  },
326
337
 
327
338
  cleanup() {
328
- if (this.checkTimer) {
329
- clearInterval(this.checkTimer);
330
- this.checkTimer = null;
331
- }
339
+ // No active timers to clean up.
332
340
  },
333
341
  };
334
342
 
@@ -416,7 +416,8 @@ const statisticsHelper = {
416
416
  async _scheduleMidnightReset() {
417
417
  const adapter = this.adapter;
418
418
  if (this.midnightTimer) {
419
- clearTimeout(this.midnightTimer);
419
+ this.adapter.clearTimeout(this.midnightTimer);
420
+ this.midnightTimer = null;
420
421
  }
421
422
 
422
423
  const now = new Date();
@@ -424,7 +425,9 @@ const statisticsHelper = {
424
425
  nextMidnight.setHours(24, 0, 5, 0);
425
426
  const msUntilMidnight = nextMidnight.getTime() - now.getTime();
426
427
 
427
- this.midnightTimer = setTimeout(async () => {
428
+ this.midnightTimer = this.adapter.setTimeout(async () => {
429
+ this.midnightTimer = null;
430
+
428
431
  await this._resetDailyTemperatureStats();
429
432
  await this._scheduleMidnightReset();
430
433
  }, msUntilMidnight);
@@ -523,7 +526,8 @@ const statisticsHelper = {
523
526
 
524
527
  cleanup() {
525
528
  if (this.midnightTimer) {
526
- clearTimeout(this.midnightTimer);
529
+ this.adapter.clearTimeout(this.midnightTimer);
530
+ this.midnightTimer = null;
527
531
  }
528
532
  },
529
533
  };