iobroker.idm-multitalent_002 2.2.0 → 2.2.1

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
@@ -56,6 +56,9 @@ Example screenshots of objects:
56
56
  ![Status](resources/ioBrokerAdapter-Status.jpg)
57
57
 
58
58
  ## Changelog
59
+ ### 2.2.1 (2026-09-13)
60
+ * (zloe) the per-sweep log line introduced in 2.2.0 (`sensor sweep done in ...`) is now logged at debug level instead of info - in normal operation a sensor sweep completes roughly every 10s, which was far too chatty for info level. In its place, a new info-level summary is logged once every 10 minutes with a small statistic (sweep count, min/avg/max duration) per group covering that window
61
+
59
62
  ### 2.2.0 (2026-09-11)
60
63
  * (zloe) sensor and settings are no longer polled in lockstep: sensor is read as fast as the protocol allows (throttled to at most once every 10s), settings only once every 60s plus once right away after any value is written to the heat pump - both intervals measured from the start of one full sweep to the start of the next, and neither configurable. This noticeably reduces how often the heat pump's own control gets polled for the slower-changing settings data, without making sensor data any less fresh
61
64
  * (zloe) a group's sweep (round-robin or multi-block alike) is never interrupted by the other group becoming due partway through - previously only a multi-block collection had this guarantee; the classic one-block-at-a-time round-robin now gets it too
@@ -265,13 +268,20 @@ sequenceDiagram
265
268
  ```
266
269
 
267
270
  Each completed sweep logs its own actual elapsed time plus a rolling average over the last 10
268
- sweeps, e.g. `sensor sweep done in 1400ms (avg of last 10: 1360ms)` - one line per group, logged
269
- the moment that group's sweep finishes. This replaces the earlier combined "full-coverage cycle"
270
- line: now that the two groups run on independent schedules there is no single shared interval left
271
- to time them together, so each gets its own line instead. As before, this is driven by data
271
+ sweeps at **debug** level, e.g. `sensor sweep done in 1400ms (avg of last 10: 1360ms)` - one line
272
+ per group, logged the moment that group's sweep finishes. As before, this is driven by data
272
273
  actually being *received* (`recordBlockRead()`), not merely requested - a block that was asked for
273
274
  but never got a reply (a retry, a response-watchdog reset, the periodic resync) does not count.
274
275
 
276
+ Since a sensor sweep normally completes roughly every 10s, debug is the right level for that
277
+ per-sweep detail - logging it at info would flood the log in normal operation. Instead, once every
278
+ 10 minutes (fixed), one small **info**-level summary line reports both groups' activity over that
279
+ window, e.g. `last 10min - sensor: 58 sweep(s), 1360ms avg (min 1210ms, max 1890ms); settings: 9
280
+ sweep(s), 640ms avg (min 600ms, max 720ms)` - or `0 sweeps` for a group that hasn't completed one
281
+ in that window. This is the only sweep-related log at info level now; it replaces the earlier
282
+ combined "full-coverage cycle" line, which no longer made sense once the two groups started running
283
+ on independent schedules.
284
+
275
285
  #### Multi-block requests, per group
276
286
 
277
287
  The serial protocol allows requesting several data blocks in one `0171` message, combined in a
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "idm-multitalent_002",
4
- "version": "2.2.0",
4
+ "version": "2.2.1",
5
5
  "news": {
6
+ "2.2.1": {
7
+ "en": "the per-sweep log line introduced in 2.2.0 (\"sensor sweep done in ...\") is now logged at debug level instead of info - a sensor sweep normally completes roughly every 10s, which was too chatty for info level. In its place, a small info-level summary is now logged once every 10 minutes, with a statistic (sweep count, min/avg/max duration) per group covering that window",
8
+ "de": "die in 2.2.0 eingeführte Log-Zeile pro Durchlauf (\"sensor sweep done in ...\") wird jetzt auf Debug-Level statt Info protokolliert - ein Sensor-Durchlauf ist normalerweise etwa alle 10s abgeschlossen, was auf Info-Level zu viel Rauschen erzeugte. Stattdessen wird jetzt alle 10 Minuten eine kleine Info-Level-Zusammenfassung protokolliert, mit einer Statistik (Anzahl Durchläufe, min/durchschnittliche/max Dauer) pro Gruppe für dieses Zeitfenster",
9
+ "ru": "строка журнала для каждого прохода, добавленная в 2.2.0 (\"sensor sweep done in ...\"), теперь записывается на уровне debug вместо info - проход сенсоров обычно завершается примерно каждые 10с, что было слишком часто для уровня info. Вместо этого теперь раз в 10 минут записывается небольшая сводка на уровне info со статистикой (количество проходов, мин/среднее/макс длительность) по каждой группе за этот период",
10
+ "pt": "a linha de log por varredura introduzida na 2.2.0 (\"sensor sweep done in ...\") agora é registrada em nível debug em vez de info - uma varredura de sensores normalmente é concluída a cada 10s, o que era muito frequente para o nível info. Em seu lugar, agora é registrado um pequeno resumo em nível info a cada 10 minutos, com uma estatística (contagem de varreduras, duração mín/média/máx) por grupo naquele período",
11
+ "nl": "de per-ronde logregel die in 2.2.0 werd geïntroduceerd (\"sensor sweep done in ...\") wordt nu op debug-niveau gelogd in plaats van info - een sensorronde is normaal ongeveer elke 10s voltooid, wat te veel ruis gaf op info-niveau. In plaats daarvan wordt nu elke 10 minuten een kleine samenvatting op info-niveau gelogd, met een statistiek (aantal rondes, min/gemiddelde/max duur) per groep over die periode",
12
+ "fr": "la ligne de journal par tour introduite dans la 2.2.0 (\"sensor sweep done in ...\") est désormais enregistrée au niveau debug au lieu de info - un tour des capteurs se termine normalement environ toutes les 10s, ce qui était trop fréquent pour le niveau info. À la place, un petit résumé de niveau info est maintenant enregistré toutes les 10 minutes, avec une statistique (nombre de tours, durée min/moyenne/max) par groupe sur cette période",
13
+ "it": "la riga di log per giro introdotta nella 2.2.0 (\"sensor sweep done in ...\") viene ora registrata a livello debug invece che info - un giro dei sensori normalmente si completa circa ogni 10s, il che era troppo frequente per il livello info. Al suo posto, ora viene registrato un piccolo riepilogo a livello info ogni 10 minuti, con una statistica (numero di giri, durata min/media/max) per gruppo relativa a quel periodo",
14
+ "es": "la línea de registro por pasada introducida en la 2.2.0 (\"sensor sweep done in ...\") ahora se registra en nivel debug en lugar de info - una pasada de sensores normalmente se completa cada 10s aproximadamente, lo cual era demasiado frecuente para el nivel info. En su lugar, ahora se registra un pequeño resumen en nivel info cada 10 minutos, con una estadística (número de pasadas, duración mín/media/máx) por grupo durante ese periodo",
15
+ "pl": "linia dziennika dla każdego przebiegu wprowadzona w 2.2.0 (\"sensor sweep done in ...\") jest teraz zapisywana na poziomie debug zamiast info - przebieg czujników zwykle kończy się co około 10s, co było zbyt częste dla poziomu info. Zamiast tego co 10 minut zapisywane jest teraz małe podsumowanie na poziomie info, ze statystyką (liczba przebiegów, min/średni/maks czas trwania) dla każdej grupy w tym okresie",
16
+ "zh-cn": "2.2.0 中引入的每轮采集日志行(\"sensor sweep done in ...\")现在改为以 debug 级别记录,而不是 info 级别——传感器一轮采集通常约每10秒完成一次,这在 info 级别下过于频繁。取而代之的是,现在每10分钟记录一条小型 info 级别摘要,包含该分组在此期间的统计信息(采集次数、最小/平均/最大耗时)",
17
+ "uk": "рядок журналу для кожного проходу, доданий у 2.2.0 (\"sensor sweep done in ...\"), тепер записується на рівні debug замість info - прохід датчиків зазвичай завершується приблизно кожні 10с, що було занадто часто для рівня info. Натомість тепер раз на 10 хвилин записується невеликий підсумок на рівні info зі статистикою (кількість проходів, мін/середня/макс тривалість) для кожної групи за цей період"
18
+ },
6
19
  "2.2.0": {
7
20
  "en": "sensor and settings are no longer polled in lockstep: sensor is read as fast as the protocol allows (throttled to at most once every 10s), while settings is only read once every 60s, plus once right away after any value is written to the heat pump - reducing how often the heat pump's own control gets polled for the slower-changing settings data, without making sensor data any less fresh. A group's sweep - round-robin or multi-block alike - is never interrupted by the other group becoming due partway through. The combined \"full-coverage cycle\" log line is replaced by one line per group, logged the moment that group's own sweep completes, showing its actual elapsed time and a rolling average over its last 10 sweeps",
8
21
  "de": "Sensor- und Settings-Abfragen laufen nicht mehr im Gleichschritt: Sensor wird so schnell wie das Protokoll es erlaubt gelesen (gedrosselt auf höchstens einmal alle 10s), während Settings nur alle 60s gelesen wird, plus einmal sofort nachdem ein Wert zur Wärmepumpe geschrieben wurde - das reduziert, wie oft die Steuerung der Wärmepumpe für die sich selten ändernden Settings-Daten abgefragt wird, ohne dass Sensordaten dadurch weniger aktuell werden. Ein Durchlauf einer Gruppe - egal ob round-robin oder multi-block - wird nie unterbrochen, nur weil die andere Gruppe währenddessen fällig wird. Die kombinierte \"full-coverage cycle\"-Logzeile wird durch eine Zeile pro Gruppe ersetzt, die genau dann protokolliert wird, wenn der Durchlauf dieser Gruppe abgeschlossen ist, und die tatsächliche verstrichene Zeit sowie einen gleitenden Durchschnitt der letzten 10 Durchläufe zeigt",
@@ -80,19 +93,6 @@
80
93
  "pl": "dla S_H726100 wartości ustawień są teraz zbierane w jednym połączonym żądaniu zamiast jednego bloku na cykl odpytywania - pełne odświeżenie ustawień trwa teraz sekundy zamiast około minuty. Pozostałe obsługiwane firmware nie są tym dotknięte",
81
94
  "zh-cn": "对于 S_H726100,设置值现在通过一次合并请求一次性收集,而不再是每个轮询周期只读取一个区块——完整刷新一次设置现在只需几秒钟,而不是大约一分钟。其他所有受支持的固件均不受影响",
82
95
  "uk": "для S_H726100 значення налаштувань тепер збираються одним об'єднаним запитом замість одного блоку за цикл опитування - повне оновлення налаштувань тепер триває секунди замість приблизно хвилини. Усі інші підтримувані прошивки це не стосується"
83
- },
84
- "1.3.10": {
85
- "en": "fix: a retry on an already finely-tuned data block no longer jumps the delay up by the full coarse step - it now corrects by the same small step the tuning had converged to, and only escalates back up if retries keep actually recurring",
86
- "de": "fix: ein Retry auf einen bereits fein eingestellten Datenblock springt das Delay nicht mehr um den vollen groben Schritt hoch - er korrigiert jetzt um denselben kleinen Schritt, auf den die Feinabstimmung bereits konvergiert war, und eskaliert nur weiter, wenn Retries tatsächlich wiederholt auftreten",
87
- "ru": "исправление: повтор для уже точно настроенного блока данных больше не увеличивает задержку сразу на весь грубый шаг - теперь коррекция происходит на тот же малый шаг, до которого сошлась точная настройка, и шаг увеличивается снова только при повторяющихся сбоях",
88
- "pt": "correção: uma nova tentativa em um bloco de dados já ajustado finamente não aumenta mais o atraso pelo passo grosseiro completo - agora corrige pelo mesmo passo pequeno ao qual o ajuste havia convergido, e só aumenta novamente se as tentativas realmente se repetirem",
89
- "nl": "fix: een retry op een al fijn afgesteld datablok verhoogt de vertraging niet meer met de volledige grove stap - er wordt nu gecorrigeerd met dezelfde kleine stap waarnaar de afstelling al was geconvergeerd, en de stap wordt alleen weer groter als retries daadwerkelijk blijven terugkomen",
90
- "fr": "correctif : une nouvelle tentative sur un bloc de données déjà finement ajusté n'augmente plus le délai du pas grossier complet - elle corrige désormais du même petit pas vers lequel l'ajustement avait convergé, et n'escalade à nouveau que si les tentatives se répètent réellement",
91
- "it": "correzione: un retry su un blocco dati già finemente calibrato non aumenta più il ritardo dell'intero passo grossolano - ora corregge con lo stesso piccolo passo a cui la calibrazione era convergente, e aumenta di nuovo solo se i retry continuano davvero a ripetersi",
92
- "es": "corrección: un reintento en un bloque de datos ya ajustado finamente ya no aumenta el retardo en el paso grueso completo - ahora corrige con el mismo paso pequeño al que había convergido el ajuste, y solo vuelve a escalar si los reintentos realmente siguen repitiéndose",
93
- "pl": "poprawka: ponowna próba dla już precyzyjnie dostrojonego bloku danych nie zwiększa już opóźnienia o pełny, zgrubny krok - teraz koryguje o ten sam mały krok, do którego zbiegło dostrajanie, i eskaluje ponownie tylko wtedy, gdy próby faktycznie się powtarzają",
94
- "zh-cn": "修复:对已经精细调优的数据块进行重试时,不再将延迟直接跳增整个粗略步长——现在会按照调优已收敛到的相同小步长进行修正,只有在重试确实反复发生时才会重新升级步长",
95
- "uk": "виправлення: повторна спроба для вже точно налаштованого блока даних більше не збільшує затримку одразу на весь грубий крок - тепер вона коригується тим самим малим кроком, до якого зійшло налаштування, і знову зростає лише якщо повтори справді повторюються"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -185,6 +185,18 @@ class IdmSession {
185
185
  // finishSweep(), the only place this is read from and pushed to.
186
186
  this.recentSweepDurationsMs = { sensor: [], settings: [] };
187
187
  this.sweepAverageWindow = 10;
188
+ // Per-sweep completions are only logged at debug level (see finishSweep()) - in normal
189
+ // operation a sensor sweep finishes roughly every sensorMinIntervalMs, which is far too
190
+ // chatty for info level. Instead reportSweepStats() (started by start(), see
191
+ // statsReportTimer below) logs one small info-level summary every statsReportIntervalMs,
192
+ // built from the counters below - reset to zero each time it reports.
193
+ this.statsReportIntervalMs = 10 * 60 * 1000; // fixed, not configurable - see class comment
194
+ this.statsReportTimer = null;
195
+ /** @type {{sensor: {count: number, totalMs: number, minMs: number | null, maxMs: number | null}, settings: {count: number, totalMs: number, minMs: number | null, maxMs: number | null}}} */
196
+ this.sweepStatsSinceReport = {
197
+ sensor: { count: 0, totalMs: 0, minMs: null, maxMs: null },
198
+ settings: { count: 0, totalMs: 0, minMs: null, maxMs: null },
199
+ };
188
200
  // Set by enqueueWrite(); consumed (and cleared) the next time chooseNextGroup() is asked -
189
201
  // forces a settings turn right after the WHOLE write queue has fully drained (request_data()
190
202
  // is only ever reached once nothing is left to send - see write_data_to_heatpump()'s
@@ -394,6 +406,8 @@ class IdmSession {
394
406
  /** Starts (or restarts) the connection process. Call once the adapter is ready. */
395
407
  start() {
396
408
  this.connectAndRead();
409
+ if (this.statsReportTimer) this.hooks.clearInterval(this.statsReportTimer);
410
+ this.statsReportTimer = this.hooks.setInterval(this.reportSweepStats.bind(this), this.statsReportIntervalMs);
397
411
  }
398
412
 
399
413
  connectAndRead() {
@@ -724,13 +738,14 @@ class IdmSession {
724
738
  /**
725
739
  * Called once `group`'s sweep has actually covered every one of its blocks (see
726
740
  * recordBlockRead(), the only caller) - logs the sweep's actual elapsed duration and a rolling
727
- * average over the last sweepAverageWindow sweeps (replacing the old combined "full-coverage
728
- * cycle" log, which no longer makes sense now that sensor and settings run on independent
729
- * cadences - see the constructor's turn-scheduling comment), and clears blocksReadThisSweep so
730
- * the NEXT sweep starts counting from zero. Does NOT touch sweepStartedAt[group] itself -
731
- * chooseNextGroup() restamps it fresh the moment this group's next sweep actually begins
732
- * (beginGroupCollection() for a multi-block group, or the first block request of a new
733
- * round-robin lap), since only then is "now" meaningful as that sweep's start.
741
+ * average over the last sweepAverageWindow sweeps at debug level (a sensor sweep normally
742
+ * completes roughly every sensorMinIntervalMs, far too often for info - the periodic info-level
743
+ * summary is reportSweepStats() instead), feeds sweepStatsSinceReport for that summary, and
744
+ * clears blocksReadThisSweep so the NEXT sweep starts counting from zero. Does NOT touch
745
+ * sweepStartedAt[group] itself - chooseNextGroup() restamps it fresh the moment this group's
746
+ * next sweep actually begins (beginGroupCollection() for a multi-block group, or the first
747
+ * block request of a new round-robin lap), since only then is "now" meaningful as that sweep's
748
+ * start.
734
749
  * @param {'sensor'|'settings'} group
735
750
  */
736
751
  finishSweep(group) {
@@ -745,8 +760,34 @@ class IdmSession {
745
760
  const avgMs = Math.round(recent.reduce((sum, ms) => sum + ms, 0) / recent.length);
746
761
  // Short on purpose (see request_data_block()'s block-07 line for the same reasoning) -
747
762
  // this only ever logs once per completed sweep, never more often than that group's own
748
- // sweep actually takes.
749
- this.log.info(group + ' sweep done in ' + elapsedMs + 'ms (avg of last ' + recent.length + ': ' + avgMs + 'ms)');
763
+ // sweep actually takes. debug level only (see reportSweepStats() for the info-level
764
+ // summary) - too frequent for info in normal operation.
765
+ this.log.debug(group + ' sweep done in ' + elapsedMs + 'ms (avg of last ' + recent.length + ': ' + avgMs + 'ms)');
766
+
767
+ const stats = this.sweepStatsSinceReport[group];
768
+ stats.count++;
769
+ stats.totalMs += elapsedMs;
770
+ stats.minMs = stats.minMs == null ? elapsedMs : Math.min(stats.minMs, elapsedMs);
771
+ stats.maxMs = stats.maxMs == null ? elapsedMs : Math.max(stats.maxMs, elapsedMs);
772
+ }
773
+
774
+ /**
775
+ * Logs one small info-level summary of both groups' sweep activity over the last
776
+ * statsReportIntervalMs (fixed at 10 minutes - see the constructor), then resets
777
+ * sweepStatsSinceReport so the next summary only covers the following window. Started as a
778
+ * fixed interval by start() and cleared by stop(); this is the only info-level sweep-related
779
+ * log in normal operation now that finishSweep() logs at debug level.
780
+ */
781
+ reportSweepStats() {
782
+ const describe = (group) => {
783
+ const stats = this.sweepStatsSinceReport[group];
784
+ if (stats.count === 0) return group + ': 0 sweeps';
785
+ const avgMs = Math.round(stats.totalMs / stats.count);
786
+ return group + ': ' + stats.count + ' sweep(s), ' + avgMs + 'ms avg (min ' + stats.minMs + 'ms, max ' + stats.maxMs + 'ms)';
787
+ };
788
+ this.log.info('last ' + Math.round(this.statsReportIntervalMs / 60000) + 'min - ' + describe('sensor') + '; ' + describe('settings'));
789
+ this.sweepStatsSinceReport.sensor = { count: 0, totalMs: 0, minMs: null, maxMs: null };
790
+ this.sweepStatsSinceReport.settings = { count: 0, totalMs: 0, minMs: null, maxMs: null };
750
791
  }
751
792
 
752
793
  /**
@@ -1355,6 +1396,7 @@ class IdmSession {
1355
1396
  if (this.sendSetValueMessageTimeout1) { this.hooks.clearTimeout(this.sendSetValueMessageTimeout1); this.sendSetValueMessageTimeout1 = null; }
1356
1397
  if (this.sendSetValueMessageTimeout2) { this.hooks.clearTimeout(this.sendSetValueMessageTimeout2); this.sendSetValueMessageTimeout2 = null; }
1357
1398
  if (this.pollScheduleTimer) { this.hooks.clearTimeout(this.pollScheduleTimer); this.pollScheduleTimer = null; }
1399
+ if (this.statsReportTimer) { this.hooks.clearInterval(this.statsReportTimer); this.statsReportTimer = null; }
1358
1400
  if (this.client) this.client.destroy();
1359
1401
  }
1360
1402
  }
@@ -233,6 +233,16 @@ describe('idm-session (connection + request/response state machine)', () => {
233
233
  expect(clock.countTimers()).to.equal(0);
234
234
  });
235
235
 
236
+ it('also clears the periodic statsReportTimer started by start() on stop()', () => {
237
+ connect();
238
+ expect(session.statsReportTimer, 'start() should have scheduled the periodic stats report').to.be.ok;
239
+
240
+ session.stop();
241
+
242
+ expect(session.statsReportTimer, 'statsReportTimer').to.not.be.ok;
243
+ expect(clock.countTimers()).to.equal(0);
244
+ });
245
+
236
246
  describe('sweep tracking reset on disconnect (see setConnected())', () => {
237
247
  it('resets both groups\' sweepStartedAt and cancels a pending idle recheck when the connection drops', () => {
238
248
  connect();
@@ -268,7 +278,7 @@ describe('idm-session (connection + request/response state machine)', () => {
268
278
  // idm701100 has 4 sensor blocks (07,09,0A,0B) - sweepIncomplete() keeps every turn on
269
279
  // sensor until all 4 have been read at least once (see the constructor's "sweep"
270
280
  // comment), regardless of settings' own due-ness in the meantime.
271
- const sensorSweepLogs = () => log.info.getCalls().filter(c => /^sensor sweep done in /.test(c.args[0]));
281
+ const sensorSweepLogs = () => log.debug.getCalls().filter(c => /^sensor sweep done in /.test(c.args[0]));
272
282
 
273
283
  socket.emit('data', versionResponse('idm701100')); // sensor block 07 in flight (1st of 4)
274
284
 
@@ -309,7 +319,7 @@ describe('idm-session (connection + request/response state machine)', () => {
309
319
  it('logs the settings sweep the same way, and its rolling average reflects multiple completed sweeps', () => {
310
320
  session.version = 'idm701100'; // 5 settings blocks: 03,04,05,06,08
311
321
  const settingsBlocks = ['03', '04', '05', '06', '08'];
312
- const settingsSweepLogs = () => log.info.getCalls().filter(c => /^settings sweep done in /.test(c.args[0]));
322
+ const settingsSweepLogs = () => log.debug.getCalls().filter(c => /^settings sweep done in /.test(c.args[0]));
313
323
 
314
324
  session.sweepStartedAt.settings = Date.now();
315
325
  clock.tick(1000);
@@ -328,7 +338,7 @@ describe('idm-session (connection + request/response state machine)', () => {
328
338
  it('caps the rolling average window at the last 10 completed sweeps', () => {
329
339
  session.version = 'idm701100';
330
340
  const settingsBlocks = ['03', '04', '05', '06', '08'];
331
- const settingsSweepLogs = () => log.info.getCalls().filter(c => /^settings sweep done in /.test(c.args[0]));
341
+ const settingsSweepLogs = () => log.debug.getCalls().filter(c => /^settings sweep done in /.test(c.args[0]));
332
342
  const doSweep = (ms) => {
333
343
  session.sweepStartedAt.settings = Date.now();
334
344
  clock.tick(ms);
@@ -343,6 +353,58 @@ describe('idm-session (connection + request/response state machine)', () => {
343
353
  });
344
354
  });
345
355
 
356
+ describe('periodic sweep stats summary (info level, every statsReportIntervalMs, see reportSweepStats())', () => {
357
+ const statsLogs = () => log.info.getCalls().filter(c => /^last \d+min - /.test(c.args[0]));
358
+
359
+ it('is started by start() and logs nothing until statsReportIntervalMs has actually elapsed', () => {
360
+ session.start();
361
+ expect(session.statsReportTimer, 'start() should schedule the periodic report').to.be.ok;
362
+
363
+ clock.tick(session.statsReportIntervalMs - 1);
364
+ expect(statsLogs()).to.have.lengthOf(0);
365
+
366
+ clock.tick(1);
367
+ expect(statsLogs()).to.have.lengthOf(1);
368
+ });
369
+
370
+ it('reports "0 sweeps" for a group that never completed a sweep in the window', () => {
371
+ session.start();
372
+ clock.tick(session.statsReportIntervalMs);
373
+
374
+ expect(statsLogs()[0].args[0]).to.equal('last 10min - sensor: 0 sweeps; settings: 0 sweeps');
375
+ });
376
+
377
+ it('summarizes count/min/avg/max per group and resets the counters afterwards', () => {
378
+ session.version = 'idm701100'; // 5 settings blocks: 03,04,05,06,08
379
+ const settingsBlocks = ['03', '04', '05', '06', '08'];
380
+ session.start();
381
+
382
+ session.sweepStartedAt.settings = Date.now();
383
+ clock.tick(1000);
384
+ for (const b of settingsBlocks) session.recordBlockRead('settings', b);
385
+
386
+ session.sweepStartedAt.settings = Date.now();
387
+ clock.tick(3000);
388
+ for (const b of settingsBlocks) session.recordBlockRead('settings', b);
389
+
390
+ clock.tick(session.statsReportIntervalMs - 4000);
391
+ const logs = statsLogs();
392
+ expect(logs).to.have.lengthOf(1);
393
+ expect(logs[0].args[0]).to.equal('last 10min - sensor: 0 sweeps; settings: 2 sweep(s), 2000ms avg (min 1000ms, max 3000ms)');
394
+
395
+ // Counters must have been reset - a second full window with no further sweeps reports
396
+ // "0 sweeps" again rather than carrying the previous window's numbers forward.
397
+ clock.tick(session.statsReportIntervalMs);
398
+ expect(statsLogs()[1].args[0]).to.equal('last 10min - sensor: 0 sweeps; settings: 0 sweeps');
399
+ });
400
+
401
+ it('keeps firing every statsReportIntervalMs for as long as the session runs', () => {
402
+ session.start();
403
+ clock.tick(session.statsReportIntervalMs * 3);
404
+ expect(statsLogs()).to.have.lengthOf(3);
405
+ });
406
+ });
407
+
346
408
  describe('adaptive per-data-block content delay (hill-climbs a sweet spot, not "wait forever")', () => {
347
409
  const dataBlock07 = '00000000000000000000000000000000000000000B270000000000000000';
348
410
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.idm-multitalent_002",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Read and write values of a iDM heatpump with multitalent.002 control.",
5
5
  "author": {
6
6
  "name": "zloe",