iobroker.idm-multitalent_002 2.2.1 → 2.2.3
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 +48 -18
- package/io-package.json +27 -15
- package/lib/datablocks/S_H726100.json +1 -1
- package/lib/datablocks/idm750100.json +1 -1
- package/lib/idm-session.test.js +154 -0
- package/package.json +4 -14
package/README.md
CHANGED
|
@@ -28,26 +28,46 @@ Currently following versions are supported (if your version is not listed but yo
|
|
|
28
28
|
| EVR-II100201 | EVR752 (EVR752101) | support in development currently, one experimental installation |
|
|
29
29
|
| TERRA130601 | S_H726 (S_H726100) | supported, one installation |
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
Sensor values are polled as fast as the protocol allows (at most once every 10s), settings values
|
|
32
|
+
much less often (once every 60s, plus right away after you change one yourself) - see
|
|
33
|
+
[Architecture](#architecture) below if you want the exact details, they're not needed to get
|
|
34
|
+
running. A value you change yourself is sent to the heat pump right away, but the state only shows
|
|
35
|
+
as acknowledged once it's actually been read back on the next turn, so it can take a few seconds to
|
|
36
|
+
visibly update.
|
|
37
|
+
|
|
38
|
+
## Hardware setup
|
|
39
|
+
You need an Ethernet-to-RS422 converter (also called a serial server or serial device server) to
|
|
40
|
+
connect to the multitalent control - the adapter talks to it over the network (TCP), and it talks
|
|
41
|
+
to the heat pump's control board over RS422. The screenshots and setup below use a Moxa serial
|
|
42
|
+
server as an example; any Ethernet-to-RS422 converter that can be configured with the serial
|
|
43
|
+
settings below should work.
|
|
39
44
|
|
|
40
45
|

|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
47
|
+
1. Connect the converter to your network (LAN) - this is what the adapter will connect to, so it
|
|
48
|
+
needs an IP address reachable from your ioBroker host.
|
|
49
|
+
2. Wire the converter's serial side to the 4-pin plug on the **back** of the Multitalent.002
|
|
50
|
+
control display: `Tx-`, `Tx+`, `Rx-`, `Rx+`, matching the same labels on the converter.
|
|
51
|
+
**Important:** also connect the converter's ground/shield to the control's/heat pump's ground -
|
|
52
|
+
without this, electrical interference can corrupt sensor readings.
|
|
53
|
+
3. Configure the converter's own serial port settings (in the converter's own web interface, not
|
|
54
|
+
in ioBroker) to match what the control expects:
|
|
55
|
+
```
|
|
56
|
+
Baud Rate(bps) 19200
|
|
57
|
+
Parity Even
|
|
58
|
+
Data Bit 8
|
|
59
|
+
Stop Bit 1
|
|
60
|
+
Flow Control None
|
|
61
|
+
UART FIFO Disable
|
|
62
|
+
```
|
|
63
|
+
4. Install the adapter (see [Installation](#installation) below) and, when adding an instance,
|
|
64
|
+
enter the converter's IP address and TCP port in the instance configuration.
|
|
65
|
+
|
|
66
|
+
**Known quirk - heat pump reboots (e.g. after a power loss):** while the control is booting, it
|
|
67
|
+
should not be polled. The adapter does **not** currently detect this on its own, so if the control
|
|
68
|
+
fails to start with the adapter running, stop the adapter, power-cycle the control, wait for it to
|
|
69
|
+
finish booting, then start the adapter again. A delayed switch-on of the serial connection is
|
|
70
|
+
already built in to reduce how often this happens, but it isn't a complete fix yet.
|
|
51
71
|
|
|
52
72
|
Example screenshots of objects:
|
|
53
73
|

|
|
@@ -56,6 +76,14 @@ Example screenshots of objects:
|
|
|
56
76
|

|
|
57
77
|
|
|
58
78
|
## Changelog
|
|
79
|
+
### 2.2.3 (2026-10-10)
|
|
80
|
+
* (zloe) fixed: "Heizkreis B" operating mode (Betriebsart) could not be changed on the S_H726100 and idm750100 control versions - it was incorrectly marked read-only in the data block definition (`writable: false` instead of `true`), while reading always worked fine. Found by cross-checking all six firmware definitions against each other: every other control version already had `function: 19` (same register, same min/max) correctly set to `writable: true`
|
|
81
|
+
|
|
82
|
+
### 2.2.2 (2026-09-13)
|
|
83
|
+
* (zloe) scheduler tests (sensor/settings priority, sweep completion, no starvation, debug-level logging) now run against every bundled firmware, not just idm701100/S_H726100 - and against a synthetic, non-bundled definition too, proving the scheduler doesn't just happen to work for the shapes this project ships, but for any valid "Custom data blocks directory" file a user adds themselves
|
|
84
|
+
* (zloe) docs: the hardware/RS422 setup instructions have their own clear, numbered "Hardware setup" section now, instead of being mixed into the intro alongside a duplicate (and out-of-place) explanation of the polling architecture
|
|
85
|
+
* (zloe) housekeeping ahead of submitting to the official ioBroker adapter repository: cleaned up devDependencies already provided by `@iobroker/testing`, updated `@iobroker/testing` and pinned `@types/node` to the actually supported Node range, removed the now-redundant `.npmignore`, added Node.js 26.x to the CI test matrix, and removed a stale `common.news` entry for a version that was never actually published to npm
|
|
86
|
+
|
|
59
87
|
### 2.2.1 (2026-09-13)
|
|
60
88
|
* (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
89
|
|
|
@@ -181,7 +209,9 @@ Example screenshots of objects:
|
|
|
181
209
|
As the adapter is not (yet) listed in the official ioBroker repository, install it through the Admin UI rather than a direct npm command:
|
|
182
210
|
1. In ioBroker Admin, go to **Adapters** and click the **"+" (custom install from URL)** icon in the top right
|
|
183
211
|
1. Paste `https://github.com/zloe/ioBroker.idm-multitalent_002` (or, for a specific released version, `iobroker.idm-multitalent_002@x.y.z`) into the field and confirm
|
|
184
|
-
1. Once installed, add an instance
|
|
212
|
+
1. Once installed, add an instance and, in its configuration, enter the IP address and TCP port of
|
|
213
|
+
your Ethernet-to-RS422 converter (see [Hardware setup](#hardware-setup) above if you haven't
|
|
214
|
+
wired that up yet)
|
|
185
215
|
|
|
186
216
|
## Developer manual
|
|
187
217
|
The serial protocol itself was reverse-engineered (RS422 sniffing, no official iDM documentation exists) mainly by user "makki" in the [KNX-User-Forum "idm Wärmepumpe" thread](https://knx-user-forum.de/forum/%C3%B6ffentlicher-bereich/knx-eib-forum/1251-idm-w%C3%A4rmepumpe) and refined further here; see also the [ioBroker adapter thread](https://forum.iobroker.net/topic/54253/test-adapter-idm-multitalent_002). Neither source documents official min/max limits for the writable values (see below) - they only cover which register a value lives in and how it is encoded.
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "idm-multitalent_002",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.2.3": {
|
|
7
|
+
"en": "fixed: \"Heizkreis B\" (heating circuit B) operating mode (Betriebsart) could not be changed on the S_H726100 and idm750100 control versions - it was incorrectly marked as read-only in the data block definition (writable:false instead of true), while reading it always worked fine. All other known control versions already had this correct; the field now matches circuit A/C/D on every firmware",
|
|
8
|
+
"de": "behoben: Die Betriebsart von \"Heizkreis B\" ließ sich bei den Steuerungsversionen S_H726100 und idm750100 nicht ändern - sie war in der Datenblock-Definition fälschlich als nur lesbar markiert (writable:false statt true), Lesen funktionierte dabei immer einwandfrei. Bei allen anderen bekannten Steuerungsversionen war das bereits korrekt; das Feld verhält sich jetzt überall wie Heizkreis A/C/D",
|
|
9
|
+
"ru": "исправлено: режим работы \"Heizkreis B\" (контур отопления B) нельзя было изменить на версиях управления S_H726100 и idm750100 - в определении блока данных он был ошибочно помечен только для чтения (writable:false вместо true), при этом чтение всегда работало корректно. На всех остальных известных версиях это уже было верно; теперь поле ведёт себя так же, как контуры A/C/D",
|
|
10
|
+
"pt": "corrigido: o modo de operação do \"Heizkreis B\" (circuito de aquecimento B) não podia ser alterado nas versões de controle S_H726100 e idm750100 - estava incorretamente marcado como somente leitura na definição do bloco de dados (writable:false em vez de true), embora a leitura sempre funcionasse. Em todas as outras versões conhecidas isso já estava correto; o campo agora se comporta como os circuitos A/C/D em todos os firmwares",
|
|
11
|
+
"nl": "opgelost: de bedrijfsmodus van \"Heizkreis B\" (verwarmingscircuit B) kon niet worden gewijzigd bij de besturingsversies S_H726100 en idm750100 - het was in de datablokdefinitie ten onrechte als alleen-lezen gemarkeerd (writable:false in plaats van true), terwijl lezen altijd goed werkte. Bij alle andere bekende besturingsversies was dit al correct; het veld gedraagt zich nu overal zoals circuit A/C/D",
|
|
12
|
+
"fr": "corrigé : le mode de fonctionnement de \"Heizkreis B\" (circuit de chauffage B) ne pouvait pas être modifié sur les versions de régulation S_H726100 et idm750100 - il était incorrectement marqué en lecture seule dans la définition du bloc de données (writable:false au lieu de true), alors que la lecture fonctionnait toujours correctement. Sur toutes les autres versions de régulation connues, c'était déjà correct ; le champ se comporte désormais comme les circuits A/C/D sur tous les firmwares",
|
|
13
|
+
"it": "risolto: la modalità operativa di \"Heizkreis B\" (circuito di riscaldamento B) non poteva essere modificata sulle versioni di controllo S_H726100 e idm750100 - era erroneamente contrassegnata come sola lettura nella definizione del blocco dati (writable:false invece di true), mentre la lettura ha sempre funzionato correttamente. Su tutte le altre versioni di controllo note era già corretto; il campo ora si comporta come i circuiti A/C/D su ogni firmware",
|
|
14
|
+
"es": "corregido: el modo de funcionamiento de \"Heizkreis B\" (circuito de calefacción B) no se podía cambiar en las versiones de control S_H726100 e idm750100 - estaba marcado incorrectamente como de solo lectura en la definición del bloque de datos (writable:false en lugar de true), aunque la lectura siempre funcionó correctamente. En todas las demás versiones de control conocidas ya era correcto; el campo ahora se comporta como los circuitos A/C/D en todos los firmwares",
|
|
15
|
+
"pl": "naprawiono: tryb pracy \"Heizkreis B\" (obiegu grzewczego B) nie mógł być zmieniany w wersjach sterowania S_H726100 i idm750100 - w definicji bloku danych był błędnie oznaczony jako tylko do odczytu (writable:false zamiast true), podczas gdy odczyt zawsze działał poprawnie. We wszystkich innych znanych wersjach sterowania było to już poprawne; pole zachowuje się teraz tak samo jak obiegi A/C/D we wszystkich firmware'ach",
|
|
16
|
+
"zh-cn": "修复:在 S_H726100 和 idm750100 控制版本上,\"Heizkreis B\"(B 采暖回路)的运行模式无法更改——数据块定义中该字段被错误标记为只读(writable:false 而非 true),而读取始终正常。所有其他已知控制版本此前已是正确的;现在该字段在所有固件上的行为与 A/C/D 回路一致",
|
|
17
|
+
"uk": "виправлено: режим роботи \"Heizkreis B\" (контуру опалення B) не можна було змінити на версіях керування S_H726100 та idm750100 - у визначенні блоку даних його помилково позначено як тільки для читання (writable:false замість true), хоча читання завжди працювало коректно. На всіх інших відомих версіях керування це вже було правильно; тепер поле поводиться так само, як контури A/C/D на будь-якій прошивці"
|
|
18
|
+
},
|
|
19
|
+
"2.2.2": {
|
|
20
|
+
"en": "scheduler tests now cover every bundled firmware plus a synthetic custom definition, not just two; hardware/RS422 setup now has its own clear, step-by-step section in the README; general housekeeping ahead of submitting to the official ioBroker adapter repository (dependency cleanup, CI matrix, stale changelog entry removed)",
|
|
21
|
+
"de": "Scheduler-Tests laufen jetzt für alle mitgelieferten Firmwares sowie eine synthetische Custom-Definition, nicht nur für zwei; die Hardware-/RS422-Einrichtung hat jetzt einen eigenen, klaren Schritt-für-Schritt-Abschnitt im README; allgemeine Aufräumarbeiten vor der Einreichung ins offizielle ioBroker-Adapter-Repository (Abhängigkeiten bereinigt, CI-Matrix erweitert, veralteten Changelog-Eintrag entfernt)",
|
|
22
|
+
"ru": "тесты планировщика теперь охватывают все встроенные прошивки, а также синтетическое пользовательское определение, а не только два; настройка оборудования/RS422 теперь описана в отдельном понятном пошаговом разделе README; общая подготовка перед подачей заявки в официальный репозиторий адаптеров ioBroker (очистка зависимостей, расширение CI-матрицы, удаление устаревшей записи журнала изменений)",
|
|
23
|
+
"pt": "os testes do agendador agora cobrem todos os firmwares incluídos, além de uma definição personalizada sintética, não apenas dois; a configuração de hardware/RS422 agora tem sua própria seção clara e passo a passo no README; organização geral antes do envio ao repositório oficial de adaptadores do ioBroker (limpeza de dependências, matriz de CI, remoção de entrada obsoleta do changelog)",
|
|
24
|
+
"nl": "scheduler-tests dekken nu elke meegeleverde firmware plus een synthetische aangepaste definitie, niet slechts twee; de hardware-/RS422-installatie heeft nu een eigen duidelijke, stapsgewijze sectie in de README; algemene opschoning voorafgaand aan indiening bij de officiële ioBroker-adapterrepository (afhankelijkheden opgeschoond, CI-matrix uitgebreid, verouderde changelog-vermelding verwijderd)",
|
|
25
|
+
"fr": "les tests de l'ordonnanceur couvrent désormais chaque firmware fourni ainsi qu'une définition personnalisée synthétique, pas seulement deux; la configuration matérielle/RS422 dispose désormais de sa propre section claire et détaillée dans le README; nettoyage général avant la soumission au dépôt officiel des adaptateurs ioBroker (dépendances nettoyées, matrice CI étendue, entrée de changelog obsolète supprimée)",
|
|
26
|
+
"it": "i test dello scheduler ora coprono ogni firmware incluso più una definizione personalizzata sintetica, non solo due; la configurazione hardware/RS422 ha ora una propria sezione chiara e dettagliata nel README; pulizia generale in vista dell'invio al repository ufficiale degli adattatori ioBroker (dipendenze ripulite, matrice CI ampliata, voce del changelog obsoleta rimossa)",
|
|
27
|
+
"es": "las pruebas del planificador ahora cubren cada firmware incluido más una definición personalizada sintética, no solo dos; la configuración de hardware/RS422 ahora tiene su propia sección clara y paso a paso en el README; limpieza general antes de enviar al repositorio oficial de adaptadores de ioBroker (dependencias depuradas, matriz de CI ampliada, entrada obsoleta del changelog eliminada)",
|
|
28
|
+
"pl": "testy harmonogramu obejmują teraz każdy dołączony firmware oraz syntetyczną, niestandardową definicję, a nie tylko dwa; konfiguracja sprzętu/RS422 ma teraz własną, przejrzystą, krok po kroku sekcję w README; ogólne porządki przed zgłoszeniem do oficjalnego repozytorium adapterów ioBroker (uporządkowane zależności, rozszerzona macierz CI, usunięty nieaktualny wpis dziennika zmian)",
|
|
29
|
+
"zh-cn": "调度器测试现在覆盖了每个内置固件以及一个合成的自定义定义,而不仅仅是两个;硬件/RS422 设置现在在 README 中有了自己清晰的分步说明部分;在提交到官方 ioBroker 适配器仓库之前进行了一些常规整理(清理依赖项、扩展 CI 测试矩阵、移除过时的更新日志条目)",
|
|
30
|
+
"uk": "тести планувальника тепер охоплюють кожну вбудовану прошивку, а також синтетичне користувацьке визначення, а не лише два; налаштування апаратного забезпечення/RS422 тепер має власний чіткий покроковий розділ у README; загальне впорядкування перед поданням до офіційного репозиторію адаптерів ioBroker (очищено залежності, розширено матрицю CI, видалено застарілий запис журналу змін)"
|
|
31
|
+
},
|
|
6
32
|
"2.2.1": {
|
|
7
33
|
"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
34
|
"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",
|
|
@@ -55,19 +81,6 @@
|
|
|
55
81
|
"zh-cn": "修复:此前,多区块分组(传感器或设置)的轮询回合在恰好一次请求/应答尝试后就会放弃,并将回合让给另一分组,即使还没有找到该分组的全部区块——在真实硬件上,设置分组通常需要多次重新请求才能完整到达,这使得设置采集几乎和 2.0.0 之前一样慢,尽管传感器数据的新鲜度已经有所改善。现在,一个回合会先采集完整个分组——以自适应退避方式重新请求,与最初 2.0.0 的机制完全一致,现在传感器分组也采用同样的方式——然后才让出回合",
|
|
56
82
|
"uk": "виправлення: хід multi-block-групи (сенсори або налаштування) раніше завершувався вже після рівно однієї спроби запит/відповідь і передавав чергу іншій групі, навіть якщо ще не всі блоки було знайдено - на реальному обладнанні, де налаштування часто потребують кількох повторних запитів, щоб надійти повністю, це робило збір налаштувань майже таким же повільним, як до версії 2.0.0, попри покращення актуальності даних датчиків. Тепер хід збирає всю групу цілком - повторюючи запити з адаптивною затримкою, точно як у первісному механізмі версії 2.0.0, тепер також для датчиків - перш ніж передати чергу"
|
|
57
83
|
},
|
|
58
|
-
"2.1.1": {
|
|
59
|
-
"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",
|
|
60
|
-
"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",
|
|
61
|
-
"ru": "исправление: во введении README всё ещё описывалось старое (до 2.0.0) поведение опроса - фиксированные \"~5-6 циклов\" для полного обновления настроек, один блок настроек за цикл - теперь описано фактическое, более быстрое поведение опроса версии 2.1.0",
|
|
62
|
-
"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",
|
|
63
|
-
"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",
|
|
64
|
-
"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",
|
|
65
|
-
"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",
|
|
66
|
-
"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",
|
|
67
|
-
"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",
|
|
68
|
-
"zh-cn": "修复:README 简介部分仍在描述旧版(2.0.0 之前)的轮询行为——固定的\"约 5-6 个周期\"才能完整刷新一次设置、每个周期只读取一个设置区块——现在改为描述 2.1.0 实际的、更快的轮询行为",
|
|
69
|
-
"uk": "виправлення: у вступі README досі описувалася стара (до 2.0.0) поведінка опитування - фіксовані \"~5-6 циклів\" для повного оновлення налаштувань, один блок налаштувань за цикл - тепер описано фактичну, швидшу поведінку опитування версії 2.1.0"
|
|
70
|
-
},
|
|
71
84
|
"2.1.0": {
|
|
72
85
|
"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",
|
|
73
86
|
"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",
|
|
@@ -125,7 +138,6 @@
|
|
|
125
138
|
"zloe <klaus@zloebl.net>"
|
|
126
139
|
],
|
|
127
140
|
"keywords": [
|
|
128
|
-
"ioBroker",
|
|
129
141
|
"iDM",
|
|
130
142
|
"heating",
|
|
131
143
|
"heatpump",
|
package/lib/idm-session.test.js
CHANGED
|
@@ -10,6 +10,9 @@ const { expect } = require('chai');
|
|
|
10
10
|
const sinon = require('sinon');
|
|
11
11
|
const proxyquire = require('proxyquire').noPreserveCache();
|
|
12
12
|
const { EventEmitter } = require('events');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
13
16
|
|
|
14
17
|
const IdmProtocol = require('./idm-protocol');
|
|
15
18
|
const idm_u = require('./idm-utils');
|
|
@@ -1357,6 +1360,157 @@ describe('idm-session (connection + request/response state machine)', () => {
|
|
|
1357
1360
|
});
|
|
1358
1361
|
});
|
|
1359
1362
|
|
|
1363
|
+
describe('group scheduling across every known firmware (not just idm701100/S_H726100 - see lib/datablocks/*.json)', () => {
|
|
1364
|
+
// The scheduling logic itself (sweepIncomplete/chooseNextGroup/finishSweep) never looks at
|
|
1365
|
+
// block CONTENT, only at each group's actual block id list for the connected version - so
|
|
1366
|
+
// unlike the multi-block/wireLength-learning tests (which need real wire bytes and are
|
|
1367
|
+
// deliberately only exercised against S_H726100, the one firmware with hand-verified
|
|
1368
|
+
// wireLengths), these can validate the same core scheduling guarantees against every
|
|
1369
|
+
// bundled firmware's REAL block lists directly, the same way the "sweep timing" and
|
|
1370
|
+
// "group scheduling" describe blocks above already do for idm701100 - just parametrized.
|
|
1371
|
+
// This is what actually answers "how do the other five firmwares behave under 2.2.1's
|
|
1372
|
+
// scheduler" instead of just assuming they behave like idm701100 because nothing else
|
|
1373
|
+
// fails to load.
|
|
1374
|
+
const ALL_KNOWN_VERSIONS = ['idm701100', 'idm712100', 'idm722100', 'idm750100', 'S_H726100', 'EVR752101'];
|
|
1375
|
+
|
|
1376
|
+
for (const version of ALL_KNOWN_VERSIONS) {
|
|
1377
|
+
describe(version, () => {
|
|
1378
|
+
let sensorBlocks, settingsBlocks;
|
|
1379
|
+
|
|
1380
|
+
beforeEach(() => {
|
|
1381
|
+
session.version = version;
|
|
1382
|
+
sensorBlocks = idm.getSensorDataBlocks(version);
|
|
1383
|
+
settingsBlocks = idm.getSettingsDataBlocks(version);
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('has both a non-empty sensor and settings block list (sanity - a typo here would silently skip every test below)', () => {
|
|
1387
|
+
expect(sensorBlocks, 'sensorBlocks').to.be.an('array').with.length.greaterThan(0);
|
|
1388
|
+
expect(settingsBlocks, 'settingsBlocks').to.be.an('array').with.length.greaterThan(0);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
it('picks sensor first when neither group has ever swept', () => {
|
|
1392
|
+
expect(session.chooseNextGroup()).to.equal('sensor');
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
it('keeps every turn on sensor until all of its blocks are read, then hands off to settings (never yet swept, so due)', () => {
|
|
1396
|
+
session.sweepStartedAt.sensor = Date.now();
|
|
1397
|
+
for (const b of sensorBlocks.slice(0, -1)) {
|
|
1398
|
+
session.recordBlockRead('sensor', b);
|
|
1399
|
+
expect(session.chooseNextGroup(), 'sweep not complete yet - must not hand off early').to.equal('sensor');
|
|
1400
|
+
}
|
|
1401
|
+
session.recordBlockRead('sensor', sensorBlocks[sensorBlocks.length - 1]); // completes the sweep
|
|
1402
|
+
expect(session.sweepIncomplete('sensor'), 'finishSweep() should have cleared the in-progress marker').to.be.false;
|
|
1403
|
+
expect(session.chooseNextGroup(), 'settings has never swept, so it is due now').to.equal('settings');
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it('keeps every turn on settings until all of its blocks are read, logging the completed sweep at debug (not info) level', () => {
|
|
1407
|
+
session.sweepStartedAt.sensor = Date.now(); // just swept - not due again for a while
|
|
1408
|
+
session.sweepStartedAt.settings = Date.now();
|
|
1409
|
+
for (const b of settingsBlocks.slice(0, -1)) {
|
|
1410
|
+
session.recordBlockRead('settings', b);
|
|
1411
|
+
expect(session.chooseNextGroup(), 'sweep not complete yet - must not hand off early').to.equal('settings');
|
|
1412
|
+
}
|
|
1413
|
+
session.recordBlockRead('settings', settingsBlocks[settingsBlocks.length - 1]); // completes the sweep
|
|
1414
|
+
|
|
1415
|
+
expect(log.debug.calledWithMatch(/^settings sweep done in \d+ms/), 'per-sweep completion must log at debug level').to.be.true;
|
|
1416
|
+
expect(log.info.calledWithMatch(/^settings sweep done in /), 'must NOT also log at info level').to.be.false;
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('a round-robin lap already partway through settings keeps its turns even while sensor is also due', () => {
|
|
1420
|
+
session.sweepStartedAt.settings = Date.now();
|
|
1421
|
+
session.recordBlockRead('settings', settingsBlocks[0]); // lap started, not finished (no-op if only 1 block)
|
|
1422
|
+
session.sweepStartedAt.sensor = null; // sensor has never swept - "due" by the plain rule too
|
|
1423
|
+
|
|
1424
|
+
if (settingsBlocks.length > 1) {
|
|
1425
|
+
expect(session.chooseNextGroup(), 'the in-progress settings lap must win, not sensor').to.equal('settings');
|
|
1426
|
+
} else {
|
|
1427
|
+
// A single-block settings group finishes its "lap" in one read, same as
|
|
1428
|
+
// completing the sweep outright - nothing left to keep locked in.
|
|
1429
|
+
expect(session.chooseNextGroup()).to.equal('sensor');
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
describe('group scheduling for a CUSTOM (non-bundled) data block definition (see native.dataBlocksDir / idm_datablocks.resolve())', () => {
|
|
1437
|
+
// Anyone can add a firmware version the adapter doesn't bundle by dropping a file into
|
|
1438
|
+
// the "Custom data blocks directory" setting (see lib/datablocks/README.md) - without
|
|
1439
|
+
// touching the adapter's own code, let alone this test file. The describe block above
|
|
1440
|
+
// proves the scheduler works for every version WE ship; this one proves it works for a
|
|
1441
|
+
// version we deliberately never heard of, built purely from the same public shape a
|
|
1442
|
+
// custom file has to follow (idm_datablocks.validateVersionFile()) - otherwise the
|
|
1443
|
+
// "every known firmware" coverage above would quietly be "every firmware someone on this
|
|
1444
|
+
// project happened to write a JSON file for", not "every firmware the scheduler actually
|
|
1445
|
+
// supports". Block ids and counts here are picked to NOT match any bundled version, so a
|
|
1446
|
+
// bug that accidentally hardcoded e.g. "4 sensor blocks" would show up here, not there.
|
|
1447
|
+
const CUSTOM_VERSION = 'customTestFirmware9000';
|
|
1448
|
+
let tmpDir, sensorBlocks, settingsBlocks;
|
|
1449
|
+
|
|
1450
|
+
/** A minimal, validateVersionFile()-passing field definition - content is irrelevant to scheduling. */
|
|
1451
|
+
function field(functionId) {
|
|
1452
|
+
return { field: 'f' + functionId, description: 'f' + functionId, length: 1, factor: 1, writable: false, function: functionId };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
beforeEach(() => {
|
|
1456
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'idm-session-test-customdir-'));
|
|
1457
|
+
sensorBlocks = ['0C', '0D', '0E']; // 3 - not the 4-block shape any bundled sensor group uses
|
|
1458
|
+
settingsBlocks = ['0F', '10']; // 2 - not the 5/6/7-block shape any bundled settings group uses
|
|
1459
|
+
const versionFile = {
|
|
1460
|
+
version: CUSTOM_VERSION,
|
|
1461
|
+
sensorBlocks,
|
|
1462
|
+
settingsBlocks,
|
|
1463
|
+
speed: 100,
|
|
1464
|
+
data_blocks: [...sensorBlocks, ...settingsBlocks].map((block_number, i) => ({
|
|
1465
|
+
block_number,
|
|
1466
|
+
definition: [field(i)],
|
|
1467
|
+
})),
|
|
1468
|
+
};
|
|
1469
|
+
fs.writeFileSync(path.join(tmpDir, 'custom.json'), JSON.stringify(versionFile));
|
|
1470
|
+
idm.initialize(tmpDir); // layers this on top of the bundled defaults the outer beforeEach already loaded
|
|
1471
|
+
session.version = CUSTOM_VERSION;
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
afterEach(() => {
|
|
1475
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
it('is actually loaded and picked up by the session (sanity)', () => {
|
|
1479
|
+
expect(sensorBlocks).to.deep.equal(session.groupDataBlocks('sensor'));
|
|
1480
|
+
expect(settingsBlocks).to.deep.equal(session.groupDataBlocks('settings'));
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it('picks sensor first when neither group has ever swept', () => {
|
|
1484
|
+
expect(session.chooseNextGroup()).to.equal('sensor');
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
it('keeps every turn on sensor until all of its blocks are read, then hands off to settings', () => {
|
|
1488
|
+
session.sweepStartedAt.sensor = Date.now();
|
|
1489
|
+
for (const b of sensorBlocks.slice(0, -1)) {
|
|
1490
|
+
session.recordBlockRead('sensor', b);
|
|
1491
|
+
expect(session.chooseNextGroup(), 'sweep not complete yet').to.equal('sensor');
|
|
1492
|
+
}
|
|
1493
|
+
session.recordBlockRead('sensor', sensorBlocks[sensorBlocks.length - 1]);
|
|
1494
|
+
expect(session.chooseNextGroup(), 'settings has never swept, so it is due now').to.equal('settings');
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
it('a round-robin lap already partway through settings keeps its turns even while sensor is also due', () => {
|
|
1498
|
+
session.sweepStartedAt.settings = Date.now();
|
|
1499
|
+
session.recordBlockRead('settings', settingsBlocks[0]); // 1 of 2 - lap started, not finished
|
|
1500
|
+
session.sweepStartedAt.sensor = null;
|
|
1501
|
+
|
|
1502
|
+
expect(session.chooseNextGroup(), 'the in-progress settings lap must win, not sensor').to.equal('settings');
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it('logs a completed sweep at debug level, with the right elapsed time', () => {
|
|
1506
|
+
session.sweepStartedAt.sensor = Date.now();
|
|
1507
|
+
clock.tick(1234);
|
|
1508
|
+
for (const b of sensorBlocks) session.recordBlockRead('sensor', b);
|
|
1509
|
+
|
|
1510
|
+
expect(log.debug.calledWithMatch(/^sensor sweep done in 1234ms \(avg of last 1: 1234ms\)$/)).to.be.true;
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1360
1514
|
describe('write-triggered settings refresh, end to end (see enqueueWrite() / settingsRefreshNeededAfterWrite)', () => {
|
|
1361
1515
|
it('a completed write forces a prompt settings read on the very next turn, even when settings was not otherwise due', () => {
|
|
1362
1516
|
const socket = connect();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.idm-multitalent_002",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Read and write values of a iDM heatpump with multitalent.002 control.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "zloe",
|
|
@@ -30,24 +30,14 @@
|
|
|
30
30
|
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
31
31
|
"@alcalzone/release-script-plugin-license": "^5.2.2",
|
|
32
32
|
"@eslint/js": "^10.0.1",
|
|
33
|
-
"@iobroker/testing": "^
|
|
34
|
-
"@types/chai": "^5.2.3",
|
|
35
|
-
"@types/chai-as-promised": "^8.0.2",
|
|
33
|
+
"@iobroker/testing": "^6.2.2",
|
|
36
34
|
"@types/gulp": "^4.0.18",
|
|
37
|
-
"@types/
|
|
38
|
-
"@types/node": "^26.4.0",
|
|
35
|
+
"@types/node": "^22.20.4",
|
|
39
36
|
"@types/proxyquire": "^1.3.31",
|
|
40
|
-
"@types/sinon": "^22.0.0",
|
|
41
|
-
"@types/sinon-chai": "^4.0.0",
|
|
42
|
-
"chai": "^6.2.2",
|
|
43
37
|
"axios": "^1.20.0",
|
|
44
|
-
"
|
|
45
|
-
"eslint": "^10.9.1",
|
|
38
|
+
"eslint": "^10.11.0",
|
|
46
39
|
"gulp": "^5.0.1",
|
|
47
|
-
"mocha": "^12.0.0",
|
|
48
40
|
"proxyquire": "^2.1.3",
|
|
49
|
-
"sinon": "^22.0.0",
|
|
50
|
-
"sinon-chai": "^4.0.1",
|
|
51
41
|
"typescript": "~7.0.2"
|
|
52
42
|
},
|
|
53
43
|
"main": "main.js",
|