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 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.38",
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.38": {
56
- "en": "Object IDs are now consistently English. The diagnostics channel was named \"sammlung\" and carried German datapoint names throughout, plus four German ones in the otherwise English \"history\" channel - 57 of 446 objects in total. On first start the adapter moves every existing value to its new ID and removes the old datapoints; collected data is not lost. Recorded history stays in the history adapter under the old ID. Anyone using the old IDs in their own scripts must follow suit: \"sammlung\" became \"collection\", \"befund\" became \"finding\", \"laufendSeit\" became \"runningSince\".",
57
- "de": "Die Objekt-IDs sind jetzt durchgängig englisch. Der Diagnosekanal hieß \"sammlung\" und trug ausschließlich deutsche Datenpunktnamen, dazu vier deutsche im sonst englischen Kanal \"history\" - zusammen 57 von 446 Objekten. Beim ersten Start zieht der Adapter alle vorhandenen Werte an ihre neuen IDs um und entfernt die alten Punkte; gesammelte Daten gehen dabei nicht verloren. Aufgezeichnete Verläufe bleiben im History-Adapter unter der alten ID stehen. Wer die alten IDs in eigenen Skripten benutzt, muss sie nachziehen: aus \"sammlung\" wurde \"collection\", aus \"befund\" wurde \"finding\", aus \"laufendSeit\" wurde \"runningSince\"."
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.37": {
60
- "en": "Device internals as datapoints: the adapter now carries the field tables of every leaf documented by the public projects MieleRESTServer and ha-miele-at-lan, including those for ovens, coffee machines and the communication module. A datapoint is only created when the appliance actually delivers the field. Fix for the value wrappers: for Generic fields the adapter read the minimum instead of the current value, leaving seven eco-leaf fields permanently at 0 (the target temperature among them). Water now comes from the appliance's own EcoFeedback first, with the impulse count as the fallback. The collector records the final state of every answering leaf at the end of a programme, and the CSV export lists them as columns. The leaf scan also covers unit 14. Plain text on the raw values (status, programType, programPhase, programId), descriptions on every writable state, the admin split into Appliances and Polling & values with 18 text blocks condensed into field help. The diagnostic branch is only created when the collection is on. The CSV now carries start, end and difference per leaf field; a second file names the match per field.",
61
- "de": "Geräteinterne Messwerte als Datenpunkte: Der Adapter kennt jetzt die Feldtabellen aller Leafs aus den öffentlichen Projekten MieleRESTServer und ha-miele-at-lan - auch die für Backofen, Kaffeevollautomat und Kommunikationsmodul. Angelegt wird ein Datenpunkt aber nur, wenn das Gerät das Feld tatsächlich liefert. Korrektur an den Wertehüllen: Bei Generic-Feldern las der Adapter das Minimum statt des Istwerts, weshalb sieben Felder des Eco-Leaf dauerhaft auf 0 standen (u. a. die Zieltemperatur). Wasser kommt jetzt vorrangig aus dem EcoFeedback des Geräts, die Impulszählung ist der Rückfall. Der Datensammler schreibt am Programmende die Schlussstände aller antwortenden Leafs mit; die CSV führt sie als eigene Spalten. Der Leaf-Scan durchsucht zusätzlich Unit 14. Klartext direkt am Rohwert (status, programType, programPhase, programId), Beschreibungen an allen beschreibbaren Datenpunkten, Admin in Geräte und Abfrage & Werte geteilt und 18 Textblöcke zu Feldhilfen eingedampft. Der Diagnosezweig entsteht nur noch bei eingeschalteter Sammlung. Die CSV führt je Leaf-Feld Start, Ende und Differenz; eine zweite Datei nennt je Feld die Übereinstimmung mit dem Vergleichswert."
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.36": {
64
- "en": "Energy and water: water consumption is read from the correct DOP2 field (field 21, confirmed within 0.5% against the cloud); final readings of short programs are no longer lost; total operating hours added. New data collection with field analysis, an ongoing check of the field mapping and a leaf scan for model-specific fields. Admin translations completed, configurable intervals capped at runtime, all data point names translated into eleven languages.",
65
- "de": "Energie und Wasser: Der Wasserverbrauch wird aus dem richtigen DOP2-Feld gelesen (Feld 21, gegen die Cloud auf 0,5 % bestätigt); die letzten Werte kurzer Programme gehen nicht mehr verloren; Betriebsstunden gesamt ergänzt. Neue Datensammlung mit Feldanalyse, laufender Kontrolle der Feldzuordnung und einem Leaf-Scan für modellspezifische Felder. Admin-Übersetzungen vervollständigt, einstellbare Intervalle zur Laufzeit begrenzt, alle Datenpunktnamen in elf Sprachen übersetzt.",
66
- "ru": "Энергия и вода: расход воды считывается из правильного поля DOP2 (поле 21, подтверждено с точностью 0,5 % по данным облака); последние показания коротких программ больше не теряются; добавлено общее время работы. Новый сбор данных с анализом полей, постоянной проверкой сопоставления полей и сканированием leaf для полей конкретных моделей. Переводы интерфейса администратора дополнены, настраиваемые интервалы ограничены во время работы, все названия точек данных переведены на одиннадцать языков.",
67
- "pt": "Energia e água: o consumo de água é lido do campo DOP2 correto (campo 21, confirmado com 0,5 % de precisão face à cloud); as últimas leituras de programas curtos já não se perdem; adicionadas as horas totais de funcionamento. Nova recolha de dados com análise de campos, verificação contínua do mapeamento de campos e uma análise de leafs para campos específicos do modelo. Traduções do admin concluídas, intervalos configuráveis limitados em tempo de execução, todos os nomes de pontos de dados traduzidos para onze idiomas.",
68
- "nl": "Energie en water: het waterverbruik wordt uit het juiste DOP2-veld gelezen (veld 21, tot op 0,5 % bevestigd tegen de cloud); de laatste waarden van korte programma's gaan niet meer verloren; totaal aantal bedrijfsuren toegevoegd. Nieuwe gegevensverzameling met veldanalyse, doorlopende controle van de veldtoewijzing en een leaf-scan voor modelspecifieke velden. Admin-vertalingen voltooid, instelbare intervallen tijdens runtime begrensd, alle datapuntnamen in elf talen vertaald.",
69
- "fr": "Énergie et eau : la consommation d'eau est lue dans le bon champ DOP2 (champ 21, confirmé à 0,5 % près par rapport au cloud) ; les dernières valeurs des programmes courts ne sont plus perdues ; ajout des heures de fonctionnement totales. Nouvelle collecte de données avec analyse des champs, contrôle continu du mappage des champs et un scan des leafs pour les champs propres au modèle. Traductions de l'admin complétées, intervalles configurables limités à l'exécution, tous les noms de points de données traduits en onze langues.",
70
- "it": "Energia e acqua: il consumo d'acqua viene letto dal campo DOP2 corretto (campo 21, confermato entro lo 0,5 % rispetto al cloud); gli ultimi valori dei programmi brevi non vanno più persi; aggiunte le ore di funzionamento totali. Nuova raccolta dati con analisi dei campi, controllo continuo della mappatura dei campi e una scansione dei leaf per i campi specifici del modello. Traduzioni dell'admin completate, intervalli configurabili limitati in fase di esecuzione, tutti i nomi dei punti dati tradotti in undici lingue.",
71
- "es": "Energía y agua: el consumo de agua se lee del campo DOP2 correcto (campo 21, confirmado con un 0,5 % de precisión frente a la nube); los últimos valores de los programas cortos ya no se pierden; se añaden las horas de funcionamiento totales. Nueva recopilación de datos con análisis de campos, control continuo de la asignación de campos y un escaneo de leafs para campos específicos del modelo. Traducciones del admin completadas, intervalos configurables limitados en tiempo de ejecución, todos los nombres de puntos de datos traducidos a once idiomas.",
72
- "pl": "Energia i woda: zużycie wody jest odczytywane z właściwego pola DOP2 (pole 21, potwierdzone z dokładnością 0,5 % względem chmury); ostatnie odczyty krótkich programów nie są już tracone; dodano łączną liczbę godzin pracy. Nowe zbieranie danych z analizą pól, bieżącą kontrolą przypisania pól i skanowaniem leafów dla pól specyficznych dla modelu. Uzupełniono tłumaczenia panelu admin, konfigurowalne interwały są ograniczane w czasie działania, wszystkie nazwy punktów danych przetłumaczono na jedenaście języków.",
73
- "uk": "Енергія та вода: витрата води зчитується з правильного поля DOP2 (поле 21, підтверджено з точністю 0,5 % щодо хмари); останні показники коротких програм більше не втрачаються; додано загальну кількість мотогодин. Новий збір даних з аналізом полів, постійною перевіркою зіставлення полів та скануванням leaf для полів конкретних моделей. Переклади адмін-панелі доповнено, налаштовувані інтервали обмежуються під час роботи, усі назви точок даних перекладено одинадцятьма мовами.",
74
- "zh-cn": "能耗与用水:用水量从正确的 DOP2 字段读取(字段 21,与云端对比误差在 0.5% 以内);短程序的最终读数不再丢失;新增总运行小时数。新的数据收集功能,包含字段分析、字段映射的持续校验,以及针对特定型号字段的 Leaf 扫描。完善管理界面翻译,可配置的间隔在运行时受到限制,所有数据点名称均已翻译为十一种语言。"
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
- { host: this.host, port: 80, method, path: `/${resource}`, headers,
62
- timeout: timeoutMs || this.timeout },
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) req.write(sendBuf);
100
+ if (sendBuf) {
101
+ req.write(sendBuf);
102
+ }
78
103
  req.end();
79
104
  });
80
105
  }
81
106
 
82
- /** Signierter GET, entschlüsselt & als JSON geparst. */
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) return null;
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) throw new Error(`GET /${resource}: kein X-Signature-Header`);
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
- /** Signierter PUT mit verschlüsseltem Body. Gibt Statuscode zurück. */
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
- /** Signierter POST mit verschlüsseltem Body. Gibt {status, location} zurück. */
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', ch: 'V52nWiniHyVotglJKplSXnX8',
22
- cz: 'npoAzuJP6okjvJ0NqUq9i5Rv', de: 'UJgKOxacIul2BcPJAzrQE6p0',
23
- dk: 'xWgykqRQSa9THqOXWfzZbxsH', es: 'D0Q4NPBR9dwP2EjX4E0_CtHE',
24
- fr: 'SOiiE3R4tSD0VxYYBvB8Pi_J', gb: 'WigtLzKGJE1Wg6yeZUECV8-P',
25
- hr: 'HD4OUUQYAw_5DtVFSe4-rYzR', hu: '2mm2yscHPGJ4tJCVjd6mp-to',
26
- it: 'ARQyaYB0ZxLxJ1SJcjJgctuV', nl: '7ItTbQXQ1wthDOue9jvBQ7Iz',
27
- pl: 'jWbgLScpvIuqjUoYvf1jS-Is', pt: '5ZVD-CuJvpG4YpCO9pQhtrGQ',
28
- se: '3Mm7m1gD1eU_sUh8yxmShL6S', si: 'UTyhG21RchpI8FPbNeb1vFg1',
29
- sk: 'pGeafLwcC1_BCLr8DRTCVxSt', us: 'HpsWh2gzgKqRBduPpkZ4Yui9',
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', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks).toString('utf8') }));
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) req.write(body);
71
+ if (body) {
72
+ req.write(body);
73
+ }
57
74
  req.end();
58
75
  });
59
76
  }
60
77
 
61
- /** Autorisierungs-URL + PKCE-Challenge erzeugen. */
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) throw new Error(`Unbekanntes Land "${cc}". Unterstützt: ${Object.keys(CONSUMER_CLIENT_IDS).join(', ')}`);
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, response_type: 'code', redirect_uri: REDIRECT_URI,
72
- scope: OAUTH_SCOPE, state, nonce,
73
- code_challenge: challenge, code_challenge_method: 'S256',
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
- /** `code` aus miele://oauth2-code/?code=...&state=... extrahieren (mit state-Prüfung). */
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')) throw new Error(`OAuth-Fehler: ${parsed.get('error')} ${parsed.get('error_description') || ''}`);
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) throw new Error('Kein "code" in der eingefügten Redirect-URL.');
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) throw new Error('state stimmt nicht überein (CSRF-Schutz) – bitte Login-URL neu erzeugen.');
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
- /** code → Token. */
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', code, client_id: challenge.clientId,
98
- redirect_uri: REDIRECT_URI, code_verifier: challenge.verifier,
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 { tok = JSON.parse(res.body); } catch { throw new Error(`Nicht-JSON-Token-Antwort: ${res.body.slice(0, 200)}`); }
106
- if (tok.error) throw new Error(`Token-Endpoint: ${tok.error} ${tok.error_description || ''}`);
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
- /** Access-Token erneuern (refresh_token kann rotieren). */
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', refresh_token: refreshToken, client_id: CONSUMER_CLIENT_IDS[cc],
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) throw new Error(`Refresh fehlgeschlagen: ${tok.error} ${tok.error_description || ''}`);
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
- /** GET /V2/GroupKeyId/ → { groupId, groupKey, devices }. */
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) throw new Error(`Unbekannte Region "${region}"`);
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}`, Accept: 'application/json',
132
- 'Accept-Language': 'de-DE', 'User-Agent': 'Miele@mobile 2.3.3 Android',
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) throw new Error(`GroupKeyId → 403 (Token ohne mcs-Scope?). ${res.body.slice(0, 150)}`);
136
- if (res.status !== 200) throw new Error(`GroupKeyId → HTTP ${res.status}: ${res.body.slice(0, 150)}`);
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) throw new Error('Kein Haushalt zurückgegeben – Konto ohne gekoppelte Geräte?');
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, REDIRECT_URI, OAUTH_SCOPE, REST_HOST_BY_REGION,
145
- buildAuthorizeUrl, parseRedirectUrl, exchangeCode, refreshAccessToken, fetchGroupKey,
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
  };