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.
- package/README.md +76 -100
- package/io-package.json +40 -40
- package/lib/helpers/chemistryOrpHelper.js +805 -0
- package/lib/helpers/chemistryPhHelper.js +274 -21
- package/lib/helpers/pumpHelper2.js +21 -11
- package/lib/helpers/speechTextHelper.js +83 -8
- package/lib/i18n/de.json +35 -1
- package/lib/i18n/en.json +35 -1
- package/lib/i18n/es.json +31 -1
- package/lib/i18n/fr.json +31 -1
- package/lib/i18n/it.json +31 -1
- package/lib/i18n/nl.json +31 -1
- package/lib/i18n/pl.json +31 -1
- package/lib/i18n/pt.json +31 -1
- package/lib/i18n/ru.json +31 -1
- package/lib/i18n/uk.json +31 -1
- package/lib/i18n/zh-cn.json +31 -1
- package/lib/stateDefinitions/chemistryOrpStates.js +1068 -0
- package/lib/stateDefinitions/chemistryPhStates.js +214 -0
- package/lib/stateDefinitions/speechStates.js +151 -0
- package/main.js +14 -0
- package/package.json +1 -1
|
@@ -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.
|
|
372
|
-
|
|
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
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
status: 'low',
|
|
680
|
+
actionRequired: true,
|
|
681
|
+
recommendation,
|
|
682
|
+
};
|
|
464
683
|
}
|
|
465
684
|
|
|
466
685
|
if (value > max) {
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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, '&')
|
|
931
|
+
.replace(/</g, '<')
|
|
932
|
+
.replace(/>/g, '>')
|
|
933
|
+
.replace(/"/g, '"')
|
|
934
|
+
.replace(/'/g, ''');
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
118
|
-
this.adapter.log.debug(
|
|
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.
|
|
122
|
-
this.adapter.log.debug(
|
|
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.
|
|
139
|
-
this.adapter.log.debug(
|
|
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.
|
|
147
|
-
this.adapter.log.debug(
|
|
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
|
}
|