iobroker.miele-local 0.3.38 → 0.3.40
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 +25 -1
- package/README_de.md +17 -1
- package/io-package.json +37 -19
- package/lib/api.js +70 -11
- package/lib/cloud.js +121 -37
- package/lib/crypto.js +79 -17
- package/lib/csv.js +172 -62
- package/lib/datenpunkte.js +112 -40
- package/lib/discovery.js +52 -19
- package/lib/dop2.js +78 -25
- package/lib/eco.js +46 -17
- package/lib/enums.js +861 -816
- package/lib/enums_de.js +457 -143
- package/lib/felder.js +139 -39
- package/lib/feldsuche.js +152 -56
- package/lib/ids.js +40 -14
- package/lib/kontrolle.js +88 -33
- package/lib/leafnamen.js +487 -189
- package/lib/leafscan.js +122 -32
- package/lib/leafverlauf.js +38 -10
- package/lib/names.js +2067 -247
- package/lib/objects.js +388 -97
- package/lib/push.js +66 -5
- package/lib/sammler.js +57 -22
- package/lib/stats.js +128 -42
- package/main.js +1301 -482
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -47,6 +47,14 @@ so the page stops at a spinning wheel and you read the address out of the browse
|
|
|
47
47
|
GroupID and GroupKey are then stored in the instance configuration, the key encrypted. You never
|
|
48
48
|
need this procedure again.
|
|
49
49
|
|
|
50
|
+
**Login fails with `invalid_request … unknown contextId`?** Miele's login service switches between
|
|
51
|
+
two domains during sign-in and loses its session when an ad blocker or strict third-party cookie
|
|
52
|
+
protection interferes. Open the login page in a private window without extensions.
|
|
53
|
+
|
|
54
|
+
**Moving to another system.** GroupID and GroupKey never change. A backup of the ioBroker
|
|
55
|
+
configuration (e.g. BackItUp) carries them over; on a fresh system repeating the login takes two
|
|
56
|
+
minutes. The admin page shows the key only as a placeholder.
|
|
57
|
+
|
|
50
58
|
## What you get
|
|
51
59
|
|
|
52
60
|
Every appliance becomes one device with its serial number as the ID. Below it:
|
|
@@ -123,7 +131,9 @@ Every field carries its explanation underneath it in the admin; this page does n
|
|
|
123
131
|
| outbound | TCP 443 → miele-iot.com | fetch the GroupKey | login only |
|
|
124
132
|
|
|
125
133
|
Without push **no inbound port** is needed. For mDNS, ioBroker and the appliances must sit in the
|
|
126
|
-
same broadcast segment.
|
|
134
|
+
same broadcast segment. Separate IoT WLANs or VLANs, firewalls (including the Windows firewall on
|
|
135
|
+
a test machine) and routers that filter multicast stop discovery just as well. In all these cases
|
|
136
|
+
the manual IP list is the reliable way.
|
|
127
137
|
|
|
128
138
|
**Docker.** In a container with bridge networking multicast is not forwarded, so discovery finds
|
|
129
139
|
nothing — enter the IP addresses by hand, polling then works normally. Push does not work there at
|
|
@@ -222,6 +232,20 @@ engineering work of the projects `MieleRESTServer` (akappner),
|
|
|
222
232
|
|
|
223
233
|
## Changelog
|
|
224
234
|
|
|
235
|
+
<!--
|
|
236
|
+
Placeholder for the next version (at the beginning of the line):
|
|
237
|
+
### **WORK IN PROGRESS**
|
|
238
|
+
-->
|
|
239
|
+
|
|
240
|
+
### 0.3.40
|
|
241
|
+
|
|
242
|
+
- (SmarthomeElektroniker) 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, thanks @meistermopper)
|
|
243
|
+
|
|
244
|
+
### 0.3.39
|
|
245
|
+
|
|
246
|
+
- (SmarthomeElektroniker) Unknown program or phase IDs are now shown as "Programm 201" / "Phase 1234" instead of keeping the text of the previous program (#13)
|
|
247
|
+
- (SmarthomeElektroniker) Dishwasher program IDs of the G7771 added (201, 206, 208, 211, 212, 213) (#13)
|
|
248
|
+
|
|
225
249
|
### 0.3.38
|
|
226
250
|
- **Object IDs are now consistently English.** The diagnostics channel was named `sammlung`
|
|
227
251
|
and carried German datapoint names throughout (`befund`, `fortschritt`, `datenJson`,
|
package/README_de.md
CHANGED
|
@@ -49,6 +49,15 @@ selbst aus dem Browser heraus.
|
|
|
49
49
|
GroupID und GroupKey stehen danach in der Instanzkonfiguration, der Schlüssel verschlüsselt.
|
|
50
50
|
Diese Prozedur braucht man nie wieder.
|
|
51
51
|
|
|
52
|
+
**Anmeldung scheitert mit `invalid_request … unknown contextId`?** Mieles Anmeldedienst wechselt
|
|
53
|
+
beim Login zwischen zwei Domains und verliert die Sitzung, wenn ein Werbeblocker oder ein strenger
|
|
54
|
+
Schutz vor Drittanbieter-Cookies dazwischenfunkt. Die Login-Seite dann in einem privaten Fenster
|
|
55
|
+
ohne Erweiterungen öffnen.
|
|
56
|
+
|
|
57
|
+
**Umzug auf ein anderes System.** GroupID und GroupKey ändern sich nie. Eine Sicherung der
|
|
58
|
+
ioBroker-Konfiguration (etwa mit BackItUp) nimmt sie mit; auf einem frischen System dauert der
|
|
59
|
+
Login erneut zwei Minuten. Die Admin-Seite zeigt den Schlüssel nur als Platzhalter.
|
|
60
|
+
|
|
52
61
|
## Was dabei herauskommt
|
|
53
62
|
|
|
54
63
|
Jedes Gerät wird ein Objekt mit seiner Seriennummer als Kennung. Darunter:
|
|
@@ -125,7 +134,9 @@ Jedes Feld trägt seine Erklärung im Admin direkt unter sich; diese Seite wiede
|
|
|
125
134
|
| ausgehend | TCP 443 → miele-iot.com | GroupKey abrufen | nur bei der Anmeldung |
|
|
126
135
|
|
|
127
136
|
Ohne Push ist **kein eingehender Port** nötig. Für mDNS müssen ioBroker und die Geräte im selben
|
|
128
|
-
Broadcast-Segment liegen.
|
|
137
|
+
Broadcast-Segment liegen. Getrennte IoT-WLANs oder VLANs, Firewalls (auch die Windows-Firewall auf
|
|
138
|
+
einem Testrechner) und Router, die Multicast filtern, verhindern die Suche genauso. In all diesen
|
|
139
|
+
Fällen ist die manuelle IP-Liste der verlässliche Weg.
|
|
129
140
|
|
|
130
141
|
**Docker.** In einem Container mit Bridge-Netz wird Multicast nicht weitergereicht, die Suche
|
|
131
142
|
findet also nichts – die IP-Adressen von Hand eintragen, das Abfragen läuft dann normal. Push
|
|
@@ -229,6 +240,11 @@ Reverse-Engineering-Arbeit der Projekte `MieleRESTServer` (akappner),
|
|
|
229
240
|
|
|
230
241
|
## Changelog
|
|
231
242
|
|
|
243
|
+
<!--
|
|
244
|
+
Placeholder for the next version (at the beginning of the line):
|
|
245
|
+
### **WORK IN PROGRESS**
|
|
246
|
+
-->
|
|
247
|
+
|
|
232
248
|
### 0.3.38
|
|
233
249
|
- **Die Objekt-IDs sind jetzt durchgängig englisch.** Der Diagnosekanal hieß `sammlung` und
|
|
234
250
|
trug ausschließlich deutsche Datenpunktnamen (`befund`, `fortschritt`, `datenJson`,
|
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.40",
|
|
5
5
|
"titleLang": {
|
|
6
6
|
"en": "Miele@local",
|
|
7
7
|
"de": "Miele@local",
|
|
@@ -52,26 +52,44 @@
|
|
|
52
52
|
"readme": "https://github.com/SmarthomeElektroniker/ioBroker.miele-local/blob/main/README.md",
|
|
53
53
|
"loglevel": "info",
|
|
54
54
|
"news": {
|
|
55
|
-
"0.3.
|
|
56
|
-
"en": "
|
|
57
|
-
"de": "
|
|
55
|
+
"0.3.40": {
|
|
56
|
+
"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).",
|
|
57
|
+
"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).",
|
|
58
|
+
"ru": "README: подсказки при неудачном входе (блокировщик рекламы), при переносе на другую систему и о том, почему mDNS ничего не находит; более понятное сообщение в журнале, если устройство не найдено (#12).",
|
|
59
|
+
"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).",
|
|
60
|
+
"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).",
|
|
61
|
+
"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).",
|
|
62
|
+
"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).",
|
|
63
|
+
"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).",
|
|
64
|
+
"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).",
|
|
65
|
+
"uk": "README: підказки при невдалому вході (блокувальник реклами), при перенесенні на іншу систему та чому mDNS нічого не знаходить; зрозуміліше повідомлення в журналі, якщо пристрій не знайдено (#12).",
|
|
66
|
+
"zh-cn": "README:登录失败(广告拦截器)、迁移到其他系统以及 mDNS 找不到设备原因的提示;未找到设备时日志信息更清晰 (#12)。"
|
|
58
67
|
},
|
|
59
|
-
"0.3.
|
|
60
|
-
"en": "
|
|
61
|
-
"de": "
|
|
68
|
+
"0.3.39": {
|
|
69
|
+
"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).",
|
|
70
|
+
"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).",
|
|
71
|
+
"ru": "Неизвестные номера программ или фаз отображаются как «Programm 201», а не сохраняют текст предыдущей программы. Добавлены номера программ посудомоечной машины G7771 (#13).",
|
|
72
|
+
"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).",
|
|
73
|
+
"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).",
|
|
74
|
+
"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).",
|
|
75
|
+
"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).",
|
|
76
|
+
"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).",
|
|
77
|
+
"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).",
|
|
78
|
+
"uk": "Невідомі номери програм або фаз відображаються як «Programm 201», а не зберігають текст попередньої програми. Додано номери програм посудомийної машини G7771 (#13).",
|
|
79
|
+
"zh-cn": "未知的程序或阶段编号显示为“Programm 201”,不再保留上一个程序的文本。新增 G7771 洗碗机的程序编号 (#13)。"
|
|
62
80
|
},
|
|
63
|
-
"0.3.
|
|
64
|
-
"en": "
|
|
65
|
-
"de": "
|
|
66
|
-
"ru": "
|
|
67
|
-
"pt": "
|
|
68
|
-
"nl": "
|
|
69
|
-
"fr": "
|
|
70
|
-
"it": "
|
|
71
|
-
"es": "
|
|
72
|
-
"pl": "
|
|
73
|
-
"uk": "
|
|
74
|
-
"zh-cn": "
|
|
81
|
+
"0.3.38": {
|
|
82
|
+
"en": "Object IDs are now consistently English: \"sammlung\" became \"collection\", \"befund\" became \"finding\", \"laufendSeit\" became \"runningSince\". On first start the adapter moves existing values to the new IDs; own scripts must follow suit, while recorded history stays under the old ID in the history adapter. Also since 0.3.5: water is read from the verified DOP2 field and taken from the appliance's own EcoFeedback first, device internals can be created as datapoints, a separate diagnostics tab with CSV export collects the raw fields, and the configurable intervals are capped at runtime.",
|
|
83
|
+
"de": "Objekt-IDs jetzt durchgängig englisch: Aus \"sammlung\" wurde \"collection\", aus \"befund\" wurde \"finding\", aus \"laufendSeit\" wurde \"runningSince\". Beim ersten Start zieht der Adapter vorhandene Werte an die neuen IDs um; eigene Skripte müssen nachgezogen werden, aufgezeichnete Verläufe bleiben unter der alten ID im History-Adapter. Außerdem seit 0.3.5: Wasser kommt aus dem geprüften DOP2-Feld und vorrangig aus dem EcoFeedback des Geräts, geräteinterne Messwerte lassen sich als Datenpunkte anlegen, ein eigener Diagnose-Reiter mit CSV-Ausdruck sammelt die Rohfelder, und die einstellbaren Intervalle sind zur Laufzeit begrenzt.",
|
|
84
|
+
"ru": "Идентификаторы объектов теперь полностью на английском: \"sammlung\" стало \"collection\", \"befund\" — \"finding\", \"laufendSeit\" — \"runningSince\". При первом запуске адаптер переносит имеющиеся значения на новые идентификаторы; собственные скрипты нужно поправить, а записанная история остаётся под старым идентификатором в адаптере history. Кроме того, начиная с 0.3.5: расход воды считывается из проверенного поля DOP2 и в первую очередь из EcoFeedback самого прибора, внутренние величины прибора можно создавать как состояния, отдельная вкладка диагностики с выгрузкой в CSV собирает сырые поля, а настраиваемые интервалы ограничиваются во время работы.",
|
|
85
|
+
"pt": "Os IDs de objeto passam a ser totalmente em inglês: \"sammlung\" tornou-se \"collection\", \"befund\" tornou-se \"finding\" e \"laufendSeit\" tornou-se \"runningSince\". No primeiro arranque o adaptador move os valores existentes para os novos IDs; os scripts próprios têm de ser adaptados, enquanto o histórico gravado permanece com o ID antigo no adaptador history. Além disso, desde a 0.3.5: a água é lida do campo DOP2 verificado e obtida primeiro do EcoFeedback do aparelho, os valores internos do aparelho podem ser criados como estados, um separador de diagnóstico próprio com exportação CSV reúne os campos brutos e os intervalos configuráveis são limitados em tempo de execução.",
|
|
86
|
+
"nl": "Object-ID's zijn nu volledig Engels: \"sammlung\" werd \"collection\", \"befund\" werd \"finding\" en \"laufendSeit\" werd \"runningSince\". Bij de eerste start verplaatst de adapter bestaande waarden naar de nieuwe ID's; eigen scripts moeten worden aangepast, opgenomen historie blijft onder de oude ID in de history-adapter staan. Verder sinds 0.3.5: water wordt uit het geverifieerde DOP2-veld gelezen en bij voorkeur uit de EcoFeedback van het apparaat gehaald, interne meetwaarden van het apparaat kunnen als datapunten worden aangemaakt, een eigen diagnosetabblad met CSV-export verzamelt de ruwe velden en de instelbare intervallen worden tijdens het draaien begrensd.",
|
|
87
|
+
"fr": "Les identifiants d'objets sont désormais entièrement en anglais : \"sammlung\" est devenu \"collection\", \"befund\" est devenu \"finding\" et \"laufendSeit\" est devenu \"runningSince\". Au premier démarrage, l'adaptateur déplace les valeurs existantes vers les nouveaux identifiants ; les scripts personnels doivent être adaptés, tandis que l'historique enregistré reste sous l'ancien identifiant dans l'adaptateur history. Depuis la 0.3.5 également : l'eau est lue dans le champ DOP2 vérifié et provient en priorité de l'EcoFeedback de l'appareil, les valeurs internes de l'appareil peuvent être créées comme états, un onglet de diagnostic dédié avec export CSV rassemble les champs bruts, et les intervalles configurables sont bornés à l'exécution.",
|
|
88
|
+
"it": "Gli ID degli oggetti sono ora interamente in inglese: \"sammlung\" è diventato \"collection\", \"befund\" è diventato \"finding\" e \"laufendSeit\" è diventato \"runningSince\". Al primo avvio l'adapter sposta i valori esistenti sui nuovi ID; gli script personali vanno adeguati, mentre lo storico registrato resta con il vecchio ID nell'adapter history. Inoltre dalla 0.3.5: l'acqua viene letta dal campo DOP2 verificato e presa in via prioritaria dall'EcoFeedback dell'apparecchio, i valori interni dell'apparecchio possono essere creati come stati, una scheda di diagnostica dedicata con esportazione CSV raccoglie i campi grezzi e gli intervalli configurabili sono limitati a runtime.",
|
|
89
|
+
"es": "Los ID de objeto son ahora íntegramente en inglés: \"sammlung\" pasó a \"collection\", \"befund\" a \"finding\" y \"laufendSeit\" a \"runningSince\". En el primer arranque el adaptador traslada los valores existentes a los nuevos ID; los scripts propios deben adaptarse, mientras que el histórico registrado permanece con el ID antiguo en el adaptador history. Además, desde la 0.3.5: el agua se lee del campo DOP2 verificado y se toma primero del EcoFeedback del propio aparato, los valores internos del aparato pueden crearse como estados, una pestaña de diagnóstico propia con exportación CSV reúne los campos en bruto y los intervalos configurables se limitan en tiempo de ejecución.",
|
|
90
|
+
"pl": "Identyfikatory obiektów są teraz w całości angielskie: \"sammlung\" zmieniono na \"collection\", \"befund\" na \"finding\", a \"laufendSeit\" na \"runningSince\". Przy pierwszym uruchomieniu adapter przenosi istniejące wartości na nowe identyfikatory; własne skrypty trzeba dostosować, a zapisana historia pozostaje pod starym identyfikatorem w adapterze history. Ponadto od wersji 0.3.5: woda jest odczytywana ze sprawdzonego pola DOP2 i pobierana w pierwszej kolejności z EcoFeedback urządzenia, wewnętrzne wartości urządzenia można tworzyć jako stany, osobna zakładka diagnostyczna z eksportem CSV zbiera pola surowe, a konfigurowalne interwały są ograniczane w czasie działania.",
|
|
91
|
+
"uk": "Ідентифікатори об'єктів тепер повністю англійські: \"sammlung\" стало \"collection\", \"befund\" — \"finding\", \"laufendSeit\" — \"runningSince\". Під час першого запуску адаптер переносить наявні значення на нові ідентифікатори; власні скрипти потрібно оновити, а записана історія залишається зі старим ідентифікатором в адаптері history. Крім того, починаючи з 0.3.5: вода зчитується з перевіреного поля DOP2 і насамперед береться з EcoFeedback самого приладу, внутрішні величини приладу можна створювати як стани, окрема вкладка діагностики з експортом у CSV збирає сирі поля, а налаштовувані інтервали обмежуються під час роботи.",
|
|
92
|
+
"zh-cn": "对象 ID 现已全部使用英文:\"sammlung\" 改为 \"collection\",\"befund\" 改为 \"finding\",\"laufendSeit\" 改为 \"runningSince\"。首次启动时,适配器会把已有数值迁移到新的 ID;自建脚本需要相应调整,已记录的历史数据仍以旧 ID 保留在 history 适配器中。此外自 0.3.5 起:用水量取自经过验证的 DOP2 字段,并优先来自设备自身的 EcoFeedback;设备内部数值可创建为数据点;独立的诊断选项卡支持导出 CSV 以收集原始字段;可配置的轮询间隔在运行时会被限制。"
|
|
75
93
|
},
|
|
76
94
|
"0.3.5": {
|
|
77
95
|
"en": "Fix: appliance status no longer flips to \"off\" during a running program, and the remaining time is read from the correct field. Frozen EcoFeedback values are no longer booked as consumption, requests are serialised per appliance, and a cycle survives an adapter restart. EcoFeedback is only requested while an appliance is actually running. Login instructions and datapoint names translated into all supported languages.",
|
package/lib/api.js
CHANGED
|
@@ -30,16 +30,33 @@ class MieleDeviceApi {
|
|
|
30
30
|
* Ruhezustand 4-5 Zeitueberschreitungen je Stunde, waehrend eines laufenden Programms - wenn
|
|
31
31
|
* das Modul ohnehin mehr zu tun hat - ueber 50. Das Geraet war dabei per Ping durchgehend
|
|
32
32
|
* erreichbar, es kam schlicht nicht hinterher.
|
|
33
|
+
*
|
|
34
|
+
* @param method
|
|
35
|
+
* @param resource
|
|
36
|
+
* @param bodyPlain
|
|
37
|
+
* @param timeoutMs
|
|
33
38
|
*/
|
|
34
39
|
_request(method, resource, bodyPlain, timeoutMs) {
|
|
35
40
|
const starten = () => this._sendeAnfrage(method, resource, bodyPlain, timeoutMs);
|
|
36
41
|
const ergebnis = this._warteschlange.then(starten, starten);
|
|
37
42
|
// Die Warteschlange selbst darf nie im Fehlerzustand bleiben - sonst uebersprungen sie
|
|
38
43
|
// den naechsten Eintrag, statt ihn zu starten.
|
|
39
|
-
this._warteschlange = ergebnis.then(
|
|
44
|
+
this._warteschlange = ergebnis.then(
|
|
45
|
+
() => {},
|
|
46
|
+
() => {},
|
|
47
|
+
);
|
|
40
48
|
return ergebnis;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Eine signierte MieleH256-Anfrage stellen und die Antwort entschluesseln.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} method HTTP-Verb
|
|
55
|
+
* @param {string} resource Pfad unterhalb der Geraetewurzel
|
|
56
|
+
* @param {Buffer|null} bodyPlain unverschluesselter Rumpf, sonst null
|
|
57
|
+
* @param {number} timeoutMs Zeitgrenze
|
|
58
|
+
* @returns {Promise<object>} Antwort des Geraets
|
|
59
|
+
*/
|
|
43
60
|
_sendeAnfrage(method, resource, bodyPlain, timeoutMs) {
|
|
44
61
|
return new Promise((resolve, reject) => {
|
|
45
62
|
const bodyMethod = method === 'PUT' || method === 'POST';
|
|
@@ -58,8 +75,14 @@ class MieleDeviceApi {
|
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
const req = http.request(
|
|
61
|
-
{
|
|
62
|
-
|
|
78
|
+
{
|
|
79
|
+
host: this.host,
|
|
80
|
+
port: 80,
|
|
81
|
+
method,
|
|
82
|
+
path: `/${resource}`,
|
|
83
|
+
headers,
|
|
84
|
+
timeout: timeoutMs || this.timeout,
|
|
85
|
+
},
|
|
63
86
|
res => {
|
|
64
87
|
const chunks = [];
|
|
65
88
|
res.on('data', c => chunks.push(c));
|
|
@@ -74,22 +97,32 @@ class MieleDeviceApi {
|
|
|
74
97
|
);
|
|
75
98
|
req.on('error', reject);
|
|
76
99
|
req.on('timeout', () => req.destroy(new Error(`Timeout ${this.host}/${resource}`)));
|
|
77
|
-
if (sendBuf)
|
|
100
|
+
if (sendBuf) {
|
|
101
|
+
req.write(sendBuf);
|
|
102
|
+
}
|
|
78
103
|
req.end();
|
|
79
104
|
});
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Signierter GET, entschlüsselt & als JSON geparst.
|
|
109
|
+
*
|
|
110
|
+
* @param resource
|
|
111
|
+
*/
|
|
83
112
|
async get(resource) {
|
|
84
113
|
const res = await this._request('GET', resource);
|
|
85
|
-
if (res.status === 204)
|
|
114
|
+
if (res.status === 204) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
86
117
|
if (res.status !== 200) {
|
|
87
118
|
const err = new Error(`GET /${resource} → HTTP ${res.status}`);
|
|
88
119
|
err.status = res.status;
|
|
89
120
|
throw err;
|
|
90
121
|
}
|
|
91
122
|
const xsig = res.headers['x-signature'];
|
|
92
|
-
if (!xsig)
|
|
123
|
+
if (!xsig) {
|
|
124
|
+
throw new Error(`GET /${resource}: kein X-Signature-Header`);
|
|
125
|
+
}
|
|
93
126
|
const plain = this.mc.decryptResponse(xsig, res.body);
|
|
94
127
|
const txt = plain.toString('utf8').replace(/[\x00\x20]+$/g, '');
|
|
95
128
|
try {
|
|
@@ -101,28 +134,55 @@ class MieleDeviceApi {
|
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Signierter PUT mit verschlüsseltem Body. Gibt Statuscode zurück.
|
|
139
|
+
*
|
|
140
|
+
* @param resource
|
|
141
|
+
* @param bodyPlain
|
|
142
|
+
*/
|
|
105
143
|
async put(resource, bodyPlain) {
|
|
106
144
|
const res = await this._request('PUT', resource, bodyPlain);
|
|
107
145
|
return res.status;
|
|
108
146
|
}
|
|
109
147
|
|
|
110
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Signierter POST mit verschlüsseltem Body. Gibt {status, location} zurück.
|
|
150
|
+
*
|
|
151
|
+
* @param resource
|
|
152
|
+
* @param bodyPlain
|
|
153
|
+
*/
|
|
111
154
|
async post(resource, bodyPlain) {
|
|
112
155
|
const res = await this._request('POST', resource, bodyPlain);
|
|
113
156
|
return { status: res.status, location: res.headers['location'] || '' };
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
// --- Convenience ---
|
|
160
|
+
/**
|
|
161
|
+
* Die Kennung eines Geraets lesen (Modell, Seriennummer, Firmware).
|
|
162
|
+
*
|
|
163
|
+
* @param {string} route Geraetepfad in der DOP2-Struktur
|
|
164
|
+
* @returns {Promise<object>} Ident-Zweig des Geraets
|
|
165
|
+
*/
|
|
117
166
|
getIdent(route) {
|
|
118
167
|
return this.get(`Devices/${route}/Ident`);
|
|
119
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Den Zustand eines Geraets lesen (Status, Programm, Restzeit).
|
|
171
|
+
*
|
|
172
|
+
* @param {string} route Geraetepfad in der DOP2-Struktur
|
|
173
|
+
* @returns {Promise<object>} State-Zweig des Geraets
|
|
174
|
+
*/
|
|
120
175
|
getState(route) {
|
|
121
176
|
return this.get(`Devices/${route}/State`);
|
|
122
177
|
}
|
|
123
178
|
/**
|
|
124
179
|
* Ein DOP2-Leaf lesen.
|
|
125
180
|
*
|
|
181
|
+
* @param route
|
|
182
|
+
* @param unit
|
|
183
|
+
* @param attr
|
|
184
|
+
* @param idx1
|
|
185
|
+
* @param idx2
|
|
126
186
|
* @param {number} [timeoutMs] eigenes Zeitlimit fuer DIESE Anfrage.
|
|
127
187
|
*
|
|
128
188
|
* WOZU DAS EIGENE LIMIT: Der Regelbetrieb fragt bekannte Adressen ab, die antworten -
|
|
@@ -133,8 +193,7 @@ class MieleDeviceApi {
|
|
|
133
193
|
* gehoert hat, hoert auch nach acht nichts.
|
|
134
194
|
*/
|
|
135
195
|
readDop2(route, unit, attr, idx1 = 0, idx2 = 0, timeoutMs = null) {
|
|
136
|
-
return this._request('GET',
|
|
137
|
-
`Devices/${route}/DOP2/${unit}/${attr}?idx1=${idx1}&idx2=${idx2}`, null, timeoutMs);
|
|
196
|
+
return this._request('GET', `Devices/${route}/DOP2/${unit}/${attr}?idx1=${idx1}&idx2=${idx2}`, null, timeoutMs);
|
|
138
197
|
}
|
|
139
198
|
}
|
|
140
199
|
|
package/lib/cloud.js
CHANGED
|
@@ -18,15 +18,24 @@ const { URL, URLSearchParams } = require('node:url');
|
|
|
18
18
|
|
|
19
19
|
// Länderspezifische MAP-Consumer-client_ids (aus offizieller Android-APK).
|
|
20
20
|
const CONSUMER_CLIENT_IDS = {
|
|
21
|
-
at: 'wNv9HJ3ZcFKH4bxvz0LExQuw',
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
at: 'wNv9HJ3ZcFKH4bxvz0LExQuw',
|
|
22
|
+
ch: 'V52nWiniHyVotglJKplSXnX8',
|
|
23
|
+
cz: 'npoAzuJP6okjvJ0NqUq9i5Rv',
|
|
24
|
+
de: 'UJgKOxacIul2BcPJAzrQE6p0',
|
|
25
|
+
dk: 'xWgykqRQSa9THqOXWfzZbxsH',
|
|
26
|
+
es: 'D0Q4NPBR9dwP2EjX4E0_CtHE',
|
|
27
|
+
fr: 'SOiiE3R4tSD0VxYYBvB8Pi_J',
|
|
28
|
+
gb: 'WigtLzKGJE1Wg6yeZUECV8-P',
|
|
29
|
+
hr: 'HD4OUUQYAw_5DtVFSe4-rYzR',
|
|
30
|
+
hu: '2mm2yscHPGJ4tJCVjd6mp-to',
|
|
31
|
+
it: 'ARQyaYB0ZxLxJ1SJcjJgctuV',
|
|
32
|
+
nl: '7ItTbQXQ1wthDOue9jvBQ7Iz',
|
|
33
|
+
pl: 'jWbgLScpvIuqjUoYvf1jS-Is',
|
|
34
|
+
pt: '5ZVD-CuJvpG4YpCO9pQhtrGQ',
|
|
35
|
+
se: '3Mm7m1gD1eU_sUh8yxmShL6S',
|
|
36
|
+
si: 'UTyhG21RchpI8FPbNeb1vFg1',
|
|
37
|
+
sk: 'pGeafLwcC1_BCLr8DRTCVxSt',
|
|
38
|
+
us: 'HpsWh2gzgKqRBduPpkZ4Yui9',
|
|
30
39
|
};
|
|
31
40
|
const REDIRECT_URI = 'miele://oauth2-code/';
|
|
32
41
|
const OAUTH_SCOPE = 'openid mcs bpdata zuora'; // `mcs` ist Pflicht für /V2/GroupKeyId/
|
|
@@ -48,29 +57,48 @@ function httpsRequest(method, urlStr, { headers = {}, body = null } = {}) {
|
|
|
48
57
|
res => {
|
|
49
58
|
const chunks = [];
|
|
50
59
|
res.on('data', c => chunks.push(c));
|
|
51
|
-
res.on('end', () =>
|
|
60
|
+
res.on('end', () =>
|
|
61
|
+
resolve({
|
|
62
|
+
status: res.statusCode,
|
|
63
|
+
headers: res.headers,
|
|
64
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
52
67
|
},
|
|
53
68
|
);
|
|
54
69
|
req.on('error', reject);
|
|
55
70
|
req.on('timeout', () => req.destroy(new Error('HTTPS Timeout')));
|
|
56
|
-
if (body)
|
|
71
|
+
if (body) {
|
|
72
|
+
req.write(body);
|
|
73
|
+
}
|
|
57
74
|
req.end();
|
|
58
75
|
});
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Autorisierungs-URL + PKCE-Challenge erzeugen.
|
|
80
|
+
*
|
|
81
|
+
* @param cc
|
|
82
|
+
*/
|
|
62
83
|
function buildAuthorizeUrl(cc) {
|
|
63
84
|
cc = String(cc).toLowerCase();
|
|
64
85
|
const clientId = CONSUMER_CLIENT_IDS[cc];
|
|
65
|
-
if (!clientId)
|
|
86
|
+
if (!clientId) {
|
|
87
|
+
throw new Error(`Unbekanntes Land "${cc}". Unterstützt: ${Object.keys(CONSUMER_CLIENT_IDS).join(', ')}`);
|
|
88
|
+
}
|
|
66
89
|
const verifier = b64url(crypto.randomBytes(64));
|
|
67
90
|
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
|
|
68
91
|
const state = b64url(crypto.randomBytes(16));
|
|
69
92
|
const nonce = b64url(crypto.randomBytes(16));
|
|
70
93
|
const params = new URLSearchParams({
|
|
71
|
-
client_id: clientId,
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
client_id: clientId,
|
|
95
|
+
response_type: 'code',
|
|
96
|
+
redirect_uri: REDIRECT_URI,
|
|
97
|
+
scope: OAUTH_SCOPE,
|
|
98
|
+
state,
|
|
99
|
+
nonce,
|
|
100
|
+
code_challenge: challenge,
|
|
101
|
+
code_challenge_method: 'S256',
|
|
74
102
|
});
|
|
75
103
|
return {
|
|
76
104
|
url: `https://prod.map.miele-iot.com/${cc}/authorize?${params}`,
|
|
@@ -78,69 +106,125 @@ function buildAuthorizeUrl(cc) {
|
|
|
78
106
|
};
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* `code` aus miele://oauth2-code/?code=...&state=... extrahieren (mit state-Prüfung).
|
|
111
|
+
*
|
|
112
|
+
* @param redirectUrl
|
|
113
|
+
* @param expectedState
|
|
114
|
+
*/
|
|
82
115
|
function parseRedirectUrl(redirectUrl, expectedState) {
|
|
83
116
|
// miele:// ist kein von URL() parsebares Schema in allen Node-Versionen → manuell.
|
|
84
117
|
const q = redirectUrl.includes('?') ? redirectUrl.slice(redirectUrl.indexOf('?') + 1) : '';
|
|
85
118
|
const parsed = new URLSearchParams(q);
|
|
86
|
-
if (parsed.get('error'))
|
|
119
|
+
if (parsed.get('error')) {
|
|
120
|
+
throw new Error(`OAuth-Fehler: ${parsed.get('error')} ${parsed.get('error_description') || ''}`);
|
|
121
|
+
}
|
|
87
122
|
const code = parsed.get('code');
|
|
88
|
-
if (!code)
|
|
123
|
+
if (!code) {
|
|
124
|
+
throw new Error('Kein "code" in der eingefügten Redirect-URL.');
|
|
125
|
+
}
|
|
89
126
|
const state = parsed.get('state');
|
|
90
|
-
if (expectedState && state !== expectedState)
|
|
127
|
+
if (expectedState && state !== expectedState) {
|
|
128
|
+
throw new Error('state stimmt nicht überein (CSRF-Schutz) – bitte Login-URL neu erzeugen.');
|
|
129
|
+
}
|
|
91
130
|
return code;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* code → Token.
|
|
135
|
+
*
|
|
136
|
+
* @param challenge
|
|
137
|
+
* @param code
|
|
138
|
+
*/
|
|
95
139
|
async function exchangeCode(challenge, code) {
|
|
96
140
|
const body = new URLSearchParams({
|
|
97
|
-
grant_type: 'authorization_code',
|
|
98
|
-
|
|
141
|
+
grant_type: 'authorization_code',
|
|
142
|
+
code,
|
|
143
|
+
client_id: challenge.clientId,
|
|
144
|
+
redirect_uri: REDIRECT_URI,
|
|
145
|
+
code_verifier: challenge.verifier,
|
|
99
146
|
}).toString();
|
|
100
147
|
const res = await httpsRequest('POST', `https://prod.map.miele-iot.com/${challenge.cc}/token`, {
|
|
101
148
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
102
149
|
body,
|
|
103
150
|
});
|
|
104
151
|
let tok;
|
|
105
|
-
try {
|
|
106
|
-
|
|
152
|
+
try {
|
|
153
|
+
tok = JSON.parse(res.body);
|
|
154
|
+
} catch {
|
|
155
|
+
throw new Error(`Nicht-JSON-Token-Antwort: ${res.body.slice(0, 200)}`);
|
|
156
|
+
}
|
|
157
|
+
if (tok.error) {
|
|
158
|
+
throw new Error(`Token-Endpoint: ${tok.error} ${tok.error_description || ''}`);
|
|
159
|
+
}
|
|
107
160
|
return tok;
|
|
108
161
|
}
|
|
109
162
|
|
|
110
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Access-Token erneuern (refresh_token kann rotieren).
|
|
165
|
+
*
|
|
166
|
+
* @param cc
|
|
167
|
+
* @param refreshToken
|
|
168
|
+
*/
|
|
111
169
|
async function refreshAccessToken(cc, refreshToken) {
|
|
112
170
|
cc = String(cc).toLowerCase();
|
|
113
171
|
const body = new URLSearchParams({
|
|
114
|
-
grant_type: 'refresh_token',
|
|
172
|
+
grant_type: 'refresh_token',
|
|
173
|
+
refresh_token: refreshToken,
|
|
174
|
+
client_id: CONSUMER_CLIENT_IDS[cc],
|
|
115
175
|
}).toString();
|
|
116
176
|
const res = await httpsRequest('POST', `https://prod.map.miele-iot.com/${cc}/token`, {
|
|
117
177
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
118
178
|
body,
|
|
119
179
|
});
|
|
120
180
|
const tok = JSON.parse(res.body);
|
|
121
|
-
if (tok.error)
|
|
181
|
+
if (tok.error) {
|
|
182
|
+
throw new Error(`Refresh fehlgeschlagen: ${tok.error} ${tok.error_description || ''}`);
|
|
183
|
+
}
|
|
122
184
|
return tok;
|
|
123
185
|
}
|
|
124
186
|
|
|
125
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* GET /V2/GroupKeyId/ → { groupId, groupKey, devices }.
|
|
189
|
+
*
|
|
190
|
+
* @param accessToken
|
|
191
|
+
* @param region
|
|
192
|
+
*/
|
|
126
193
|
async function fetchGroupKey(accessToken, region = 'EU') {
|
|
127
194
|
const host = REST_HOST_BY_REGION[String(region).toUpperCase()];
|
|
128
|
-
if (!host)
|
|
195
|
+
if (!host) {
|
|
196
|
+
throw new Error(`Unbekannte Region "${region}"`);
|
|
197
|
+
}
|
|
129
198
|
const res = await httpsRequest('GET', `https://${host}/V2/GroupKeyId/`, {
|
|
130
199
|
headers: {
|
|
131
|
-
Authorization: `Bearer ${accessToken}`,
|
|
132
|
-
|
|
200
|
+
Authorization: `Bearer ${accessToken}`,
|
|
201
|
+
Accept: 'application/json',
|
|
202
|
+
'Accept-Language': 'de-DE',
|
|
203
|
+
'User-Agent': 'Miele@mobile 2.3.3 Android',
|
|
133
204
|
},
|
|
134
205
|
});
|
|
135
|
-
if (res.status === 403)
|
|
136
|
-
|
|
206
|
+
if (res.status === 403) {
|
|
207
|
+
throw new Error(`GroupKeyId → 403 (Token ohne mcs-Scope?). ${res.body.slice(0, 150)}`);
|
|
208
|
+
}
|
|
209
|
+
if (res.status !== 200) {
|
|
210
|
+
throw new Error(`GroupKeyId → HTTP ${res.status}: ${res.body.slice(0, 150)}`);
|
|
211
|
+
}
|
|
137
212
|
const groups = JSON.parse(res.body);
|
|
138
|
-
if (!groups || !groups.length)
|
|
213
|
+
if (!groups || !groups.length) {
|
|
214
|
+
throw new Error('Kein Haushalt zurückgegeben – Konto ohne gekoppelte Geräte?');
|
|
215
|
+
}
|
|
139
216
|
const g = groups[0];
|
|
140
217
|
return { groupId: g.groupId, groupKey: g.groupKey, devices: g.devices || [] };
|
|
141
218
|
}
|
|
142
219
|
|
|
143
220
|
module.exports = {
|
|
144
|
-
CONSUMER_CLIENT_IDS,
|
|
145
|
-
|
|
221
|
+
CONSUMER_CLIENT_IDS,
|
|
222
|
+
REDIRECT_URI,
|
|
223
|
+
OAUTH_SCOPE,
|
|
224
|
+
REST_HOST_BY_REGION,
|
|
225
|
+
buildAuthorizeUrl,
|
|
226
|
+
parseRedirectUrl,
|
|
227
|
+
exchangeCode,
|
|
228
|
+
refreshAccessToken,
|
|
229
|
+
fetchGroupKey,
|
|
146
230
|
};
|