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 CHANGED
@@ -36,6 +36,7 @@ It provides automation for pumps, heating, solar and photovoltaic control as wel
36
36
 
37
37
  - **Solar Control**
38
38
  - Collector on/off thresholds with hysteresis
39
+ - Live collector-surface delta for dashboards and scripts
39
40
  - Collector warning threshold
40
41
  - Optional speech output for warnings
41
42
  - Automatic reset logic
@@ -43,10 +44,12 @@ It provides automation for pumps, heating, solar and photovoltaic control as wel
43
44
  - **Solar Extended**
44
45
  - Separate control for external solar actuators
45
46
  - Delta on/off thresholds
47
+ - Live collector-pool reference delta for dashboards and scripts
46
48
  - Maximum pool temperature limits
47
49
  - Diagnostic and reason states
48
50
  - Priority and block logic
49
51
  - Status section under `solar.extended.*`
52
+ - Runtime changes to `solar.extended.pool_temperature_source` are applied automatically; because Solar Extended uses a cyclic check interval, calculation, control logic, and `solar.extended.collector_pool_reference_delta` may take up to approximately 60 seconds to update.
50
53
 
51
54
  - **Photovoltaic Control**
52
55
  - Pump control based on PV surplus and household consumption
@@ -184,6 +187,15 @@ It provides automation for pumps, heating, solar and photovoltaic control as wel
184
187
  - No chlorine control
185
188
  - No automatic dosing
186
189
 
190
+ **Two-tier bounded chemistry history**
191
+ - Existing samples_json states remain the 7-day, 15-minute short-term history: at most 672 samples and 64 KB each
192
+ - New internal daily_json states keep compact local-calendar-day aggregates with min/max/avg/last/count: at most 32 entries and 8 KB each
193
+ - 24h and 7d trends use samples_json; 30d trends use the last value of the matching daily aggregate
194
+ - Existing 24h, 7d, and 30d trend states and text/HTML/JSON reports retain their API and meaning
195
+ - The daily aggregates complement but do not replace raw history; valid legacy data is normalized during migration and oversized JSON is rejected before parsing
196
+ - Raw long-term histories belong in a dedicated ioBroker history/time-series database
197
+ - If an oversized states.jsonl already prevents js-controller startup, it must be repaired externally before PoolControl can run
198
+
187
199
  **Chemistry Tools**
188
200
  - pH Plus calculator
189
201
  - pH Minus calculator
@@ -271,6 +283,22 @@ New features are added regularly – please refer to the changelog.
271
283
  ---
272
284
 
273
285
  ## Changelog
286
+ ### 1.3.34 (2026-06-27)
287
+
288
+ - **Major stability improvement:** Completely redesigned the internal chemistry history (pH, ORP and TDS) to prevent unbounded JSON state growth. This significantly reduces the risk of oversized `states.jsonl` files and potential js-controller startup failures.
289
+ - **New two-stage history architecture:** Chemistry history now uses a compact short-term history for recent measurements together with a dedicated daily history for long-term trends. All existing 24-hour, 7-day and 30-day trend calculations and reports remain fully available.
290
+ - **Protected history storage:** Added strict limits for chemistry history sample count and JSON size. Oversized or invalid history states are now safely detected, validated and handled before being processed.
291
+ - **Daily aggregates introduced:** Added compact daily aggregates for pH, ORP and TDS containing minimum, maximum, average and last measurement together with the number of valid samples. This preserves long-term trend analysis without storing large raw histories.
292
+ - **Additional safeguards:** Added size protection for the solar logbook and debug log to prevent uncontrolled state growth.
293
+ - **Maintenance:** Updated the `@iobroker/adapter-core` dependency to the latest recommended version.
294
+
295
+ ### 1.3.33 (2026-06-18)
296
+
297
+ - Added live delta states for standard solar and Solar Extended control:
298
+ - `solar.collector_surface_delta`
299
+ - `solar.extended.collector_pool_reference_delta`
300
+ - Documented that runtime changes to `solar.extended.pool_temperature_source` are applied automatically, but may take up to approximately 60 seconds due to the Solar Extended cyclic check interval.
301
+
274
302
  ### 1.3.32 (2026-06-08)
275
303
 
276
304
  - Added circulation plausibility diagnostics for daily circulation calculations.
@@ -288,20 +316,6 @@ New features are added regularly – please refer to the changelog.
288
316
  - Cleaned up outdated Admin i18n keys.
289
317
  - Replaced native timers in AI weather helpers with ioBroker adapter timers.
290
318
 
291
- ### 1.3.29 (2026-06-04)
292
-
293
- - Added Pool Insights V1 with observations, status evaluation and runtime i18n support.
294
- - Improved Pool Insights text generation and removed dependency on external summary blocks.
295
- - Added configurable startup power check timeout state for pump startup monitoring.
296
- - Fixed missing initialization of season and solar warning runtime states from adapter configuration.
297
- - Added admin UI information about initial configuration values and runtime datapoint control.
298
-
299
- ### 1.3.28 (2026-06-03)
300
-
301
- - Added configurable startup power check timeout for pump monitoring (`pump.startup_power_check_timeout_sec`).
302
- - Default behavior remains unchanged (5 seconds).
303
- - Improved compatibility with delayed power measurements from smart plugs and power meters.
304
-
305
319
  ## Archived Release History
306
320
 
307
321
  For older releases and archived version history see:
@@ -88,12 +88,12 @@
88
88
  "Object ID Auxiliary pump 3": "Ідентифікатор об’єкта Допоміжний насос 3",
89
89
  "Object ID Collector Sensor": "Датчик збирача ідентифікаторів об’єктів",
90
90
  "Object ID Current Power (W)": "Ідентифікатор об'єкта Поточна потужність (Вт)",
91
- "Object ID Flow Sensor": "Object ID Flow Sensor",
92
- "Object ID Ground Sensor": "Object ID Ground Sensor",
91
+ "Object ID Flow Sensor": "Ідентифікатор об'єкта датчика потоку",
92
+ "Object ID Ground Sensor": "Ідентифікатор об'єкта датчика температури дна",
93
93
  "Object ID Heating / Heat Pump": "Ідентифікатор об'єкта Опалення / Тепловий насос",
94
- "Object ID Lighting 1": "Object ID Lighting 1",
95
- "Object ID Lighting 2": "Object ID Lighting 2",
96
- "Object ID Lighting 3": "Object ID Lighting 3",
94
+ "Object ID Lighting 1": "Ідентифікатор об'єкта освітлення 1",
95
+ "Object ID Lighting 2": "Ідентифікатор об'єкта Освітлення 2",
96
+ "Object ID Lighting 3": "Ідентифікатор об'єкта Освітлення 3",
97
97
  "Object ID Outside Temperature Sensor": "Ідентифікатор об’єкта Датчик зовнішньої температури",
98
98
  "Object ID PV generation power (W)": "Ідентифікатор об'єкта Потужність генерації PV (Вт)",
99
99
  "Object ID Pressure Sensor (bar)": "Ідентифікатор об'єкта Датчик тиску (бар)",
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "poolcontrol",
4
- "version": "1.3.32",
4
+ "version": "1.3.34",
5
5
  "news": {
6
+ "1.3.34": {
7
+ "en": "Improved chemistry history architecture with protected short-term and daily history, prevented oversized JSON states, added safeguards against state database growth, enhanced solar logbook/debug log protection, and updated adapter-core dependency.",
8
+ "de": "Chemie-Historie grundlegend verbessert: geschützte Kurzzeit- und Tageshistorie eingeführt, übergroße JSON-States verhindert, Schutz vor unkontrolliertem Wachstum der State-Datenbank ergänzt, Solar-Logbuch und Debug-Log zusätzlich abgesichert sowie adapter-core-Abhängigkeit aktualisiert.",
9
+ "ru": "Улучшенная архитектура истории химических процессов с защищенной краткосрочной и ежедневной историей, предотвращение чрезмерного размера состояний JSON, добавлена ​​защита от роста базы данных состояний, улучшена защита журнала солнечной энергии/журнала отладки, а также обновлена ​​зависимость от ядра адаптера.",
10
+ "pt": "Arquitetura de histórico químico aprimorada com histórico diário e de curto prazo protegido, prevenção de estados JSON superdimensionados, salvaguardas adicionais contra o crescimento do banco de dados estadual, proteção aprimorada de logbook solar/log de depuração e dependência atualizada do núcleo do adaptador.",
11
+ "nl": "Verbeterde architectuur van de chemiegeschiedenis met beschermde kortetermijn- en dagelijkse geschiedenis, voorkoming van te grote JSON-statussen, extra beveiliging tegen de groei van de staatsdatabase, verbeterde bescherming van het zonne-logboek/debug-logboek en bijgewerkte adapter-core-afhankelijkheid.",
12
+ "fr": "Architecture d'historique chimique améliorée avec historique protégé à court terme et quotidien, prévention des états JSON surdimensionnés, protections supplémentaires contre la croissance de la base de données d'état, protection améliorée du journal solaire/journal de débogage et dépendance mise à jour du noyau de l'adaptateur.",
13
+ "it": "Architettura della cronologia chimica migliorata con cronologia protetta a breve termine e giornaliera, prevenzione di stati JSON sovradimensionati, protezioni aggiuntive contro la crescita del database di stato, protezione migliorata del registro solare/registro di debug e dipendenza aggiornata del core dell'adattatore.",
14
+ "es": "Se mejoró la arquitectura del historial químico con historial diario y a corto plazo protegido, se evitaron estados JSON de gran tamaño, se agregaron salvaguardias contra el crecimiento de la base de datos estatal, se mejoró la protección del registro solar/registro de depuración y se actualizó la dependencia del núcleo del adaptador.",
15
+ "pl": "Ulepszona architektura historii chemii z chronioną historią krótkoterminową i codzienną, zapobieganie nadmiernym stanom JSON, dodane zabezpieczenia przed rozrostem bazy danych stanu, ulepszona ochrona dziennika solarnego/dziennika debugowania oraz zaktualizowana zależność od rdzenia adaptera.",
16
+ "uk": "Покращена архітектура хімічної історії із захищеною короткостроковою та щоденною історією, запобіганням надмірним станам JSON, доданим запобіжним засобам проти зростання бази даних стану, покращеному захисту сонячного журналу/журналу налагодження та оновленій залежності ядра адаптера.",
17
+ "zh-cn": "改进了化学历史架构,具有受保护的短期和每日历史记录,防止过大的 JSON 状态,增加了针对状态数据库增长的保护措施,增强了太阳能日志/调试日志保护,并更新了适配器核心依赖性。"
18
+ },
19
+ "1.3.33": {
20
+ "en": "Added live delta states for standard solar and Solar Extended control. Documented Solar Extended pool reference runtime update behavior.",
21
+ "de": "Live-Delta-Datenpunkte für Standard-Solar und Solar Extended ergänzt. Laufzeitverhalten der Solar-Extended-Poolreferenz dokumentiert.",
22
+ "ru": "Добавлены дельта-состояния в реальном времени для стандартного и расширенного управления солнечной энергией. Документированное поведение обновления во время выполнения ссылки на расширенный пул Solar.",
23
+ "pt": "Adicionados estados delta ao vivo para controle solar padrão e solar estendido. Comportamento de atualização de tempo de execução de referência do pool Solar Extended documentado.",
24
+ "nl": "Live deltatoestanden toegevoegd voor standaard zonne-energie en Solar Extended-regeling. Gedocumenteerd runtime-updategedrag van de Solar Extended-poolreferentie.",
25
+ "fr": "Ajout d'états delta en direct pour le contrôle solaire standard et solaire étendu. Comportement documenté de la mise à jour du runtime de référence du pool Solar Extended.",
26
+ "it": "Aggiunti stati delta live per il controllo solare standard e Solar Extended. Comportamento documentato dell'aggiornamento runtime di riferimento del pool Solar Extended.",
27
+ "es": "Se agregaron estados delta en vivo para control solar estándar y solar extendido. Comportamiento de actualización del tiempo de ejecución de referencia del grupo Solar Extended documentado.",
28
+ "pl": "Dodano stany delta na żywo dla standardowego sterowania solarnego i Solar Extended. Udokumentowane zachowanie aktualizacji środowiska uruchomieniowego referencyjnej puli Solar Extended.",
29
+ "uk": "Додано живі дельта-стани для стандартного сонячного та розширеного керування Solar. Задокументована поведінка оновлення середовища виконання довідкового пулу Solar Extended.",
30
+ "zh-cn": "为标准太阳能和太阳能扩展控制添加了实时增量状态。记录了 Solar Extended 池参考运行时更新行为。"
31
+ },
6
32
  "1.3.32": {
7
33
  "en": "Added circulation plausibility diagnostics. New checks detect implausible pump power, flow rates, and daily circulation jumps. Added detailed diagnostic states under circulation.plausibility to simplify troubleshooting and analysis of circulation calculations.",
8
34
  "de": "Plausibilitätsdiagnose für die Umwälzberechnung hinzugefügt. Neue Prüfungen erkennen unplausible Pumpenleistungen, Durchflusswerte und Sprünge der Tagesumwälzung. Detaillierte Diagnose-States unter circulation.plausibility erleichtern die Fehlersuche und Analyse der Umwälzberechnung.",
@@ -41,32 +67,6 @@
41
67
  "pl": "Zaktualizowano narzędzia wydania do wymaganej wersji minimalnej. Wyczyszczono nieaktualne klucze administratora i18n. Zastąpiono natywne liczniki czasu w pomocnikach pogodowych AI zegarami adaptera ioBroker.",
42
68
  "uk": "Оновлено інструмент випуску до необхідної мінімальної версії. Очищено застарілі ключі адміністратора i18n. Вбудовані таймери в помічниках погоди штучного інтелекту замінено на таймери адаптера ioBroker.",
43
69
  "zh-cn": "将发布工具更新为所需的最低版本。清理了过时的 Admin i18n 密钥。将 AI 天气助手中的本机计时器替换为 ioBroker 适配器计时器。"
44
- },
45
- "1.3.29": {
46
- "en": "Added Pool Insights V1 with observations, status evaluation and runtime i18n support. Improved Pool Insights text generation and removed dependency on external summary blocks. Fixed missing initialization of season and solar warning runtime states from adapter configuration. Added admin UI notes explaining initial values versus runtime datapoint control.",
47
- "de": "Pool Insights V1 mit Beobachtungen, Statusbewertung und Runtime-i18n hinzugefügt. Pool-Insights-Texte verbessert und die Abhängigkeit von externen Summary-Blöcken entfernt. Fehlende Initialisierung der Saison- und Solarwarnungs-States aus der Adapterkonfiguration behoben. Hinweise in der Admin-Oberfläche ergänzt, die den Unterschied zwischen Initialwerten und Runtime-Datenpunkten erläutern.",
48
- "ru": "Добавлен Pool Insights V1 с наблюдениями, оценкой состояния и поддержкой i18n во время выполнения. Улучшено создание текста Pool Insights и удалена зависимость от внешних сводных блоков. Исправлена ​​отсутствующая инициализация состояний выполнения сезонных и солнечных предупреждений из конфигурации адаптера. Добавлены примечания к интерфейсу администратора, объясняющие начальные значения и контроль точек данных во время выполнения.",
49
- "pt": "Adicionado Pool Insights V1 com observações, avaliação de status e suporte ao tempo de execução i18n. Geração de texto do Pool Insights aprimorada e dependência removida de blocos de resumo externos. Corrigida a falta de inicialização dos estados de tempo de execução de aviso solar e de estação na configuração do adaptador. Adicionadas notas da UI administrativa explicando os valores iniciais versus controle de ponto de dados em tempo de execução.",
50
- "nl": "Pool Insights V1 toegevoegd met observaties, statusevaluatie en runtime i18n-ondersteuning. Verbeterde tekstgeneratie in Pool Insights en verwijderde de afhankelijkheid van externe samenvattingsblokken. Probleem opgelost waarbij de initialisatie van seizoens- en zonne-waarschuwingsruntimestatussen uit de adapterconfiguratie ontbreekt. Aantekeningen in de beheerdersinterface toegevoegd waarin initiële waarden versus runtime-datapuntcontrole worden uitgelegd.",
51
- "fr": "Ajout de Pool Insights V1 avec observations, évaluation de l'état et prise en charge du runtime i18n. Amélioration de la génération de texte Pool Insights et suppression de la dépendance aux blocs de résumé externes. Correction de l'initialisation manquante des états d'exécution de la saison et de l'avertissement solaire dans la configuration de l'adaptateur. Ajout de notes sur l'interface utilisateur d'administration expliquant les valeurs initiales par rapport au contrôle des points de données d'exécution.",
52
- "it": "Aggiunto Pool Insights V1 con osservazioni, valutazione dello stato e supporto i18n runtime. Migliorata la generazione del testo di Pool Insights e rimossa la dipendenza dai blocchi di riepilogo esterni. Risolto il problema con l'inizializzazione mancante degli stati di runtime dell'avviso stagionale e solare dalla configurazione dell'adattatore. Aggiunte note sull'interfaccia utente di amministrazione che spiegano i valori iniziali rispetto al controllo del punto dati di runtime.",
53
- "es": "Se agregó Pool Insights V1 con observaciones, evaluación de estado y soporte de tiempo de ejecución i18n. Se mejoró la generación de texto de Pool Insights y se eliminó la dependencia de bloques de resumen externos. Se corrigió la falta de inicialización de los estados de tiempo de ejecución de advertencia solar y de temporada desde la configuración del adaptador. Se agregaron notas de la interfaz de usuario del administrador que explican los valores iniciales versus el control de puntos de datos en tiempo de ejecución.",
54
- "pl": "Dodano usługę Pool Insights V1 z obserwacjami, oceną stanu i obsługą środowiska wykonawczego i18n. Ulepszone generowanie tekstu Pool Insights i usunięta zależność od zewnętrznych bloków podsumowań. Naprawiono brakującą inicjalizację stanów wykonawczych sezonu i ostrzeżenia słonecznego z konfiguracji adaptera. Dodano uwagi do interfejsu administratora wyjaśniające wartości początkowe w porównaniu z kontrolą punktów danych w czasie wykonywania.",
55
- "uk": "Додано Pool Insights V1 із спостереженнями, оцінкою стану та підтримкою i18n під час виконання. Покращено створення тексту Pool Insights і видалено залежність від зовнішніх блоків підсумків. Виправлено відсутність ініціалізації сезону та станів попередження про сонячне світло з конфігурації адаптера. Додано примітки інтерфейсу користувача адміністратора, що пояснюють початкові значення та керування точкою даних під час виконання.",
56
- "zh-cn": "添加了 Pool Insights V1,具有观察、状态评估和运行时 i18n 支持。改进了 Pool Insights 文本生成并消除了对外部摘要块的依赖。修复了适配器配置中缺少季节和太阳警告运行时状态初始化的问题。添加了管理 UI 注释,解释初始值与运行时数据点控制。"
57
- },
58
- "1.3.28": {
59
- "en": "Added configurable startup power check timeout for pump monitoring (5-10 seconds). Improved compatibility with delayed power measurements from smart plugs and power meters.",
60
- "de": "Konfigurierbares Timeout für die Leistungsprüfung nach Pumpenstart (5-10 Sekunden) hinzugefügt. Kompatibilität mit verzögert aktualisierten Leistungswerten von Smart-Steckdosen und Leistungsmessern verbessert.",
61
- "ru": "Добавлен настраиваемый тайм-аут проверки мощности при запуске для мониторинга насоса (5–10 секунд). Улучшена совместимость с измерениями мощности с задержкой от интеллектуальных розеток и измерителей мощности.",
62
- "pt": "Adicionado tempo limite de verificação de energia de inicialização configurável para monitoramento da bomba (5 a 10 segundos). Compatibilidade aprimorada com medições de energia atrasadas de plugues inteligentes e medidores de energia.",
63
- "nl": "Configureerbare time-out voor stroomcontrole bij opstarten toegevoegd voor pompbewaking (5-10 seconden). Verbeterde compatibiliteit met vertraagde stroommetingen van slimme stekkers en stroommeters.",
64
- "fr": "Ajout d'un délai d'expiration configurable pour la vérification de l'alimentation au démarrage pour la surveillance de la pompe (5 à 10 secondes). Compatibilité améliorée avec les mesures de puissance retardées des prises intelligentes et des compteurs de puissance.",
65
- "it": "Aggiunto timeout configurabile del controllo dell'alimentazione all'avvio per il monitoraggio della pompa (5-10 secondi). Compatibilità migliorata con misurazioni di potenza ritardate da prese intelligenti e misuratori di potenza.",
66
- "es": "Se agregó un tiempo de espera de verificación de energía de inicio configurable para el monitoreo de la bomba (5-10 segundos). Compatibilidad mejorada con mediciones de potencia retrasadas de enchufes inteligentes y medidores de potencia.",
67
- "pl": "Dodano konfigurowalny limit czasu sprawdzania mocy rozruchowej dla monitorowania pompy (5-10 sekund). Poprawiona kompatybilność z opóźnionymi pomiarami mocy z inteligentnych wtyczek i mierników mocy.",
68
- "uk": "Додано настроюваний тайм-аут перевірки живлення при запуску для моніторингу насоса (5-10 секунд). Покращена сумісність із затримкою вимірювання потужності від розумних розеток і лічильників.",
69
- "zh-cn": "添加了用于泵监控的可配置启动电源检查超时(5-10 秒)。改进了与智能插头和功率计的延迟功率测量的兼容性。"
70
70
  }
71
71
  },
72
72
  "titleLang": {
@@ -2,8 +2,14 @@
2
2
 
3
3
  const { I18n } = require('@iobroker/adapter-core');
4
4
 
5
- const MAX_HISTORY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
5
+ const MAX_HISTORY_AGE_MS = 7 * 24 * 60 * 60 * 1000;
6
+ const MAX_HISTORY_SAMPLES = 672;
7
+ const MAX_HISTORY_BYTES = 64 * 1024;
6
8
  const MIN_SAMPLE_INTERVAL_MS = 15 * 60 * 1000;
9
+ const MAX_DAILY_HISTORY_AGE_MS = 32 * 24 * 60 * 60 * 1000;
10
+ const MAX_DAILY_HISTORY_SAMPLES = 32;
11
+ const MAX_DAILY_HISTORY_BYTES = 8 * 1024;
12
+ const DAILY_REFERENCE_TOLERANCE_MS = 4 * 24 * 60 * 60 * 1000;
7
13
 
8
14
  const chemistryOrpHelper = {
9
15
  adapter: null,
@@ -167,7 +173,7 @@ const chemistryOrpHelper = {
167
173
  return;
168
174
  }
169
175
 
170
- await this._processValue(source, rawValue, reason, true);
176
+ await this._processValue(source, rawValue, reason, source === 'manual');
171
177
  },
172
178
 
173
179
  _scheduleEvaluation(reason, delayMs = 500) {
@@ -292,7 +298,7 @@ const chemistryOrpHelper = {
292
298
  await this._updateLastValues(value, now);
293
299
 
294
300
  const history = await this._updateHistory(value, now, forceSample);
295
- const trend = await this._calculateTrend(value, now, history);
301
+ const trend = await this._calculateTrend(value, now, history.samples, history.dailySamples);
296
302
  const phReference = await this._updatePhReference();
297
303
  const evaluation = await this._evaluateOrp(value, phReference);
298
304
 
@@ -406,16 +412,17 @@ const chemistryOrpHelper = {
406
412
 
407
413
  async _updateHistory(value, now, forceSample) {
408
414
  let samples = await this._readJsonArray('chemistry.orp.history.samples_json');
415
+ const dailySeedSamples = samples;
409
416
  const nowTs = now.getTime();
410
417
  const minTs = nowTs - MAX_HISTORY_AGE_MS;
411
418
 
412
- samples = samples.filter(
413
- sample => sample && Number(sample.ts) >= minTs && Number.isFinite(Number(sample.value)),
414
- );
419
+ samples = samples.filter(sample => sample.ts >= minTs && sample.ts <= nowTs);
415
420
 
416
421
  const newest = samples.length ? samples[samples.length - 1] : null;
417
422
  const newestTs = newest ? Number(newest.ts) : 0;
418
- const shouldStore = forceSample || !newest || nowTs - newestTs >= MIN_SAMPLE_INTERVAL_MS;
423
+ const withinSampleInterval = newest && nowTs - newestTs < MIN_SAMPLE_INTERVAL_MS;
424
+ const sameValue = newest && Number(newest.value) === value;
425
+ const shouldStore = !newest || !withinSampleInterval || (forceSample && !sameValue);
419
426
 
420
427
  if (shouldStore) {
421
428
  samples.push({
@@ -425,9 +432,12 @@ const chemistryOrpHelper = {
425
432
  });
426
433
  }
427
434
 
428
- samples = samples.filter(sample => sample && Number(sample.ts) >= minTs);
435
+ samples = samples.filter(sample => sample.ts >= minTs && sample.ts <= nowTs).slice(-MAX_HISTORY_SAMPLES);
436
+
437
+ const preparedHistory = this._prepareHistoryForWrite(samples, 'chemistry.orp.history.samples_json');
438
+ samples = preparedHistory.samples;
429
439
 
430
- await this._setString('chemistry.orp.history.samples_json', JSON.stringify(samples));
440
+ await this._setString('chemistry.orp.history.samples_json', preparedHistory.json);
431
441
  await this._setNumber('chemistry.orp.history.samples_count', samples.length);
432
442
 
433
443
  if (samples.length) {
@@ -443,15 +453,20 @@ const chemistryOrpHelper = {
443
453
  );
444
454
  }
445
455
 
446
- return samples;
456
+ const dailySamples = await this._updateDailyHistory(dailySeedSamples, value, now, shouldStore);
457
+ return { samples, dailySamples };
447
458
  },
448
459
 
449
- async _calculateTrend(currentValue, now, samples) {
460
+ async _calculateTrend(currentValue, now, samples, dailySamples) {
450
461
  const nowTs = now.getTime();
451
462
 
452
463
  const ref24h = this._findReferenceSample(samples, nowTs - 24 * 60 * 60 * 1000);
453
464
  const ref7d = this._findReferenceSample(samples, nowTs - 7 * 24 * 60 * 60 * 1000);
454
- const ref30d = this._findReferenceSample(samples, nowTs - 30 * 24 * 60 * 60 * 1000);
465
+ const ref30d = this._findReferenceSample(
466
+ dailySamples,
467
+ nowTs - 30 * 24 * 60 * 60 * 1000,
468
+ DAILY_REFERENCE_TOLERANCE_MS,
469
+ );
455
470
 
456
471
  const delta24h = ref24h ? currentValue - ref24h.value : 0;
457
472
  const delta7d = ref7d ? currentValue - ref7d.value : 0;
@@ -472,7 +487,7 @@ const chemistryOrpHelper = {
472
487
  };
473
488
  },
474
489
 
475
- _findReferenceSample(samples, targetTs) {
490
+ _findReferenceSample(samples, targetTs, toleranceMs = null) {
476
491
  if (!samples.length) {
477
492
  return null;
478
493
  }
@@ -482,7 +497,7 @@ const chemistryOrpHelper = {
482
497
 
483
498
  for (const sample of samples) {
484
499
  const ts = Number(sample.ts);
485
- const value = Number(sample.value);
500
+ const value = Number(sample.value ?? sample.last);
486
501
 
487
502
  if (!Number.isFinite(ts) || !Number.isFinite(value)) {
488
503
  continue;
@@ -500,7 +515,7 @@ const chemistryOrpHelper = {
500
515
  }
501
516
  }
502
517
 
503
- return best;
518
+ return toleranceMs === null || bestDistance <= toleranceMs ? best : null;
504
519
  },
505
520
 
506
521
  _getOverallDirection(ref24h, ref7d, ref30d, delta24h, delta7d, delta30d) {
@@ -748,17 +763,264 @@ const chemistryOrpHelper = {
748
763
  return !!state?.val;
749
764
  },
750
765
 
766
+ async _updateDailyHistory(shortTermSamples, value, now, sampleStored) {
767
+ const id = 'chemistry.orp.history.daily_json';
768
+ let dailySamples = await this._readDailyJson(id);
769
+ const nowTs = now.getTime();
770
+ const minTs = nowTs - MAX_DAILY_HISTORY_AGE_MS;
771
+ const initializeFromSeeds = dailySamples.length === 0;
772
+
773
+ if (initializeFromSeeds) {
774
+ const legacyValueState = await this.adapter.getStateAsync('chemistry.orp.trend.reference_30d_value');
775
+ const legacyAtState = await this.adapter.getStateAsync('chemistry.orp.trend.reference_30d_at');
776
+ const legacyValue = Number(legacyValueState?.val);
777
+ const legacyTs = Number(legacyAtState?.val);
778
+
779
+ if (
780
+ Number.isFinite(legacyValue) &&
781
+ legacyValue >= 0 &&
782
+ legacyValue <= 1200 &&
783
+ Number.isFinite(legacyTs) &&
784
+ legacyTs >= minTs &&
785
+ legacyTs <= nowTs &&
786
+ !shortTermSamples.some(sample => this._dayKey(sample.ts) === this._dayKey(legacyTs))
787
+ ) {
788
+ this._addDailySample(dailySamples, legacyTs, legacyValue);
789
+ }
790
+
791
+ for (const sample of shortTermSamples) {
792
+ if (sample.ts >= minTs && sample.ts <= nowTs) {
793
+ this._addDailySample(dailySamples, sample.ts, sample.value);
794
+ }
795
+ }
796
+ if (sampleStored) {
797
+ this._addDailySample(dailySamples, nowTs, value);
798
+ }
799
+ } else if (sampleStored) {
800
+ this._addDailySample(dailySamples, nowTs, value);
801
+ }
802
+
803
+ dailySamples = dailySamples
804
+ .filter(sample => sample.ts >= minTs && sample.ts <= nowTs)
805
+ .sort((a, b) => a.ts - b.ts)
806
+ .slice(-MAX_DAILY_HISTORY_SAMPLES);
807
+
808
+ const preparedHistory = this._prepareDailyHistoryForWrite(dailySamples, id);
809
+ await this._setString(id, preparedHistory.json);
810
+ return preparedHistory.samples;
811
+ },
812
+
813
+ _addDailySample(dailySamples, sampleTs, value) {
814
+ const day = this._dayKey(sampleTs);
815
+ const existing = dailySamples.find(sample => sample.day === day);
816
+
817
+ if (!existing) {
818
+ dailySamples.push({
819
+ day,
820
+ ts: this._dayStartTs(sampleTs),
821
+ min: value,
822
+ max: value,
823
+ avg: value,
824
+ last: value,
825
+ count: 1,
826
+ });
827
+ return;
828
+ }
829
+
830
+ existing.min = Math.min(existing.min, value);
831
+ existing.max = Math.max(existing.max, value);
832
+ existing.avg = (existing.avg * existing.count + value) / (existing.count + 1);
833
+ existing.last = value;
834
+ existing.count += 1;
835
+ },
836
+
837
+ async _readDailyJson(id) {
838
+ const state = await this.adapter.getStateAsync(id);
839
+ const rawValue = state?.val;
840
+
841
+ if (rawValue === null || rawValue === undefined || rawValue === '') {
842
+ return [];
843
+ }
844
+
845
+ if (typeof rawValue !== 'string') {
846
+ this.adapter.log.warn(`[chemistryOrpHelper] Ignoring non-string daily history state ${id}.`);
847
+ return [];
848
+ }
849
+
850
+ if (Buffer.byteLength(rawValue, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
851
+ this.adapter.log.warn(`[chemistryOrpHelper] Ignoring oversized daily history state ${id} (> 8 KB).`);
852
+ return [];
853
+ }
854
+
855
+ try {
856
+ const parsed = JSON.parse(rawValue);
857
+ if (!Array.isArray(parsed)) {
858
+ return [];
859
+ }
860
+
861
+ const nowTs = Date.now();
862
+ const normalized = parsed
863
+ .map(sample => {
864
+ const sourceTs = Number(sample?.ts);
865
+ const legacyValue = Number(sample?.value);
866
+ const min = Number(sample?.min ?? legacyValue);
867
+ const max = Number(sample?.max ?? legacyValue);
868
+ const avg = Number(sample?.avg ?? legacyValue);
869
+ const last = Number(sample?.last ?? legacyValue);
870
+ const count = sample?.count === undefined ? 1 : Number(sample.count);
871
+
872
+ if (!Number.isFinite(sourceTs) || sourceTs <= 0 || sourceTs > nowTs) {
873
+ return null;
874
+ }
875
+ if (
876
+ !Number.isFinite(min) ||
877
+ !Number.isFinite(max) ||
878
+ !Number.isFinite(avg) ||
879
+ !Number.isFinite(last) ||
880
+ min < 0 ||
881
+ max > 1200 ||
882
+ min > max ||
883
+ avg < min ||
884
+ avg > max ||
885
+ last < min ||
886
+ last > max
887
+ ) {
888
+ return null;
889
+ }
890
+ if (!Number.isInteger(count) || count < 1 || count > 1000000) {
891
+ return null;
892
+ }
893
+
894
+ return {
895
+ day: this._dayKey(sourceTs),
896
+ ts: this._dayStartTs(sourceTs),
897
+ min,
898
+ max,
899
+ avg,
900
+ last,
901
+ count,
902
+ };
903
+ })
904
+ .filter(Boolean)
905
+ .sort((a, b) => a.ts - b.ts);
906
+
907
+ const byDay = new Map();
908
+ for (const sample of normalized) {
909
+ if (!byDay.has(sample.day)) {
910
+ byDay.set(sample.day, sample);
911
+ }
912
+ }
913
+ return [...byDay.values()].slice(-MAX_DAILY_HISTORY_SAMPLES);
914
+ } catch {
915
+ return [];
916
+ }
917
+ },
918
+
919
+ _prepareDailyHistoryForWrite(samples, id) {
920
+ let limitedSamples = samples.slice(-MAX_DAILY_HISTORY_SAMPLES);
921
+ let json = JSON.stringify(limitedSamples);
922
+ let trimmedForSize = false;
923
+
924
+ while (limitedSamples.length && Buffer.byteLength(json, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
925
+ limitedSamples = limitedSamples.slice(1);
926
+ json = JSON.stringify(limitedSamples);
927
+ trimmedForSize = true;
928
+ }
929
+
930
+ if (trimmedForSize) {
931
+ this.adapter.log.warn(`[chemistryOrpHelper] Daily history ${id} was trimmed to stay within 8 KB.`);
932
+ }
933
+
934
+ if (Buffer.byteLength(json, 'utf8') > MAX_DAILY_HISTORY_BYTES) {
935
+ this.adapter.log.warn(`[chemistryOrpHelper] Daily history ${id} could not be limited; resetting it.`);
936
+ return { samples: [], json: '[]' };
937
+ }
938
+
939
+ return { samples: limitedSamples, json };
940
+ },
941
+
942
+ _dayKey(ts) {
943
+ const date = new Date(ts);
944
+ const year = String(date.getFullYear());
945
+ const month = String(date.getMonth() + 1).padStart(2, '0');
946
+ const day = String(date.getDate()).padStart(2, '0');
947
+ return [year, month, day].join('-');
948
+ },
949
+
950
+ _dayStartTs(ts) {
951
+ const date = new Date(ts);
952
+ date.setHours(0, 0, 0, 0);
953
+ return date.getTime();
954
+ },
955
+
751
956
  async _readJsonArray(id) {
752
957
  const state = await this.adapter.getStateAsync(id);
958
+ const rawValue = state?.val;
959
+
960
+ if (rawValue === null || rawValue === undefined || rawValue === '') {
961
+ return [];
962
+ }
963
+
964
+ if (typeof rawValue !== 'string') {
965
+ this.adapter.log.warn(`[chemistryOrpHelper] Ignoring non-string history state ${id}.`);
966
+ return [];
967
+ }
968
+
969
+ if (Buffer.byteLength(rawValue, 'utf8') > MAX_HISTORY_BYTES) {
970
+ this.adapter.log.warn(`[chemistryOrpHelper] Ignoring oversized history state ${id} (> 64 KB).`);
971
+ return [];
972
+ }
753
973
 
754
974
  try {
755
- const parsed = JSON.parse(String(state?.val || '[]'));
756
- return Array.isArray(parsed) ? parsed : [];
975
+ const parsed = JSON.parse(rawValue);
976
+ if (!Array.isArray(parsed)) {
977
+ return [];
978
+ }
979
+
980
+ const nowTs = Date.now();
981
+ return parsed
982
+ .map(sample => {
983
+ const ts = Number(sample?.ts);
984
+ const value = Number(sample?.value);
985
+ if (!Number.isFinite(ts) || ts <= 0 || ts > nowTs) {
986
+ return null;
987
+ }
988
+ if (!Number.isFinite(value) || value < 0 || value > 1200) {
989
+ return null;
990
+ }
991
+ return { ts, time: this._formatDateTime(new Date(ts)), value };
992
+ })
993
+ .filter(Boolean)
994
+ .sort((a, b) => a.ts - b.ts)
995
+ .slice(-MAX_HISTORY_SAMPLES);
757
996
  } catch {
758
997
  return [];
759
998
  }
760
999
  },
761
1000
 
1001
+ _prepareHistoryForWrite(samples, id) {
1002
+ let limitedSamples = samples.slice(-MAX_HISTORY_SAMPLES);
1003
+ let json = JSON.stringify(limitedSamples);
1004
+ let trimmedForSize = false;
1005
+
1006
+ while (limitedSamples.length && Buffer.byteLength(json, 'utf8') > MAX_HISTORY_BYTES) {
1007
+ limitedSamples = limitedSamples.slice(1);
1008
+ json = JSON.stringify(limitedSamples);
1009
+ trimmedForSize = true;
1010
+ }
1011
+
1012
+ if (trimmedForSize) {
1013
+ this.adapter.log.warn(`[chemistryOrpHelper] History ${id} was trimmed to stay within 64 KB.`);
1014
+ }
1015
+
1016
+ if (Buffer.byteLength(json, 'utf8') > MAX_HISTORY_BYTES) {
1017
+ this.adapter.log.warn(`[chemistryOrpHelper] History ${id} could not be limited safely; resetting it.`);
1018
+ return { samples: [], json: '[]' };
1019
+ }
1020
+
1021
+ return { samples: limitedSamples, json };
1022
+ },
1023
+
762
1024
  async _setString(id, value) {
763
1025
  await this.adapter.setStateChangedAsync(id, { val: String(value ?? ''), ack: true });
764
1026
  },