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.
@@ -1,8 +1,14 @@
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;
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 === 'external_state' || reason === 'manual_value');
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 shouldStore = forceSample || !newest || nowTs - newestTs >= MIN_SAMPLE_INTERVAL_MS;
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 && Number(sample.ts) >= minTs);
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', JSON.stringify(samples));
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
- return samples;
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(samples, nowTs - 30 * 24 * 60 * 60 * 1000);
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(String(state?.val || '[]'));
894
- return Array.isArray(parsed) ? parsed : [];
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;