iobroker.poolcontrol 1.3.14 → 1.3.17

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.
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const { I18n } = require('@iobroker/adapter-core');
4
+ const MAX_HISTORY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
5
+ const MIN_SAMPLE_INTERVAL_MS = 15 * 60 * 1000;
4
6
 
5
7
  /**
6
8
  * chemistryPhHelper
@@ -107,6 +109,11 @@ const chemistryPhHelper = {
107
109
  return;
108
110
  }
109
111
 
112
+ if (id.endsWith('chemistry.ph.input.manual_value') && state.ack === false) {
113
+ await this._processValue('manual', state.val, 'manual_value');
114
+ return;
115
+ }
116
+
110
117
  if (this._isRelevantOwnState(id)) {
111
118
  this._scheduleEvaluation(`state_change:${id}`, 250);
112
119
  }
@@ -368,8 +375,14 @@ const chemistryPhHelper = {
368
375
  await this._setBool('chemistry.ph.measurement.allowed', true);
369
376
  await this._setString('chemistry.ph.measurement.ignored_reason', '');
370
377
 
371
- await this._updateHistory(value, now);
372
- await this._evaluateValue(value);
378
+ await this._updateLastValues(value, now);
379
+
380
+ const history = await this._updateHistory(value, now, reason === 'external_state' || reason === 'manual_value');
381
+ const trend = await this._calculateTrend(value, now, history);
382
+ const evaluation = await this._evaluateValue(value);
383
+
384
+ await this._writeTrend(trend);
385
+ await this._writeOutputs(value, trend, evaluation);
373
386
 
374
387
  await this._setString('chemistry.ph.debug.last_reason', reason || 'value_processed');
375
388
  await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(now));
@@ -427,7 +440,7 @@ const chemistryPhHelper = {
427
440
  return { allowed: true, reason: '', status: 'ok', recommendation: '' };
428
441
  },
429
442
 
430
- async _updateHistory(value, now) {
443
+ async _updateLastValues(value, now) {
431
444
  const lastValid = await this._readNumberOrNull('chemistry.ph.input.last_valid_value');
432
445
  const lastValidAt = await this._readString('chemistry.ph.input.last_valid_value_at');
433
446
 
@@ -447,40 +460,255 @@ const chemistryPhHelper = {
447
460
  await this._setString('chemistry.ph.input.last_valid_value_at', this._formatDateTime(now));
448
461
  },
449
462
 
463
+ async _updateHistory(value, now, forceSample) {
464
+ let samples = await this._readJsonArray('chemistry.ph.history.samples_json');
465
+ const nowTs = now.getTime();
466
+ const minTs = nowTs - MAX_HISTORY_AGE_MS;
467
+
468
+ samples = samples.filter(
469
+ sample => sample && Number(sample.ts) >= minTs && Number.isFinite(Number(sample.value)),
470
+ );
471
+
472
+ const newest = samples.length ? samples[samples.length - 1] : null;
473
+ const newestTs = newest ? Number(newest.ts) : 0;
474
+ const shouldStore = forceSample || !newest || nowTs - newestTs >= MIN_SAMPLE_INTERVAL_MS;
475
+
476
+ if (shouldStore) {
477
+ samples.push({
478
+ ts: nowTs,
479
+ time: this._formatDateTime(now),
480
+ value,
481
+ });
482
+ }
483
+
484
+ samples = samples.filter(sample => sample && Number(sample.ts) >= minTs);
485
+
486
+ await this._setString('chemistry.ph.history.samples_json', JSON.stringify(samples));
487
+ await this._setNumber('chemistry.ph.history.samples_count', samples.length);
488
+
489
+ if (samples.length) {
490
+ await this._setString(
491
+ 'chemistry.ph.history.oldest_sample_at',
492
+ samples[0].time || this._formatDateTime(new Date(samples[0].ts)),
493
+ );
494
+ await this._setString(
495
+ 'chemistry.ph.history.newest_sample_at',
496
+ samples[samples.length - 1].time || this._formatDateTime(new Date(samples[samples.length - 1].ts)),
497
+ );
498
+ }
499
+
500
+ return samples;
501
+ },
502
+
503
+ async _calculateTrend(currentValue, now, samples) {
504
+ const nowTs = now.getTime();
505
+
506
+ const ref24h = this._findReferenceSample(samples, nowTs - 24 * 60 * 60 * 1000);
507
+ const ref7d = this._findReferenceSample(samples, nowTs - 7 * 24 * 60 * 60 * 1000);
508
+ const ref30d = this._findReferenceSample(samples, nowTs - 30 * 24 * 60 * 60 * 1000);
509
+
510
+ const delta24h = ref24h ? currentValue - ref24h.value : 0;
511
+ const delta7d = ref7d ? currentValue - ref7d.value : 0;
512
+ const delta30d = ref30d ? currentValue - ref30d.value : 0;
513
+
514
+ const direction = this._getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d);
515
+ const status = this._getTrendStatus(delta24h, delta7d, delta30d, ref24h, ref7d, ref30d);
516
+
517
+ return {
518
+ ref24h,
519
+ ref7d,
520
+ ref30d,
521
+ delta24h,
522
+ delta7d,
523
+ delta30d,
524
+ direction,
525
+ status,
526
+ };
527
+ },
528
+
529
+ _findReferenceSample(samples, targetTs) {
530
+ if (!samples.length) {
531
+ return null;
532
+ }
533
+
534
+ let best = null;
535
+ let bestDistance = Number.MAX_SAFE_INTEGER;
536
+
537
+ for (const sample of samples) {
538
+ const ts = Number(sample.ts);
539
+ const value = Number(sample.value);
540
+
541
+ if (!Number.isFinite(ts) || !Number.isFinite(value)) {
542
+ continue;
543
+ }
544
+
545
+ const distance = Math.abs(ts - targetTs);
546
+
547
+ if (distance < bestDistance) {
548
+ bestDistance = distance;
549
+ best = {
550
+ ts,
551
+ value,
552
+ time: sample.time || this._formatDateTime(new Date(ts)),
553
+ };
554
+ }
555
+ }
556
+
557
+ return best;
558
+ },
559
+
560
+ _getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d) {
561
+ const available = [ref24h ? delta24h : null, ref7d ? delta7d : null, ref30d ? delta30d : null].filter(
562
+ value => value !== null,
563
+ );
564
+
565
+ if (!available.length) {
566
+ return 'not_enough_data';
567
+ }
568
+
569
+ const strongest = available.reduce((prev, current) => (Math.abs(current) > Math.abs(prev) ? current : prev), 0);
570
+
571
+ if (Math.abs(strongest) < 0.05) {
572
+ return 'stable';
573
+ }
574
+
575
+ return strongest > 0 ? 'rising' : 'falling';
576
+ },
577
+
578
+ _getTrendStatus(delta24h, delta7d, delta30d, ref24h, ref7d, ref30d) {
579
+ if (!ref24h && !ref7d && !ref30d) {
580
+ return 'not_enough_data';
581
+ }
582
+
583
+ if (delta24h > 0.2 || delta7d > 0.4 || delta30d > 0.6) {
584
+ return 'rising_fast';
585
+ }
586
+
587
+ if (delta24h > 0.1 || delta7d > 0.25 || delta30d > 0.4) {
588
+ return 'rising_noticeable';
589
+ }
590
+
591
+ if (delta24h > 0.05 || delta7d > 0.15 || delta30d > 0.25) {
592
+ return 'rising_slowly';
593
+ }
594
+
595
+ if (delta24h < -0.05 || delta7d < -0.15 || delta30d < -0.25) {
596
+ return 'falling';
597
+ }
598
+
599
+ return 'stable';
600
+ },
601
+
602
+ async _writeTrend(trend) {
603
+ await this._setNumber('chemistry.ph.trend.reference_24h_value', trend.ref24h ? trend.ref24h.value : 0);
604
+ await this._setString('chemistry.ph.trend.reference_24h_at', trend.ref24h ? trend.ref24h.time : '');
605
+ await this._setNumber('chemistry.ph.trend.delta_24h', trend.ref24h ? trend.delta24h : 0);
606
+
607
+ await this._setNumber('chemistry.ph.trend.reference_7d_value', trend.ref7d ? trend.ref7d.value : 0);
608
+ await this._setString('chemistry.ph.trend.reference_7d_at', trend.ref7d ? trend.ref7d.time : '');
609
+ await this._setNumber('chemistry.ph.trend.delta_7d', trend.ref7d ? trend.delta7d : 0);
610
+
611
+ await this._setNumber('chemistry.ph.trend.reference_30d_value', trend.ref30d ? trend.ref30d.value : 0);
612
+ await this._setString('chemistry.ph.trend.reference_30d_at', trend.ref30d ? trend.ref30d.time : '');
613
+ await this._setNumber('chemistry.ph.trend.delta_30d', trend.ref30d ? trend.delta30d : 0);
614
+
615
+ await this._setString('chemistry.ph.trend.direction', trend.direction);
616
+ await this._setString('chemistry.ph.trend.status', trend.status);
617
+ },
618
+
619
+ async _writeOutputs(value, trend, evaluation) {
620
+ const text =
621
+ `${I18n.translate('Current pH value')}: ${value.toFixed(2)}. ` +
622
+ `24h: ${trend.ref24h ? this._formatDelta(trend.delta24h) : I18n.translate('not enough data')}, ` +
623
+ `7d: ${trend.ref7d ? this._formatDelta(trend.delta7d) : I18n.translate('not enough data')}, ` +
624
+ `30d: ${trend.ref30d ? this._formatDelta(trend.delta30d) : I18n.translate('not enough data')}. ` +
625
+ `${evaluation.recommendation}`;
626
+
627
+ const html =
628
+ `<div>` +
629
+ `<b>${I18n.translate('Current pH value')}:</b> ${value.toFixed(2)}<br>` +
630
+ `<b>24h:</b> ${trend.ref24h ? `${trend.ref24h.value.toFixed(2)} / ${this._formatDelta(trend.delta24h)}` : I18n.translate('not enough data')}<br>` +
631
+ `<b>7d:</b> ${trend.ref7d ? `${trend.ref7d.value.toFixed(2)} / ${this._formatDelta(trend.delta7d)}` : I18n.translate('not enough data')}<br>` +
632
+ `<b>30d:</b> ${trend.ref30d ? `${trend.ref30d.value.toFixed(2)} / ${this._formatDelta(trend.delta30d)}` : I18n.translate('not enough data')}<br>` +
633
+ `<b>${I18n.translate('Trend status')}:</b> ${trend.status}<br>` +
634
+ `<b>${I18n.translate('Status')}:</b> ${evaluation.status}<br>` +
635
+ `<b>${I18n.translate('Recommendation')}:</b> ${this._escapeHtml(evaluation.recommendation)}` +
636
+ `</div>`;
637
+
638
+ const json = {
639
+ current: Number(value.toFixed(2)),
640
+ unit: 'pH',
641
+ trend_24h: {
642
+ reference: trend.ref24h ? Number(trend.ref24h.value.toFixed(2)) : null,
643
+ delta: trend.ref24h ? Number(trend.delta24h.toFixed(2)) : null,
644
+ },
645
+ trend_7d: {
646
+ reference: trend.ref7d ? Number(trend.ref7d.value.toFixed(2)) : null,
647
+ delta: trend.ref7d ? Number(trend.delta7d.toFixed(2)) : null,
648
+ },
649
+ trend_30d: {
650
+ reference: trend.ref30d ? Number(trend.ref30d.value.toFixed(2)) : null,
651
+ delta: trend.ref30d ? Number(trend.delta30d.toFixed(2)) : null,
652
+ },
653
+ direction: trend.direction,
654
+ trend_status: trend.status,
655
+ status: evaluation.status,
656
+ action_required: evaluation.actionRequired,
657
+ recommendation: evaluation.recommendation,
658
+ };
659
+
660
+ await this._setString('chemistry.ph.outputs.summary_text', text);
661
+ await this._setString('chemistry.ph.outputs.summary_html', html);
662
+ await this._setString('chemistry.ph.outputs.summary_json', JSON.stringify(json));
663
+ },
664
+
450
665
  async _evaluateValue(value) {
451
666
  const min = await this._readNumber('chemistry.ph.evaluation.target_min');
452
667
  const max = await this._readNumber('chemistry.ph.evaluation.target_max');
453
668
 
454
669
  if (value < min) {
455
- await this._setString('chemistry.ph.evaluation.status', 'low');
456
- await this._setString(
457
- 'chemistry.ph.evaluation.recommendation',
458
- I18n.translate(
459
- 'pH value is too low. Check whether pH plus should be added according to the product instructions. Then circulate and measure again.',
460
- ),
670
+ const recommendation = I18n.translate(
671
+ 'pH value is too low. Check whether pH plus should be added according to the product instructions. Then circulate and measure again.',
461
672
  );
673
+
674
+ await this._setString('chemistry.ph.evaluation.status', 'low');
675
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
462
676
  await this._setBool('chemistry.ph.evaluation.action_required', true);
463
- return;
677
+
678
+ return {
679
+ status: 'low',
680
+ actionRequired: true,
681
+ recommendation,
682
+ };
464
683
  }
465
684
 
466
685
  if (value > max) {
467
- await this._setString('chemistry.ph.evaluation.status', 'high');
468
- await this._setString(
469
- 'chemistry.ph.evaluation.recommendation',
470
- I18n.translate(
471
- 'pH value is too high. Check whether pH minus should be added according to the product instructions. Then circulate and measure again.',
472
- ),
686
+ const recommendation = I18n.translate(
687
+ 'pH value is too high. Check whether pH minus should be added according to the product instructions. Then circulate and measure again.',
473
688
  );
689
+
690
+ await this._setString('chemistry.ph.evaluation.status', 'high');
691
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
474
692
  await this._setBool('chemistry.ph.evaluation.action_required', true);
475
- return;
693
+
694
+ return {
695
+ status: 'high',
696
+ actionRequired: true,
697
+ recommendation,
698
+ };
476
699
  }
477
700
 
701
+ const recommendation = I18n.translate('pH value is within the target range. No action is required.');
702
+
478
703
  await this._setString('chemistry.ph.evaluation.status', 'ok');
479
- await this._setString(
480
- 'chemistry.ph.evaluation.recommendation',
481
- I18n.translate('pH value is within the target range. No action is required.'),
482
- );
704
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
483
705
  await this._setBool('chemistry.ph.evaluation.action_required', false);
706
+
707
+ return {
708
+ status: 'ok',
709
+ actionRequired: false,
710
+ recommendation,
711
+ };
484
712
  },
485
713
 
486
714
  async _startMixingRun() {
@@ -641,6 +869,17 @@ const chemistryPhHelper = {
641
869
  return Number.isFinite(value) ? value : null;
642
870
  },
643
871
 
872
+ async _readJsonArray(id) {
873
+ const state = await this.adapter.getStateAsync(id);
874
+
875
+ try {
876
+ const parsed = JSON.parse(String(state?.val || '[]'));
877
+ return Array.isArray(parsed) ? parsed : [];
878
+ } catch {
879
+ return [];
880
+ }
881
+ },
882
+
644
883
  async _readBoolean(id) {
645
884
  const state = await this.adapter.getStateAsync(id);
646
885
  return !!state?.val;
@@ -659,6 +898,11 @@ const chemistryPhHelper = {
659
898
  await this.adapter.setStateChangedAsync(id, { val: !!value, ack: true });
660
899
  },
661
900
 
901
+ _formatDelta(value) {
902
+ const rounded = Number(value) || 0;
903
+ return `${rounded >= 0 ? '+' : ''}${rounded.toFixed(2)}`;
904
+ },
905
+
662
906
  _formatDateTime(date) {
663
907
  return date.toLocaleString('de-DE', {
664
908
  year: 'numeric',
@@ -681,6 +925,15 @@ const chemistryPhHelper = {
681
925
  return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
682
926
  },
683
927
 
928
+ _escapeHtml(value) {
929
+ return String(value ?? '')
930
+ .replace(/&/g, '&amp;')
931
+ .replace(/</g, '&lt;')
932
+ .replace(/>/g, '&gt;')
933
+ .replace(/"/g, '&quot;')
934
+ .replace(/'/g, '&#039;');
935
+ },
936
+
684
937
  cleanup() {
685
938
  if (this.evalTimer) {
686
939
  this.adapter.clearTimeout(this.evalTimer);
@@ -53,27 +53,37 @@ const pumpHelper2 = {
53
53
  * @param {ioBroker.State | null | undefined} state - Neuer Statewert
54
54
  */
55
55
  async handleStateChange(id, state) {
56
- if (!state || state.ack === false) {
56
+ if (!state) {
57
57
  return;
58
58
  }
59
59
 
60
60
  // Leistungsänderung → reelle Durchflusswerte aktualisieren
61
61
  if (id.endsWith('pump.current_power')) {
62
+ if (state.ack === false) {
63
+ return;
64
+ }
65
+
62
66
  await this._updateLiveValues();
67
+ return;
63
68
  }
64
69
 
65
- // Pumpenstatus-Änderung → letzten Durchflusswert sichern
70
+ // Pumpenstatus-Änderung → Livewerte aktualisieren bzw. letzten Durchflusswert sichern
66
71
  if (id.endsWith('pump.pump_switch')) {
67
72
  const pumpOn = state.val === true;
68
- if (!pumpOn) {
69
- // FIX: Verwende den zuletzt gemerkten Wert, statt live zu lesen (verhindert 0-Durchfluss)
70
- const flowBeforeStop = this.lastKnownFlow;
71
- if (flowBeforeStop > 0) {
72
- await this._setIfChanged('pump.live.last_flow_lh', flowBeforeStop);
73
- this.adapter.log.debug(`[pumpHelper2] FIX: Last flow value stored: ${flowBeforeStop} l/h`);
74
- } else {
75
- this.adapter.log.debug('[pumpHelper2] No stored flow value available.');
76
- }
73
+
74
+ if (pumpOn) {
75
+ // FIX: Helper-driven pump starts use ack=false, but live flow must still be recalculated.
76
+ await this._updateLiveValues();
77
+ return;
78
+ }
79
+
80
+ // FIX: Verwende den zuletzt gemerkten Wert, statt live zu lesen (verhindert 0-Durchfluss)
81
+ const flowBeforeStop = this.lastKnownFlow;
82
+ if (flowBeforeStop > 0) {
83
+ await this._setIfChanged('pump.live.last_flow_lh', flowBeforeStop);
84
+ this.adapter.log.debug(`[pumpHelper2] FIX: Last flow value stored: ${flowBeforeStop} l/h`);
85
+ } else {
86
+ this.adapter.log.debug('[pumpHelper2] No stored flow value available.');
77
87
  }
78
88
  }
79
89
  },
@@ -77,6 +77,14 @@ const speechTextHelper = {
77
77
  if (id.endsWith('solar.collector_warning')) {
78
78
  const val = !!state.val;
79
79
 
80
+ const warnSpeech = !!(await this.adapter.getStateAsync('solar.warn_speech'))?.val;
81
+ if (!warnSpeech) {
82
+ this.adapter.log.debug(
83
+ '[speechTextHelper] Solar warning speech skipped (solar.warn_speech=false).',
84
+ );
85
+ return;
86
+ }
87
+
80
88
  if (val) {
81
89
  // Neue Warnung aktiv
82
90
  const collectorTemp = Number(
@@ -100,6 +108,8 @@ const speechTextHelper = {
100
108
  // --- NEU: Reaktion auf Solarsteuerung ---
101
109
  if (id.endsWith('speech.solar_active')) {
102
110
  const val = !!state.val;
111
+ const canSpeak = await this._canSendFromSource('solar');
112
+
103
113
  // Pumpenstatus aktualisieren, damit auch im VIS korrekt sichtbar
104
114
  if (val) {
105
115
  await this.adapter.setStateAsync('pump.status', {
@@ -114,12 +124,20 @@ const speechTextHelper = {
114
124
  }
115
125
  if (val) {
116
126
  const text = 'Die Poolpumpe wurde durch die Solarsteuerung eingeschaltet.';
117
- await this._sendSpeech(text);
118
- this.adapter.log.debug('[speechTextHelper] Solar control activated → announcement sent.');
127
+ const sent = await this._sendSpeechFromSource('solar', text, canSpeak);
128
+ this.adapter.log.debug(
129
+ sent
130
+ ? '[speechTextHelper] Solar control activated → announcement sent.'
131
+ : '[speechTextHelper] Solar control activated → announcement skipped.',
132
+ );
119
133
  } else {
120
134
  const text = 'Solarsteuerung beendet – Poolpumpe ausgeschaltet.';
121
- await this._sendSpeech(text);
122
- this.adapter.log.debug('[speechTextHelper] Solar control deactivated → announcement sent.');
135
+ const sent = await this._sendSpeechFromSource('solar', text, canSpeak);
136
+ this.adapter.log.debug(
137
+ sent
138
+ ? '[speechTextHelper] Solar control deactivated → announcement sent.'
139
+ : '[speechTextHelper] Solar control deactivated → announcement skipped.',
140
+ );
123
141
  }
124
142
  return;
125
143
  }
@@ -127,6 +145,7 @@ const speechTextHelper = {
127
145
  // --- NEU: Reaktion auf Zeitsteuerung ---
128
146
  if (id.endsWith('speech.time_active')) {
129
147
  const val = !!state.val;
148
+ const canSpeak = await this._canSendFromSource('time');
130
149
 
131
150
  // Pumpenstatus mitpflegen
132
151
  if (val) {
@@ -135,16 +154,24 @@ const speechTextHelper = {
135
154
  ack: true,
136
155
  });
137
156
  const text = 'Die Poolpumpe wurde durch die Zeitsteuerung eingeschaltet.';
138
- await this._sendSpeech(text);
139
- this.adapter.log.debug('[speechTextHelper] Time control activated → announcement sent.');
157
+ const sent = await this._sendSpeechFromSource('time', text, canSpeak);
158
+ this.adapter.log.debug(
159
+ sent
160
+ ? '[speechTextHelper] Time control activated → announcement sent.'
161
+ : '[speechTextHelper] Time control activated → announcement skipped.',
162
+ );
140
163
  } else {
141
164
  await this.adapter.setStateAsync('pump.status', {
142
165
  val: 'AUS (Zeitsteuerung beendet)',
143
166
  ack: true,
144
167
  });
145
168
  const text = 'Zeitsteuerung beendet – Poolpumpe ausgeschaltet.';
146
- await this._sendSpeech(text);
147
- this.adapter.log.debug('[speechTextHelper] Time control deactivated → announcement sent.');
169
+ const sent = await this._sendSpeechFromSource('time', text, canSpeak);
170
+ this.adapter.log.debug(
171
+ sent
172
+ ? '[speechTextHelper] Time control deactivated → announcement sent.'
173
+ : '[speechTextHelper] Time control deactivated → announcement skipped.',
174
+ );
148
175
  }
149
176
  return;
150
177
  }
@@ -155,6 +182,54 @@ const speechTextHelper = {
155
182
  }
156
183
  },
157
184
 
185
+ async _canSendFromSource(source) {
186
+ const enabled = !!(await this.adapter.getStateAsync(`speech.sources.${source}.enabled`))?.val;
187
+ if (!enabled) {
188
+ this.adapter.log.debug(`[speechTextHelper] Speech source "${source}" is disabled.`);
189
+ return false;
190
+ }
191
+
192
+ const cooldownMinutes = Number(
193
+ (await this.adapter.getStateAsync(`speech.sources.${source}.cooldown_minutes`))?.val || 0,
194
+ );
195
+ if (cooldownMinutes <= 0) {
196
+ return true;
197
+ }
198
+
199
+ const lastSentRaw = (await this.adapter.getStateAsync(`speech.sources.${source}.last_sent`))?.val;
200
+ const lastSentTs = Date.parse(String(lastSentRaw || ''));
201
+
202
+ if (!Number.isFinite(lastSentTs)) {
203
+ return true;
204
+ }
205
+
206
+ const elapsedMs = Date.now() - lastSentTs;
207
+ const cooldownMs = cooldownMinutes * 60 * 1000;
208
+
209
+ if (elapsedMs < cooldownMs) {
210
+ this.adapter.log.debug(
211
+ `[speechTextHelper] Speech source "${source}" skipped by cooldown (${cooldownMinutes} min).`,
212
+ );
213
+ return false;
214
+ }
215
+
216
+ return true;
217
+ },
218
+
219
+ async _sendSpeechFromSource(source, text, canSpeak) {
220
+ if (!canSpeak) {
221
+ return false;
222
+ }
223
+
224
+ await this._sendSpeech(text);
225
+ await this.adapter.setStateAsync(`speech.sources.${source}.last_sent`, {
226
+ val: new Date().toISOString(),
227
+ ack: true,
228
+ });
229
+
230
+ return true;
231
+ },
232
+
158
233
  /**
159
234
  * Sendet Text an speech.queue.
160
235
  *
package/lib/i18n/de.json CHANGED
@@ -217,7 +217,15 @@
217
217
  "pH mixing run finished. Pump was switched off by the pH helper.": "pH-Mischlauf beendet. Die Pumpe wurde vom pH-Helper ausgeschaltet.",
218
218
  "pH mixing run finished. Pump was not switched off because another helper is active.": "pH-Mischlauf beendet. Die Pumpe wurde nicht ausgeschaltet, weil ein anderer Helper aktiv ist.",
219
219
  "pH mixing run finished. Pump was already running and was not switched off by the pH helper.": "pH-Mischlauf beendet. Die Pumpe lief bereits und wurde vom pH-Helper nicht ausgeschaltet.",
220
+ "pH trend": "pH-Trend",
221
+ "Overall pH status": "Gesamtstatus pH",
220
222
 
223
+ "pH is rising very quickly. Check dosing, alkalinity and water balance.": "Der pH-Wert steigt sehr schnell an. Prüfe Dosierung, Alkalinität und Wasserbalance.",
224
+ "pH is rising noticeably. Observe the trend and check alkalinity and water balance.": "Der pH-Wert steigt merklich an. Beobachte den Trend und prüfe Alkalinität und Wasserbalance.",
225
+ "pH is slowly rising. Continue observing the trend.": "Der pH-Wert steigt langsam an. Beobachte den Trend weiter.",
226
+ "pH is falling. This can be plausible after pH correction or fresh water.": "Der pH-Wert fällt. Das kann nach einer pH-Korrektur oder Frischwasserzugabe plausibel sein.",
227
+
228
+ "Not enough pH history is available yet. Collect more valid measurements.": "Es sind noch nicht genügend pH-Historienwerte vorhanden. Sammle weitere gültige Messwerte.",
221
229
  "No TDS source state configured.": "Kein TDS-Quell-Datenpunkt konfiguriert.",
222
230
  "TDS source state configured.": "TDS-Quell-Datenpunkt konfiguriert.",
223
231
  "TDS source state could not be subscribed.": "TDS-Quell-Datenpunkt konnte nicht abonniert werden.",
@@ -246,5 +254,31 @@
246
254
  "not available": "nicht verfügbar",
247
255
  "Trend status": "Trendstatus",
248
256
  "Overall status": "Gesamtstatus",
249
- "Recommendation": "Empfehlung"
257
+ "Recommendation": "Empfehlung",
258
+
259
+ "No ORP source state configured.": "Kein ORP-Quelldatenpunkt konfiguriert.",
260
+ "ORP source state configured.": "ORP-Quelldatenpunkt konfiguriert.",
261
+ "ORP source state could not be subscribed.": "ORP-Quelldatenpunkt konnte nicht abonniert werden.",
262
+ "ORP input is disabled.": "ORP-Eingang ist deaktiviert.",
263
+ "No valid ORP source is configured.": "Keine gültige ORP-Quelle konfiguriert.",
264
+ "The configured ORP source state does not exist.": "Der konfigurierte ORP-Quelldatenpunkt existiert nicht.",
265
+ "The configured ORP source could not be read.": "Der konfigurierte ORP-Quelldatenpunkt konnte nicht gelesen werden.",
266
+ "Unknown ORP source mode.": "Unbekannter ORP-Quellenmodus.",
267
+ "Manual ORP value is used.": "Manueller ORP-Wert wird verwendet.",
268
+ "External ORP source is valid.": "Externe ORP-Quelle ist gültig.",
269
+ "The ORP value is invalid. Please check the measurement or sensor.": "Der ORP-Wert ist ungültig. Bitte Messung oder Sensor prüfen.",
270
+ "ORP evaluation is waiting for the pool pump because the sensor is in a measurement section.": "Die ORP-Auswertung wartet auf die Poolpumpe, da sich der Sensor in einer Messstrecke befindet.",
271
+ "ORP evaluation is waiting until the measurement section has stabilized after pump start.": "Die ORP-Auswertung wartet, bis sich die Messstrecke nach dem Pumpenstart stabilisiert hat.",
272
+ "ORP trend": "ORP-Trend",
273
+ "ORP value is available, but pH reference is missing, disabled or outside the expected range. ORP interpretation is limited.": "ORP-Wert vorhanden, aber die pH-Referenz fehlt, ist deaktiviert oder außerhalb des erwarteten Bereichs. Die ORP-Interpretation ist eingeschränkt.",
274
+ "ORP value is low. Check pH and chlorine values manually and evaluate the water care situation.": "ORP-Wert niedrig. Bitte pH- und Chlorwerte manuell prüfen und die Wasserpflege bewerten.",
275
+ "ORP value is high. Check whether the measurement is plausible and evaluate the water values together.": "ORP-Wert hoch. Bitte prüfen, ob die Messung plausibel ist und die Wasserwerte gemeinsam bewerten.",
276
+ "ORP value is within the configured reference range.": "ORP-Wert befindet sich im konfigurierten Referenzbereich.",
277
+ "unknown": "unbekannt",
278
+ "not enough data": "nicht genügend Daten",
279
+ "Current ORP value": "Aktueller ORP-Wert",
280
+ "pH reference": "pH-Referenz",
281
+ "Status": "Status",
282
+ "Recommendation": "Empfehlung",
283
+ "ORP evaluation is disabled.": "ORP-Auswertung ist deaktiviert."
250
284
  }
package/lib/i18n/en.json CHANGED
@@ -288,6 +288,14 @@
288
288
  "pH mixing run finished. Pump was switched off by the pH helper.": "pH mixing run finished. Pump was switched off by the pH helper.",
289
289
  "pH mixing run finished. Pump was not switched off because another helper is active.": "pH mixing run finished. Pump was not switched off because another helper is active.",
290
290
  "pH mixing run finished. Pump was already running and was not switched off by the pH helper.": "pH mixing run finished. Pump was already running and was not switched off by the pH helper.",
291
+ "pH trend": "pH trend",
292
+ "Overall pH status": "Overall pH status",
293
+
294
+ "pH is rising very quickly. Check dosing, alkalinity and water balance.": "pH is rising very quickly. Check dosing, alkalinity and water balance.",
295
+ "pH is rising noticeably. Observe the trend and check alkalinity and water balance.": "pH is rising noticeably. Observe the trend and check alkalinity and water balance.",
296
+ "pH is slowly rising. Continue observing the trend.": "pH is slowly rising. Continue observing the trend.",
297
+ "pH is falling. This can be plausible after pH correction or fresh water.": "pH is falling. This can be plausible after pH correction or fresh water.",
298
+ "Not enough pH history is available yet. Collect more valid measurements.": "Not enough pH history is available yet. Collect more valid measurements.",
291
299
 
292
300
  "No TDS source state configured.": "No TDS source state configured.",
293
301
  "TDS source state configured.": "TDS source state configured.",
@@ -317,5 +325,31 @@
317
325
  "not available": "not available",
318
326
  "Trend status": "Trend status",
319
327
  "Overall status": "Overall status",
320
- "Recommendation": "Recommendation"
328
+ "Recommendation": "Recommendation",
329
+
330
+ "No ORP source state configured.": "No ORP source state configured.",
331
+ "ORP source state configured.": "ORP source state configured.",
332
+ "ORP source state could not be subscribed.": "ORP source state could not be subscribed.",
333
+ "ORP input is disabled.": "ORP input is disabled.",
334
+ "No valid ORP source is configured.": "No valid ORP source is configured.",
335
+ "The configured ORP source state does not exist.": "The configured ORP source state does not exist.",
336
+ "The configured ORP source could not be read.": "The configured ORP source could not be read.",
337
+ "Unknown ORP source mode.": "Unknown ORP source mode.",
338
+ "Manual ORP value is used.": "Manual ORP value is used.",
339
+ "External ORP source is valid.": "External ORP source is valid.",
340
+ "The ORP value is invalid. Please check the measurement or sensor.": "The ORP value is invalid. Please check the measurement or sensor.",
341
+ "ORP evaluation is waiting for the pool pump because the sensor is in a measurement section.": "ORP evaluation is waiting for the pool pump because the sensor is in a measurement section.",
342
+ "ORP evaluation is waiting until the measurement section has stabilized after pump start.": "ORP evaluation is waiting until the measurement section has stabilized after pump start.",
343
+ "ORP trend": "ORP trend",
344
+ "ORP value is available, but pH reference is missing, disabled or outside the expected range. ORP interpretation is limited.": "ORP value is available, but pH reference is missing, disabled or outside the expected range. ORP interpretation is limited.",
345
+ "ORP value is low. Check pH and chlorine values manually and evaluate the water care situation.": "ORP value is low. Check pH and chlorine values manually and evaluate the water care situation.",
346
+ "ORP value is high. Check whether the measurement is plausible and evaluate the water values together.": "ORP value is high. Check whether the measurement is plausible and evaluate the water values together.",
347
+ "ORP value is within the configured reference range.": "ORP value is within the configured reference range.",
348
+ "unknown": "unknown",
349
+ "not enough data": "not enough data",
350
+ "Current ORP value": "Current ORP value",
351
+ "pH reference": "pH reference",
352
+ "Status": "Status",
353
+ "Recommendation": "Recommendation",
354
+ "ORP evaluation is disabled.": "ORP evaluation is disabled."
321
355
  }