iobroker.poolcontrol 1.3.15 → 1.3.18

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
  }
@@ -208,7 +215,8 @@ const chemistryPhHelper = {
208
215
  await this._setBool('chemistry.ph.measurement.allowed', false);
209
216
  await this._setString('chemistry.ph.measurement.ignored_reason', 'disabled');
210
217
  await this._setString('chemistry.ph.debug.last_reason', reason || 'disabled');
211
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
218
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
219
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
212
220
  return;
213
221
  }
214
222
 
@@ -227,7 +235,8 @@ const chemistryPhHelper = {
227
235
  await this._setBool('chemistry.ph.measurement.allowed', false);
228
236
  await this._setString('chemistry.ph.measurement.ignored_reason', 'source_disabled');
229
237
  await this._setString('chemistry.ph.debug.last_reason', reason || 'source_disabled');
230
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
238
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
239
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
231
240
  return;
232
241
  }
233
242
 
@@ -256,7 +265,8 @@ const chemistryPhHelper = {
256
265
  await this._setBool('chemistry.ph.measurement.allowed', false);
257
266
  await this._setString('chemistry.ph.measurement.ignored_reason', 'missing_source_state');
258
267
  await this._setString('chemistry.ph.debug.last_reason', reason || 'missing_source_state');
259
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
268
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
269
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
260
270
  return;
261
271
  }
262
272
 
@@ -280,7 +290,8 @@ const chemistryPhHelper = {
280
290
  await this._setBool('chemistry.ph.measurement.allowed', false);
281
291
  await this._setString('chemistry.ph.measurement.ignored_reason', 'source_read_error');
282
292
  await this._setString('chemistry.ph.debug.last_reason', `source_read_error: ${err.message}`);
283
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
293
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
294
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
284
295
  return;
285
296
  }
286
297
 
@@ -300,7 +311,8 @@ const chemistryPhHelper = {
300
311
  await this._setBool('chemistry.ph.measurement.allowed', false);
301
312
  await this._setString('chemistry.ph.measurement.ignored_reason', 'source_not_found');
302
313
  await this._setString('chemistry.ph.debug.last_reason', reason || 'source_not_found');
303
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
314
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
315
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
304
316
  return;
305
317
  }
306
318
 
@@ -313,7 +325,8 @@ const chemistryPhHelper = {
313
325
  await this._setBool('chemistry.ph.evaluation.action_required', false);
314
326
  await this._setString('chemistry.ph.measurement.ignored_reason', 'unknown_source_mode');
315
327
  await this._setString('chemistry.ph.debug.last_reason', reason || 'unknown_source_mode');
316
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(new Date()));
328
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
329
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX
317
330
  } catch (err) {
318
331
  this.adapter.log.warn(`[chemistryPhHelper] Evaluation failed: ${err.message}`);
319
332
  }
@@ -323,7 +336,7 @@ const chemistryPhHelper = {
323
336
  const now = new Date();
324
337
  const value = Number(rawValue);
325
338
 
326
- await this._setString('chemistry.ph.input.last_value_at', this._formatDateTime(now));
339
+ await this._setNumber('chemistry.ph.input.last_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
327
340
 
328
341
  if (source === 'manual') {
329
342
  await this._setBool('chemistry.ph.input.source_valid', true);
@@ -348,7 +361,7 @@ const chemistryPhHelper = {
348
361
  await this._setBool('chemistry.ph.measurement.allowed', false);
349
362
  await this._setString('chemistry.ph.measurement.ignored_reason', 'invalid_value');
350
363
  await this._setString('chemistry.ph.debug.last_reason', reason || 'invalid_value');
351
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(now));
364
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
352
365
  return;
353
366
  }
354
367
 
@@ -361,18 +374,24 @@ const chemistryPhHelper = {
361
374
  await this._setBool('chemistry.ph.measurement.allowed', false);
362
375
  await this._setString('chemistry.ph.measurement.ignored_reason', measurementAllowed.reason);
363
376
  await this._setString('chemistry.ph.debug.last_reason', reason || measurementAllowed.reason);
364
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(now));
377
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
365
378
  return;
366
379
  }
367
380
 
368
381
  await this._setBool('chemistry.ph.measurement.allowed', true);
369
382
  await this._setString('chemistry.ph.measurement.ignored_reason', '');
370
383
 
371
- await this._updateHistory(value, now);
372
- await this._evaluateValue(value);
384
+ await this._updateLastValues(value, now);
385
+
386
+ const history = await this._updateHistory(value, now, reason === 'external_state' || reason === 'manual_value');
387
+ const trend = await this._calculateTrend(value, now, history);
388
+ const evaluation = await this._evaluateValue(value);
389
+
390
+ await this._writeTrend(trend);
391
+ await this._writeOutputs(value, trend, evaluation);
373
392
 
374
393
  await this._setString('chemistry.ph.debug.last_reason', reason || 'value_processed');
375
- await this._setString('chemistry.ph.debug.last_update', this._formatDateTime(now));
394
+ await this._setNumber('chemistry.ph.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
376
395
  },
377
396
 
378
397
  async _checkMeasurementAllowed(now) {
@@ -427,24 +446,224 @@ const chemistryPhHelper = {
427
446
  return { allowed: true, reason: '', status: 'ok', recommendation: '' };
428
447
  },
429
448
 
430
- async _updateHistory(value, now) {
449
+ async _updateLastValues(value, now) {
431
450
  const lastValid = await this._readNumberOrNull('chemistry.ph.input.last_valid_value');
432
- const lastValidAt = await this._readString('chemistry.ph.input.last_valid_value_at');
451
+ const lastValidAt = await this._readTimestampOrNull('chemistry.ph.input.last_valid_value_at'); // FIX: accept numeric ms timestamps and legacy German strings.
433
452
 
434
453
  if (lastValid !== null && lastValidAt) {
435
454
  await this._setNumber('chemistry.ph.input.previous_value', lastValid);
436
- await this._setString('chemistry.ph.input.previous_value_at', lastValidAt);
455
+ await this._setNumber('chemistry.ph.input.previous_value_at', lastValidAt); // FIX: pass stored timestamp through unchanged.
456
+
457
+ const minutes = Math.max(0, Math.round((now.getTime() - lastValidAt) / 60000)); // FIX: calculate from numeric ms timestamp.
458
+ await this._setNumber('chemistry.ph.input.minutes_since_previous_value', minutes);
459
+ }
437
460
 
438
- const previousDate = this._parseGermanDateTime(lastValidAt);
461
+ await this._setNumber('chemistry.ph.input.last_valid_value', value);
462
+ await this._setNumber('chemistry.ph.input.last_valid_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
463
+ },
439
464
 
440
- if (previousDate) {
441
- const minutes = Math.max(0, Math.round((now.getTime() - previousDate.getTime()) / 60000));
442
- await this._setNumber('chemistry.ph.input.minutes_since_previous_value', minutes);
465
+ async _updateHistory(value, now, forceSample) {
466
+ let samples = await this._readJsonArray('chemistry.ph.history.samples_json');
467
+ const nowTs = now.getTime();
468
+ const minTs = nowTs - MAX_HISTORY_AGE_MS;
469
+
470
+ samples = samples.filter(
471
+ sample => sample && Number(sample.ts) >= minTs && Number.isFinite(Number(sample.value)),
472
+ );
473
+
474
+ const newest = samples.length ? samples[samples.length - 1] : null;
475
+ const newestTs = newest ? Number(newest.ts) : 0;
476
+ const shouldStore = forceSample || !newest || nowTs - newestTs >= MIN_SAMPLE_INTERVAL_MS;
477
+
478
+ if (shouldStore) {
479
+ samples.push({
480
+ ts: nowTs,
481
+ time: this._formatDateTime(now),
482
+ value,
483
+ });
484
+ }
485
+
486
+ samples = samples.filter(sample => sample && Number(sample.ts) >= minTs);
487
+
488
+ await this._setString('chemistry.ph.history.samples_json', JSON.stringify(samples));
489
+ await this._setNumber('chemistry.ph.history.samples_count', samples.length);
490
+
491
+ if (samples.length) {
492
+ await this._setNumber(
493
+ // FIX: history value.time states store sample timestamps, not readable strings.
494
+ 'chemistry.ph.history.oldest_sample_at',
495
+ Number(samples[0].ts),
496
+ );
497
+ await this._setNumber(
498
+ // FIX: history value.time states store sample timestamps, not readable strings.
499
+ 'chemistry.ph.history.newest_sample_at',
500
+ Number(samples[samples.length - 1].ts),
501
+ );
502
+ }
503
+
504
+ return samples;
505
+ },
506
+
507
+ async _calculateTrend(currentValue, now, samples) {
508
+ const nowTs = now.getTime();
509
+
510
+ const ref24h = this._findReferenceSample(samples, nowTs - 24 * 60 * 60 * 1000);
511
+ const ref7d = this._findReferenceSample(samples, nowTs - 7 * 24 * 60 * 60 * 1000);
512
+ const ref30d = this._findReferenceSample(samples, nowTs - 30 * 24 * 60 * 60 * 1000);
513
+
514
+ const delta24h = ref24h ? currentValue - ref24h.value : 0;
515
+ const delta7d = ref7d ? currentValue - ref7d.value : 0;
516
+ const delta30d = ref30d ? currentValue - ref30d.value : 0;
517
+
518
+ const direction = this._getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d);
519
+ const status = this._getTrendStatus(delta24h, delta7d, delta30d, ref24h, ref7d, ref30d);
520
+
521
+ return {
522
+ ref24h,
523
+ ref7d,
524
+ ref30d,
525
+ delta24h,
526
+ delta7d,
527
+ delta30d,
528
+ direction,
529
+ status,
530
+ };
531
+ },
532
+
533
+ _findReferenceSample(samples, targetTs) {
534
+ if (!samples.length) {
535
+ return null;
536
+ }
537
+
538
+ let best = null;
539
+ let bestDistance = Number.MAX_SAFE_INTEGER;
540
+
541
+ for (const sample of samples) {
542
+ const ts = Number(sample.ts);
543
+ const value = Number(sample.value);
544
+
545
+ if (!Number.isFinite(ts) || !Number.isFinite(value)) {
546
+ continue;
547
+ }
548
+
549
+ const distance = Math.abs(ts - targetTs);
550
+
551
+ if (distance < bestDistance) {
552
+ bestDistance = distance;
553
+ best = {
554
+ ts,
555
+ value,
556
+ time: sample.time || this._formatDateTime(new Date(ts)),
557
+ };
443
558
  }
444
559
  }
445
560
 
446
- await this._setNumber('chemistry.ph.input.last_valid_value', value);
447
- await this._setString('chemistry.ph.input.last_valid_value_at', this._formatDateTime(now));
561
+ return best;
562
+ },
563
+
564
+ _getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d) {
565
+ const available = [ref24h ? delta24h : null, ref7d ? delta7d : null, ref30d ? delta30d : null].filter(
566
+ value => value !== null,
567
+ );
568
+
569
+ if (!available.length) {
570
+ return 'not_enough_data';
571
+ }
572
+
573
+ const strongest = available.reduce((prev, current) => (Math.abs(current) > Math.abs(prev) ? current : prev), 0);
574
+
575
+ if (Math.abs(strongest) < 0.05) {
576
+ return 'stable';
577
+ }
578
+
579
+ return strongest > 0 ? 'rising' : 'falling';
580
+ },
581
+
582
+ _getTrendStatus(delta24h, delta7d, delta30d, ref24h, ref7d, ref30d) {
583
+ if (!ref24h && !ref7d && !ref30d) {
584
+ return 'not_enough_data';
585
+ }
586
+
587
+ if (delta24h > 0.2 || delta7d > 0.4 || delta30d > 0.6) {
588
+ return 'rising_fast';
589
+ }
590
+
591
+ if (delta24h > 0.1 || delta7d > 0.25 || delta30d > 0.4) {
592
+ return 'rising_noticeable';
593
+ }
594
+
595
+ if (delta24h > 0.05 || delta7d > 0.15 || delta30d > 0.25) {
596
+ return 'rising_slowly';
597
+ }
598
+
599
+ if (delta24h < -0.05 || delta7d < -0.15 || delta30d < -0.25) {
600
+ return 'falling';
601
+ }
602
+
603
+ return 'stable';
604
+ },
605
+
606
+ async _writeTrend(trend) {
607
+ await this._setNumber('chemistry.ph.trend.reference_24h_value', trend.ref24h ? trend.ref24h.value : 0);
608
+ await this._setNumber('chemistry.ph.trend.reference_24h_at', trend.ref24h ? trend.ref24h.ts : 0); // FIX
609
+ await this._setNumber('chemistry.ph.trend.delta_24h', trend.ref24h ? trend.delta24h : 0);
610
+
611
+ await this._setNumber('chemistry.ph.trend.reference_7d_value', trend.ref7d ? trend.ref7d.value : 0);
612
+ await this._setNumber('chemistry.ph.trend.reference_7d_at', trend.ref7d ? trend.ref7d.ts : 0); // FIX
613
+ await this._setNumber('chemistry.ph.trend.delta_7d', trend.ref7d ? trend.delta7d : 0);
614
+
615
+ await this._setNumber('chemistry.ph.trend.reference_30d_value', trend.ref30d ? trend.ref30d.value : 0);
616
+ await this._setNumber('chemistry.ph.trend.reference_30d_at', trend.ref30d ? trend.ref30d.ts : 0); // FIX
617
+ await this._setNumber('chemistry.ph.trend.delta_30d', trend.ref30d ? trend.delta30d : 0);
618
+
619
+ await this._setString('chemistry.ph.trend.direction', trend.direction);
620
+ await this._setString('chemistry.ph.trend.status', trend.status);
621
+ },
622
+
623
+ async _writeOutputs(value, trend, evaluation) {
624
+ const text =
625
+ `${I18n.translate('Current pH value')}: ${value.toFixed(2)}. ` +
626
+ `24h: ${trend.ref24h ? this._formatDelta(trend.delta24h) : I18n.translate('not enough data')}, ` +
627
+ `7d: ${trend.ref7d ? this._formatDelta(trend.delta7d) : I18n.translate('not enough data')}, ` +
628
+ `30d: ${trend.ref30d ? this._formatDelta(trend.delta30d) : I18n.translate('not enough data')}. ` +
629
+ `${evaluation.recommendation}`;
630
+
631
+ const html =
632
+ `<div>` +
633
+ `<b>${I18n.translate('Current pH value')}:</b> ${value.toFixed(2)}<br>` +
634
+ `<b>24h:</b> ${trend.ref24h ? `${trend.ref24h.value.toFixed(2)} / ${this._formatDelta(trend.delta24h)}` : I18n.translate('not enough data')}<br>` +
635
+ `<b>7d:</b> ${trend.ref7d ? `${trend.ref7d.value.toFixed(2)} / ${this._formatDelta(trend.delta7d)}` : I18n.translate('not enough data')}<br>` +
636
+ `<b>30d:</b> ${trend.ref30d ? `${trend.ref30d.value.toFixed(2)} / ${this._formatDelta(trend.delta30d)}` : I18n.translate('not enough data')}<br>` +
637
+ `<b>${I18n.translate('Trend status')}:</b> ${trend.status}<br>` +
638
+ `<b>${I18n.translate('Status')}:</b> ${evaluation.status}<br>` +
639
+ `<b>${I18n.translate('Recommendation')}:</b> ${this._escapeHtml(evaluation.recommendation)}` +
640
+ `</div>`;
641
+
642
+ const json = {
643
+ current: Number(value.toFixed(2)),
644
+ unit: 'pH',
645
+ trend_24h: {
646
+ reference: trend.ref24h ? Number(trend.ref24h.value.toFixed(2)) : null,
647
+ delta: trend.ref24h ? Number(trend.delta24h.toFixed(2)) : null,
648
+ },
649
+ trend_7d: {
650
+ reference: trend.ref7d ? Number(trend.ref7d.value.toFixed(2)) : null,
651
+ delta: trend.ref7d ? Number(trend.delta7d.toFixed(2)) : null,
652
+ },
653
+ trend_30d: {
654
+ reference: trend.ref30d ? Number(trend.ref30d.value.toFixed(2)) : null,
655
+ delta: trend.ref30d ? Number(trend.delta30d.toFixed(2)) : null,
656
+ },
657
+ direction: trend.direction,
658
+ trend_status: trend.status,
659
+ status: evaluation.status,
660
+ action_required: evaluation.actionRequired,
661
+ recommendation: evaluation.recommendation,
662
+ };
663
+
664
+ await this._setString('chemistry.ph.outputs.summary_text', text);
665
+ await this._setString('chemistry.ph.outputs.summary_html', html);
666
+ await this._setString('chemistry.ph.outputs.summary_json', JSON.stringify(json));
448
667
  },
449
668
 
450
669
  async _evaluateValue(value) {
@@ -452,35 +671,48 @@ const chemistryPhHelper = {
452
671
  const max = await this._readNumber('chemistry.ph.evaluation.target_max');
453
672
 
454
673
  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
- ),
674
+ const recommendation = I18n.translate(
675
+ 'pH value is too low. Check whether pH plus should be added according to the product instructions. Then circulate and measure again.',
461
676
  );
677
+
678
+ await this._setString('chemistry.ph.evaluation.status', 'low');
679
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
462
680
  await this._setBool('chemistry.ph.evaluation.action_required', true);
463
- return;
681
+
682
+ return {
683
+ status: 'low',
684
+ actionRequired: true,
685
+ recommendation,
686
+ };
464
687
  }
465
688
 
466
689
  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
- ),
690
+ const recommendation = I18n.translate(
691
+ 'pH value is too high. Check whether pH minus should be added according to the product instructions. Then circulate and measure again.',
473
692
  );
693
+
694
+ await this._setString('chemistry.ph.evaluation.status', 'high');
695
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
474
696
  await this._setBool('chemistry.ph.evaluation.action_required', true);
475
- return;
697
+
698
+ return {
699
+ status: 'high',
700
+ actionRequired: true,
701
+ recommendation,
702
+ };
476
703
  }
477
704
 
705
+ const recommendation = I18n.translate('pH value is within the target range. No action is required.');
706
+
478
707
  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
- );
708
+ await this._setString('chemistry.ph.evaluation.recommendation', recommendation);
483
709
  await this._setBool('chemistry.ph.evaluation.action_required', false);
710
+
711
+ return {
712
+ status: 'ok',
713
+ actionRequired: false,
714
+ recommendation,
715
+ };
484
716
  },
485
717
 
486
718
  async _startMixingRun() {
@@ -641,6 +873,30 @@ const chemistryPhHelper = {
641
873
  return Number.isFinite(value) ? value : null;
642
874
  },
643
875
 
876
+ async _readTimestampOrNull(id) {
877
+ const state = await this.adapter.getStateAsync(id);
878
+ const value = state?.val;
879
+ const numberValue = Number(value);
880
+
881
+ if (Number.isFinite(numberValue) && numberValue > 0) {
882
+ return numberValue; // FIX: numeric value.time timestamps are used directly.
883
+ }
884
+
885
+ const legacyDate = this._parseGermanDateTime(value); // FIX: keep backward compatibility for old string values.
886
+ return legacyDate ? legacyDate.getTime() : null;
887
+ },
888
+
889
+ async _readJsonArray(id) {
890
+ const state = await this.adapter.getStateAsync(id);
891
+
892
+ try {
893
+ const parsed = JSON.parse(String(state?.val || '[]'));
894
+ return Array.isArray(parsed) ? parsed : [];
895
+ } catch {
896
+ return [];
897
+ }
898
+ },
899
+
644
900
  async _readBoolean(id) {
645
901
  const state = await this.adapter.getStateAsync(id);
646
902
  return !!state?.val;
@@ -659,6 +915,11 @@ const chemistryPhHelper = {
659
915
  await this.adapter.setStateChangedAsync(id, { val: !!value, ack: true });
660
916
  },
661
917
 
918
+ _formatDelta(value) {
919
+ const rounded = Number(value) || 0;
920
+ return `${rounded >= 0 ? '+' : ''}${rounded.toFixed(2)}`;
921
+ },
922
+
662
923
  _formatDateTime(date) {
663
924
  return date.toLocaleString('de-DE', {
664
925
  year: 'numeric',
@@ -681,6 +942,15 @@ const chemistryPhHelper = {
681
942
  return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
682
943
  },
683
944
 
945
+ _escapeHtml(value) {
946
+ return String(value ?? '')
947
+ .replace(/&/g, '&amp;')
948
+ .replace(/</g, '&lt;')
949
+ .replace(/>/g, '&gt;')
950
+ .replace(/"/g, '&quot;')
951
+ .replace(/'/g, '&#039;');
952
+ },
953
+
684
954
  cleanup() {
685
955
  if (this.evalTimer) {
686
956
  this.adapter.clearTimeout(this.evalTimer);
@@ -246,7 +246,7 @@ const chemistryTdsHelper = {
246
246
  const now = new Date();
247
247
  const value = Number(rawValue);
248
248
 
249
- await this._setString('chemistry.tds.input.last_value_at', this._formatDateTime(now));
249
+ await this._setNumber('chemistry.tds.input.last_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
250
250
 
251
251
  if (source === 'manual') {
252
252
  await this._setBool('chemistry.tds.input.source_valid', true);
@@ -278,7 +278,7 @@ const chemistryTdsHelper = {
278
278
  await this._setString('chemistry.tds.evaluation.recommendation', measurementAllowed.recommendation);
279
279
  await this._setBool('chemistry.tds.evaluation.action_required', false);
280
280
  await this._setString('chemistry.tds.debug.last_reason', reason || measurementAllowed.reason);
281
- await this._setString('chemistry.tds.debug.last_update', this._formatDateTime(now));
281
+ await this._setNumber('chemistry.tds.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
282
282
  return;
283
283
  }
284
284
 
@@ -297,7 +297,7 @@ const chemistryTdsHelper = {
297
297
  await this._writeOutputs(value, reference, trend, evaluation);
298
298
 
299
299
  await this._setString('chemistry.tds.debug.last_reason', reason || 'value_processed');
300
- await this._setString('chemistry.tds.debug.last_update', this._formatDateTime(now));
300
+ await this._setNumber('chemistry.tds.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
301
301
  },
302
302
 
303
303
  async _checkMeasurementAllowed(now) {
@@ -357,22 +357,18 @@ const chemistryTdsHelper = {
357
357
 
358
358
  async _updateLastValues(value, now) {
359
359
  const lastValid = await this._readNumberOrNull('chemistry.tds.input.last_valid_value');
360
- const lastValidAt = await this._readString('chemistry.tds.input.last_valid_value_at');
360
+ const lastValidAt = await this._readTimestampOrNull('chemistry.tds.input.last_valid_value_at'); // FIX: accept numeric ms timestamps and legacy German strings.
361
361
 
362
362
  if (lastValid !== null && lastValidAt) {
363
363
  await this._setNumber('chemistry.tds.input.previous_value', lastValid);
364
- await this._setString('chemistry.tds.input.previous_value_at', lastValidAt);
364
+ await this._setNumber('chemistry.tds.input.previous_value_at', lastValidAt); // FIX: pass stored timestamp through unchanged.
365
365
 
366
- const previousDate = this._parseGermanDateTime(lastValidAt);
367
-
368
- if (previousDate) {
369
- const minutes = Math.max(0, Math.round((now.getTime() - previousDate.getTime()) / 60000));
370
- await this._setNumber('chemistry.tds.input.minutes_since_previous_value', minutes);
371
- }
366
+ const minutes = Math.max(0, Math.round((now.getTime() - lastValidAt) / 60000)); // FIX: calculate from numeric ms timestamp.
367
+ await this._setNumber('chemistry.tds.input.minutes_since_previous_value', minutes);
372
368
  }
373
369
 
374
370
  await this._setNumber('chemistry.tds.input.last_valid_value', value);
375
- await this._setString('chemistry.tds.input.last_valid_value_at', this._formatDateTime(now));
371
+ await this._setNumber('chemistry.tds.input.last_valid_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
376
372
  },
377
373
 
378
374
  async _updateHistory(value, now, forceSample) {
@@ -402,13 +398,15 @@ const chemistryTdsHelper = {
402
398
  await this._setNumber('chemistry.tds.history.samples_count', samples.length);
403
399
 
404
400
  if (samples.length) {
405
- await this._setString(
401
+ await this._setNumber(
402
+ // FIX: history value.time states store sample timestamps, not readable strings.
406
403
  'chemistry.tds.history.oldest_sample_at',
407
- samples[0].time || this._formatDateTime(new Date(samples[0].ts)),
404
+ Number(samples[0].ts),
408
405
  );
409
- await this._setString(
406
+ await this._setNumber(
407
+ // FIX: history value.time states store sample timestamps, not readable strings.
410
408
  'chemistry.tds.history.newest_sample_at',
411
- samples[samples.length - 1].time || this._formatDateTime(new Date(samples[samples.length - 1].ts)),
409
+ Number(samples[samples.length - 1].ts),
412
410
  );
413
411
  }
414
412
 
@@ -424,7 +422,7 @@ const chemistryTdsHelper = {
424
422
  initialSet = true;
425
423
 
426
424
  await this._setNumber('chemistry.tds.reference.initial_value', initialValue);
427
- await this._setString('chemistry.tds.reference.initial_value_at', this._formatDateTime(now));
425
+ await this._setNumber('chemistry.tds.reference.initial_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
428
426
  await this._setBool('chemistry.tds.reference.initial_set', true);
429
427
  }
430
428
 
@@ -450,11 +448,11 @@ const chemistryTdsHelper = {
450
448
  const now = new Date();
451
449
 
452
450
  await this._setNumber('chemistry.tds.reference.initial_value', currentValue);
453
- await this._setString('chemistry.tds.reference.initial_value_at', this._formatDateTime(now));
451
+ await this._setNumber('chemistry.tds.reference.initial_value_at', now.getTime()); // FIX: value.time uses numeric ms timestamp.
454
452
  await this._setBool('chemistry.tds.reference.initial_set', true);
455
453
  await this._setNumber('chemistry.tds.reference.delta_since_initial', 0);
456
454
  await this._setString('chemistry.tds.debug.last_reason', 'reference_reset');
457
- await this._setString('chemistry.tds.debug.last_update', this._formatDateTime(now));
455
+ await this._setNumber('chemistry.tds.debug.last_update', now.getTime()); // FIX: value.time uses numeric ms timestamp.
458
456
 
459
457
  this._scheduleEvaluation('reference_reset', 500);
460
458
  },
@@ -560,15 +558,15 @@ const chemistryTdsHelper = {
560
558
 
561
559
  async _writeTrend(trend) {
562
560
  await this._setNumber('chemistry.tds.trend.reference_24h_value', trend.ref24h ? trend.ref24h.value : 0);
563
- await this._setString('chemistry.tds.trend.reference_24h_at', trend.ref24h ? trend.ref24h.time : '');
561
+ await this._setNumber('chemistry.tds.trend.reference_24h_at', trend.ref24h ? trend.ref24h.ts : 0); // FIX
564
562
  await this._setNumber('chemistry.tds.trend.delta_24h', trend.ref24h ? trend.delta24h : 0);
565
563
 
566
564
  await this._setNumber('chemistry.tds.trend.reference_7d_value', trend.ref7d ? trend.ref7d.value : 0);
567
- await this._setString('chemistry.tds.trend.reference_7d_at', trend.ref7d ? trend.ref7d.time : '');
565
+ await this._setNumber('chemistry.tds.trend.reference_7d_at', trend.ref7d ? trend.ref7d.ts : 0); // FIX
568
566
  await this._setNumber('chemistry.tds.trend.delta_7d', trend.ref7d ? trend.delta7d : 0);
569
567
 
570
568
  await this._setNumber('chemistry.tds.trend.reference_30d_value', trend.ref30d ? trend.ref30d.value : 0);
571
- await this._setString('chemistry.tds.trend.reference_30d_at', trend.ref30d ? trend.ref30d.time : '');
569
+ await this._setNumber('chemistry.tds.trend.reference_30d_at', trend.ref30d ? trend.ref30d.ts : 0); // FIX
572
570
  await this._setNumber('chemistry.tds.trend.delta_30d', trend.ref30d ? trend.delta30d : 0);
573
571
 
574
572
  await this._setString('chemistry.tds.trend.direction', trend.direction);
@@ -718,7 +716,8 @@ const chemistryTdsHelper = {
718
716
  await this._setString('chemistry.tds.evaluation.recommendation', I18n.translate('TDS evaluation is disabled.'));
719
717
  await this._setBool('chemistry.tds.evaluation.action_required', false);
720
718
  await this._setString('chemistry.tds.debug.last_reason', reason);
721
- await this._setString('chemistry.tds.debug.last_update', this._formatDateTime(new Date()));
719
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
720
+ await this._setNumber('chemistry.tds.debug.last_update', now.getTime()); // FIX
722
721
  },
723
722
 
724
723
  async _writeInvalid(recommendation, reason) {
@@ -730,7 +729,8 @@ const chemistryTdsHelper = {
730
729
  await this._setString('chemistry.tds.evaluation.recommendation', recommendation);
731
730
  await this._setBool('chemistry.tds.evaluation.action_required', false);
732
731
  await this._setString('chemistry.tds.debug.last_reason', reason);
733
- await this._setString('chemistry.tds.debug.last_update', this._formatDateTime(new Date()));
732
+ const now = new Date(); // FIX: write value.time as numeric Unix timestamp in milliseconds.
733
+ await this._setNumber('chemistry.tds.debug.last_update', now.getTime()); // FIX
734
734
  },
735
735
 
736
736
  async _readString(id) {
@@ -750,6 +750,19 @@ const chemistryTdsHelper = {
750
750
  return Number.isFinite(value) ? value : null;
751
751
  },
752
752
 
753
+ async _readTimestampOrNull(id) {
754
+ const state = await this.adapter.getStateAsync(id);
755
+ const value = state?.val;
756
+ const numberValue = Number(value);
757
+
758
+ if (Number.isFinite(numberValue) && numberValue > 0) {
759
+ return numberValue; // FIX: numeric value.time timestamps are used directly.
760
+ }
761
+
762
+ const legacyDate = this._parseGermanDateTime(value); // FIX: keep backward compatibility for old string values.
763
+ return legacyDate ? legacyDate.getTime() : null;
764
+ },
765
+
753
766
  async _readBoolean(id) {
754
767
  const state = await this.adapter.getStateAsync(id);
755
768
  return !!state?.val;