iobroker.poolcontrol 1.3.32 → 1.3.34
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 +28 -14
- package/admin/i18n/uk.json +5 -5
- package/io-package.json +27 -27
- package/lib/helpers/chemistryOrpHelper.js +279 -17
- package/lib/helpers/chemistryPhHelper.js +279 -17
- package/lib/helpers/chemistryTdsHelper.js +279 -17
- package/lib/helpers/debugLogHelper.js +17 -2
- package/lib/helpers/runtimeHelper.js +22 -0
- package/lib/helpers/solarExtendedHelper.js +29 -0
- package/lib/helpers/solarHelper.js +20 -2
- package/lib/helpers/solarLogbookHelper.js +34 -2
- package/lib/i18n/de.json +2 -1
- package/lib/i18n/en.json +2 -1
- package/lib/i18n/es.json +2 -1
- package/lib/i18n/fr.json +2 -1
- package/lib/i18n/it.json +2 -1
- package/lib/i18n/nl.json +2 -1
- package/lib/i18n/pl.json +2 -1
- package/lib/i18n/pt.json +2 -1
- package/lib/i18n/ru.json +2 -1
- package/lib/i18n/uk.json +2 -1
- package/lib/i18n/zh-cn.json +2 -1
- package/lib/stateDefinitions/chemistryOrpStates.js +19 -2
- package/lib/stateDefinitions/chemistryPhStates.js +19 -2
- package/lib/stateDefinitions/chemistryTdsStates.js +19 -2
- package/lib/stateDefinitions/runtimeStates.js +13 -1
- package/lib/stateDefinitions/solarExtendedStates.js +22 -0
- package/lib/stateDefinitions/solarStates.js +23 -0
- package/package.json +2 -2
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { I18n } = require('@iobroker/adapter-core');
|
|
4
|
-
const MAX_HISTORY_AGE_MS =
|
|
4
|
+
const MAX_HISTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
const MAX_HISTORY_SAMPLES = 672;
|
|
6
|
+
const MAX_HISTORY_BYTES = 64 * 1024;
|
|
5
7
|
const MIN_SAMPLE_INTERVAL_MS = 15 * 60 * 1000;
|
|
8
|
+
const MAX_DAILY_HISTORY_AGE_MS = 32 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const MAX_DAILY_HISTORY_SAMPLES = 32;
|
|
10
|
+
const MAX_DAILY_HISTORY_BYTES = 8 * 1024;
|
|
11
|
+
const DAILY_REFERENCE_TOLERANCE_MS = 4 * 24 * 60 * 60 * 1000;
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* chemistryPhHelper
|
|
@@ -383,8 +389,8 @@ const chemistryPhHelper = {
|
|
|
383
389
|
|
|
384
390
|
await this._updateLastValues(value, now);
|
|
385
391
|
|
|
386
|
-
const history = await this._updateHistory(value, now, reason === '
|
|
387
|
-
const trend = await this._calculateTrend(value, now, history);
|
|
392
|
+
const history = await this._updateHistory(value, now, reason === 'manual_value');
|
|
393
|
+
const trend = await this._calculateTrend(value, now, history.samples, history.dailySamples);
|
|
388
394
|
const evaluation = await this._evaluateValue(value);
|
|
389
395
|
|
|
390
396
|
await this._writeTrend(trend);
|
|
@@ -464,16 +470,17 @@ const chemistryPhHelper = {
|
|
|
464
470
|
|
|
465
471
|
async _updateHistory(value, now, forceSample) {
|
|
466
472
|
let samples = await this._readJsonArray('chemistry.ph.history.samples_json');
|
|
473
|
+
const dailySeedSamples = samples;
|
|
467
474
|
const nowTs = now.getTime();
|
|
468
475
|
const minTs = nowTs - MAX_HISTORY_AGE_MS;
|
|
469
476
|
|
|
470
|
-
samples = samples.filter(
|
|
471
|
-
sample => sample && Number(sample.ts) >= minTs && Number.isFinite(Number(sample.value)),
|
|
472
|
-
);
|
|
477
|
+
samples = samples.filter(sample => sample.ts >= minTs && sample.ts <= nowTs);
|
|
473
478
|
|
|
474
479
|
const newest = samples.length ? samples[samples.length - 1] : null;
|
|
475
480
|
const newestTs = newest ? Number(newest.ts) : 0;
|
|
476
|
-
const
|
|
481
|
+
const withinSampleInterval = newest && nowTs - newestTs < MIN_SAMPLE_INTERVAL_MS;
|
|
482
|
+
const sameValue = newest && Number(newest.value) === value;
|
|
483
|
+
const shouldStore = !newest || !withinSampleInterval || (forceSample && !sameValue);
|
|
477
484
|
|
|
478
485
|
if (shouldStore) {
|
|
479
486
|
samples.push({
|
|
@@ -483,9 +490,12 @@ const chemistryPhHelper = {
|
|
|
483
490
|
});
|
|
484
491
|
}
|
|
485
492
|
|
|
486
|
-
samples = samples.filter(sample => sample &&
|
|
493
|
+
samples = samples.filter(sample => sample.ts >= minTs && sample.ts <= nowTs).slice(-MAX_HISTORY_SAMPLES);
|
|
494
|
+
|
|
495
|
+
const preparedHistory = this._prepareHistoryForWrite(samples, 'chemistry.ph.history.samples_json');
|
|
496
|
+
samples = preparedHistory.samples;
|
|
487
497
|
|
|
488
|
-
await this._setString('chemistry.ph.history.samples_json',
|
|
498
|
+
await this._setString('chemistry.ph.history.samples_json', preparedHistory.json);
|
|
489
499
|
await this._setNumber('chemistry.ph.history.samples_count', samples.length);
|
|
490
500
|
|
|
491
501
|
if (samples.length) {
|
|
@@ -501,15 +511,20 @@ const chemistryPhHelper = {
|
|
|
501
511
|
);
|
|
502
512
|
}
|
|
503
513
|
|
|
504
|
-
|
|
514
|
+
const dailySamples = await this._updateDailyHistory(dailySeedSamples, value, now, shouldStore);
|
|
515
|
+
return { samples, dailySamples };
|
|
505
516
|
},
|
|
506
517
|
|
|
507
|
-
async _calculateTrend(currentValue, now, samples) {
|
|
518
|
+
async _calculateTrend(currentValue, now, samples, dailySamples) {
|
|
508
519
|
const nowTs = now.getTime();
|
|
509
520
|
|
|
510
521
|
const ref24h = this._findReferenceSample(samples, nowTs - 24 * 60 * 60 * 1000);
|
|
511
522
|
const ref7d = this._findReferenceSample(samples, nowTs - 7 * 24 * 60 * 60 * 1000);
|
|
512
|
-
const ref30d = this._findReferenceSample(
|
|
523
|
+
const ref30d = this._findReferenceSample(
|
|
524
|
+
dailySamples,
|
|
525
|
+
nowTs - 30 * 24 * 60 * 60 * 1000,
|
|
526
|
+
DAILY_REFERENCE_TOLERANCE_MS,
|
|
527
|
+
);
|
|
513
528
|
|
|
514
529
|
const delta24h = ref24h ? currentValue - ref24h.value : 0;
|
|
515
530
|
const delta7d = ref7d ? currentValue - ref7d.value : 0;
|
|
@@ -530,7 +545,7 @@ const chemistryPhHelper = {
|
|
|
530
545
|
};
|
|
531
546
|
},
|
|
532
547
|
|
|
533
|
-
_findReferenceSample(samples, targetTs) {
|
|
548
|
+
_findReferenceSample(samples, targetTs, toleranceMs = null) {
|
|
534
549
|
if (!samples.length) {
|
|
535
550
|
return null;
|
|
536
551
|
}
|
|
@@ -540,7 +555,7 @@ const chemistryPhHelper = {
|
|
|
540
555
|
|
|
541
556
|
for (const sample of samples) {
|
|
542
557
|
const ts = Number(sample.ts);
|
|
543
|
-
const value = Number(sample.value);
|
|
558
|
+
const value = Number(sample.value ?? sample.last);
|
|
544
559
|
|
|
545
560
|
if (!Number.isFinite(ts) || !Number.isFinite(value)) {
|
|
546
561
|
continue;
|
|
@@ -558,7 +573,7 @@ const chemistryPhHelper = {
|
|
|
558
573
|
}
|
|
559
574
|
}
|
|
560
575
|
|
|
561
|
-
return best;
|
|
576
|
+
return toleranceMs === null || bestDistance <= toleranceMs ? best : null;
|
|
562
577
|
},
|
|
563
578
|
|
|
564
579
|
_getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d) {
|
|
@@ -886,17 +901,264 @@ const chemistryPhHelper = {
|
|
|
886
901
|
return legacyDate ? legacyDate.getTime() : null;
|
|
887
902
|
},
|
|
888
903
|
|
|
904
|
+
async _updateDailyHistory(shortTermSamples, value, now, sampleStored) {
|
|
905
|
+
const id = 'chemistry.ph.history.daily_json';
|
|
906
|
+
let dailySamples = await this._readDailyJson(id);
|
|
907
|
+
const nowTs = now.getTime();
|
|
908
|
+
const minTs = nowTs - MAX_DAILY_HISTORY_AGE_MS;
|
|
909
|
+
const initializeFromSeeds = dailySamples.length === 0;
|
|
910
|
+
|
|
911
|
+
if (initializeFromSeeds) {
|
|
912
|
+
const legacyValueState = await this.adapter.getStateAsync('chemistry.ph.trend.reference_30d_value');
|
|
913
|
+
const legacyAtState = await this.adapter.getStateAsync('chemistry.ph.trend.reference_30d_at');
|
|
914
|
+
const legacyValue = Number(legacyValueState?.val);
|
|
915
|
+
const legacyTs = Number(legacyAtState?.val);
|
|
916
|
+
|
|
917
|
+
if (
|
|
918
|
+
Number.isFinite(legacyValue) &&
|
|
919
|
+
legacyValue >= 0 &&
|
|
920
|
+
legacyValue <= 14 &&
|
|
921
|
+
Number.isFinite(legacyTs) &&
|
|
922
|
+
legacyTs >= minTs &&
|
|
923
|
+
legacyTs <= nowTs &&
|
|
924
|
+
!shortTermSamples.some(sample => this._dayKey(sample.ts) === this._dayKey(legacyTs))
|
|
925
|
+
) {
|
|
926
|
+
this._addDailySample(dailySamples, legacyTs, legacyValue);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
for (const sample of shortTermSamples) {
|
|
930
|
+
if (sample.ts >= minTs && sample.ts <= nowTs) {
|
|
931
|
+
this._addDailySample(dailySamples, sample.ts, sample.value);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (sampleStored) {
|
|
935
|
+
this._addDailySample(dailySamples, nowTs, value);
|
|
936
|
+
}
|
|
937
|
+
} else if (sampleStored) {
|
|
938
|
+
this._addDailySample(dailySamples, nowTs, value);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
dailySamples = dailySamples
|
|
942
|
+
.filter(sample => sample.ts >= minTs && sample.ts <= nowTs)
|
|
943
|
+
.sort((a, b) => a.ts - b.ts)
|
|
944
|
+
.slice(-MAX_DAILY_HISTORY_SAMPLES);
|
|
945
|
+
|
|
946
|
+
const preparedHistory = this._prepareDailyHistoryForWrite(dailySamples, id);
|
|
947
|
+
await this._setString(id, preparedHistory.json);
|
|
948
|
+
return preparedHistory.samples;
|
|
949
|
+
},
|
|
950
|
+
|
|
951
|
+
_addDailySample(dailySamples, sampleTs, value) {
|
|
952
|
+
const day = this._dayKey(sampleTs);
|
|
953
|
+
const existing = dailySamples.find(sample => sample.day === day);
|
|
954
|
+
|
|
955
|
+
if (!existing) {
|
|
956
|
+
dailySamples.push({
|
|
957
|
+
day,
|
|
958
|
+
ts: this._dayStartTs(sampleTs),
|
|
959
|
+
min: value,
|
|
960
|
+
max: value,
|
|
961
|
+
avg: value,
|
|
962
|
+
last: value,
|
|
963
|
+
count: 1,
|
|
964
|
+
});
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
existing.min = Math.min(existing.min, value);
|
|
969
|
+
existing.max = Math.max(existing.max, value);
|
|
970
|
+
existing.avg = (existing.avg * existing.count + value) / (existing.count + 1);
|
|
971
|
+
existing.last = value;
|
|
972
|
+
existing.count += 1;
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
async _readDailyJson(id) {
|
|
976
|
+
const state = await this.adapter.getStateAsync(id);
|
|
977
|
+
const rawValue = state?.val;
|
|
978
|
+
|
|
979
|
+
if (rawValue === null || rawValue === undefined || rawValue === '') {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (typeof rawValue !== 'string') {
|
|
984
|
+
this.adapter.log.warn(`[chemistryPhHelper] Ignoring non-string daily history state ${id}.`);
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (Buffer.byteLength(rawValue, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
|
|
989
|
+
this.adapter.log.warn(`[chemistryPhHelper] Ignoring oversized daily history state ${id} (> 8 KB).`);
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
const parsed = JSON.parse(rawValue);
|
|
995
|
+
if (!Array.isArray(parsed)) {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const nowTs = Date.now();
|
|
1000
|
+
const normalized = parsed
|
|
1001
|
+
.map(sample => {
|
|
1002
|
+
const sourceTs = Number(sample?.ts);
|
|
1003
|
+
const legacyValue = Number(sample?.value);
|
|
1004
|
+
const min = Number(sample?.min ?? legacyValue);
|
|
1005
|
+
const max = Number(sample?.max ?? legacyValue);
|
|
1006
|
+
const avg = Number(sample?.avg ?? legacyValue);
|
|
1007
|
+
const last = Number(sample?.last ?? legacyValue);
|
|
1008
|
+
const count = sample?.count === undefined ? 1 : Number(sample.count);
|
|
1009
|
+
|
|
1010
|
+
if (!Number.isFinite(sourceTs) || sourceTs <= 0 || sourceTs > nowTs) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
if (
|
|
1014
|
+
!Number.isFinite(min) ||
|
|
1015
|
+
!Number.isFinite(max) ||
|
|
1016
|
+
!Number.isFinite(avg) ||
|
|
1017
|
+
!Number.isFinite(last) ||
|
|
1018
|
+
min < 0 ||
|
|
1019
|
+
max > 14 ||
|
|
1020
|
+
min > max ||
|
|
1021
|
+
avg < min ||
|
|
1022
|
+
avg > max ||
|
|
1023
|
+
last < min ||
|
|
1024
|
+
last > max
|
|
1025
|
+
) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
if (!Number.isInteger(count) || count < 1 || count > 1000000) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
day: this._dayKey(sourceTs),
|
|
1034
|
+
ts: this._dayStartTs(sourceTs),
|
|
1035
|
+
min,
|
|
1036
|
+
max,
|
|
1037
|
+
avg,
|
|
1038
|
+
last,
|
|
1039
|
+
count,
|
|
1040
|
+
};
|
|
1041
|
+
})
|
|
1042
|
+
.filter(Boolean)
|
|
1043
|
+
.sort((a, b) => a.ts - b.ts);
|
|
1044
|
+
|
|
1045
|
+
const byDay = new Map();
|
|
1046
|
+
for (const sample of normalized) {
|
|
1047
|
+
if (!byDay.has(sample.day)) {
|
|
1048
|
+
byDay.set(sample.day, sample);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return [...byDay.values()].slice(-MAX_DAILY_HISTORY_SAMPLES);
|
|
1052
|
+
} catch {
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
|
|
1057
|
+
_prepareDailyHistoryForWrite(samples, id) {
|
|
1058
|
+
let limitedSamples = samples.slice(-MAX_DAILY_HISTORY_SAMPLES);
|
|
1059
|
+
let json = JSON.stringify(limitedSamples);
|
|
1060
|
+
let trimmedForSize = false;
|
|
1061
|
+
|
|
1062
|
+
while (limitedSamples.length && Buffer.byteLength(json, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
|
|
1063
|
+
limitedSamples = limitedSamples.slice(1);
|
|
1064
|
+
json = JSON.stringify(limitedSamples);
|
|
1065
|
+
trimmedForSize = true;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (trimmedForSize) {
|
|
1069
|
+
this.adapter.log.warn(`[chemistryPhHelper] Daily history ${id} was trimmed to stay within 8 KB.`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (Buffer.byteLength(json, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
|
|
1073
|
+
this.adapter.log.warn(`[chemistryPhHelper] Daily history ${id} could not be limited; resetting it.`);
|
|
1074
|
+
return { samples: [], json: '[]' };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return { samples: limitedSamples, json };
|
|
1078
|
+
},
|
|
1079
|
+
|
|
1080
|
+
_dayKey(ts) {
|
|
1081
|
+
const date = new Date(ts);
|
|
1082
|
+
const year = String(date.getFullYear());
|
|
1083
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1084
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
1085
|
+
return [year, month, day].join('-');
|
|
1086
|
+
},
|
|
1087
|
+
|
|
1088
|
+
_dayStartTs(ts) {
|
|
1089
|
+
const date = new Date(ts);
|
|
1090
|
+
date.setHours(0, 0, 0, 0);
|
|
1091
|
+
return date.getTime();
|
|
1092
|
+
},
|
|
1093
|
+
|
|
889
1094
|
async _readJsonArray(id) {
|
|
890
1095
|
const state = await this.adapter.getStateAsync(id);
|
|
1096
|
+
const rawValue = state?.val;
|
|
1097
|
+
|
|
1098
|
+
if (rawValue === null || rawValue === undefined || rawValue === '') {
|
|
1099
|
+
return [];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (typeof rawValue !== 'string') {
|
|
1103
|
+
this.adapter.log.warn(`[chemistryPhHelper] Ignoring non-string history state ${id}.`);
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (Buffer.byteLength(rawValue, 'utf8') > MAX_HISTORY_BYTES) {
|
|
1108
|
+
this.adapter.log.warn(`[chemistryPhHelper] Ignoring oversized history state ${id} (> 64 KB).`);
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
891
1111
|
|
|
892
1112
|
try {
|
|
893
|
-
const parsed = JSON.parse(
|
|
894
|
-
|
|
1113
|
+
const parsed = JSON.parse(rawValue);
|
|
1114
|
+
if (!Array.isArray(parsed)) {
|
|
1115
|
+
return [];
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const nowTs = Date.now();
|
|
1119
|
+
return parsed
|
|
1120
|
+
.map(sample => {
|
|
1121
|
+
const ts = Number(sample?.ts);
|
|
1122
|
+
const value = Number(sample?.value);
|
|
1123
|
+
if (!Number.isFinite(ts) || ts <= 0 || ts > nowTs) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
if (!Number.isFinite(value) || value < 0 || value > 14) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
return { ts, time: this._formatDateTime(new Date(ts)), value };
|
|
1130
|
+
})
|
|
1131
|
+
.filter(Boolean)
|
|
1132
|
+
.sort((a, b) => a.ts - b.ts)
|
|
1133
|
+
.slice(-MAX_HISTORY_SAMPLES);
|
|
895
1134
|
} catch {
|
|
896
1135
|
return [];
|
|
897
1136
|
}
|
|
898
1137
|
},
|
|
899
1138
|
|
|
1139
|
+
_prepareHistoryForWrite(samples, id) {
|
|
1140
|
+
let limitedSamples = samples.slice(-MAX_HISTORY_SAMPLES);
|
|
1141
|
+
let json = JSON.stringify(limitedSamples);
|
|
1142
|
+
let trimmedForSize = false;
|
|
1143
|
+
|
|
1144
|
+
while (limitedSamples.length && Buffer.byteLength(json, 'utf8') > MAX_HISTORY_BYTES) {
|
|
1145
|
+
limitedSamples = limitedSamples.slice(1);
|
|
1146
|
+
json = JSON.stringify(limitedSamples);
|
|
1147
|
+
trimmedForSize = true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (trimmedForSize) {
|
|
1151
|
+
this.adapter.log.warn(`[chemistryPhHelper] History ${id} was trimmed to stay within 64 KB.`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (Buffer.byteLength(json, 'utf8') > MAX_HISTORY_BYTES) {
|
|
1155
|
+
this.adapter.log.warn(`[chemistryPhHelper] History ${id} could not be limited safely; resetting it.`);
|
|
1156
|
+
return { samples: [], json: '[]' };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return { samples: limitedSamples, json };
|
|
1160
|
+
},
|
|
1161
|
+
|
|
900
1162
|
async _readBoolean(id) {
|
|
901
1163
|
const state = await this.adapter.getStateAsync(id);
|
|
902
1164
|
return !!state?.val;
|