iobroker.miele-local 0.3.45 → 0.3.47
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 +10 -0
- package/README_de.md +10 -0
- package/io-package.json +27 -27
- package/lib/befehle.js +138 -0
- package/lib/discovery.js +26 -1
- package/lib/objects.js +30 -0
- package/lib/takt.js +55 -0
- package/main.js +151 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,16 @@ engineering work of the projects `MieleRESTServer` (akappner),
|
|
|
237
237
|
### **WORK IN PROGRESS**
|
|
238
238
|
-->
|
|
239
239
|
|
|
240
|
+
### 0.3.47
|
|
241
|
+
- (SmarthomeElektroniker) A command under `control.*` that cannot be executed (control disabled, appliance not connected, HTTP error, connection error) is now acknowledged (`val=false`, `ack=true`) with the reason in the state comment `c` and a matching quality `q`. Until now it stayed unacknowledged - one `control.powerOn` stood there for 52 days
|
|
242
|
+
- (SmarthomeElektroniker) Unacknowledged commands left over from before a restart are closed on start and are **not** sent to the appliance
|
|
243
|
+
- (SmarthomeElektroniker) `remainingMinutes`/`remainingHHMM` are 0 while the appliance is off, finished or not connected (status 1/7/255), and no end time is calculated. A G5840 reported a remaining time of 0:01 while switched off
|
|
244
|
+
- (SmarthomeElektroniker) A lost connection to an appliance and its return are logged once at info level (until now only at debug)
|
|
245
|
+
- (SmarthomeElektroniker) The IP fallback search ("Search the subnet for the new IP if a device drops out") also runs in the background when mDNS discovery is off - exactly the Docker bridge setup it is meant for. It only probes appliances that currently fail, and searches every /24 network that has a missing appliance, not only the network of the first configured IP
|
|
246
|
+
|
|
247
|
+
### 0.3.46
|
|
248
|
+
- (SmarthomeElektroniker) Only the running appliance is polled at the short interval ("Poll interval (active)"); idle appliances keep the idle interval. Until now all appliances were polled at the short interval as soon as one was running - fewer requests to the Miele modules, which serve only one connection at a time
|
|
249
|
+
|
|
240
250
|
### 0.3.45
|
|
241
251
|
- (SmarthomeElektroniker) The last German state ID is gone: `eco.quelle` is now `eco.source`, its value is always English. Existing installations are migrated on start
|
|
242
252
|
- (SmarthomeElektroniker) `statusText`, `programText`, `programPhaseText`, `programTypeText` and `dryingStepText` follow the "German names" option - with the option off they are English (until now they were always German)
|
package/README_de.md
CHANGED
|
@@ -245,6 +245,16 @@ Reverse-Engineering-Arbeit der Projekte `MieleRESTServer` (akappner),
|
|
|
245
245
|
### **WORK IN PROGRESS**
|
|
246
246
|
-->
|
|
247
247
|
|
|
248
|
+
### 0.3.47
|
|
249
|
+
- Ein Befehl unter `control.*`, der nicht ausgeführt werden kann (Steuerung aus, Gerät nicht verbunden, HTTP-Fehler, Verbindungsfehler), wird jetzt bestätigt (`val=false`, `ack=true`), der Grund steht im Kommentar `c` des Datenpunkts, dazu eine passende Qualität `q`. Bisher blieb er unbestätigt stehen – ein `control.powerOn` stand so 52 Tage da.
|
|
250
|
+
- Unbestätigte Befehle von vor einem Neustart werden beim Start geschlossen und **nicht** an das Gerät gesendet.
|
|
251
|
+
- `remainingMinutes`/`remainingHHMM` sind 0, solange das Gerät aus, fertig oder nicht verbunden ist (Status 1/7/255), und es wird kein Programmende berechnet. Eine G5840 meldete ausgeschaltet eine Restzeit von 0:01.
|
|
252
|
+
- Verbindungsverlust zu einem Gerät und seine Rückkehr stehen je einmal im Log (Stufe info, bisher nur debug).
|
|
253
|
+
- Die IP-Suche bei Ausfall („Bei Ausfall die neue IP im Subnetz suchen“) läuft auch dann im Hintergrund, wenn die mDNS-Suche aus ist – also genau im Docker-Bridge-Betrieb, für den sie gedacht ist. Sie fragt nur Geräte, deren Abfrage gerade scheitert, und durchsucht jedes /24-Netz mit einem fehlenden Gerät, nicht nur das Netz der ersten eingetragenen IP.
|
|
254
|
+
|
|
255
|
+
### 0.3.46
|
|
256
|
+
- Nur das laufende Gerät wird im kurzen Takt abgefragt („Abfrageintervall (aktiv)“), Geräte in Ruhe behalten den Ruhetakt. Bisher wurden alle Geräte im kurzen Takt abgefragt, sobald eines lief – weniger Anfragen an die Miele-Module, die nur eine Verbindung gleichzeitig bedienen.
|
|
257
|
+
|
|
248
258
|
### 0.3.45
|
|
249
259
|
- Die letzte deutsche Datenpunkt-ID ist weg: `eco.quelle` heißt jetzt `eco.source`, der Wert ist immer englisch. Vorhandene Installationen ziehen beim Start um.
|
|
250
260
|
- `statusText`, `programText`, `programPhaseText`, `programTypeText` und `dryingStepText` folgen der Option „Deutsche Namen“ - ist sie aus, sind die Texte englisch (bisher immer deutsch).
|
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "miele-local",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.47",
|
|
5
5
|
"titleLang": {
|
|
6
6
|
"en": "Miele@local",
|
|
7
7
|
"de": "Miele@local",
|
|
@@ -52,6 +52,32 @@
|
|
|
52
52
|
"readme": "https://github.com/SmarthomeElektroniker/ioBroker.miele-local/blob/main/README.md",
|
|
53
53
|
"loglevel": "info",
|
|
54
54
|
"news": {
|
|
55
|
+
"0.3.47": {
|
|
56
|
+
"en": "Commands that cannot be executed are acknowledged with the reason (state comment and quality) instead of staying unacknowledged; pending commands from before a restart are closed without being sent. Remaining time is 0 while an appliance is off or finished. A lost and restored connection is logged once at info level. The IP fallback search also runs without mDNS and in every subnet with a missing appliance",
|
|
57
|
+
"de": "Nicht ausführbare Befehle werden mit Grund bestätigt (Kommentar und Qualität des Datenpunkts), statt unbestätigt stehen zu bleiben; offene Befehle von vor einem Neustart werden geschlossen, ohne sie zu senden. Restzeit ist 0, solange ein Gerät aus oder fertig ist. Verbindungsverlust und Wiederkehr stehen je einmal im Log (info). Die IP-Suche bei Ausfall läuft auch ohne mDNS und in jedem Subnetz mit einem fehlenden Gerät",
|
|
58
|
+
"ru": "Невыполнимые команды подтверждаются с указанием причины (комментарий и качество состояния), а не остаются неподтверждёнными; ожидающие команды до перезапуска закрываются без отправки. Оставшееся время равно 0, пока прибор выключен или завершил работу. Потеря и восстановление связи записываются в журнал один раз (info). Поиск IP при сбое работает и без mDNS, во всех подсетях с отсутствующим прибором",
|
|
59
|
+
"pt": "Comandos que não podem ser executados são confirmados com o motivo (comentário e qualidade do estado) em vez de ficarem por confirmar; comandos pendentes de antes de um reinício são fechados sem serem enviados. O tempo restante é 0 enquanto o aparelho está desligado ou concluído. Perda e recuperação da ligação são registadas uma vez (info). A procura de IP em caso de falha também funciona sem mDNS e em todas as sub-redes com um aparelho em falta",
|
|
60
|
+
"nl": "Opdrachten die niet uitgevoerd kunnen worden, worden met de reden bevestigd (commentaar en kwaliteit van de state) in plaats van onbevestigd te blijven; openstaande opdrachten van vóór een herstart worden gesloten zonder ze te verzenden. De resttijd is 0 zolang een apparaat uit of klaar is. Verbindingsverlies en herstel worden één keer gelogd (info). Het zoeken naar het IP-adres bij uitval werkt ook zonder mDNS en in elk subnet met een ontbrekend apparaat",
|
|
61
|
+
"fr": "Les commandes non exécutables sont acquittées avec la raison (commentaire et qualité de l'état) au lieu de rester non acquittées ; les commandes en attente d'avant un redémarrage sont fermées sans être envoyées. Le temps restant vaut 0 tant qu'un appareil est éteint ou terminé. La perte et le retour de la connexion sont journalisés une seule fois (info). La recherche d'IP en cas de panne fonctionne aussi sans mDNS et dans chaque sous-réseau où un appareil manque",
|
|
62
|
+
"it": "I comandi non eseguibili vengono confermati con il motivo (commento e qualità dello stato) invece di restare non confermati; i comandi in sospeso precedenti a un riavvio vengono chiusi senza essere inviati. Il tempo residuo è 0 finché l'elettrodomestico è spento o ha finito. Perdita e ripristino della connessione vengono registrati una sola volta (info). La ricerca dell'IP in caso di guasto funziona anche senza mDNS e in ogni sottorete con un elettrodomestico mancante",
|
|
63
|
+
"es": "Los comandos que no se pueden ejecutar se confirman con el motivo (comentario y calidad del estado) en lugar de quedar sin confirmar; los comandos pendientes de antes de un reinicio se cierran sin enviarse. El tiempo restante es 0 mientras el aparato está apagado o ha terminado. La pérdida y la recuperación de la conexión se registran una sola vez (info). La búsqueda de IP ante un fallo funciona también sin mDNS y en cada subred con un aparato ausente",
|
|
64
|
+
"pl": "Polecenia, których nie można wykonać, są potwierdzane z podaniem przyczyny (komentarz i jakość stanu), zamiast pozostawać niepotwierdzone; oczekujące polecenia sprzed restartu są zamykane bez wysyłania. Pozostały czas wynosi 0, gdy urządzenie jest wyłączone lub zakończyło pracę. Utrata i powrót połączenia są zapisywane w dzienniku jednorazowo (info). Wyszukiwanie IP po awarii działa także bez mDNS i w każdej podsieci z brakującym urządzeniem",
|
|
65
|
+
"uk": "Команди, які неможливо виконати, підтверджуються із зазначенням причини (коментар і якість стану), а не залишаються непідтвердженими; очікувані команди до перезапуску закриваються без надсилання. Залишковий час дорівнює 0, поки прилад вимкнений або завершив роботу. Втрата та відновлення зв'язку записуються в журнал один раз (info). Пошук IP при збої працює і без mDNS, у кожній підмережі з відсутнім приладом",
|
|
66
|
+
"zh-cn": "无法执行的命令会附带原因(状态注释和质量)确认,而不再保持未确认;重启前的待处理命令会被关闭且不发送。电器关闭或程序结束时剩余时间为 0。连接丢失与恢复只在日志中记录一次(info)。IP 故障搜索在没有 mDNS 时也会运行,并覆盖每个缺少电器的子网"
|
|
67
|
+
},
|
|
68
|
+
"0.3.46": {
|
|
69
|
+
"en": "Only the running appliance is polled at the short interval, idle appliances keep the idle interval - fewer requests to the Miele modules (until now all appliances were polled at the short interval as soon as one was running)",
|
|
70
|
+
"de": "Nur das laufende Gerät wird im kurzen Takt abgefragt, Geräte in Ruhe behalten den Ruhetakt - weniger Anfragen an die Miele-Module (bisher wurden alle Geräte im kurzen Takt abgefragt, sobald eines lief)",
|
|
71
|
+
"ru": "С коротким интервалом опрашивается только работающий прибор, приборы в покое сохраняют интервал покоя - меньше запросов к модулям Miele (раньше все приборы опрашивались с коротким интервалом, как только работал один)",
|
|
72
|
+
"pt": "Apenas o aparelho em funcionamento é consultado no intervalo curto, aparelhos inativos mantêm o intervalo de repouso - menos pedidos aos módulos Miele (até agora todos eram consultados no intervalo curto assim que um estava a funcionar)",
|
|
73
|
+
"nl": "Alleen het draaiende apparaat wordt met het korte interval opgevraagd, apparaten in rust houden het rustinterval - minder verzoeken aan de Miele-modules (tot nu toe werden alle apparaten kort opgevraagd zodra er één draaide)",
|
|
74
|
+
"fr": "Seul l'appareil en marche est interrogé à l'intervalle court, les appareils au repos gardent l'intervalle de repos - moins de requêtes vers les modules Miele (jusqu'ici tous étaient interrogés à l'intervalle court dès qu'un appareil fonctionnait)",
|
|
75
|
+
"it": "Solo l'elettrodomestico in funzione viene interrogato all'intervallo breve, quelli inattivi mantengono l'intervallo di riposo - meno richieste ai moduli Miele (finora tutti venivano interrogati all'intervallo breve appena uno era in funzione)",
|
|
76
|
+
"es": "Solo el aparato en marcha se consulta con el intervalo corto, los aparatos en reposo mantienen el intervalo de reposo - menos peticiones a los módulos Miele (hasta ahora todos se consultaban con el intervalo corto en cuanto uno funcionaba)",
|
|
77
|
+
"pl": "Tylko pracujące urządzenie jest odpytywane w krótkim interwale, urządzenia w spoczynku zachowują interwał spoczynku - mniej zapytań do modułów Miele (dotąd wszystkie były odpytywane krótko, gdy tylko jedno pracowało)",
|
|
78
|
+
"uk": "З коротким інтервалом опитується лише працюючий прилад, прилади у спокої зберігають інтервал спокою - менше запитів до модулів Miele (досі всі прилади опитувалися з коротким інтервалом, щойно працював один)",
|
|
79
|
+
"zh-cn": "只有正在运行的电器使用短间隔轮询,空闲电器保持空闲间隔 - 减少对 Miele 模块的请求(此前只要有一台在运行,所有电器都按短间隔轮询)"
|
|
80
|
+
},
|
|
55
81
|
"0.3.45": {
|
|
56
82
|
"en": "Only English IDs left: eco.quelle -> eco.source, eco.felderJson -> eco.fieldsJson; settings use English keys (collectorActive, leafStates, energyMeters ...), migrated once on start; text states follow the German names option; background loops never overlap",
|
|
57
83
|
"de": "Nur noch englische IDs: eco.quelle -> eco.source, eco.felderJson -> eco.fieldsJson; Einstellungen mit englischen Namen (collectorActive, leafStates, energyMeters ...), beim Start einmalig übernommen; Textdatenpunkte folgen der Option „Deutsche Namen“; Hintergrundschleifen überlappen nicht mehr",
|
|
@@ -116,32 +142,6 @@
|
|
|
116
142
|
"pl": "Poprawiono typy urządzeń (16 mikrofalówka, 67 piekarnik Dialog, 25 podgrzewacz naczyń, pralko-suszarka z programami pralki); nowe typy: robot sprzątający, piekarnik parowy kombi, piekarnik parowy z mikrofalą (418 programów), urządzenia profesjonalne. Podziękowania dla Home Assistant za tabele.",
|
|
117
143
|
"uk": "Виправлено типи пристроїв (16 мікрохвильова піч, 67 піч Dialog, 25 підігрівач посуду, прально-сушильна машина з програмами пральної машини); нові типи: робот-пилосос, комбі-пароварка, пароварка з мікрохвильовою піччю (418 програм), професійні прилади. Подяка Home Assistant за таблиці.",
|
|
118
144
|
"zh-cn": "修正设备类型(16 微波炉、67 Dialog 烤箱、25 暖碟机、洗干一体机使用洗衣机程序);新增类型:扫地机器人、蒸烤箱、微波蒸箱(418 个程序)、专业设备。感谢 Home Assistant 提供的表格。"
|
|
119
|
-
},
|
|
120
|
-
"0.3.40": {
|
|
121
|
-
"en": "README: hints for a failing login (ad blocker), moving to another system and why mDNS may find nothing; clearer log message when no appliance is found (#12).",
|
|
122
|
-
"de": "README: Hinweise bei scheiterndem Login (Werbeblocker), zum Umzug auf ein anderes System und warum mDNS nichts findet; deutlichere Logmeldung, wenn kein Gerät gefunden wird (#12).",
|
|
123
|
-
"ru": "README: подсказки при неудачном входе (блокировщик рекламы), при переносе на другую систему и о том, почему mDNS ничего не находит; более понятное сообщение в журнале, если устройство не найдено (#12).",
|
|
124
|
-
"pt": "README: dicas para um login falhado (bloqueador de anúncios), para mudar para outro sistema e porque o mDNS pode não encontrar nada; mensagem de registo mais clara quando nenhum aparelho é encontrado (#12).",
|
|
125
|
-
"nl": "README: tips bij een mislukte login (adblocker), bij verhuizing naar een ander systeem en waarom mDNS niets vindt; duidelijkere logmelding als er geen apparaat wordt gevonden (#12).",
|
|
126
|
-
"fr": "README : conseils en cas d'échec de connexion (bloqueur de publicités), pour migrer vers un autre système et pourquoi mDNS ne trouve rien ; message de journal plus clair lorsqu'aucun appareil n'est trouvé (#12).",
|
|
127
|
-
"it": "README: suggerimenti per un login non riuscito (blocco pubblicità), per il trasferimento su un altro sistema e perché mDNS non trova nulla; messaggio di log più chiaro quando non viene trovato alcun apparecchio (#12).",
|
|
128
|
-
"es": "README: consejos para un inicio de sesión fallido (bloqueador de anuncios), para migrar a otro sistema y por qué mDNS no encuentra nada; mensaje de registro más claro cuando no se encuentra ningún aparato (#12).",
|
|
129
|
-
"pl": "README: wskazówki przy nieudanym logowaniu (bloker reklam), przy przenoszeniu na inny system i dlaczego mDNS nic nie znajduje; czytelniejszy komunikat w dzienniku, gdy nie znaleziono urządzenia (#12).",
|
|
130
|
-
"uk": "README: підказки при невдалому вході (блокувальник реклами), при перенесенні на іншу систему та чому mDNS нічого не знаходить; зрозуміліше повідомлення в журналі, якщо пристрій не знайдено (#12).",
|
|
131
|
-
"zh-cn": "README:登录失败(广告拦截器)、迁移到其他系统以及 mDNS 找不到设备原因的提示;未找到设备时日志信息更清晰 (#12)。"
|
|
132
|
-
},
|
|
133
|
-
"0.3.39": {
|
|
134
|
-
"en": "Unknown program or phase IDs are shown as \"Programm 201\" instead of keeping the text of the previous program. Dishwasher program IDs of the G7771 added (#13).",
|
|
135
|
-
"de": "Unbekannte Programm- oder Phasennummern erscheinen als „Programm 201“, statt den Text des vorigen Programms stehen zu lassen. Programmnummern des Geschirrspülers G7771 ergänzt (#13).",
|
|
136
|
-
"ru": "Неизвестные номера программ или фаз отображаются как «Programm 201», а не сохраняют текст предыдущей программы. Добавлены номера программ посудомоечной машины G7771 (#13).",
|
|
137
|
-
"pt": "IDs de programa ou fase desconhecidos aparecem como \"Programm 201\" em vez de manter o texto do programa anterior. Adicionados os IDs de programa da máquina de lavar loiça G7771 (#13).",
|
|
138
|
-
"nl": "Onbekende programma- of fasenummers verschijnen als \"Programm 201\" in plaats van de tekst van het vorige programma te laten staan. Programmanummers van de vaatwasser G7771 toegevoegd (#13).",
|
|
139
|
-
"fr": "Les numéros de programme ou de phase inconnus s'affichent comme « Programm 201 » au lieu de conserver le texte du programme précédent. Numéros de programme du lave-vaisselle G7771 ajoutés (#13).",
|
|
140
|
-
"it": "I numeri di programma o di fase sconosciuti vengono mostrati come \"Programm 201\" invece di mantenere il testo del programma precedente. Aggiunti i numeri di programma della lavastoviglie G7771 (#13).",
|
|
141
|
-
"es": "Los números de programa o fase desconocidos se muestran como \"Programm 201\" en lugar de conservar el texto del programa anterior. Añadidos los números de programa del lavavajillas G7771 (#13).",
|
|
142
|
-
"pl": "Nieznane numery programów lub faz są wyświetlane jako „Programm 201” zamiast pozostawiać tekst poprzedniego programu. Dodano numery programów zmywarki G7771 (#13).",
|
|
143
|
-
"uk": "Невідомі номери програм або фаз відображаються як «Programm 201», а не зберігають текст попередньої програми. Додано номери програм посудомийної машини G7771 (#13).",
|
|
144
|
-
"zh-cn": "未知的程序或阶段编号显示为“Programm 201”,不再保留上一个程序的文本。新增 G7771 洗碗机的程序编号 (#13)。"
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
"mode": "daemon",
|
package/lib/befehle.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Steuerbefehle: ausfuehren oder sauber abschliessen (seit 0.3.47).
|
|
5
|
+
*
|
|
6
|
+
* Bis 0.3.46 blieb ein Befehl, der an der Freigabe, am Geraet oder an der Verbindung
|
|
7
|
+
* scheiterte, mit ack=false stehen. Am 16.10.2026 stand so ein control.powerOn = true seit
|
|
8
|
+
* 52 Tagen unbestaetigt da (gesetzt am 25.08., die Steuerung war damals aus). Wer den
|
|
9
|
+
* Datenpunkt liest, haelt das fuer einen Befehl, der noch unterwegs ist.
|
|
10
|
+
*
|
|
11
|
+
* Jetzt endet jeder Befehl mit val=false und ack=true. Wurde er nicht ausgefuehrt, steht der
|
|
12
|
+
* Grund im Kommentarfeld c, und die Qualitaet q ordnet ihn ein - sichtbar im Objektbrowser und
|
|
13
|
+
* fuer Skripte lesbar. Ein spaeterer erfolgreicher Befehl setzt q wieder auf 0.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** ioBroker-Qualitaetscodes, die hier vorkommen. */
|
|
17
|
+
const Q = {
|
|
18
|
+
INSTANZ: 0x11, // general problem by instance
|
|
19
|
+
NICHT_VERBUNDEN: 0x42, // device not connected
|
|
20
|
+
GERAETEFEHLER: 0x44, // device reports error
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Einen Befehl abschliessen, der nicht ausgefuehrt wurde.
|
|
25
|
+
*
|
|
26
|
+
* @param adapter Adapterinstanz (log, setStateAsync)
|
|
27
|
+
* @param {string} id vollstaendige ID des Steuer-Datenpunkts
|
|
28
|
+
* @param {string} sub Name des Befehls (fuers Log)
|
|
29
|
+
* @param {string} grund Fehlermeldung, englisch
|
|
30
|
+
* @param {number} q ioBroker-Qualitaetscode
|
|
31
|
+
*/
|
|
32
|
+
async function abschliessen(adapter, id, sub, grund, q) {
|
|
33
|
+
adapter.log.warn(`Command '${sub}' not executed: ${grund}`);
|
|
34
|
+
try {
|
|
35
|
+
await adapter.setStateAsync(id, { val: false, ack: true, q, c: `not executed: ${grund}` });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
adapter.log.debug(`${id}: could not acknowledge - ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Einen Nutzerbefehl unter control.* verarbeiten.
|
|
43
|
+
*
|
|
44
|
+
* @param adapter Adapterinstanz (config, devices, log, setStateAsync, getObjectAsync)
|
|
45
|
+
* @param {{id: string, deviceId: string, sub: string, val: unknown}} befehl der Befehl
|
|
46
|
+
* @param {{senden: (dev: object, opcode: number) => Promise<number>, nachErfolg?: () => void}} wege
|
|
47
|
+
* senden schickt den Befehl und liefert den HTTP-Status; nachErfolg holt den neuen Zustand
|
|
48
|
+
*/
|
|
49
|
+
async function verarbeiten(adapter, befehl, wege) {
|
|
50
|
+
const { id, deviceId, sub, val } = befehl;
|
|
51
|
+
const dev = adapter.devices && adapter.devices[deviceId];
|
|
52
|
+
if (!dev) {
|
|
53
|
+
return abschliessen(adapter, id, sub, 'device not connected', Q.NICHT_VERBUNDEN);
|
|
54
|
+
}
|
|
55
|
+
if (!adapter.config.allowControl) {
|
|
56
|
+
return abschliessen(adapter, id, sub, 'device control is disabled in the instance settings', Q.INSTANZ);
|
|
57
|
+
}
|
|
58
|
+
if (!val) {
|
|
59
|
+
// false loest nichts aus - nur bestaetigen, damit nichts offen bleibt.
|
|
60
|
+
await adapter.setStateAsync(id, { val: false, ack: true });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const obj = await adapter.getObjectAsync(id);
|
|
64
|
+
const opcode = obj && obj.native ? obj.native.opcode : null;
|
|
65
|
+
if (opcode == null) {
|
|
66
|
+
return abschliessen(adapter, id, sub, 'unknown command', Q.INSTANZ);
|
|
67
|
+
}
|
|
68
|
+
let status;
|
|
69
|
+
try {
|
|
70
|
+
status = await wege.senden(dev, opcode);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return abschliessen(
|
|
73
|
+
adapter,
|
|
74
|
+
id,
|
|
75
|
+
sub,
|
|
76
|
+
`${deviceId} - ${e.message} (remote control might not be supported by the firmware)`,
|
|
77
|
+
Q.NICHT_VERBUNDEN,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (status === 200 || status === 204) {
|
|
81
|
+
adapter.log.info(`Command '${sub}' sent to ${deviceId} (opcode 0x${opcode.toString(16)}).`);
|
|
82
|
+
await adapter.setStateAsync(id, { val: false, ack: true });
|
|
83
|
+
if (wege.nachErfolg) {
|
|
84
|
+
wege.nachErfolg();
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return abschliessen(
|
|
89
|
+
adapter,
|
|
90
|
+
id,
|
|
91
|
+
sub,
|
|
92
|
+
`HTTP ${status} from ${deviceId} (is MobileStart enabled on the device?)`,
|
|
93
|
+
Q.GERAETEFEHLER,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Unbestaetigte Steuerbefehle beim Start schliessen - OHNE sie auszufuehren.
|
|
99
|
+
*
|
|
100
|
+
* Ein Befehl, der beim Start noch mit ack=false dasteht, stammt aus einer frueheren Laufzeit:
|
|
101
|
+
* gesetzt, waehrend der Adapter aus war, oder unter 0.3.46 und frueher gescheitert, ohne
|
|
102
|
+
* zurueckgesetzt zu werden. Ihn jetzt nachzuholen waere gefaehrlich - ein "Start" von vor
|
|
103
|
+
* Wochen gehoert nicht an eine Maschine, die inzwischen anders beladen ist.
|
|
104
|
+
*
|
|
105
|
+
* @param adapter Adapterinstanz (getStatesAsync, setStateAsync, log)
|
|
106
|
+
* @returns {Promise<number>} Zahl der geschlossenen Befehle
|
|
107
|
+
*/
|
|
108
|
+
async function offeneSchliessen(adapter) {
|
|
109
|
+
let offen = {};
|
|
110
|
+
try {
|
|
111
|
+
offen = (await adapter.getStatesAsync('*.control.*')) || {};
|
|
112
|
+
} catch (e) {
|
|
113
|
+
adapter.log.debug(`Pending commands not checked: ${e.message}`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
let n = 0;
|
|
117
|
+
for (const [id, st] of Object.entries(offen)) {
|
|
118
|
+
if (!st || st.ack !== false) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const seit = st.ts ? new Date(st.ts).toISOString().slice(0, 10) : 'unknown';
|
|
122
|
+
adapter.log.info(`${id}: unacknowledged command from ${seit} discarded, not sent to the device`);
|
|
123
|
+
try {
|
|
124
|
+
await adapter.setStateAsync(id, {
|
|
125
|
+
val: false,
|
|
126
|
+
ack: true,
|
|
127
|
+
q: Q.INSTANZ,
|
|
128
|
+
c: `not executed: still pending at adapter start (set ${seit})`,
|
|
129
|
+
});
|
|
130
|
+
n++;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
adapter.log.debug(`${id}: could not acknowledge - ${e.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return n;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { Q, abschliessen, verarbeiten, offeneSchliessen };
|
package/lib/discovery.js
CHANGED
|
@@ -191,4 +191,29 @@ async function scanSubnet(vorlageIp, log = () => {}, opt = {}) {
|
|
|
191
191
|
return gefunden;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Eine Vorlage-Adresse je /24-Netz, in dem eine der Adressen liegt (Reihenfolge bleibt).
|
|
196
|
+
*
|
|
197
|
+
* Bis 0.3.46 suchte der Rueckfall nur im Netz der ERSTEN eingetragenen Adresse. Liegen die
|
|
198
|
+
* Geraete in verschiedenen Netzen - etwa waehrend eines Umzugs ins IoT-Netz -, blieb ein
|
|
199
|
+
* Geraet im zweiten Netz unauffindbar.
|
|
200
|
+
*
|
|
201
|
+
* @param {string[]} ips Adressen, deren Netze durchsucht werden sollen
|
|
202
|
+
* @returns {string[]} je Netz die erste passende Adresse
|
|
203
|
+
*/
|
|
204
|
+
function subnetzVorlagen(ips) {
|
|
205
|
+
const netze = new Map();
|
|
206
|
+
for (const ip of ips || []) {
|
|
207
|
+
const teile = String(ip || '').split('.');
|
|
208
|
+
if (teile.length !== 4) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const netz = teile.slice(0, 3).join('.');
|
|
212
|
+
if (!netze.has(netz)) {
|
|
213
|
+
netze.set(netz, ip);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return [...netze.values()];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { discover, SERVICE, scanSubnet, istMiele, subnetzVorlagen };
|
package/lib/objects.js
CHANGED
|
@@ -24,6 +24,34 @@ function timeToMinutes(arr) {
|
|
|
24
24
|
return arr[0] * 60 + arr[1];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/*
|
|
28
|
+
* Restzeit nur, wenn es ein Programm gibt (seit 0.3.47).
|
|
29
|
+
*
|
|
30
|
+
* Die Spuelmaschine G5840 meldete am 16.10.2026 im Zustand "Aus" (Status 1) eine Restzeit
|
|
31
|
+
* von [0, 1] - Rohwert des Geraets, im signierten GET /State nachgelesen. Durchgereicht
|
|
32
|
+
* stand im Datenpunkt "0:01", und daraus wurde ein Programmende eine Minute in der Zukunft
|
|
33
|
+
* berechnet, das nie kam. Ein ausgeschaltetes, fertiges oder nicht verbundenes Geraet hat
|
|
34
|
+
* keine Restzeit; die Anzeige des Panels ist dann ein Ueberbleibsel.
|
|
35
|
+
*
|
|
36
|
+
* Nicht betroffen sind 2 (An), 3 (programmiert) und 4 (wartet auf Start): Dort zeigt das
|
|
37
|
+
* Geraet die Dauer des gewaehlten Programms, und die ist eine echte Auskunft.
|
|
38
|
+
*/
|
|
39
|
+
const OHNE_RESTZEIT = new Set([1, 7, 255]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Setzt die Restzeit eines /State-Datensatzes auf 0, wenn das Geraet kein Programm hat.
|
|
43
|
+
* Gibt den Datensatz unveraendert zurueck, wenn nichts zu tun ist, sonst eine Kopie.
|
|
44
|
+
*
|
|
45
|
+
* @param {Record<string, unknown>} state Rohantwort von /State
|
|
46
|
+
* @returns {Record<string, unknown>} bereinigter Datensatz
|
|
47
|
+
*/
|
|
48
|
+
function restzeitBereinigen(state) {
|
|
49
|
+
if (!state || !('RemainingTime' in state) || !OHNE_RESTZEIT.has(Number(state.Status))) {
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
return { ...state, RemainingTime: [0, 0] };
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
/**
|
|
28
56
|
* [h, min] → "H:MM"-String (wie mielecloudservice), null bei fehlend.
|
|
29
57
|
*
|
|
@@ -850,6 +878,8 @@ module.exports = {
|
|
|
850
878
|
fieldAllowed,
|
|
851
879
|
nameFor,
|
|
852
880
|
timeToMinutes,
|
|
881
|
+
restzeitBereinigen,
|
|
882
|
+
OHNE_RESTZEIT,
|
|
853
883
|
temp,
|
|
854
884
|
pathGet,
|
|
855
885
|
TEMP_SENTINEL,
|
package/lib/takt.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Abfragetakt je Geraet (seit 0.3.46).
|
|
5
|
+
*
|
|
6
|
+
* Bis 0.3.45 galt der kurze Takt fuer ALLE Geraete, sobald eines lief: Backte der Ofen, wurden
|
|
7
|
+
* Wasch- und Spuelmaschine im Minutentakt mitgefragt, obwohl sie aus waren. Ein XKM-Modul bedient
|
|
8
|
+
* nur eine Verbindung gleichzeitig - jede ueberfluessige Frage nimmt der Miele-App einen
|
|
9
|
+
* Zeitschlitz weg. Jetzt hat jedes Geraet seinen eigenen Termin: kurz, solange es laeuft, sonst
|
|
10
|
+
* der Ruhetakt. Die Schleife wacht zum fruehesten Termin auf und fragt nur, wer dran ist.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Kein Wecken in kuerzeren Abstaenden als diesem - auch wenn mehrere Termine dicht liegen. */
|
|
14
|
+
const MIN_WECKABSTAND_MS = 1000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Abstand bis zur naechsten Abfrage eines Geraets.
|
|
18
|
+
*
|
|
19
|
+
* @param {{active?: boolean}} dev Geraeteeintrag
|
|
20
|
+
* @param {number} aktivMs Takt im Betrieb
|
|
21
|
+
* @param {number} ruheMs Takt in Ruhe
|
|
22
|
+
* @returns {number} Millisekunden
|
|
23
|
+
*/
|
|
24
|
+
function abstand(dev, aktivMs, ruheMs) {
|
|
25
|
+
return dev && dev.active ? aktivMs : ruheMs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ist dieses Geraet jetzt dran? Ein Geraet ohne Termin (gerade gefunden) ist sofort dran.
|
|
30
|
+
*
|
|
31
|
+
* @param {{naechsteAbfrage?: number}} dev Geraeteeintrag
|
|
32
|
+
* @param {number} jetzt Zeitpunkt in ms
|
|
33
|
+
* @returns {boolean} true, wenn es jetzt abgefragt werden soll
|
|
34
|
+
*/
|
|
35
|
+
function istFaellig(dev, jetzt) {
|
|
36
|
+
return (dev.naechsteAbfrage || 0) <= jetzt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wann die Schleife wieder aufwachen soll: zum fruehesten Termin aller Geraete.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{naechsteAbfrage?: number}>} geraete alle Geraeteeintraege
|
|
43
|
+
* @param {number} jetzt Zeitpunkt in ms
|
|
44
|
+
* @param {number} ruheMs Abstand, wenn (noch) kein Geraet bekannt ist
|
|
45
|
+
* @returns {number} Millisekunden bis zum naechsten Durchgang
|
|
46
|
+
*/
|
|
47
|
+
function weckAbstand(geraete, jetzt, ruheMs) {
|
|
48
|
+
if (!geraete.length) {
|
|
49
|
+
return ruheMs;
|
|
50
|
+
}
|
|
51
|
+
const fruehester = Math.min(...geraete.map(d => d.naechsteAbfrage || 0));
|
|
52
|
+
return Math.max(MIN_WECKABSTAND_MS, fruehester - jetzt);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { abstand, istFaellig, weckAbstand, MIN_WECKABSTAND_MS };
|
package/main.js
CHANGED
|
@@ -11,7 +11,9 @@ const utils = require('@iobroker/adapter-core');
|
|
|
11
11
|
const paket = require('./package.json');
|
|
12
12
|
const { MieleCrypto } = require('./lib/crypto');
|
|
13
13
|
const { MieleDeviceApi } = require('./lib/api');
|
|
14
|
-
const { discover, scanSubnet, istMiele } = require('./lib/discovery');
|
|
14
|
+
const { discover, scanSubnet, istMiele, subnetzVorlagen } = require('./lib/discovery');
|
|
15
|
+
const befehle = require('./lib/befehle');
|
|
16
|
+
const takt = require('./lib/takt');
|
|
15
17
|
const cloud = require('./lib/cloud');
|
|
16
18
|
const objdef = require('./lib/objects');
|
|
17
19
|
const namen = require('./lib/names');
|
|
@@ -336,11 +338,22 @@ class MieleLocal extends utils.Adapter {
|
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
// Periodisches Re-Discovery im Hintergrund (z. B. für Geräte, die aus dem Standby aufwachen)
|
|
339
|
-
|
|
341
|
+
/*
|
|
342
|
+
* Auch ohne mDNS, sobald IPs von Hand eingetragen sind und der IP-Rueckfall an ist.
|
|
343
|
+
*
|
|
344
|
+
* Bis 0.3.46 hing die ganze Hintergrundsuche an autoDiscover. Genau dort, wo der
|
|
345
|
+
* Rueckfall gebraucht wird - Docker-Bridge, mDNS aus, IPs von Hand -, lief er deshalb
|
|
346
|
+
* nur ein einziges Mal beim Start. Ein Geraet, das danach die Adresse wechselte oder
|
|
347
|
+
* beim Start nicht erreichbar war, kam bis zum naechsten Neustart nicht zurueck.
|
|
348
|
+
*/
|
|
349
|
+
const handIps = (this.config.devices || []).some(e => (typeof e === 'string' ? e : e && e.ip));
|
|
350
|
+
if (this.config.autoDiscover !== false || (this.config.ipFallbackScan !== false && handIps)) {
|
|
340
351
|
const discInterval = MieleLocal.intervallMs(this.config.autoDiscoverInterval, 10, 1, 60000);
|
|
341
352
|
this.schleife('discoveryTimer', () => this.discoverDevices(), discInterval);
|
|
342
353
|
}
|
|
343
354
|
|
|
355
|
+
// Erst Altlasten schliessen, dann abonnieren - siehe lib/befehle.js offeneSchliessen().
|
|
356
|
+
await befehle.offeneSchliessen(this);
|
|
344
357
|
await this.subscribeStatesAsync('*.control.*');
|
|
345
358
|
|
|
346
359
|
// Push-Listener + Enrollment
|
|
@@ -458,17 +471,39 @@ class MieleLocal extends utils.Adapter {
|
|
|
458
471
|
* und Statistik bleiben also dieselben, nur die Adresse ist neu.
|
|
459
472
|
*/
|
|
460
473
|
if (this.config.ipFallbackScan !== false && konfigurierte.length) {
|
|
461
|
-
|
|
462
|
-
|
|
474
|
+
// Nur nachsehen, wo die Abfrage gerade scheitert oder noch nie geklappt hat - ein
|
|
475
|
+
// laufendes Geraet bekommt keine zusaetzliche Anfrage (das Modul bedient nur eine
|
|
476
|
+
// Verbindung gleichzeitig).
|
|
477
|
+
const kandidaten = konfigurierte.filter(ip => {
|
|
478
|
+
if (this.alteIps && this.alteIps.has(ip)) {
|
|
479
|
+
return false; // Geraet laeuft schon unter seiner neuen Adresse
|
|
480
|
+
}
|
|
481
|
+
const d = Object.values(this.devices).find(x => x.ip === ip);
|
|
482
|
+
return !d || d.erreichbar === false;
|
|
483
|
+
});
|
|
484
|
+
const erreichbar = await Promise.all(kandidaten.map(ip => istMiele(ip, 1500)));
|
|
485
|
+
const vermisst = kandidaten.filter((_, i) => !erreichbar[i]);
|
|
463
486
|
/*
|
|
464
|
-
* Hoechstens alle 30 Minuten.
|
|
487
|
+
* Hoechstens alle 30 Minuten (mit Verlaengerung, siehe unten).
|
|
465
488
|
*
|
|
466
489
|
* Ist ein Geraet schlicht ausgeschaltet - der Backofen ist es die meiste Zeit -,
|
|
467
490
|
* bleibt es "vermisst", und ohne diese Sperre liefe bei jedem Discovery-Lauf ein
|
|
468
491
|
* Subnetz-Scan, der nichts Neues findet.
|
|
469
492
|
*/
|
|
493
|
+
/*
|
|
494
|
+
* Findet ein Scan nichts Neues, verdoppelt sich die Pause bis auf vier Stunden.
|
|
495
|
+
*
|
|
496
|
+
* Ein Geraet, das nach einem WLAN-Ausfall einfach nicht zurueckkommt (Waschmaschine,
|
|
497
|
+
* 16.10.2026), wuerde sonst die ganze Nacht alle 30 Minuten 254 Adressen abklopfen
|
|
498
|
+
* und zwei Logzeilen schreiben. Sobald alle wieder antworten oder eine neue Adresse
|
|
499
|
+
* auftaucht, gilt wieder die kurze Pause.
|
|
500
|
+
*/
|
|
501
|
+
const pauseMs = this.scanPauseMs || 30 * 60 * 1000;
|
|
502
|
+
if (!vermisst.length) {
|
|
503
|
+
this.scanPauseMs = 30 * 60 * 1000;
|
|
504
|
+
}
|
|
470
505
|
const seitLetztem = Date.now() - (this.letzterScan || 0);
|
|
471
|
-
if (vermisst.length && seitLetztem <
|
|
506
|
+
if (vermisst.length && seitLetztem < pauseMs) {
|
|
472
507
|
this.log.debug(
|
|
473
508
|
`${vermisst.length} device(s) not reachable, last subnet scan ` +
|
|
474
509
|
`${Math.round(seitLetztem / 60000)} min ago - waiting`,
|
|
@@ -479,14 +514,24 @@ class MieleLocal extends utils.Adapter {
|
|
|
479
514
|
`${vermisst.length} configured device(s) not responding ` +
|
|
480
515
|
`(${vermisst.join(', ')}) - searching the subnet for the new address`,
|
|
481
516
|
);
|
|
482
|
-
|
|
517
|
+
// Jedes Subnetz, in dem ein Geraet fehlt - nicht nur das der ersten Adresse.
|
|
518
|
+
const imNetz = [];
|
|
519
|
+
for (const vorlage of subnetzVorlagen(vermisst)) {
|
|
520
|
+
imNetz.push(...(await scanSubnet(vorlage, m => this.log.info(m))));
|
|
521
|
+
}
|
|
522
|
+
let neue = 0;
|
|
483
523
|
for (const ip of imNetz) {
|
|
484
524
|
if (!seenIp.has(ip)) {
|
|
485
525
|
found.push({ ip, techType: '', deviceType: null });
|
|
486
526
|
seenIp.add(ip);
|
|
527
|
+
neue++;
|
|
487
528
|
this.log.info(`New address found: ${ip}`);
|
|
488
529
|
}
|
|
489
530
|
}
|
|
531
|
+
this.scanPauseMs = neue ? 30 * 60 * 1000 : Math.min(4 * 60 * 60 * 1000, pauseMs * 2);
|
|
532
|
+
if (!neue) {
|
|
533
|
+
this.log.info(`No new address found - next subnet search in ${this.scanPauseMs / 60000} min`);
|
|
534
|
+
}
|
|
490
535
|
}
|
|
491
536
|
}
|
|
492
537
|
|
|
@@ -495,7 +540,8 @@ class MieleLocal extends utils.Adapter {
|
|
|
495
540
|
}
|
|
496
541
|
|
|
497
542
|
for (const f of found) {
|
|
498
|
-
const alreadyKnown =
|
|
543
|
+
const alreadyKnown =
|
|
544
|
+
Object.values(this.devices).some(d => d.ip === f.ip) || (this.alteIps && this.alteIps.has(f.ip));
|
|
499
545
|
if (!alreadyKnown) {
|
|
500
546
|
try {
|
|
501
547
|
await this.initDevice(f);
|
|
@@ -560,6 +606,15 @@ class MieleLocal extends utils.Adapter {
|
|
|
560
606
|
// Vor allem anderen: was aus 0.3.37 noch unter deutschen IDs liegt, umziehen.
|
|
561
607
|
await this.idsUmziehen(deviceId);
|
|
562
608
|
|
|
609
|
+
const vorher = this.devices[deviceId];
|
|
610
|
+
if (vorher && vorher.ip !== f.ip) {
|
|
611
|
+
// Gefunden ueber den IP-Rueckfall: dieselbe Seriennummer unter neuer Adresse.
|
|
612
|
+
this.alteIps = this.alteIps || new Set();
|
|
613
|
+
this.alteIps.add(vorher.ip);
|
|
614
|
+
this.log.info(
|
|
615
|
+
`${deviceId}: now at ${f.ip} (was ${vorher.ip}) - please update the IP on the 'Appliances' tab`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
563
618
|
this.devices[deviceId] = { ip: f.ip, route, deviceType, api, active: false };
|
|
564
619
|
/*
|
|
565
620
|
* Die Sammlungsobjekte schon beim Verbinden anlegen, nicht erst am Zyklusende.
|
|
@@ -888,6 +943,9 @@ class MieleLocal extends utils.Adapter {
|
|
|
888
943
|
if ('Status' in state && !(await this.statusPlausibel(deviceId, state.Status))) {
|
|
889
944
|
return;
|
|
890
945
|
}
|
|
946
|
+
// Restzeit eines ausgeschalteten/fertigen Geraets ist ein Ueberbleibsel - siehe
|
|
947
|
+
// objects.restzeitBereinigen. Vor dem Dekodieren, damit auch das Programmende stimmt.
|
|
948
|
+
state = objdef.restzeitBereinigen(state);
|
|
891
949
|
// Fuer die Einschalt-Erkennung: was stand vorher da?
|
|
892
950
|
const statusVorher =
|
|
893
951
|
'Status' in state ? ((await this.getStateAsync(`${deviceId}.state.status`)) || {}).val : null;
|
|
@@ -2164,19 +2222,41 @@ class MieleLocal extends utils.Adapter {
|
|
|
2164
2222
|
this._histCreated[deviceId] = true;
|
|
2165
2223
|
}
|
|
2166
2224
|
|
|
2225
|
+
/**
|
|
2226
|
+
* Abstand bis zur naechsten Abfrage EINES Geraets: kurz, solange es laeuft, sonst der Ruhetakt.
|
|
2227
|
+
*
|
|
2228
|
+
* Bis 0.3.45 galt der kurze Takt fuer ALLE Geraete, sobald eines lief - backte der Ofen,
|
|
2229
|
+
* wurden Wasch- und Spuelmaschine im Minutentakt mitgefragt, obwohl sie aus waren. Ein
|
|
2230
|
+
* XKM-Modul bedient nur eine Verbindung; jede ueberfluessige Frage nimmt der Miele-App
|
|
2231
|
+
* einen Zeitschlitz weg.
|
|
2232
|
+
*
|
|
2233
|
+
* @param dev Geraeteeintrag aus this.devices
|
|
2234
|
+
*/
|
|
2235
|
+
abstandFuer(dev) {
|
|
2236
|
+
return takt.abstand(
|
|
2237
|
+
dev,
|
|
2238
|
+
MieleLocal.intervallMs(this.config.activePollInterval, 5, 1),
|
|
2239
|
+
MieleLocal.intervallMs(this.config.pollInterval, 15, 1),
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2167
2243
|
schedulePoll(immediate = false) {
|
|
2168
2244
|
if (this.pollTimer) {
|
|
2169
2245
|
this.clearTimeout(this.pollTimer);
|
|
2170
2246
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2247
|
+
// Geweckt wird zur naechsten faelligen Abfrage irgendeines Geraets; pollAll(true) fragt
|
|
2248
|
+
// dann nur die faelligen. Ein Geraet ohne Termin (neu gefunden) ist sofort faellig.
|
|
2249
|
+
const interval = takt.weckAbstand(
|
|
2250
|
+
Object.values(this.devices),
|
|
2251
|
+
Date.now(),
|
|
2252
|
+
MieleLocal.intervallMs(this.config.pollInterval, 15, 1),
|
|
2253
|
+
);
|
|
2175
2254
|
const run = async () => {
|
|
2176
2255
|
if (this.stopping) {
|
|
2177
2256
|
return;
|
|
2178
2257
|
}
|
|
2179
|
-
|
|
2258
|
+
// Ein sofortiger Durchgang (nach einem Schreibbefehl) fragt alle, sonst nur die faelligen.
|
|
2259
|
+
await this.pollAll(!immediate);
|
|
2180
2260
|
this.schedulePoll(false);
|
|
2181
2261
|
};
|
|
2182
2262
|
const nextDelayMs = immediate ? 100 : interval;
|
|
@@ -2286,7 +2366,7 @@ class MieleLocal extends utils.Adapter {
|
|
|
2286
2366
|
* stauten sich am Modul. Jetzt gibt es nie zwei Laeufe derselben Aufgabe gleichzeitig.
|
|
2287
2367
|
*
|
|
2288
2368
|
* @param {string} name Feld, in dem der Timer steht (fuer clearTimeout in onUnload)
|
|
2289
|
-
* @param {() =>
|
|
2369
|
+
* @param {() => (void | Promise<void>)} aufgabe die Aufgabe; darf ein Promise liefern
|
|
2290
2370
|
* @param {number} abstandMs Pause zwischen Ende eines Laufs und Beginn des naechsten
|
|
2291
2371
|
*/
|
|
2292
2372
|
schleife(name, aufgabe, abstandMs) {
|
|
@@ -3180,9 +3260,20 @@ class MieleLocal extends utils.Adapter {
|
|
|
3180
3260
|
return 1500;
|
|
3181
3261
|
}
|
|
3182
3262
|
|
|
3183
|
-
|
|
3263
|
+
/**
|
|
3264
|
+
* Alle Geraete abfragen - oder nur die, deren Termin gekommen ist (siehe abstandFuer()).
|
|
3265
|
+
*
|
|
3266
|
+
* @param nurFaellige true: Geraete mit spaeterem Termin auslassen
|
|
3267
|
+
*/
|
|
3268
|
+
async pollAll(nurFaellige = false) {
|
|
3184
3269
|
let ok = false;
|
|
3270
|
+
const jetzt = Date.now();
|
|
3185
3271
|
for (const [deviceId, dev] of Object.entries(this.devices)) {
|
|
3272
|
+
if (nurFaellige && !takt.istFaellig(dev, jetzt)) {
|
|
3273
|
+
// Nicht dran - fuer info.connection zaehlt sein letzter Stand.
|
|
3274
|
+
ok = ok || dev.erreichbar === true;
|
|
3275
|
+
continue;
|
|
3276
|
+
}
|
|
3186
3277
|
// Zweiter Anlauf, bevor ein Abruf als Fehler gilt. Das XKM-Modul der Geraete legt im
|
|
3187
3278
|
// laufenden Betrieb sporadisch auf ("read ECONNRESET") oder antwortet kurz mit 404 -
|
|
3188
3279
|
// am 23.08.2026 an der laufenden Waschmaschine mit 21 % der Abrufe gemessen, waehrend
|
|
@@ -3215,6 +3306,29 @@ class MieleLocal extends utils.Adapter {
|
|
|
3215
3306
|
if (fehler) {
|
|
3216
3307
|
await this.setStateAsync(`${deviceId}.info.connected`, { val: false, ack: true });
|
|
3217
3308
|
}
|
|
3309
|
+
/*
|
|
3310
|
+
* Den Wechsel einmal melden, nicht jeden Fehlversuch.
|
|
3311
|
+
*
|
|
3312
|
+
* Bis 0.3.46 stand ein Ausfall nur im debug-Log - auf Stufe info sah man nichts,
|
|
3313
|
+
* waehrend die Waschmaschine am 16.10.2026 nach einem WLAN-Ausfall stundenlang
|
|
3314
|
+
* verschwunden blieb. Jede Abfrage einzeln auf info zu heben waere das Gegenteil:
|
|
3315
|
+
* alle zwei Minuten dieselbe Zeile. Also: eine Zeile beim Wegfall, eine bei der
|
|
3316
|
+
* Rueckkehr, dazwischen nur debug.
|
|
3317
|
+
*/
|
|
3318
|
+
if (fehler && dev.erreichbar !== false) {
|
|
3319
|
+
this.log.info(
|
|
3320
|
+
`${deviceId} (${dev.ip}) not reachable: ${fehler.message} - ` +
|
|
3321
|
+
`still trying every ${Math.round(this.abstandFuer(dev) / 1000)} s`,
|
|
3322
|
+
);
|
|
3323
|
+
dev.wegSeit = Date.now();
|
|
3324
|
+
} else if (!fehler && dev.erreichbar === false) {
|
|
3325
|
+
const min = dev.wegSeit ? Math.round((Date.now() - dev.wegSeit) / 60000) : null;
|
|
3326
|
+
this.log.info(`${deviceId} (${dev.ip}) reachable again${min !== null ? ` after ${min} min` : ''}`);
|
|
3327
|
+
dev.wegSeit = null;
|
|
3328
|
+
}
|
|
3329
|
+
dev.erreichbar = !fehler;
|
|
3330
|
+
// Erst nach applyState: dev.active ist dann auf dem Stand dieser Abfrage.
|
|
3331
|
+
dev.naechsteAbfrage = Date.now() + this.abstandFuer(dev);
|
|
3218
3332
|
await this.verbucheAbfrage(deviceId, fehler, wiederholt && !fehler);
|
|
3219
3333
|
}
|
|
3220
3334
|
await this.setStateAsync('info.connection', { val: ok, ack: true });
|
|
@@ -4170,52 +4284,28 @@ class MieleLocal extends utils.Adapter {
|
|
|
4170
4284
|
if (idx < 0) {
|
|
4171
4285
|
return;
|
|
4172
4286
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
const status = await dev.api.put(
|
|
4196
|
-
`Devices/${dev.route}/DOP2/${USER_REQ_UNIT}/${USER_REQ_LEAF}?idx1=0&idx2=0`,
|
|
4197
|
-
payload,
|
|
4198
|
-
);
|
|
4199
|
-
if (status === 200 || status === 204) {
|
|
4200
|
-
this.log.info(`Command '${sub}' sent to ${deviceId} (opcode 0x${opcode.toString(16)}).`);
|
|
4201
|
-
await this.setStateAsync(id, { val: false, ack: true });
|
|
4202
|
-
// Polling kurz pausieren, dann gezielt den neuen Zustand holen.
|
|
4203
|
-
this.pausePollUntil = Date.now() + 2500;
|
|
4204
|
-
if (this.pollTimer) {
|
|
4205
|
-
this.clearTimeout(this.pollTimer);
|
|
4206
|
-
}
|
|
4207
|
-
this.setTimeout(async () => {
|
|
4208
|
-
await this.pollAll();
|
|
4209
|
-
this.schedulePoll(false);
|
|
4210
|
-
}, 2600);
|
|
4211
|
-
} else {
|
|
4212
|
-
this.log.warn(`Command '${sub}' to ${deviceId}: HTTP ${status} (is MobileStart enabled on device?).`);
|
|
4213
|
-
}
|
|
4214
|
-
} catch (e) {
|
|
4215
|
-
this.log.warn(
|
|
4216
|
-
`Command '${sub}' to ${deviceId} failed: ${e.message} (remote control might not be supported by firmware).`,
|
|
4217
|
-
);
|
|
4218
|
-
}
|
|
4287
|
+
await befehle.verarbeiten(
|
|
4288
|
+
this,
|
|
4289
|
+
{ id, deviceId: parts[idx - 1], sub: parts[idx + 1], val: state.val },
|
|
4290
|
+
{
|
|
4291
|
+
senden: (dev, opcode) =>
|
|
4292
|
+
dev.api.put(
|
|
4293
|
+
`Devices/${dev.route}/DOP2/${USER_REQ_UNIT}/${USER_REQ_LEAF}?idx1=0&idx2=0`,
|
|
4294
|
+
buildUserRequest(opcode),
|
|
4295
|
+
),
|
|
4296
|
+
nachErfolg: () => {
|
|
4297
|
+
// Polling kurz pausieren, dann gezielt den neuen Zustand holen.
|
|
4298
|
+
this.pausePollUntil = Date.now() + 2500;
|
|
4299
|
+
if (this.pollTimer) {
|
|
4300
|
+
this.clearTimeout(this.pollTimer);
|
|
4301
|
+
}
|
|
4302
|
+
this.setTimeout(async () => {
|
|
4303
|
+
await this.pollAll();
|
|
4304
|
+
this.schedulePoll(false);
|
|
4305
|
+
}, 2600);
|
|
4306
|
+
},
|
|
4307
|
+
},
|
|
4308
|
+
);
|
|
4219
4309
|
}
|
|
4220
4310
|
|
|
4221
4311
|
startPush() {
|
package/package.json
CHANGED