@camstack/addon-provider-bosch 0.1.1 → 0.1.2

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.
@@ -1,6 +1,9 @@
1
- import { C as record, D as EventCategory, E as unknown, S as object, T as union, _ as isSecretMarkerLike, a as connectionTestCapability, b as literal, f as DeviceType, g as isSecretMarker, h as defineCustomActions, m as customAction, n as BaseDeviceProvider, o as deviceAdoptionCapability, p as errMsg, s as dishwasherCapability, t as BaseDevice, v as _enum, w as string, y as array } from "../dist-CY6qHAEx.mjs";
2
- import { n as cascadeRemoveDevicesForIntegration, t as setIntegrationDevicesOnline } from "../account-integration-offline-CH7JBu8l.mjs";
1
+ import { A as union, C as _enum, D as object, M as EventCategory, O as record, S as isSecretMarkerLike, T as literal, _ as DeviceType, b as defineCustomActions, c as dishwasherCapability, d as numericSensorCapability, f as powerMeterCapability, g as DeviceRole, i as binaryCapability, j as unknown, k as string, l as enumSensorCapability, n as BaseDeviceProvider, o as connectionTestCapability, s as deviceAdoptionCapability, t as BaseDevice, v as errMsg, w as array, x as isSecretMarker, y as customAction } from "../dist-BoMB3-eL.mjs";
2
+ import { n as cascadeRemoveDevicesForIntegration, t as setIntegrationDevicesOnline } from "../account-integration-offline-l27nXZT3.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
+ import { connect } from "node:tls";
5
+ import { inflateRawSync } from "node:zlib";
6
+ import { lookup } from "node:dns/promises";
4
7
  //#region src/home-connect/custom-actions.ts
5
8
  var homeConnectActions = defineCustomActions({
6
9
  beginSignIn: customAction(object({}), object({
@@ -27,9 +30,331 @@ var homeConnectActions = defineCustomActions({
27
30
  })
28
31
  });
29
32
  //#endregion
33
+ //#region src/home-connect/home-connect-device-description.ts
34
+ /** Common Bosch Home Connect UIDs (homeconnect_websocket DESCRIPTION — live dishwasher). */
35
+ var HOME_CONNECT_COMMON_ENTITY_FALLBACK = {
36
+ "5": {
37
+ uid: 5,
38
+ name: "BSH.Common.Status.BackendConnected"
39
+ },
40
+ "517": {
41
+ uid: 517,
42
+ name: "BSH.Common.Status.RemoteControlStartAllowed"
43
+ },
44
+ "523": {
45
+ uid: 523,
46
+ name: "BSH.Common.Status.RemoteControlActive"
47
+ },
48
+ "527": {
49
+ uid: 527,
50
+ name: "BSH.Common.Status.DoorState",
51
+ enumeration: {
52
+ 0: "Open",
53
+ 1: "Closed"
54
+ }
55
+ },
56
+ "552": {
57
+ uid: 552,
58
+ name: "BSH.Common.Status.OperationState",
59
+ enumeration: {
60
+ 0: "Inactive",
61
+ 1: "Ready",
62
+ 2: "DelayedStart",
63
+ 3: "Run",
64
+ 4: "Pause",
65
+ 5: "ActionRequired",
66
+ 6: "Finished",
67
+ 7: "Error",
68
+ 8: "Aborting"
69
+ }
70
+ },
71
+ "539": {
72
+ uid: 539,
73
+ name: "BSH.Common.Setting.PowerState",
74
+ enumeration: {
75
+ 1: "Off",
76
+ 2: "On"
77
+ }
78
+ },
79
+ "542": {
80
+ uid: 542,
81
+ name: "BSH.Common.Option.ProgramProgress"
82
+ },
83
+ "544": {
84
+ uid: 544,
85
+ name: "BSH.Common.Option.RemainingProgramTime"
86
+ },
87
+ "558": {
88
+ uid: 558,
89
+ name: "BSH.Common.Option.StartInRelative"
90
+ },
91
+ "561": {
92
+ uid: 561,
93
+ name: "BSH.Common.Option.EnergyForecast"
94
+ },
95
+ "562": {
96
+ uid: 562,
97
+ name: "BSH.Common.Option.WaterForecast"
98
+ },
99
+ "256": {
100
+ uid: 256,
101
+ name: "BSH.Common.Root.ActiveProgram"
102
+ },
103
+ "257": {
104
+ uid: 257,
105
+ name: "BSH.Common.Root.SelectedProgram"
106
+ }
107
+ };
108
+ function buildCommonEntityFallbackDescription(input) {
109
+ return {
110
+ info: {
111
+ deviceID: input.haId,
112
+ deviceType: input.type,
113
+ model: input.name
114
+ },
115
+ program: [],
116
+ entities: HOME_CONNECT_COMMON_ENTITY_FALLBACK
117
+ };
118
+ }
119
+ function isDescriptionRoot(value) {
120
+ return typeof value === "object" && value !== null && !Array.isArray(value);
121
+ }
122
+ function parseHexUid(raw) {
123
+ return Number.parseInt(raw, 16);
124
+ }
125
+ function parseFeatureMappingXml(xml) {
126
+ const features = /* @__PURE__ */ new Map();
127
+ for (const match of xml.matchAll(/<feature\s+refUID="([0-9A-Fa-f]+)"[^>]*>([^<]+)<\/feature>/g)) {
128
+ const uidHex = match[1];
129
+ const name = match[2]?.trim();
130
+ if (uidHex && name) features.set(parseHexUid(uidHex), name);
131
+ }
132
+ const enumerations = /* @__PURE__ */ new Map();
133
+ for (const block of xml.matchAll(/<enumDescription\s+refENID="([0-9A-Fa-f]+)"[^>]*>([\s\S]*?)<\/enumDescription>/g)) {
134
+ const enidHex = block[1];
135
+ const inner = block[2] ?? "";
136
+ if (!enidHex) continue;
137
+ const members = {};
138
+ for (const m of inner.matchAll(/<enumMember\s+refValue="(\d+)"[^>]*>([^<]+)<\/enumMember>/g)) {
139
+ const valueStr = m[1];
140
+ const label = m[2]?.trim();
141
+ if (valueStr && label) members[Number.parseInt(valueStr, 10)] = label;
142
+ }
143
+ enumerations.set(parseHexUid(enidHex), members);
144
+ }
145
+ return {
146
+ features,
147
+ enumerations
148
+ };
149
+ }
150
+ function tagAttrs(openTag) {
151
+ const attrs = {};
152
+ for (const m of openTag.matchAll(/(\w+)="([^"]*)"/g)) {
153
+ const key = m[1];
154
+ const val = m[2];
155
+ if (key && val !== void 0) attrs[key] = val;
156
+ }
157
+ return attrs;
158
+ }
159
+ var ENTITY_TAGS = [
160
+ "status",
161
+ "setting",
162
+ "option",
163
+ "command",
164
+ "event",
165
+ "program",
166
+ "activeProgram",
167
+ "selectedProgram"
168
+ ];
169
+ function collectEntitiesFromDeviceXml(xml, features, enumerations) {
170
+ const out = [];
171
+ for (const tag of ENTITY_TAGS) {
172
+ const re = new RegExp(`<${tag}\\b([^>/]*)(?:\\/>|>([\\s\\S]*?)<\\/${tag}>)`, "g");
173
+ for (const match of xml.matchAll(re)) {
174
+ const attrs = tagAttrs(match[1] ?? "");
175
+ const uidRaw = attrs["uid"];
176
+ if (!uidRaw) continue;
177
+ const uid = parseHexUid(uidRaw);
178
+ const mapped = features.get(uid);
179
+ if (!mapped) continue;
180
+ const name = mapped.includes(".") ? mapped : `BSH.Common.${mapped}`;
181
+ let enumeration;
182
+ const enType = attrs["enumerationType"];
183
+ if (enType) enumeration = { ...enumerations.get(parseHexUid(enType)) };
184
+ out.push({
185
+ uid,
186
+ name,
187
+ enumeration
188
+ });
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+ function infoFromDeviceXml(xml, deviceIdFallback) {
194
+ const type = xml.match(/<type>([^<]+)<\/type>/)?.[1] ?? "Appliance";
195
+ const brand = xml.match(/<brand>([^<]+)<\/brand>/)?.[1];
196
+ return {
197
+ deviceID: deviceIdFallback,
198
+ deviceType: type,
199
+ model: xml.match(/<model>([^<]+)<\/model>/)?.[1] ?? deviceIdFallback,
200
+ brand
201
+ };
202
+ }
203
+ function programsFromDeviceXml(xml, features) {
204
+ const programs = [];
205
+ for (const match of xml.matchAll(/<program\b([^>/]*)(?:\/>|>[\s\S]*?<\/program>)/g)) {
206
+ const attrs = tagAttrs(match[1] ?? "");
207
+ const uidRaw = attrs["uid"];
208
+ if (!uidRaw) continue;
209
+ const uid = parseHexUid(uidRaw);
210
+ const mapped = features.get(uid);
211
+ if (!mapped) continue;
212
+ const name = mapped.startsWith("BSH.") ? mapped : `BSH.Common.Program.${mapped}`;
213
+ programs.push({
214
+ name: name.split(".").pop() ?? name,
215
+ uid: name,
216
+ available: attrs["available"] !== "false"
217
+ });
218
+ }
219
+ return programs;
220
+ }
221
+ /** Parse IDDF XML pair into compact JSON stored on the appliance connection. */
222
+ function parseHomeConnectIddfXml(input) {
223
+ const { features, enumerations } = parseFeatureMappingXml(input.featureMappingXml);
224
+ const entitiesList = collectEntitiesFromDeviceXml(input.deviceDescriptionXml, features, enumerations);
225
+ const entities = {};
226
+ for (const entity of entitiesList) entities[String(entity.uid)] = entity;
227
+ return {
228
+ info: infoFromDeviceXml(input.deviceDescriptionXml, input.haId),
229
+ program: programsFromDeviceXml(input.deviceDescriptionXml, features),
230
+ entities
231
+ };
232
+ }
233
+ function entityIndexFromDescription(input) {
234
+ const index = /* @__PURE__ */ new Map();
235
+ if (input === void 0 || input === null) return index;
236
+ try {
237
+ let root;
238
+ if (typeof input === "string") {
239
+ const parsed = JSON.parse(input.trim());
240
+ if (!isDescriptionRoot(parsed)) return index;
241
+ root = parsed;
242
+ } else if (isDescriptionRoot(input)) root = input;
243
+ else return index;
244
+ const entities = root["entities"];
245
+ if (!isDescriptionRoot(entities)) return index;
246
+ for (const [key, val] of Object.entries(entities)) {
247
+ if (!isDescriptionRoot(val)) continue;
248
+ const name = val["name"];
249
+ const uid = Number.parseInt(key, 10);
250
+ if (typeof name !== "string" || Number.isNaN(uid)) continue;
251
+ let enumeration;
252
+ const enumRaw = val["enumeration"];
253
+ if (isDescriptionRoot(enumRaw)) {
254
+ enumeration = {};
255
+ for (const [ek, ev] of Object.entries(enumRaw)) if (typeof ev === "string") enumeration[Number.parseInt(ek, 10)] = ev;
256
+ }
257
+ index.set(uid, {
258
+ uid,
259
+ name,
260
+ enumeration
261
+ });
262
+ }
263
+ } catch {
264
+ return index;
265
+ }
266
+ return index;
267
+ }
268
+ function decodeHomeConnectValue(uid, value, index) {
269
+ const entity = index.get(uid);
270
+ if (!entity) return null;
271
+ if (entity.enumeration && (typeof value === "number" || typeof value === "string" && /^\d+$/.test(value))) {
272
+ const num = typeof value === "number" ? value : Number.parseInt(value, 10);
273
+ const label = entity.enumeration[num];
274
+ return {
275
+ key: entity.name,
276
+ value: label ?? value
277
+ };
278
+ }
279
+ return {
280
+ key: entity.name,
281
+ value
282
+ };
283
+ }
284
+ function encodeHomeConnectValue(featureName, value, index) {
285
+ for (const entity of index.values()) {
286
+ if (entity.name !== featureName) continue;
287
+ if (entity.enumeration && typeof value === "string") {
288
+ for (const [num, label] of Object.entries(entity.enumeration)) if (label === value) return {
289
+ uid: entity.uid,
290
+ value: Number.parseInt(num, 10)
291
+ };
292
+ }
293
+ return {
294
+ uid: entity.uid,
295
+ value
296
+ };
297
+ }
298
+ return null;
299
+ }
300
+ //#endregion
301
+ //#region src/home-connect/home-connect-iddf.ts
302
+ function readUInt16LE(buf, offset) {
303
+ return buf.readUInt16LE(offset);
304
+ }
305
+ function readUInt32LE(buf, offset) {
306
+ return buf.readUInt32LE(offset);
307
+ }
308
+ function decodeZipPayload(method, payload) {
309
+ if (method === 0) return payload;
310
+ if (method === 8) try {
311
+ return inflateRawSync(payload);
312
+ } catch {
313
+ return null;
314
+ }
315
+ return null;
316
+ }
317
+ /** Extract file bodies from a ZIP (store + deflate). */
318
+ function extractZipEntries(zip) {
319
+ const out = /* @__PURE__ */ new Map();
320
+ let pos = 0;
321
+ while (pos + 30 <= zip.length) {
322
+ const sig = readUInt32LE(zip, pos);
323
+ if (sig === 101010256) break;
324
+ if (sig !== 67324752) break;
325
+ const method = readUInt16LE(zip, pos + 8);
326
+ const compSize = readUInt32LE(zip, pos + 18);
327
+ const nameLen = readUInt16LE(zip, pos + 26);
328
+ const extraLen = readUInt16LE(zip, pos + 28);
329
+ const nameStart = pos + 30;
330
+ const name = zip.subarray(nameStart, nameStart + nameLen).toString("utf8");
331
+ const dataStart = nameStart + nameLen + extraLen;
332
+ const decoded = decodeZipPayload(method, zip.subarray(dataStart, dataStart + compSize));
333
+ if (decoded !== null) out.set(name, decoded);
334
+ pos = dataStart + compSize;
335
+ }
336
+ return out;
337
+ }
338
+ function parseIddfZipBuffer(zip, haId) {
339
+ const entries = extractZipEntries(zip);
340
+ let deviceXml;
341
+ let mappingXml;
342
+ for (const [name, body] of entries) {
343
+ if (name.endsWith("_DeviceDescription.xml")) deviceXml = body.toString("utf8");
344
+ if (name.endsWith("_FeatureMapping.xml")) mappingXml = body.toString("utf8");
345
+ }
346
+ if (!deviceXml || !mappingXml) return null;
347
+ return parseHomeConnectIddfXml({
348
+ deviceDescriptionXml: deviceXml,
349
+ featureMappingXml: mappingXml,
350
+ haId
351
+ });
352
+ }
353
+ var HOME_CONNECT_IDDF_URL = (haId) => `https://eu.services.home-connect.com/api/iddf/v1/iddf/${encodeURIComponent(haId)}`;
354
+ //#endregion
30
355
  //#region src/home-connect/home-connect-login.ts
31
356
  /** Public OAuth client ids from the Home Connect app login (hcpy). */
32
- var APP_ID = "9B75AC9EC512F36C84256AC47D813E2C1DD0D6520DF774B020E1E6E2EB29B1F3";
357
+ var APP_ID$1 = "9B75AC9EC512F36C84256AC47D813E2C1DD0D6520DF774B020E1E6E2EB29B1F3";
33
358
  var OAUTH_BASE = "https://api.home-connect.com/security/oauth/";
34
359
  var ASSET_BASE = "https://eu.services.home-connect.com/";
35
360
  var REDIRECT_URI = "hcauth://auth/prod";
@@ -56,7 +381,7 @@ var CookieJar = class {
56
381
  function headerList(value) {
57
382
  return value ? [value] : [];
58
383
  }
59
- async function defaultFetch(url, init) {
384
+ async function homeConnectLoginFetch(url, init) {
60
385
  const response = await fetch(url, {
61
386
  method: init.method,
62
387
  headers: { ...init.headers },
@@ -66,7 +391,8 @@ async function defaultFetch(url, init) {
66
391
  return {
67
392
  status: response.status,
68
393
  headers: response.headers,
69
- text: () => response.text()
394
+ text: () => response.text(),
395
+ arrayBuffer: () => response.arrayBuffer()
70
396
  };
71
397
  }
72
398
  function b64url(bytes) {
@@ -196,12 +522,34 @@ async function appliancesForToken(token, fetchImpl, jar) {
196
522
  }
197
523
  if (!isRecord(encryption)) continue;
198
524
  const appliance = listedFromPaired(row, encryption);
199
- if (appliance) out.push(appliance);
525
+ if (!appliance) continue;
526
+ const iddfResponse = await fetchImpl(HOME_CONNECT_IDDF_URL(haId), {
527
+ method: "GET",
528
+ headers: {
529
+ authorization: `Bearer ${token}`,
530
+ accept: "application/zip"
531
+ },
532
+ redirect: "manual"
533
+ });
534
+ if (iddfResponse.status < 400 && iddfResponse.arrayBuffer) {
535
+ const compact = parseIddfZipBuffer(Buffer.from(await iddfResponse.arrayBuffer()), haId);
536
+ if (compact) {
537
+ out.push({
538
+ ...appliance,
539
+ connection: {
540
+ ...appliance.connection,
541
+ deviceDescription: JSON.stringify(compact)
542
+ }
543
+ });
544
+ continue;
545
+ }
546
+ }
547
+ out.push(appliance);
200
548
  }
201
549
  return out;
202
550
  }
203
551
  /** Load every appliance on the account. A stale access token is refreshed once when a refresh token exists. */
204
- async function fetchHomeConnectAccount(creds, fetchImpl = defaultFetch) {
552
+ async function fetchHomeConnectAccount(creds, fetchImpl = homeConnectLoginFetch) {
205
553
  const jar = new CookieJar();
206
554
  let token = creds.token;
207
555
  let refresh = creds.refresh ?? "";
@@ -219,7 +567,7 @@ async function fetchHomeConnectAccount(creds, fetchImpl = defaultFetch) {
219
567
  const refreshed = await exchange(fetchImpl, jar, `${OAUTH_BASE}token`, "POST", { "content-type": "application/x-www-form-urlencoded" }, new URLSearchParams({
220
568
  grant_type: "refresh_token",
221
569
  refresh_token: refresh,
222
- client_id: APP_ID
570
+ client_id: APP_ID$1
223
571
  }).toString());
224
572
  if (refreshed.status >= 400) throw new Error("Home Connect sign-in was rejected");
225
573
  const next = tokensFromBody(refreshed.body);
@@ -240,7 +588,7 @@ function beginHomeConnectSignIn() {
240
588
  const state = b64url(randomBytes(16));
241
589
  const authorize = new URL("authorize", OAUTH_BASE);
242
590
  authorize.searchParams.set("redirect_url", REDIRECT_URI);
243
- authorize.searchParams.set("client_id", APP_ID);
591
+ authorize.searchParams.set("client_id", APP_ID$1);
244
592
  authorize.searchParams.set("response_type", "code");
245
593
  authorize.searchParams.set("prompt", "login");
246
594
  authorize.searchParams.set("code_challenge_method", "S256");
@@ -271,13 +619,13 @@ function callbackParams(callback) {
271
619
  };
272
620
  }
273
621
  /** Exchange the pasted redirect for the account token and each appliance profile. */
274
- async function completeHomeConnectSignIn(input, fetchImpl = defaultFetch) {
622
+ async function completeHomeConnectSignIn(input, fetchImpl = homeConnectLoginFetch) {
275
623
  const parsed = callbackParams(input.callback);
276
624
  if (parsed.state !== input.state) throw new Error("State mismatch. Create the login link again.");
277
625
  const { code, grantType } = parsed;
278
626
  const token = await exchange(fetchImpl, new CookieJar(), `${OAUTH_BASE}token`, "POST", { "content-type": "application/x-www-form-urlencoded" }, new URLSearchParams({
279
627
  grant_type: grantType,
280
- client_id: APP_ID,
628
+ client_id: APP_ID$1,
281
629
  code,
282
630
  code_verifier: input.verifier,
283
631
  redirect_uri: REDIRECT_URI
@@ -285,31 +633,359 @@ async function completeHomeConnectSignIn(input, fetchImpl = defaultFetch) {
285
633
  if (token.status >= 400) throw new Error("Home Connect sign-in was rejected");
286
634
  return fetchHomeConnectAccount(tokensFromBody(token.body), fetchImpl);
287
635
  }
636
+ var HC_FEATURE_ACTIVE_PROGRAM = "BSH.Common.Setting.Program.Active";
637
+ var HC_FEATURE_ROOT_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram";
638
+ var HC_FEATURE_ROOT_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram";
639
+ var HC_FEATURE_REMAINING = "BSH.Common.Option.RemainingProgramTime";
640
+ var HC_FEATURE_PROGRESS = "BSH.Common.Option.ProgramProgress";
641
+ var HC_FEATURE_DOOR = "BSH.Common.Status.DoorState";
642
+ var HC_FEATURE_REMOTE_START = "BSH.Common.Status.RemoteControlActive";
643
+ var HC_FEATURE_POWER = "BSH.Common.Setting.PowerState";
644
+ var HC_FEATURE_SALT = "BSH.Common.Setting.SaltLevel";
645
+ var HC_FEATURE_RINSE = "BSH.Common.Setting.RinseAidLevel";
646
+ var HC_FEATURE_HALF_LOAD = "BSH.Common.Option.HalfLoad";
647
+ var HC_FEATURE_HYGIENE = "BSH.Common.Option.HygienePlus";
648
+ var HC_FEATURE_EXTRA_DRY = "BSH.Common.Option.ExtraDry";
649
+ var HC_FEATURE_INTENSIVE_ZONE = "BSH.Common.Option.IntensivZone";
650
+ var HC_FEATURE_INTENSIVE_ZONE_ALT = "BSH.Common.Option.IntensiveZone";
651
+ var HC_FEATURE_VARIO_SPEED = "BSH.Common.Option.VarioSpeed";
652
+ var HC_FEATURE_VARIO_SPEED_PLUS = "BSH.Common.Option.VarioSpeedPlus";
653
+ var HC_FEATURE_CHILD_LOCK = "BSH.Common.Setting.ChildLock";
654
+ var HC_FEATURE_ENERGY_FORECAST = "BSH.Common.Option.EnergyForecast";
655
+ var HC_FEATURE_WATER_FORECAST = "BSH.Common.Option.WaterForecast";
656
+ var HC_FEATURE_ENERGY_CONSUMPTION = "BSH.Common.Option.EnergyConsumption";
657
+ var HC_FEATURE_ENERGY_CONSUMPTION_STAT = "BSH.Common.Statistic.EnergyConsumption";
658
+ var HC_FEATURE_WATER_CONSUMPTION = "BSH.Common.Option.WaterConsumption";
659
+ var HC_FEATURE_REMOTE_CONTROL_LEVEL = "BSH.Common.Status.RemoteControlLevel";
660
+ var HC_FEATURE_START_IN_RELATIVE = "BSH.Common.Option.StartInRelative";
661
+ var HC_FEATURE_AMBIENT_LIGHT = "BSH.Common.Setting.AmbientLightEnabled";
662
+ var HC_FEATURE_INTERIOR_LIGHT = "BSH.Common.Setting.InteriorLighting";
663
+ var HC_FEATURE_BACKEND_CONNECTED = "BSH.Common.Status.BackendConnected";
664
+ var HC_FEATURE_CLOUD_CONNECTED = "BSH.Common.Status.CloudConnected";
665
+ var HC_FEATURE_PROGRAM_PHASE = "BSH.Common.Status.ProgramPhase";
666
+ var HC_FEATURE_DISHWASHER_PROGRAM_PHASE = "Dishcare.Dishwasher.Status.ProgramPhase";
667
+ var HC_PROGRAM_FEATURE_KEYS = [
668
+ HC_FEATURE_ACTIVE_PROGRAM,
669
+ HC_FEATURE_ROOT_ACTIVE_PROGRAM,
670
+ HC_FEATURE_ROOT_SELECTED_PROGRAM
671
+ ];
672
+ /** Write key for intensive zone — matches the spelling the appliance exposes. */
673
+ function intensiveZoneFeatureKeyFromFeatures(features) {
674
+ if (features.has("BSH.Common.Option.IntensiveZone") && !features.has("BSH.Common.Option.IntensivZone")) return HC_FEATURE_INTENSIVE_ZONE_ALT;
675
+ return HC_FEATURE_INTENSIVE_ZONE;
676
+ }
677
+ var HOME_CONNECT_SENSOR_CATALOG = [
678
+ {
679
+ stableIdSuffix: "energy-forecast",
680
+ kind: "energy-forecast",
681
+ featureKeys: [HC_FEATURE_ENERGY_FORECAST],
682
+ cap: "numeric-sensor",
683
+ defaultUnit: "%",
684
+ nameSuffix: "Energy forecast",
685
+ role: DeviceRole.EnergySensor
686
+ },
687
+ {
688
+ stableIdSuffix: "water-forecast",
689
+ kind: "water-forecast",
690
+ featureKeys: [HC_FEATURE_WATER_FORECAST],
691
+ cap: "numeric-sensor",
692
+ defaultUnit: "%",
693
+ nameSuffix: "Water forecast",
694
+ role: DeviceRole.NumericSensor
695
+ },
696
+ {
697
+ stableIdSuffix: "energy-consumed",
698
+ kind: "energy-consumed",
699
+ featureKeys: [HC_FEATURE_ENERGY_CONSUMPTION, HC_FEATURE_ENERGY_CONSUMPTION_STAT],
700
+ cap: "power-meter",
701
+ defaultUnit: "kWh",
702
+ nameSuffix: "Energy consumed",
703
+ role: DeviceRole.EnergySensor
704
+ },
705
+ {
706
+ stableIdSuffix: "water-consumed",
707
+ kind: "water-consumed",
708
+ featureKeys: [HC_FEATURE_WATER_CONSUMPTION],
709
+ cap: "numeric-sensor",
710
+ defaultUnit: "l",
711
+ nameSuffix: "Water consumed",
712
+ role: DeviceRole.NumericSensor
713
+ },
714
+ {
715
+ stableIdSuffix: "remote-control-level",
716
+ kind: "remote-control-level",
717
+ featureKeys: [HC_FEATURE_REMOTE_CONTROL_LEVEL],
718
+ cap: "enum-sensor",
719
+ nameSuffix: "Remote control level",
720
+ role: DeviceRole.EnumSensor
721
+ },
722
+ {
723
+ stableIdSuffix: "delayed-start",
724
+ kind: "delayed-start",
725
+ featureKeys: [HC_FEATURE_START_IN_RELATIVE],
726
+ cap: "numeric-sensor",
727
+ defaultUnit: "min",
728
+ nameSuffix: "Delayed start",
729
+ role: DeviceRole.NumericSensor
730
+ },
731
+ {
732
+ stableIdSuffix: "interior-light",
733
+ kind: "interior-light",
734
+ featureKeys: [HC_FEATURE_AMBIENT_LIGHT, HC_FEATURE_INTERIOR_LIGHT],
735
+ cap: "binary",
736
+ nameSuffix: "Interior light",
737
+ role: DeviceRole.BinaryHelper
738
+ },
739
+ {
740
+ stableIdSuffix: "connectivity",
741
+ kind: "connectivity",
742
+ featureKeys: [HC_FEATURE_BACKEND_CONNECTED, HC_FEATURE_CLOUD_CONNECTED],
743
+ cap: "binary",
744
+ nameSuffix: "Connectivity",
745
+ role: DeviceRole.ConnectivitySensor
746
+ },
747
+ {
748
+ stableIdSuffix: "program-phase",
749
+ kind: "program-phase",
750
+ featureKeys: [HC_FEATURE_PROGRAM_PHASE, HC_FEATURE_DISHWASHER_PROGRAM_PHASE],
751
+ cap: "enum-sensor",
752
+ nameSuffix: "Program phase",
753
+ role: DeviceRole.EnumSensor
754
+ }
755
+ ];
756
+ function homeConnectSensorCatalogEntry(kind) {
757
+ const entry = HOME_CONNECT_SENSOR_CATALOG.find((e) => e.kind === kind);
758
+ if (entry === void 0) throw new Error(`home-connect: unknown sensor kind ${kind}`);
759
+ return entry;
760
+ }
761
+ var OPERATION_TO_STATE = {
762
+ Ready: "idle",
763
+ Inactive: "idle",
764
+ DelayedStart: "idle",
765
+ ActionRequired: "idle",
766
+ Run: "running",
767
+ Aborting: "running",
768
+ Pause: "paused",
769
+ Finished: "finished",
770
+ Error: "error"
771
+ };
772
+ function normalizeConsumableToken(raw) {
773
+ return raw.trim().toLowerCase().replace(/_/g, "");
774
+ }
775
+ function mapConsumable(raw) {
776
+ if (typeof raw !== "string" || raw.length === 0) return null;
777
+ const norm = normalizeConsumableToken(raw);
778
+ if (norm === "full" || norm === "ok") return "ok";
779
+ if (norm === "low" || norm === "nearlyempty") return "low";
780
+ if (norm === "empty") return "empty";
781
+ return null;
782
+ }
783
+ function readBool(raw) {
784
+ return raw === true || raw === "On" || raw === "Open" || raw === "Connected";
785
+ }
786
+ function readPowerOn(raw) {
787
+ if (raw === true || raw === "On" || raw === "OnState") return true;
788
+ if (raw === false || raw === "Off" || raw === "Standby") return false;
789
+ return false;
790
+ }
791
+ function readString(raw) {
792
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
793
+ }
794
+ function readNumber(raw) {
795
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
796
+ if (typeof raw === "string") {
797
+ const n = Number(raw);
798
+ return Number.isFinite(n) ? n : null;
799
+ }
800
+ return null;
801
+ }
802
+ function readFeatureValue(features, keys) {
803
+ for (const key of keys) if (features.has(key)) return features.get(key);
804
+ }
805
+ /** Convert BSH remaining time raw value → whole minutes for the UI. */
806
+ function remainingMinutesFromRaw(raw) {
807
+ const n = readNumber(raw);
808
+ if (n === null) return null;
809
+ if (n > 600) return Math.round(n / 60);
810
+ if (n > 0 && n < 48 && !Number.isInteger(n)) return Math.round(n * 60);
811
+ return Math.round(n);
812
+ }
813
+ function errorLabelFromFeatures(features, operationState, state) {
814
+ if (state !== "error") return null;
815
+ return readString(readFeatureValue(features, ["BSH.Common.Status.EventPresentState"])) ?? operationState;
816
+ }
817
+ /**
818
+ * Map BSH.Common.* feature values → {@link DishwasherStatus}. Pure — the only
819
+ * place dishwasher slice assembly happens for Home Connect local.
820
+ */
821
+ function dishwasherStatusFromFeatures(input) {
822
+ const { features, availablePrograms, nowMs } = input;
823
+ const op = readString(features.get("BSH.Common.Status.OperationState")) ?? "Ready";
824
+ const state = OPERATION_TO_STATE[op] ?? "unavailable";
825
+ const program = readString(readFeatureValue(features, HC_PROGRAM_FEATURE_KEYS));
826
+ const remainingMinutes = remainingMinutesFromRaw(features.get(HC_FEATURE_REMAINING));
827
+ const progressPercent = readNumber(features.get(HC_FEATURE_PROGRESS));
828
+ const doorOpen = features.get(HC_FEATURE_DOOR) === "Open";
829
+ const remoteStartAllowed = readBool(features.get(HC_FEATURE_REMOTE_START));
830
+ const powerOn = readPowerOn(features.get(HC_FEATURE_POWER));
831
+ return {
832
+ state,
833
+ program,
834
+ availablePrograms: [...availablePrograms],
835
+ remainingMinutes,
836
+ progressPercent,
837
+ doorOpen,
838
+ remoteStartAllowed,
839
+ powerOn,
840
+ saltLevel: mapConsumable(features.get(HC_FEATURE_SALT)),
841
+ rinseAidLevel: mapConsumable(features.get(HC_FEATURE_RINSE)),
842
+ halfLoad: readBool(features.get(HC_FEATURE_HALF_LOAD)),
843
+ hygienePlus: readBool(features.get(HC_FEATURE_HYGIENE)),
844
+ extraDry: readBool(features.get(HC_FEATURE_EXTRA_DRY)),
845
+ intensiveZone: readBool(features.get("BSH.Common.Option.IntensivZone")) || readBool(features.get("BSH.Common.Option.IntensiveZone")),
846
+ varioSpeedPlus: readBool(readFeatureValue(features, [HC_FEATURE_VARIO_SPEED_PLUS, HC_FEATURE_VARIO_SPEED])),
847
+ childLock: readBool(features.get(HC_FEATURE_CHILD_LOCK)),
848
+ errorCode: state === "error" ? 1 : null,
849
+ errorLabel: errorLabelFromFeatures(features, op, state),
850
+ lastFetchedAt: nowMs
851
+ };
852
+ }
853
+ function homeConnectSensorSliceFromFeatures(input) {
854
+ const entry = homeConnectSensorCatalogEntry(input.kind);
855
+ const raw = readFeatureValue(input.features, entry.featureKeys);
856
+ const nowMs = input.nowMs;
857
+ if (entry.cap === "enum-sensor") return {
858
+ cap: "enum-sensor",
859
+ status: {
860
+ value: readString(raw) ?? "unknown",
861
+ lastFetchedAt: nowMs
862
+ }
863
+ };
864
+ if (entry.cap === "binary") return {
865
+ cap: "binary",
866
+ status: {
867
+ on: readBool(raw),
868
+ lastChangedAt: nowMs
869
+ }
870
+ };
871
+ if (entry.cap === "power-meter") {
872
+ const n = readNumber(raw);
873
+ return {
874
+ cap: "power-meter",
875
+ status: {
876
+ ...n !== null ? { kwhTotal: n } : {},
877
+ unit: entry.defaultUnit,
878
+ lastFetchedAt: nowMs
879
+ }
880
+ };
881
+ }
882
+ const n = readNumber(raw);
883
+ let unit = entry.defaultUnit;
884
+ if (input.kind === "water-forecast" || input.kind === "water-consumed") {
885
+ if (n !== null && n >= 1e4 && unit === "l") unit = "ml";
886
+ }
887
+ if (input.kind === "delayed-start" && n !== null && n > 600) return {
888
+ cap: "numeric-sensor",
889
+ status: {
890
+ value: Math.round(n / 60),
891
+ unit: "min",
892
+ lastFetchedAt: nowMs
893
+ }
894
+ };
895
+ if (n === null) return {
896
+ cap: "numeric-sensor",
897
+ status: null
898
+ };
899
+ return {
900
+ cap: "numeric-sensor",
901
+ status: {
902
+ value: n,
903
+ ...unit !== void 0 ? { unit } : {},
904
+ lastFetchedAt: nowMs
905
+ }
906
+ };
907
+ }
908
+ /** Command resource keys for WS POST-style mutations. */
909
+ var HC_CMD_START = "BSH.Common.Command.StartProgram";
910
+ var HC_CMD_PAUSE = "BSH.Common.Command.PauseProgram";
911
+ var HC_CMD_STOP = "BSH.Common.Command.AbortProgram";
912
+ var HC_CMD_SET_PROGRAM = "BSH.Common.Setting.Program.Active";
913
+ var HC_CMD_SET_POWER = "BSH.Common.Setting.PowerState";
288
914
  //#endregion
289
915
  //#region src/home-connect/home-connect-discovery.ts
916
+ /** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
917
+ function homeConnectDishwasherEntityName(containerName) {
918
+ const base = containerName.trim();
919
+ return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
920
+ }
921
+ function buildHomeConnectApplianceChildSpecs(applianceName, haId) {
922
+ const label = applianceName.trim().length > 0 ? applianceName.trim() : haId;
923
+ return [{
924
+ role: "dishwasher",
925
+ stableIdSuffix: "dishwasher",
926
+ name: homeConnectDishwasherEntityName(applianceName)
927
+ }, ...HOME_CONNECT_SENSOR_CATALOG.map((entry) => ({
928
+ role: entry.kind,
929
+ stableIdSuffix: entry.stableIdSuffix,
930
+ name: `${label} — ${entry.nameSuffix}`
931
+ }))];
932
+ }
290
933
  function deviceTypeForApplianceType(type) {
291
- if (type.toLowerCase().includes("dishwasher")) return DeviceType.Dishwasher;
934
+ if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
292
935
  return null;
293
936
  }
294
- function buildHomeConnectCandidates(input) {
295
- const out = [];
296
- for (const app of input.appliances) {
297
- const camType = deviceTypeForApplianceType(app.type);
298
- if (camType === null) continue;
299
- const adoptedId = input.adopted.get(app.haId) ?? null;
300
- out.push({
301
- childNativeId: app.haId,
302
- name: app.name.length > 0 ? app.name : app.haId,
303
- type: camType,
937
+ function sensorDiscoveryChild(haId, spec) {
938
+ const cap = HOME_CONNECT_SENSOR_CATALOG.find((e) => e.stableIdSuffix === spec.stableIdSuffix)?.cap ?? "numeric-sensor";
939
+ return {
940
+ childNativeId: `${haId}#${spec.stableIdSuffix}`,
941
+ name: spec.name,
942
+ type: DeviceType.Sensor,
943
+ status: "online",
944
+ metadata: { serialNumber: haId },
945
+ alreadyAdopted: false,
946
+ adoptedDeviceId: null,
947
+ capabilities: [cap]
948
+ };
949
+ }
950
+ function dishwasherContainerCandidate(app, adoptedId) {
951
+ const name = app.name.length > 0 ? app.name : app.haId;
952
+ const children = buildHomeConnectApplianceChildSpecs(name, app.haId).map((spec) => {
953
+ if (spec.role === "dishwasher") return {
954
+ childNativeId: `${app.haId}#dishwasher`,
955
+ name: spec.name,
956
+ type: DeviceType.Dishwasher,
304
957
  status: "online",
305
958
  metadata: {
306
959
  serialNumber: app.haId,
307
960
  model: app.type
308
961
  },
309
- alreadyAdopted: adoptedId !== null,
310
- adoptedDeviceId: adoptedId,
311
- capabilities: camType === DeviceType.Dishwasher ? ["dishwasher"] : []
312
- });
962
+ alreadyAdopted: false,
963
+ adoptedDeviceId: null,
964
+ capabilities: ["dishwasher"]
965
+ };
966
+ return sensorDiscoveryChild(app.haId, spec);
967
+ });
968
+ return {
969
+ childNativeId: app.haId,
970
+ name,
971
+ type: DeviceType.Container,
972
+ status: "online",
973
+ metadata: {
974
+ serialNumber: app.haId,
975
+ model: app.type
976
+ },
977
+ alreadyAdopted: adoptedId !== null,
978
+ adoptedDeviceId: adoptedId,
979
+ capabilities: [],
980
+ children
981
+ };
982
+ }
983
+ function buildHomeConnectCandidates(input) {
984
+ const out = [];
985
+ for (const app of input.appliances) {
986
+ if (deviceTypeForApplianceType(app.type) === null) continue;
987
+ const adoptedId = input.adopted.get(app.haId) ?? null;
988
+ out.push(dishwasherContainerCandidate(app, adoptedId));
313
989
  }
314
990
  return out;
315
991
  }
@@ -465,7 +1141,8 @@ var homeConnectStoredApplianceSchema = object({
465
1141
  host: string().min(1),
466
1142
  psk: string().min(1),
467
1143
  mode: HomeConnectModeSchema,
468
- iv64: string().optional()
1144
+ iv64: string().optional(),
1145
+ deviceDescription: homeConnectDeviceDescriptionSchema
469
1146
  });
470
1147
  /** The integration is the Home Connect account. Profiles are downloaded once at sign-in. */
471
1148
  var homeConnectConfigSchema = object({
@@ -678,6 +1355,428 @@ var HomeConnectConnectionResolver = class {
678
1355
  };
679
1356
  var homeConnectConnections = new HomeConnectConnectionResolver();
680
1357
  //#endregion
1358
+ //#region src/home-connect/home-connect-tls-wire.ts
1359
+ function decodePsk64(psk64) {
1360
+ return Buffer.from(`${psk64}===`, "base64url");
1361
+ }
1362
+ function parseHttpHeaders(buffer) {
1363
+ const marker = buffer.indexOf("\r\n\r\n");
1364
+ if (marker < 0) return null;
1365
+ return {
1366
+ headers: buffer.subarray(0, marker + 4),
1367
+ rest: buffer.subarray(marker + 4)
1368
+ };
1369
+ }
1370
+ function readWsFrame(buffer) {
1371
+ if (buffer.length < 2) return null;
1372
+ const b0 = buffer.at(0);
1373
+ const b1 = buffer.at(1);
1374
+ if (b0 === void 0 || b1 === void 0) return null;
1375
+ const opcode = b0 & 15;
1376
+ const masked = (b1 & 128) !== 0;
1377
+ let payloadLen = b1 & 127;
1378
+ let offset = 2;
1379
+ if (payloadLen === 126) {
1380
+ if (buffer.length < 4) return null;
1381
+ payloadLen = buffer.readUInt16BE(2);
1382
+ offset = 4;
1383
+ } else if (payloadLen === 127) {
1384
+ if (buffer.length < 10) return null;
1385
+ payloadLen = Number(buffer.readBigUInt64BE(2));
1386
+ offset = 10;
1387
+ }
1388
+ const maskLen = masked ? 4 : 0;
1389
+ if (buffer.length < offset + maskLen + payloadLen) return null;
1390
+ let payload = buffer.subarray(offset + maskLen, offset + maskLen + payloadLen);
1391
+ if (masked) {
1392
+ const mask = buffer.subarray(offset, offset + 4);
1393
+ const unmasked = Buffer.alloc(payload.length);
1394
+ for (let i = 0; i < payload.length; i += 1) unmasked[i] = (payload.at(i) ?? 0) ^ (mask.at(i % 4) ?? 0);
1395
+ payload = unmasked;
1396
+ }
1397
+ if (opcode === 8) return {
1398
+ payload: Buffer.alloc(0),
1399
+ consumed: offset + maskLen + payloadLen
1400
+ };
1401
+ return {
1402
+ payload,
1403
+ consumed: offset + maskLen + payloadLen
1404
+ };
1405
+ }
1406
+ function writeWsTextFrame(text) {
1407
+ const payload = Buffer.from(text, "utf8");
1408
+ const mask = randomBytes(4);
1409
+ const headerLen = payload.length < 126 ? 2 : payload.length < 65536 ? 4 : 10;
1410
+ const frame = Buffer.alloc(headerLen + 4 + payload.length);
1411
+ frame[0] = 129;
1412
+ if (payload.length < 126) {
1413
+ frame[1] = 128 | payload.length;
1414
+ mask.copy(frame, 2);
1415
+ for (let i = 0; i < payload.length; i += 1) {
1416
+ const pb = payload.at(i) ?? 0;
1417
+ const mb = mask.at(i % 4) ?? 0;
1418
+ frame[6 + i] = pb ^ mb;
1419
+ }
1420
+ } else if (payload.length < 65536) {
1421
+ frame[1] = 254;
1422
+ frame.writeUInt16BE(payload.length, 2);
1423
+ mask.copy(frame, 4);
1424
+ for (let i = 0; i < payload.length; i += 1) {
1425
+ const pb = payload.at(i) ?? 0;
1426
+ const mb = mask.at(i % 4) ?? 0;
1427
+ frame[8 + i] = pb ^ mb;
1428
+ }
1429
+ } else {
1430
+ frame[1] = 255;
1431
+ frame.writeBigUInt64BE(BigInt(payload.length), 2);
1432
+ mask.copy(frame, 10);
1433
+ for (let i = 0; i < payload.length; i += 1) {
1434
+ const pb = payload.at(i) ?? 0;
1435
+ const mb = mask.at(i % 4) ?? 0;
1436
+ frame[14 + i] = pb ^ mb;
1437
+ }
1438
+ }
1439
+ return frame;
1440
+ }
1441
+ function createHomeConnectTlsWire(input) {
1442
+ let socket = null;
1443
+ let open = false;
1444
+ const textListeners = /* @__PURE__ */ new Set();
1445
+ const closedListeners = /* @__PURE__ */ new Set();
1446
+ let recvBuffer = Buffer.alloc(0);
1447
+ let upgraded = false;
1448
+ function emitText(text) {
1449
+ for (const fn of textListeners) fn(text);
1450
+ }
1451
+ function emitClosed() {
1452
+ open = false;
1453
+ for (const fn of closedListeners) fn();
1454
+ }
1455
+ function drainFrames() {
1456
+ while (true) {
1457
+ const parsed = readWsFrame(recvBuffer);
1458
+ if (!parsed) return;
1459
+ recvBuffer = Buffer.from(recvBuffer.subarray(parsed.consumed));
1460
+ if (parsed.payload.length === 0) continue;
1461
+ emitText(parsed.payload.toString("utf8"));
1462
+ }
1463
+ }
1464
+ return {
1465
+ get open() {
1466
+ return open;
1467
+ },
1468
+ async connect() {
1469
+ if (open) return;
1470
+ const psk = decodePsk64(input.psk);
1471
+ await new Promise((resolve, reject) => {
1472
+ socket = connect({
1473
+ host: input.connectAddress,
1474
+ port: 443,
1475
+ servername: input.host,
1476
+ ciphers: "PSK",
1477
+ minVersion: "TLSv1.2",
1478
+ maxVersion: "TLSv1.2",
1479
+ rejectUnauthorized: false,
1480
+ checkServerIdentity: () => void 0,
1481
+ pskCallback: () => ({
1482
+ psk,
1483
+ identity: ""
1484
+ })
1485
+ }, () => {
1486
+ const key = randomBytes(16).toString("base64");
1487
+ const req = [
1488
+ "GET /homeconnect HTTP/1.1",
1489
+ `Host: ${input.host}`,
1490
+ "Upgrade: websocket",
1491
+ "Connection: Upgrade",
1492
+ `Sec-WebSocket-Key: ${key}`,
1493
+ "Sec-WebSocket-Version: 13",
1494
+ "",
1495
+ ""
1496
+ ].join("\r\n");
1497
+ socket?.write(req);
1498
+ });
1499
+ socket.on("data", (chunk) => {
1500
+ recvBuffer = Buffer.concat([recvBuffer, chunk]);
1501
+ if (!upgraded) {
1502
+ const parsed = parseHttpHeaders(recvBuffer);
1503
+ if (!parsed) return;
1504
+ if (!(parsed.headers.toString("utf8").split("\r\n")[0] ?? "").includes("101")) {
1505
+ reject(/* @__PURE__ */ new Error("Home Connect WebSocket upgrade failed"));
1506
+ return;
1507
+ }
1508
+ upgraded = true;
1509
+ recvBuffer = Buffer.from(parsed.rest);
1510
+ open = true;
1511
+ resolve();
1512
+ drainFrames();
1513
+ return;
1514
+ }
1515
+ drainFrames();
1516
+ });
1517
+ socket.on("error", (err) => reject(err));
1518
+ socket.on("close", () => emitClosed());
1519
+ });
1520
+ },
1521
+ send(text) {
1522
+ if (!socket || !open) throw new Error("Home Connect TLS wire not open");
1523
+ socket.write(writeWsTextFrame(text));
1524
+ },
1525
+ close() {
1526
+ socket?.destroy();
1527
+ socket = null;
1528
+ emitClosed();
1529
+ },
1530
+ onText(listener) {
1531
+ textListeners.add(listener);
1532
+ return () => textListeners.delete(listener);
1533
+ },
1534
+ onClosed(listener) {
1535
+ closedListeners.add(listener);
1536
+ return () => closedListeners.delete(listener);
1537
+ }
1538
+ };
1539
+ }
1540
+ //#endregion
1541
+ //#region src/home-connect/home-connect-message.ts
1542
+ var HC_ACTION = {
1543
+ GET: "GET",
1544
+ POST: "POST",
1545
+ RESPONSE: "RESPONSE",
1546
+ NOTIFY: "NOTIFY"
1547
+ };
1548
+ function loadHomeConnectMessage(raw) {
1549
+ const msg = JSON.parse(raw);
1550
+ if (typeof msg !== "object" || msg === null) throw new Error("Invalid Home Connect message");
1551
+ const row = msg;
1552
+ const sID = row["sID"];
1553
+ const msgID = row["msgID"];
1554
+ const resource = row["resource"];
1555
+ const version = row["version"];
1556
+ const action = row["action"];
1557
+ if (typeof sID !== "number" || typeof msgID !== "number" || typeof resource !== "string" || typeof version !== "number" || typeof action !== "string") throw new Error("Invalid Home Connect message fields");
1558
+ let data;
1559
+ const dataRaw = row["data"];
1560
+ if (dataRaw !== void 0) data = (Array.isArray(dataRaw) ? dataRaw : [dataRaw]).filter((item) => typeof item === "object" && item !== null);
1561
+ const code = row["code"];
1562
+ return {
1563
+ sID,
1564
+ msgID,
1565
+ resource,
1566
+ version,
1567
+ action,
1568
+ data,
1569
+ code: typeof code === "number" ? code : void 0
1570
+ };
1571
+ }
1572
+ function dumpHomeConnectMessage(input) {
1573
+ const msg = {
1574
+ sID: input.sID,
1575
+ msgID: input.msgID,
1576
+ resource: input.resource,
1577
+ version: input.version,
1578
+ action: input.action
1579
+ };
1580
+ if (input.data !== void 0) msg["data"] = Array.isArray(input.data) ? input.data : [input.data];
1581
+ if (input.code !== void 0) msg["code"] = input.code;
1582
+ return JSON.stringify(msg);
1583
+ }
1584
+ function responseTo(init, data) {
1585
+ return {
1586
+ sID: init.sID,
1587
+ msgID: init.msgID,
1588
+ resource: init.resource,
1589
+ version: init.version,
1590
+ action: HC_ACTION.RESPONSE,
1591
+ data: [data]
1592
+ };
1593
+ }
1594
+ //#endregion
1595
+ //#region src/home-connect/home-connect-session.ts
1596
+ var APP_NAME = "CamStack";
1597
+ var APP_ID = "camstack-hc";
1598
+ /** Max wait for appliance `/ei/initialValues` after the socket opens. */
1599
+ var HOME_CONNECT_INIT_RESOURCE_TIMEOUT_MS = 12e3;
1600
+ var HomeConnectSession = class {
1601
+ #wire;
1602
+ #entityIndex;
1603
+ #onFeature;
1604
+ #onEmptyEntityIndex;
1605
+ #sid = 0;
1606
+ #warnedUndecodedUid = false;
1607
+ #nextMsgId = 1;
1608
+ #serviceVersions = {};
1609
+ #pending = /* @__PURE__ */ new Map();
1610
+ #connected = false;
1611
+ #unsubText = null;
1612
+ constructor(options) {
1613
+ this.#wire = options.wire;
1614
+ this.#entityIndex = options.entityIndex;
1615
+ this.#onFeature = options.onFeature;
1616
+ this.#onEmptyEntityIndex = options.onEmptyEntityIndex;
1617
+ }
1618
+ get connected() {
1619
+ return this.#connected;
1620
+ }
1621
+ async connect() {
1622
+ if (this.#connected) return;
1623
+ this.#unsubText = this.#wire.onText((text) => this.#handleIncoming(text));
1624
+ const initPromise = this.#waitForResource("/ei/initialValues");
1625
+ await this.#wire.connect();
1626
+ const init = loadHomeConnectMessage(await initPromise);
1627
+ this.#sid = init.sID;
1628
+ const edMsgId = (init.data?.[0])?.["edMsgID"];
1629
+ if (typeof edMsgId === "number") this.#nextMsgId = edMsgId;
1630
+ this.#wire.send(dumpHomeConnectMessage(responseTo(init, {
1631
+ deviceType: init.version === 1 ? 2 : "Application",
1632
+ deviceName: APP_NAME,
1633
+ deviceID: APP_ID
1634
+ })));
1635
+ const services = await this.#sendGet("/ci/services", 1);
1636
+ this.#setServiceVersions(services);
1637
+ if (this.#serviceVersions["ei"] === 2) await this.#sendNotify("/ei/deviceReady");
1638
+ if ("iz" in this.#serviceVersions) await this.#sendGet("/iz/info", this.#serviceVersions["iz"] ?? 1).catch(() => void 0);
1639
+ const mandatory = await this.#sendGet("/ro/allMandatoryValues", this.#serviceVersions["ro"] ?? 1);
1640
+ this.#applyValuePayload(mandatory.data);
1641
+ if (this.#entityIndex.size === 0) this.#onEmptyEntityIndex?.();
1642
+ this.#connected = true;
1643
+ }
1644
+ close() {
1645
+ this.#connected = false;
1646
+ this.#unsubText?.();
1647
+ this.#unsubText = null;
1648
+ for (const pending of this.#pending.values()) pending.reject(/* @__PURE__ */ new Error("Home Connect session closed"));
1649
+ this.#pending.clear();
1650
+ this.#wire.close();
1651
+ }
1652
+ async setFeatureByName(key, value) {
1653
+ const encoded = encodeHomeConnectValue(key, value, this.#entityIndex);
1654
+ if (!encoded) throw new Error(`Unknown Home Connect feature: ${key}`);
1655
+ await this.#sendPost("/ro/values", this.#serviceVersions["ro"] ?? 1, [{
1656
+ uid: encoded.uid,
1657
+ value: encoded.value
1658
+ }]);
1659
+ this.#onFeature(key, value);
1660
+ }
1661
+ #setServiceVersions(message) {
1662
+ const versions = {};
1663
+ for (const row of message.data ?? []) {
1664
+ const service = row["service"];
1665
+ const version = row["version"];
1666
+ if (typeof service === "string" && typeof version === "number") versions[service] = version;
1667
+ }
1668
+ this.#serviceVersions = versions;
1669
+ }
1670
+ #applyValuePayload(data) {
1671
+ if (!data) return;
1672
+ for (const row of data) this.#applyUidRow(row);
1673
+ }
1674
+ #applyUidRow(row) {
1675
+ const uidRaw = row["uid"];
1676
+ const value = row["value"];
1677
+ if (typeof uidRaw !== "number") return;
1678
+ const decoded = decodeHomeConnectValue(uidRaw, value, this.#entityIndex);
1679
+ if (decoded) {
1680
+ this.#onFeature(decoded.key, decoded.value);
1681
+ return;
1682
+ }
1683
+ if (this.#entityIndex.size === 0 && !this.#warnedUndecodedUid) {
1684
+ this.#warnedUndecodedUid = true;
1685
+ this.#onEmptyEntityIndex?.();
1686
+ }
1687
+ }
1688
+ #handleIncoming(text) {
1689
+ let message;
1690
+ try {
1691
+ message = loadHomeConnectMessage(text);
1692
+ } catch {
1693
+ return;
1694
+ }
1695
+ if (message.action === HC_ACTION.RESPONSE) {
1696
+ const waiter = this.#pending.get(message.msgID);
1697
+ if (waiter) {
1698
+ this.#pending.delete(message.msgID);
1699
+ waiter.resolve(message);
1700
+ }
1701
+ return;
1702
+ }
1703
+ if (message.action === HC_ACTION.NOTIFY && message.resource === "/ro/values") this.#applyValuePayload(message.data);
1704
+ }
1705
+ #waitForResource(resource) {
1706
+ return new Promise((resolve, reject) => {
1707
+ let settled = false;
1708
+ const finish = (fn) => {
1709
+ if (settled) return;
1710
+ settled = true;
1711
+ clearTimeout(timer);
1712
+ unsubText();
1713
+ unsubClosed();
1714
+ fn();
1715
+ };
1716
+ const unsubText = this.#wire.onText((text) => {
1717
+ try {
1718
+ if (loadHomeConnectMessage(text).resource === resource) finish(() => resolve(text));
1719
+ } catch {}
1720
+ });
1721
+ const unsubClosed = this.#wire.onClosed(() => {
1722
+ finish(() => reject(/* @__PURE__ */ new Error("Home Connect wire closed before init")));
1723
+ });
1724
+ const timer = setTimeout(() => {
1725
+ finish(() => reject(/* @__PURE__ */ new Error(`Home Connect init timeout waiting for ${resource}`)));
1726
+ }, HOME_CONNECT_INIT_RESOURCE_TIMEOUT_MS);
1727
+ });
1728
+ }
1729
+ async #sendGet(resource, version) {
1730
+ return this.#sendAndWaitResponse({
1731
+ sID: this.#sid,
1732
+ msgID: this.#allocMsgId(),
1733
+ resource,
1734
+ version,
1735
+ action: HC_ACTION.GET
1736
+ });
1737
+ }
1738
+ async #sendPost(resource, version, data) {
1739
+ return this.#sendAndWaitResponse({
1740
+ sID: this.#sid,
1741
+ msgID: this.#allocMsgId(),
1742
+ resource,
1743
+ version,
1744
+ action: HC_ACTION.POST,
1745
+ data
1746
+ });
1747
+ }
1748
+ async #sendNotify(resource) {
1749
+ const msg = {
1750
+ sID: this.#sid,
1751
+ msgID: this.#allocMsgId(),
1752
+ resource,
1753
+ version: this.#serviceVersions[resource.slice(1, 3)] ?? 1,
1754
+ action: HC_ACTION.NOTIFY
1755
+ };
1756
+ this.#wire.send(dumpHomeConnectMessage(msg));
1757
+ }
1758
+ #allocMsgId() {
1759
+ const id = this.#nextMsgId;
1760
+ this.#nextMsgId += 1;
1761
+ return id;
1762
+ }
1763
+ async #sendAndWaitResponse(outgoing) {
1764
+ return new Promise((resolve, reject) => {
1765
+ this.#pending.set(outgoing.msgID, {
1766
+ resolve,
1767
+ reject
1768
+ });
1769
+ this.#wire.send(dumpHomeConnectMessage(outgoing));
1770
+ setTimeout(() => {
1771
+ if (this.#pending.has(outgoing.msgID)) {
1772
+ this.#pending.delete(outgoing.msgID);
1773
+ reject(/* @__PURE__ */ new Error(`Home Connect response timeout for ${outgoing.resource}`));
1774
+ }
1775
+ }, 5e3);
1776
+ });
1777
+ }
1778
+ };
1779
+ //#endregion
681
1780
  //#region src/home-connect/home-connect-ws-client.ts
682
1781
  var WS_OPEN = 1;
683
1782
  function programNamesFromLegacyList(programsRaw) {
@@ -753,10 +1852,110 @@ function parseDeviceDescription(input) {
753
1852
  return { appliances: [] };
754
1853
  }
755
1854
  }
756
- function createHomeConnectWsClient(config, deps = {}) {
757
- const wsFactory = deps.wsFactory ?? defaultWsFactory;
1855
+ function usesProtocolStack(config, deps) {
1856
+ if (deps.wireFactory) return true;
1857
+ return config.mode === "TLS";
1858
+ }
1859
+ async function resolveConnectAddress(host) {
1860
+ try {
1861
+ const result = await lookup(host, { verbatim: true });
1862
+ return typeof result === "string" ? result : result.address;
1863
+ } catch {
1864
+ return host.replace(/^https?:\/\//, "").split("/")[0] ?? host;
1865
+ }
1866
+ }
1867
+ var defaultWireFactory = (input) => {
1868
+ if (input.mode === "TLS") return createHomeConnectTlsWire({
1869
+ host: input.host,
1870
+ connectAddress: input.connectAddress,
1871
+ psk: input.psk
1872
+ });
1873
+ throw new Error("Home Connect AES local socket is not implemented — use TLS pairing");
1874
+ };
1875
+ function createHomeConnectWsClient(config, deps = {}) {
1876
+ const appliances = [...parseDeviceDescription(config.deviceDescription).appliances];
1877
+ const entityIndex = entityIndexFromDescription(config.deviceDescription);
1878
+ const primaryHaId = appliances[0]?.haId ?? config.host;
1879
+ if (usesProtocolStack(config, deps)) return createProtocolHomeConnectClient({
1880
+ config,
1881
+ appliances,
1882
+ entityIndex,
1883
+ primaryHaId,
1884
+ wireFactory: deps.wireFactory ?? defaultWireFactory,
1885
+ onProtocolWarn: deps.onProtocolWarn
1886
+ });
1887
+ if (entityIndex.size > 0 && config.mode === "AES") deps.onProtocolWarn?.("aes-legacy-fallback-with-entities");
1888
+ return createLegacySubscribeClient(config, deps, appliances);
1889
+ }
1890
+ function createProtocolHomeConnectClient(input) {
1891
+ const featureStore = /* @__PURE__ */ new Map();
1892
+ const listeners = /* @__PURE__ */ new Set();
1893
+ let session = null;
1894
+ let connected = false;
1895
+ const haId = input.primaryHaId;
1896
+ function ensureFeatures() {
1897
+ let map = featureStore.get(haId);
1898
+ if (!map) {
1899
+ map = /* @__PURE__ */ new Map();
1900
+ featureStore.set(haId, map);
1901
+ }
1902
+ return map;
1903
+ }
1904
+ function applyFeature(key, value) {
1905
+ ensureFeatures().set(key, value);
1906
+ for (const fn of listeners) fn(haId, key, value);
1907
+ }
1908
+ return {
1909
+ async connect() {
1910
+ if (connected) return;
1911
+ const connectAddress = await resolveConnectAddress(input.config.host);
1912
+ const wire = input.wireFactory({
1913
+ host: input.config.host,
1914
+ connectAddress,
1915
+ psk: input.config.psk,
1916
+ mode: input.config.mode,
1917
+ iv64: input.config.iv64
1918
+ });
1919
+ let warnedEmptyIndex = false;
1920
+ session = new HomeConnectSession({
1921
+ wire,
1922
+ entityIndex: input.entityIndex,
1923
+ onFeature: applyFeature,
1924
+ onEmptyEntityIndex: () => {
1925
+ if (warnedEmptyIndex) return;
1926
+ warnedEmptyIndex = true;
1927
+ input.onProtocolWarn?.("empty-entity-index");
1928
+ }
1929
+ });
1930
+ await session.connect();
1931
+ connected = true;
1932
+ },
1933
+ close() {
1934
+ connected = false;
1935
+ session?.close();
1936
+ session = null;
1937
+ },
1938
+ getAppliances() {
1939
+ return input.appliances;
1940
+ },
1941
+ getFeatures(targetHaId) {
1942
+ if (targetHaId !== haId && !featureStore.has(targetHaId)) return /* @__PURE__ */ new Map();
1943
+ return featureStore.get(targetHaId) ?? ensureFeatures();
1944
+ },
1945
+ async setFeature(targetHaId, key, value) {
1946
+ if (!session?.connected) throw new Error("Home Connect WebSocket not connected");
1947
+ if (targetHaId !== haId) throw new Error(`Unknown appliance ${targetHaId}`);
1948
+ await session.setFeatureByName(key, value);
1949
+ },
1950
+ onFeatureChange(listener) {
1951
+ listeners.add(listener);
1952
+ return () => listeners.delete(listener);
1953
+ }
1954
+ };
1955
+ }
1956
+ function createLegacySubscribeClient(config, deps, appliances) {
1957
+ const wsFactory = deps.wsFactory ?? defaultWsFactoryLegacy;
758
1958
  const url = homeConnectWsUrl(config);
759
- const appliances = [...parseDeviceDescription(config.deviceDescription).appliances];
760
1959
  const featureStore = /* @__PURE__ */ new Map();
761
1960
  const listeners = /* @__PURE__ */ new Set();
762
1961
  let ws = null;
@@ -801,27 +2000,9 @@ function createHomeConnectWsClient(config, deps = {}) {
801
2000
  if (typeof msg !== "object" || msg === null) return;
802
2001
  const row = msg;
803
2002
  if (row["type"] === "feature") {
804
- const haId = row["haId"];
2003
+ const msgHaId = row["haId"];
805
2004
  const key = row["key"];
806
- if (typeof haId === "string" && typeof key === "string") applyFeature(haId, key, row["value"]);
807
- }
808
- if (row["type"] === "appliance") {
809
- const haId = row["haId"];
810
- const name = row["name"];
811
- const type = row["type"];
812
- const programs = row["programs"];
813
- if (typeof haId === "string") {
814
- const programList = [];
815
- if (Array.isArray(programs)) {
816
- for (const p of programs) if (typeof p === "string") programList.push(p);
817
- }
818
- appliances.push({
819
- haId,
820
- name: typeof name === "string" ? name : haId,
821
- type: typeof type === "string" ? type : "Dishwasher",
822
- availablePrograms: programList
823
- });
824
- }
2005
+ if (typeof msgHaId === "string" && typeof key === "string") applyFeature(msgHaId, key, row["value"]);
825
2006
  }
826
2007
  } catch {}
827
2008
  });
@@ -856,10 +2037,10 @@ function createHomeConnectWsClient(config, deps = {}) {
856
2037
  }
857
2038
  function isWsLike(value) {
858
2039
  if (typeof value !== "object" || value === null) return false;
859
- const ws = value;
860
- return typeof ws["readyState"] === "number" && typeof ws["send"] === "function" && typeof ws["close"] === "function" && typeof ws["addEventListener"] === "function" && typeof ws["removeEventListener"] === "function";
2040
+ const row = value;
2041
+ return typeof row["readyState"] === "number" && typeof row["send"] === "function" && typeof row["close"] === "function" && typeof row["addEventListener"] === "function" && typeof row["removeEventListener"] === "function";
861
2042
  }
862
- function defaultWsFactory(url) {
2043
+ function defaultWsFactoryLegacy(url) {
863
2044
  const WS = globalThis.WebSocket;
864
2045
  if (typeof WS !== "function") throw new Error(`WebSocket unavailable — cannot connect to ${url}`);
865
2046
  const socket = new WS(url);
@@ -920,8 +2101,115 @@ function accountClient(appliances, makeGateway) {
920
2101
  };
921
2102
  }
922
2103
  //#endregion
923
- //#region src/home-connect/home-connect-integration-manager.ts
924
- function storedAppliance(row) {
2104
+ //#region src/home-connect/home-connect-iddf-fetch.ts
2105
+ function entityCountInStoredDescription(description) {
2106
+ if (description === void 0) return 0;
2107
+ return entityIndexFromDescription(description).size;
2108
+ }
2109
+ async function fetchHomeConnectIddfCompact(haId, token, fetchImpl) {
2110
+ const response = await fetchImpl(HOME_CONNECT_IDDF_URL(haId), {
2111
+ method: "GET",
2112
+ headers: {
2113
+ authorization: `Bearer ${token}`,
2114
+ accept: "application/zip"
2115
+ },
2116
+ redirect: "manual"
2117
+ });
2118
+ if (response.status >= 400 || !response.arrayBuffer) return null;
2119
+ const bytes = await response.arrayBuffer();
2120
+ return parseIddfZipBuffer(Buffer.from(bytes), haId);
2121
+ }
2122
+ function compactDescriptionToStored(compact) {
2123
+ return JSON.parse(JSON.stringify(compact));
2124
+ }
2125
+ function entityCountForCompact(compact) {
2126
+ if (compact === null) return 0;
2127
+ return entityIndexFromDescription(JSON.parse(JSON.stringify(compact))).size;
2128
+ }
2129
+ /** Fill missing IDDF entity maps on stored appliances (TLS first). Returns whether any row changed. */
2130
+ async function enrichHomeConnectConnection(connection, fetchImpl, hooks = {}) {
2131
+ const appliances = connection.appliances ?? [];
2132
+ if (appliances.length === 0) return {
2133
+ connection,
2134
+ changed: false
2135
+ };
2136
+ const token = connection.token;
2137
+ if (isSecretMarker(token) || isSecretMarkerLike(token)) return {
2138
+ connection,
2139
+ changed: false
2140
+ };
2141
+ let changed = false;
2142
+ const nextAppliances = [];
2143
+ for (const row of appliances) {
2144
+ if (!(row.mode === "TLS" && entityCountInStoredDescription(row.deviceDescription) === 0)) {
2145
+ nextAppliances.push(row);
2146
+ continue;
2147
+ }
2148
+ let descriptionToStore = await fetchHomeConnectIddfCompact(row.haId, token, fetchImpl);
2149
+ let entityCount = entityCountForCompact(descriptionToStore);
2150
+ if (entityCount === 0) {
2151
+ descriptionToStore = buildCommonEntityFallbackDescription({
2152
+ haId: row.haId,
2153
+ type: row.type,
2154
+ name: row.name
2155
+ });
2156
+ entityCount = entityCountForCompact(descriptionToStore);
2157
+ hooks.onInfo?.("using common BSH entity fallback", { haId: row.haId });
2158
+ }
2159
+ if (entityCount === 0 || descriptionToStore === null) {
2160
+ nextAppliances.push(row);
2161
+ continue;
2162
+ }
2163
+ changed = true;
2164
+ nextAppliances.push({
2165
+ ...row,
2166
+ deviceDescription: compactDescriptionToStored(descriptionToStore)
2167
+ });
2168
+ }
2169
+ if (!changed) return {
2170
+ connection,
2171
+ changed: false
2172
+ };
2173
+ return {
2174
+ connection: {
2175
+ ...connection,
2176
+ appliances: nextAppliances
2177
+ },
2178
+ changed: true
2179
+ };
2180
+ }
2181
+ //#endregion
2182
+ //#region src/home-connect/home-connect-stored-appliance.ts
2183
+ function stubDeviceDescriptionObject(row) {
2184
+ return { info: {
2185
+ deviceID: row.haId,
2186
+ deviceType: row.type,
2187
+ model: row.name
2188
+ } };
2189
+ }
2190
+ function stubDeviceDescription(row) {
2191
+ return JSON.stringify(stubDeviceDescriptionObject(row));
2192
+ }
2193
+ function serializedDeviceDescription(row) {
2194
+ if (row.deviceDescription === void 0) return stubDeviceDescription(row);
2195
+ if (typeof row.deviceDescription === "string") return row.deviceDescription;
2196
+ return JSON.stringify(row.deviceDescription);
2197
+ }
2198
+ function isDescriptionRecord(value) {
2199
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2200
+ }
2201
+ function parseLoginDeviceDescription(raw) {
2202
+ const trimmed = raw.trim();
2203
+ if (trimmed.length === 0) return {};
2204
+ try {
2205
+ const parsed = JSON.parse(trimmed);
2206
+ if (isDescriptionRecord(parsed)) return { deviceDescription: parsed };
2207
+ } catch {
2208
+ return {};
2209
+ }
2210
+ return {};
2211
+ }
2212
+ function listedApplianceFromStored(row) {
925
2213
  return {
926
2214
  haId: row.haId,
927
2215
  name: row.name,
@@ -931,21 +2219,48 @@ function storedAppliance(row) {
931
2219
  psk: row.psk,
932
2220
  mode: row.mode,
933
2221
  iv64: row.iv64 ?? "",
934
- deviceDescription: JSON.stringify({ info: {
935
- deviceID: row.haId,
936
- deviceType: row.type,
937
- model: row.name
938
- } })
2222
+ deviceDescription: serializedDeviceDescription(row)
939
2223
  }
940
2224
  };
941
2225
  }
942
- async function openStoredOrAccount(connection) {
943
- const stored = connection.appliances ?? [];
944
- if (stored.length > 0) return accountClient(stored.map(storedAppliance), defaultHomeConnectWsClientFactory);
945
- return openHomeConnectAccount({
946
- token: connection.token,
947
- refresh: connection.refresh ?? ""
948
- });
2226
+ //#endregion
2227
+ //#region src/home-connect/home-connect-integration-manager.ts
2228
+ async function openStoredOrAccount(connection, deps) {
2229
+ let working = connection;
2230
+ const stored = working.appliances ?? [];
2231
+ if (stored.length > 0) {
2232
+ const token = working.token;
2233
+ if (isSecretMarker(token) || isSecretMarkerLike(token)) deps.logger?.warn("Home Connect skipping IDDF enrich — access token not resolved yet", { meta: { tokenIsSecretMarker: isSecretMarker(token) } });
2234
+ const enriched = isSecretMarker(token) || isSecretMarkerLike(token) ? {
2235
+ connection: working,
2236
+ changed: false
2237
+ } : await enrichHomeConnectConnection(working, deps.fetchImpl ?? homeConnectLoginFetch, { onInfo: (message, meta) => {
2238
+ deps.logger?.info(message, { meta });
2239
+ } });
2240
+ working = enriched.connection;
2241
+ if (enriched.changed) deps.logger?.info("Home Connect enriched appliance IDDF from cloud", { meta: { applianceCount: working.appliances?.length ?? 0 } });
2242
+ }
2243
+ if (stored.length > 0) {
2244
+ for (const row of working.appliances ?? []) if (row.mode === "TLS" && entityCountInStoredDescription(row.deviceDescription) === 0) deps.logger?.warn("Home Connect appliance has no IDDF entity map — re-sign-in or cloud IDDF fetch failed; features will not decode", { meta: {
2245
+ haId: row.haId,
2246
+ host: row.host
2247
+ } });
2248
+ const warnProtocol = (code) => {
2249
+ deps.logger?.warn("Home Connect local protocol warning", { meta: { code } });
2250
+ };
2251
+ return {
2252
+ connection: working,
2253
+ client: accountClient(working.appliances.map(listedApplianceFromStored), (gateway) => createHomeConnectWsClient(gateway, { onProtocolWarn: warnProtocol }))
2254
+ };
2255
+ }
2256
+ return {
2257
+ connection: working,
2258
+ client: await openHomeConnectAccount({
2259
+ token: working.token,
2260
+ refresh: working.refresh ?? "",
2261
+ fetchImpl: deps.fetchImpl
2262
+ })
2263
+ };
949
2264
  }
950
2265
  var HomeConnectIntegrationManager = class {
951
2266
  #id;
@@ -956,6 +2271,8 @@ var HomeConnectIntegrationManager = class {
956
2271
  #onDisconnected;
957
2272
  #surfaceSink;
958
2273
  #makeClient;
2274
+ #fetchImpl;
2275
+ #onConnectionUpdated;
959
2276
  #client = null;
960
2277
  #status = "disconnected";
961
2278
  #lastCheckedAt = null;
@@ -969,6 +2286,8 @@ var HomeConnectIntegrationManager = class {
969
2286
  this.#onDisconnected = options.onDisconnected;
970
2287
  this.#surfaceSink = options.surfaceSink;
971
2288
  this.#makeClient = options.makeClient ?? null;
2289
+ this.#fetchImpl = options.fetchImpl;
2290
+ this.#onConnectionUpdated = options.onConnectionUpdated;
972
2291
  }
973
2292
  getConnection() {
974
2293
  return this.#connection;
@@ -989,7 +2308,18 @@ var HomeConnectIntegrationManager = class {
989
2308
  if (this.#client !== null) return;
990
2309
  this.#status = "connecting";
991
2310
  this.#error = null;
992
- const client = this.#makeClient ? this.#makeClient(this.#connection) : await openStoredOrAccount(this.#connection);
2311
+ const before = this.#connection;
2312
+ let client;
2313
+ if (this.#makeClient) client = this.#makeClient(this.#connection);
2314
+ else {
2315
+ const opened = await openStoredOrAccount(this.#connection, {
2316
+ fetchImpl: this.#fetchImpl,
2317
+ logger: this.#logger
2318
+ });
2319
+ client = opened.client;
2320
+ this.#connection = opened.connection;
2321
+ if (!sameHomeConnectConnection(before, this.#connection)) this.#onConnectionUpdated?.(this.#id, this.#connection);
2322
+ }
993
2323
  this.#client = client;
994
2324
  const seededAppliances = client.getAppliances();
995
2325
  if (seededAppliances.length > 0) this.#surfaceSink.set(this.#id, {
@@ -1056,12 +2386,15 @@ var HomeConnectClientRegistry = class {
1056
2386
  #onConnected;
1057
2387
  #onDisconnected;
1058
2388
  #makeManager;
2389
+ #onConnectionUpdated;
1059
2390
  #entries = /* @__PURE__ */ new Map();
1060
2391
  constructor(logger, deps = {}) {
1061
2392
  this.#logger = logger;
1062
2393
  this.#onConnected = deps.onConnected ?? (() => void 0);
1063
2394
  this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
2395
+ this.#onConnectionUpdated = deps.onConnectionUpdated;
1064
2396
  const makeClient = deps.makeClient;
2397
+ const fetchImpl = deps.fetchImpl;
1065
2398
  this.#makeManager = deps.makeManager ?? ((opts) => new HomeConnectIntegrationManager({
1066
2399
  id: opts.integrationId,
1067
2400
  name: opts.name,
@@ -1070,7 +2403,9 @@ var HomeConnectClientRegistry = class {
1070
2403
  onConnected: opts.onConnected,
1071
2404
  onDisconnected: opts.onDisconnected,
1072
2405
  surfaceSink: homeConnectConnections,
1073
- makeClient
2406
+ makeClient,
2407
+ fetchImpl,
2408
+ onConnectionUpdated: this.#onConnectionUpdated
1074
2409
  }));
1075
2410
  }
1076
2411
  setOnConnected(cb) {
@@ -1087,7 +2422,7 @@ var HomeConnectClientRegistry = class {
1087
2422
  await existing.manager.applyConnection(connection);
1088
2423
  this.#entries.set(integrationId, {
1089
2424
  manager: existing.manager,
1090
- connection
2425
+ connection: existing.manager.getConnection()
1091
2426
  });
1092
2427
  } catch (err) {
1093
2428
  this.#logger.warn("HomeConnectClientRegistry: applyConnection failed", { meta: {
@@ -1111,6 +2446,10 @@ var HomeConnectClientRegistry = class {
1111
2446
  });
1112
2447
  try {
1113
2448
  await manager.start();
2449
+ this.#entries.set(integrationId, {
2450
+ manager,
2451
+ connection: manager.getConnection()
2452
+ });
1114
2453
  } catch (err) {
1115
2454
  this.#logger.warn("HomeConnectClientRegistry: manager start failed", { meta: {
1116
2455
  integrationId,
@@ -1212,83 +2551,6 @@ function buildHomeConnectConnectionTestProvider(deps) {
1212
2551
  }
1213
2552
  };
1214
2553
  }
1215
- var HC_FEATURE_ACTIVE_PROGRAM = "BSH.Common.Setting.Program.Active";
1216
- var HC_FEATURE_REMAINING = "BSH.Common.Option.RemainingProgramTime";
1217
- var HC_FEATURE_PROGRESS = "BSH.Common.Option.ProgramProgress";
1218
- var HC_FEATURE_DOOR = "BSH.Common.Status.DoorState";
1219
- var HC_FEATURE_REMOTE_START = "BSH.Common.Status.RemoteControlActive";
1220
- var HC_FEATURE_POWER = "BSH.Common.Setting.PowerState";
1221
- var HC_FEATURE_SALT = "BSH.Common.Setting.SaltLevel";
1222
- var HC_FEATURE_RINSE = "BSH.Common.Setting.RinseAidLevel";
1223
- var HC_FEATURE_HALF_LOAD = "BSH.Common.Option.HalfLoad";
1224
- var HC_FEATURE_HYGIENE = "BSH.Common.Option.HygienePlus";
1225
- var OPERATION_TO_STATE = {
1226
- Ready: "idle",
1227
- Inactive: "idle",
1228
- Run: "running",
1229
- Pause: "paused",
1230
- Finished: "finished",
1231
- Error: "error"
1232
- };
1233
- function mapConsumable(raw) {
1234
- if (raw === "Full" || raw === "OK" || raw === "Ok") return "ok";
1235
- if (raw === "Low" || raw === "NearlyEmpty") return "low";
1236
- if (raw === "Empty") return "empty";
1237
- return null;
1238
- }
1239
- function readBool(raw) {
1240
- return raw === true || raw === "On" || raw === "Open" || raw === "Connected";
1241
- }
1242
- function readString(raw) {
1243
- return typeof raw === "string" && raw.length > 0 ? raw : null;
1244
- }
1245
- function readNumber(raw) {
1246
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
1247
- if (typeof raw === "string") {
1248
- const n = Number(raw);
1249
- return Number.isFinite(n) ? n : null;
1250
- }
1251
- return null;
1252
- }
1253
- /**
1254
- * Map BSH.Common.* feature values → {@link DishwasherStatus}. Pure — the only
1255
- * place dishwasher slice assembly happens for Home Connect local.
1256
- */
1257
- function dishwasherStatusFromFeatures(input) {
1258
- const { features, availablePrograms, nowMs } = input;
1259
- const op = readString(features.get("BSH.Common.Status.OperationState")) ?? "Ready";
1260
- const state = OPERATION_TO_STATE[op] ?? "unavailable";
1261
- const program = readString(features.get(HC_FEATURE_ACTIVE_PROGRAM));
1262
- let remainingMinutes = readNumber(features.get(HC_FEATURE_REMAINING));
1263
- if (remainingMinutes !== null && remainingMinutes > 600) remainingMinutes = Math.round(remainingMinutes / 60);
1264
- const progressPercent = readNumber(features.get(HC_FEATURE_PROGRESS));
1265
- const doorOpen = features.get(HC_FEATURE_DOOR) === "Open";
1266
- const remoteStartAllowed = readBool(features.get(HC_FEATURE_REMOTE_START));
1267
- const powerOn = features.get(HC_FEATURE_POWER) !== "Off";
1268
- return {
1269
- state,
1270
- program,
1271
- availablePrograms: [...availablePrograms],
1272
- remainingMinutes,
1273
- progressPercent,
1274
- doorOpen,
1275
- remoteStartAllowed,
1276
- powerOn,
1277
- saltLevel: mapConsumable(features.get(HC_FEATURE_SALT)),
1278
- rinseAidLevel: mapConsumable(features.get(HC_FEATURE_RINSE)),
1279
- halfLoad: readBool(features.get(HC_FEATURE_HALF_LOAD)),
1280
- hygienePlus: readBool(features.get(HC_FEATURE_HYGIENE)),
1281
- errorCode: state === "error" ? 1 : null,
1282
- errorLabel: state === "error" ? op : null,
1283
- lastFetchedAt: nowMs
1284
- };
1285
- }
1286
- /** Command resource keys for WS POST-style mutations. */
1287
- var HC_CMD_START = "BSH.Common.Command.StartProgram";
1288
- var HC_CMD_PAUSE = "BSH.Common.Command.PauseProgram";
1289
- var HC_CMD_STOP = "BSH.Common.Command.AbortProgram";
1290
- var HC_CMD_SET_PROGRAM = "BSH.Common.Setting.Program.Active";
1291
- var HC_CMD_SET_POWER = "BSH.Common.Setting.PowerState";
1292
2554
  //#endregion
1293
2555
  //#region src/home-connect/devices/home-connect-dishwasher-device.ts
1294
2556
  var homeConnectDishwasherSchema = object({
@@ -1313,6 +2575,10 @@ var COLD_START = {
1313
2575
  rinseAidLevel: null,
1314
2576
  halfLoad: false,
1315
2577
  hygienePlus: false,
2578
+ extraDry: false,
2579
+ intensiveZone: false,
2580
+ varioSpeedPlus: false,
2581
+ childLock: false,
1316
2582
  errorCode: null,
1317
2583
  errorLabel: null,
1318
2584
  lastFetchedAt: 0
@@ -1323,9 +2589,10 @@ var HomeConnectDishwasherDevice = class extends BaseDevice {
1323
2589
  #brokerId;
1324
2590
  #availablePrograms;
1325
2591
  #featureUnsub = null;
2592
+ #intensiveZoneWriteKey = HC_FEATURE_INTENSIVE_ZONE;
1326
2593
  constructor(ctx) {
1327
2594
  const persisted = homeConnectDishwasherSchema.parse(ctx.persistedConfig ?? {});
1328
- super(ctx, homeConnectDishwasherSchema, { type: ctx.deviceMeta.type });
2595
+ super(ctx, homeConnectDishwasherSchema, { type: DeviceType.Dishwasher });
1329
2596
  this.#haId = persisted.haId;
1330
2597
  this.#brokerId = persisted.brokerId;
1331
2598
  this.#availablePrograms = persisted.availablePrograms;
@@ -1350,8 +2617,9 @@ var HomeConnectDishwasherDevice = class extends BaseDevice {
1350
2617
  attachFeatureListener() {
1351
2618
  const client = homeConnectConnections.getClient(this.#brokerId);
1352
2619
  if (!client) return;
1353
- this.#featureUnsub = client.onFeatureChange((haId, _key, _value) => {
2620
+ this.#featureUnsub = client.onFeatureChange((haId, key, _value) => {
1354
2621
  if (haId !== this.#haId) return;
2622
+ if (key === "BSH.Common.Option.IntensivZone" || key === "BSH.Common.Option.IntensiveZone") this.#intensiveZoneWriteKey = key;
1355
2623
  this.recomputeSlice();
1356
2624
  });
1357
2625
  }
@@ -1364,8 +2632,10 @@ var HomeConnectDishwasherDevice = class extends BaseDevice {
1364
2632
  });
1365
2633
  return;
1366
2634
  }
2635
+ const features = client.getFeatures(this.#haId);
2636
+ this.#intensiveZoneWriteKey = intensiveZoneFeatureKeyFromFeatures(features);
1367
2637
  const slice = dishwasherStatusFromFeatures({
1368
- features: client.getFeatures(this.#haId),
2638
+ features,
1369
2639
  availablePrograms: this.#availablePrograms,
1370
2640
  nowMs: Date.now()
1371
2641
  });
@@ -1400,6 +2670,18 @@ var HomeConnectDishwasherDevice = class extends BaseDevice {
1400
2670
  setHygienePlus: async ({ on }) => {
1401
2671
  await this.requireClient().setFeature(this.#haId, HC_FEATURE_HYGIENE, on);
1402
2672
  },
2673
+ setExtraDry: async ({ on }) => {
2674
+ await this.requireClient().setFeature(this.#haId, HC_FEATURE_EXTRA_DRY, on);
2675
+ },
2676
+ setIntensiveZone: async ({ on }) => {
2677
+ await this.requireClient().setFeature(this.#haId, this.#intensiveZoneWriteKey, on);
2678
+ },
2679
+ setVarioSpeedPlus: async ({ on }) => {
2680
+ await this.requireClient().setFeature(this.#haId, HC_FEATURE_VARIO_SPEED_PLUS, on);
2681
+ },
2682
+ setChildLock: async ({ on }) => {
2683
+ await this.requireClient().setFeature(this.#haId, HC_FEATURE_CHILD_LOCK, on);
2684
+ },
1403
2685
  setOption: async ({ key, value }) => {
1404
2686
  await this.requireClient().setFeature(this.#haId, key, value);
1405
2687
  }
@@ -1407,18 +2689,256 @@ var HomeConnectDishwasherDevice = class extends BaseDevice {
1407
2689
  }
1408
2690
  };
1409
2691
  //#endregion
2692
+ //#region src/home-connect/devices/home-connect-sensor-device.ts
2693
+ var homeConnectSensorSchema = object({
2694
+ haId: string().min(1),
2695
+ brokerId: string().min(1),
2696
+ sensorKind: string().min(1),
2697
+ system: literal("home-connect").optional(),
2698
+ integrationId: string().optional()
2699
+ });
2700
+ var NUMERIC_COLD = {
2701
+ value: 0,
2702
+ lastFetchedAt: 0
2703
+ };
2704
+ var POWER_COLD = { lastFetchedAt: 0 };
2705
+ var ENUM_COLD = {
2706
+ value: "unknown",
2707
+ lastFetchedAt: 0
2708
+ };
2709
+ var BINARY_COLD = {
2710
+ on: false,
2711
+ lastChangedAt: 0
2712
+ };
2713
+ /**
2714
+ * One Home Connect read-only metric as a Sensor accessory child of the appliance
2715
+ * container (energy/water forecasts, remote-control level, delayed start, …).
2716
+ */
2717
+ var HomeConnectSensorDevice = class extends BaseDevice {
2718
+ features = [];
2719
+ #haId;
2720
+ #brokerId;
2721
+ #sensorKind;
2722
+ #capName;
2723
+ #featureUnsub = null;
2724
+ constructor(ctx) {
2725
+ const persisted = homeConnectSensorSchema.parse(ctx.persistedConfig ?? {});
2726
+ super(ctx, homeConnectSensorSchema, { type: ctx.deviceMeta.type });
2727
+ this.#haId = persisted.haId;
2728
+ this.#brokerId = persisted.brokerId;
2729
+ this.#sensorKind = persisted.sensorKind;
2730
+ const catalog = homeConnectSensorCatalogEntry(this.#sensorKind);
2731
+ this.#capName = catalog.cap;
2732
+ this.online = true;
2733
+ this.updateSourceInfo({
2734
+ id: `${this.#haId}:${catalog.stableIdSuffix}`,
2735
+ system: "home-connect"
2736
+ });
2737
+ }
2738
+ async onActivate() {
2739
+ await super.onActivate();
2740
+ this.registerCap();
2741
+ this.seedColdStart();
2742
+ this.attachFeatureListener();
2743
+ this.recomputeSlice();
2744
+ }
2745
+ async removeDevice() {
2746
+ this.#featureUnsub?.();
2747
+ this.#featureUnsub = null;
2748
+ await super.removeDevice();
2749
+ }
2750
+ seedColdStart() {
2751
+ switch (this.#capName) {
2752
+ case "numeric-sensor":
2753
+ this.runtimeState.setCapState("numeric-sensor", NUMERIC_COLD);
2754
+ break;
2755
+ case "power-meter":
2756
+ this.runtimeState.setCapState("power-meter", POWER_COLD);
2757
+ break;
2758
+ case "enum-sensor":
2759
+ this.runtimeState.setCapState("enum-sensor", ENUM_COLD);
2760
+ break;
2761
+ case "binary":
2762
+ this.runtimeState.setCapState("binary", BINARY_COLD);
2763
+ break;
2764
+ default: break;
2765
+ }
2766
+ }
2767
+ attachFeatureListener() {
2768
+ const client = homeConnectConnections.getClient(this.#brokerId);
2769
+ if (!client) return;
2770
+ this.#featureUnsub = client.onFeatureChange((haId) => {
2771
+ if (haId !== this.#haId) return;
2772
+ this.recomputeSlice();
2773
+ });
2774
+ }
2775
+ recomputeSlice() {
2776
+ const client = homeConnectConnections.getClient(this.#brokerId);
2777
+ const nowMs = Date.now();
2778
+ if (!client) {
2779
+ this.seedColdStart();
2780
+ return;
2781
+ }
2782
+ const parsed = homeConnectSensorSliceFromFeatures({
2783
+ kind: this.#sensorKind,
2784
+ features: client.getFeatures(this.#haId),
2785
+ nowMs
2786
+ });
2787
+ if (parsed.cap === "numeric-sensor") {
2788
+ if (parsed.status === null) return;
2789
+ this.runtimeState.setCapState("numeric-sensor", parsed.status);
2790
+ return;
2791
+ }
2792
+ this.runtimeState.setCapState(parsed.cap, parsed.status);
2793
+ }
2794
+ registerCap() {
2795
+ if (this.#capName === "numeric-sensor") {
2796
+ this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState("numeric-sensor") ?? NUMERIC_COLD });
2797
+ return;
2798
+ }
2799
+ if (this.#capName === "power-meter") {
2800
+ this.ctx.registerNativeCap(powerMeterCapability, { getStatus: async () => this.runtimeState.getCapState("power-meter") ?? POWER_COLD });
2801
+ return;
2802
+ }
2803
+ if (this.#capName === "enum-sensor") {
2804
+ this.ctx.registerNativeCap(enumSensorCapability, { getStatus: async () => this.runtimeState.getCapState("enum-sensor") ?? ENUM_COLD });
2805
+ return;
2806
+ }
2807
+ this.ctx.registerNativeCap(binaryCapability, { getStatus: async () => this.runtimeState.getCapState("binary") ?? BINARY_COLD });
2808
+ }
2809
+ };
2810
+ //#endregion
2811
+ //#region src/home-connect/devices/home-connect-appliance-device.ts
2812
+ var homeConnectApplianceChildSchema = object({
2813
+ role: string().min(1),
2814
+ stableIdSuffix: string().min(1),
2815
+ name: string()
2816
+ });
2817
+ var homeConnectApplianceSchema = object({
2818
+ haId: string().min(1),
2819
+ brokerId: string().min(1),
2820
+ applianceType: string().optional(),
2821
+ availablePrograms: array(string()).default([]),
2822
+ children: array(homeConnectApplianceChildSchema).min(1),
2823
+ system: literal("home-connect").optional(),
2824
+ integrationId: string().optional()
2825
+ });
2826
+ /** One Home Connect appliance as a container with dishwasher + metric sensor children. */
2827
+ var HomeConnectApplianceDevice = class extends BaseDevice {
2828
+ features = [];
2829
+ integrationId;
2830
+ brokerId;
2831
+ haId;
2832
+ applianceType;
2833
+ availablePrograms;
2834
+ constructor(ctx) {
2835
+ const cfg = homeConnectApplianceSchema.parse(ctx.persistedConfig ?? {});
2836
+ super(ctx, homeConnectApplianceSchema, { type: DeviceType.Container });
2837
+ this.brokerId = cfg.brokerId;
2838
+ this.haId = cfg.haId;
2839
+ this.integrationId = cfg.integrationId;
2840
+ this.applianceType = cfg.applianceType;
2841
+ this.availablePrograms = cfg.availablePrograms;
2842
+ this.online = true;
2843
+ this.updateSourceInfo({
2844
+ id: cfg.haId,
2845
+ system: "home-connect"
2846
+ });
2847
+ }
2848
+ getAccessoryChildren() {
2849
+ const cfg = this.config.values;
2850
+ const containerName = this.name;
2851
+ const catalogSpecs = buildHomeConnectApplianceChildSpecs(containerName, this.haId);
2852
+ const persistedBySuffix = new Map(cfg.children.map((c) => [c.stableIdSuffix, c]));
2853
+ const childRows = catalogSpecs.map((spec) => {
2854
+ const saved = persistedBySuffix.get(spec.stableIdSuffix);
2855
+ if (saved !== void 0) return {
2856
+ role: spec.role,
2857
+ stableIdSuffix: saved.stableIdSuffix,
2858
+ name: saved.name.length > 0 ? saved.name : spec.name
2859
+ };
2860
+ return {
2861
+ role: spec.role,
2862
+ stableIdSuffix: spec.stableIdSuffix,
2863
+ name: spec.name
2864
+ };
2865
+ });
2866
+ const out = [];
2867
+ for (const child of childRows) {
2868
+ if (child.role === "dishwasher") {
2869
+ const config = {
2870
+ haId: this.haId,
2871
+ brokerId: this.brokerId,
2872
+ applianceType: this.applianceType,
2873
+ availablePrograms: [...this.availablePrograms],
2874
+ system: "home-connect",
2875
+ ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
2876
+ };
2877
+ out.push({
2878
+ stableIdSuffix: child.stableIdSuffix,
2879
+ meta: {
2880
+ type: DeviceType.Dishwasher,
2881
+ name: child.name.length > 0 ? child.name : homeConnectDishwasherEntityName(containerName),
2882
+ linkDeviceId: this.id,
2883
+ ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
2884
+ },
2885
+ config,
2886
+ factory: (childCtx) => new HomeConnectDishwasherDevice(childCtx)
2887
+ });
2888
+ continue;
2889
+ }
2890
+ const sensorKind = child.role;
2891
+ const catalog = HOME_CONNECT_SENSOR_CATALOG.find((entry) => entry.kind === sensorKind);
2892
+ if (catalog === void 0) {
2893
+ this.ctx.logger.warn("Home Connect appliance: skipping unknown sensor child role", { meta: {
2894
+ haId: this.haId,
2895
+ role: child.role,
2896
+ stableIdSuffix: child.stableIdSuffix
2897
+ } });
2898
+ continue;
2899
+ }
2900
+ const config = {
2901
+ haId: this.haId,
2902
+ brokerId: this.brokerId,
2903
+ sensorKind,
2904
+ system: "home-connect",
2905
+ ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
2906
+ };
2907
+ out.push({
2908
+ stableIdSuffix: child.stableIdSuffix,
2909
+ meta: {
2910
+ type: DeviceType.Sensor,
2911
+ role: catalog.role,
2912
+ name: child.name,
2913
+ linkDeviceId: this.id,
2914
+ ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
2915
+ },
2916
+ config,
2917
+ factory: (childCtx) => new HomeConnectSensorDevice(childCtx)
2918
+ });
2919
+ }
2920
+ return out;
2921
+ }
2922
+ };
2923
+ //#endregion
1410
2924
  //#region src/home-connect/addon.ts
1411
2925
  var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
1412
2926
  addonId = "bosch-home-connect";
1413
2927
  providerName = "Bosch Home Connect";
1414
- deviceClasses = { [DeviceType.Dishwasher]: HomeConnectDishwasherDevice };
2928
+ deviceClasses = {
2929
+ [DeviceType.Container]: HomeConnectApplianceDevice,
2930
+ [DeviceType.Dishwasher]: HomeConnectDishwasherDevice,
2931
+ [DeviceType.Sensor]: HomeConnectSensorDevice
2932
+ };
1415
2933
  clients = null;
1416
2934
  constructor() {
1417
2935
  super({});
1418
2936
  }
1419
2937
  async onInitialize() {
1420
2938
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
1421
- this.clients = new HomeConnectClientRegistry(this.ctx.logger);
2939
+ this.clients = new HomeConnectClientRegistry(this.ctx.logger, { onConnectionUpdated: (integrationId, connection) => {
2940
+ this.persistIntegrationAppliances(integrationId, connection);
2941
+ } });
1422
2942
  this.clients.setOnConnected((integrationId) => {
1423
2943
  this.setIntegrationDevicesOnline(integrationId, true);
1424
2944
  });
@@ -1463,7 +2983,8 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
1463
2983
  host: app.connection.host,
1464
2984
  psk: app.connection.psk,
1465
2985
  mode: app.connection.mode,
1466
- ...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {}
2986
+ ...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {},
2987
+ ...parseLoginDeviceDescription(app.connection.deviceDescription)
1467
2988
  }));
1468
2989
  return {
1469
2990
  token: account.token,
@@ -1488,13 +3009,32 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
1488
3009
  if (!this.clients) throw new Error("Home Connect provider not initialised");
1489
3010
  return this.clients;
1490
3011
  }
1491
- generateStableId(_type, config) {
3012
+ generateStableId(type, config) {
1492
3013
  const integrationId = config?.["brokerId"];
1493
3014
  if (typeof integrationId !== "string" || integrationId.length === 0) throw new Error("home-connect generateStableId: brokerId required");
1494
3015
  const haId = config?.["haId"];
1495
3016
  if (typeof haId !== "string" || haId.length === 0) throw new Error("home-connect generateStableId: haId required");
3017
+ if (type === DeviceType.Container) return `home-connect:${integrationId}:${haId}:appliance`;
1496
3018
  return `home-connect:${integrationId}:${haId}`;
1497
3019
  }
3020
+ async persistIntegrationAppliances(integrationId, connection) {
3021
+ if (!connection.appliances || connection.appliances.length === 0) return;
3022
+ try {
3023
+ const merged = homeConnectSettingsForPersist({
3024
+ ...await this.ctx.api.integrations.getSettings.query({ id: integrationId }),
3025
+ appliances: connection.appliances
3026
+ });
3027
+ await this.ctx.api.integrations.setSettings.mutate({
3028
+ id: integrationId,
3029
+ settings: merged
3030
+ });
3031
+ } catch (err) {
3032
+ this.ctx.logger.warn("Home Connect failed to persist enriched appliance IDDF", { meta: {
3033
+ integrationId,
3034
+ error: errMsg(err)
3035
+ } });
3036
+ }
3037
+ }
1498
3038
  async reconcileIntegrations() {
1499
3039
  const reg = this.requireClients();
1500
3040
  try {
@@ -1572,20 +3112,24 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
1572
3112
  adoptDevice: async ({ haId, integrationId, type, name, applianceType, availablePrograms }) => {
1573
3113
  const devices = this.ctx.kernel.devices;
1574
3114
  if (!devices) throw new Error("home-connect adopt: devices unavailable");
3115
+ if (type !== DeviceType.Container) throw new Error(`home-connect adopt: expected Container, got ${type}`);
3116
+ const children = buildHomeConnectApplianceChildSpecs(name, haId).map((child) => ({
3117
+ role: child.role,
3118
+ stableIdSuffix: child.stableIdSuffix,
3119
+ name: child.name
3120
+ }));
1575
3121
  const config = {
1576
3122
  haId,
1577
3123
  brokerId: integrationId,
1578
3124
  applianceType,
1579
3125
  availablePrograms: [...availablePrograms],
3126
+ children,
1580
3127
  system: "home-connect",
1581
- integrationId,
1582
- name
3128
+ integrationId
1583
3129
  };
1584
3130
  const stableId = this.generateStableId(type, config);
1585
- const DeviceClass = this.deviceClasses[type];
1586
- if (!DeviceClass) throw new Error(`home-connect adopt: unsupported type ${type}`);
1587
- return { deviceId: (await devices.create(stableId, DeviceClass, config, null, {
1588
- type,
3131
+ return { deviceId: (await devices.create(stableId, HomeConnectApplianceDevice, config, null, {
3132
+ type: DeviceType.Container,
1589
3133
  name,
1590
3134
  integrationId
1591
3135
  })).id };