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