@impact0815/node-red-contrib-home-connect 0.0.0-stage → 0.2.1

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.
Files changed (55) hide show
  1. package/BUILD-INFO.json +23 -0
  2. package/CHANGELOG.md +53 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +19 -0
  5. package/README.de.md +344 -0
  6. package/README.md +343 -2
  7. package/SECURITY.md +47 -0
  8. package/docs/API-COVERAGE.md +59 -0
  9. package/docs/ARCHITECTURE.md +95 -0
  10. package/docs/DASHBOARD.de.md +121 -0
  11. package/docs/DASHBOARD.md +121 -0
  12. package/docs/FIRST-PUSH.md +42 -0
  13. package/docs/GITHUB-UPDATE.md +76 -0
  14. package/docs/LOCAL-LAN.de.md +150 -0
  15. package/docs/LOCAL-LAN.md +150 -0
  16. package/docs/MESSAGE-API.md +111 -0
  17. package/docs/RELEASE.md +91 -0
  18. package/docs/TROUBLESHOOTING.de.md +230 -0
  19. package/docs/TROUBLESHOOTING.md +230 -0
  20. package/docs/images/README.md +10 -0
  21. package/docs/images/dashboard-details-en.png +0 -0
  22. package/docs/images/dashboard-overview-en.png +0 -0
  23. package/docs/official-api-contract.json +418 -0
  24. package/examples/01-first-steps-device-list.json +148 -0
  25. package/examples/02-events.json +165 -0
  26. package/examples/03-safe-read-write.json +155 -0
  27. package/examples/04-local-read-only.json +214 -0
  28. package/examples/README.de.md +62 -0
  29. package/examples/README.md +62 -0
  30. package/examples/dashboard-dishwasher-washer-oven.json +628 -0
  31. package/examples/dashboard-screenshot-simulated.json +405 -0
  32. package/examples/demo-home-connect-flowfuse-1.34.1-scriptless.json +390 -0
  33. package/lib/client.js +600 -0
  34. package/lib/constants.js +66 -0
  35. package/lib/dashboard-i18n.js +255 -0
  36. package/lib/device-label.js +39 -0
  37. package/lib/errors.js +51 -0
  38. package/lib/health.js +525 -0
  39. package/lib/local-client.js +1014 -0
  40. package/lib/local-crypto.js +122 -0
  41. package/lib/local-diagnostics.js +115 -0
  42. package/lib/local-profile.js +706 -0
  43. package/lib/local-trace.js +103 -0
  44. package/lib/oauth.js +23 -0
  45. package/lib/rate-limiter.js +186 -0
  46. package/lib/simulator.js +290 -0
  47. package/lib/sse.js +58 -0
  48. package/lib/validation.js +194 -0
  49. package/lib/websocket-transport.js +374 -0
  50. package/nodes/home-connect.html +795 -0
  51. package/nodes/home-connect.js +699 -0
  52. package/nodes/locales/de/home-connect.html +19 -0
  53. package/nodes/locales/de/home-connect.json +10 -0
  54. package/nodes/locales/en-US/home-connect.json +10 -0
  55. package/package.json +76 -4
@@ -0,0 +1,628 @@
1
+ [
2
+ {
3
+ "id": "hc-dashboard-flow",
4
+ "type": "tab",
5
+ "label": "Home Connect Dashboard",
6
+ "disabled": false,
7
+ "info": "Immediate demo with the built-in simulator. Replace the configuration node and appliance IDs for real appliances."
8
+ },
9
+ {
10
+ "id": "hc-comment",
11
+ "type": "comment",
12
+ "z": "hc-dashboard-flow",
13
+ "name": "Dashboard 2.0 · DE/EN lokalisiert · Cloud + Local read-only · Built-in simulator",
14
+ "info": "Requires @flowfuse/node-red-dashboard and @impact0815/node-red-contrib-home-connect. Health schema: home-connect-health/v1. Device information only; no firmware endpoint is used.",
15
+ "x": 360,
16
+ "y": 40,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "hc-sim-config",
21
+ "type": "home-connect-config",
22
+ "name": "Home Connect built-in simulator",
23
+ "environment": "builtin-simulator",
24
+ "redirectUri": "https://localhost:1880/home-connect/oauth/callback",
25
+ "scope": "IdentifyAppliance",
26
+ "language": "de-DE",
27
+ "allowWrite": true,
28
+ "requestTimeout": 15000,
29
+ "maxRequestsPerDay": 900,
30
+ "minimumRequestInterval": 250
31
+ },
32
+ {
33
+ "id": "hc-ui-base",
34
+ "type": "ui-base",
35
+ "name": "Home Connect Dashboard",
36
+ "path": "/dashboard",
37
+ "appIcon": "",
38
+ "includeClientData": true,
39
+ "acceptsClientConfig": [
40
+ "ui-notification",
41
+ "ui-control"
42
+ ],
43
+ "showPathInSidebar": false,
44
+ "headerContent": "page",
45
+ "navigationStyle": "default",
46
+ "titleBarStyle": "default",
47
+ "showReconnectNotification": true,
48
+ "notificationDisplayTime": 1,
49
+ "showDisconnectNotification": true,
50
+ "allowInstall": false
51
+ },
52
+ {
53
+ "id": "hc-ui-theme",
54
+ "type": "ui-theme",
55
+ "name": "Home Connect",
56
+ "colors": {
57
+ "surface": "#ffffff",
58
+ "primary": "#00629B",
59
+ "bgPage": "#f3f6f9",
60
+ "groupBg": "#ffffff",
61
+ "groupOutline": "#d8e2eb"
62
+ },
63
+ "sizes": {
64
+ "density": "default",
65
+ "pagePadding": "12px",
66
+ "groupGap": "12px",
67
+ "groupBorderRadius": "6px",
68
+ "widgetGap": "12px"
69
+ }
70
+ },
71
+ {
72
+ "id": "hc-ui-page",
73
+ "type": "ui-page",
74
+ "name": "Home Connect · Details",
75
+ "ui": "hc-ui-base",
76
+ "path": "/home-connect",
77
+ "icon": "home",
78
+ "layout": "grid",
79
+ "theme": "hc-ui-theme",
80
+ "breakpoints": [
81
+ {
82
+ "name": "Default",
83
+ "px": "0",
84
+ "cols": "3"
85
+ },
86
+ {
87
+ "name": "Tablet",
88
+ "px": "576",
89
+ "cols": "6"
90
+ },
91
+ {
92
+ "name": "Small Desktop",
93
+ "px": "768",
94
+ "cols": "9"
95
+ },
96
+ {
97
+ "name": "Desktop",
98
+ "px": "1024",
99
+ "cols": "12"
100
+ }
101
+ ],
102
+ "order": 2,
103
+ "className": "",
104
+ "visible": "true",
105
+ "disabled": "false"
106
+ },
107
+ {
108
+ "id": "hc-group-dw",
109
+ "type": "ui-group",
110
+ "name": "Geschirrspülmaschine",
111
+ "page": "hc-ui-page",
112
+ "width": "4",
113
+ "height": "1",
114
+ "order": 1,
115
+ "showTitle": false,
116
+ "className": "",
117
+ "visible": "true",
118
+ "disabled": "false"
119
+ },
120
+ {
121
+ "id": "hc-group-wm",
122
+ "type": "ui-group",
123
+ "name": "Waschmaschine",
124
+ "page": "hc-ui-page",
125
+ "width": "4",
126
+ "height": "1",
127
+ "order": 2,
128
+ "showTitle": false,
129
+ "className": "",
130
+ "visible": "true",
131
+ "disabled": "false"
132
+ },
133
+ {
134
+ "id": "hc-group-ov",
135
+ "type": "ui-group",
136
+ "name": "Backofen",
137
+ "page": "hc-ui-page",
138
+ "width": "4",
139
+ "height": "1",
140
+ "order": 3,
141
+ "showTitle": false,
142
+ "className": "",
143
+ "visible": "true",
144
+ "disabled": "false"
145
+ },
146
+ {
147
+ "id": "hc-refresh",
148
+ "type": "inject",
149
+ "z": "hc-dashboard-flow",
150
+ "name": "Initial snapshot + refresh every 30 min",
151
+ "props": [
152
+ {
153
+ "p": "payload"
154
+ },
155
+ {
156
+ "p": "topic",
157
+ "vt": "str"
158
+ }
159
+ ],
160
+ "repeat": "1800",
161
+ "crontab": "",
162
+ "once": true,
163
+ "onceDelay": "1",
164
+ "topic": "refresh",
165
+ "payload": "",
166
+ "payloadType": "date",
167
+ "x": 130,
168
+ "y": 100,
169
+ "wires": [
170
+ [
171
+ "hc-read-dw",
172
+ "hc-read-wm",
173
+ "hc-read-ov"
174
+ ]
175
+ ]
176
+ },
177
+ {
178
+ "id": "hc-events",
179
+ "type": "home-connect-events",
180
+ "z": "hc-dashboard-flow",
181
+ "name": "All appliance events",
182
+ "connection": "hc-sim-config",
183
+ "haId": "*",
184
+ "x": 130,
185
+ "y": 160,
186
+ "wires": [
187
+ [
188
+ "hc-dashboard-state"
189
+ ],
190
+ [
191
+ "hc-dashboard-state",
192
+ "hc-debug-error"
193
+ ]
194
+ ]
195
+ },
196
+ {
197
+ "id": "hc-dashboard-state",
198
+ "type": "function",
199
+ "z": "hc-dashboard-flow",
200
+ "name": "Snapshot/Event → Zustand zusammenführen",
201
+ "func": "const store=flow.get('hcDashboardState')||{};\nconst input=(msg.payload&&typeof msg.payload==='object')?msg.payload:{};\nconst snapshot=input.appliance?input:null;\nconst event=input.event?input:null;\nconst haId=msg.haId||snapshot?.appliance?.haId||event?.haId;\nif(!haId)return null;\nconst category=(item)=>{\n const declared=String(item?.category||'').toLowerCase(); const key=String(item?.key||'');\n if(declared==='status'||declared==='event'||/\\.Status\\.|\\.Event\\./.test(key))return 'status';\n if(declared==='setting'||/\\.Setting\\./.test(key))return 'setting';\n return 'value';\n};\nconst model=store[haId]||{haId,appliance:{haId,connected:undefined},status:{},settings:{},values:{},itemMeta:{},programs:{available:[],active:null,selected:null,options:[]},health:'ok',warnings:[],readOnly:false,simulated:false,stale:false};\nmodel.itemMeta=model.itemMeta||{}; model.programs=model.programs||{available:[],active:null,selected:null,options:[]};\nconst mergeItems=(items)=>{\n for(const item of Array.isArray(items)?items:[]){\n if(!item||item.key===undefined)continue;\n model.itemMeta[item.key]={...item}; model.values[item.key]=item.value;\n const group=category(item); if(group==='status')model.status[item.key]=item.value; if(group==='setting')model.settings[item.key]=item.value;\n if(item.key==='BSH.Common.Root.ActiveProgram'&&item.valueResolved!==false)model.programs.active=item.value?{key:item.value,uid:item.rawValue}:null;\n if(item.key==='BSH.Common.Root.SelectedProgram'&&item.valueResolved!==false)model.programs.selected=item.value?{key:item.value,uid:item.rawValue}:null;\n }\n};\nif(snapshot){\n model.appliance={...model.appliance,...snapshot.appliance}; model.readOnly=snapshot.readOnly===true; model.simulated=snapshot.simulated===true;\n mergeItems(snapshot.values?.items); mergeItems(snapshot.status?.items); mergeItems(snapshot.settings?.items);\n if(Array.isArray(snapshot.programs?.available))model.programs.available=snapshot.programs.available;\n if(snapshot.programs&&Object.hasOwn(snapshot.programs,'active'))model.programs.active=snapshot.programs.active;\n if(snapshot.programs&&Object.hasOwn(snapshot.programs,'selected'))model.programs.selected=snapshot.programs.selected;\n if(Array.isArray(snapshot.programs?.options))model.programs.options=snapshot.programs.options;\n model.connection={...(model.connection||{}),...(snapshot.connection||{})}; model.stale=snapshot.stale===true;\n model.health=snapshot.health||msg.health||model.health; model.warnings=snapshot.warnings||msg.warnings||model.warnings;\n model.profileResolution=snapshot.profileResolution||model.profileResolution;\n}\nif(event){\n model.readOnly=model.readOnly||event.connection?.readOnly===true;\n model.connection={...(model.connection||{}),...(event.connection||{}),lastEvent:event.event,lastAction:event.connection?.action};\n if(event.event==='CONNECTED'){model.appliance.connected=true;model.stale=false;}\n if(event.event==='DISCONNECTED'){model.appliance.connected=false;model.stale=Object.keys(model.values).length>0;}\n mergeItems(event.data?.items);\n if(event.data?.programs){\n if(Object.hasOwn(event.data.programs,'active'))model.programs.active=event.data.programs.active;\n if(Object.hasOwn(event.data.programs,'selected'))model.programs.selected=event.data.programs.selected;\n if(Array.isArray(event.data.programs.options))model.programs.options=event.data.programs.options;\n }\n model.health=msg.health||(event.event==='DISCONNECTED'?'offline':model.health); model.warnings=msg.warnings||model.warnings;\n}\nif(!snapshot&&!event&&msg.homeConnect?.ok===false){\n model.health=msg.health||model.health||'error';\n if(model.health==='offline'){model.appliance.connected=false;model.stale=Object.keys(model.values).length>0;}\n model.lastError={code:input.code||'HOME_CONNECT_ERROR',at:new Date().toISOString()};\n}\nmodel.updatedAt=new Date().toISOString(); store[haId]=model; flow.set('hcDashboardState',store);\nconst kindOf=(value)=>{const t=String(value?.appliance?.type||'').toLowerCase();return t.includes('dish')?'dishwasher':t.includes('washer')||t.includes('laundry')?'washer':t.includes('oven')?'oven':''};\nconst view=(value)=>{\n const activeOptions=Object.fromEntries((value.programs.active?.options||[]).map(i=>[i.key,i.value]));\n const remainingItem=value.itemMeta['BSH.Common.Option.RemainingProgramTime'];\n return {appliance:value.appliance,simulated:value.simulated===true,programs:value.programs.available,active:value.programs.active,selected:value.programs.selected,\n options:value.programs.options||value.programs.active?.options||value.programs.selected?.options||[],\n state:value.status['BSH.Common.Status.OperationState'],stateItem:value.itemMeta['BSH.Common.Status.OperationState'],\n door:value.status['BSH.Common.Status.DoorState'],doorItem:value.itemMeta['BSH.Common.Status.DoorState'],\n remaining:remainingItem?.value??activeOptions['BSH.Common.Option.RemainingProgramTime'],remainingItem,\n remoteControlItem:value.itemMeta['BSH.Common.Status.RemoteControlActive'],remoteStartItem:value.itemMeta['BSH.Common.Status.RemoteControlStartAllowed'],localControlItem:value.itemMeta['BSH.Common.Status.LocalControlActive'],\n status:value.status,settings:value.settings,values:value.values,itemMeta:value.itemMeta,health:value.health,warnings:value.warnings,\n readOnly:value.readOnly,stale:value.stale,connection:value.connection,profileResolution:value.profileResolution,updatedAt:value.updatedAt};\n};\nconst byKind={}; for(const value of Object.values(store)){const kind=kindOf(value);if(kind)byKind[kind]=view(value);}\nconst kind=kindOf(model); const current=view(model); const detail={...msg,topic:kind,payload:current};\nreturn [kind==='dishwasher'?detail:null,kind==='washer'?detail:null,kind==='oven'?detail:null,{topic:'home-connect-overview',payload:byKind}];",
202
+ "outputs": 4,
203
+ "timeout": 0,
204
+ "noerr": 0,
205
+ "initialize": "",
206
+ "finalize": "",
207
+ "libs": [],
208
+ "x": 520,
209
+ "y": 190,
210
+ "wires": [
211
+ [
212
+ "hc-ui-dw"
213
+ ],
214
+ [
215
+ "hc-ui-wm"
216
+ ],
217
+ [
218
+ "hc-ui-ov"
219
+ ],
220
+ [
221
+ "hc-ui-overview"
222
+ ]
223
+ ]
224
+ },
225
+ {
226
+ "id": "hc-read-dw",
227
+ "type": "home-connect-read",
228
+ "z": "hc-dashboard-flow",
229
+ "name": "Read Geschirrspülmaschine",
230
+ "connection": "hc-sim-config",
231
+ "operation": "snapshot",
232
+ "haId": "dishwasher-001",
233
+ "key": "",
234
+ "x": 170,
235
+ "y": 260,
236
+ "wires": [
237
+ [
238
+ "hc-dashboard-state"
239
+ ],
240
+ [
241
+ "hc-dashboard-state",
242
+ "hc-debug-error"
243
+ ]
244
+ ]
245
+ },
246
+ {
247
+ "id": "hc-ui-dw",
248
+ "type": "ui-template",
249
+ "z": "hc-dashboard-flow",
250
+ "group": "hc-group-dw",
251
+ "page": "",
252
+ "ui": "",
253
+ "name": "Geschirrspülmaschine",
254
+ "order": 1,
255
+ "width": 0,
256
+ "height": 0,
257
+ "head": "",
258
+ "format": "<template>\n <v-card :id=\"'hc-'+kind.toLowerCase()\" class=\"hc-card\" elevation=\"2\">\n <v-card-title><v-icon class=\"mr-2\">mdi-dishwasher</v-icon>{{ deviceTitle }}<v-spacer></v-spacer><v-chip v-if=\"demoMode\" color=\"secondary\" size=\"small\" class=\"mr-2\">{{ hcLabels.simulated }}</v-chip><v-chip :color=\"healthColor\" size=\"small\">{{ healthText }}</v-chip></v-card-title>\n <v-card-text>\n <div class=\"hc-device-info\"><b>{{ appliance.name || deviceTitle }}</b><br>{{ appliance.brand || '—' }} · {{ applianceType }}<br><span v-if=\"appliance.vib\">VIB/E-Nummer: {{ appliance.vib }}</span><br><span>{{ hcLabels.connection }}: {{ online ? hcLabels.online : hcLabels.offline }}</span><span v-if=\"model.stale\"> · {{ hcLabels.lastKnownState }}</span></div>\n <v-alert v-for=\"warning in visibleWarnings\" :key=\"warning.source+warning.code+(warning.key||'')\" :type=\"warningType(warning)\" density=\"compact\" variant=\"tonal\" class=\"mb-2\"><b>{{ warningText(warning) }}</b></v-alert>\n <div class=\"hc-state\"><b>{{ hcLabels.status }}:</b> {{ semanticText(model.stateItem) }}</div>\n <div><b>{{ hcLabels.door }}:</b> {{ semanticText(model.doorItem) }}</div>\n <div><b>{{ hcLabels.active }}:</b> {{ displayName(model.active) }}</div>\n <div><b>{{ hcLabels.selected }}:</b> {{ displayName(model.selected) }}</div>\n <div><b>{{ hcLabels.remaining }}:</b> {{ remainingText }}</div>\n <div v-if=\"statusOnly\" class=\"hc-readonly-panel\">\n <div class=\"hc-readonly-title\">{{ demoMode ? hcLabels.simulatedValues : hcLabels.localProfileReadOnly }}</div>\n <div><b>{{ hcLabels.remoteControl }}:</b> {{ booleanText(model.remoteControlItem) }}</div>\n <div><b>{{ hcLabels.remoteStart }}:</b> {{ booleanText(model.remoteStartItem) }}</div>\n <div><b>{{ hcLabels.localControl }}:</b> {{ booleanText(model.localControlItem) }}</div>\n <div v-if=\"model.options?.length\" class=\"mt-2\"><b>{{ hcLabels.currentOptions }}:</b><div v-for=\"item in model.options\" :key=\"item.key\" class=\"hc-option\"><span :title=\"item.key\">{{ displayKey(item.key) }}</span>: {{ semanticText(item) }}<span v-if=\"item.unit\"> {{ item.unit }}</span></div></div>\n <div v-if=\"programs.length\" class=\"mt-2\"><b>{{ hcLabels.availablePrograms }}:</b> {{ programs.map(p=>p.title).join(', ') }}</div>\n </div>\n <template v-else>\n <v-select class=\"mt-4\" v-model=\"program\" :items=\"programs\" item-title=\"title\" item-value=\"key\" :label=\"hcLabels.program\" density=\"compact\"></v-select>\n <v-text-field v-if=\"kind === 'Oven'\" v-model.number=\"temperature\" :label=\"hcLabels.targetTemperature+' °C'\" type=\"number\" min=\"30\" max=\"250\" step=\"5\" density=\"compact\"></v-text-field>\n <v-checkbox v-if=\"kind === 'Dishwasher'\" v-model=\"boost\" :label=\"displayKey('Dishcare.Dishwasher.Option.VarioSpeedPlus')\" density=\"compact\"></v-checkbox>\n <v-select v-if=\"kind === 'Washer'\" v-model=\"temperatureKey\" :items=\"washerTemperatures\" item-title=\"title\" item-value=\"value\" :label=\"hcLabels.temperature\" density=\"compact\"></v-select>\n <div v-if=\"!canStart && !running\" class=\"hc-control-hint\">{{ hcLabels.remoteStartUnavailable }}</div>\n </template>\n </v-card-text>\n <v-card-actions>\n <template v-if=\"!statusOnly\"><v-btn color=\"primary\" :disabled=\"!program || !canStart\" @click=\"start\">{{ hcLabels.start }}</v-btn><v-btn color=\"error\" variant=\"outlined\" :disabled=\"!running || !online\" @click=\"stop\">{{ hcLabels.stop }}</v-btn></template>\n <v-btn variant=\"text\" @click=\"refresh\">{{ hcLabels.refresh }}</v-btn>\n </v-card-actions>\n </v-card>\n</template>\n<script>\nconst createDashboardI18n=function createDashboardI18n(environment = {}, options = {}) {\n const STORAGE_KEY = options.storageKey || 'home-connect-dashboard-language';\n const EVENT_NAME = options.eventName || 'home-connect-language-changed';\n const supported = ['de', 'en'];\n\n const labels = {\n de: {\n overview: 'Übersicht', applianceDetails: 'Gerätedetails', connection: 'Verbindung', status: 'Status', door: 'Tür', active: 'Aktiv',\n selected: 'Ausgewählt', remaining: 'Restzeit', localProfileReadOnly: 'Lokale Profilwerte · nur lesend', remoteControl: 'Fernsteuerung',\n remoteStart: 'Fernstart', localControl: 'Lokale Bedienung', currentOptions: 'Aktuelle Optionen', availablePrograms: 'Verfügbare Programme',\n program: 'Programm', targetTemperature: 'Solltemperatur', temperature: 'Temperatur', refresh: 'Aktualisieren', start: 'Start', stop: 'Stopp',\n openDetails: 'Details öffnen', backToOverview: 'Zur Übersicht', lastKnownState: 'letzter bekannter Stand', remoteStartUnavailable: 'Fernstart ist aktuell nicht verfügbar.',\n language: 'Sprache', german: 'Deutsch', english: 'Englisch', online: 'online', offline: 'offline', yes: 'Ja', no: 'Nein',\n unavailable: '—', unknownValue: 'Unbekannter Wert', rawValue: 'Rohwert', unknownWarning: 'Unbekannter Hinweis',\n readOnly: 'nur lesend', simulated: 'SIMULIERT', simulatedValues: 'Simulierte Beispielwerte · nur Anzeige'\n },\n en: {\n overview: 'Overview', applianceDetails: 'Appliance details', connection: 'Connection', status: 'Status', door: 'Door', active: 'Active',\n selected: 'Selected', remaining: 'Remaining', localProfileReadOnly: 'Local profile values · read-only', remoteControl: 'Remote control',\n remoteStart: 'Remote start', localControl: 'Local control', currentOptions: 'Current options', availablePrograms: 'Available programs',\n program: 'Program', targetTemperature: 'Target temperature', temperature: 'Temperature', refresh: 'Refresh', start: 'Start', stop: 'Stop',\n openDetails: 'Open details', backToOverview: 'Back to overview', lastKnownState: 'last known state', remoteStartUnavailable: 'Remote start is currently unavailable.',\n language: 'Language', german: 'German', english: 'English', online: 'online', offline: 'offline', yes: 'Yes', no: 'No',\n unavailable: '—', unknownValue: 'Unknown value', rawValue: 'raw value', unknownWarning: 'Unknown notice',\n readOnly: 'read-only', simulated: 'SIMULATED', simulatedValues: 'Simulated example values · display only'\n }\n };\n\n const healthNames = {\n ok: { de: 'OK', en: 'OK' }, running: { de: 'Läuft', en: 'Running' }, info: { de: 'Info', en: 'Info' },\n warning: { de: 'Warnung', en: 'Warning' }, action_required: { de: 'Aktion nötig', en: 'Action required' },\n offline: { de: 'Offline', en: 'Offline' }, error: { de: 'Fehler', en: 'Error' }\n };\n\n const deviceTypes = {\n Dishwasher: { de: 'Geschirrspüler', en: 'Dishwasher' }, Washer: { de: 'Waschmaschine', en: 'Washer' },\n Oven: { de: 'Backofen', en: 'Oven' }, Dryer: { de: 'Trockner', en: 'Dryer' },\n WasherDryer: { de: 'Waschtrockner', en: 'Washer-dryer' }, Refrigerator: { de: 'Kühlschrank', en: 'Refrigerator' },\n Freezer: { de: 'Gefrierschrank', en: 'Freezer' }, FridgeFreezer: { de: 'Kühl-Gefrier-Kombination', en: 'Fridge-freezer' },\n CoffeeMaker: { de: 'Kaffeevollautomat', en: 'Coffee maker' }, Hood: { de: 'Dunstabzugshaube', en: 'Hood' },\n Hob: { de: 'Kochfeld', en: 'Hob' }\n };\n\n const valueNames = {\n Inactive: { de: 'Inaktiv', en: 'Inactive' }, Ready: { de: 'Bereit', en: 'Ready' }, Run: { de: 'Läuft', en: 'Running' },\n Finished: { de: 'Beendet', en: 'Finished' }, DelayedStart: { de: 'Startzeitvorwahl', en: 'Delayed start' },\n Pause: { de: 'Pausiert', en: 'Paused' }, Paused: { de: 'Pausiert', en: 'Paused' },\n ActionRequired: { de: 'Aktion erforderlich', en: 'Action required' }, Aborting: { de: 'Wird abgebrochen', en: 'Aborting' },\n Error: { de: 'Fehler', en: 'Error' }, Open: { de: 'Offen', en: 'Open' }, Closed: { de: 'Geschlossen', en: 'Closed' },\n Locked: { de: 'Verriegelt', en: 'Locked' }, On: { de: 'Ein', en: 'On' }, Off: { de: 'Aus', en: 'Off' },\n Standby: { de: 'Standby', en: 'Standby' }, Present: { de: 'Vorhanden', en: 'Present' }, Absent: { de: 'Nicht vorhanden', en: 'Absent' },\n Confirmed: { de: 'Bestätigt', en: 'Confirmed' }, Enabled: { de: 'Aktiviert', en: 'Enabled' }, Disabled: { de: 'Deaktiviert', en: 'Disabled' },\n Cleaning: { de: 'Reinigen', en: 'Cleaning' }, Rinsing: { de: 'Spülen', en: 'Rinsing' }, Drying: { de: 'Trocknen', en: 'Drying' },\n PreWash: { de: 'Vorspülen', en: 'Pre-wash' }, MainWash: { de: 'Hauptspülen', en: 'Main wash' },\n Heating: { de: 'Aufheizen', en: 'Heating' }, Cooling: { de: 'Abkühlen', en: 'Cooling' },\n DE: { de: 'Deutsch', en: 'German' }, EN: { de: 'Englisch', en: 'English' }\n };\n\n const displayNames = {\n ProgramProgress: { de: 'Programmfortschritt', en: 'Program progress' }, RemainingProgramTime: { de: 'Restzeit', en: 'Remaining program time' },\n EnergyForecast: { de: 'Energieprognose', en: 'Energy forecast' }, WaterForecast: { de: 'Wasserprognose', en: 'Water forecast' },\n StartInRelative: { de: 'Start in', en: 'Start in' }, Duration: { de: 'Dauer', en: 'Duration' },\n RemoteControlActive: { de: 'Fernsteuerung aktiv', en: 'Remote control active' },\n RemoteControlStartAllowed: { de: 'Fernstart erlaubt', en: 'Remote start allowed' }, LocalControlActive: { de: 'Lokale Bedienung aktiv', en: 'Local control active' },\n OperationState: { de: 'Betriebszustand', en: 'Operation state' }, DoorState: { de: 'Türzustand', en: 'Door state' },\n PowerState: { de: 'Betriebsart', en: 'Power state' }, ProgramPhase: { de: 'Programmphase', en: 'Program phase' },\n Language: { de: 'Sprache', en: 'Language' }, SaltNearlyEmpty: { de: 'Salz fast leer', en: 'Salt nearly empty' },\n RinseAidNearlyEmpty: { de: 'Klarspüler fast leer', en: 'Rinse aid nearly empty' },\n HygienePlus: { de: 'Hygiene Plus', en: 'Hygiene Plus' }, IntensivZone: { de: 'Intensivzone', en: 'Intensive zone' },\n VarioSpeedPlus: { de: 'varioSpeed Plus', en: 'varioSpeed Plus' }, BrillianceDry: { de: 'Glanztrocknen', en: 'Brilliance dry' },\n ExtraDry: { de: 'Extra trocknen', en: 'Extra dry' }, HalfLoad: { de: 'Halbe Beladung', en: 'Half load' },\n SilenceOnDemand: { de: 'Leisebetrieb', en: 'Silence on demand' }, Temperature: { de: 'Temperatur', en: 'Temperature' },\n SpinSpeed: { de: 'Schleuderdrehzahl', en: 'Spin speed' }, Stains: { de: 'Flecken', en: 'Stains' },\n SetpointTemperature: { de: 'Solltemperatur', en: 'Target temperature' }, FastPreHeat: { de: 'Schnellaufheizen', en: 'Fast preheat' },\n AddedSteam: { de: 'Dampfzugabe', en: 'Added steam' },\n\n Intensiv70: { de: 'Intensiv 70 °C', en: 'Intensive 70 °C' }, Auto2: { de: 'Automatik 45–65 °C', en: 'Auto 45–65 °C' },\n Eco50: { de: 'Eco 50 °C', en: 'Eco 50 °C' }, Glas40: { de: 'Glas 40 °C', en: 'Glass 40 °C' },\n Quick45: { de: 'Kurz 45 °C', en: 'Quick 45 °C' }, PreRinse: { de: 'Vorspülen', en: 'Pre-rinse' },\n MachineCare: { de: 'Maschinenpflege', en: 'Machine care' },\n Cotton: { de: 'Baumwolle', en: 'Cotton' }, EasyCare: { de: 'Pflegeleicht', en: 'Easy-care' }, Mix: { de: 'Mix', en: 'Mix' },\n DelicatesSilk: { de: 'Fein/Seide', en: 'Delicates/Silk' }, Wool: { de: 'Wolle', en: 'Wool' }, DarkWash: { de: 'Dunkle Wäsche', en: 'Dark wash' },\n ShirtsBlouses: { de: 'Hemden/Blusen', en: 'Shirts/Blouses' }, Curtains: { de: 'Gardinen', en: 'Curtains' }, Super15: { de: 'Super 15', en: 'Super 15' },\n Dessous: { de: 'Dessous', en: 'Lingerie' }, Outdoor: { de: 'Outdoor', en: 'Outdoor' }, Sensitive: { de: 'Sensitiv', en: 'Sensitive' },\n DrumClean: { de: 'Trommel reinigen', en: 'Drum clean' }, Rinse: { de: 'Spülen', en: 'Rinse' }, Spin: { de: 'Schleudern', en: 'Spin' },\n Drain: { de: 'Abpumpen', en: 'Drain' }, NightWash: { de: 'Nachtwäsche', en: 'Night wash' }, CottonEco: { de: 'Baumwolle Eco', en: 'Cotton Eco' },\n Memory1: { de: 'Favorit 1', en: 'Memory 1' }, JustStart: { de: 'Nur Start', en: 'Just start' },\n HotAir: { de: 'Heißluft', en: 'Hot air' }, TopBottomHeating: { de: 'Ober-/Unterhitze', en: 'Top/bottom heat' },\n PreHeating: { de: 'Vorheizen', en: 'Preheating' }, HotAirEco: { de: 'Heißluft Eco', en: 'Hot air Eco' },\n TopBottomHeatingEco: { de: 'Ober-/Unterhitze Eco', en: 'Top/bottom heat Eco' }, HotAirGrilling: { de: 'Umluftgrill', en: 'Hot-air grilling' },\n PizzaSetting: { de: 'Pizzastufe', en: 'Pizza setting' }, IntensiveHeat: { de: 'Intensivhitze', en: 'Intensive heat' },\n GrillLargeArea: { de: 'Großflächengrill', en: 'Large-area grill' }, GrillSmallArea: { de: 'Kleinflächengrill', en: 'Small-area grill' },\n SlowCook: { de: 'Sanftgaren', en: 'Slow cook' }, BottomHeating: { de: 'Unterhitze', en: 'Bottom heat' },\n KeepWarm: { de: 'Warmhalten', en: 'Keep warm' }, PreheatOvenware: { de: 'Geschirr vorwärmen', en: 'Preheat ovenware' },\n FrozenHeatupSpecial: { de: 'Tiefkühl-Spezial', en: 'Frozen food special' }, SabbathProgramme: { de: 'Sabbatprogramm', en: 'Sabbath program' },\n Steam: { de: 'Dampf', en: 'Steam' }, Reheat: { de: 'Aufwärmen', en: 'Reheat' }, DoughProving: { de: 'Gärstufe', en: 'Dough proving' },\n Defrosting: { de: 'Auftauen', en: 'Defrosting' }, Ecolysis: { de: 'Ecolyse', en: 'Ecolysis' }, Descale: { de: 'Entkalken', en: 'Descale' },\n Flushing: { de: 'Spülen', en: 'Flushing' }, Draining: { de: 'Abpumpen', en: 'Draining' }, SubsequentCooking: { de: 'Nachgaren', en: 'Subsequent cooking' },\n KuchenInFormen: { de: 'Kuchen in Formen', en: 'Cakes in tins' }, KuchenAufBackblech: { de: 'Kuchen auf Backblech', en: 'Tray-baked cakes' },\n Strudel: { de: 'Strudel', en: 'Strudel' }, Pavlova: { de: 'Pavlova', en: 'Pavlova' }, WaeheOderTart: { de: 'Wähe/Tarte', en: 'Flan/Tart' },\n Blaetterteiggebaeck: { de: 'Blätterteiggebäck', en: 'Puff pastry' }, Brandteiggebaeck: { de: 'Brandteiggebäck', en: 'Choux pastry' },\n Muffins: { de: 'Muffins', en: 'Muffins' }, Plundergebaeck: { de: 'Plundergebäck', en: 'Danish pastry' },\n Hefeteiggebaeck: { de: 'Hefeteiggebäck', en: 'Yeast pastries' }, Muerbeteiggebaeck: { de: 'Mürbeteiggebäck', en: 'Shortcrust pastry' },\n Baiser: { de: 'Baiser', en: 'Meringue' }, Makronen: { de: 'Makronen', en: 'Macaroons' }, Spritzgebaeck: { de: 'Spritzgebäck', en: 'Spritz biscuits' },\n Plaetzchen: { de: 'Plätzchen', en: 'Biscuits' }, Vollkornbrot: { de: 'Vollkornbrot', en: 'Wholegrain bread' },\n WeizenbrotWeizenmischbrotAufBackblech: { de: 'Weizen-/Weizenmischbrot auf Backblech', en: 'Wheat/mixed-wheat bread on tray' }\n };\n\n function unwrap(value) {\n if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return value.value;\n return value;\n }\n\n function normalizeLocale(value) {\n const text = String(unwrap(value) || '').trim().toLowerCase().replace('_', '-');\n if (text === 'de' || text.startsWith('de-')) return 'de';\n if (text === 'en' || text.startsWith('en-')) return 'en';\n return '';\n }\n\n function readStoredLocale() {\n try { return normalizeLocale(environment.localStorage?.getItem(STORAGE_KEY)); }\n catch { return ''; }\n }\n\n function detectLocale(component, message) {\n const msg = message || component?.msg || {};\n const candidates = [\n msg.uiLocale, msg.locale, readStoredLocale(), component?.$i18n?.locale, component?.$vuetify?.locale?.current,\n environment.document?.documentElement?.lang, ...(environment.navigator?.languages || []), environment.navigator?.language\n ];\n for (const candidate of candidates) {\n const locale = normalizeLocale(candidate);\n if (locale) return locale;\n }\n return 'en';\n }\n\n function setLocale(value, notify = true) {\n const locale = normalizeLocale(value) || 'en';\n try { environment.localStorage?.setItem(STORAGE_KEY, locale); }\n catch { /* Storage can be blocked by browser policy. */ }\n if (notify) {\n try { environment.dispatchEvent?.(new environment.CustomEvent(EVENT_NAME, { detail: { locale } })); }\n catch { /* CustomEvent may be unavailable in non-browser tests. */ }\n }\n return locale;\n }\n\n function t(key, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return labels[lang][key] ?? labels.en[key] ?? key;\n }\n\n function token(value) {\n return String(value ?? '').split('.').pop();\n }\n\n function humanize(value) {\n const leaf = token(value)\n .replace(/[_-]+/g, ' ')\n .replace(/([a-zäöüß])([A-Z])/g, '$1 $2')\n .replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n return leaf || '—';\n }\n\n function localized(entry, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return entry?.[lang] ?? entry?.en;\n }\n\n function deviceType(value, locale) {\n const leaf = token(value);\n const known = localized(deviceTypes[leaf], locale);\n if (known) return known;\n if (!leaf) return t('unavailable', locale);\n const prefix = normalizeLocale(locale) === 'de' ? 'Gerätetyp' : 'Appliance type';\n return `${prefix}: ${humanize(leaf)}`;\n }\n\n function displayKey(value, locale) {\n const leaf = token(value);\n return localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale) || humanize(leaf);\n }\n\n function displayName(value, locale) {\n const source = value && typeof value === 'object' ? (value.key || value.name) : value;\n if (source === undefined || source === null || source === '' || source === '—') return t('unavailable', locale);\n const leaf = token(source);\n const known = localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale);\n if (known) return known;\n const fallbackSource = value && typeof value === 'object' && value.name && !String(value.name).includes('.') ? value.name : leaf;\n return humanize(fallbackSource);\n }\n\n function unknown(rawValue, locale) {\n const printable = rawValue === undefined ? '?' : String(rawValue);\n return `${t('unknownValue', locale)} (${t('rawValue', locale)}: ${printable})`;\n }\n\n function semantic(item, locale) {\n if (!item) return t('unavailable', locale);\n if (item.valueResolved === false) return unknown(item.rawValue, locale);\n const value = item.value;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (value === true) return t('yes', locale);\n if (value === false) return t('no', locale);\n if (typeof value === 'number') return new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(value);\n return displayName(value, locale);\n }\n\n function booleanText(item, locale) {\n return semantic(item, locale);\n }\n\n function healthLabel(value, locale) {\n return localized(healthNames[value], locale) || humanize(value);\n }\n\n function warningText(value, locale) {\n if (!value) return '';\n const lang = normalizeLocale(locale) || 'en';\n return value.text?.[lang] || value.text?.en || value.text?.de || `${t('unknownWarning', lang)} (${value.code || '?'})`;\n }\n\n function remaining(model, locale) {\n const value = model?.remaining;\n const item = model?.remainingItem;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (item?.valueResolved === false) return unknown(item.rawValue, locale);\n const number = Number(value);\n if (!Number.isFinite(number)) return String(value);\n const unit = String(item?.unit || '').toLowerCase();\n if (unit.includes('min')) return `${Math.ceil(number)} min`;\n if (unit.includes('sec') || !model?.connection?.local) return `${Math.ceil(number / 60)} min`;\n const formatted = new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(number);\n return item?.unit ? `${formatted} ${item.unit}` : formatted;\n }\n\n function programItems(programs, locale) {\n return (Array.isArray(programs) ? programs : []).map(item => ({ ...item, title: displayName(item, locale) }));\n }\n\n return {\n STORAGE_KEY, EVENT_NAME, supported, normalizeLocale, detectLocale, setLocale, t, token, humanize,\n deviceType, displayKey, displayName, semantic, booleanText, healthLabel, warningText, remaining, programItems\n };\n};\nconst HC=createDashboardI18n(typeof window==='undefined'?{}:window);\nexport default {\n data(){return{kind:'Dishwasher',locale:HC.detectLocale(),localeListener:null,program:null,temperature:180,boost:false,temperatureKey:'LaundryCare.Washer.EnumType.Temperature.GC40',washerTemperatures:[{title:'30 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC30'},{title:'40 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC40'},{title:'60 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC60'}]}},\n computed:{\n hcLabels(){return {overview:HC.t('overview',this.locale),connection:HC.t('connection',this.locale),status:HC.t('status',this.locale),door:HC.t('door',this.locale),active:HC.t('active',this.locale),selected:HC.t('selected',this.locale),remaining:HC.t('remaining',this.locale),localProfileReadOnly:HC.t('localProfileReadOnly',this.locale),remoteControl:HC.t('remoteControl',this.locale),remoteStart:HC.t('remoteStart',this.locale),localControl:HC.t('localControl',this.locale),currentOptions:HC.t('currentOptions',this.locale),availablePrograms:HC.t('availablePrograms',this.locale),program:HC.t('program',this.locale),targetTemperature:HC.t('targetTemperature',this.locale),temperature:HC.t('temperature',this.locale),refresh:HC.t('refresh',this.locale),start:HC.t('start',this.locale),stop:HC.t('stop',this.locale),openDetails:HC.t('openDetails',this.locale),backToOverview:HC.t('backToOverview',this.locale),lastKnownState:HC.t('lastKnownState',this.locale),remoteStartUnavailable:HC.t('remoteStartUnavailable',this.locale),language:HC.t('language',this.locale),online:HC.t('online',this.locale),offline:HC.t('offline',this.locale),simulated:HC.t('simulated',this.locale),simulatedValues:HC.t('simulatedValues',this.locale)}},model(){return this.msg?.payload||{}},appliance(){return this.model.appliance||{}},readOnly(){return this.model.readOnly===true},demoMode(){return this.model.simulated===true},statusOnly(){return this.readOnly||this.demoMode},\n deviceTitle(){return HC.deviceType(this.kind,this.locale)},applianceType(){return HC.deviceType(this.appliance.type||this.kind,this.locale)},\n health(){return this.model.health||'ok'},warnings(){return this.model.warnings||[]},visibleWarnings(){return this.warnings.slice(0,4)},\n healthColor(){return({ok:'success',running:'info',info:'info',warning:'warning',action_required:'warning',offline:'grey',error:'error'})[this.health]||'grey'},healthText(){return HC.healthLabel(this.health,this.locale)},\n online(){return this.appliance.connected!==false&&this.health!=='offline'},programs(){return HC.programItems(this.model.programs||[],this.locale)},\n remainingText(){return HC.remaining(this.model,this.locale)},running(){return this.health==='running'||String(this.model.state).endsWith('.Run')},\n canStart(){const s=this.model.status||{};return!this.statusOnly&&this.online&&!this.running&&this.health!=='error'&&this.health!=='action_required'&&s['BSH.Common.Status.RemoteControlActive']!==false&&s['BSH.Common.Status.RemoteControlStartAllowed']!==false&&s['BSH.Common.Status.LocalControlActive']!==true}\n },\n watch:{programs:{immediate:true,handler(value){if(!this.program&&value?.length)this.program=value[0].key}}},\n mounted(){this.locale=HC.detectLocale(this,this.msg);this.localeListener=(event)=>{this.locale=HC.normalizeLocale(event.detail?.locale)||this.locale};window.addEventListener(HC.EVENT_NAME,this.localeListener)},\n beforeUnmount(){if(this.localeListener)window.removeEventListener(HC.EVENT_NAME,this.localeListener)},\n methods:{\n displayName(value){return HC.displayName(value,this.locale)},displayKey(value){return HC.displayKey(value,this.locale)},semanticText(item){return HC.semantic(item,this.locale)},booleanText(item){return HC.booleanText(item,this.locale)},warningText(value){return HC.warningText(value,this.locale)},\n warningType(value){return value.severity==='critical'||value.severity==='error'?'error':value.severity==='warning'?'warning':'info'},\n refresh(){this.send({payload:{action:'refresh'}})},\n start(){const options=[];if(this.kind==='Oven')options.push({key:'Cooking.Oven.Option.SetpointTemperature',value:Number(this.temperature),unit:'°C'});if(this.kind==='Dishwasher'&&this.program==='Dishcare.Dishwasher.Program.Eco50')options.push({key:'Dishcare.Dishwasher.Option.VarioSpeedPlus',value:this.boost});if(this.kind==='Washer'&&this.program==='LaundryCare.Washer.Program.Cotton')options.push({key:'LaundryCare.Washer.Option.Temperature',value:this.temperatureKey});this.send({payload:{action:'start-program',program:this.program,options,confirm:'CONFIRM'}})},\n stop(){this.send({payload:{action:'stop-program',confirm:'CONFIRM'}})}\n }\n}\n</script>\n<style>.hc-card{border-top:4px solid rgb(var(--v-theme-primary));height:100%;scroll-margin-top:72px}.hc-state{margin-bottom:6px}.hc-device-info{padding:8px 0 12px;color:rgba(var(--v-theme-on-surface),.75)}.hc-control-hint{margin-top:8px;color:rgb(var(--v-theme-warning));font-size:.9rem}.hc-readonly-panel{margin-top:14px;padding:12px;border:1px solid rgba(var(--v-theme-on-surface),.18);border-radius:6px;background:rgba(var(--v-theme-on-surface),.035)}.hc-readonly-title{font-weight:600;margin-bottom:8px}.hc-option{padding-left:10px}</style>\n",
259
+ "storeOutMessages": true,
260
+ "passthru": false,
261
+ "resendOnRefresh": true,
262
+ "templateScope": "local",
263
+ "className": "",
264
+ "x": 670,
265
+ "y": 260,
266
+ "wires": [
267
+ [
268
+ "hc-route-dw"
269
+ ]
270
+ ]
271
+ },
272
+ {
273
+ "id": "hc-route-dw",
274
+ "type": "function",
275
+ "z": "hc-dashboard-flow",
276
+ "name": "Refresh or safe write",
277
+ "func": "if(msg.payload?.action==='refresh'){msg.payload={};return [msg,null];} msg.confirm=msg.payload?.confirm; return [null,msg];",
278
+ "outputs": 2,
279
+ "timeout": 0,
280
+ "noerr": 0,
281
+ "initialize": "",
282
+ "finalize": "",
283
+ "libs": [],
284
+ "x": 910,
285
+ "y": 260,
286
+ "wires": [
287
+ [
288
+ "hc-read-dw"
289
+ ],
290
+ [
291
+ "hc-write-dw"
292
+ ]
293
+ ]
294
+ },
295
+ {
296
+ "id": "hc-write-dw",
297
+ "type": "home-connect-write",
298
+ "z": "hc-dashboard-flow",
299
+ "name": "Safe write Geschirrspülmaschine",
300
+ "connection": "hc-sim-config",
301
+ "action": "from-message",
302
+ "haId": "dishwasher-001",
303
+ "dryRun": false,
304
+ "confirmationMode": "token",
305
+ "allowedApplianceTypes": "Dishwasher",
306
+ "x": 1180,
307
+ "y": 290,
308
+ "wires": [
309
+ [
310
+ "hc-read-dw"
311
+ ],
312
+ [
313
+ "hc-debug-error"
314
+ ]
315
+ ]
316
+ },
317
+ {
318
+ "id": "hc-read-wm",
319
+ "type": "home-connect-read",
320
+ "z": "hc-dashboard-flow",
321
+ "name": "Read Waschmaschine",
322
+ "connection": "hc-sim-config",
323
+ "operation": "snapshot",
324
+ "haId": "washer-001",
325
+ "key": "",
326
+ "x": 170,
327
+ "y": 380,
328
+ "wires": [
329
+ [
330
+ "hc-dashboard-state"
331
+ ],
332
+ [
333
+ "hc-dashboard-state",
334
+ "hc-debug-error"
335
+ ]
336
+ ]
337
+ },
338
+ {
339
+ "id": "hc-ui-wm",
340
+ "type": "ui-template",
341
+ "z": "hc-dashboard-flow",
342
+ "group": "hc-group-wm",
343
+ "page": "",
344
+ "ui": "",
345
+ "name": "Waschmaschine",
346
+ "order": 1,
347
+ "width": 0,
348
+ "height": 0,
349
+ "head": "",
350
+ "format": "<template>\n <v-card :id=\"'hc-'+kind.toLowerCase()\" class=\"hc-card\" elevation=\"2\">\n <v-card-title><v-icon class=\"mr-2\">mdi-washing-machine</v-icon>{{ deviceTitle }}<v-spacer></v-spacer><v-chip v-if=\"demoMode\" color=\"secondary\" size=\"small\" class=\"mr-2\">{{ hcLabels.simulated }}</v-chip><v-chip :color=\"healthColor\" size=\"small\">{{ healthText }}</v-chip></v-card-title>\n <v-card-text>\n <div class=\"hc-device-info\"><b>{{ appliance.name || deviceTitle }}</b><br>{{ appliance.brand || '—' }} · {{ applianceType }}<br><span v-if=\"appliance.vib\">VIB/E-Nummer: {{ appliance.vib }}</span><br><span>{{ hcLabels.connection }}: {{ online ? hcLabels.online : hcLabels.offline }}</span><span v-if=\"model.stale\"> · {{ hcLabels.lastKnownState }}</span></div>\n <v-alert v-for=\"warning in visibleWarnings\" :key=\"warning.source+warning.code+(warning.key||'')\" :type=\"warningType(warning)\" density=\"compact\" variant=\"tonal\" class=\"mb-2\"><b>{{ warningText(warning) }}</b></v-alert>\n <div class=\"hc-state\"><b>{{ hcLabels.status }}:</b> {{ semanticText(model.stateItem) }}</div>\n <div><b>{{ hcLabels.door }}:</b> {{ semanticText(model.doorItem) }}</div>\n <div><b>{{ hcLabels.active }}:</b> {{ displayName(model.active) }}</div>\n <div><b>{{ hcLabels.selected }}:</b> {{ displayName(model.selected) }}</div>\n <div><b>{{ hcLabels.remaining }}:</b> {{ remainingText }}</div>\n <div v-if=\"statusOnly\" class=\"hc-readonly-panel\">\n <div class=\"hc-readonly-title\">{{ demoMode ? hcLabels.simulatedValues : hcLabels.localProfileReadOnly }}</div>\n <div><b>{{ hcLabels.remoteControl }}:</b> {{ booleanText(model.remoteControlItem) }}</div>\n <div><b>{{ hcLabels.remoteStart }}:</b> {{ booleanText(model.remoteStartItem) }}</div>\n <div><b>{{ hcLabels.localControl }}:</b> {{ booleanText(model.localControlItem) }}</div>\n <div v-if=\"model.options?.length\" class=\"mt-2\"><b>{{ hcLabels.currentOptions }}:</b><div v-for=\"item in model.options\" :key=\"item.key\" class=\"hc-option\"><span :title=\"item.key\">{{ displayKey(item.key) }}</span>: {{ semanticText(item) }}<span v-if=\"item.unit\"> {{ item.unit }}</span></div></div>\n <div v-if=\"programs.length\" class=\"mt-2\"><b>{{ hcLabels.availablePrograms }}:</b> {{ programs.map(p=>p.title).join(', ') }}</div>\n </div>\n <template v-else>\n <v-select class=\"mt-4\" v-model=\"program\" :items=\"programs\" item-title=\"title\" item-value=\"key\" :label=\"hcLabels.program\" density=\"compact\"></v-select>\n <v-text-field v-if=\"kind === 'Oven'\" v-model.number=\"temperature\" :label=\"hcLabels.targetTemperature+' °C'\" type=\"number\" min=\"30\" max=\"250\" step=\"5\" density=\"compact\"></v-text-field>\n <v-checkbox v-if=\"kind === 'Dishwasher'\" v-model=\"boost\" :label=\"displayKey('Dishcare.Dishwasher.Option.VarioSpeedPlus')\" density=\"compact\"></v-checkbox>\n <v-select v-if=\"kind === 'Washer'\" v-model=\"temperatureKey\" :items=\"washerTemperatures\" item-title=\"title\" item-value=\"value\" :label=\"hcLabels.temperature\" density=\"compact\"></v-select>\n <div v-if=\"!canStart && !running\" class=\"hc-control-hint\">{{ hcLabels.remoteStartUnavailable }}</div>\n </template>\n </v-card-text>\n <v-card-actions>\n <template v-if=\"!statusOnly\"><v-btn color=\"primary\" :disabled=\"!program || !canStart\" @click=\"start\">{{ hcLabels.start }}</v-btn><v-btn color=\"error\" variant=\"outlined\" :disabled=\"!running || !online\" @click=\"stop\">{{ hcLabels.stop }}</v-btn></template>\n <v-btn variant=\"text\" @click=\"refresh\">{{ hcLabels.refresh }}</v-btn>\n </v-card-actions>\n </v-card>\n</template>\n<script>\nconst createDashboardI18n=function createDashboardI18n(environment = {}, options = {}) {\n const STORAGE_KEY = options.storageKey || 'home-connect-dashboard-language';\n const EVENT_NAME = options.eventName || 'home-connect-language-changed';\n const supported = ['de', 'en'];\n\n const labels = {\n de: {\n overview: 'Übersicht', applianceDetails: 'Gerätedetails', connection: 'Verbindung', status: 'Status', door: 'Tür', active: 'Aktiv',\n selected: 'Ausgewählt', remaining: 'Restzeit', localProfileReadOnly: 'Lokale Profilwerte · nur lesend', remoteControl: 'Fernsteuerung',\n remoteStart: 'Fernstart', localControl: 'Lokale Bedienung', currentOptions: 'Aktuelle Optionen', availablePrograms: 'Verfügbare Programme',\n program: 'Programm', targetTemperature: 'Solltemperatur', temperature: 'Temperatur', refresh: 'Aktualisieren', start: 'Start', stop: 'Stopp',\n openDetails: 'Details öffnen', backToOverview: 'Zur Übersicht', lastKnownState: 'letzter bekannter Stand', remoteStartUnavailable: 'Fernstart ist aktuell nicht verfügbar.',\n language: 'Sprache', german: 'Deutsch', english: 'Englisch', online: 'online', offline: 'offline', yes: 'Ja', no: 'Nein',\n unavailable: '—', unknownValue: 'Unbekannter Wert', rawValue: 'Rohwert', unknownWarning: 'Unbekannter Hinweis',\n readOnly: 'nur lesend', simulated: 'SIMULIERT', simulatedValues: 'Simulierte Beispielwerte · nur Anzeige'\n },\n en: {\n overview: 'Overview', applianceDetails: 'Appliance details', connection: 'Connection', status: 'Status', door: 'Door', active: 'Active',\n selected: 'Selected', remaining: 'Remaining', localProfileReadOnly: 'Local profile values · read-only', remoteControl: 'Remote control',\n remoteStart: 'Remote start', localControl: 'Local control', currentOptions: 'Current options', availablePrograms: 'Available programs',\n program: 'Program', targetTemperature: 'Target temperature', temperature: 'Temperature', refresh: 'Refresh', start: 'Start', stop: 'Stop',\n openDetails: 'Open details', backToOverview: 'Back to overview', lastKnownState: 'last known state', remoteStartUnavailable: 'Remote start is currently unavailable.',\n language: 'Language', german: 'German', english: 'English', online: 'online', offline: 'offline', yes: 'Yes', no: 'No',\n unavailable: '—', unknownValue: 'Unknown value', rawValue: 'raw value', unknownWarning: 'Unknown notice',\n readOnly: 'read-only', simulated: 'SIMULATED', simulatedValues: 'Simulated example values · display only'\n }\n };\n\n const healthNames = {\n ok: { de: 'OK', en: 'OK' }, running: { de: 'Läuft', en: 'Running' }, info: { de: 'Info', en: 'Info' },\n warning: { de: 'Warnung', en: 'Warning' }, action_required: { de: 'Aktion nötig', en: 'Action required' },\n offline: { de: 'Offline', en: 'Offline' }, error: { de: 'Fehler', en: 'Error' }\n };\n\n const deviceTypes = {\n Dishwasher: { de: 'Geschirrspüler', en: 'Dishwasher' }, Washer: { de: 'Waschmaschine', en: 'Washer' },\n Oven: { de: 'Backofen', en: 'Oven' }, Dryer: { de: 'Trockner', en: 'Dryer' },\n WasherDryer: { de: 'Waschtrockner', en: 'Washer-dryer' }, Refrigerator: { de: 'Kühlschrank', en: 'Refrigerator' },\n Freezer: { de: 'Gefrierschrank', en: 'Freezer' }, FridgeFreezer: { de: 'Kühl-Gefrier-Kombination', en: 'Fridge-freezer' },\n CoffeeMaker: { de: 'Kaffeevollautomat', en: 'Coffee maker' }, Hood: { de: 'Dunstabzugshaube', en: 'Hood' },\n Hob: { de: 'Kochfeld', en: 'Hob' }\n };\n\n const valueNames = {\n Inactive: { de: 'Inaktiv', en: 'Inactive' }, Ready: { de: 'Bereit', en: 'Ready' }, Run: { de: 'Läuft', en: 'Running' },\n Finished: { de: 'Beendet', en: 'Finished' }, DelayedStart: { de: 'Startzeitvorwahl', en: 'Delayed start' },\n Pause: { de: 'Pausiert', en: 'Paused' }, Paused: { de: 'Pausiert', en: 'Paused' },\n ActionRequired: { de: 'Aktion erforderlich', en: 'Action required' }, Aborting: { de: 'Wird abgebrochen', en: 'Aborting' },\n Error: { de: 'Fehler', en: 'Error' }, Open: { de: 'Offen', en: 'Open' }, Closed: { de: 'Geschlossen', en: 'Closed' },\n Locked: { de: 'Verriegelt', en: 'Locked' }, On: { de: 'Ein', en: 'On' }, Off: { de: 'Aus', en: 'Off' },\n Standby: { de: 'Standby', en: 'Standby' }, Present: { de: 'Vorhanden', en: 'Present' }, Absent: { de: 'Nicht vorhanden', en: 'Absent' },\n Confirmed: { de: 'Bestätigt', en: 'Confirmed' }, Enabled: { de: 'Aktiviert', en: 'Enabled' }, Disabled: { de: 'Deaktiviert', en: 'Disabled' },\n Cleaning: { de: 'Reinigen', en: 'Cleaning' }, Rinsing: { de: 'Spülen', en: 'Rinsing' }, Drying: { de: 'Trocknen', en: 'Drying' },\n PreWash: { de: 'Vorspülen', en: 'Pre-wash' }, MainWash: { de: 'Hauptspülen', en: 'Main wash' },\n Heating: { de: 'Aufheizen', en: 'Heating' }, Cooling: { de: 'Abkühlen', en: 'Cooling' },\n DE: { de: 'Deutsch', en: 'German' }, EN: { de: 'Englisch', en: 'English' }\n };\n\n const displayNames = {\n ProgramProgress: { de: 'Programmfortschritt', en: 'Program progress' }, RemainingProgramTime: { de: 'Restzeit', en: 'Remaining program time' },\n EnergyForecast: { de: 'Energieprognose', en: 'Energy forecast' }, WaterForecast: { de: 'Wasserprognose', en: 'Water forecast' },\n StartInRelative: { de: 'Start in', en: 'Start in' }, Duration: { de: 'Dauer', en: 'Duration' },\n RemoteControlActive: { de: 'Fernsteuerung aktiv', en: 'Remote control active' },\n RemoteControlStartAllowed: { de: 'Fernstart erlaubt', en: 'Remote start allowed' }, LocalControlActive: { de: 'Lokale Bedienung aktiv', en: 'Local control active' },\n OperationState: { de: 'Betriebszustand', en: 'Operation state' }, DoorState: { de: 'Türzustand', en: 'Door state' },\n PowerState: { de: 'Betriebsart', en: 'Power state' }, ProgramPhase: { de: 'Programmphase', en: 'Program phase' },\n Language: { de: 'Sprache', en: 'Language' }, SaltNearlyEmpty: { de: 'Salz fast leer', en: 'Salt nearly empty' },\n RinseAidNearlyEmpty: { de: 'Klarspüler fast leer', en: 'Rinse aid nearly empty' },\n HygienePlus: { de: 'Hygiene Plus', en: 'Hygiene Plus' }, IntensivZone: { de: 'Intensivzone', en: 'Intensive zone' },\n VarioSpeedPlus: { de: 'varioSpeed Plus', en: 'varioSpeed Plus' }, BrillianceDry: { de: 'Glanztrocknen', en: 'Brilliance dry' },\n ExtraDry: { de: 'Extra trocknen', en: 'Extra dry' }, HalfLoad: { de: 'Halbe Beladung', en: 'Half load' },\n SilenceOnDemand: { de: 'Leisebetrieb', en: 'Silence on demand' }, Temperature: { de: 'Temperatur', en: 'Temperature' },\n SpinSpeed: { de: 'Schleuderdrehzahl', en: 'Spin speed' }, Stains: { de: 'Flecken', en: 'Stains' },\n SetpointTemperature: { de: 'Solltemperatur', en: 'Target temperature' }, FastPreHeat: { de: 'Schnellaufheizen', en: 'Fast preheat' },\n AddedSteam: { de: 'Dampfzugabe', en: 'Added steam' },\n\n Intensiv70: { de: 'Intensiv 70 °C', en: 'Intensive 70 °C' }, Auto2: { de: 'Automatik 45–65 °C', en: 'Auto 45–65 °C' },\n Eco50: { de: 'Eco 50 °C', en: 'Eco 50 °C' }, Glas40: { de: 'Glas 40 °C', en: 'Glass 40 °C' },\n Quick45: { de: 'Kurz 45 °C', en: 'Quick 45 °C' }, PreRinse: { de: 'Vorspülen', en: 'Pre-rinse' },\n MachineCare: { de: 'Maschinenpflege', en: 'Machine care' },\n Cotton: { de: 'Baumwolle', en: 'Cotton' }, EasyCare: { de: 'Pflegeleicht', en: 'Easy-care' }, Mix: { de: 'Mix', en: 'Mix' },\n DelicatesSilk: { de: 'Fein/Seide', en: 'Delicates/Silk' }, Wool: { de: 'Wolle', en: 'Wool' }, DarkWash: { de: 'Dunkle Wäsche', en: 'Dark wash' },\n ShirtsBlouses: { de: 'Hemden/Blusen', en: 'Shirts/Blouses' }, Curtains: { de: 'Gardinen', en: 'Curtains' }, Super15: { de: 'Super 15', en: 'Super 15' },\n Dessous: { de: 'Dessous', en: 'Lingerie' }, Outdoor: { de: 'Outdoor', en: 'Outdoor' }, Sensitive: { de: 'Sensitiv', en: 'Sensitive' },\n DrumClean: { de: 'Trommel reinigen', en: 'Drum clean' }, Rinse: { de: 'Spülen', en: 'Rinse' }, Spin: { de: 'Schleudern', en: 'Spin' },\n Drain: { de: 'Abpumpen', en: 'Drain' }, NightWash: { de: 'Nachtwäsche', en: 'Night wash' }, CottonEco: { de: 'Baumwolle Eco', en: 'Cotton Eco' },\n Memory1: { de: 'Favorit 1', en: 'Memory 1' }, JustStart: { de: 'Nur Start', en: 'Just start' },\n HotAir: { de: 'Heißluft', en: 'Hot air' }, TopBottomHeating: { de: 'Ober-/Unterhitze', en: 'Top/bottom heat' },\n PreHeating: { de: 'Vorheizen', en: 'Preheating' }, HotAirEco: { de: 'Heißluft Eco', en: 'Hot air Eco' },\n TopBottomHeatingEco: { de: 'Ober-/Unterhitze Eco', en: 'Top/bottom heat Eco' }, HotAirGrilling: { de: 'Umluftgrill', en: 'Hot-air grilling' },\n PizzaSetting: { de: 'Pizzastufe', en: 'Pizza setting' }, IntensiveHeat: { de: 'Intensivhitze', en: 'Intensive heat' },\n GrillLargeArea: { de: 'Großflächengrill', en: 'Large-area grill' }, GrillSmallArea: { de: 'Kleinflächengrill', en: 'Small-area grill' },\n SlowCook: { de: 'Sanftgaren', en: 'Slow cook' }, BottomHeating: { de: 'Unterhitze', en: 'Bottom heat' },\n KeepWarm: { de: 'Warmhalten', en: 'Keep warm' }, PreheatOvenware: { de: 'Geschirr vorwärmen', en: 'Preheat ovenware' },\n FrozenHeatupSpecial: { de: 'Tiefkühl-Spezial', en: 'Frozen food special' }, SabbathProgramme: { de: 'Sabbatprogramm', en: 'Sabbath program' },\n Steam: { de: 'Dampf', en: 'Steam' }, Reheat: { de: 'Aufwärmen', en: 'Reheat' }, DoughProving: { de: 'Gärstufe', en: 'Dough proving' },\n Defrosting: { de: 'Auftauen', en: 'Defrosting' }, Ecolysis: { de: 'Ecolyse', en: 'Ecolysis' }, Descale: { de: 'Entkalken', en: 'Descale' },\n Flushing: { de: 'Spülen', en: 'Flushing' }, Draining: { de: 'Abpumpen', en: 'Draining' }, SubsequentCooking: { de: 'Nachgaren', en: 'Subsequent cooking' },\n KuchenInFormen: { de: 'Kuchen in Formen', en: 'Cakes in tins' }, KuchenAufBackblech: { de: 'Kuchen auf Backblech', en: 'Tray-baked cakes' },\n Strudel: { de: 'Strudel', en: 'Strudel' }, Pavlova: { de: 'Pavlova', en: 'Pavlova' }, WaeheOderTart: { de: 'Wähe/Tarte', en: 'Flan/Tart' },\n Blaetterteiggebaeck: { de: 'Blätterteiggebäck', en: 'Puff pastry' }, Brandteiggebaeck: { de: 'Brandteiggebäck', en: 'Choux pastry' },\n Muffins: { de: 'Muffins', en: 'Muffins' }, Plundergebaeck: { de: 'Plundergebäck', en: 'Danish pastry' },\n Hefeteiggebaeck: { de: 'Hefeteiggebäck', en: 'Yeast pastries' }, Muerbeteiggebaeck: { de: 'Mürbeteiggebäck', en: 'Shortcrust pastry' },\n Baiser: { de: 'Baiser', en: 'Meringue' }, Makronen: { de: 'Makronen', en: 'Macaroons' }, Spritzgebaeck: { de: 'Spritzgebäck', en: 'Spritz biscuits' },\n Plaetzchen: { de: 'Plätzchen', en: 'Biscuits' }, Vollkornbrot: { de: 'Vollkornbrot', en: 'Wholegrain bread' },\n WeizenbrotWeizenmischbrotAufBackblech: { de: 'Weizen-/Weizenmischbrot auf Backblech', en: 'Wheat/mixed-wheat bread on tray' }\n };\n\n function unwrap(value) {\n if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return value.value;\n return value;\n }\n\n function normalizeLocale(value) {\n const text = String(unwrap(value) || '').trim().toLowerCase().replace('_', '-');\n if (text === 'de' || text.startsWith('de-')) return 'de';\n if (text === 'en' || text.startsWith('en-')) return 'en';\n return '';\n }\n\n function readStoredLocale() {\n try { return normalizeLocale(environment.localStorage?.getItem(STORAGE_KEY)); }\n catch { return ''; }\n }\n\n function detectLocale(component, message) {\n const msg = message || component?.msg || {};\n const candidates = [\n msg.uiLocale, msg.locale, readStoredLocale(), component?.$i18n?.locale, component?.$vuetify?.locale?.current,\n environment.document?.documentElement?.lang, ...(environment.navigator?.languages || []), environment.navigator?.language\n ];\n for (const candidate of candidates) {\n const locale = normalizeLocale(candidate);\n if (locale) return locale;\n }\n return 'en';\n }\n\n function setLocale(value, notify = true) {\n const locale = normalizeLocale(value) || 'en';\n try { environment.localStorage?.setItem(STORAGE_KEY, locale); }\n catch { /* Storage can be blocked by browser policy. */ }\n if (notify) {\n try { environment.dispatchEvent?.(new environment.CustomEvent(EVENT_NAME, { detail: { locale } })); }\n catch { /* CustomEvent may be unavailable in non-browser tests. */ }\n }\n return locale;\n }\n\n function t(key, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return labels[lang][key] ?? labels.en[key] ?? key;\n }\n\n function token(value) {\n return String(value ?? '').split('.').pop();\n }\n\n function humanize(value) {\n const leaf = token(value)\n .replace(/[_-]+/g, ' ')\n .replace(/([a-zäöüß])([A-Z])/g, '$1 $2')\n .replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n return leaf || '—';\n }\n\n function localized(entry, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return entry?.[lang] ?? entry?.en;\n }\n\n function deviceType(value, locale) {\n const leaf = token(value);\n const known = localized(deviceTypes[leaf], locale);\n if (known) return known;\n if (!leaf) return t('unavailable', locale);\n const prefix = normalizeLocale(locale) === 'de' ? 'Gerätetyp' : 'Appliance type';\n return `${prefix}: ${humanize(leaf)}`;\n }\n\n function displayKey(value, locale) {\n const leaf = token(value);\n return localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale) || humanize(leaf);\n }\n\n function displayName(value, locale) {\n const source = value && typeof value === 'object' ? (value.key || value.name) : value;\n if (source === undefined || source === null || source === '' || source === '—') return t('unavailable', locale);\n const leaf = token(source);\n const known = localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale);\n if (known) return known;\n const fallbackSource = value && typeof value === 'object' && value.name && !String(value.name).includes('.') ? value.name : leaf;\n return humanize(fallbackSource);\n }\n\n function unknown(rawValue, locale) {\n const printable = rawValue === undefined ? '?' : String(rawValue);\n return `${t('unknownValue', locale)} (${t('rawValue', locale)}: ${printable})`;\n }\n\n function semantic(item, locale) {\n if (!item) return t('unavailable', locale);\n if (item.valueResolved === false) return unknown(item.rawValue, locale);\n const value = item.value;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (value === true) return t('yes', locale);\n if (value === false) return t('no', locale);\n if (typeof value === 'number') return new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(value);\n return displayName(value, locale);\n }\n\n function booleanText(item, locale) {\n return semantic(item, locale);\n }\n\n function healthLabel(value, locale) {\n return localized(healthNames[value], locale) || humanize(value);\n }\n\n function warningText(value, locale) {\n if (!value) return '';\n const lang = normalizeLocale(locale) || 'en';\n return value.text?.[lang] || value.text?.en || value.text?.de || `${t('unknownWarning', lang)} (${value.code || '?'})`;\n }\n\n function remaining(model, locale) {\n const value = model?.remaining;\n const item = model?.remainingItem;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (item?.valueResolved === false) return unknown(item.rawValue, locale);\n const number = Number(value);\n if (!Number.isFinite(number)) return String(value);\n const unit = String(item?.unit || '').toLowerCase();\n if (unit.includes('min')) return `${Math.ceil(number)} min`;\n if (unit.includes('sec') || !model?.connection?.local) return `${Math.ceil(number / 60)} min`;\n const formatted = new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(number);\n return item?.unit ? `${formatted} ${item.unit}` : formatted;\n }\n\n function programItems(programs, locale) {\n return (Array.isArray(programs) ? programs : []).map(item => ({ ...item, title: displayName(item, locale) }));\n }\n\n return {\n STORAGE_KEY, EVENT_NAME, supported, normalizeLocale, detectLocale, setLocale, t, token, humanize,\n deviceType, displayKey, displayName, semantic, booleanText, healthLabel, warningText, remaining, programItems\n };\n};\nconst HC=createDashboardI18n(typeof window==='undefined'?{}:window);\nexport default {\n data(){return{kind:'Washer',locale:HC.detectLocale(),localeListener:null,program:null,temperature:180,boost:false,temperatureKey:'LaundryCare.Washer.EnumType.Temperature.GC40',washerTemperatures:[{title:'30 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC30'},{title:'40 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC40'},{title:'60 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC60'}]}},\n computed:{\n hcLabels(){return {overview:HC.t('overview',this.locale),connection:HC.t('connection',this.locale),status:HC.t('status',this.locale),door:HC.t('door',this.locale),active:HC.t('active',this.locale),selected:HC.t('selected',this.locale),remaining:HC.t('remaining',this.locale),localProfileReadOnly:HC.t('localProfileReadOnly',this.locale),remoteControl:HC.t('remoteControl',this.locale),remoteStart:HC.t('remoteStart',this.locale),localControl:HC.t('localControl',this.locale),currentOptions:HC.t('currentOptions',this.locale),availablePrograms:HC.t('availablePrograms',this.locale),program:HC.t('program',this.locale),targetTemperature:HC.t('targetTemperature',this.locale),temperature:HC.t('temperature',this.locale),refresh:HC.t('refresh',this.locale),start:HC.t('start',this.locale),stop:HC.t('stop',this.locale),openDetails:HC.t('openDetails',this.locale),backToOverview:HC.t('backToOverview',this.locale),lastKnownState:HC.t('lastKnownState',this.locale),remoteStartUnavailable:HC.t('remoteStartUnavailable',this.locale),language:HC.t('language',this.locale),online:HC.t('online',this.locale),offline:HC.t('offline',this.locale),simulated:HC.t('simulated',this.locale),simulatedValues:HC.t('simulatedValues',this.locale)}},model(){return this.msg?.payload||{}},appliance(){return this.model.appliance||{}},readOnly(){return this.model.readOnly===true},demoMode(){return this.model.simulated===true},statusOnly(){return this.readOnly||this.demoMode},\n deviceTitle(){return HC.deviceType(this.kind,this.locale)},applianceType(){return HC.deviceType(this.appliance.type||this.kind,this.locale)},\n health(){return this.model.health||'ok'},warnings(){return this.model.warnings||[]},visibleWarnings(){return this.warnings.slice(0,4)},\n healthColor(){return({ok:'success',running:'info',info:'info',warning:'warning',action_required:'warning',offline:'grey',error:'error'})[this.health]||'grey'},healthText(){return HC.healthLabel(this.health,this.locale)},\n online(){return this.appliance.connected!==false&&this.health!=='offline'},programs(){return HC.programItems(this.model.programs||[],this.locale)},\n remainingText(){return HC.remaining(this.model,this.locale)},running(){return this.health==='running'||String(this.model.state).endsWith('.Run')},\n canStart(){const s=this.model.status||{};return!this.statusOnly&&this.online&&!this.running&&this.health!=='error'&&this.health!=='action_required'&&s['BSH.Common.Status.RemoteControlActive']!==false&&s['BSH.Common.Status.RemoteControlStartAllowed']!==false&&s['BSH.Common.Status.LocalControlActive']!==true}\n },\n watch:{programs:{immediate:true,handler(value){if(!this.program&&value?.length)this.program=value[0].key}}},\n mounted(){this.locale=HC.detectLocale(this,this.msg);this.localeListener=(event)=>{this.locale=HC.normalizeLocale(event.detail?.locale)||this.locale};window.addEventListener(HC.EVENT_NAME,this.localeListener)},\n beforeUnmount(){if(this.localeListener)window.removeEventListener(HC.EVENT_NAME,this.localeListener)},\n methods:{\n displayName(value){return HC.displayName(value,this.locale)},displayKey(value){return HC.displayKey(value,this.locale)},semanticText(item){return HC.semantic(item,this.locale)},booleanText(item){return HC.booleanText(item,this.locale)},warningText(value){return HC.warningText(value,this.locale)},\n warningType(value){return value.severity==='critical'||value.severity==='error'?'error':value.severity==='warning'?'warning':'info'},\n refresh(){this.send({payload:{action:'refresh'}})},\n start(){const options=[];if(this.kind==='Oven')options.push({key:'Cooking.Oven.Option.SetpointTemperature',value:Number(this.temperature),unit:'°C'});if(this.kind==='Dishwasher'&&this.program==='Dishcare.Dishwasher.Program.Eco50')options.push({key:'Dishcare.Dishwasher.Option.VarioSpeedPlus',value:this.boost});if(this.kind==='Washer'&&this.program==='LaundryCare.Washer.Program.Cotton')options.push({key:'LaundryCare.Washer.Option.Temperature',value:this.temperatureKey});this.send({payload:{action:'start-program',program:this.program,options,confirm:'CONFIRM'}})},\n stop(){this.send({payload:{action:'stop-program',confirm:'CONFIRM'}})}\n }\n}\n</script>\n<style>.hc-card{border-top:4px solid rgb(var(--v-theme-primary));height:100%;scroll-margin-top:72px}.hc-state{margin-bottom:6px}.hc-device-info{padding:8px 0 12px;color:rgba(var(--v-theme-on-surface),.75)}.hc-control-hint{margin-top:8px;color:rgb(var(--v-theme-warning));font-size:.9rem}.hc-readonly-panel{margin-top:14px;padding:12px;border:1px solid rgba(var(--v-theme-on-surface),.18);border-radius:6px;background:rgba(var(--v-theme-on-surface),.035)}.hc-readonly-title{font-weight:600;margin-bottom:8px}.hc-option{padding-left:10px}</style>\n",
351
+ "storeOutMessages": true,
352
+ "passthru": false,
353
+ "resendOnRefresh": true,
354
+ "templateScope": "local",
355
+ "className": "",
356
+ "x": 670,
357
+ "y": 380,
358
+ "wires": [
359
+ [
360
+ "hc-route-wm"
361
+ ]
362
+ ]
363
+ },
364
+ {
365
+ "id": "hc-route-wm",
366
+ "type": "function",
367
+ "z": "hc-dashboard-flow",
368
+ "name": "Refresh or safe write",
369
+ "func": "if(msg.payload?.action==='refresh'){msg.payload={};return [msg,null];} msg.confirm=msg.payload?.confirm; return [null,msg];",
370
+ "outputs": 2,
371
+ "timeout": 0,
372
+ "noerr": 0,
373
+ "initialize": "",
374
+ "finalize": "",
375
+ "libs": [],
376
+ "x": 910,
377
+ "y": 380,
378
+ "wires": [
379
+ [
380
+ "hc-read-wm"
381
+ ],
382
+ [
383
+ "hc-write-wm"
384
+ ]
385
+ ]
386
+ },
387
+ {
388
+ "id": "hc-write-wm",
389
+ "type": "home-connect-write",
390
+ "z": "hc-dashboard-flow",
391
+ "name": "Safe write Waschmaschine",
392
+ "connection": "hc-sim-config",
393
+ "action": "from-message",
394
+ "haId": "washer-001",
395
+ "dryRun": false,
396
+ "confirmationMode": "token",
397
+ "allowedApplianceTypes": "Washer",
398
+ "x": 1180,
399
+ "y": 410,
400
+ "wires": [
401
+ [
402
+ "hc-read-wm"
403
+ ],
404
+ [
405
+ "hc-debug-error"
406
+ ]
407
+ ]
408
+ },
409
+ {
410
+ "id": "hc-read-ov",
411
+ "type": "home-connect-read",
412
+ "z": "hc-dashboard-flow",
413
+ "name": "Read Backofen",
414
+ "connection": "hc-sim-config",
415
+ "operation": "snapshot",
416
+ "haId": "oven-001",
417
+ "key": "",
418
+ "x": 170,
419
+ "y": 500,
420
+ "wires": [
421
+ [
422
+ "hc-dashboard-state"
423
+ ],
424
+ [
425
+ "hc-dashboard-state",
426
+ "hc-debug-error"
427
+ ]
428
+ ]
429
+ },
430
+ {
431
+ "id": "hc-ui-ov",
432
+ "type": "ui-template",
433
+ "z": "hc-dashboard-flow",
434
+ "group": "hc-group-ov",
435
+ "page": "",
436
+ "ui": "",
437
+ "name": "Backofen",
438
+ "order": 1,
439
+ "width": 0,
440
+ "height": 0,
441
+ "head": "",
442
+ "format": "<template>\n <v-card :id=\"'hc-'+kind.toLowerCase()\" class=\"hc-card\" elevation=\"2\">\n <v-card-title><v-icon class=\"mr-2\">mdi-stove</v-icon>{{ deviceTitle }}<v-spacer></v-spacer><v-chip v-if=\"demoMode\" color=\"secondary\" size=\"small\" class=\"mr-2\">{{ hcLabels.simulated }}</v-chip><v-chip :color=\"healthColor\" size=\"small\">{{ healthText }}</v-chip></v-card-title>\n <v-card-text>\n <div class=\"hc-device-info\"><b>{{ appliance.name || deviceTitle }}</b><br>{{ appliance.brand || '—' }} · {{ applianceType }}<br><span v-if=\"appliance.vib\">VIB/E-Nummer: {{ appliance.vib }}</span><br><span>{{ hcLabels.connection }}: {{ online ? hcLabels.online : hcLabels.offline }}</span><span v-if=\"model.stale\"> · {{ hcLabels.lastKnownState }}</span></div>\n <v-alert v-for=\"warning in visibleWarnings\" :key=\"warning.source+warning.code+(warning.key||'')\" :type=\"warningType(warning)\" density=\"compact\" variant=\"tonal\" class=\"mb-2\"><b>{{ warningText(warning) }}</b></v-alert>\n <div class=\"hc-state\"><b>{{ hcLabels.status }}:</b> {{ semanticText(model.stateItem) }}</div>\n <div><b>{{ hcLabels.door }}:</b> {{ semanticText(model.doorItem) }}</div>\n <div><b>{{ hcLabels.active }}:</b> {{ displayName(model.active) }}</div>\n <div><b>{{ hcLabels.selected }}:</b> {{ displayName(model.selected) }}</div>\n <div><b>{{ hcLabels.remaining }}:</b> {{ remainingText }}</div>\n <div v-if=\"statusOnly\" class=\"hc-readonly-panel\">\n <div class=\"hc-readonly-title\">{{ demoMode ? hcLabels.simulatedValues : hcLabels.localProfileReadOnly }}</div>\n <div><b>{{ hcLabels.remoteControl }}:</b> {{ booleanText(model.remoteControlItem) }}</div>\n <div><b>{{ hcLabels.remoteStart }}:</b> {{ booleanText(model.remoteStartItem) }}</div>\n <div><b>{{ hcLabels.localControl }}:</b> {{ booleanText(model.localControlItem) }}</div>\n <div v-if=\"model.options?.length\" class=\"mt-2\"><b>{{ hcLabels.currentOptions }}:</b><div v-for=\"item in model.options\" :key=\"item.key\" class=\"hc-option\"><span :title=\"item.key\">{{ displayKey(item.key) }}</span>: {{ semanticText(item) }}<span v-if=\"item.unit\"> {{ item.unit }}</span></div></div>\n <div v-if=\"programs.length\" class=\"mt-2\"><b>{{ hcLabels.availablePrograms }}:</b> {{ programs.map(p=>p.title).join(', ') }}</div>\n </div>\n <template v-else>\n <v-select class=\"mt-4\" v-model=\"program\" :items=\"programs\" item-title=\"title\" item-value=\"key\" :label=\"hcLabels.program\" density=\"compact\"></v-select>\n <v-text-field v-if=\"kind === 'Oven'\" v-model.number=\"temperature\" :label=\"hcLabels.targetTemperature+' °C'\" type=\"number\" min=\"30\" max=\"250\" step=\"5\" density=\"compact\"></v-text-field>\n <v-checkbox v-if=\"kind === 'Dishwasher'\" v-model=\"boost\" :label=\"displayKey('Dishcare.Dishwasher.Option.VarioSpeedPlus')\" density=\"compact\"></v-checkbox>\n <v-select v-if=\"kind === 'Washer'\" v-model=\"temperatureKey\" :items=\"washerTemperatures\" item-title=\"title\" item-value=\"value\" :label=\"hcLabels.temperature\" density=\"compact\"></v-select>\n <div v-if=\"!canStart && !running\" class=\"hc-control-hint\">{{ hcLabels.remoteStartUnavailable }}</div>\n </template>\n </v-card-text>\n <v-card-actions>\n <template v-if=\"!statusOnly\"><v-btn color=\"primary\" :disabled=\"!program || !canStart\" @click=\"start\">{{ hcLabels.start }}</v-btn><v-btn color=\"error\" variant=\"outlined\" :disabled=\"!running || !online\" @click=\"stop\">{{ hcLabels.stop }}</v-btn></template>\n <v-btn variant=\"text\" @click=\"refresh\">{{ hcLabels.refresh }}</v-btn>\n </v-card-actions>\n </v-card>\n</template>\n<script>\nconst createDashboardI18n=function createDashboardI18n(environment = {}, options = {}) {\n const STORAGE_KEY = options.storageKey || 'home-connect-dashboard-language';\n const EVENT_NAME = options.eventName || 'home-connect-language-changed';\n const supported = ['de', 'en'];\n\n const labels = {\n de: {\n overview: 'Übersicht', applianceDetails: 'Gerätedetails', connection: 'Verbindung', status: 'Status', door: 'Tür', active: 'Aktiv',\n selected: 'Ausgewählt', remaining: 'Restzeit', localProfileReadOnly: 'Lokale Profilwerte · nur lesend', remoteControl: 'Fernsteuerung',\n remoteStart: 'Fernstart', localControl: 'Lokale Bedienung', currentOptions: 'Aktuelle Optionen', availablePrograms: 'Verfügbare Programme',\n program: 'Programm', targetTemperature: 'Solltemperatur', temperature: 'Temperatur', refresh: 'Aktualisieren', start: 'Start', stop: 'Stopp',\n openDetails: 'Details öffnen', backToOverview: 'Zur Übersicht', lastKnownState: 'letzter bekannter Stand', remoteStartUnavailable: 'Fernstart ist aktuell nicht verfügbar.',\n language: 'Sprache', german: 'Deutsch', english: 'Englisch', online: 'online', offline: 'offline', yes: 'Ja', no: 'Nein',\n unavailable: '—', unknownValue: 'Unbekannter Wert', rawValue: 'Rohwert', unknownWarning: 'Unbekannter Hinweis',\n readOnly: 'nur lesend', simulated: 'SIMULIERT', simulatedValues: 'Simulierte Beispielwerte · nur Anzeige'\n },\n en: {\n overview: 'Overview', applianceDetails: 'Appliance details', connection: 'Connection', status: 'Status', door: 'Door', active: 'Active',\n selected: 'Selected', remaining: 'Remaining', localProfileReadOnly: 'Local profile values · read-only', remoteControl: 'Remote control',\n remoteStart: 'Remote start', localControl: 'Local control', currentOptions: 'Current options', availablePrograms: 'Available programs',\n program: 'Program', targetTemperature: 'Target temperature', temperature: 'Temperature', refresh: 'Refresh', start: 'Start', stop: 'Stop',\n openDetails: 'Open details', backToOverview: 'Back to overview', lastKnownState: 'last known state', remoteStartUnavailable: 'Remote start is currently unavailable.',\n language: 'Language', german: 'German', english: 'English', online: 'online', offline: 'offline', yes: 'Yes', no: 'No',\n unavailable: '—', unknownValue: 'Unknown value', rawValue: 'raw value', unknownWarning: 'Unknown notice',\n readOnly: 'read-only', simulated: 'SIMULATED', simulatedValues: 'Simulated example values · display only'\n }\n };\n\n const healthNames = {\n ok: { de: 'OK', en: 'OK' }, running: { de: 'Läuft', en: 'Running' }, info: { de: 'Info', en: 'Info' },\n warning: { de: 'Warnung', en: 'Warning' }, action_required: { de: 'Aktion nötig', en: 'Action required' },\n offline: { de: 'Offline', en: 'Offline' }, error: { de: 'Fehler', en: 'Error' }\n };\n\n const deviceTypes = {\n Dishwasher: { de: 'Geschirrspüler', en: 'Dishwasher' }, Washer: { de: 'Waschmaschine', en: 'Washer' },\n Oven: { de: 'Backofen', en: 'Oven' }, Dryer: { de: 'Trockner', en: 'Dryer' },\n WasherDryer: { de: 'Waschtrockner', en: 'Washer-dryer' }, Refrigerator: { de: 'Kühlschrank', en: 'Refrigerator' },\n Freezer: { de: 'Gefrierschrank', en: 'Freezer' }, FridgeFreezer: { de: 'Kühl-Gefrier-Kombination', en: 'Fridge-freezer' },\n CoffeeMaker: { de: 'Kaffeevollautomat', en: 'Coffee maker' }, Hood: { de: 'Dunstabzugshaube', en: 'Hood' },\n Hob: { de: 'Kochfeld', en: 'Hob' }\n };\n\n const valueNames = {\n Inactive: { de: 'Inaktiv', en: 'Inactive' }, Ready: { de: 'Bereit', en: 'Ready' }, Run: { de: 'Läuft', en: 'Running' },\n Finished: { de: 'Beendet', en: 'Finished' }, DelayedStart: { de: 'Startzeitvorwahl', en: 'Delayed start' },\n Pause: { de: 'Pausiert', en: 'Paused' }, Paused: { de: 'Pausiert', en: 'Paused' },\n ActionRequired: { de: 'Aktion erforderlich', en: 'Action required' }, Aborting: { de: 'Wird abgebrochen', en: 'Aborting' },\n Error: { de: 'Fehler', en: 'Error' }, Open: { de: 'Offen', en: 'Open' }, Closed: { de: 'Geschlossen', en: 'Closed' },\n Locked: { de: 'Verriegelt', en: 'Locked' }, On: { de: 'Ein', en: 'On' }, Off: { de: 'Aus', en: 'Off' },\n Standby: { de: 'Standby', en: 'Standby' }, Present: { de: 'Vorhanden', en: 'Present' }, Absent: { de: 'Nicht vorhanden', en: 'Absent' },\n Confirmed: { de: 'Bestätigt', en: 'Confirmed' }, Enabled: { de: 'Aktiviert', en: 'Enabled' }, Disabled: { de: 'Deaktiviert', en: 'Disabled' },\n Cleaning: { de: 'Reinigen', en: 'Cleaning' }, Rinsing: { de: 'Spülen', en: 'Rinsing' }, Drying: { de: 'Trocknen', en: 'Drying' },\n PreWash: { de: 'Vorspülen', en: 'Pre-wash' }, MainWash: { de: 'Hauptspülen', en: 'Main wash' },\n Heating: { de: 'Aufheizen', en: 'Heating' }, Cooling: { de: 'Abkühlen', en: 'Cooling' },\n DE: { de: 'Deutsch', en: 'German' }, EN: { de: 'Englisch', en: 'English' }\n };\n\n const displayNames = {\n ProgramProgress: { de: 'Programmfortschritt', en: 'Program progress' }, RemainingProgramTime: { de: 'Restzeit', en: 'Remaining program time' },\n EnergyForecast: { de: 'Energieprognose', en: 'Energy forecast' }, WaterForecast: { de: 'Wasserprognose', en: 'Water forecast' },\n StartInRelative: { de: 'Start in', en: 'Start in' }, Duration: { de: 'Dauer', en: 'Duration' },\n RemoteControlActive: { de: 'Fernsteuerung aktiv', en: 'Remote control active' },\n RemoteControlStartAllowed: { de: 'Fernstart erlaubt', en: 'Remote start allowed' }, LocalControlActive: { de: 'Lokale Bedienung aktiv', en: 'Local control active' },\n OperationState: { de: 'Betriebszustand', en: 'Operation state' }, DoorState: { de: 'Türzustand', en: 'Door state' },\n PowerState: { de: 'Betriebsart', en: 'Power state' }, ProgramPhase: { de: 'Programmphase', en: 'Program phase' },\n Language: { de: 'Sprache', en: 'Language' }, SaltNearlyEmpty: { de: 'Salz fast leer', en: 'Salt nearly empty' },\n RinseAidNearlyEmpty: { de: 'Klarspüler fast leer', en: 'Rinse aid nearly empty' },\n HygienePlus: { de: 'Hygiene Plus', en: 'Hygiene Plus' }, IntensivZone: { de: 'Intensivzone', en: 'Intensive zone' },\n VarioSpeedPlus: { de: 'varioSpeed Plus', en: 'varioSpeed Plus' }, BrillianceDry: { de: 'Glanztrocknen', en: 'Brilliance dry' },\n ExtraDry: { de: 'Extra trocknen', en: 'Extra dry' }, HalfLoad: { de: 'Halbe Beladung', en: 'Half load' },\n SilenceOnDemand: { de: 'Leisebetrieb', en: 'Silence on demand' }, Temperature: { de: 'Temperatur', en: 'Temperature' },\n SpinSpeed: { de: 'Schleuderdrehzahl', en: 'Spin speed' }, Stains: { de: 'Flecken', en: 'Stains' },\n SetpointTemperature: { de: 'Solltemperatur', en: 'Target temperature' }, FastPreHeat: { de: 'Schnellaufheizen', en: 'Fast preheat' },\n AddedSteam: { de: 'Dampfzugabe', en: 'Added steam' },\n\n Intensiv70: { de: 'Intensiv 70 °C', en: 'Intensive 70 °C' }, Auto2: { de: 'Automatik 45–65 °C', en: 'Auto 45–65 °C' },\n Eco50: { de: 'Eco 50 °C', en: 'Eco 50 °C' }, Glas40: { de: 'Glas 40 °C', en: 'Glass 40 °C' },\n Quick45: { de: 'Kurz 45 °C', en: 'Quick 45 °C' }, PreRinse: { de: 'Vorspülen', en: 'Pre-rinse' },\n MachineCare: { de: 'Maschinenpflege', en: 'Machine care' },\n Cotton: { de: 'Baumwolle', en: 'Cotton' }, EasyCare: { de: 'Pflegeleicht', en: 'Easy-care' }, Mix: { de: 'Mix', en: 'Mix' },\n DelicatesSilk: { de: 'Fein/Seide', en: 'Delicates/Silk' }, Wool: { de: 'Wolle', en: 'Wool' }, DarkWash: { de: 'Dunkle Wäsche', en: 'Dark wash' },\n ShirtsBlouses: { de: 'Hemden/Blusen', en: 'Shirts/Blouses' }, Curtains: { de: 'Gardinen', en: 'Curtains' }, Super15: { de: 'Super 15', en: 'Super 15' },\n Dessous: { de: 'Dessous', en: 'Lingerie' }, Outdoor: { de: 'Outdoor', en: 'Outdoor' }, Sensitive: { de: 'Sensitiv', en: 'Sensitive' },\n DrumClean: { de: 'Trommel reinigen', en: 'Drum clean' }, Rinse: { de: 'Spülen', en: 'Rinse' }, Spin: { de: 'Schleudern', en: 'Spin' },\n Drain: { de: 'Abpumpen', en: 'Drain' }, NightWash: { de: 'Nachtwäsche', en: 'Night wash' }, CottonEco: { de: 'Baumwolle Eco', en: 'Cotton Eco' },\n Memory1: { de: 'Favorit 1', en: 'Memory 1' }, JustStart: { de: 'Nur Start', en: 'Just start' },\n HotAir: { de: 'Heißluft', en: 'Hot air' }, TopBottomHeating: { de: 'Ober-/Unterhitze', en: 'Top/bottom heat' },\n PreHeating: { de: 'Vorheizen', en: 'Preheating' }, HotAirEco: { de: 'Heißluft Eco', en: 'Hot air Eco' },\n TopBottomHeatingEco: { de: 'Ober-/Unterhitze Eco', en: 'Top/bottom heat Eco' }, HotAirGrilling: { de: 'Umluftgrill', en: 'Hot-air grilling' },\n PizzaSetting: { de: 'Pizzastufe', en: 'Pizza setting' }, IntensiveHeat: { de: 'Intensivhitze', en: 'Intensive heat' },\n GrillLargeArea: { de: 'Großflächengrill', en: 'Large-area grill' }, GrillSmallArea: { de: 'Kleinflächengrill', en: 'Small-area grill' },\n SlowCook: { de: 'Sanftgaren', en: 'Slow cook' }, BottomHeating: { de: 'Unterhitze', en: 'Bottom heat' },\n KeepWarm: { de: 'Warmhalten', en: 'Keep warm' }, PreheatOvenware: { de: 'Geschirr vorwärmen', en: 'Preheat ovenware' },\n FrozenHeatupSpecial: { de: 'Tiefkühl-Spezial', en: 'Frozen food special' }, SabbathProgramme: { de: 'Sabbatprogramm', en: 'Sabbath program' },\n Steam: { de: 'Dampf', en: 'Steam' }, Reheat: { de: 'Aufwärmen', en: 'Reheat' }, DoughProving: { de: 'Gärstufe', en: 'Dough proving' },\n Defrosting: { de: 'Auftauen', en: 'Defrosting' }, Ecolysis: { de: 'Ecolyse', en: 'Ecolysis' }, Descale: { de: 'Entkalken', en: 'Descale' },\n Flushing: { de: 'Spülen', en: 'Flushing' }, Draining: { de: 'Abpumpen', en: 'Draining' }, SubsequentCooking: { de: 'Nachgaren', en: 'Subsequent cooking' },\n KuchenInFormen: { de: 'Kuchen in Formen', en: 'Cakes in tins' }, KuchenAufBackblech: { de: 'Kuchen auf Backblech', en: 'Tray-baked cakes' },\n Strudel: { de: 'Strudel', en: 'Strudel' }, Pavlova: { de: 'Pavlova', en: 'Pavlova' }, WaeheOderTart: { de: 'Wähe/Tarte', en: 'Flan/Tart' },\n Blaetterteiggebaeck: { de: 'Blätterteiggebäck', en: 'Puff pastry' }, Brandteiggebaeck: { de: 'Brandteiggebäck', en: 'Choux pastry' },\n Muffins: { de: 'Muffins', en: 'Muffins' }, Plundergebaeck: { de: 'Plundergebäck', en: 'Danish pastry' },\n Hefeteiggebaeck: { de: 'Hefeteiggebäck', en: 'Yeast pastries' }, Muerbeteiggebaeck: { de: 'Mürbeteiggebäck', en: 'Shortcrust pastry' },\n Baiser: { de: 'Baiser', en: 'Meringue' }, Makronen: { de: 'Makronen', en: 'Macaroons' }, Spritzgebaeck: { de: 'Spritzgebäck', en: 'Spritz biscuits' },\n Plaetzchen: { de: 'Plätzchen', en: 'Biscuits' }, Vollkornbrot: { de: 'Vollkornbrot', en: 'Wholegrain bread' },\n WeizenbrotWeizenmischbrotAufBackblech: { de: 'Weizen-/Weizenmischbrot auf Backblech', en: 'Wheat/mixed-wheat bread on tray' }\n };\n\n function unwrap(value) {\n if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return value.value;\n return value;\n }\n\n function normalizeLocale(value) {\n const text = String(unwrap(value) || '').trim().toLowerCase().replace('_', '-');\n if (text === 'de' || text.startsWith('de-')) return 'de';\n if (text === 'en' || text.startsWith('en-')) return 'en';\n return '';\n }\n\n function readStoredLocale() {\n try { return normalizeLocale(environment.localStorage?.getItem(STORAGE_KEY)); }\n catch { return ''; }\n }\n\n function detectLocale(component, message) {\n const msg = message || component?.msg || {};\n const candidates = [\n msg.uiLocale, msg.locale, readStoredLocale(), component?.$i18n?.locale, component?.$vuetify?.locale?.current,\n environment.document?.documentElement?.lang, ...(environment.navigator?.languages || []), environment.navigator?.language\n ];\n for (const candidate of candidates) {\n const locale = normalizeLocale(candidate);\n if (locale) return locale;\n }\n return 'en';\n }\n\n function setLocale(value, notify = true) {\n const locale = normalizeLocale(value) || 'en';\n try { environment.localStorage?.setItem(STORAGE_KEY, locale); }\n catch { /* Storage can be blocked by browser policy. */ }\n if (notify) {\n try { environment.dispatchEvent?.(new environment.CustomEvent(EVENT_NAME, { detail: { locale } })); }\n catch { /* CustomEvent may be unavailable in non-browser tests. */ }\n }\n return locale;\n }\n\n function t(key, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return labels[lang][key] ?? labels.en[key] ?? key;\n }\n\n function token(value) {\n return String(value ?? '').split('.').pop();\n }\n\n function humanize(value) {\n const leaf = token(value)\n .replace(/[_-]+/g, ' ')\n .replace(/([a-zäöüß])([A-Z])/g, '$1 $2')\n .replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n return leaf || '—';\n }\n\n function localized(entry, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return entry?.[lang] ?? entry?.en;\n }\n\n function deviceType(value, locale) {\n const leaf = token(value);\n const known = localized(deviceTypes[leaf], locale);\n if (known) return known;\n if (!leaf) return t('unavailable', locale);\n const prefix = normalizeLocale(locale) === 'de' ? 'Gerätetyp' : 'Appliance type';\n return `${prefix}: ${humanize(leaf)}`;\n }\n\n function displayKey(value, locale) {\n const leaf = token(value);\n return localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale) || humanize(leaf);\n }\n\n function displayName(value, locale) {\n const source = value && typeof value === 'object' ? (value.key || value.name) : value;\n if (source === undefined || source === null || source === '' || source === '—') return t('unavailable', locale);\n const leaf = token(source);\n const known = localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale);\n if (known) return known;\n const fallbackSource = value && typeof value === 'object' && value.name && !String(value.name).includes('.') ? value.name : leaf;\n return humanize(fallbackSource);\n }\n\n function unknown(rawValue, locale) {\n const printable = rawValue === undefined ? '?' : String(rawValue);\n return `${t('unknownValue', locale)} (${t('rawValue', locale)}: ${printable})`;\n }\n\n function semantic(item, locale) {\n if (!item) return t('unavailable', locale);\n if (item.valueResolved === false) return unknown(item.rawValue, locale);\n const value = item.value;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (value === true) return t('yes', locale);\n if (value === false) return t('no', locale);\n if (typeof value === 'number') return new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(value);\n return displayName(value, locale);\n }\n\n function booleanText(item, locale) {\n return semantic(item, locale);\n }\n\n function healthLabel(value, locale) {\n return localized(healthNames[value], locale) || humanize(value);\n }\n\n function warningText(value, locale) {\n if (!value) return '';\n const lang = normalizeLocale(locale) || 'en';\n return value.text?.[lang] || value.text?.en || value.text?.de || `${t('unknownWarning', lang)} (${value.code || '?'})`;\n }\n\n function remaining(model, locale) {\n const value = model?.remaining;\n const item = model?.remainingItem;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (item?.valueResolved === false) return unknown(item.rawValue, locale);\n const number = Number(value);\n if (!Number.isFinite(number)) return String(value);\n const unit = String(item?.unit || '').toLowerCase();\n if (unit.includes('min')) return `${Math.ceil(number)} min`;\n if (unit.includes('sec') || !model?.connection?.local) return `${Math.ceil(number / 60)} min`;\n const formatted = new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(number);\n return item?.unit ? `${formatted} ${item.unit}` : formatted;\n }\n\n function programItems(programs, locale) {\n return (Array.isArray(programs) ? programs : []).map(item => ({ ...item, title: displayName(item, locale) }));\n }\n\n return {\n STORAGE_KEY, EVENT_NAME, supported, normalizeLocale, detectLocale, setLocale, t, token, humanize,\n deviceType, displayKey, displayName, semantic, booleanText, healthLabel, warningText, remaining, programItems\n };\n};\nconst HC=createDashboardI18n(typeof window==='undefined'?{}:window);\nexport default {\n data(){return{kind:'Oven',locale:HC.detectLocale(),localeListener:null,program:null,temperature:180,boost:false,temperatureKey:'LaundryCare.Washer.EnumType.Temperature.GC40',washerTemperatures:[{title:'30 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC30'},{title:'40 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC40'},{title:'60 °C',value:'LaundryCare.Washer.EnumType.Temperature.GC60'}]}},\n computed:{\n hcLabels(){return {overview:HC.t('overview',this.locale),connection:HC.t('connection',this.locale),status:HC.t('status',this.locale),door:HC.t('door',this.locale),active:HC.t('active',this.locale),selected:HC.t('selected',this.locale),remaining:HC.t('remaining',this.locale),localProfileReadOnly:HC.t('localProfileReadOnly',this.locale),remoteControl:HC.t('remoteControl',this.locale),remoteStart:HC.t('remoteStart',this.locale),localControl:HC.t('localControl',this.locale),currentOptions:HC.t('currentOptions',this.locale),availablePrograms:HC.t('availablePrograms',this.locale),program:HC.t('program',this.locale),targetTemperature:HC.t('targetTemperature',this.locale),temperature:HC.t('temperature',this.locale),refresh:HC.t('refresh',this.locale),start:HC.t('start',this.locale),stop:HC.t('stop',this.locale),openDetails:HC.t('openDetails',this.locale),backToOverview:HC.t('backToOverview',this.locale),lastKnownState:HC.t('lastKnownState',this.locale),remoteStartUnavailable:HC.t('remoteStartUnavailable',this.locale),language:HC.t('language',this.locale),online:HC.t('online',this.locale),offline:HC.t('offline',this.locale),simulated:HC.t('simulated',this.locale),simulatedValues:HC.t('simulatedValues',this.locale)}},model(){return this.msg?.payload||{}},appliance(){return this.model.appliance||{}},readOnly(){return this.model.readOnly===true},demoMode(){return this.model.simulated===true},statusOnly(){return this.readOnly||this.demoMode},\n deviceTitle(){return HC.deviceType(this.kind,this.locale)},applianceType(){return HC.deviceType(this.appliance.type||this.kind,this.locale)},\n health(){return this.model.health||'ok'},warnings(){return this.model.warnings||[]},visibleWarnings(){return this.warnings.slice(0,4)},\n healthColor(){return({ok:'success',running:'info',info:'info',warning:'warning',action_required:'warning',offline:'grey',error:'error'})[this.health]||'grey'},healthText(){return HC.healthLabel(this.health,this.locale)},\n online(){return this.appliance.connected!==false&&this.health!=='offline'},programs(){return HC.programItems(this.model.programs||[],this.locale)},\n remainingText(){return HC.remaining(this.model,this.locale)},running(){return this.health==='running'||String(this.model.state).endsWith('.Run')},\n canStart(){const s=this.model.status||{};return!this.statusOnly&&this.online&&!this.running&&this.health!=='error'&&this.health!=='action_required'&&s['BSH.Common.Status.RemoteControlActive']!==false&&s['BSH.Common.Status.RemoteControlStartAllowed']!==false&&s['BSH.Common.Status.LocalControlActive']!==true}\n },\n watch:{programs:{immediate:true,handler(value){if(!this.program&&value?.length)this.program=value[0].key}}},\n mounted(){this.locale=HC.detectLocale(this,this.msg);this.localeListener=(event)=>{this.locale=HC.normalizeLocale(event.detail?.locale)||this.locale};window.addEventListener(HC.EVENT_NAME,this.localeListener)},\n beforeUnmount(){if(this.localeListener)window.removeEventListener(HC.EVENT_NAME,this.localeListener)},\n methods:{\n displayName(value){return HC.displayName(value,this.locale)},displayKey(value){return HC.displayKey(value,this.locale)},semanticText(item){return HC.semantic(item,this.locale)},booleanText(item){return HC.booleanText(item,this.locale)},warningText(value){return HC.warningText(value,this.locale)},\n warningType(value){return value.severity==='critical'||value.severity==='error'?'error':value.severity==='warning'?'warning':'info'},\n refresh(){this.send({payload:{action:'refresh'}})},\n start(){const options=[];if(this.kind==='Oven')options.push({key:'Cooking.Oven.Option.SetpointTemperature',value:Number(this.temperature),unit:'°C'});if(this.kind==='Dishwasher'&&this.program==='Dishcare.Dishwasher.Program.Eco50')options.push({key:'Dishcare.Dishwasher.Option.VarioSpeedPlus',value:this.boost});if(this.kind==='Washer'&&this.program==='LaundryCare.Washer.Program.Cotton')options.push({key:'LaundryCare.Washer.Option.Temperature',value:this.temperatureKey});this.send({payload:{action:'start-program',program:this.program,options,confirm:'CONFIRM'}})},\n stop(){this.send({payload:{action:'stop-program',confirm:'CONFIRM'}})}\n }\n}\n</script>\n<style>.hc-card{border-top:4px solid rgb(var(--v-theme-primary));height:100%;scroll-margin-top:72px}.hc-state{margin-bottom:6px}.hc-device-info{padding:8px 0 12px;color:rgba(var(--v-theme-on-surface),.75)}.hc-control-hint{margin-top:8px;color:rgb(var(--v-theme-warning));font-size:.9rem}.hc-readonly-panel{margin-top:14px;padding:12px;border:1px solid rgba(var(--v-theme-on-surface),.18);border-radius:6px;background:rgba(var(--v-theme-on-surface),.035)}.hc-readonly-title{font-weight:600;margin-bottom:8px}.hc-option{padding-left:10px}</style>\n",
443
+ "storeOutMessages": true,
444
+ "passthru": false,
445
+ "resendOnRefresh": true,
446
+ "templateScope": "local",
447
+ "className": "",
448
+ "x": 670,
449
+ "y": 500,
450
+ "wires": [
451
+ [
452
+ "hc-route-ov"
453
+ ]
454
+ ]
455
+ },
456
+ {
457
+ "id": "hc-route-ov",
458
+ "type": "function",
459
+ "z": "hc-dashboard-flow",
460
+ "name": "Refresh or safe write",
461
+ "func": "if(msg.payload?.action==='refresh'){msg.payload={};return [msg,null];} msg.confirm=msg.payload?.confirm; return [null,msg];",
462
+ "outputs": 2,
463
+ "timeout": 0,
464
+ "noerr": 0,
465
+ "initialize": "",
466
+ "finalize": "",
467
+ "libs": [],
468
+ "x": 910,
469
+ "y": 500,
470
+ "wires": [
471
+ [
472
+ "hc-read-ov"
473
+ ],
474
+ [
475
+ "hc-write-ov"
476
+ ]
477
+ ]
478
+ },
479
+ {
480
+ "id": "hc-write-ov",
481
+ "type": "home-connect-write",
482
+ "z": "hc-dashboard-flow",
483
+ "name": "Safe write Backofen",
484
+ "connection": "hc-sim-config",
485
+ "action": "from-message",
486
+ "haId": "oven-001",
487
+ "dryRun": false,
488
+ "confirmationMode": "token",
489
+ "allowedApplianceTypes": "Oven",
490
+ "x": 1180,
491
+ "y": 530,
492
+ "wires": [
493
+ [
494
+ "hc-read-ov"
495
+ ],
496
+ [
497
+ "hc-debug-error"
498
+ ]
499
+ ]
500
+ },
501
+ {
502
+ "id": "hc-debug-error",
503
+ "type": "debug",
504
+ "z": "hc-dashboard-flow",
505
+ "name": "Home Connect errors",
506
+ "active": true,
507
+ "tosidebar": true,
508
+ "console": false,
509
+ "tostatus": false,
510
+ "complete": "payload",
511
+ "targetType": "msg",
512
+ "statusVal": "",
513
+ "statusType": "auto",
514
+ "x": 1180,
515
+ "y": 620,
516
+ "wires": []
517
+ },
518
+ {
519
+ "id": "hc-ui-overview-page",
520
+ "type": "ui-page",
521
+ "name": "Home Connect",
522
+ "ui": "hc-ui-base",
523
+ "path": "/overview",
524
+ "icon": "home",
525
+ "layout": "grid",
526
+ "theme": "hc-ui-theme",
527
+ "breakpoints": [
528
+ {
529
+ "name": "Default",
530
+ "px": "0",
531
+ "cols": "3"
532
+ },
533
+ {
534
+ "name": "Tablet",
535
+ "px": "576",
536
+ "cols": "6"
537
+ },
538
+ {
539
+ "name": "Small Desktop",
540
+ "px": "768",
541
+ "cols": "9"
542
+ },
543
+ {
544
+ "name": "Desktop",
545
+ "px": "1024",
546
+ "cols": "12"
547
+ }
548
+ ],
549
+ "order": 1,
550
+ "className": "",
551
+ "visible": "true",
552
+ "disabled": "false"
553
+ },
554
+ {
555
+ "id": "hc-ui-overview-group",
556
+ "type": "ui-group",
557
+ "name": "Home Connect Übersicht",
558
+ "page": "hc-ui-overview-page",
559
+ "width": "12",
560
+ "height": "1",
561
+ "order": 1,
562
+ "showTitle": false,
563
+ "className": "",
564
+ "visible": "true",
565
+ "disabled": "false"
566
+ },
567
+ {
568
+ "id": "hc-ui-overview",
569
+ "type": "ui-template",
570
+ "z": "hc-dashboard-flow",
571
+ "group": "hc-ui-overview-group",
572
+ "page": "",
573
+ "ui": "",
574
+ "name": "Kompakte Gerätekarten",
575
+ "order": 1,
576
+ "width": 0,
577
+ "height": 0,
578
+ "head": "",
579
+ "format": "<template>\n <div>\n <div class=\"hc-toolbar\"><div class=\"hc-heading\">{{ hcLabels.overview }}</div><v-select v-model=\"locale\" :items=\"languageItems\" item-title=\"title\" item-value=\"value\" :label=\"hcLabels.language\" density=\"compact\" hide-details class=\"hc-language\" @update:model-value=\"setLanguage\"></v-select></div>\n <div class=\"hc-overview-grid\">\n <v-card v-for=\"device in cards\" :key=\"device.key\" class=\"hc-overview-card\" elevation=\"2\" role=\"link\" tabindex=\"0\" @click=\"openDetails(device)\" @keyup.enter=\"openDetails(device)\">\n <v-card-title><v-icon class=\"mr-2\">{{ device.icon }}</v-icon>{{ device.title }}<v-spacer></v-spacer><v-chip v-if=\"device.model.simulated\" color=\"secondary\" size=\"small\" class=\"mr-2\">{{ hcLabels.simulated }}</v-chip><v-chip :color=\"healthColor(device.health)\" size=\"small\">{{ healthText(device.health) }}</v-chip></v-card-title>\n <v-card-text>\n <div class=\"hc-meta\">{{ device.model.appliance?.name || device.title }} · {{ device.model.appliance?.brand || '—' }} · {{ applianceType(device.model.appliance?.type||device.kind) }}</div>\n <div><b>{{ hcLabels.status }}:</b> {{ semantic(device.model.stateItem) }}</div>\n <div><b>{{ hcLabels.program }}:</b> {{ displayName(device.model.active) }}</div>\n <div><b>{{ hcLabels.remaining }}:</b> {{ remaining(device.model) }}</div>\n <v-alert v-if=\"device.warning\" :type=\"warningType(device.warning)\" density=\"compact\" variant=\"tonal\" class=\"mt-2\"><b>{{ warningText(device.warning) }}</b></v-alert>\n <div class=\"hc-open\">{{ hcLabels.openDetails }} →</div>\n </v-card-text>\n </v-card>\n </div>\n </div>\n</template>\n<script>\nconst createDashboardI18n=function createDashboardI18n(environment = {}, options = {}) {\n const STORAGE_KEY = options.storageKey || 'home-connect-dashboard-language';\n const EVENT_NAME = options.eventName || 'home-connect-language-changed';\n const supported = ['de', 'en'];\n\n const labels = {\n de: {\n overview: 'Übersicht', applianceDetails: 'Gerätedetails', connection: 'Verbindung', status: 'Status', door: 'Tür', active: 'Aktiv',\n selected: 'Ausgewählt', remaining: 'Restzeit', localProfileReadOnly: 'Lokale Profilwerte · nur lesend', remoteControl: 'Fernsteuerung',\n remoteStart: 'Fernstart', localControl: 'Lokale Bedienung', currentOptions: 'Aktuelle Optionen', availablePrograms: 'Verfügbare Programme',\n program: 'Programm', targetTemperature: 'Solltemperatur', temperature: 'Temperatur', refresh: 'Aktualisieren', start: 'Start', stop: 'Stopp',\n openDetails: 'Details öffnen', backToOverview: 'Zur Übersicht', lastKnownState: 'letzter bekannter Stand', remoteStartUnavailable: 'Fernstart ist aktuell nicht verfügbar.',\n language: 'Sprache', german: 'Deutsch', english: 'Englisch', online: 'online', offline: 'offline', yes: 'Ja', no: 'Nein',\n unavailable: '—', unknownValue: 'Unbekannter Wert', rawValue: 'Rohwert', unknownWarning: 'Unbekannter Hinweis',\n readOnly: 'nur lesend', simulated: 'SIMULIERT', simulatedValues: 'Simulierte Beispielwerte · nur Anzeige'\n },\n en: {\n overview: 'Overview', applianceDetails: 'Appliance details', connection: 'Connection', status: 'Status', door: 'Door', active: 'Active',\n selected: 'Selected', remaining: 'Remaining', localProfileReadOnly: 'Local profile values · read-only', remoteControl: 'Remote control',\n remoteStart: 'Remote start', localControl: 'Local control', currentOptions: 'Current options', availablePrograms: 'Available programs',\n program: 'Program', targetTemperature: 'Target temperature', temperature: 'Temperature', refresh: 'Refresh', start: 'Start', stop: 'Stop',\n openDetails: 'Open details', backToOverview: 'Back to overview', lastKnownState: 'last known state', remoteStartUnavailable: 'Remote start is currently unavailable.',\n language: 'Language', german: 'German', english: 'English', online: 'online', offline: 'offline', yes: 'Yes', no: 'No',\n unavailable: '—', unknownValue: 'Unknown value', rawValue: 'raw value', unknownWarning: 'Unknown notice',\n readOnly: 'read-only', simulated: 'SIMULATED', simulatedValues: 'Simulated example values · display only'\n }\n };\n\n const healthNames = {\n ok: { de: 'OK', en: 'OK' }, running: { de: 'Läuft', en: 'Running' }, info: { de: 'Info', en: 'Info' },\n warning: { de: 'Warnung', en: 'Warning' }, action_required: { de: 'Aktion nötig', en: 'Action required' },\n offline: { de: 'Offline', en: 'Offline' }, error: { de: 'Fehler', en: 'Error' }\n };\n\n const deviceTypes = {\n Dishwasher: { de: 'Geschirrspüler', en: 'Dishwasher' }, Washer: { de: 'Waschmaschine', en: 'Washer' },\n Oven: { de: 'Backofen', en: 'Oven' }, Dryer: { de: 'Trockner', en: 'Dryer' },\n WasherDryer: { de: 'Waschtrockner', en: 'Washer-dryer' }, Refrigerator: { de: 'Kühlschrank', en: 'Refrigerator' },\n Freezer: { de: 'Gefrierschrank', en: 'Freezer' }, FridgeFreezer: { de: 'Kühl-Gefrier-Kombination', en: 'Fridge-freezer' },\n CoffeeMaker: { de: 'Kaffeevollautomat', en: 'Coffee maker' }, Hood: { de: 'Dunstabzugshaube', en: 'Hood' },\n Hob: { de: 'Kochfeld', en: 'Hob' }\n };\n\n const valueNames = {\n Inactive: { de: 'Inaktiv', en: 'Inactive' }, Ready: { de: 'Bereit', en: 'Ready' }, Run: { de: 'Läuft', en: 'Running' },\n Finished: { de: 'Beendet', en: 'Finished' }, DelayedStart: { de: 'Startzeitvorwahl', en: 'Delayed start' },\n Pause: { de: 'Pausiert', en: 'Paused' }, Paused: { de: 'Pausiert', en: 'Paused' },\n ActionRequired: { de: 'Aktion erforderlich', en: 'Action required' }, Aborting: { de: 'Wird abgebrochen', en: 'Aborting' },\n Error: { de: 'Fehler', en: 'Error' }, Open: { de: 'Offen', en: 'Open' }, Closed: { de: 'Geschlossen', en: 'Closed' },\n Locked: { de: 'Verriegelt', en: 'Locked' }, On: { de: 'Ein', en: 'On' }, Off: { de: 'Aus', en: 'Off' },\n Standby: { de: 'Standby', en: 'Standby' }, Present: { de: 'Vorhanden', en: 'Present' }, Absent: { de: 'Nicht vorhanden', en: 'Absent' },\n Confirmed: { de: 'Bestätigt', en: 'Confirmed' }, Enabled: { de: 'Aktiviert', en: 'Enabled' }, Disabled: { de: 'Deaktiviert', en: 'Disabled' },\n Cleaning: { de: 'Reinigen', en: 'Cleaning' }, Rinsing: { de: 'Spülen', en: 'Rinsing' }, Drying: { de: 'Trocknen', en: 'Drying' },\n PreWash: { de: 'Vorspülen', en: 'Pre-wash' }, MainWash: { de: 'Hauptspülen', en: 'Main wash' },\n Heating: { de: 'Aufheizen', en: 'Heating' }, Cooling: { de: 'Abkühlen', en: 'Cooling' },\n DE: { de: 'Deutsch', en: 'German' }, EN: { de: 'Englisch', en: 'English' }\n };\n\n const displayNames = {\n ProgramProgress: { de: 'Programmfortschritt', en: 'Program progress' }, RemainingProgramTime: { de: 'Restzeit', en: 'Remaining program time' },\n EnergyForecast: { de: 'Energieprognose', en: 'Energy forecast' }, WaterForecast: { de: 'Wasserprognose', en: 'Water forecast' },\n StartInRelative: { de: 'Start in', en: 'Start in' }, Duration: { de: 'Dauer', en: 'Duration' },\n RemoteControlActive: { de: 'Fernsteuerung aktiv', en: 'Remote control active' },\n RemoteControlStartAllowed: { de: 'Fernstart erlaubt', en: 'Remote start allowed' }, LocalControlActive: { de: 'Lokale Bedienung aktiv', en: 'Local control active' },\n OperationState: { de: 'Betriebszustand', en: 'Operation state' }, DoorState: { de: 'Türzustand', en: 'Door state' },\n PowerState: { de: 'Betriebsart', en: 'Power state' }, ProgramPhase: { de: 'Programmphase', en: 'Program phase' },\n Language: { de: 'Sprache', en: 'Language' }, SaltNearlyEmpty: { de: 'Salz fast leer', en: 'Salt nearly empty' },\n RinseAidNearlyEmpty: { de: 'Klarspüler fast leer', en: 'Rinse aid nearly empty' },\n HygienePlus: { de: 'Hygiene Plus', en: 'Hygiene Plus' }, IntensivZone: { de: 'Intensivzone', en: 'Intensive zone' },\n VarioSpeedPlus: { de: 'varioSpeed Plus', en: 'varioSpeed Plus' }, BrillianceDry: { de: 'Glanztrocknen', en: 'Brilliance dry' },\n ExtraDry: { de: 'Extra trocknen', en: 'Extra dry' }, HalfLoad: { de: 'Halbe Beladung', en: 'Half load' },\n SilenceOnDemand: { de: 'Leisebetrieb', en: 'Silence on demand' }, Temperature: { de: 'Temperatur', en: 'Temperature' },\n SpinSpeed: { de: 'Schleuderdrehzahl', en: 'Spin speed' }, Stains: { de: 'Flecken', en: 'Stains' },\n SetpointTemperature: { de: 'Solltemperatur', en: 'Target temperature' }, FastPreHeat: { de: 'Schnellaufheizen', en: 'Fast preheat' },\n AddedSteam: { de: 'Dampfzugabe', en: 'Added steam' },\n\n Intensiv70: { de: 'Intensiv 70 °C', en: 'Intensive 70 °C' }, Auto2: { de: 'Automatik 45–65 °C', en: 'Auto 45–65 °C' },\n Eco50: { de: 'Eco 50 °C', en: 'Eco 50 °C' }, Glas40: { de: 'Glas 40 °C', en: 'Glass 40 °C' },\n Quick45: { de: 'Kurz 45 °C', en: 'Quick 45 °C' }, PreRinse: { de: 'Vorspülen', en: 'Pre-rinse' },\n MachineCare: { de: 'Maschinenpflege', en: 'Machine care' },\n Cotton: { de: 'Baumwolle', en: 'Cotton' }, EasyCare: { de: 'Pflegeleicht', en: 'Easy-care' }, Mix: { de: 'Mix', en: 'Mix' },\n DelicatesSilk: { de: 'Fein/Seide', en: 'Delicates/Silk' }, Wool: { de: 'Wolle', en: 'Wool' }, DarkWash: { de: 'Dunkle Wäsche', en: 'Dark wash' },\n ShirtsBlouses: { de: 'Hemden/Blusen', en: 'Shirts/Blouses' }, Curtains: { de: 'Gardinen', en: 'Curtains' }, Super15: { de: 'Super 15', en: 'Super 15' },\n Dessous: { de: 'Dessous', en: 'Lingerie' }, Outdoor: { de: 'Outdoor', en: 'Outdoor' }, Sensitive: { de: 'Sensitiv', en: 'Sensitive' },\n DrumClean: { de: 'Trommel reinigen', en: 'Drum clean' }, Rinse: { de: 'Spülen', en: 'Rinse' }, Spin: { de: 'Schleudern', en: 'Spin' },\n Drain: { de: 'Abpumpen', en: 'Drain' }, NightWash: { de: 'Nachtwäsche', en: 'Night wash' }, CottonEco: { de: 'Baumwolle Eco', en: 'Cotton Eco' },\n Memory1: { de: 'Favorit 1', en: 'Memory 1' }, JustStart: { de: 'Nur Start', en: 'Just start' },\n HotAir: { de: 'Heißluft', en: 'Hot air' }, TopBottomHeating: { de: 'Ober-/Unterhitze', en: 'Top/bottom heat' },\n PreHeating: { de: 'Vorheizen', en: 'Preheating' }, HotAirEco: { de: 'Heißluft Eco', en: 'Hot air Eco' },\n TopBottomHeatingEco: { de: 'Ober-/Unterhitze Eco', en: 'Top/bottom heat Eco' }, HotAirGrilling: { de: 'Umluftgrill', en: 'Hot-air grilling' },\n PizzaSetting: { de: 'Pizzastufe', en: 'Pizza setting' }, IntensiveHeat: { de: 'Intensivhitze', en: 'Intensive heat' },\n GrillLargeArea: { de: 'Großflächengrill', en: 'Large-area grill' }, GrillSmallArea: { de: 'Kleinflächengrill', en: 'Small-area grill' },\n SlowCook: { de: 'Sanftgaren', en: 'Slow cook' }, BottomHeating: { de: 'Unterhitze', en: 'Bottom heat' },\n KeepWarm: { de: 'Warmhalten', en: 'Keep warm' }, PreheatOvenware: { de: 'Geschirr vorwärmen', en: 'Preheat ovenware' },\n FrozenHeatupSpecial: { de: 'Tiefkühl-Spezial', en: 'Frozen food special' }, SabbathProgramme: { de: 'Sabbatprogramm', en: 'Sabbath program' },\n Steam: { de: 'Dampf', en: 'Steam' }, Reheat: { de: 'Aufwärmen', en: 'Reheat' }, DoughProving: { de: 'Gärstufe', en: 'Dough proving' },\n Defrosting: { de: 'Auftauen', en: 'Defrosting' }, Ecolysis: { de: 'Ecolyse', en: 'Ecolysis' }, Descale: { de: 'Entkalken', en: 'Descale' },\n Flushing: { de: 'Spülen', en: 'Flushing' }, Draining: { de: 'Abpumpen', en: 'Draining' }, SubsequentCooking: { de: 'Nachgaren', en: 'Subsequent cooking' },\n KuchenInFormen: { de: 'Kuchen in Formen', en: 'Cakes in tins' }, KuchenAufBackblech: { de: 'Kuchen auf Backblech', en: 'Tray-baked cakes' },\n Strudel: { de: 'Strudel', en: 'Strudel' }, Pavlova: { de: 'Pavlova', en: 'Pavlova' }, WaeheOderTart: { de: 'Wähe/Tarte', en: 'Flan/Tart' },\n Blaetterteiggebaeck: { de: 'Blätterteiggebäck', en: 'Puff pastry' }, Brandteiggebaeck: { de: 'Brandteiggebäck', en: 'Choux pastry' },\n Muffins: { de: 'Muffins', en: 'Muffins' }, Plundergebaeck: { de: 'Plundergebäck', en: 'Danish pastry' },\n Hefeteiggebaeck: { de: 'Hefeteiggebäck', en: 'Yeast pastries' }, Muerbeteiggebaeck: { de: 'Mürbeteiggebäck', en: 'Shortcrust pastry' },\n Baiser: { de: 'Baiser', en: 'Meringue' }, Makronen: { de: 'Makronen', en: 'Macaroons' }, Spritzgebaeck: { de: 'Spritzgebäck', en: 'Spritz biscuits' },\n Plaetzchen: { de: 'Plätzchen', en: 'Biscuits' }, Vollkornbrot: { de: 'Vollkornbrot', en: 'Wholegrain bread' },\n WeizenbrotWeizenmischbrotAufBackblech: { de: 'Weizen-/Weizenmischbrot auf Backblech', en: 'Wheat/mixed-wheat bread on tray' }\n };\n\n function unwrap(value) {\n if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return value.value;\n return value;\n }\n\n function normalizeLocale(value) {\n const text = String(unwrap(value) || '').trim().toLowerCase().replace('_', '-');\n if (text === 'de' || text.startsWith('de-')) return 'de';\n if (text === 'en' || text.startsWith('en-')) return 'en';\n return '';\n }\n\n function readStoredLocale() {\n try { return normalizeLocale(environment.localStorage?.getItem(STORAGE_KEY)); }\n catch { return ''; }\n }\n\n function detectLocale(component, message) {\n const msg = message || component?.msg || {};\n const candidates = [\n msg.uiLocale, msg.locale, readStoredLocale(), component?.$i18n?.locale, component?.$vuetify?.locale?.current,\n environment.document?.documentElement?.lang, ...(environment.navigator?.languages || []), environment.navigator?.language\n ];\n for (const candidate of candidates) {\n const locale = normalizeLocale(candidate);\n if (locale) return locale;\n }\n return 'en';\n }\n\n function setLocale(value, notify = true) {\n const locale = normalizeLocale(value) || 'en';\n try { environment.localStorage?.setItem(STORAGE_KEY, locale); }\n catch { /* Storage can be blocked by browser policy. */ }\n if (notify) {\n try { environment.dispatchEvent?.(new environment.CustomEvent(EVENT_NAME, { detail: { locale } })); }\n catch { /* CustomEvent may be unavailable in non-browser tests. */ }\n }\n return locale;\n }\n\n function t(key, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return labels[lang][key] ?? labels.en[key] ?? key;\n }\n\n function token(value) {\n return String(value ?? '').split('.').pop();\n }\n\n function humanize(value) {\n const leaf = token(value)\n .replace(/[_-]+/g, ' ')\n .replace(/([a-zäöüß])([A-Z])/g, '$1 $2')\n .replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n return leaf || '—';\n }\n\n function localized(entry, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return entry?.[lang] ?? entry?.en;\n }\n\n function deviceType(value, locale) {\n const leaf = token(value);\n const known = localized(deviceTypes[leaf], locale);\n if (known) return known;\n if (!leaf) return t('unavailable', locale);\n const prefix = normalizeLocale(locale) === 'de' ? 'Gerätetyp' : 'Appliance type';\n return `${prefix}: ${humanize(leaf)}`;\n }\n\n function displayKey(value, locale) {\n const leaf = token(value);\n return localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale) || humanize(leaf);\n }\n\n function displayName(value, locale) {\n const source = value && typeof value === 'object' ? (value.key || value.name) : value;\n if (source === undefined || source === null || source === '' || source === '—') return t('unavailable', locale);\n const leaf = token(source);\n const known = localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale);\n if (known) return known;\n const fallbackSource = value && typeof value === 'object' && value.name && !String(value.name).includes('.') ? value.name : leaf;\n return humanize(fallbackSource);\n }\n\n function unknown(rawValue, locale) {\n const printable = rawValue === undefined ? '?' : String(rawValue);\n return `${t('unknownValue', locale)} (${t('rawValue', locale)}: ${printable})`;\n }\n\n function semantic(item, locale) {\n if (!item) return t('unavailable', locale);\n if (item.valueResolved === false) return unknown(item.rawValue, locale);\n const value = item.value;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (value === true) return t('yes', locale);\n if (value === false) return t('no', locale);\n if (typeof value === 'number') return new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(value);\n return displayName(value, locale);\n }\n\n function booleanText(item, locale) {\n return semantic(item, locale);\n }\n\n function healthLabel(value, locale) {\n return localized(healthNames[value], locale) || humanize(value);\n }\n\n function warningText(value, locale) {\n if (!value) return '';\n const lang = normalizeLocale(locale) || 'en';\n return value.text?.[lang] || value.text?.en || value.text?.de || `${t('unknownWarning', lang)} (${value.code || '?'})`;\n }\n\n function remaining(model, locale) {\n const value = model?.remaining;\n const item = model?.remainingItem;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (item?.valueResolved === false) return unknown(item.rawValue, locale);\n const number = Number(value);\n if (!Number.isFinite(number)) return String(value);\n const unit = String(item?.unit || '').toLowerCase();\n if (unit.includes('min')) return `${Math.ceil(number)} min`;\n if (unit.includes('sec') || !model?.connection?.local) return `${Math.ceil(number / 60)} min`;\n const formatted = new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(number);\n return item?.unit ? `${formatted} ${item.unit}` : formatted;\n }\n\n function programItems(programs, locale) {\n return (Array.isArray(programs) ? programs : []).map(item => ({ ...item, title: displayName(item, locale) }));\n }\n\n return {\n STORAGE_KEY, EVENT_NAME, supported, normalizeLocale, detectLocale, setLocale, t, token, humanize,\n deviceType, displayKey, displayName, semantic, booleanText, healthLabel, warningText, remaining, programItems\n };\n};\nconst HC=createDashboardI18n(typeof window==='undefined'?{}:window);\nexport default{\n data(){return{locale:HC.detectLocale(),localeListener:null}},\n computed:{\n hcLabels(){return {overview:HC.t('overview',this.locale),connection:HC.t('connection',this.locale),status:HC.t('status',this.locale),door:HC.t('door',this.locale),active:HC.t('active',this.locale),selected:HC.t('selected',this.locale),remaining:HC.t('remaining',this.locale),localProfileReadOnly:HC.t('localProfileReadOnly',this.locale),remoteControl:HC.t('remoteControl',this.locale),remoteStart:HC.t('remoteStart',this.locale),localControl:HC.t('localControl',this.locale),currentOptions:HC.t('currentOptions',this.locale),availablePrograms:HC.t('availablePrograms',this.locale),program:HC.t('program',this.locale),targetTemperature:HC.t('targetTemperature',this.locale),temperature:HC.t('temperature',this.locale),refresh:HC.t('refresh',this.locale),start:HC.t('start',this.locale),stop:HC.t('stop',this.locale),openDetails:HC.t('openDetails',this.locale),backToOverview:HC.t('backToOverview',this.locale),lastKnownState:HC.t('lastKnownState',this.locale),remoteStartUnavailable:HC.t('remoteStartUnavailable',this.locale),language:HC.t('language',this.locale),online:HC.t('online',this.locale),offline:HC.t('offline',this.locale),simulated:HC.t('simulated',this.locale),simulatedValues:HC.t('simulatedValues',this.locale)}},models(){return this.msg?.payload||{}},languageItems(){return[{title:'Deutsch',value:'de'},{title:'English',value:'en'}]},\n cards(){return[{key:'dishwasher',kind:'Dishwasher',icon:'mdi-dishwasher',model:this.models.dishwasher||{}},{key:'washer',kind:'Washer',icon:'mdi-washing-machine',model:this.models.washer||{}},{key:'oven',kind:'Oven',icon:'mdi-stove',model:this.models.oven||{}}].map(device=>({...device,title:HC.deviceType(device.kind,this.locale),health:device.model.health||'ok',warning:(device.model.warnings||[])[0]}))}\n },\n mounted(){this.locale=HC.detectLocale(this,this.msg);this.localeListener=(event)=>{this.locale=HC.normalizeLocale(event.detail?.locale)||this.locale};window.addEventListener(HC.EVENT_NAME,this.localeListener)},\n beforeUnmount(){if(this.localeListener)window.removeEventListener(HC.EVENT_NAME,this.localeListener)},\n methods:{\n setLanguage(value){this.locale=HC.setLocale(value)},applianceType(value){return HC.deviceType(value,this.locale)},displayName(value){return HC.displayName(value,this.locale)},semantic(item){return HC.semantic(item,this.locale)},remaining(model){return HC.remaining(model,this.locale)},healthText(value){return HC.healthLabel(value,this.locale)},warningText(value){return HC.warningText(value,this.locale)},\n healthColor(value){return({ok:'success',running:'info',info:'info',warning:'warning',action_required:'warning',offline:'grey',error:'error'})[value]||'grey'},warningType(value){return value?.severity==='critical'||value?.severity==='error'?'error':value?.severity==='warning'?'warning':'info'},\n openDetails(device){window.location.assign(window.location.pathname.replace(/\\/[^/]*$/,'/home-connect')+'#hc-'+device.key)}\n }\n}\n</script>\n<style>.hc-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.hc-heading{font-size:1.35rem;font-weight:600}.hc-language{max-width:180px}.hc-overview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:12px}.hc-overview-card{cursor:pointer;border-top:4px solid rgb(var(--v-theme-primary));transition:transform .12s}.hc-overview-card:hover,.hc-overview-card:focus{transform:translateY(-2px);outline:2px solid rgb(var(--v-theme-primary))}.hc-meta{color:rgba(var(--v-theme-on-surface),.7);margin-bottom:8px}.hc-open{margin-top:12px;color:rgb(var(--v-theme-primary));font-weight:600}</style>\n",
580
+ "storeOutMessages": true,
581
+ "passthru": false,
582
+ "resendOnRefresh": true,
583
+ "templateScope": "local",
584
+ "className": "",
585
+ "x": 930,
586
+ "y": 190,
587
+ "wires": [
588
+ []
589
+ ]
590
+ },
591
+ {
592
+ "id": "hc-ui-nav-group",
593
+ "type": "ui-group",
594
+ "name": "Navigation",
595
+ "page": "hc-ui-page",
596
+ "width": "12",
597
+ "height": "1",
598
+ "order": 0,
599
+ "showTitle": false,
600
+ "className": "",
601
+ "visible": "true",
602
+ "disabled": "false"
603
+ },
604
+ {
605
+ "id": "hc-ui-nav",
606
+ "type": "ui-template",
607
+ "z": "hc-dashboard-flow",
608
+ "group": "hc-ui-nav-group",
609
+ "page": "",
610
+ "ui": "",
611
+ "name": "Zurück zur Übersicht",
612
+ "order": 1,
613
+ "width": 0,
614
+ "height": 0,
615
+ "head": "",
616
+ "format": "<template><div class=\"hc-nav\"><v-btn variant=\"text\" prepend-icon=\"mdi-arrow-left\" @click=\"back\">{{ hcLabels.backToOverview }}</v-btn><v-select v-model=\"locale\" :items=\"languageItems\" item-title=\"title\" item-value=\"value\" :label=\"hcLabels.language\" density=\"compact\" hide-details class=\"hc-language\" @update:model-value=\"setLanguage\"></v-select></div></template><script>\nconst createDashboardI18n=function createDashboardI18n(environment = {}, options = {}) {\n const STORAGE_KEY = options.storageKey || 'home-connect-dashboard-language';\n const EVENT_NAME = options.eventName || 'home-connect-language-changed';\n const supported = ['de', 'en'];\n\n const labels = {\n de: {\n overview: 'Übersicht', applianceDetails: 'Gerätedetails', connection: 'Verbindung', status: 'Status', door: 'Tür', active: 'Aktiv',\n selected: 'Ausgewählt', remaining: 'Restzeit', localProfileReadOnly: 'Lokale Profilwerte · nur lesend', remoteControl: 'Fernsteuerung',\n remoteStart: 'Fernstart', localControl: 'Lokale Bedienung', currentOptions: 'Aktuelle Optionen', availablePrograms: 'Verfügbare Programme',\n program: 'Programm', targetTemperature: 'Solltemperatur', temperature: 'Temperatur', refresh: 'Aktualisieren', start: 'Start', stop: 'Stopp',\n openDetails: 'Details öffnen', backToOverview: 'Zur Übersicht', lastKnownState: 'letzter bekannter Stand', remoteStartUnavailable: 'Fernstart ist aktuell nicht verfügbar.',\n language: 'Sprache', german: 'Deutsch', english: 'Englisch', online: 'online', offline: 'offline', yes: 'Ja', no: 'Nein',\n unavailable: '—', unknownValue: 'Unbekannter Wert', rawValue: 'Rohwert', unknownWarning: 'Unbekannter Hinweis',\n readOnly: 'nur lesend', simulated: 'SIMULIERT', simulatedValues: 'Simulierte Beispielwerte · nur Anzeige'\n },\n en: {\n overview: 'Overview', applianceDetails: 'Appliance details', connection: 'Connection', status: 'Status', door: 'Door', active: 'Active',\n selected: 'Selected', remaining: 'Remaining', localProfileReadOnly: 'Local profile values · read-only', remoteControl: 'Remote control',\n remoteStart: 'Remote start', localControl: 'Local control', currentOptions: 'Current options', availablePrograms: 'Available programs',\n program: 'Program', targetTemperature: 'Target temperature', temperature: 'Temperature', refresh: 'Refresh', start: 'Start', stop: 'Stop',\n openDetails: 'Open details', backToOverview: 'Back to overview', lastKnownState: 'last known state', remoteStartUnavailable: 'Remote start is currently unavailable.',\n language: 'Language', german: 'German', english: 'English', online: 'online', offline: 'offline', yes: 'Yes', no: 'No',\n unavailable: '—', unknownValue: 'Unknown value', rawValue: 'raw value', unknownWarning: 'Unknown notice',\n readOnly: 'read-only', simulated: 'SIMULATED', simulatedValues: 'Simulated example values · display only'\n }\n };\n\n const healthNames = {\n ok: { de: 'OK', en: 'OK' }, running: { de: 'Läuft', en: 'Running' }, info: { de: 'Info', en: 'Info' },\n warning: { de: 'Warnung', en: 'Warning' }, action_required: { de: 'Aktion nötig', en: 'Action required' },\n offline: { de: 'Offline', en: 'Offline' }, error: { de: 'Fehler', en: 'Error' }\n };\n\n const deviceTypes = {\n Dishwasher: { de: 'Geschirrspüler', en: 'Dishwasher' }, Washer: { de: 'Waschmaschine', en: 'Washer' },\n Oven: { de: 'Backofen', en: 'Oven' }, Dryer: { de: 'Trockner', en: 'Dryer' },\n WasherDryer: { de: 'Waschtrockner', en: 'Washer-dryer' }, Refrigerator: { de: 'Kühlschrank', en: 'Refrigerator' },\n Freezer: { de: 'Gefrierschrank', en: 'Freezer' }, FridgeFreezer: { de: 'Kühl-Gefrier-Kombination', en: 'Fridge-freezer' },\n CoffeeMaker: { de: 'Kaffeevollautomat', en: 'Coffee maker' }, Hood: { de: 'Dunstabzugshaube', en: 'Hood' },\n Hob: { de: 'Kochfeld', en: 'Hob' }\n };\n\n const valueNames = {\n Inactive: { de: 'Inaktiv', en: 'Inactive' }, Ready: { de: 'Bereit', en: 'Ready' }, Run: { de: 'Läuft', en: 'Running' },\n Finished: { de: 'Beendet', en: 'Finished' }, DelayedStart: { de: 'Startzeitvorwahl', en: 'Delayed start' },\n Pause: { de: 'Pausiert', en: 'Paused' }, Paused: { de: 'Pausiert', en: 'Paused' },\n ActionRequired: { de: 'Aktion erforderlich', en: 'Action required' }, Aborting: { de: 'Wird abgebrochen', en: 'Aborting' },\n Error: { de: 'Fehler', en: 'Error' }, Open: { de: 'Offen', en: 'Open' }, Closed: { de: 'Geschlossen', en: 'Closed' },\n Locked: { de: 'Verriegelt', en: 'Locked' }, On: { de: 'Ein', en: 'On' }, Off: { de: 'Aus', en: 'Off' },\n Standby: { de: 'Standby', en: 'Standby' }, Present: { de: 'Vorhanden', en: 'Present' }, Absent: { de: 'Nicht vorhanden', en: 'Absent' },\n Confirmed: { de: 'Bestätigt', en: 'Confirmed' }, Enabled: { de: 'Aktiviert', en: 'Enabled' }, Disabled: { de: 'Deaktiviert', en: 'Disabled' },\n Cleaning: { de: 'Reinigen', en: 'Cleaning' }, Rinsing: { de: 'Spülen', en: 'Rinsing' }, Drying: { de: 'Trocknen', en: 'Drying' },\n PreWash: { de: 'Vorspülen', en: 'Pre-wash' }, MainWash: { de: 'Hauptspülen', en: 'Main wash' },\n Heating: { de: 'Aufheizen', en: 'Heating' }, Cooling: { de: 'Abkühlen', en: 'Cooling' },\n DE: { de: 'Deutsch', en: 'German' }, EN: { de: 'Englisch', en: 'English' }\n };\n\n const displayNames = {\n ProgramProgress: { de: 'Programmfortschritt', en: 'Program progress' }, RemainingProgramTime: { de: 'Restzeit', en: 'Remaining program time' },\n EnergyForecast: { de: 'Energieprognose', en: 'Energy forecast' }, WaterForecast: { de: 'Wasserprognose', en: 'Water forecast' },\n StartInRelative: { de: 'Start in', en: 'Start in' }, Duration: { de: 'Dauer', en: 'Duration' },\n RemoteControlActive: { de: 'Fernsteuerung aktiv', en: 'Remote control active' },\n RemoteControlStartAllowed: { de: 'Fernstart erlaubt', en: 'Remote start allowed' }, LocalControlActive: { de: 'Lokale Bedienung aktiv', en: 'Local control active' },\n OperationState: { de: 'Betriebszustand', en: 'Operation state' }, DoorState: { de: 'Türzustand', en: 'Door state' },\n PowerState: { de: 'Betriebsart', en: 'Power state' }, ProgramPhase: { de: 'Programmphase', en: 'Program phase' },\n Language: { de: 'Sprache', en: 'Language' }, SaltNearlyEmpty: { de: 'Salz fast leer', en: 'Salt nearly empty' },\n RinseAidNearlyEmpty: { de: 'Klarspüler fast leer', en: 'Rinse aid nearly empty' },\n HygienePlus: { de: 'Hygiene Plus', en: 'Hygiene Plus' }, IntensivZone: { de: 'Intensivzone', en: 'Intensive zone' },\n VarioSpeedPlus: { de: 'varioSpeed Plus', en: 'varioSpeed Plus' }, BrillianceDry: { de: 'Glanztrocknen', en: 'Brilliance dry' },\n ExtraDry: { de: 'Extra trocknen', en: 'Extra dry' }, HalfLoad: { de: 'Halbe Beladung', en: 'Half load' },\n SilenceOnDemand: { de: 'Leisebetrieb', en: 'Silence on demand' }, Temperature: { de: 'Temperatur', en: 'Temperature' },\n SpinSpeed: { de: 'Schleuderdrehzahl', en: 'Spin speed' }, Stains: { de: 'Flecken', en: 'Stains' },\n SetpointTemperature: { de: 'Solltemperatur', en: 'Target temperature' }, FastPreHeat: { de: 'Schnellaufheizen', en: 'Fast preheat' },\n AddedSteam: { de: 'Dampfzugabe', en: 'Added steam' },\n\n Intensiv70: { de: 'Intensiv 70 °C', en: 'Intensive 70 °C' }, Auto2: { de: 'Automatik 45–65 °C', en: 'Auto 45–65 °C' },\n Eco50: { de: 'Eco 50 °C', en: 'Eco 50 °C' }, Glas40: { de: 'Glas 40 °C', en: 'Glass 40 °C' },\n Quick45: { de: 'Kurz 45 °C', en: 'Quick 45 °C' }, PreRinse: { de: 'Vorspülen', en: 'Pre-rinse' },\n MachineCare: { de: 'Maschinenpflege', en: 'Machine care' },\n Cotton: { de: 'Baumwolle', en: 'Cotton' }, EasyCare: { de: 'Pflegeleicht', en: 'Easy-care' }, Mix: { de: 'Mix', en: 'Mix' },\n DelicatesSilk: { de: 'Fein/Seide', en: 'Delicates/Silk' }, Wool: { de: 'Wolle', en: 'Wool' }, DarkWash: { de: 'Dunkle Wäsche', en: 'Dark wash' },\n ShirtsBlouses: { de: 'Hemden/Blusen', en: 'Shirts/Blouses' }, Curtains: { de: 'Gardinen', en: 'Curtains' }, Super15: { de: 'Super 15', en: 'Super 15' },\n Dessous: { de: 'Dessous', en: 'Lingerie' }, Outdoor: { de: 'Outdoor', en: 'Outdoor' }, Sensitive: { de: 'Sensitiv', en: 'Sensitive' },\n DrumClean: { de: 'Trommel reinigen', en: 'Drum clean' }, Rinse: { de: 'Spülen', en: 'Rinse' }, Spin: { de: 'Schleudern', en: 'Spin' },\n Drain: { de: 'Abpumpen', en: 'Drain' }, NightWash: { de: 'Nachtwäsche', en: 'Night wash' }, CottonEco: { de: 'Baumwolle Eco', en: 'Cotton Eco' },\n Memory1: { de: 'Favorit 1', en: 'Memory 1' }, JustStart: { de: 'Nur Start', en: 'Just start' },\n HotAir: { de: 'Heißluft', en: 'Hot air' }, TopBottomHeating: { de: 'Ober-/Unterhitze', en: 'Top/bottom heat' },\n PreHeating: { de: 'Vorheizen', en: 'Preheating' }, HotAirEco: { de: 'Heißluft Eco', en: 'Hot air Eco' },\n TopBottomHeatingEco: { de: 'Ober-/Unterhitze Eco', en: 'Top/bottom heat Eco' }, HotAirGrilling: { de: 'Umluftgrill', en: 'Hot-air grilling' },\n PizzaSetting: { de: 'Pizzastufe', en: 'Pizza setting' }, IntensiveHeat: { de: 'Intensivhitze', en: 'Intensive heat' },\n GrillLargeArea: { de: 'Großflächengrill', en: 'Large-area grill' }, GrillSmallArea: { de: 'Kleinflächengrill', en: 'Small-area grill' },\n SlowCook: { de: 'Sanftgaren', en: 'Slow cook' }, BottomHeating: { de: 'Unterhitze', en: 'Bottom heat' },\n KeepWarm: { de: 'Warmhalten', en: 'Keep warm' }, PreheatOvenware: { de: 'Geschirr vorwärmen', en: 'Preheat ovenware' },\n FrozenHeatupSpecial: { de: 'Tiefkühl-Spezial', en: 'Frozen food special' }, SabbathProgramme: { de: 'Sabbatprogramm', en: 'Sabbath program' },\n Steam: { de: 'Dampf', en: 'Steam' }, Reheat: { de: 'Aufwärmen', en: 'Reheat' }, DoughProving: { de: 'Gärstufe', en: 'Dough proving' },\n Defrosting: { de: 'Auftauen', en: 'Defrosting' }, Ecolysis: { de: 'Ecolyse', en: 'Ecolysis' }, Descale: { de: 'Entkalken', en: 'Descale' },\n Flushing: { de: 'Spülen', en: 'Flushing' }, Draining: { de: 'Abpumpen', en: 'Draining' }, SubsequentCooking: { de: 'Nachgaren', en: 'Subsequent cooking' },\n KuchenInFormen: { de: 'Kuchen in Formen', en: 'Cakes in tins' }, KuchenAufBackblech: { de: 'Kuchen auf Backblech', en: 'Tray-baked cakes' },\n Strudel: { de: 'Strudel', en: 'Strudel' }, Pavlova: { de: 'Pavlova', en: 'Pavlova' }, WaeheOderTart: { de: 'Wähe/Tarte', en: 'Flan/Tart' },\n Blaetterteiggebaeck: { de: 'Blätterteiggebäck', en: 'Puff pastry' }, Brandteiggebaeck: { de: 'Brandteiggebäck', en: 'Choux pastry' },\n Muffins: { de: 'Muffins', en: 'Muffins' }, Plundergebaeck: { de: 'Plundergebäck', en: 'Danish pastry' },\n Hefeteiggebaeck: { de: 'Hefeteiggebäck', en: 'Yeast pastries' }, Muerbeteiggebaeck: { de: 'Mürbeteiggebäck', en: 'Shortcrust pastry' },\n Baiser: { de: 'Baiser', en: 'Meringue' }, Makronen: { de: 'Makronen', en: 'Macaroons' }, Spritzgebaeck: { de: 'Spritzgebäck', en: 'Spritz biscuits' },\n Plaetzchen: { de: 'Plätzchen', en: 'Biscuits' }, Vollkornbrot: { de: 'Vollkornbrot', en: 'Wholegrain bread' },\n WeizenbrotWeizenmischbrotAufBackblech: { de: 'Weizen-/Weizenmischbrot auf Backblech', en: 'Wheat/mixed-wheat bread on tray' }\n };\n\n function unwrap(value) {\n if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return value.value;\n return value;\n }\n\n function normalizeLocale(value) {\n const text = String(unwrap(value) || '').trim().toLowerCase().replace('_', '-');\n if (text === 'de' || text.startsWith('de-')) return 'de';\n if (text === 'en' || text.startsWith('en-')) return 'en';\n return '';\n }\n\n function readStoredLocale() {\n try { return normalizeLocale(environment.localStorage?.getItem(STORAGE_KEY)); }\n catch { return ''; }\n }\n\n function detectLocale(component, message) {\n const msg = message || component?.msg || {};\n const candidates = [\n msg.uiLocale, msg.locale, readStoredLocale(), component?.$i18n?.locale, component?.$vuetify?.locale?.current,\n environment.document?.documentElement?.lang, ...(environment.navigator?.languages || []), environment.navigator?.language\n ];\n for (const candidate of candidates) {\n const locale = normalizeLocale(candidate);\n if (locale) return locale;\n }\n return 'en';\n }\n\n function setLocale(value, notify = true) {\n const locale = normalizeLocale(value) || 'en';\n try { environment.localStorage?.setItem(STORAGE_KEY, locale); }\n catch { /* Storage can be blocked by browser policy. */ }\n if (notify) {\n try { environment.dispatchEvent?.(new environment.CustomEvent(EVENT_NAME, { detail: { locale } })); }\n catch { /* CustomEvent may be unavailable in non-browser tests. */ }\n }\n return locale;\n }\n\n function t(key, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return labels[lang][key] ?? labels.en[key] ?? key;\n }\n\n function token(value) {\n return String(value ?? '').split('.').pop();\n }\n\n function humanize(value) {\n const leaf = token(value)\n .replace(/[_-]+/g, ' ')\n .replace(/([a-zäöüß])([A-Z])/g, '$1 $2')\n .replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n return leaf || '—';\n }\n\n function localized(entry, locale) {\n const lang = normalizeLocale(locale) || 'en';\n return entry?.[lang] ?? entry?.en;\n }\n\n function deviceType(value, locale) {\n const leaf = token(value);\n const known = localized(deviceTypes[leaf], locale);\n if (known) return known;\n if (!leaf) return t('unavailable', locale);\n const prefix = normalizeLocale(locale) === 'de' ? 'Gerätetyp' : 'Appliance type';\n return `${prefix}: ${humanize(leaf)}`;\n }\n\n function displayKey(value, locale) {\n const leaf = token(value);\n return localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale) || humanize(leaf);\n }\n\n function displayName(value, locale) {\n const source = value && typeof value === 'object' ? (value.key || value.name) : value;\n if (source === undefined || source === null || source === '' || source === '—') return t('unavailable', locale);\n const leaf = token(source);\n const known = localized(displayNames[leaf], locale) || localized(valueNames[leaf], locale);\n if (known) return known;\n const fallbackSource = value && typeof value === 'object' && value.name && !String(value.name).includes('.') ? value.name : leaf;\n return humanize(fallbackSource);\n }\n\n function unknown(rawValue, locale) {\n const printable = rawValue === undefined ? '?' : String(rawValue);\n return `${t('unknownValue', locale)} (${t('rawValue', locale)}: ${printable})`;\n }\n\n function semantic(item, locale) {\n if (!item) return t('unavailable', locale);\n if (item.valueResolved === false) return unknown(item.rawValue, locale);\n const value = item.value;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (value === true) return t('yes', locale);\n if (value === false) return t('no', locale);\n if (typeof value === 'number') return new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(value);\n return displayName(value, locale);\n }\n\n function booleanText(item, locale) {\n return semantic(item, locale);\n }\n\n function healthLabel(value, locale) {\n return localized(healthNames[value], locale) || humanize(value);\n }\n\n function warningText(value, locale) {\n if (!value) return '';\n const lang = normalizeLocale(locale) || 'en';\n return value.text?.[lang] || value.text?.en || value.text?.de || `${t('unknownWarning', lang)} (${value.code || '?'})`;\n }\n\n function remaining(model, locale) {\n const value = model?.remaining;\n const item = model?.remainingItem;\n if (value === undefined || value === null || value === '') return t('unavailable', locale);\n if (item?.valueResolved === false) return unknown(item.rawValue, locale);\n const number = Number(value);\n if (!Number.isFinite(number)) return String(value);\n const unit = String(item?.unit || '').toLowerCase();\n if (unit.includes('min')) return `${Math.ceil(number)} min`;\n if (unit.includes('sec') || !model?.connection?.local) return `${Math.ceil(number / 60)} min`;\n const formatted = new Intl.NumberFormat(normalizeLocale(locale) === 'de' ? 'de-DE' : 'en-US').format(number);\n return item?.unit ? `${formatted} ${item.unit}` : formatted;\n }\n\n function programItems(programs, locale) {\n return (Array.isArray(programs) ? programs : []).map(item => ({ ...item, title: displayName(item, locale) }));\n }\n\n return {\n STORAGE_KEY, EVENT_NAME, supported, normalizeLocale, detectLocale, setLocale, t, token, humanize,\n deviceType, displayKey, displayName, semantic, booleanText, healthLabel, warningText, remaining, programItems\n };\n};\nconst HC=createDashboardI18n(typeof window==='undefined'?{}:window);\nexport default{data(){return{locale:HC.detectLocale(),localeListener:null}},computed:{hcLabels(){return {overview:HC.t('overview',this.locale),connection:HC.t('connection',this.locale),status:HC.t('status',this.locale),door:HC.t('door',this.locale),active:HC.t('active',this.locale),selected:HC.t('selected',this.locale),remaining:HC.t('remaining',this.locale),localProfileReadOnly:HC.t('localProfileReadOnly',this.locale),remoteControl:HC.t('remoteControl',this.locale),remoteStart:HC.t('remoteStart',this.locale),localControl:HC.t('localControl',this.locale),currentOptions:HC.t('currentOptions',this.locale),availablePrograms:HC.t('availablePrograms',this.locale),program:HC.t('program',this.locale),targetTemperature:HC.t('targetTemperature',this.locale),temperature:HC.t('temperature',this.locale),refresh:HC.t('refresh',this.locale),start:HC.t('start',this.locale),stop:HC.t('stop',this.locale),openDetails:HC.t('openDetails',this.locale),backToOverview:HC.t('backToOverview',this.locale),lastKnownState:HC.t('lastKnownState',this.locale),remoteStartUnavailable:HC.t('remoteStartUnavailable',this.locale),language:HC.t('language',this.locale),online:HC.t('online',this.locale),offline:HC.t('offline',this.locale),simulated:HC.t('simulated',this.locale),simulatedValues:HC.t('simulatedValues',this.locale)}},languageItems(){return[{title:'Deutsch',value:'de'},{title:'English',value:'en'}]}},mounted(){this.locale=HC.detectLocale(this,this.msg);this.localeListener=(event)=>{this.locale=HC.normalizeLocale(event.detail?.locale)||this.locale};window.addEventListener(HC.EVENT_NAME,this.localeListener)},\n beforeUnmount(){if(this.localeListener)window.removeEventListener(HC.EVENT_NAME,this.localeListener)},methods:{setLanguage(value){this.locale=HC.setLocale(value)},back(){window.location.assign(window.location.pathname.replace(/\\/[^/]*$/,'/overview'))}}}</script><style>.hc-nav{display:flex;align-items:center;justify-content:space-between;gap:16px}.hc-language{max-width:180px}</style>",
617
+ "storeOutMessages": true,
618
+ "passthru": false,
619
+ "resendOnRefresh": true,
620
+ "templateScope": "local",
621
+ "className": "",
622
+ "x": 930,
623
+ "y": 240,
624
+ "wires": [
625
+ []
626
+ ]
627
+ }
628
+ ]