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.
- package/README.md +67 -74
- package/io-package.json +40 -40
- package/lib/helpers/chemistryOrpHelper.js +818 -0
- package/lib/helpers/chemistryPhHelper.js +309 -39
- package/lib/helpers/chemistryTdsHelper.js +37 -24
- package/lib/helpers/pumpHelper2.js +21 -11
- 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 +222 -8
- package/lib/stateDefinitions/chemistryTdsStates.js +16 -16
- 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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
372
|
-
|
|
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.
|
|
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
|
|
449
|
+
async _updateLastValues(value, now) {
|
|
431
450
|
const lastValid = await this._readNumberOrNull('chemistry.ph.input.last_valid_value');
|
|
432
|
-
const lastValidAt = await this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
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
|
-
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
status: 'low',
|
|
684
|
+
actionRequired: true,
|
|
685
|
+
recommendation,
|
|
686
|
+
};
|
|
464
687
|
}
|
|
465
688
|
|
|
466
689
|
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
|
-
),
|
|
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
|
-
|
|
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, '&')
|
|
948
|
+
.replace(/</g, '<')
|
|
949
|
+
.replace(/>/g, '>')
|
|
950
|
+
.replace(/"/g, '"')
|
|
951
|
+
.replace(/'/g, ''');
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
364
|
+
await this._setNumber('chemistry.tds.input.previous_value_at', lastValidAt); // FIX: pass stored timestamp through unchanged.
|
|
365
365
|
|
|
366
|
-
const
|
|
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.
|
|
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.
|
|
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
|
-
|
|
404
|
+
Number(samples[0].ts),
|
|
408
405
|
);
|
|
409
|
-
await this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|