@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.
- package/dist/{account-integration-offline-Cbixus9v.js → account-integration-offline-B_AZ8wAX.js} +1 -1
- package/dist/{account-integration-offline-CH7JBu8l.mjs → account-integration-offline-l27nXZT3.mjs} +1 -1
- package/dist/{dist-CY6qHAEx.mjs → dist-BoMB3-eL.mjs} +88 -4
- package/dist/{dist-QXDYCZIL.js → dist-DvEvuaoA.js} +117 -3
- package/dist/home-connect/addon.js +1704 -160
- package/dist/home-connect/addon.mjs +1704 -160
- package/dist/homecom/addon.js +2 -2
- package/dist/homecom/addon.mjs +2 -2
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dist = require("../dist-
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
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
|
|
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)
|
|
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 =
|
|
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 =
|
|
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.
|
|
935
|
+
if (type.toLowerCase().includes("dishwasher")) return require_dist.DeviceType.Container;
|
|
293
936
|
return null;
|
|
294
937
|
}
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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:
|
|
311
|
-
adoptedDeviceId:
|
|
312
|
-
capabilities:
|
|
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
|
|
758
|
-
|
|
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
|
|
2004
|
+
const msgHaId = row["haId"];
|
|
806
2005
|
const key = row["key"];
|
|
807
|
-
if (typeof
|
|
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
|
|
861
|
-
return typeof
|
|
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
|
|
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-
|
|
925
|
-
function
|
|
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:
|
|
936
|
-
deviceID: row.haId,
|
|
937
|
-
deviceType: row.type,
|
|
938
|
-
model: row.name
|
|
939
|
-
} })
|
|
2223
|
+
deviceDescription: serializedDeviceDescription(row)
|
|
940
2224
|
}
|
|
941
2225
|
};
|
|
942
2226
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
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 = {
|
|
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(
|
|
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
|
-
|
|
1587
|
-
|
|
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 };
|