@camstack/addon-provider-bosch 0.1.28 → 0.1.30
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/CHANGELOG.md +16 -1
- package/README.md +20 -13
- package/dist/{account-integration-offline-2MI94wOE.js → account-integration-offline-BMoYdiX2.js} +126 -1
- package/dist/{account-integration-offline-DKaazSyb.mjs → account-integration-offline-pv6cqOKu.mjs} +115 -2
- package/dist/{dist-DEZDBpGW.js → dist-BioIJ3NA.js} +1818 -126
- package/dist/{dist-Cbcvh8mB.mjs → dist-DBUnIovD.mjs} +1807 -127
- package/dist/home-connect/addon.js +349 -259
- package/dist/home-connect/addon.mjs +349 -259
- package/dist/homecom/addon.js +94 -12
- package/dist/homecom/addon.mjs +94 -12
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +15 -2
- package/i18n/it.json +15 -2
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as
|
|
1
|
+
import { A as defineCustomActions, B as string, D as errMsg, E as DeviceType, F as boolean, H as unknown, I as literal, M as isSecretMarkerLike, N as _enum, O as createEvent, P as array, R as object, T as DeviceRole, U as EventCategory, V as union, _ as numericSensorCapability, a as VendorLoginProxy, b as runningFromDishwasherOperation, d as deviceAdoptionCapability, f as dishwasherCapability, g as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarker, k as customAction, l as connectionTestCapability, m as eventEmitterCapability, n as BaseDeviceProvider, p as enumSensorCapability, r as DishwasherOperationStateSchema, s as binaryCapability, t as BaseDevice, u as consumablesCapability, v as powerMeterCapability, x as runningStateCapability, y as quantityFromUnit, z as record } from "../dist-DBUnIovD.mjs";
|
|
2
|
+
import { a as createSignInAttemptStore, i as openSignInProxy, n as cascadeRemoveDevicesForIntegration, o as loginFailureForAttempt, r as createSignInClaims, s as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-pv6cqOKu.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { connect } from "node:tls";
|
|
5
5
|
import { inflateRawSync } from "node:zlib";
|
|
@@ -30,6 +30,23 @@ var homeConnectActions = defineCustomActions({
|
|
|
30
30
|
kind: "mutation",
|
|
31
31
|
auth: "admin"
|
|
32
32
|
}),
|
|
33
|
+
/**
|
|
34
|
+
* The form asks while the proxied login page is open: `pending` until the hub captured the
|
|
35
|
+
* app-scheme redirect and finished the exchange, then the same fields `finishSignIn` returns.
|
|
36
|
+
* Claimed once. A failed exchange answers with its operator-facing message as an error.
|
|
37
|
+
*/
|
|
38
|
+
claimSignIn: customAction(object({ attemptId: string().min(1) }), object({
|
|
39
|
+
pending: boolean(),
|
|
40
|
+
token: string().optional().meta({ secret: true }),
|
|
41
|
+
refresh: string().optional().meta({ secret: true }),
|
|
42
|
+
accountLabel: string().optional(),
|
|
43
|
+
appliances: string().optional(),
|
|
44
|
+
applianceSecrets: string().optional().meta({ secret: true }),
|
|
45
|
+
summary: string().optional()
|
|
46
|
+
}), {
|
|
47
|
+
kind: "mutation",
|
|
48
|
+
auth: "admin"
|
|
49
|
+
}),
|
|
33
50
|
signOut: customAction(object({}), object({
|
|
34
51
|
signedOut: literal(true),
|
|
35
52
|
summary: string()
|
|
@@ -986,192 +1003,6 @@ async function finishHomeConnectSignIn(input, store, fetchImpl = homeConnectLogi
|
|
|
986
1003
|
}, fetchImpl);
|
|
987
1004
|
}
|
|
988
1005
|
var homeConnectSignInAttempts = createSignInAttemptStore();
|
|
989
|
-
//#endregion
|
|
990
|
-
//#region src/home-connect/home-connect-discovery.ts
|
|
991
|
-
/** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
|
|
992
|
-
function homeConnectDishwasherEntityName(containerName) {
|
|
993
|
-
const base = containerName.trim();
|
|
994
|
-
return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
|
|
995
|
-
}
|
|
996
|
-
function deviceTypeForApplianceType(type) {
|
|
997
|
-
if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* The container candidate for one appliance. Its sensor children are not listed here:
|
|
1002
|
-
* they are the entities the appliance's own description declares, known once it is
|
|
1003
|
-
* connected, and they are attached to the container as that description is read.
|
|
1004
|
-
*/
|
|
1005
|
-
function dishwasherContainerCandidate(app, adoptedId) {
|
|
1006
|
-
const name = app.name.length > 0 ? app.name : app.haId;
|
|
1007
|
-
const children = [{
|
|
1008
|
-
childNativeId: `${app.haId}#dishwasher`,
|
|
1009
|
-
name: homeConnectDishwasherEntityName(name),
|
|
1010
|
-
type: DeviceType.Dishwasher,
|
|
1011
|
-
status: "online",
|
|
1012
|
-
metadata: {
|
|
1013
|
-
serialNumber: app.haId,
|
|
1014
|
-
model: app.type
|
|
1015
|
-
},
|
|
1016
|
-
alreadyAdopted: false,
|
|
1017
|
-
adoptedDeviceId: null,
|
|
1018
|
-
capabilities: ["dishwasher"]
|
|
1019
|
-
}];
|
|
1020
|
-
return {
|
|
1021
|
-
childNativeId: app.haId,
|
|
1022
|
-
name,
|
|
1023
|
-
type: DeviceType.Container,
|
|
1024
|
-
status: "online",
|
|
1025
|
-
metadata: {
|
|
1026
|
-
serialNumber: app.haId,
|
|
1027
|
-
model: app.type
|
|
1028
|
-
},
|
|
1029
|
-
alreadyAdopted: adoptedId !== null,
|
|
1030
|
-
adoptedDeviceId: adoptedId,
|
|
1031
|
-
capabilities: [],
|
|
1032
|
-
children
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
function buildHomeConnectCandidates(input) {
|
|
1036
|
-
const out = [];
|
|
1037
|
-
for (const app of input.appliances) {
|
|
1038
|
-
if (deviceTypeForApplianceType(app.type) === null) continue;
|
|
1039
|
-
const adoptedId = input.adopted.get(app.haId) ?? null;
|
|
1040
|
-
out.push(dishwasherContainerCandidate(app, adoptedId));
|
|
1041
|
-
}
|
|
1042
|
-
return out;
|
|
1043
|
-
}
|
|
1044
|
-
//#endregion
|
|
1045
|
-
//#region src/home-connect/home-connect-adoption-provider.ts
|
|
1046
|
-
var DEVICES_FILTER = {
|
|
1047
|
-
id: "devices",
|
|
1048
|
-
label: "Devices",
|
|
1049
|
-
isDefault: true
|
|
1050
|
-
};
|
|
1051
|
-
async function adoptedMapForIntegration(integrationId, listAdopted) {
|
|
1052
|
-
const all = await listAdopted();
|
|
1053
|
-
const map = /* @__PURE__ */ new Map();
|
|
1054
|
-
for (const device of all) if (device.config["system"] === "home-connect" && device.config["brokerId"] === integrationId) {
|
|
1055
|
-
const haId = device.config["haId"];
|
|
1056
|
-
if (typeof haId === "string") map.set(haId, device.id);
|
|
1057
|
-
}
|
|
1058
|
-
return map;
|
|
1059
|
-
}
|
|
1060
|
-
function buildHomeConnectAdoptionProvider(deps) {
|
|
1061
|
-
const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
1062
|
-
async function allCandidatesForIntegration(integrationId) {
|
|
1063
|
-
return buildHomeConnectCandidates({
|
|
1064
|
-
appliances: registry.appliances(integrationId),
|
|
1065
|
-
adopted: await adoptedMapForIntegration(integrationId, listAdopted)
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
return {
|
|
1069
|
-
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
1070
|
-
listCandidates: async ({ integrationId, page, pageSize }) => {
|
|
1071
|
-
const cands = await allCandidatesForIntegration(integrationId);
|
|
1072
|
-
const start = (page - 1) * pageSize;
|
|
1073
|
-
return {
|
|
1074
|
-
candidates: cands.slice(start, start + pageSize),
|
|
1075
|
-
totalCount: cands.length,
|
|
1076
|
-
page,
|
|
1077
|
-
pageSize
|
|
1078
|
-
};
|
|
1079
|
-
},
|
|
1080
|
-
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
1081
|
-
return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
1082
|
-
},
|
|
1083
|
-
getStatus: async () => {
|
|
1084
|
-
try {
|
|
1085
|
-
let candidateCount = 0;
|
|
1086
|
-
for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
|
|
1087
|
-
const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "home-connect").length;
|
|
1088
|
-
return {
|
|
1089
|
-
lastDiscoveryAt: Date.now(),
|
|
1090
|
-
candidateCount,
|
|
1091
|
-
adoptedCount,
|
|
1092
|
-
lastError: null
|
|
1093
|
-
};
|
|
1094
|
-
} catch (err) {
|
|
1095
|
-
return {
|
|
1096
|
-
lastDiscoveryAt: null,
|
|
1097
|
-
candidateCount: 0,
|
|
1098
|
-
adoptedCount: 0,
|
|
1099
|
-
lastError: errMsg(err)
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
},
|
|
1103
|
-
refresh: async ({ integrationId }) => {
|
|
1104
|
-
const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
|
|
1105
|
-
const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
|
|
1106
|
-
return {
|
|
1107
|
-
lastDiscoveryAt: Date.now(),
|
|
1108
|
-
candidateCount,
|
|
1109
|
-
adoptedCount,
|
|
1110
|
-
lastError: null
|
|
1111
|
-
};
|
|
1112
|
-
},
|
|
1113
|
-
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
1114
|
-
if (!registry.has(integrationId)) throw new Error(`home-connect adopt: integration ${integrationId} not connected`);
|
|
1115
|
-
const appliances = registry.appliances(integrationId);
|
|
1116
|
-
const adopted = [];
|
|
1117
|
-
let failures = 0;
|
|
1118
|
-
for (const haId of childNativeIds) try {
|
|
1119
|
-
const app = appliances.find((a) => a.haId === haId);
|
|
1120
|
-
if (!app) {
|
|
1121
|
-
failures++;
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1124
|
-
const candidate = buildHomeConnectCandidates({
|
|
1125
|
-
appliances: [app],
|
|
1126
|
-
adopted: /* @__PURE__ */ new Map()
|
|
1127
|
-
})[0];
|
|
1128
|
-
if (!candidate) {
|
|
1129
|
-
failures++;
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
const name = perCandidate?.[haId]?.name ?? app.name;
|
|
1133
|
-
const { deviceId } = await adoptDevice({
|
|
1134
|
-
haId,
|
|
1135
|
-
integrationId,
|
|
1136
|
-
type: candidate.type,
|
|
1137
|
-
name,
|
|
1138
|
-
applianceType: app.type
|
|
1139
|
-
});
|
|
1140
|
-
adopted.push({
|
|
1141
|
-
childNativeId: haId,
|
|
1142
|
-
parentDeviceId: deviceId
|
|
1143
|
-
});
|
|
1144
|
-
} catch (err) {
|
|
1145
|
-
logger.warn("home-connect adopt failed", { meta: {
|
|
1146
|
-
haId,
|
|
1147
|
-
integrationId,
|
|
1148
|
-
error: errMsg(err)
|
|
1149
|
-
} });
|
|
1150
|
-
failures++;
|
|
1151
|
-
}
|
|
1152
|
-
if (adopted.length === 0 && failures > 0) throw new Error(`home-connect adopt: all ${failures} adopt(s) failed`);
|
|
1153
|
-
return { adopted: adopted.map((a) => ({
|
|
1154
|
-
...a,
|
|
1155
|
-
accessoryDeviceIds: []
|
|
1156
|
-
})) };
|
|
1157
|
-
},
|
|
1158
|
-
release: async ({ camDeviceId }) => {
|
|
1159
|
-
await removeDevice(camDeviceId);
|
|
1160
|
-
},
|
|
1161
|
-
resync: async ({ camDeviceId }) => {
|
|
1162
|
-
const cfg = await findDeviceConfig(camDeviceId);
|
|
1163
|
-
if (!cfg || cfg["system"] !== "home-connect") throw new Error(`home-connect resync: device ${camDeviceId} not found`);
|
|
1164
|
-
const integrationId = String(cfg["brokerId"]);
|
|
1165
|
-
const haId = String(cfg["haId"]);
|
|
1166
|
-
if (!hasIntegration(integrationId)) throw new Error(`home-connect resync: integration ${integrationId} not connected`);
|
|
1167
|
-
if (!registry.appliances(integrationId).some((a) => a.haId === haId)) throw new Error(`home-connect resync: appliance ${haId} no longer present`);
|
|
1168
|
-
return {
|
|
1169
|
-
changed: false,
|
|
1170
|
-
rebuiltChildren: 0
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
1006
|
var HomeConnectModeSchema = _enum(["AES", "TLS"]);
|
|
1176
1007
|
function isPlainObject$1(value) {
|
|
1177
1008
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1439,7 +1270,7 @@ function buildHomeConnectFormSchema() {
|
|
|
1439
1270
|
id: "gateway",
|
|
1440
1271
|
title: "Home Connect",
|
|
1441
1272
|
titleKey: "provider-bosch:home-connect.s_gateway.title",
|
|
1442
|
-
description: "Sign in with your Home Connect account.
|
|
1273
|
+
description: "Sign in with your Home Connect account. A new tab opens the sign-in page through this hub and finishes on its own.",
|
|
1443
1274
|
descriptionKey: "provider-bosch:home-connect.s_gateway.description",
|
|
1444
1275
|
columns: 1,
|
|
1445
1276
|
fields: [
|
|
@@ -1452,6 +1283,7 @@ function buildHomeConnectFormSchema() {
|
|
|
1452
1283
|
providerLabel: "Home Connect",
|
|
1453
1284
|
beginAction: "beginSignIn",
|
|
1454
1285
|
completeAction: "finishSignIn",
|
|
1286
|
+
claimAction: "claimSignIn",
|
|
1455
1287
|
signOutAction: "signOut",
|
|
1456
1288
|
accountKey: "accountLabel",
|
|
1457
1289
|
countFromKey: "appliances",
|
|
@@ -1536,6 +1368,295 @@ function mergeAppliancesForPersist(storedAppliances, fromConnection) {
|
|
|
1536
1368
|
return out;
|
|
1537
1369
|
}
|
|
1538
1370
|
//#endregion
|
|
1371
|
+
//#region src/home-connect/home-connect-stored-appliance.ts
|
|
1372
|
+
function stubDeviceDescriptionObject(row) {
|
|
1373
|
+
return { info: {
|
|
1374
|
+
deviceID: row.haId,
|
|
1375
|
+
deviceType: row.type,
|
|
1376
|
+
model: row.name
|
|
1377
|
+
} };
|
|
1378
|
+
}
|
|
1379
|
+
function stubDeviceDescription(row) {
|
|
1380
|
+
return JSON.stringify(stubDeviceDescriptionObject(row));
|
|
1381
|
+
}
|
|
1382
|
+
function serializedDeviceDescription(row) {
|
|
1383
|
+
if (row.deviceDescription === void 0) return stubDeviceDescription(row);
|
|
1384
|
+
if (typeof row.deviceDescription === "string") return row.deviceDescription;
|
|
1385
|
+
return JSON.stringify(row.deviceDescription);
|
|
1386
|
+
}
|
|
1387
|
+
function isDescriptionRecord(value) {
|
|
1388
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1389
|
+
}
|
|
1390
|
+
function parseLoginDeviceDescription(raw) {
|
|
1391
|
+
const trimmed = raw.trim();
|
|
1392
|
+
if (trimmed.length === 0) return {};
|
|
1393
|
+
try {
|
|
1394
|
+
const parsed = JSON.parse(trimmed);
|
|
1395
|
+
if (isDescriptionRecord(parsed)) return { deviceDescription: parsed };
|
|
1396
|
+
} catch {
|
|
1397
|
+
return {};
|
|
1398
|
+
}
|
|
1399
|
+
return {};
|
|
1400
|
+
}
|
|
1401
|
+
function listedApplianceFromStored(row) {
|
|
1402
|
+
return {
|
|
1403
|
+
haId: row.haId,
|
|
1404
|
+
name: row.name,
|
|
1405
|
+
type: row.type,
|
|
1406
|
+
connection: {
|
|
1407
|
+
host: row.host,
|
|
1408
|
+
psk: row.psk,
|
|
1409
|
+
mode: row.mode,
|
|
1410
|
+
iv64: row.iv64 ?? "",
|
|
1411
|
+
deviceDescription: serializedDeviceDescription(row)
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/home-connect/home-connect-sign-in-result.ts
|
|
1417
|
+
function homeConnectFinishResult(account) {
|
|
1418
|
+
const { rows, secrets } = splitApplianceSecrets(account.appliances.map((app) => ({
|
|
1419
|
+
haId: app.haId,
|
|
1420
|
+
name: app.name,
|
|
1421
|
+
type: app.type,
|
|
1422
|
+
host: app.connection.host,
|
|
1423
|
+
psk: app.connection.psk,
|
|
1424
|
+
mode: app.connection.mode,
|
|
1425
|
+
...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {},
|
|
1426
|
+
...parseLoginDeviceDescription(app.connection.deviceDescription)
|
|
1427
|
+
})));
|
|
1428
|
+
return {
|
|
1429
|
+
token: account.token,
|
|
1430
|
+
...account.refresh.length > 0 ? { refresh: account.refresh } : {},
|
|
1431
|
+
accountLabel: account.accountId,
|
|
1432
|
+
appliances: JSON.stringify(rows),
|
|
1433
|
+
applianceSecrets: JSON.stringify(secrets),
|
|
1434
|
+
summary: account.summary
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
//#endregion
|
|
1438
|
+
//#region src/home-connect/home-connect-sign-in-proxy.ts
|
|
1439
|
+
/**
|
|
1440
|
+
* Home Connect signs in through its own identity pages, which end on `hcauth://auth/prod?...`
|
|
1441
|
+
* — an address no browser can open. These are the hosts those pages are served from; the
|
|
1442
|
+
* proxy reaches nothing else. (Not verified against the live pages: no vendor credentials.)
|
|
1443
|
+
*/
|
|
1444
|
+
var HOME_CONNECT_LOGIN_HOSTS = ["api.home-connect.com", "singlekey-id.com"];
|
|
1445
|
+
var HOME_CONNECT_PROXY_PREFIX = "login";
|
|
1446
|
+
var HOME_CONNECT_APP_SCHEME = HOME_CONNECT_REDIRECT_URI.slice(0, HOME_CONNECT_REDIRECT_URI.indexOf("://") + 3);
|
|
1447
|
+
var homeConnectSignInClaims = createSignInClaims();
|
|
1448
|
+
/** Start an attempt whose `authorizeUrl` is the hub-relative proxied login page. */
|
|
1449
|
+
function beginHomeConnectProxiedSignIn(deps) {
|
|
1450
|
+
const nowMs = deps.nowMs ?? (() => Date.now());
|
|
1451
|
+
const published = publishHomeConnectSignIn(homeConnectSignInAttempts, nowMs());
|
|
1452
|
+
if (deps.proxy === null) return published;
|
|
1453
|
+
const entry = openSignInProxy({
|
|
1454
|
+
proxy: deps.proxy,
|
|
1455
|
+
claims: homeConnectSignInClaims,
|
|
1456
|
+
addonId: deps.addonId,
|
|
1457
|
+
attemptId: published.attemptId,
|
|
1458
|
+
authorizeUrl: published.authorizeUrl,
|
|
1459
|
+
hosts: HOME_CONNECT_LOGIN_HOSTS,
|
|
1460
|
+
appScheme: HOME_CONNECT_APP_SCHEME,
|
|
1461
|
+
finish: async (callback) => homeConnectFinishResult(await finishHomeConnectSignIn({
|
|
1462
|
+
attemptId: published.attemptId,
|
|
1463
|
+
callback,
|
|
1464
|
+
nowMs: nowMs()
|
|
1465
|
+
}, homeConnectSignInAttempts, deps.fetchImpl)),
|
|
1466
|
+
nowMs
|
|
1467
|
+
});
|
|
1468
|
+
return {
|
|
1469
|
+
attemptId: published.attemptId,
|
|
1470
|
+
authorizeUrl: entry
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/home-connect/home-connect-discovery.ts
|
|
1475
|
+
/** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
|
|
1476
|
+
function homeConnectDishwasherEntityName(containerName) {
|
|
1477
|
+
const base = containerName.trim();
|
|
1478
|
+
return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
|
|
1479
|
+
}
|
|
1480
|
+
function deviceTypeForApplianceType(type) {
|
|
1481
|
+
if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* The container candidate for one appliance. Its sensor children are not listed here:
|
|
1486
|
+
* they are the entities the appliance's own description declares, known once it is
|
|
1487
|
+
* connected, and they are attached to the container as that description is read.
|
|
1488
|
+
*/
|
|
1489
|
+
function dishwasherContainerCandidate(app, adoptedId) {
|
|
1490
|
+
const name = app.name.length > 0 ? app.name : app.haId;
|
|
1491
|
+
const children = [{
|
|
1492
|
+
childNativeId: `${app.haId}#dishwasher`,
|
|
1493
|
+
name: homeConnectDishwasherEntityName(name),
|
|
1494
|
+
type: DeviceType.Dishwasher,
|
|
1495
|
+
status: "online",
|
|
1496
|
+
metadata: {
|
|
1497
|
+
serialNumber: app.haId,
|
|
1498
|
+
model: app.type
|
|
1499
|
+
},
|
|
1500
|
+
alreadyAdopted: false,
|
|
1501
|
+
adoptedDeviceId: null,
|
|
1502
|
+
capabilities: ["dishwasher"]
|
|
1503
|
+
}];
|
|
1504
|
+
return {
|
|
1505
|
+
childNativeId: app.haId,
|
|
1506
|
+
name,
|
|
1507
|
+
type: DeviceType.Container,
|
|
1508
|
+
status: "online",
|
|
1509
|
+
metadata: {
|
|
1510
|
+
serialNumber: app.haId,
|
|
1511
|
+
model: app.type
|
|
1512
|
+
},
|
|
1513
|
+
alreadyAdopted: adoptedId !== null,
|
|
1514
|
+
adoptedDeviceId: adoptedId,
|
|
1515
|
+
capabilities: [],
|
|
1516
|
+
children
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function buildHomeConnectCandidates(input) {
|
|
1520
|
+
const out = [];
|
|
1521
|
+
for (const app of input.appliances) {
|
|
1522
|
+
if (deviceTypeForApplianceType(app.type) === null) continue;
|
|
1523
|
+
const adoptedId = input.adopted.get(app.haId) ?? null;
|
|
1524
|
+
out.push(dishwasherContainerCandidate(app, adoptedId));
|
|
1525
|
+
}
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/home-connect/home-connect-adoption-provider.ts
|
|
1530
|
+
var DEVICES_FILTER = {
|
|
1531
|
+
id: "devices",
|
|
1532
|
+
label: "Devices",
|
|
1533
|
+
isDefault: true
|
|
1534
|
+
};
|
|
1535
|
+
async function adoptedMapForIntegration(integrationId, listAdopted) {
|
|
1536
|
+
const all = await listAdopted();
|
|
1537
|
+
const map = /* @__PURE__ */ new Map();
|
|
1538
|
+
for (const device of all) if (device.config["system"] === "home-connect" && device.config["brokerId"] === integrationId) {
|
|
1539
|
+
const haId = device.config["haId"];
|
|
1540
|
+
if (typeof haId === "string") map.set(haId, device.id);
|
|
1541
|
+
}
|
|
1542
|
+
return map;
|
|
1543
|
+
}
|
|
1544
|
+
function buildHomeConnectAdoptionProvider(deps) {
|
|
1545
|
+
const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
1546
|
+
async function allCandidatesForIntegration(integrationId) {
|
|
1547
|
+
return buildHomeConnectCandidates({
|
|
1548
|
+
appliances: registry.appliances(integrationId),
|
|
1549
|
+
adopted: await adoptedMapForIntegration(integrationId, listAdopted)
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
return {
|
|
1553
|
+
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
1554
|
+
listCandidates: async ({ integrationId, page, pageSize }) => {
|
|
1555
|
+
const cands = await allCandidatesForIntegration(integrationId);
|
|
1556
|
+
const start = (page - 1) * pageSize;
|
|
1557
|
+
return {
|
|
1558
|
+
candidates: cands.slice(start, start + pageSize),
|
|
1559
|
+
totalCount: cands.length,
|
|
1560
|
+
page,
|
|
1561
|
+
pageSize
|
|
1562
|
+
};
|
|
1563
|
+
},
|
|
1564
|
+
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
1565
|
+
return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
1566
|
+
},
|
|
1567
|
+
getStatus: async () => {
|
|
1568
|
+
try {
|
|
1569
|
+
let candidateCount = 0;
|
|
1570
|
+
for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
|
|
1571
|
+
const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "home-connect").length;
|
|
1572
|
+
return {
|
|
1573
|
+
lastDiscoveryAt: Date.now(),
|
|
1574
|
+
candidateCount,
|
|
1575
|
+
adoptedCount,
|
|
1576
|
+
lastError: null
|
|
1577
|
+
};
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
return {
|
|
1580
|
+
lastDiscoveryAt: null,
|
|
1581
|
+
candidateCount: 0,
|
|
1582
|
+
adoptedCount: 0,
|
|
1583
|
+
lastError: errMsg(err)
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
refresh: async ({ integrationId }) => {
|
|
1588
|
+
const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
|
|
1589
|
+
const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
|
|
1590
|
+
return {
|
|
1591
|
+
lastDiscoveryAt: Date.now(),
|
|
1592
|
+
candidateCount,
|
|
1593
|
+
adoptedCount,
|
|
1594
|
+
lastError: null
|
|
1595
|
+
};
|
|
1596
|
+
},
|
|
1597
|
+
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
1598
|
+
if (!registry.has(integrationId)) throw new Error(`home-connect adopt: integration ${integrationId} not connected`);
|
|
1599
|
+
const appliances = registry.appliances(integrationId);
|
|
1600
|
+
const adopted = [];
|
|
1601
|
+
let failures = 0;
|
|
1602
|
+
for (const haId of childNativeIds) try {
|
|
1603
|
+
const app = appliances.find((a) => a.haId === haId);
|
|
1604
|
+
if (!app) {
|
|
1605
|
+
failures++;
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const candidate = buildHomeConnectCandidates({
|
|
1609
|
+
appliances: [app],
|
|
1610
|
+
adopted: /* @__PURE__ */ new Map()
|
|
1611
|
+
})[0];
|
|
1612
|
+
if (!candidate) {
|
|
1613
|
+
failures++;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const name = perCandidate?.[haId]?.name ?? app.name;
|
|
1617
|
+
const { deviceId } = await adoptDevice({
|
|
1618
|
+
haId,
|
|
1619
|
+
integrationId,
|
|
1620
|
+
type: candidate.type,
|
|
1621
|
+
name,
|
|
1622
|
+
applianceType: app.type
|
|
1623
|
+
});
|
|
1624
|
+
adopted.push({
|
|
1625
|
+
childNativeId: haId,
|
|
1626
|
+
parentDeviceId: deviceId
|
|
1627
|
+
});
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
logger.warn("home-connect adopt failed", { meta: {
|
|
1630
|
+
haId,
|
|
1631
|
+
integrationId,
|
|
1632
|
+
error: errMsg(err)
|
|
1633
|
+
} });
|
|
1634
|
+
failures++;
|
|
1635
|
+
}
|
|
1636
|
+
if (adopted.length === 0 && failures > 0) throw new Error(`home-connect adopt: all ${failures} adopt(s) failed`);
|
|
1637
|
+
return { adopted: adopted.map((a) => ({
|
|
1638
|
+
...a,
|
|
1639
|
+
accessoryDeviceIds: []
|
|
1640
|
+
})) };
|
|
1641
|
+
},
|
|
1642
|
+
release: async ({ camDeviceId }) => {
|
|
1643
|
+
await removeDevice(camDeviceId);
|
|
1644
|
+
},
|
|
1645
|
+
resync: async ({ camDeviceId }) => {
|
|
1646
|
+
const cfg = await findDeviceConfig(camDeviceId);
|
|
1647
|
+
if (!cfg || cfg["system"] !== "home-connect") throw new Error(`home-connect resync: device ${camDeviceId} not found`);
|
|
1648
|
+
const integrationId = String(cfg["brokerId"]);
|
|
1649
|
+
const haId = String(cfg["haId"]);
|
|
1650
|
+
if (!hasIntegration(integrationId)) throw new Error(`home-connect resync: integration ${integrationId} not connected`);
|
|
1651
|
+
if (!registry.appliances(integrationId).some((a) => a.haId === haId)) throw new Error(`home-connect resync: appliance ${haId} no longer present`);
|
|
1652
|
+
return {
|
|
1653
|
+
changed: false,
|
|
1654
|
+
rebuiltChildren: 0
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
//#endregion
|
|
1539
1660
|
//#region src/home-connect/home-connect-gateway.ts
|
|
1540
1661
|
/**
|
|
1541
1662
|
* Where devices find the client of their integration. A device that started before
|
|
@@ -3118,51 +3239,6 @@ function unmapped(appliances) {
|
|
|
3118
3239
|
return appliances.filter((a) => a.mode === "TLS" && entityCountInStoredDescription(a.deviceDescription) === 0).map((a) => a.haId);
|
|
3119
3240
|
}
|
|
3120
3241
|
//#endregion
|
|
3121
|
-
//#region src/home-connect/home-connect-stored-appliance.ts
|
|
3122
|
-
function stubDeviceDescriptionObject(row) {
|
|
3123
|
-
return { info: {
|
|
3124
|
-
deviceID: row.haId,
|
|
3125
|
-
deviceType: row.type,
|
|
3126
|
-
model: row.name
|
|
3127
|
-
} };
|
|
3128
|
-
}
|
|
3129
|
-
function stubDeviceDescription(row) {
|
|
3130
|
-
return JSON.stringify(stubDeviceDescriptionObject(row));
|
|
3131
|
-
}
|
|
3132
|
-
function serializedDeviceDescription(row) {
|
|
3133
|
-
if (row.deviceDescription === void 0) return stubDeviceDescription(row);
|
|
3134
|
-
if (typeof row.deviceDescription === "string") return row.deviceDescription;
|
|
3135
|
-
return JSON.stringify(row.deviceDescription);
|
|
3136
|
-
}
|
|
3137
|
-
function isDescriptionRecord(value) {
|
|
3138
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3139
|
-
}
|
|
3140
|
-
function parseLoginDeviceDescription(raw) {
|
|
3141
|
-
const trimmed = raw.trim();
|
|
3142
|
-
if (trimmed.length === 0) return {};
|
|
3143
|
-
try {
|
|
3144
|
-
const parsed = JSON.parse(trimmed);
|
|
3145
|
-
if (isDescriptionRecord(parsed)) return { deviceDescription: parsed };
|
|
3146
|
-
} catch {
|
|
3147
|
-
return {};
|
|
3148
|
-
}
|
|
3149
|
-
return {};
|
|
3150
|
-
}
|
|
3151
|
-
function listedApplianceFromStored(row) {
|
|
3152
|
-
return {
|
|
3153
|
-
haId: row.haId,
|
|
3154
|
-
name: row.name,
|
|
3155
|
-
type: row.type,
|
|
3156
|
-
connection: {
|
|
3157
|
-
host: row.host,
|
|
3158
|
-
psk: row.psk,
|
|
3159
|
-
mode: row.mode,
|
|
3160
|
-
iv64: row.iv64 ?? "",
|
|
3161
|
-
deviceDescription: serializedDeviceDescription(row)
|
|
3162
|
-
}
|
|
3163
|
-
};
|
|
3164
|
-
}
|
|
3165
|
-
//#endregion
|
|
3166
3242
|
//#region src/home-connect/home-connect-integration-manager.ts
|
|
3167
3243
|
/** Delays between IDDF fetch retries for an appliance that has no entity map; the last repeats. */
|
|
3168
3244
|
var HOME_CONNECT_IDDF_RETRY_MS = [
|
|
@@ -5242,11 +5318,14 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
|
|
|
5242
5318
|
};
|
|
5243
5319
|
clients = null;
|
|
5244
5320
|
reconciler = null;
|
|
5321
|
+
loginProxy = null;
|
|
5322
|
+
loginPlane = null;
|
|
5245
5323
|
constructor() {
|
|
5246
5324
|
super({});
|
|
5247
5325
|
}
|
|
5248
5326
|
async onInitialize() {
|
|
5249
5327
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
5328
|
+
await this.serveLoginProxy();
|
|
5250
5329
|
this.clients = new HomeConnectClientRegistry(this.ctx.logger, { onConnectionUpdated: (integrationId, connection) => {
|
|
5251
5330
|
this.persistIntegrationAppliances(integrationId, connection);
|
|
5252
5331
|
} });
|
|
@@ -5276,32 +5355,22 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
|
|
|
5276
5355
|
customActions: homeConnectActions,
|
|
5277
5356
|
actionHandlers: {
|
|
5278
5357
|
beginSignIn: async () => ({
|
|
5279
|
-
...
|
|
5280
|
-
|
|
5358
|
+
...beginHomeConnectProxiedSignIn({
|
|
5359
|
+
proxy: this.loginProxy,
|
|
5360
|
+
addonId: this.ctx.id
|
|
5361
|
+
}),
|
|
5362
|
+
summary: "Open the sign-in page and sign in."
|
|
5281
5363
|
}),
|
|
5282
|
-
finishSignIn: async (input) => {
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
const
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
host: app.connection.host,
|
|
5293
|
-
psk: app.connection.psk,
|
|
5294
|
-
mode: app.connection.mode,
|
|
5295
|
-
...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {},
|
|
5296
|
-
...parseLoginDeviceDescription(app.connection.deviceDescription)
|
|
5297
|
-
})));
|
|
5298
|
-
return {
|
|
5299
|
-
token: account.token,
|
|
5300
|
-
...account.refresh.length > 0 ? { refresh: account.refresh } : {},
|
|
5301
|
-
accountLabel: account.accountId,
|
|
5302
|
-
appliances: JSON.stringify(rows),
|
|
5303
|
-
applianceSecrets: JSON.stringify(secrets),
|
|
5304
|
-
summary: account.summary
|
|
5364
|
+
finishSignIn: async (input) => homeConnectFinishResult(await finishHomeConnectSignIn({
|
|
5365
|
+
attemptId: input.attemptId,
|
|
5366
|
+
callback: input.callback,
|
|
5367
|
+
nowMs: Date.now()
|
|
5368
|
+
}, homeConnectSignInAttempts)),
|
|
5369
|
+
claimSignIn: async (input) => {
|
|
5370
|
+
const claim = homeConnectSignInClaims.claim(input.attemptId, Date.now());
|
|
5371
|
+
return claim.state === "pending" ? { pending: true } : {
|
|
5372
|
+
pending: false,
|
|
5373
|
+
...claim.result
|
|
5305
5374
|
};
|
|
5306
5375
|
},
|
|
5307
5376
|
signOut: async () => ({
|
|
@@ -5311,7 +5380,28 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
|
|
|
5311
5380
|
}
|
|
5312
5381
|
};
|
|
5313
5382
|
}
|
|
5383
|
+
/** The hub's HTTPS face for the vendor's login pages (D955): admin session only. */
|
|
5384
|
+
async serveLoginProxy() {
|
|
5385
|
+
const proxy = new VendorLoginProxy({
|
|
5386
|
+
addonId: this.ctx.id,
|
|
5387
|
+
prefix: HOME_CONNECT_PROXY_PREFIX,
|
|
5388
|
+
logger: this.ctx.logger
|
|
5389
|
+
});
|
|
5390
|
+
try {
|
|
5391
|
+
this.loginPlane = await this.ctx.dataPlane?.serve({
|
|
5392
|
+
prefix: "login",
|
|
5393
|
+
access: "admin",
|
|
5394
|
+
handler: proxy.handler
|
|
5395
|
+
}) ?? null;
|
|
5396
|
+
if (this.loginPlane !== null) this.loginProxy = proxy;
|
|
5397
|
+
} catch (err) {
|
|
5398
|
+
this.ctx.logger.warn("Home Connect login proxy failed to serve — sign-in falls back to paste", { meta: { error: errMsg(err) } });
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5314
5401
|
async onShutdown() {
|
|
5402
|
+
await this.loginPlane?.dispose().catch(() => void 0);
|
|
5403
|
+
this.loginPlane = null;
|
|
5404
|
+
this.loginProxy = null;
|
|
5315
5405
|
this.reconciler?.stop();
|
|
5316
5406
|
this.reconciler = null;
|
|
5317
5407
|
try {
|