iobroker.miele-local 0.2.0

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.
@@ -0,0 +1,237 @@
1
+ 'use strict';
2
+
3
+ // Deutsche Übersetzungen der Miele-Enums (an mielecloudservice-Begriffe angelehnt:
4
+ // in_use→"In Betrieb", off→"Aus", main_wash→"Waschen"). Fallback = englischer Schlüssel.
5
+
6
+ const StateStatusDe = {
7
+ 0: 'Reserviert', 1: 'Aus', 2: 'Ein', 3: 'Programmiert', 4: 'Warten auf Start',
8
+ 5: 'In Betrieb', 6: 'Pause', 7: 'Programm beendet', 8: 'Fehler', 9: 'Programm unterbrochen',
9
+ 10: 'Bereit', 11: 'Spülstopp', 12: 'Service', 13: 'SuperFrost', 14: 'SuperKühlen',
10
+ 15: 'Aufheizen', 146: 'SuperKühlen/SuperFrost', 147: 'Selbstreinigung', 255: 'Nicht verbunden',
11
+ };
12
+
13
+ const StateProgramTypeDe = {
14
+ 0: 'Normalbetrieb', 1: 'Eigenes Programm', 2: 'Automatikprogramm', 3: 'Automatikprogramm', 4: 'Wartungsprogramm',
15
+ };
16
+
17
+ const StateDryingStepDe = {
18
+ 0: 'Extra trocken', 1: 'Normal plus', 2: 'Normal', 3: 'Leicht trocken', 4: 'Handtuchtrocken',
19
+ 5: 'Bügeltrocken', 6: 'Schranktrocken', 7: 'Schranktrocken plus',
20
+ };
21
+
22
+ const ProgramPhaseWashingMachineDe = {
23
+ 0: '', 256: '', 257: 'Vorwäsche', 258: 'Einweichen', 259: 'Vorwäsche', 260: 'Waschen',
24
+ 261: 'Spülen', 262: 'Spülstopp', 263: 'Reinigung', 264: 'Abkühlen', 265: 'Abpumpen',
25
+ 266: 'Schleudern', 267: 'Knitterschutz', 268: 'Ende', 269: 'Belüften', 270: 'Stärkestopp',
26
+ 271: 'Auffrischen/Befeuchten', 272: 'Dampfglätten', 279: 'Hygiene', 280: 'Trocknen',
27
+ 285: 'Desinfizieren', 295: 'Dampfglätten', 11004: 'Waschen', 11005: 'Spülen', 11010: 'Schleudern',
28
+ 11012: 'Ende', 11029: 'Knitterschutz', 11044: 'Automatikstart', 11047: 'FlexLoad aktiv', 65535: '',
29
+ };
30
+
31
+ const ProgramPhaseDishwasherDe = {
32
+ 0: '', 1792: '', 1793: 'Regenerieren', 1794: 'Vorspülen', 1795: 'Reinigen', 1796: 'Spülen',
33
+ 1797: 'Zwischenspülen', 1798: 'Klarspülen', 1799: 'Trocknen', 1800: 'Ende', 1801: 'Vorspülen', 65535: '',
34
+ };
35
+
36
+ const ProgramPhaseOvenDe = {
37
+ 0: '', 3073: 'Aufheizen', 3074: 'In Betrieb', 3078: 'Beendet', 3080: 'Anbraten',
38
+ 3081: 'Braten', 3084: 'Energiesparen', 3099: 'Vorheizen', 65535: '',
39
+ };
40
+
41
+ // Programmnamen (Schlüssel = englischer Enum-Wert aus enums.js → Deutsch).
42
+ const ProgramNameDe = {
43
+ // gemeinsame
44
+ no_program: '',
45
+ // Waschmaschine
46
+ cottons: 'Baumwolle', minimum_iron: 'Pflegeleicht', easy_care: 'Pflegeleicht',
47
+ delicates: 'Feinwäsche', woollens: 'Wolle', silks: 'Seide', starch: 'Stärken',
48
+ rinse: 'Spülen', drain_spin: 'Abpumpen/Schleudern', curtains: 'Gardinen',
49
+ shirts: 'Oberhemden', denim: 'Jeans', proofing: 'Imprägnieren', sportswear: 'Sportbekleidung',
50
+ automatic_plus: 'Automatic Plus', outerwear: 'Outdoor', pillows: 'Kopfkissen',
51
+ dark_garments: 'Dunkles/Jeans', dark_jeans: 'Dunkles/Jeans', separate_rinse_starch: 'Separates Spülen/Stärken',
52
+ first_wash: 'Erstwäsche', cottons_hygiene: 'Baumwolle Hygiene', cottons_eco: 'Baumwolle Eco',
53
+ trainers: 'Sportschuhe', trainers_refresh: 'Sportschuhe auffrischen', clean_machine: 'Maschine reinigen',
54
+ down_duvets: 'Daunen', down_filled_items: 'Daunen', express_20: 'Express 20',
55
+ quick_power_wash: 'QuickPowerWash', eco_40_60: 'Eco 40-60', bed_linen: 'Bettwäsche',
56
+ outdoor_garments: 'Outdoor', pre_ironing: 'Vorbügeln', cottonrepair: 'CottonRepair',
57
+ smartmatic: 'SmartMatic', stuffed_toys: 'Kuscheltiere', game_pieces: 'Spielsachen', powerfresh: 'PowerFresh',
58
+ // Spülmaschine
59
+ intensive: 'Intensiv', maintenance: 'Maschinenpflege', eco: 'Eco', automatic: 'Automatic',
60
+ solar_save: 'Solarspar', gentle: 'Schonen', extra_quiet: 'Extra leise', hygiene: 'Hygiene',
61
+ pasta_paela: 'Pasta/Paella', tall_items: 'Große Teile', glasses_warm: 'Gläser warm',
62
+ normal: 'Normal', comfort_wash: 'ComfortWash', comfort_wash_plus: 'ComfortWash Plus',
63
+ power_wash: 'PowerWash', rinse_salt: 'Spülen/Salz',
64
+ // Backofen – Betriebsarten
65
+ conventional_heat: 'Ober-/Unterhitze', top_heat: 'Oberhitze', bottom_heat: 'Unterhitze',
66
+ fan_plus: 'Umluft', eco_fan_heat: 'Eco Umluft', fan_grill: 'Umluftgrill', grill: 'Grill',
67
+ full_grill: 'Großflächengrill', economy_grill: 'Spargrill', intensive_bake: 'Intensivbacken',
68
+ auto_roast: 'Automatikbraten', low_temperature_cooking: 'Niedertemperaturgaren',
69
+ moisture_plus_conventional_heat: 'Feuchtigkeit plus – Ober-/Unterhitze',
70
+ moisture_plus_fan_plus: 'Feuchtigkeit plus – Umluft',
71
+ moisture_plus_auto_roast: 'Feuchtigkeit plus – Automatikbraten',
72
+ moisture_plus_intensive_bake: 'Feuchtigkeit plus – Intensivbacken',
73
+ microwave: 'Mikrowelle', microwave_fan_plus: 'Mikrowelle – Umluft',
74
+ microwave_grill: 'Mikrowelle – Grill', microwave_fan_grill: 'Mikrowelle – Umluftgrill',
75
+ microwave_auto_roast: 'Mikrowelle – Automatikbraten', quick_microwave: 'Schnell-Mikrowelle',
76
+ steam_cooking: 'Dampfgaren', steam_bake: 'Dampfbacken', defrost: 'Auftauen',
77
+ defrost_meat: 'Fleisch auftauen', defrost_vegetables: 'Gemüse auftauen',
78
+ keeping_warm: 'Warmhalten', heat_crockery: 'Geschirr wärmen',
79
+ heating_bakes_gratins: 'Aufläufe/Gratins', heating_vegetables: 'Gemüse erwärmen',
80
+ evaporate_water: 'Wasser verdampfen', descale: 'Entkalken', pyrolytic: 'Pyrolyse',
81
+ drying: 'Trocknen', prove_dough: 'Teig gehen lassen', prove_15_min: 'Teig gehen (15 min)',
82
+ prove_30_min: 'Teig gehen (30 min)', prove_45_min: 'Teig gehen (45 min)', popcorn: 'Popcorn',
83
+ shabbat_program: 'Schabbat-Programm', yom_tov: 'Jom Tov',
84
+ // Backofen – eigene Programme
85
+ custom_program_1: 'Eigenes Programm 1', custom_program_2: 'Eigenes Programm 2',
86
+ custom_program_3: 'Eigenes Programm 3', custom_program_4: 'Eigenes Programm 4',
87
+ custom_program_5: 'Eigenes Programm 5', custom_program_6: 'Eigenes Programm 6',
88
+ custom_program_7: 'Eigenes Programm 7', custom_program_8: 'Eigenes Programm 8',
89
+ custom_program_9: 'Eigenes Programm 9', custom_program_10: 'Eigenes Programm 10',
90
+ custom_program_11: 'Eigenes Programm 11', custom_program_12: 'Eigenes Programm 12',
91
+ custom_program_13: 'Eigenes Programm 13', custom_program_14: 'Eigenes Programm 14',
92
+ custom_program_15: 'Eigenes Programm 15', custom_program_16: 'Eigenes Programm 16',
93
+ custom_program_17: 'Eigenes Programm 17', custom_program_18: 'Eigenes Programm 18',
94
+ custom_program_19: 'Eigenes Programm 19', custom_program_20: 'Eigenes Programm 20',
95
+ // Backofen – Brot & Brötchen
96
+ white_bread_baking_tin: 'Weißbrot (Kastenform)', white_bread_on_tray: 'Weißbrot (Blech)',
97
+ white_rolls: 'Weiße Brötchen', multigrain_rolls: 'Mehrkornbrötchen', rye_rolls: 'Roggenbrötchen',
98
+ mixed_rye_bread: 'Roggenmischbrot', dark_mixed_grain_bread: 'Dunkles Mehrkornbrot',
99
+ spelt_bread: 'Dinkelbrot', walnut_bread: 'Walnussbrot', seeded_loaf: 'Saatenbrot',
100
+ flat_bread: 'Fladenbrot', baguettes: 'Baguette', plaited_loaf: 'Hefezopf',
101
+ plaited_swiss_loaf: 'Schweizer Zopf', swiss_farmhouse_bread: 'Schweizer Bauernbrot',
102
+ tiger_bread: 'Tigerbrot', ginger_loaf: 'Ingwerkuchen', stollen: 'Stollen',
103
+ // Backofen – Kuchen & Gebäck
104
+ apple_pie: 'Apfelkuchen', apple_sponge: 'Apfel-Biskuit', butter_cake: 'Butterkuchen',
105
+ marble_cake: 'Marmorkuchen', madeira_cake: 'Sandkuchen', belgian_sponge_cake: 'Belgischer Biskuit',
106
+ swiss_roll: 'Biskuitrolle', sponge_base: 'Biskuitboden', sachertorte: 'Sachertorte',
107
+ fruit_streusel_cake: 'Obst-Streuselkuchen', fruit_flan_puff_pastry: 'Obstkuchen (Blätterteig)',
108
+ fruit_flan_short_crust_pastry: 'Obstkuchen (Mürbeteig)',
109
+ savoury_flan_puff_pastry: 'Herzhafter Kuchen (Blätterteig)',
110
+ savoury_flan_short_crust_pastry: 'Herzhafter Kuchen (Mürbeteig)',
111
+ chocolate_hazlenut_cake_one_large: 'Schoko-Nuss-Kuchen (groß)',
112
+ chocolate_hazlenut_cake_several_small: 'Schoko-Nuss-Kuchen (klein)',
113
+ viennese_apple_strudel: 'Wiener Apfelstrudel', lemon_meringue_pie: 'Zitronen-Baiser-Torte',
114
+ quiche_lorraine: 'Quiche Lorraine', cheese_souffle: 'Käsesoufflé', choux_buns: 'Windbeutel',
115
+ springform_tin_15cm: 'Springform 15 cm', springform_tin_20cm: 'Springform 20 cm',
116
+ springform_tin_25cm: 'Springform 25 cm',
117
+ // Backofen – Kekse & Kleingebäck
118
+ vanilla_biscuits_1_tray: 'Vanillekipferl (1 Blech)', vanilla_biscuits_2_trays: 'Vanillekipferl (2 Bleche)',
119
+ almond_macaroons_1_tray: 'Mandelmakronen (1 Blech)', almond_macaroons_2_trays: 'Mandelmakronen (2 Bleche)',
120
+ biscuits_short_crust_pastry_1_tray: 'Mürbeteigplätzchen (1 Blech)',
121
+ biscuits_short_crust_pastry_2_trays: 'Mürbeteigplätzchen (2 Bleche)',
122
+ drop_cookies_1_tray: 'Spritzgebäck (1 Blech)', drop_cookies_2_trays: 'Spritzgebäck (2 Bleche)',
123
+ linzer_augen_1_tray: 'Linzer Augen (1 Blech)', linzer_augen_2_trays: 'Linzer Augen (2 Bleche)',
124
+ baiser_one_large: 'Baiser (groß)', baiser_several_small: 'Baiser (mehrere kleine)',
125
+ blueberry_muffins: 'Blaubeer-Muffins', walnut_muffins: 'Walnuss-Muffins',
126
+ // Backofen – Pizza & Flammkuchen
127
+ pizza_yeast_dough_baking_tray: 'Pizza Hefeteig (Blech)', pizza_yeast_dough_round_baking_tine: 'Pizza Hefeteig (rund)',
128
+ pizza_oil_cheese_dough_baking_tray: 'Pizza Öl-Käse-Teig (Blech)',
129
+ pizza_oil_cheese_dough_round_baking_tine: 'Pizza Öl-Käse-Teig (rund)', tart_flambe: 'Flammkuchen',
130
+ // Backofen – Fleisch (Rind/Kalb)
131
+ roast_beef_roast: 'Roastbeef braten', roast_beef_low_temperature_cooking: 'Roastbeef (Niedertemperatur)',
132
+ beef_fillet_roast: 'Rinderfilet braten', beef_fillet_low_temperature_cooking: 'Rinderfilet (Niedertemperatur)',
133
+ braised_beef: 'Schmorbraten (Rind)', beef_wellington: 'Beef Wellington', beef_hash: 'Rinderhackgericht',
134
+ meat_loaf: 'Hackbraten', braised_veal: 'Schmorbraten (Kalb)', veal_fillet_roast: 'Kalbsfilet braten',
135
+ veal_fillet_low_temperature_cooking: 'Kalbsfilet (Niedertemperatur)', saddle_of_veal_roast: 'Kalbsrücken braten',
136
+ saddle_of_veal_low_temperature_cooking: 'Kalbsrücken (Niedertemperatur)', veal_knuckle: 'Kalbshaxe',
137
+ // Backofen – Fleisch (Schwein/Lamm/Wild)
138
+ pork_fillet_roast: 'Schweinefilet braten', pork_fillet_low_temperature_cooking: 'Schweinefilet (Niedertemperatur)',
139
+ pork_belly: 'Schweinebauch', pork_with_crackling: 'Krustenbraten', ham_roast: 'Schinkenbraten',
140
+ pork_smoked_ribs_roast: 'Kasseler braten', pork_smoked_ribs_low_temperature_cooking: 'Kasseler (Niedertemperatur)',
141
+ leg_of_lamb: 'Lammkeule', saddle_of_lamb_roast: 'Lammrücken braten',
142
+ saddle_of_lamb_low_temperature_cooking: 'Lammrücken (Niedertemperatur)',
143
+ rack_of_lamb_with_vegetables: 'Lammkarree mit Gemüse', saddle_of_roebuck: 'Rehrücken',
144
+ saddle_of_venison: 'Hirschrücken', osso_buco: 'Osso Buco', rabbit: 'Kaninchen',
145
+ // Backofen – Geflügel
146
+ chicken_whole: 'Hähnchen (ganz)', chicken_thighs: 'Hähnchenschenkel', duck: 'Ente',
147
+ goose_stuffed: 'Gans (gefüllt)', goose_unstuffed: 'Gans (ungefüllt)', turkey_whole: 'Pute (ganz)',
148
+ turkey_drumsticks: 'Putenkeulen',
149
+ // Backofen – Fisch
150
+ salmon_fillet: 'Lachsfilet', salmon_trout: 'Lachsforelle', trout: 'Forelle', carp: 'Karpfen',
151
+ pikeperch_fillet_with_vegetables: 'Zanderfilet mit Gemüse',
152
+ // Backofen – Beilagen & Sonstiges
153
+ potato_gratin: 'Kartoffelgratin', potato_cheese_gratin: 'Kartoffel-Käse-Gratin',
154
+ yorkshire_pudding: 'Yorkshire Pudding',
155
+ // Trockner
156
+ basket_program: 'Korbprogramm', cool_air: 'Kaltluft', downs_duvets: 'Daunen', express: 'Express',
157
+ gentle_denim: 'Schonend Jeans', gentle_smoothing: 'Schonend Glätten', large_pillows: 'Große Kopfkissen',
158
+ pillows_sanitize: 'Kopfkissen Hygiene', quick_hygiene: 'Schnell Hygiene', quick_power_dry: 'QuickPowerDry',
159
+ silks_handcare: 'Seide Handpflege', smoothing: 'Glätten', standard_pillows: 'Kopfkissen',
160
+ steam_smoothing: 'Dampfglätten', warm_air: 'Warmluft', woollens_handcare: 'Wolle Handpflege',
161
+ // Kaffeevollautomat
162
+ appliance_rinse: 'Gerätespülung', automatic_maintenance: 'Automatische Pflege', barista_assistant: 'Barista-Assistent',
163
+ black_tea: 'Schwarzer Tee', brewing_unit_degrease: 'Brüheinheit entfetten', cafe_au_lait: 'Café au Lait',
164
+ caffe_latte: 'Caffè Latte', cappuccino: 'Cappuccino', cappuccino_italiano: 'Cappuccino Italiano',
165
+ check_appliance: 'Gerät prüfen', coffee: 'Kaffee', coffee_pot: 'Kaffeekanne', descaling: 'Entkalken',
166
+ espresso: 'Espresso', espresso_macchiato: 'Espresso Macchiato', flat_white: 'Flat White',
167
+ fruit_tea: 'Früchtetee', green_tea: 'Grüner Tee', herbal_tea: 'Kräutertee', hot_milk: 'Heiße Milch',
168
+ hot_water: 'Heißwasser', intermediate_rinsing: 'Zwischenspülung', japanese_tea: 'Japanischer Tee',
169
+ latte_macchiato: 'Latte Macchiato', long_coffee: 'Langer Kaffee', milk_foam: 'Milchschaum',
170
+ milk_pipework_clean: 'Milchleitung reinigen', milk_pipework_rinse: 'Milchleitung spülen',
171
+ ristretto: 'Ristretto', very_hot_water: 'Sehr heißes Wasser', white_tea: 'Weißer Tee',
172
+ // Wärmeschublade
173
+ keep_warm: 'Warmhalten', slow_roasting: 'Niedertemperaturgaren',
174
+ warm_cups_glasses: 'Tassen/Gläser wärmen', warm_dishes_plates: 'Geschirr/Teller wärmen',
175
+ };
176
+
177
+ /**
178
+ * Deutsche Phasennamen, keyed nach ENGLISCHEM Enum-Wert (wie ProgramNameDe) - deckt so alle
179
+ * Gerätetypen auf einmal ab. Wird als Fallback nach den numerischen BY_TYPE_DE-Tabellen genutzt
180
+ * (siehe objects.js phaseTextDe), sodass bestehende Übersetzungen unveraendert bleiben.
181
+ */
182
+ const PhaseNameDe = {
183
+ not_running: '', program_running: 'In Betrieb', process_running: 'In Betrieb',
184
+ drying: 'Trocknen', finished: 'Ende', process_finished: 'Beendet', cooling_down: 'Abkühlen',
185
+ comfort_cooling: 'Komfortabkühlen', safety_cooling: 'Sicherheitsabkühlung', anti_crease: 'Knitterschutz',
186
+ normal: 'Normal', normal_plus: 'Normal plus', extra_dry: 'Extratrocken', slightly_dry: 'Leicht trocken',
187
+ hand_iron: 'Bügelfeucht', hand_iron_1: 'Handbügelfeucht 1', hand_iron_2: 'Handbügelfeucht 2',
188
+ machine_iron: 'Maschinenbügelfeucht', moisten: 'Befeuchten', thermo_spin: 'Thermoschleudern',
189
+ timed_drying: 'Zeitprogramm', warm_air: 'Warmluft', steam_smoothing: 'Dampfglätten', smoothing: 'Glätten',
190
+ rinse_out_lint: 'Flusen ausspülen', rinses: 'Spülen', rinse: 'Spülen', automatic_start: 'Automatikstart',
191
+ perfect_dry_active: 'PerfectDry aktiv',
192
+ // Kaffeevollautomat
193
+ heating_up: 'Aufheizen', espresso: 'Espresso', hot_milk: 'Heiße Milch', milk_foam: 'Milchschaum',
194
+ dispensing: 'Ausgabe', pre_brewing: 'Vorbrühen', grinding: 'Mahlen', second_espresso: 'Zweiter Espresso',
195
+ second_pre_brewing: 'Zweites Vorbrühen', second_grinding: 'Zweites Mahlen',
196
+ // Dampfgarer / Dampfbackofen
197
+ steam_reduction: 'Dampfreduktion', waiting_for_start: 'Warten auf Start', heating_up_phase: 'Aufheizphase',
198
+ searing: 'Anbraten', roasting: 'Braten', energy_save: 'Energiesparen', pre_heating: 'Vorheizen',
199
+ // Wärmeschublade
200
+ door_open: 'Tür offen', keeping_warm: 'Warmhalten',
201
+ };
202
+
203
+ /** Deutscher Phasenname aus englischem Enum-Wert; null wenn unbekannt (Fallback engl.). */
204
+ function phaseNameDe(enText) {
205
+ if (enText == null) return null;
206
+ return PhaseNameDe[enText] != null ? PhaseNameDe[enText] : null;
207
+ }
208
+
209
+ /** Deutscher Programmname aus englischem Enum-Wert; null wenn unbekannt (Fallback engl.). */
210
+ function programNameDe(enText) {
211
+ if (enText == null) return null;
212
+ return ProgramNameDe[enText] != null ? ProgramNameDe[enText] : null;
213
+ }
214
+
215
+ // deviceType → { statusDe, phaseDe, programDe? }
216
+ const BY_TYPE_DE = {
217
+ 1: { phase: ProgramPhaseWashingMachineDe }, // Waschmaschine
218
+ 2: { phase: ProgramPhaseWashingMachineDe }, // Trockner (nutzt DryingStep separat)
219
+ 24: { phase: ProgramPhaseWashingMachineDe }, // Waschtrockner
220
+ 7: { phase: ProgramPhaseDishwasherDe }, // Spülmaschine
221
+ 12: { phase: ProgramPhaseOvenDe }, // Backofen
222
+ 13: { phase: ProgramPhaseOvenDe },
223
+ };
224
+
225
+ function statusDe(v) { return StateStatusDe[v] != null ? StateStatusDe[v] : null; }
226
+ function programTypeDe(v) { return StateProgramTypeDe[v] != null ? StateProgramTypeDe[v] : null; }
227
+ function dryingStepDe(v) { return StateDryingStepDe[v] != null ? StateDryingStepDe[v] : null; }
228
+ function phaseDe(deviceType, v) {
229
+ const m = BY_TYPE_DE[deviceType];
230
+ if (m && m.phase && m.phase[v] != null) return m.phase[v];
231
+ return null;
232
+ }
233
+
234
+ module.exports = {
235
+ StateStatusDe, StateProgramTypeDe, StateDryingStepDe, ProgramNameDe, PhaseNameDe,
236
+ statusDe, programTypeDe, dryingStepDe, phaseDe, programNameDe, phaseNameDe,
237
+ };
package/lib/objects.js ADDED
@@ -0,0 +1,285 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Deklaratives Objektmodell: /State- und /Ident-Felder → ioBroker-States.
5
+ * Enthält Rollen, Typen, Einheiten und Dekodierfunktionen.
6
+ */
7
+
8
+ const enums = require('./enums');
9
+ const de = require('./enums_de');
10
+
11
+ const TEMP_SENTINEL = -32768; // "nicht verfügbar"
12
+
13
+ /** [h, min] → Gesamtminuten (null bei fehlend). */
14
+ function timeToMinutes(arr) {
15
+ if (!Array.isArray(arr) || arr.length < 2) return null;
16
+ return arr[0] * 60 + arr[1];
17
+ }
18
+
19
+ /** [h, min] → "H:MM"-String (wie mielecloudservice), null bei fehlend. */
20
+ function timeToHHMM(arr) {
21
+ if (!Array.isArray(arr) || arr.length < 2) return null;
22
+ return `${arr[0]}:${String(arr[1]).padStart(2, '0')}`;
23
+ }
24
+
25
+ // Deutsch mit englischem Fallback
26
+ function statusTextDe(v) { return de.statusDe(v) || enums.statusText(v); }
27
+ function programTypeTextDe(v) { return de.programTypeDe(v) || enums.programTypeText(v); }
28
+ function dryingStepTextDe(v) { return de.dryingStepDe(v) || enums.dryingStepText(v); }
29
+ function programTextDe(dt, v) { const en = enums.programText(dt, v); return de.programNameDe(en) || en; }
30
+ // Phase: erst numerische DE-Tabelle (Waschen/Spülen/Backofen), dann englisch-basierte DE-Map
31
+ // (deckt alle uebrigen Gerätetypen ab), sonst englischer Klartext.
32
+ function phaseTextDe(dt, v) { const en = enums.phaseText(dt, v); return de.phaseDe(dt, v) || de.phaseNameDe(en) || en; }
33
+
34
+ /** Miele-Temperatur (Hundertstel °C) → °C, Sentinel → null. */
35
+ function temp(v) {
36
+ if (v == null || v === TEMP_SENTINEL) return null;
37
+ return v / 100;
38
+ }
39
+
40
+ /**
41
+ * Zustands-Kanäle (Rohwert + optionaler Klartext) je /State-Feld.
42
+ * decode(value, ctx) → { states: [{sub, val}] }
43
+ * ctx = { deviceType }
44
+ */
45
+ const STATE_FIELDS = {
46
+ Status: {
47
+ states: [
48
+ { sub: 'status', role: 'value', type: 'number', name: 'Status (raw)' },
49
+ { sub: 'statusText', role: 'text', type: 'string', name: 'Status' },
50
+ ],
51
+ decode: (v) => [{ sub: 'status', val: v }, { sub: 'statusText', val: statusTextDe(v) }],
52
+ },
53
+ ProgramType: {
54
+ states: [
55
+ { sub: 'programType', role: 'value', type: 'number', name: 'Program type (raw)' },
56
+ { sub: 'programTypeText', role: 'text', type: 'string', name: 'Program type' },
57
+ ],
58
+ decode: (v) => [{ sub: 'programType', val: v }, { sub: 'programTypeText', val: programTypeTextDe(v) }],
59
+ },
60
+ ProgramID: {
61
+ states: [
62
+ { sub: 'programId', role: 'value', type: 'number', name: 'Program (raw)' },
63
+ { sub: 'programText', role: 'text', type: 'string', name: 'Program' },
64
+ ],
65
+ decode: (v, ctx) => [{ sub: 'programId', val: v }, { sub: 'programText', val: programTextDe(ctx.deviceType, v) }],
66
+ },
67
+ ProgramPhase: {
68
+ states: [
69
+ { sub: 'programPhase', role: 'value', type: 'number', name: 'Phase (raw)' },
70
+ { sub: 'programPhaseText', role: 'text', type: 'string', name: 'Phase' },
71
+ ],
72
+ decode: (v, ctx) => [{ sub: 'programPhase', val: v }, { sub: 'programPhaseText', val: phaseTextDe(ctx.deviceType, v) }],
73
+ },
74
+ RemainingTime: {
75
+ states: [
76
+ { sub: 'remainingMinutes', role: 'value.interval', type: 'number', unit: 'min', name: 'Remaining time' },
77
+ { sub: 'remainingHHMM', role: 'text', type: 'string', name: 'Remaining time (H:MM)' },
78
+ // Voraussichtliches Programmende (wie mielecloudservice.estimatedEndTime). Der Wert wird
79
+ // NICHT hier dekodiert (die Rohantwort kennt keine Uhrzeit), sondern in main.js aus
80
+ // "jetzt + Restzeit" berechnet - hier nur die Objektanlage.
81
+ { sub: 'estimatedEndTime', role: 'value.time', type: 'number', name: 'Estimated end time' },
82
+ { sub: 'estimatedEndTimeText', role: 'text', type: 'string', name: 'Estimated end time (HH:MM)' },
83
+ ],
84
+ decode: (v) => [{ sub: 'remainingMinutes', val: timeToMinutes(v) }, { sub: 'remainingHHMM', val: timeToHHMM(v) }],
85
+ },
86
+ ElapsedTime: {
87
+ states: [
88
+ { sub: 'elapsedMinutes', role: 'value.interval', type: 'number', unit: 'min', name: 'Elapsed time' },
89
+ { sub: 'elapsedHHMM', role: 'text', type: 'string', name: 'Elapsed time (H:MM)' },
90
+ ],
91
+ decode: (v) => [{ sub: 'elapsedMinutes', val: timeToMinutes(v) }, { sub: 'elapsedHHMM', val: timeToHHMM(v) }],
92
+ },
93
+ StartTime: {
94
+ states: [
95
+ { sub: 'startInMinutes', role: 'value.interval', type: 'number', unit: 'min', name: 'Start delay' },
96
+ { sub: 'startHHMM', role: 'text', type: 'string', name: 'Start delay (H:MM)' },
97
+ ],
98
+ decode: (v) => [{ sub: 'startInMinutes', val: timeToMinutes(v) }, { sub: 'startHHMM', val: timeToHHMM(v) }],
99
+ },
100
+ TargetTemperature: {
101
+ states: [
102
+ { sub: 'targetTemperature', role: 'value.temperature', type: 'number', unit: '°C', name: 'Target temperature' },
103
+ { sub: 'targetTemperatureZone2', role: 'value.temperature', type: 'number', unit: '°C', name: 'Target temperature zone 2' },
104
+ { sub: 'targetTemperatureZone3', role: 'value.temperature', type: 'number', unit: '°C', name: 'Target temperature zone 3' },
105
+ ],
106
+ decode: (v) => [
107
+ { sub: 'targetTemperature', val: temp(v && v[0]) },
108
+ { sub: 'targetTemperatureZone2', val: temp(v && v[1]) },
109
+ { sub: 'targetTemperatureZone3', val: temp(v && v[2]) },
110
+ ],
111
+ },
112
+ Temperature: {
113
+ states: [
114
+ { sub: 'temperature', role: 'value.temperature', type: 'number', unit: '°C', name: 'Temperature' },
115
+ { sub: 'temperatureZone2', role: 'value.temperature', type: 'number', unit: '°C', name: 'Temperature zone 2' },
116
+ { sub: 'temperatureZone3', role: 'value.temperature', type: 'number', unit: '°C', name: 'Temperature zone 3' },
117
+ ],
118
+ decode: (v) => [
119
+ { sub: 'temperature', val: temp(v && v[0]) },
120
+ { sub: 'temperatureZone2', val: temp(v && v[1]) },
121
+ { sub: 'temperatureZone3', val: temp(v && v[2]) },
122
+ ],
123
+ },
124
+ SignalInfo: {
125
+ states: [{ sub: 'signalInfo', role: 'indicator', type: 'boolean', name: 'Info signal' }],
126
+ decode: (v) => [{ sub: 'signalInfo', val: !!v }],
127
+ },
128
+ SignalFailure: {
129
+ states: [{ sub: 'signalFailure', role: 'indicator.maintenance', type: 'boolean', name: 'Failure signal' }],
130
+ decode: (v) => [{ sub: 'signalFailure', val: !!v }],
131
+ },
132
+ SignalDoor: {
133
+ states: [{ sub: 'signalDoor', role: 'sensor.door', type: 'boolean', name: 'Door open' }],
134
+ decode: (v) => [{ sub: 'signalDoor', val: !!v }],
135
+ },
136
+ RemoteEnable: {
137
+ states: [
138
+ { sub: 'mobileStart', role: 'indicator', type: 'boolean', name: 'MobileStart enabled' },
139
+ { sub: 'remoteEnableRaw', role: 'json', type: 'string', name: 'RemoteEnable (raw)' },
140
+ ],
141
+ // Element[1] = MobileStart-Freigabe (0/1). Steuerung nur möglich, wenn 1.
142
+ decode: (v) => [
143
+ { sub: 'mobileStart', val: Array.isArray(v) ? !!v[1] : false },
144
+ { sub: 'remoteEnableRaw', val: JSON.stringify(v) },
145
+ ],
146
+ },
147
+ ProcessAction: {
148
+ states: [{ sub: 'processAction', role: 'value', type: 'number', name: 'Process action' }],
149
+ decode: (v) => [{ sub: 'processAction', val: v }],
150
+ },
151
+ DeviceAction: {
152
+ states: [{ sub: 'deviceAction', role: 'value', type: 'number', name: 'Device action' }],
153
+ decode: (v) => [{ sub: 'deviceAction', val: v }],
154
+ },
155
+ Light: {
156
+ states: [{ sub: 'light', role: 'switch.light', type: 'boolean', name: 'Light on' }],
157
+ // Light: 0 = kein Licht/aus, 1 = an, 2 = aus (gerätespezifisch); >0 && !=2 → an
158
+ decode: (v) => [{ sub: 'light', val: v === 1 }],
159
+ },
160
+ StandbyState: {
161
+ states: [{ sub: 'standbyState', role: 'value', type: 'number', name: 'Standby state' }],
162
+ decode: (v) => [{ sub: 'standbyState', val: v }],
163
+ },
164
+ SpinningSpeed: {
165
+ states: [{ sub: 'spinningSpeed', role: 'value', type: 'number', unit: 'rpm', name: 'Spin speed' }],
166
+ decode: (v) => [{ sub: 'spinningSpeed', val: v }],
167
+ },
168
+ DryingStep: {
169
+ states: [
170
+ { sub: 'dryingStep', role: 'value', type: 'number', name: 'Drying step (raw)' },
171
+ { sub: 'dryingStepText', role: 'text', type: 'string', name: 'Drying step' },
172
+ ],
173
+ decode: (v) => [{ sub: 'dryingStep', val: v }, { sub: 'dryingStepText', val: dryingStepTextDe(v) }],
174
+ },
175
+ SyncState: {
176
+ states: [{ sub: 'syncState', role: 'value', type: 'number', name: 'Sync state' }],
177
+ decode: (v) => [{ sub: 'syncState', val: v }],
178
+ },
179
+ InternalState: {
180
+ states: [{ sub: 'internalState', role: 'value', type: 'number', name: 'Internal state' }],
181
+ decode: (v) => [{ sub: 'internalState', val: v }],
182
+ },
183
+ };
184
+
185
+ /** Ident-Felder → info-States (statisch). */
186
+ const IDENT_FIELDS = [
187
+ { sub: 'techType', role: 'info.name', type: 'string', name: 'Model / TechType', path: ['DeviceIdentLabel', 'TechType'] },
188
+ { sub: 'fabNumber', role: 'info.serial', type: 'string', name: 'Serial number', path: ['DeviceIdentLabel', 'FabNumber'] },
189
+ { sub: 'matNumber', role: 'text', type: 'string', name: 'Material number', path: ['DeviceIdentLabel', 'MatNumber'] },
190
+ { sub: 'deviceType', role: 'value', type: 'number', name: 'Device type', path: ['DeviceType'] },
191
+ { sub: 'xkmType', role: 'text', type: 'string', name: 'WiFi module type', path: ['XKMIdentLabel', 'TechType'] },
192
+ { sub: 'xkmVersion', role: 'info.firmware', type: 'string', name: 'WiFi module firmware', path: ['XKMIdentLabel', 'ReleaseVersion'] },
193
+ { sub: 'protocolVersion', role: 'value', type: 'number', name: 'Protocol version', path: ['ProtocolVersion'] },
194
+ ];
195
+
196
+ /** Steuer-States (beschreibbar, per Opcode). Nur angelegt, wenn allowControl. */
197
+ const CONTROL_STATES = [
198
+ { sub: 'start', role: 'button.start', name: 'Start program', opcode: 0x01 },
199
+ { sub: 'stop', role: 'button.stop', name: 'Stop program', opcode: 0x37 },
200
+ { sub: 'pause', role: 'button.pause', name: 'Pause program', opcode: 0x03 },
201
+ { sub: 'powerOn', role: 'button', name: 'Switch on', opcode: 0x10 },
202
+ { sub: 'powerOff', role: 'button', name: 'Switch off', opcode: 0x13 },
203
+ { sub: 'lightOn', role: 'button', name: 'Light on', opcode: 0x0d },
204
+ { sub: 'lightOff', role: 'button', name: 'Light off', opcode: 0x0e },
205
+ ];
206
+
207
+ // Geräteart je DeviceType (für den Objektnamen).
208
+ const DEVICE_CATEGORY = {
209
+ 1: 'Waschmaschine', 2: 'Trockner', 24: 'Waschtrockner',
210
+ 7: 'Spülmaschine', 12: 'Backofen', 13: 'Backofen mit Mikrowelle',
211
+ 15: 'Dampfgarer', 16: 'Dampfbackofen', 17: 'Kaffeevollautomat',
212
+ 18: 'Kühlschrank', 19: 'Gefrierschrank', 20: 'Kühl-Gefrier-Kombination',
213
+ 21: 'Weinkühlschrank', 23: 'Dunstabzug', 25: 'Kochfeld', 27: 'Mikrowelle',
214
+ 67: 'Wärmeschublade', 68: 'Wärmeschublade',
215
+ };
216
+
217
+ function deviceCategory(deviceType) {
218
+ return DEVICE_CATEGORY[deviceType] || null;
219
+ }
220
+
221
+ // Gerätespezifische STATE_FIELDS nur beim passenden Gerätetyp anlegen/schreiben - sonst stünde z. B.
222
+ // die Schleuderdrehzahl auch bei Spülmaschine und Backofen (sinnlos). Key = STATE_FIELDS-Schlüssel,
223
+ // Wert = erlaubte deviceTypes. Nicht gelistete Felder gelten für ALLE Geräte (Status, Programm, …).
224
+ const FIELD_DEVICE_TYPES = {
225
+ SpinningSpeed: [1, 24], // Schleuderdrehzahl: Waschmaschine, Waschtrockner
226
+ DryingStep: [2, 24], // Trockenstufe: Trockner, Waschtrockner
227
+ };
228
+
229
+ /** true, wenn das STATE_FIELDS-Feld [key] für [deviceType] angelegt/geschrieben werden soll. */
230
+ function fieldAllowed(key, deviceType) {
231
+ const allowed = FIELD_DEVICE_TYPES[key];
232
+ return !allowed || allowed.includes(deviceType);
233
+ }
234
+
235
+ // Deutsche Datenpunkt-Namen (überlappende Punkte an mielecloudservice angelehnt).
236
+ const DE_NAMES = {
237
+ state: {
238
+ status: 'Status (Rohwert)', statusText: 'Status',
239
+ programType: 'Programmart (Rohwert)', programTypeText: 'Programmart',
240
+ programId: 'Programmbezeichnung (Rohwert)', programText: 'Programmbezeichnung',
241
+ programPhase: 'Programmphase (Rohwert)', programPhaseText: 'Programmphase',
242
+ remainingMinutes: 'Restzeit (Minuten)', remainingHHMM: 'Restzeit',
243
+ estimatedEndTime: 'Voraussichtliches Ende', estimatedEndTimeText: 'Voraussichtliches Ende (Uhrzeit)',
244
+ elapsedMinutes: 'Verstrichene Zeit (Minuten)', elapsedHHMM: 'Verstrichene Zeit',
245
+ startInMinutes: 'Startvorwahl (Minuten)', startHHMM: 'Startvorwahl',
246
+ targetTemperature: 'Zieltemperatur', targetTemperatureZone2: 'Zieltemperatur Zone 2', targetTemperatureZone3: 'Zieltemperatur Zone 3',
247
+ temperature: 'Temperatur', temperatureZone2: 'Temperatur Zone 2', temperatureZone3: 'Temperatur Zone 3',
248
+ signalInfo: 'Info-Signal', signalFailure: 'Störungssignal', signalDoor: 'Tür offen',
249
+ mobileStart: 'MobileStart verfügbar', remoteEnableRaw: 'Fernsteuer-Freigabe (Rohwert)',
250
+ processAction: 'Prozess-Aktion', deviceAction: 'Geräte-Aktion', light: 'Licht',
251
+ standbyState: 'Standby-Zustand', spinningSpeed: 'Schleuderdrehzahl',
252
+ dryingStep: 'Trockenstufe (Rohwert)', dryingStepText: 'Trockenstufe',
253
+ syncState: 'Sync-Zustand', internalState: 'Interner Zustand',
254
+ },
255
+ info: {
256
+ techType: 'Gerätetyp (Technik)', fabNumber: 'Seriennummer', matNumber: 'Materialnummer',
257
+ deviceType: 'Gerätetyp', xkmType: 'Kommunikationsmodul-Typ', xkmVersion: 'Kommunikationsmodul-Firmware',
258
+ protocolVersion: 'Protokollversion',
259
+ },
260
+ control: {
261
+ start: 'Programm starten', stop: 'Programm stoppen', pause: 'Programm pausieren',
262
+ powerOn: 'Einschalten', powerOff: 'Ausschalten', lightOn: 'Licht an', lightOff: 'Licht aus',
263
+ },
264
+ };
265
+
266
+ /**
267
+ * common.name für einen Datenpunkt: {en, de} wenn eine deutsche Übersetzung existiert,
268
+ * sonst nur der englische Name. Bei german=false wird immer nur Englisch verwendet.
269
+ */
270
+ function nameFor(channel, sub, en, german) {
271
+ const de = DE_NAMES[channel] && DE_NAMES[channel][sub];
272
+ if (german && de) return { en, de };
273
+ return en;
274
+ }
275
+
276
+ function pathGet(obj, path) {
277
+ let cur = obj;
278
+ for (const p of path) {
279
+ if (cur == null) return undefined;
280
+ cur = cur[p];
281
+ }
282
+ return cur;
283
+ }
284
+
285
+ module.exports = { STATE_FIELDS, IDENT_FIELDS, CONTROL_STATES, DE_NAMES, DEVICE_CATEGORY, deviceCategory, FIELD_DEVICE_TYPES, fieldAllowed, nameFor, timeToMinutes, temp, pathGet, TEMP_SENTINEL };