iobroker.idm-multitalent_002 2.0.0 → 2.1.2

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
@@ -30,9 +30,8 @@ Currently following versions are supported (if your version is not listed but yo
30
30
 
31
31
  You need a Ethernet to RS422 converter to connect to the multitalent control.
32
32
  **Note** that you have to connect ground/shield of your converter to the ground of the control/heatpump in order to prevent electric influences on the sensor readings.
33
- There are sensor values and settings values. During a cycle all sensor values and one part of the settings values are read. So the sensor values are read more frequently than the settings values.
34
- The changed values are transferred immediately.
35
- Note that settings of the heatpump are only read all ~5-6 cycles, so when setting values the acknowledgment might take some time.
33
+ There are sensor values and settings values; sensor values are read about twice as often as settings values, so they stay more up to date. Depending on your control's firmware, the adapter either reads a whole group (all sensor values, or all settings values) together in one request, or - for firmwares where that isn't (yet) established - one settings block at a time, round-robin, so a complete refresh of every setting can take a few polling cycles either way (see the Architecture section below for the exact schedule).
34
+ Values you change yourself are sent to the heat pump right away; the state's acknowledgment only follows once that value is actually read back from the heat pump on its next turn, so it can take a little while to show up.
36
35
 
37
36
  During bootup of the heatpump control (e.g. after a power loss) no values should be polled. This is currently **NOT** ensured by the adapter. So you **manually** need to **stop** it. If the control of the heatpump did not start due to the adapter then simply stop the adapter and power cycle the control. This should fix the problem. Afterwards you can start the adapter again. I implemented a delayed switch-on of the serial server. This also mitigates the problem.
38
37
 
@@ -57,6 +56,15 @@ Example screenshots of objects:
57
56
  ![Status](resources/ioBrokerAdapter-Status.jpg)
58
57
 
59
58
  ## Changelog
59
+ ### 2.1.2 (2026-09-10)
60
+ * (zloe) fix: 2.1.0's interleaving made a multi-block group's turn give up after exactly one `0171`/`0172` attempt and yield to the other group, even if that attempt hadn't found every block yet - on real hardware, where settings typically needs several re-asks to fully arrive, this meant each of those re-asks now had to wait through two full sensor turns and a connection resync in between, so a full settings refresh actually got close to as slow as before 2.0.0 despite sensor freshness genuinely improving. A group's turn now again collects the whole group - re-asking with a bare `0172` at an adaptive backoff, exactly like the original (2.0.0) settings-only mechanism, just extended to sensor too - before yielding to the next turn in the poll pattern (see the Architecture section)
61
+
62
+ ### 2.1.1 (2026-09-10)
63
+ * (zloe) fix: the intro's description of how sensor/settings values are polled still described the pre-2.0.0 behavior (one settings block read per cycle, a fixed "~5-6 cycles" for a full settings refresh) - now describes the actual (2.1.0) polling, without repeating the Architecture section's exact schedule
64
+
65
+ ### 2.1.0 (2026-09-10)
66
+ * (zloe) auto-learn each data block's actual wire length from real traffic instead of only relying on hand-verified values (logged, and persisted to the `info.measuredWireLengths` state so it survives a restart - though every restart still re-confirms it once, in case a heat pump setting somehow affects it) - once a firmware's SENSOR blocks (not just its settings blocks, extending 2.0.0) all have a trusted length this way, they too are requested as one multi-block batch. Sensor and settings polling is now also interleaved (2 sensor turns for every 1 settings turn, sensor first) instead of settings collection running to completion before sensor data gets another look in, so sensor freshness no longer suffers while a slow settings collection is still catching up (see the Architecture section)
67
+
60
68
  ### 2.0.0 (2026-09-10)
61
69
  * (zloe) for S_H726100 (currently the only firmware with every data block's actual wire length verified against real hardware), collect all settings data blocks in one multi-block request per settings turn instead of one block per poll cycle, with automatic re-asking/backoff for the control's typically-partial replies - a full settings refresh now takes seconds instead of roughly a minute. Every other supported firmware is completely unaffected and keeps requesting settings blocks one at a time (see the Architecture section)
62
70
 
@@ -207,17 +215,33 @@ the "adaptive per-data-block content delay" tests in `lib/idm-session.test.js`.
207
215
 
208
216
  `IdmSession` also logs how long a full-coverage cycle actually took, once the next one completes
209
217
  (there is nothing to compare the very first cycle against yet) - every sensor block *and* every
210
- settings block actually read at least once, gated by the settings side since the sensor blocks are
211
- already re-read on every single sweep (only one settings block is requested per sweep, round-robin
212
- - see `request_data()`). This is driven by data actually being *received* (`recordBlockRead()`,
213
- called from `receive_data()`'s successful-data branch), not merely requested - a block that was
214
- requested but never got a reply back (a retry, a response-watchdog reset, the periodic resync) does
215
- not count, so the log only ever fires once every block has genuinely been read. It also carries a
216
- running total of how many full-coverage cycles have completed since the adapter started (not
217
- persisted across restarts) - together with the elapsed time, that's the overall effect of all the
218
- delays above added together, so it's what actually shows whether a change to them made polling
219
- faster or slower. (An earlier, more frequent "one full poll cycle" line - logged every single
220
- sensor sweep - was dropped as too noisy; only this line remains.)
218
+ settings block actually read at least once. This is driven by data actually being *received*
219
+ (`recordBlockRead()`, called from `receive_data()`'s successful-data branch), not merely requested
220
+ - a block that was requested but never got a reply back (a retry, a response-watchdog reset, the
221
+ periodic resync) does not count, so the log only ever fires once every block has genuinely been
222
+ read. It also carries a running total of how many full-coverage cycles have completed since the
223
+ adapter started (not persisted across restarts) and a breakdown of the delay currently in use per
224
+ block, grouped by shared value (e.g. `(07,08) 650ms, (09,04,05) 2300ms`) - together with the
225
+ elapsed time, that's the overall effect of all the delays and polling behavior below added
226
+ together, so it's what actually shows whether a change to any of it made polling faster or slower.
227
+ (An earlier, more frequent "one full poll cycle" line - logged every single sensor sweep - was
228
+ dropped as too noisy; only this line remains.)
229
+
230
+ #### Poll pattern: sensor and settings
231
+
232
+ Every call to `request_data()` is one "turn", and which of the two groups (sensor or settings) a
233
+ turn is for comes from `IdmSession#pollPattern` (`['sensor', 'sensor', 'settings']`, cycled via
234
+ `pollPatternIndex`): sensor gets two turns for every one settings turn, and always goes first -
235
+ both after connecting and after every settings turn - so the freshest-changing data (sensor
236
+ readings) is never left waiting behind a settings collection, however long that takes. Within a
237
+ turn, each group independently uses either the classic one-block-at-a-time round-robin (unchanged
238
+ since the very first version) or a multi-block request for the whole group at once, whichever it
239
+ currently qualifies for - see the next two sections. Where a group is multi-block-capable, its turn
240
+ collects that WHOLE group - every one of its blocks actually found - before the next turn in the
241
+ pattern begins; it is not interrupted partway through (see the next section for why), so the
242
+ pattern is what settles how *often* each group gets a turn, not how long any one turn takes.
243
+
244
+ #### Multi-block requests, per group
221
245
 
222
246
  The serial protocol actually allows requesting several data blocks in one `0171` message, which
223
247
  come back combined in a single `01F2`/`0172` reply - but the control's replies turned out to be
@@ -228,25 +252,68 @@ ETX/checksum framing finds the boundary regardless - but fatal for parsing sever
228
252
  one reply, where the exact length of each block is the only way to find where the next one
229
253
  starts), and a single `0172` typically only returns a PARTIAL subset of the requested blocks,
230
254
  requiring the request to be repeated until everything has actually come back. Because of this, the
231
- multi-block request path is strictly opt-in per firmware: `IdmProtocol#firmwareSupportsMultiBlockRequests()`
232
- only returns true once every one of that firmware's settings blocks has an explicit, hardware-
233
- verified `wireLength` in its data block definition (see
234
- [`lib/datablocks/README.md`](lib/datablocks/README.md)) - currently only S_H726100. For every other
235
- firmware, `request_data()` keeps using the plain one-settings-block-per-cycle round-robin described
236
- above, completely unchanged.
237
-
238
- Where it is supported, `IdmSession#beginSettingsBatchCollection()` requests every settings block at
239
- once instead of just one, reusing the very same request/response state machine (the "R1" ack and
240
- the `0172` content request are exactly the same messages the single-block path already sends) -
241
- only the reply is parsed differently (`IdmProtocol#parse_multi_block_reply()`, using each block's
242
- verified `wireLength` to find its boundary) and re-asked (plain `0172` again, no need to repeat the
243
- block list) with a growing backoff (`multiBlockReaskBaseDelay`, capped at
244
- `multiBlockReaskMaxDelay`) until every requested block has actually been seen, up to
245
- `multiBlockMaxReasks` attempts before giving up on the stragglers for this cycle - the next
246
- settings turn starts a fresh batch for everything again, so nothing is permanently lost, only
247
- deferred. See the "multi-block settings batch collection" tests in `lib/idm-session.test.js`,
248
- which drive this against a small simulated control (`MultiBlockControllerSim`) modeling the
249
- partial-reply/stale-repeat/not-ready behavior actually observed on real hardware.
255
+ multi-block request path is strictly opt-in, gated independently for each of the two groups:
256
+ `IdmProtocol#firmwareSupportsMultiBlockRequestsForBlocks()` (and its `firmwareSupportsMultiBlockRequests()`
257
+ /`firmwareSupportsMultiBlockRequestsForSensors()` convenience wrappers for the settings/sensor
258
+ groups) only returns true once every one of that group's blocks has a TRUSTED wire length - either
259
+ an explicit, hand-verified `wireLength` in its data block definition (see
260
+ [`lib/datablocks/README.md`](lib/datablocks/README.md)), or one confirmed from real traffic (see
261
+ "wireLength learning" below). A firmware's two groups can be in different states (e.g. settings
262
+ qualified from its JSON definition while sensor is still being learned, or vice versa); whichever
263
+ group doesn't (yet) qualify keeps using the plain one-block-per-turn round-robin, completely
264
+ unaffected.
265
+
266
+ Where a group is multi-block-capable, `IdmSession#beginGroupCollection()` requests every block in
267
+ that group at once instead of just one - a single `0171` naming the whole group - reusing the very
268
+ same request/response state machine (the "R1" ack and the `0172` content request are exactly the
269
+ same messages the single-block path already sends) - only the reply is parsed differently
270
+ (`IdmProtocol#parse_multi_block_reply()`, using each block's verified wire length to find its
271
+ boundary). Because a single `0172` typically only returns a partial subset (see above), the
272
+ collection then keeps RE-ASKING - a bare `0172`, without repeating the block list, since the
273
+ control already knows what was requested from the initial `0171` - until every block has actually
274
+ been found, merging newly-found blocks into what it already has and only asking again for whatever
275
+ is still missing. Each re-ask waits `multiBlockReaskBaseDelay` (900ms) after the previous reply by
276
+ default; a reply that comes back with nothing NEW doubles that wait next time (capped at
277
+ `multiBlockReaskMaxDelay`, 4000ms) - real traffic showed the control sometimes needs longer to
278
+ finish preparing the rest, or occasionally repeats an identical stale partial reply if re-asked too
279
+ soon - while a reply that DOES make progress resets the wait back down to the base. If a
280
+ collection still hasn't finished after `multiBlockMaxReasks` (20) re-asks, it gives up on the
281
+ stragglers for now (falling back to idle exactly as if it had completed) and starts completely
282
+ fresh - asking for every one of that group's blocks again - next time that group's turn comes
283
+ around, so nothing is permanently lost, only deferred.
284
+
285
+ This deliberately mirrors how the very first (2.0.0) version of this feature worked, rather than
286
+ resending a fresh `0171` on every re-ask and force-yielding to the other group after a single
287
+ attempt regardless of whether the collection had finished (as a brief interim design in 2.1.0 did):
288
+ a collection's own re-asks are never interleaved with an unrelated request from the other group in
289
+ between, so there is no uncertainty here about whether the control could resume a partial reply
290
+ across such an interruption - that situation simply never arises - and settings, which on real
291
+ hardware often needs several re-asks to fully arrive, no longer has to wait through two full sensor
292
+ turns and a connection resync between every one of them. See the "multi-block collection, sensor
293
+ and settings" tests in `lib/idm-session.test.js`, which drive this against a small simulated
294
+ control (`MultiBlockControllerSim`) modeling the partial-reply/stale-repeat/not-ready behavior
295
+ actually observed on real hardware.
296
+
297
+ #### wireLength learning
298
+
299
+ A block's wire length only needs to be trusted, not necessarily hand-verified up front: every
300
+ classic single-block reply is fully checksum-framed regardless of its actual length, so
301
+ `IdmSession#learnWireLengthFrom()` passively measures it from ordinary traffic at essentially no
302
+ cost, for any block that doesn't already have a trusted length. A measurement alone isn't trusted
303
+ immediately, though - `IdmProtocol#recordMeasuredWireLength()` requires the SAME length twice in a
304
+ row (a disagreement logs a warning and restarts the count from the new value, never averages)
305
+ before treating it as confirmed, in case a firmware turns out to add a different number of
306
+ undocumented bytes on different occasions. Every outcome is logged (routine progress at `info`, a
307
+ disagreement at `warn`), and the moment a group's every block becomes trusted this way, that's
308
+ logged too and multi-block requests for it start from its very next turn. Each newly CONFIRMED
309
+ length is also persisted to the `info.measuredWireLengths` ioBroker state (`onWireLengthLearned`
310
+ hook, wired up in `main.js`) as `{"<firmware version>": {"<block id>": <byte length>}}`, and
311
+ restored on the next adapter start (`loadMeasuredWireLengths()`, before the session starts) via
312
+ `IdmProtocol#seedMeasuredWireLength()` - but only as a CANDIDATE, not as already-trusted: every
313
+ restart still re-confirms it with one fresh matching measurement before relying on it again, in
314
+ case some heat pump setting turns out to affect a block's length after all. See the "wireLength
315
+ learning" tests in `lib/idm-session.test.js` (and `idm-protocol.test.js` for the underlying
316
+ confirm/mismatch/seed mechanics).
250
317
 
251
318
  ### Overriding the data blocks without an adapter update
252
319
  The instance setting **"Custom data blocks directory"** (`native.dataBlocksDir`) can point at a directory of your own such files. Each file's `"version"` field is matched against the version string the heat pump reports after connecting - a match REPLACES that version's bundled definition entirely (it is not merged field-by-field), useful for adding min/max limits you have verified for your own installation, fixing a field, or adding a not-yet-supported control version, all without reinstalling or upgrading the adapter. Versions with no matching (and valid) custom file keep using their bundled definition. A file that fails validation, or two files claiming the same version, are both rejected with a warning in the adapter's log - the bundled definition (if any) is kept in that case.
package/io-package.json CHANGED
@@ -1,8 +1,47 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "idm-multitalent_002",
4
- "version": "2.0.0",
4
+ "version": "2.1.2",
5
5
  "news": {
6
+ "2.1.2": {
7
+ "en": "fix: a multi-block group's turn (sensor or settings) used to give up after exactly one request/reply attempt and hand off to the other group even if it hadn't found every block yet - on real hardware, where settings often needs several re-asks to fully arrive, this made settings collection nearly as slow as before 2.0.0 despite sensor freshness having improved. A turn now collects the whole group - re-asking with an adaptive backoff, exactly like the original 2.0.0 mechanism - before handing off, extended to sensor too",
8
+ "de": "Fehlerbehebung: der Durchlauf einer multi-block-fähigen Gruppe (Sensor oder Settings) brach bisher nach genau einem Anfrage-/Antwortversuch ab und übergab an die andere Gruppe, selbst wenn noch nicht alle Blöcke gefunden waren - auf echter Hardware, wo Settings oft mehrere Nachfragen benötigen, bis alles vollständig da ist, machte das die Settings-Sammlung fast so langsam wie vor 2.0.0, obwohl sich die Aktualität der Sensordaten verbessert hatte. Ein Durchlauf sammelt jetzt die ganze Gruppe - mit adaptivem Backoff nachfragend, genau wie beim ursprünglichen 2.0.0-Mechanismus, jetzt auch für Sensor - bevor übergeben wird",
9
+ "ru": "исправление: при опросе multi-block-группы (сенсоры или настройки) раньше выполнялась ровно одна попытка запроса/ответа, после чего управление передавалось другой группе, даже если ещё не все блоки были найдены - на реальном оборудовании, где для настроек часто требуется несколько повторных запросов, это делало сбор настроек почти таким же медленным, как до версии 2.0.0, несмотря на улучшение актуальности сенсорных данных. Теперь за один заход собирается вся группа целиком - с повторными запросами по адаптивной задержке, точно как в исходном механизме версии 2.0.0, теперь и для сенсоров - прежде чем передать управление дальше",
10
+ "pt": "correção: o turno de um grupo multi-block (sensores ou configurações) costumava desistir após exatamente uma tentativa de requisição/resposta e passar para o outro grupo mesmo sem ter encontrado todos os blocos - em hardware real, onde as configurações costumam precisar de várias novas tentativas até chegar por completo, isso deixava a coleta de configurações quase tão lenta quanto antes da 2.0.0, apesar da atualização dos sensores ter melhorado. Um turno agora coleta o grupo inteiro - tentando novamente com espera adaptativa, exatamente como o mecanismo original da 2.0.0, agora também para os sensores - antes de passar adiante",
11
+ "nl": "fix: de beurt van een multi-block-groep (sensor of instellingen) gaf voorheen op na precies één verzoek-/antwoordpoging en gaf de beurt door aan de andere groep, zelfs als nog niet alle blokken gevonden waren - op echte hardware, waar instellingen vaak meerdere nieuwe pogingen nodig hebben om volledig binnen te komen, maakte dit het verzamelen van instellingen bijna net zo traag als vóór 2.0.0, ondanks dat de actualiteit van sensordata verbeterd was. Een beurt verzamelt nu de hele groep - met adaptieve wachttijd opnieuw vragend, precies zoals het oorspronkelijke 2.0.0-mechanisme, nu ook voor sensor - voordat de beurt wordt doorgegeven",
12
+ "fr": "correctif : le tour d'un groupe multi-block (capteurs ou réglages) abandonnait auparavant après exactement une tentative de requête/réponse et cédait la main à l'autre groupe même si tous les blocs n'avaient pas encore été trouvés - sur du matériel réel, où les réglages nécessitent souvent plusieurs nouvelles demandes pour arriver complètement, cela rendait la collecte des réglages presque aussi lente qu'avant la 2.0.0, malgré l'amélioration de la fraîcheur des données capteurs. Un tour collecte désormais le groupe entier - en relançant avec un délai adaptatif, exactement comme le mécanisme original de la 2.0.0, désormais aussi pour les capteurs - avant de céder la main",
13
+ "it": "correzione: il turno di un gruppo multi-block (sensori o impostazioni) prima si arrendeva dopo esattamente un tentativo di richiesta/risposta e cedeva il turno all'altro gruppo anche se non erano ancora stati trovati tutti i blocchi - su hardware reale, dove le impostazioni spesso richiedono più richieste ripetute per arrivare completamente, questo rendeva la raccolta delle impostazioni quasi lenta quanto prima della 2.0.0, nonostante il miglioramento dell'aggiornamento dei sensori. Un turno ora raccoglie l'intero gruppo - richiedendo di nuovo con un ritardo adattivo, esattamente come il meccanismo originale della 2.0.0, ora anche per i sensori - prima di cedere il turno",
14
+ "es": "corrección: el turno de un grupo multi-block (sensores o configuración) antes se rendía tras exactamente un intento de solicitud/respuesta y cedía el turno al otro grupo aunque no se hubieran encontrado todos los bloques todavía - en hardware real, donde la configuración a menudo necesita varias nuevas solicitudes para llegar por completo, esto hacía que la recopilación de configuración fuera casi tan lenta como antes de la 2.0.0, a pesar de que la actualidad de los sensores había mejorado. Un turno ahora recopila todo el grupo - reintentando con una espera adaptativa, exactamente como el mecanismo original de la 2.0.0, ahora también para los sensores - antes de ceder el turno",
15
+ "pl": "poprawka: tura grupy obsługującej multi-block (czujniki lub ustawienia) wcześniej poddawała się po dokładnie jednej próbie żądanie/odpowiedź i oddawała turę drugiej grupie, nawet jeśli nie znaleziono jeszcze wszystkich bloków - na rzeczywistym sprzęcie, gdzie ustawienia często wymagają kilku ponownych zapytań, zanim dotrą w całości, powodowało to, że zbieranie ustawień było niemal tak wolne jak przed wersją 2.0.0, mimo poprawy aktualności danych z czujników. Tura teraz zbiera całą grupę - ponawiając zapytania z adaptacyjnym opóźnieniem, dokładnie jak w pierwotnym mechanizmie z wersji 2.0.0, teraz także dla czujników - zanim odda turę",
16
+ "zh-cn": "修复:此前,多区块分组(传感器或设置)的轮询回合在恰好一次请求/应答尝试后就会放弃,并将回合让给另一分组,即使还没有找到该分组的全部区块——在真实硬件上,设置分组通常需要多次重新请求才能完整到达,这使得设置采集几乎和 2.0.0 之前一样慢,尽管传感器数据的新鲜度已经有所改善。现在,一个回合会先采集完整个分组——以自适应退避方式重新请求,与最初 2.0.0 的机制完全一致,现在传感器分组也采用同样的方式——然后才让出回合",
17
+ "uk": "виправлення: хід multi-block-групи (сенсори або налаштування) раніше завершувався вже після рівно однієї спроби запит/відповідь і передавав чергу іншій групі, навіть якщо ще не всі блоки було знайдено - на реальному обладнанні, де налаштування часто потребують кількох повторних запитів, щоб надійти повністю, це робило збір налаштувань майже таким же повільним, як до версії 2.0.0, попри покращення актуальності даних датчиків. Тепер хід збирає всю групу цілком - повторюючи запити з адаптивною затримкою, точно як у первісному механізмі версії 2.0.0, тепер також для датчиків - перш ніж передати чергу"
18
+ },
19
+ "2.1.1": {
20
+ "en": "fix: the README's intro still described the old (pre-2.0.0) polling behavior - a fixed \"~5-6 cycles\" for a full settings refresh, one settings block per cycle - now describes the actual, faster 2.1.0 polling instead",
21
+ "de": "Fehlerbehebung: die Einleitung im README beschrieb noch das alte (Vor-2.0.0) Abfrageverhalten - feste \"~5-6 Zyklen\" für eine vollständige Settings-Aktualisierung, ein Settings-Block pro Zyklus - jetzt wird stattdessen das tatsächliche, schnellere 2.1.0-Abfrageverhalten beschrieben",
22
+ "ru": "исправление: во введении README всё ещё описывалось старое (до 2.0.0) поведение опроса - фиксированные \"~5-6 циклов\" для полного обновления настроек, один блок настроек за цикл - теперь описано фактическое, более быстрое поведение опроса версии 2.1.0",
23
+ "pt": "correção: a introdução do README ainda descrevia o comportamento antigo (pré-2.0.0) de sondagem - \"~5-6 ciclos\" fixos para uma atualização completa das configurações, um bloco de configuração por ciclo - agora descreve a sondagem real e mais rápida da versão 2.1.0",
24
+ "nl": "fix: de inleiding van de README beschreef nog het oude (pre-2.0.0) pollgedrag - een vaste \"~5-6 cycli\" voor een volledige update van de instellingen, één instellingenblok per cyclus - beschrijft nu het daadwerkelijke, snellere pollgedrag van 2.1.0",
25
+ "fr": "correctif : l'introduction du README décrivait encore l'ancien comportement d'interrogation (avant 2.0.0) - un nombre fixe \"~5-6 cycles\" pour une actualisation complète des réglages, un bloc de réglages par cycle - elle décrit désormais le comportement réel, plus rapide, de la version 2.1.0",
26
+ "it": "correzione: l'introduzione del README descriveva ancora il vecchio comportamento di polling (precedente alla 2.0.0) - un fisso \"~5-6 cicli\" per un aggiornamento completo delle impostazioni, un blocco di impostazioni per ciclo - ora descrive il comportamento di polling reale e più veloce della 2.1.0",
27
+ "es": "corrección: la introducción del README seguía describiendo el antiguo comportamiento de sondeo (anterior a 2.0.0) - un fijo \"~5-6 ciclos\" para una actualización completa de la configuración, un bloque de configuración por ciclo - ahora describe el comportamiento de sondeo real y más rápido de la versión 2.1.0",
28
+ "pl": "poprawka: wstęp w README nadal opisywał stare (sprzed 2.0.0) zachowanie odpytywania - stałe \"~5-6 cykli\" dla pełnego odświeżenia ustawień, jeden blok ustawień na cykl - teraz opisuje rzeczywiste, szybsze odpytywanie w wersji 2.1.0",
29
+ "zh-cn": "修复:README 简介部分仍在描述旧版(2.0.0 之前)的轮询行为——固定的\"约 5-6 个周期\"才能完整刷新一次设置、每个周期只读取一个设置区块——现在改为描述 2.1.0 实际的、更快的轮询行为",
30
+ "uk": "виправлення: у вступі README досі описувалася стара (до 2.0.0) поведінка опитування - фіксовані \"~5-6 циклів\" для повного оновлення налаштувань, один блок налаштувань за цикл - тепер описано фактичну, швидшу поведінку опитування версії 2.1.0"
31
+ },
32
+ "2.1.0": {
33
+ "en": "sensor data blocks can now also be collected in one combined request per firmware (not just settings), and the adapter auto-learns each block's wire length from real traffic instead of requiring it to be hand-verified first. Sensor and settings polling is now interleaved so sensor data stays fresh while a settings collection is still in progress",
34
+ "de": "Sensor-Datenblöcke können jetzt ebenfalls in einer einzigen kombinierten Anfrage pro Firmware gesammelt werden (nicht mehr nur Settings), und der Adapter lernt die Länge jedes Blocks selbstständig aus dem realen Datenverkehr, statt sie händisch verifiziert vorgeben zu müssen. Sensor- und Settings-Abfragen werden jetzt verschränkt durchgeführt, sodass Sensordaten aktuell bleiben, während eine Settings-Sammlung noch läuft",
35
+ "ru": "блоки сенсорных данных теперь также могут собираться одним объединённым запросом для каждой прошивки (не только настройки), и адаптер автоматически определяет длину каждого блока по реальному трафику, вместо того чтобы требовать её ручной проверки заранее. Опрос сенсоров и настроек теперь чередуется, поэтому сенсорные данные остаются актуальными, пока сбор настроек ещё продолжается",
36
+ "pt": "os blocos de dados de sensores agora também podem ser coletados em uma única requisição combinada por firmware (não apenas as configurações), e o adaptador aprende automaticamente o comprimento de cada bloco a partir do tráfego real, em vez de exigir verificação manual prévia. A sondagem de sensores e configurações agora é intercalada, para que os dados dos sensores permaneçam atualizados enquanto uma coleta de configurações ainda está em andamento",
37
+ "nl": "sensor-datablokken kunnen nu ook in één gecombineerd verzoek per firmware worden verzameld (niet alleen instellingen), en de adapter leert de lengte van elk blok automatisch uit het echte verkeer, in plaats van dat deze vooraf handmatig geverifieerd moet zijn. Sensor- en instellingenpolling worden nu afgewisseld, zodat sensordata actueel blijft terwijl een instellingenverzameling nog bezig is",
38
+ "fr": "les blocs de données capteurs peuvent désormais eux aussi être collectés en une seule requête combinée par firmware (pas seulement les réglages), et l'adaptateur apprend automatiquement la longueur de chaque bloc à partir du trafic réel, au lieu d'exiger une vérification manuelle préalable. L'interrogation des capteurs et des réglages est désormais entrelacée, afin que les données des capteurs restent à jour pendant qu'une collecte des réglages est encore en cours",
39
+ "it": "anche i blocchi di dati dei sensori possono ora essere raccolti in un'unica richiesta combinata per firmware (non solo le impostazioni), e l'adattatore apprende automaticamente la lunghezza di ciascun blocco dal traffico reale, invece di richiederne la verifica manuale preventiva. Il polling di sensori e impostazioni è ora intercalato, in modo che i dati dei sensori restino aggiornati mentre una raccolta delle impostazioni è ancora in corso",
40
+ "es": "los bloques de datos de sensores ahora también pueden recopilarse en una única solicitud combinada por firmware (no solo la configuración), y el adaptador aprende automáticamente la longitud de cada bloque a partir del tráfico real, en lugar de requerir una verificación manual previa. El sondeo de sensores y configuración ahora se intercala, de modo que los datos de los sensores se mantienen actualizados mientras una recopilación de configuración todavía está en curso",
41
+ "pl": "bloki danych czujników mogą teraz być również zbierane w jednym połączonym żądaniu dla danej wersji firmware (nie tylko ustawienia), a adapter automatycznie uczy się długości każdego bloku na podstawie rzeczywistego ruchu, zamiast wymagać wcześniejszej ręcznej weryfikacji. Odpytywanie czujników i ustawień jest teraz przeplatane, dzięki czemu dane z czujników pozostają aktualne, gdy zbieranie ustawień wciąż trwa",
42
+ "zh-cn": "传感器数据区块现在也可以按固件在一次合并请求中收集(不再仅限于设置),并且适配器会根据实际通信流量自动学习每个区块的长度,而不再需要事先手动验证。传感器和设置的轮询现在是交错进行的,因此在设置仍在收集时,传感器数据也能保持最新",
43
+ "uk": "блоки даних датчиків тепер також можуть збиратися одним об'єднаним запитом для кожної прошивки (не лише налаштування), і адаптер автоматично визначає довжину кожного блоку з реального трафіку, замість того щоб вимагати попередньої ручної перевірки. Опитування датчиків і налаштувань тепер чергується, тому дані датчиків залишаються актуальними, поки збір налаштувань ще триває"
44
+ },
6
45
  "2.0.0": {
7
46
  "en": "for S_H726100, all settings values are now collected in one combined request instead of one block per poll cycle - a full settings refresh now takes seconds instead of roughly a minute. Every other supported firmware is unaffected",
8
47
  "de": "für S_H726100 werden Settings-Werte jetzt in einer einzigen kombinierten Anfrage statt einem Block pro Abfragezyklus gesammelt - eine vollständige Settings-Aktualisierung dauert jetzt Sekunden statt rund eine Minute. Alle anderen unterstützten Firmwares sind davon unberührt",
@@ -54,45 +93,6 @@
54
93
  "pl": "całkowicie usunięto częsty wiersz logu 'completed one full poll cycle' (uruchamiał się co ~14s) - pozostał tylko znacznie rzadszy wiersz pełnego cyklu, którego poprawność potwierdzono na rzeczywistych logach produkcyjnych",
55
94
  "zh-cn": "彻底移除了频繁出现的 'completed one full poll cycle' 日志行(约每 14 秒触发一次)- 仅保留频率低得多的完整周期日志行,其正确性已通过真实生产日志验证",
56
95
  "uk": "повністю видалено частий рядок журналу 'completed one full poll cycle' (спрацьовував що ~14с) - залишився лише значно рідший рядок повного циклу, коректність якого підтверджено на реальних лог-файлах"
57
- },
58
- "1.3.7": {
59
- "en": "fix: the full-coverage cycle log line fired too often on real hardware because it was gated by request timing, not actual data reads - now gated by actually received data",
60
- "de": "Fix: die Log-Zeile für den vollständigen Abfragezyklus feuerte auf echter Hardware zu oft, weil sie am Zeitpunkt der Anfrage statt am tatsächlichen Datenempfang hing - jetzt an tatsächlich empfangenen Daten festgemacht",
61
- "ru": "исправление: строка журнала о полном цикле опроса срабатывала слишком часто на реальном оборудовании, так как зависела от момента запроса, а не от фактического получения данных - теперь привязана к фактически полученным данным",
62
- "pt": "correção: a linha de log do ciclo completo disparava com muita frequência em hardware real porque dependia do momento da solicitação, não da leitura real dos dados - agora depende dos dados efetivamente recebidos",
63
- "nl": "fix: de logregel voor de volledige cyclus vuurde op echte hardware te vaak af omdat deze afhing van het moment van aanvragen, niet van het daadwerkelijk ontvangen van data - nu gebaseerd op daadwerkelijk ontvangen data",
64
- "fr": "correctif : la ligne de log du cycle complet se déclenchait trop souvent sur du matériel réel car elle dépendait du moment de la requête, pas de la réception réelle des données - désormais basée sur les données réellement reçues",
65
- "it": "correzione: la riga di log del ciclo completo scattava troppo spesso su hardware reale perché dipendeva dal momento della richiesta e non dall'effettiva ricezione dei dati - ora basata sui dati effettivamente ricevuti",
66
- "es": "corrección: la línea de registro del ciclo completo se disparaba con demasiada frecuencia en hardware real porque dependía del momento de la solicitud, no de la recepción real de datos - ahora se basa en los datos realmente recibidos",
67
- "pl": "poprawka: wiersz logu pełnego cyklu uruchamiał się zbyt często na rzeczywistym sprzęcie, ponieważ zależał od momentu żądania, a nie faktycznego odczytu danych - teraz oparty na faktycznie odebranych danych",
68
- "zh-cn": "修复:完整周期日志行在真实硬件上触发过于频繁,因为它取决于请求时机而非实际数据读取 - 现在改为基于实际接收到的数据",
69
- "uk": "виправлення: рядок журналу повного циклу спрацьовував занадто часто на реальному обладнанні, оскільки залежав від моменту запиту, а не фактичного отримання даних - тепер прив'язаний до фактично отриманих даних"
70
- },
71
- "1.3.6": {
72
- "en": "shorten the full-coverage cycle log line, and fix three type-check errors surfaced by the typescript 7 / axios 1.20 updates (no runtime behavior change)",
73
- "de": "die Log-Zeile für den vollständigen Abfragezyklus gekürzt und drei durch die typescript-7-/axios-1.20-Updates aufgedeckte Typecheck-Fehler behoben (keine Verhaltensänderung)",
74
- "ru": "сокращена строка журнала о полном цикле опроса и исправлены три ошибки проверки типов, выявленные обновлениями typescript 7/axios 1.20 (поведение не изменилось)",
75
- "pt": "linha de log do ciclo completo encurtada e corrigidos três erros de verificação de tipos revelados pelas atualizações do typescript 7/axios 1.20 (sem alteração de comportamento)",
76
- "nl": "de logregel voor de volledige cyclus ingekort en drie door de typescript 7-/axios 1.20-updates aan het licht gebrachte typecheck-fouten opgelost (geen gedragswijziging)",
77
- "fr": "ligne de log du cycle complet raccourcie et correction de trois erreurs de vérification de types révélées par les mises à jour typescript 7/axios 1.20 (aucun changement de comportement)",
78
- "it": "riga di log del ciclo completo abbreviata e corretti tre errori di controllo dei tipi emersi con gli aggiornamenti typescript 7/axios 1.20 (nessuna modifica al comportamento)",
79
- "es": "línea de registro del ciclo completo acortada y corregidos tres errores de verificación de tipos surgidos con las actualizaciones de typescript 7/axios 1.20 (sin cambios de comportamiento)",
80
- "pl": "skrócono linię logu pełnego cyklu oraz naprawiono trzy błędy sprawdzania typów ujawnione przez aktualizacje typescript 7/axios 1.20 (bez zmiany zachowania)",
81
- "zh-cn": "精简了完整周期的日志行,并修复了 typescript 7/axios 1.20 更新暴露出的三个类型检查错误(不影响运行时行为)",
82
- "uk": "скорочено рядок журналу повного циклу та виправлено три помилки перевірки типів, виявлені оновленнями typescript 7/axios 1.20 (без зміни поведінки)"
83
- },
84
- "1.3.5": {
85
- "en": "hotfix: 1.3.4's CI run failed on Node 20 because engines>=22 and testing on Node 20 don't work together (EBADENGINE) - dropped Node 20 from the CI test matrix again",
86
- "de": "Hotfix: der CI-Lauf von 1.3.4 ist auf Node 20 fehlgeschlagen, weil engines>=22 und ein Test auf Node 20 nicht zusammenpassen (EBADENGINE) - Node 20 wieder aus der CI-Testmatrix entfernt",
87
- "ru": "исправление: CI-прогон версии 1.3.4 не прошёл на Node 20, так как engines>=22 и тестирование на Node 20 несовместимы (EBADENGINE) - Node 20 снова убран из матрицы тестов CI",
88
- "pt": "correção urgente: a execução de CI da 1.3.4 falhou no Node 20 porque engines>=22 e testar no Node 20 são incompatíveis (EBADENGINE) - o Node 20 foi novamente removido da matriz de testes de CI",
89
- "nl": "hotfix: de CI-run van 1.3.4 mislukte op Node 20 omdat engines>=22 en testen op Node 20 niet samengaan (EBADENGINE) - Node 20 weer uit de CI-testmatrix verwijderd",
90
- "fr": "correctif : l'exécution CI de la 1.3.4 a échoué sur Node 20 car engines>=22 et un test sur Node 20 sont incompatibles (EBADENGINE) - Node 20 de nouveau retiré de la matrice de tests CI",
91
- "it": "hotfix: l'esecuzione CI della 1.3.4 è fallita su Node 20 perché engines>=22 e il test su Node 20 sono incompatibili (EBADENGINE) - Node 20 rimosso di nuovo dalla matrice di test CI",
92
- "es": "corrección urgente: la ejecución de CI de 1.3.4 falló en Node 20 porque engines>=22 y probar en Node 20 son incompatibles (EBADENGINE) - se eliminó de nuevo Node 20 de la matriz de pruebas de CI",
93
- "pl": "poprawka: przebieg CI wersji 1.3.4 nie powiódł się na Node 20, ponieważ engines>=22 i testowanie na Node 20 są niekompatybilne (EBADENGINE) - Node 20 ponownie usunięto z macierzy testów CI",
94
- "zh-cn": "热修复:由于 engines>=22 与在 Node 20 上测试互不兼容(EBADENGINE),1.3.4 的 CI 运行失败 - 已再次从 CI 测试矩阵中移除 Node 20",
95
- "uk": "гарячий виправлення: прогін CI версії 1.3.4 не вдався на Node 20, оскільки engines>=22 і тестування на Node 20 несумісні (EBADENGINE) - Node 20 знову прибрано з матриці тестів CI"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -26,6 +26,7 @@ definition (if any) is kept in that case rather than guessing which one to use.
26
26
  "data_blocks": [
27
27
  {
28
28
  "block_number": "03",
29
+ "wireLength": 28,
29
30
  "definition": [
30
31
  { "statename": "Heizkreis-A.Betriebsart", "field": "betrieb_A", "description": "Betriebsart HK A", "length": 2, "factor": 1, "writable": true, "function": 11, "min": 0, "max": 5 }
31
32
  ]
@@ -40,20 +41,31 @@ definition (if any) is kept in that case rather than guessing which one to use.
40
41
  fast-changing sensor data vs. slower-changing settings data
41
42
  * `speed` - percentage of normal polling speed this control can keep up with (100 = normal;
42
43
  some versions need to be polled more slowly)
43
- * `data_blocks` - one entry per block number, each with a `definition` array of fields:
44
- * `statename` - the ioBroker state created for this field (empty for padding/unused positions)
45
- * `field` - internal field name (only used for debugging/comments)
46
- * `function` - the register id the multitalent control uses to identify this value; must be
47
- unique within its data block (checked by validation - a collision has previously caused a
48
- write meant for one field to land on another, see the 1.2.7 changelog entry)
49
- * `length` - 1 or 2 bytes on the wire
50
- * `factor` - raw value is divided/multiplied by this to get/set the real-world value
51
- * `writable` - whether the adapter subscribes to this state and sends changes back to the heat pump
52
- * `min` / `max` - optional. When present, a write outside this range is rejected (logged,
53
- never sent to the heat pump, and the displayed value is reverted) instead of being
54
- forwarded. **Only add these once you are sure of the correct value for your hardware** -
55
- this is sent to a live heat pump, and a wrong guess is enforced just as strictly as a
56
- correct one.
44
+ * `data_blocks` - one entry per block number:
45
+ * `wireLength` - optional. This block's reply length in bytes, as actually observed on the
46
+ wire (which can be a few bytes longer than its documented fields account for - the extra
47
+ bytes are simply ignored, they just have to be skipped correctly). **Only add this once you
48
+ have verified it against real hardware traffic.** An explicit, hand-verified `wireLength`
49
+ here is trusted immediately; without one, the adapter measures and confirms it for itself
50
+ from ordinary traffic at runtime instead (see the main README's "wireLength learning"
51
+ section) - either way, once every block in a firmware's sensor or settings group has a
52
+ trusted length, that whole group is requested together in one combined multi-block request
53
+ instead of one block at a time. Leaving this out just means it takes a little longer to kick
54
+ in after every restart, not that it never will.
55
+ * `definition` - array of fields:
56
+ * `statename` - the ioBroker state created for this field (empty for padding/unused positions)
57
+ * `field` - internal field name (only used for debugging/comments)
58
+ * `function` - the register id the multitalent control uses to identify this value; must be
59
+ unique within its data block (checked by validation - a collision has previously caused a
60
+ write meant for one field to land on another, see the 1.2.7 changelog entry)
61
+ * `length` - 1 or 2 bytes on the wire
62
+ * `factor` - raw value is divided/multiplied by this to get/set the real-world value
63
+ * `writable` - whether the adapter subscribes to this state and sends changes back to the heat pump
64
+ * `min` / `max` - optional. When present, a write outside this range is rejected (logged,
65
+ never sent to the heat pump, and the displayed value is reverted) instead of being
66
+ forwarded. **Only add these once you are sure of the correct value for your hardware** -
67
+ this is sent to a live heat pump, and a wrong guess is enforced just as strictly as a
68
+ correct one.
57
69
 
58
70
  ## Known operating-mode enumerations
59
71
 
@@ -41,6 +41,14 @@ class IdmProtocol {
41
41
  this.receive_state = 0;
42
42
  this.receive_chksum = '';
43
43
  this.remaining_data = '';
44
+
45
+ // version -> Map(block -> {length, confirmed}) - see recordMeasuredWireLength() and
46
+ // getVerifiedBlockWireLength(). Learned passively from real single-block traffic for a
47
+ // block that has no JSON-declared "wireLength" yet, so that firmware EVENTUALLY qualifies
48
+ // for multi-block requests too (see firmwareSupportsMultiBlockRequestsForBlocks())
49
+ // without a maintainer having to hand-verify it against a traffic capture first - see
50
+ // IdmSession's wireLength-learning hook in receive_data().
51
+ this.measuredWireLengths = new Map();
44
52
  }
45
53
 
46
54
  /**
@@ -61,6 +69,13 @@ class IdmProtocol {
61
69
  this.sensorDataBlocks.clear();
62
70
  this.settingsDataBlocks.clear();
63
71
  this.speed.clear();
72
+ // Also reset here: main.js always calls initialize() (constructor, then again in
73
+ // onReady() with the configured customDir) BEFORE ever seeding a measurement persisted
74
+ // from a previous run (see seedMeasuredWireLength()), so this is a no-op for real
75
+ // operation - but it keeps a test suite that reuses one IdmProtocol instance across
76
+ // several initialize() calls (e.g. one per `it`, via a shared beforeEach) from leaking
77
+ // measurements learned in one test into the next.
78
+ this.measuredWireLengths.clear();
64
79
 
65
80
  for (const [version, data] of byVersion) {
66
81
  this.dataDefinitions.set(version, data);
@@ -177,26 +192,126 @@ class IdmProtocol {
177
192
  return entry.definition.reduce((s, f) => s + (typeof f.length === 'number' ? f.length : 0), 0);
178
193
  }
179
194
 
195
+ /**
196
+ * The TRUSTED wire length for `block`/`version`, for anything that needs to walk a
197
+ * multi-block reply's byte boundaries (see parse_multi_block_reply()) rather than just
198
+ * tolerate extra trailing bytes the way a single-block reply's own framing already does:
199
+ * either a JSON-declared "wireLength" (hand-verified by a maintainer against a real traffic
200
+ * capture), or a length CONFIRMED from two consecutive live measurements (see
201
+ * recordMeasuredWireLength()/seedMeasuredWireLength()). Deliberately never falls back to the
202
+ * documented field-sum getBlockWireLength() does for everything else - that fallback is a
203
+ * plausible guess (blocks are known to carry undocumented trailing bytes beyond their listed
204
+ * fields), and a wrong guess here wouldn't just mis-parse one block's own fields, it would
205
+ * misalign every block AFTER it in the same reply too.
206
+ * @param {string | undefined} version
207
+ * @param {string} block
208
+ * @returns {number | null}
209
+ */
210
+ getVerifiedBlockWireLength(version, block) {
211
+ const definitions = this.dataDefinitions.get(version);
212
+ const entry = definitions ? definitions.data_blocks.find((b) => b.block_number === block) : undefined;
213
+ if (entry && typeof entry.wireLength === 'number') return entry.wireLength;
214
+ const measured = version ? this.measuredWireLengths.get(version)?.get(block) : undefined;
215
+ if (measured && measured.confirmed) return measured.length;
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * Records one single-block reply's ACTUAL wire length for `block`/`version`, learned
221
+ * passively from real traffic (see IdmSession's wireLength-learning hook in receive_data(),
222
+ * the only caller) for a block that has no JSON-declared "wireLength" yet - a no-op once the
223
+ * block already has a trusted length (see getVerifiedBlockWireLength()), whether from the
224
+ * data blocks file or an earlier confirmed measurement.
225
+ *
226
+ * A single-block reply is fully checksum-verified before this is ever called (see
227
+ * add_to_packet()), so a CORRUPTED length can't slip in that way - what this guards against
228
+ * instead is something we simply don't know either way, never having tested it: whether a
229
+ * given firmware could append a different number of undocumented reserved/padding bytes on
230
+ * different occasions. Two CONSECUTIVE matching measurements are required before a length is
231
+ * trusted enough to unlock multi-block requests for its group (see
232
+ * firmwareSupportsMultiBlockRequestsForBlocks()) - a disagreement simply restarts the count
233
+ * from the new value rather than ever averaging or guessing which one was right. A value
234
+ * restored from a previous run (see seedMeasuredWireLength()) counts as the FIRST of those
235
+ * two measurements, not a free pass - every restart re-confirms once before its group is
236
+ * trusted for multi-block again, exactly like a block seen for the very first time.
237
+ * @param {string | undefined} version
238
+ * @param {string} block
239
+ * @param {number} length byte length of this reply's payload (after the 01F200+block header)
240
+ * @returns {{status: 'already-verified' | 'first-measurement' | 'confirmed' | 'mismatch', length: number}}
241
+ */
242
+ recordMeasuredWireLength(version, block, length) {
243
+ if (this.getVerifiedBlockWireLength(version, block) !== null) {
244
+ return { status: 'already-verified', length };
245
+ }
246
+ if (!version) return { status: 'first-measurement', length };
247
+ if (!this.measuredWireLengths.has(version)) this.measuredWireLengths.set(version, new Map());
248
+ const perVersion = this.measuredWireLengths.get(version);
249
+ const previous = perVersion.get(block);
250
+ if (previous && previous.length === length) {
251
+ perVersion.set(block, { length, confirmed: true });
252
+ return { status: 'confirmed', length };
253
+ }
254
+ perVersion.set(block, { length, confirmed: false });
255
+ return { status: previous ? 'mismatch' : 'first-measurement', length };
256
+ }
257
+
258
+ /**
259
+ * Seeds `length` as a CANDIDATE measured wireLength for `block`/`version`, restored from a
260
+ * PREVIOUS adapter run (see main.js, which persists confirmed measurements to an ioBroker
261
+ * state and replays them here via this method on startup, before the connection is started).
262
+ *
263
+ * Deliberately does NOT mark it confirmed outright - every restart re-measures once before
264
+ * trusting it again (see recordMeasuredWireLength(), whose normal "two consecutive matching
265
+ * measurements" rule this feeds into as if the seeded value were the first of those two): we
266
+ * have no hard evidence a control's wire length is fixed forever rather than depending on
267
+ * some heat pump setting, so treating a restored value as an unconfirmed candidate - needing
268
+ * exactly one fresh matching measurement THIS run, same as any other block - costs one extra
269
+ * request per block per restart but means every block, freshly seen or previously learned,
270
+ * is held to the same standard rather than a persisted value being trusted blindly forever.
271
+ * @param {string} version
272
+ * @param {string} block
273
+ * @param {number} length
274
+ */
275
+ seedMeasuredWireLength(version, block, length) {
276
+ if (!this.measuredWireLengths.has(version)) this.measuredWireLengths.set(version, new Map());
277
+ this.measuredWireLengths.get(version).set(block, { length, confirmed: false });
278
+ }
279
+
180
280
  /**
181
281
  * Whether MULTI-block requests (asking for several data blocks in one 0171 message - see
182
- * IdmSession's settings block collection) are safe to use for this firmware version: every
183
- * one of its settings blocks must have an explicitly VERIFIED "wireLength" (not just the
184
- * documented field-sum fallback getBlockWireLength() falls back to otherwise) - an
185
- * unverified/wrong length would silently misparse a multi-block reply and could write a
186
- * garbage value derived from the wrong byte range into an ioBroker state. Currently true
187
- * only for S_H726100, the firmware this was verified against on real hardware; every other
188
- * supported version keeps requesting settings blocks one at a time.
282
+ * IdmSession's multi-block collector) are safe to use for `blockIds` under `version`: every
283
+ * one of them must have a TRUSTED wire length - see getVerifiedBlockWireLength(). Generalizes
284
+ * what used to be a settings-only check to any group of block ids, so the same gate now also
285
+ * covers sensor blocks (see firmwareSupportsMultiBlockRequestsForSensors()) - each group
286
+ * qualifies independently, since a block's wireLength may be hand-verified for one group and
287
+ * still only measured-and-unconfirmed (or entirely unmeasured) for the other.
288
+ * @param {string | undefined} version
289
+ * @param {string[] | null | undefined} blockIds
290
+ * @returns {boolean}
291
+ */
292
+ firmwareSupportsMultiBlockRequestsForBlocks(version, blockIds) {
293
+ if (!blockIds || blockIds.length === 0) return false;
294
+ return blockIds.every((blockId) => this.getVerifiedBlockWireLength(version, blockId) !== null);
295
+ }
296
+
297
+ /**
298
+ * Settings-blocks convenience wrapper - see firmwareSupportsMultiBlockRequestsForBlocks().
299
+ * Currently true for every version whose settings blocks all have a JSON-declared
300
+ * "wireLength" (S_H726100) OR have since been fully learned via recordMeasuredWireLength().
189
301
  * @param {string | undefined} version
190
302
  * @returns {boolean}
191
303
  */
192
304
  firmwareSupportsMultiBlockRequests(version) {
193
- const definitions = this.dataDefinitions.get(version);
194
- const settingsBlocks = this.settingsDataBlocks.get(version);
195
- if (!definitions || !settingsBlocks || settingsBlocks.length === 0) return false;
196
- return settingsBlocks.every((blockId) => {
197
- const entry = definitions.data_blocks.find((b) => b.block_number === blockId);
198
- return entry !== undefined && typeof entry.wireLength === 'number';
199
- });
305
+ return this.firmwareSupportsMultiBlockRequestsForBlocks(version, this.settingsDataBlocks.get(version));
306
+ }
307
+
308
+ /**
309
+ * Sensor-blocks convenience wrapper - see firmwareSupportsMultiBlockRequestsForBlocks().
310
+ * @param {string | undefined} version
311
+ * @returns {boolean}
312
+ */
313
+ firmwareSupportsMultiBlockRequestsForSensors(version) {
314
+ return this.firmwareSupportsMultiBlockRequestsForBlocks(version, this.sensorDataBlocks.get(version));
200
315
  }
201
316
 
202
317
  /**
@@ -219,7 +334,7 @@ class IdmProtocol {
219
334
  * Parses a multi-block data reply (see create_request_multi_block_message()) into its
220
335
  * individual blocks. Unlike protocol_state()/interpret_data(), which only ever look at the
221
336
  * FIRST block in a reply (the single-block request path never gets more than one), this
222
- * walks the whole payload using each block's VERIFIED wire length (getBlockWireLength()) to
337
+ * walks the whole payload using each block's VERIFIED wire length (getVerifiedBlockWireLength()) to
223
338
  * find where the next block's id starts. It stops and reports an error as soon as anything
224
339
  * looks inconsistent (unknown block id, truncated trailing data) rather than guessing - a
225
340
  * misparsed boundary could otherwise silently attribute one block's bytes to another
@@ -245,7 +360,7 @@ class IdmProtocol {
245
360
  }
246
361
  const block = data.slice(pos, pos + 2).toUpperCase();
247
362
  pos += 2;
248
- const wireLength = this.getBlockWireLength(version, block);
363
+ const wireLength = this.getVerifiedBlockWireLength(version, block);
249
364
  if (wireLength === null) {
250
365
  return { blocks, error: `unknown block id "${block}" at offset ${pos - 2} - lost sync (missing wireLength, or corrupted/misaligned data)` };
251
366
  }