iobroker.miele-local 0.3.46 → 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 +7 -0
- package/README_de.md +7 -0
- package/io-package.json +14 -14
- package/lib/befehle.js +138 -0
- package/lib/discovery.js +26 -1
- package/lib/objects.js +30 -0
- package/main.js +107 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,13 @@ 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
|
+
|
|
240
247
|
### 0.3.46
|
|
241
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
|
|
242
249
|
|
package/README_de.md
CHANGED
|
@@ -245,6 +245,13 @@ 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
|
+
|
|
248
255
|
### 0.3.46
|
|
249
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.
|
|
250
257
|
|
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,19 @@
|
|
|
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
|
+
},
|
|
55
68
|
"0.3.46": {
|
|
56
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)",
|
|
57
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)",
|
|
@@ -129,19 +142,6 @@
|
|
|
129
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.",
|
|
130
143
|
"uk": "Виправлено типи пристроїв (16 мікрохвильова піч, 67 піч Dialog, 25 підігрівач посуду, прально-сушильна машина з програмами пральної машини); нові типи: робот-пилосос, комбі-пароварка, пароварка з мікрохвильовою піччю (418 програм), професійні прилади. Подяка Home Assistant за таблиці.",
|
|
131
144
|
"zh-cn": "修正设备类型(16 微波炉、67 Dialog 烤箱、25 暖碟机、洗干一体机使用洗衣机程序);新增类型:扫地机器人、蒸烤箱、微波蒸箱(418 个程序)、专业设备。感谢 Home Assistant 提供的表格。"
|
|
132
|
-
},
|
|
133
|
-
"0.3.40": {
|
|
134
|
-
"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).",
|
|
135
|
-
"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).",
|
|
136
|
-
"ru": "README: подсказки при неудачном входе (блокировщик рекламы), при переносе на другую систему и о том, почему mDNS ничего не находит; более понятное сообщение в журнале, если устройство не найдено (#12).",
|
|
137
|
-
"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).",
|
|
138
|
-
"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).",
|
|
139
|
-
"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).",
|
|
140
|
-
"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).",
|
|
141
|
-
"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).",
|
|
142
|
-
"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).",
|
|
143
|
-
"uk": "README: підказки при невдалому вході (блокувальник реклами), при перенесенні на іншу систему та чому mDNS нічого не знаходить; зрозуміліше повідомлення в журналі, якщо пристрій не знайдено (#12).",
|
|
144
|
-
"zh-cn": "README:登录失败(广告拦截器)、迁移到其他系统以及 mDNS 找不到设备原因的提示;未找到设备时日志信息更清晰 (#12)。"
|
|
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/main.js
CHANGED
|
@@ -11,7 +11,8 @@ 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');
|
|
15
16
|
const takt = require('./lib/takt');
|
|
16
17
|
const cloud = require('./lib/cloud');
|
|
17
18
|
const objdef = require('./lib/objects');
|
|
@@ -337,11 +338,22 @@ class MieleLocal extends utils.Adapter {
|
|
|
337
338
|
}
|
|
338
339
|
|
|
339
340
|
// Periodisches Re-Discovery im Hintergrund (z. B. für Geräte, die aus dem Standby aufwachen)
|
|
340
|
-
|
|
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)) {
|
|
341
351
|
const discInterval = MieleLocal.intervallMs(this.config.autoDiscoverInterval, 10, 1, 60000);
|
|
342
352
|
this.schleife('discoveryTimer', () => this.discoverDevices(), discInterval);
|
|
343
353
|
}
|
|
344
354
|
|
|
355
|
+
// Erst Altlasten schliessen, dann abonnieren - siehe lib/befehle.js offeneSchliessen().
|
|
356
|
+
await befehle.offeneSchliessen(this);
|
|
345
357
|
await this.subscribeStatesAsync('*.control.*');
|
|
346
358
|
|
|
347
359
|
// Push-Listener + Enrollment
|
|
@@ -459,17 +471,39 @@ class MieleLocal extends utils.Adapter {
|
|
|
459
471
|
* und Statistik bleiben also dieselben, nur die Adresse ist neu.
|
|
460
472
|
*/
|
|
461
473
|
if (this.config.ipFallbackScan !== false && konfigurierte.length) {
|
|
462
|
-
|
|
463
|
-
|
|
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]);
|
|
464
486
|
/*
|
|
465
|
-
* Hoechstens alle 30 Minuten.
|
|
487
|
+
* Hoechstens alle 30 Minuten (mit Verlaengerung, siehe unten).
|
|
466
488
|
*
|
|
467
489
|
* Ist ein Geraet schlicht ausgeschaltet - der Backofen ist es die meiste Zeit -,
|
|
468
490
|
* bleibt es "vermisst", und ohne diese Sperre liefe bei jedem Discovery-Lauf ein
|
|
469
491
|
* Subnetz-Scan, der nichts Neues findet.
|
|
470
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
|
+
}
|
|
471
505
|
const seitLetztem = Date.now() - (this.letzterScan || 0);
|
|
472
|
-
if (vermisst.length && seitLetztem <
|
|
506
|
+
if (vermisst.length && seitLetztem < pauseMs) {
|
|
473
507
|
this.log.debug(
|
|
474
508
|
`${vermisst.length} device(s) not reachable, last subnet scan ` +
|
|
475
509
|
`${Math.round(seitLetztem / 60000)} min ago - waiting`,
|
|
@@ -480,14 +514,24 @@ class MieleLocal extends utils.Adapter {
|
|
|
480
514
|
`${vermisst.length} configured device(s) not responding ` +
|
|
481
515
|
`(${vermisst.join(', ')}) - searching the subnet for the new address`,
|
|
482
516
|
);
|
|
483
|
-
|
|
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;
|
|
484
523
|
for (const ip of imNetz) {
|
|
485
524
|
if (!seenIp.has(ip)) {
|
|
486
525
|
found.push({ ip, techType: '', deviceType: null });
|
|
487
526
|
seenIp.add(ip);
|
|
527
|
+
neue++;
|
|
488
528
|
this.log.info(`New address found: ${ip}`);
|
|
489
529
|
}
|
|
490
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
|
+
}
|
|
491
535
|
}
|
|
492
536
|
}
|
|
493
537
|
|
|
@@ -496,7 +540,8 @@ class MieleLocal extends utils.Adapter {
|
|
|
496
540
|
}
|
|
497
541
|
|
|
498
542
|
for (const f of found) {
|
|
499
|
-
const alreadyKnown =
|
|
543
|
+
const alreadyKnown =
|
|
544
|
+
Object.values(this.devices).some(d => d.ip === f.ip) || (this.alteIps && this.alteIps.has(f.ip));
|
|
500
545
|
if (!alreadyKnown) {
|
|
501
546
|
try {
|
|
502
547
|
await this.initDevice(f);
|
|
@@ -561,6 +606,15 @@ class MieleLocal extends utils.Adapter {
|
|
|
561
606
|
// Vor allem anderen: was aus 0.3.37 noch unter deutschen IDs liegt, umziehen.
|
|
562
607
|
await this.idsUmziehen(deviceId);
|
|
563
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
|
+
}
|
|
564
618
|
this.devices[deviceId] = { ip: f.ip, route, deviceType, api, active: false };
|
|
565
619
|
/*
|
|
566
620
|
* Die Sammlungsobjekte schon beim Verbinden anlegen, nicht erst am Zyklusende.
|
|
@@ -889,6 +943,9 @@ class MieleLocal extends utils.Adapter {
|
|
|
889
943
|
if ('Status' in state && !(await this.statusPlausibel(deviceId, state.Status))) {
|
|
890
944
|
return;
|
|
891
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);
|
|
892
949
|
// Fuer die Einschalt-Erkennung: was stand vorher da?
|
|
893
950
|
const statusVorher =
|
|
894
951
|
'Status' in state ? ((await this.getStateAsync(`${deviceId}.state.status`)) || {}).val : null;
|
|
@@ -3249,6 +3306,26 @@ class MieleLocal extends utils.Adapter {
|
|
|
3249
3306
|
if (fehler) {
|
|
3250
3307
|
await this.setStateAsync(`${deviceId}.info.connected`, { val: false, ack: true });
|
|
3251
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
|
+
}
|
|
3252
3329
|
dev.erreichbar = !fehler;
|
|
3253
3330
|
// Erst nach applyState: dev.active ist dann auf dem Stand dieser Abfrage.
|
|
3254
3331
|
dev.naechsteAbfrage = Date.now() + this.abstandFuer(dev);
|
|
@@ -4207,52 +4284,28 @@ class MieleLocal extends utils.Adapter {
|
|
|
4207
4284
|
if (idx < 0) {
|
|
4208
4285
|
return;
|
|
4209
4286
|
}
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
const status = await dev.api.put(
|
|
4233
|
-
`Devices/${dev.route}/DOP2/${USER_REQ_UNIT}/${USER_REQ_LEAF}?idx1=0&idx2=0`,
|
|
4234
|
-
payload,
|
|
4235
|
-
);
|
|
4236
|
-
if (status === 200 || status === 204) {
|
|
4237
|
-
this.log.info(`Command '${sub}' sent to ${deviceId} (opcode 0x${opcode.toString(16)}).`);
|
|
4238
|
-
await this.setStateAsync(id, { val: false, ack: true });
|
|
4239
|
-
// Polling kurz pausieren, dann gezielt den neuen Zustand holen.
|
|
4240
|
-
this.pausePollUntil = Date.now() + 2500;
|
|
4241
|
-
if (this.pollTimer) {
|
|
4242
|
-
this.clearTimeout(this.pollTimer);
|
|
4243
|
-
}
|
|
4244
|
-
this.setTimeout(async () => {
|
|
4245
|
-
await this.pollAll();
|
|
4246
|
-
this.schedulePoll(false);
|
|
4247
|
-
}, 2600);
|
|
4248
|
-
} else {
|
|
4249
|
-
this.log.warn(`Command '${sub}' to ${deviceId}: HTTP ${status} (is MobileStart enabled on device?).`);
|
|
4250
|
-
}
|
|
4251
|
-
} catch (e) {
|
|
4252
|
-
this.log.warn(
|
|
4253
|
-
`Command '${sub}' to ${deviceId} failed: ${e.message} (remote control might not be supported by firmware).`,
|
|
4254
|
-
);
|
|
4255
|
-
}
|
|
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
|
+
);
|
|
4256
4309
|
}
|
|
4257
4310
|
|
|
4258
4311
|
startPush() {
|
package/package.json
CHANGED