@camstack/addon-provider-bosch 0.1.10 → 0.1.12
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-BDOU9UIQ.js → account-integration-offline-CQ9kUtrn.js} +1 -1
- package/dist/{account-integration-offline-CifBq3c2.mjs → account-integration-offline-ei95Ru3m.mjs} +1 -1
- package/dist/{dist-Dwa_n1ZO.js → dist-CiKY7_En.js} +9 -15
- package/dist/{dist-EaW3D8xD.mjs → dist-DwSpbpO9.mjs} +9 -15
- package/dist/home-connect/addon.js +493 -121
- package/dist/home-connect/addon.mjs +493 -121
- 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,6 +1,6 @@
|
|
|
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-CiKY7_En.js");
|
|
3
|
+
const require_account_integration_offline = require("../account-integration-offline-CQ9kUtrn.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_tls = require("node:tls");
|
|
6
6
|
let node_zlib = require("node:zlib");
|
|
@@ -34,6 +34,53 @@ var homeConnectActions = require_dist.defineCustomActions({
|
|
|
34
34
|
})
|
|
35
35
|
});
|
|
36
36
|
//#endregion
|
|
37
|
+
//#region src/home-connect/home-connect-container-tracker.ts
|
|
38
|
+
/**
|
|
39
|
+
* Put a reconciler watch on every adopted appliance container not watched yet. Called at
|
|
40
|
+
* init, on connect, on adopt AND after the device restore — in a forked runner the devices
|
|
41
|
+
* are restored only after `onInitialize` returns, so a pass at init sees none of them and
|
|
42
|
+
* the containers were never watched: a description that arrived later derived no children.
|
|
43
|
+
* Returns how many containers it started watching.
|
|
44
|
+
*/
|
|
45
|
+
async function trackHomeConnectContainers(deps, trigger) {
|
|
46
|
+
let started = 0;
|
|
47
|
+
let devices;
|
|
48
|
+
try {
|
|
49
|
+
devices = await deps.listDevices();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
deps.logger.warn("Home Connect could not list its devices to watch their children", { meta: {
|
|
52
|
+
trigger,
|
|
53
|
+
error: require_dist.errMsg(err)
|
|
54
|
+
} });
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
for (const d of devices) {
|
|
58
|
+
if (d.type !== require_dist.DeviceType.Container || deps.reconciler.tracks(d.id)) continue;
|
|
59
|
+
const cfg = await deps.loadConfig(d.id).catch(() => ({}));
|
|
60
|
+
const brokerId = cfg["brokerId"];
|
|
61
|
+
const haId = cfg["haId"];
|
|
62
|
+
if (cfg["system"] !== "home-connect" || typeof brokerId !== "string") continue;
|
|
63
|
+
if (typeof haId !== "string") continue;
|
|
64
|
+
deps.reconciler.track({
|
|
65
|
+
deviceId: d.id,
|
|
66
|
+
brokerId,
|
|
67
|
+
haId
|
|
68
|
+
});
|
|
69
|
+
started += 1;
|
|
70
|
+
deps.logger.info("Home Connect watching appliance children", {
|
|
71
|
+
tags: {
|
|
72
|
+
deviceId: d.id,
|
|
73
|
+
integrationId: brokerId
|
|
74
|
+
},
|
|
75
|
+
meta: {
|
|
76
|
+
haId,
|
|
77
|
+
trigger
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return started;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
37
84
|
//#region src/home-connect/home-connect-xml.ts
|
|
38
85
|
var TOKEN_RE = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[([\s\S]*?)\]\]>|<!DOCTYPE[^>]*>|<\/([\w:.-]+)\s*>|<([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>|([^<]+)/g;
|
|
39
86
|
var ATTR_RE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
@@ -464,8 +511,52 @@ function decodeZipPayload(method, payload) {
|
|
|
464
511
|
}
|
|
465
512
|
return null;
|
|
466
513
|
}
|
|
467
|
-
|
|
514
|
+
var EOCD_SIG = 101010256;
|
|
515
|
+
var CENTRAL_SIG = 33639248;
|
|
516
|
+
var LOCAL_SIG = 67324752;
|
|
517
|
+
/** The end-of-central-directory record is 22 bytes plus a comment of at most 64 KiB. */
|
|
518
|
+
var EOCD_MAX_SCAN = 65557;
|
|
519
|
+
function findEndOfCentralDirectory(zip) {
|
|
520
|
+
const lowest = Math.max(0, zip.length - EOCD_MAX_SCAN);
|
|
521
|
+
for (let pos = zip.length - 22; pos >= lowest; pos -= 1) if (readUInt32LE(zip, pos) === EOCD_SIG) return pos;
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Entries named by the central directory — the authoritative sizes. A streamed archive (bit 3,
|
|
526
|
+
* as a Java `ZipOutputStream` writes it, and as the cloud IDDF arrives) has ZERO sizes in every
|
|
527
|
+
* local header; only the central directory and the data descriptors carry them.
|
|
528
|
+
*/
|
|
529
|
+
function entriesFromCentralDirectory(zip) {
|
|
530
|
+
const eocd = findEndOfCentralDirectory(zip);
|
|
531
|
+
if (eocd === null) return null;
|
|
532
|
+
const count = readUInt16LE(zip, eocd + 10);
|
|
533
|
+
let pos = readUInt32LE(zip, eocd + 16);
|
|
534
|
+
const out = /* @__PURE__ */ new Map();
|
|
535
|
+
for (let i = 0; i < count && pos + 46 <= zip.length; i += 1) {
|
|
536
|
+
if (readUInt32LE(zip, pos) !== CENTRAL_SIG) break;
|
|
537
|
+
const method = readUInt16LE(zip, pos + 10);
|
|
538
|
+
const compSize = readUInt32LE(zip, pos + 20);
|
|
539
|
+
const nameLen = readUInt16LE(zip, pos + 28);
|
|
540
|
+
const extraLen = readUInt16LE(zip, pos + 30);
|
|
541
|
+
const commentLen = readUInt16LE(zip, pos + 32);
|
|
542
|
+
const localOffset = readUInt32LE(zip, pos + 42);
|
|
543
|
+
const name = zip.subarray(pos + 46, pos + 46 + nameLen).toString("utf8");
|
|
544
|
+
pos += 46 + nameLen + extraLen + commentLen;
|
|
545
|
+
if (localOffset + 30 > zip.length || readUInt32LE(zip, localOffset) !== LOCAL_SIG) continue;
|
|
546
|
+
const dataStart = localOffset + 30 + readUInt16LE(zip, localOffset + 26) + readUInt16LE(zip, localOffset + 28);
|
|
547
|
+
const decoded = decodeZipPayload(method, zip.subarray(dataStart, dataStart + compSize));
|
|
548
|
+
if (decoded !== null) out.set(name, decoded);
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
/** Extract file bodies from a ZIP (store + deflate): central directory first, local headers as a fallback. */
|
|
468
553
|
function extractZipEntries(zip) {
|
|
554
|
+
if (zip.length < 22) return /* @__PURE__ */ new Map();
|
|
555
|
+
const central = entriesFromCentralDirectory(zip);
|
|
556
|
+
if (central !== null && central.size > 0) return central;
|
|
557
|
+
return entriesFromLocalHeaders(zip);
|
|
558
|
+
}
|
|
559
|
+
function entriesFromLocalHeaders(zip) {
|
|
469
560
|
const out = /* @__PURE__ */ new Map();
|
|
470
561
|
let pos = 0;
|
|
471
562
|
while (pos + 30 <= zip.length) {
|
|
@@ -500,6 +591,13 @@ function parseIddfZipBuffer(zip, haId) {
|
|
|
500
591
|
haId
|
|
501
592
|
});
|
|
502
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* The IDDF request carries the bearer token and NOTHING else — the reference downloaders send
|
|
596
|
+
* no `Accept`, and the endpoint answered 403 to `accept: application/zip` on a live hub.
|
|
597
|
+
*/
|
|
598
|
+
function homeConnectIddfHeaders(token) {
|
|
599
|
+
return { authorization: `Bearer ${token}` };
|
|
600
|
+
}
|
|
503
601
|
var HOME_CONNECT_IDDF_URL = (haId) => `https://eu.services.home-connect.com/api/iddf/v1/iddf/${encodeURIComponent(haId)}`;
|
|
504
602
|
//#endregion
|
|
505
603
|
//#region src/home-connect/home-connect-login.ts
|
|
@@ -683,10 +781,7 @@ async function appliancesForToken(token, fetchImpl, jar) {
|
|
|
683
781
|
if (!appliance) continue;
|
|
684
782
|
const iddfResponse = await fetchImpl(HOME_CONNECT_IDDF_URL(haId), {
|
|
685
783
|
method: "GET",
|
|
686
|
-
headers:
|
|
687
|
-
authorization: `Bearer ${token}`,
|
|
688
|
-
accept: "application/zip"
|
|
689
|
-
},
|
|
784
|
+
headers: homeConnectIddfHeaders(token),
|
|
690
785
|
redirect: "manual"
|
|
691
786
|
});
|
|
692
787
|
if (iddfResponse.status < 400 && iddfResponse.arrayBuffer) {
|
|
@@ -709,8 +804,8 @@ async function appliancesForToken(token, fetchImpl, jar) {
|
|
|
709
804
|
/** Load every appliance on the account. A stale access token is refreshed once when a refresh token exists. */
|
|
710
805
|
async function fetchHomeConnectAccount(creds, fetchImpl = homeConnectLoginFetch) {
|
|
711
806
|
const jar = new CookieJar();
|
|
712
|
-
|
|
713
|
-
|
|
807
|
+
const token = creds.token;
|
|
808
|
+
const refresh = creds.refresh ?? "";
|
|
714
809
|
try {
|
|
715
810
|
const appliances = await appliancesForToken(token, fetchImpl, jar);
|
|
716
811
|
return {
|
|
@@ -722,23 +817,76 @@ async function fetchHomeConnectAccount(creds, fetchImpl = homeConnectLoginFetch)
|
|
|
722
817
|
} catch (err) {
|
|
723
818
|
if (refresh.length === 0) throw err;
|
|
724
819
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
client_id: APP_ID$1
|
|
729
|
-
}).toString());
|
|
730
|
-
if (refreshed.status >= 400) throw new Error("Home Connect sign-in was rejected");
|
|
731
|
-
const next = tokensFromBody(refreshed.body);
|
|
732
|
-
token = next.token;
|
|
733
|
-
refresh = next.refresh;
|
|
734
|
-
const appliances = await appliancesForToken(token, fetchImpl, jar);
|
|
820
|
+
const next = await refreshHomeConnectToken(refresh, fetchImpl);
|
|
821
|
+
if (!next.ok) throw new Error("Home Connect sign-in was rejected");
|
|
822
|
+
const appliances = await appliancesForToken(next.token, fetchImpl, jar);
|
|
735
823
|
return {
|
|
736
|
-
token,
|
|
737
|
-
refresh,
|
|
824
|
+
token: next.token,
|
|
825
|
+
refresh: next.refresh,
|
|
738
826
|
appliances,
|
|
739
827
|
summary: accountSummary(appliances)
|
|
740
828
|
};
|
|
741
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Exchange the refresh token for a new access token. Never throws; a failure says why and
|
|
832
|
+
* carries no credential. A response without a new refresh token keeps the one that was used.
|
|
833
|
+
*/
|
|
834
|
+
async function refreshHomeConnectToken(refresh, fetchImpl = homeConnectLoginFetch) {
|
|
835
|
+
if (refresh.length === 0) return {
|
|
836
|
+
ok: false,
|
|
837
|
+
reason: "no-refresh-token",
|
|
838
|
+
status: null
|
|
839
|
+
};
|
|
840
|
+
let status;
|
|
841
|
+
let body;
|
|
842
|
+
try {
|
|
843
|
+
const refreshed = await exchange(fetchImpl, new CookieJar(), `${OAUTH_BASE}token`, "POST", { "content-type": "application/x-www-form-urlencoded" }, new URLSearchParams({
|
|
844
|
+
grant_type: "refresh_token",
|
|
845
|
+
refresh_token: refresh,
|
|
846
|
+
client_id: APP_ID$1
|
|
847
|
+
}).toString());
|
|
848
|
+
status = refreshed.status;
|
|
849
|
+
body = refreshed.body;
|
|
850
|
+
} catch (err) {
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
reason: "network",
|
|
854
|
+
status: null,
|
|
855
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (status >= 400) return {
|
|
859
|
+
ok: false,
|
|
860
|
+
reason: "rejected",
|
|
861
|
+
status
|
|
862
|
+
};
|
|
863
|
+
try {
|
|
864
|
+
const next = tokensFromBody(body);
|
|
865
|
+
return {
|
|
866
|
+
ok: true,
|
|
867
|
+
token: next.token,
|
|
868
|
+
refresh: next.refresh.length > 0 ? next.refresh : refresh
|
|
869
|
+
};
|
|
870
|
+
} catch {
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
reason: "unreadable-response",
|
|
874
|
+
status
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/** The access token's JWT `exp` in epoch ms; `null` when the token carries none. */
|
|
879
|
+
function homeConnectTokenExpiresAtMs(token) {
|
|
880
|
+
const payload = token.split(".")[1];
|
|
881
|
+
if (!payload) return null;
|
|
882
|
+
try {
|
|
883
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
884
|
+
if (!isRecord$1(parsed) || typeof parsed["exp"] !== "number") return null;
|
|
885
|
+
return parsed["exp"] * 1e3;
|
|
886
|
+
} catch {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
742
890
|
/** Browser login link, matching Home Connect Local (`redirect_url` + the app scope). */
|
|
743
891
|
function beginHomeConnectSignIn() {
|
|
744
892
|
const verifier = b64url((0, node_crypto.randomBytes)(32));
|
|
@@ -2745,34 +2893,141 @@ function accountClient(appliances, makeGateway) {
|
|
|
2745
2893
|
}
|
|
2746
2894
|
};
|
|
2747
2895
|
}
|
|
2748
|
-
//#endregion
|
|
2749
|
-
//#region src/home-connect/home-connect-iddf-fetch.ts
|
|
2750
2896
|
function entityCountInStoredDescription(description) {
|
|
2751
2897
|
if (description === void 0) return 0;
|
|
2752
2898
|
return entityIndexFromDescription(description).size;
|
|
2753
2899
|
}
|
|
2754
|
-
|
|
2900
|
+
/** How many leading body bytes the unparsable-body probe logs. */
|
|
2901
|
+
var HEAD_HEX_BYTES = 16;
|
|
2902
|
+
async function fetchHomeConnectIddf(haId, token, fetchImpl) {
|
|
2755
2903
|
const response = await fetchImpl(HOME_CONNECT_IDDF_URL(haId), {
|
|
2756
2904
|
method: "GET",
|
|
2757
|
-
headers:
|
|
2758
|
-
authorization: `Bearer ${token}`,
|
|
2759
|
-
accept: "application/zip"
|
|
2760
|
-
},
|
|
2905
|
+
headers: homeConnectIddfHeaders(token),
|
|
2761
2906
|
redirect: "manual"
|
|
2762
2907
|
});
|
|
2763
|
-
|
|
2764
|
-
const
|
|
2765
|
-
|
|
2908
|
+
const contentType = response.headers.get("content-type");
|
|
2909
|
+
const headers = {
|
|
2910
|
+
status: response.status,
|
|
2911
|
+
contentType,
|
|
2912
|
+
contentEncoding: response.headers.get("content-encoding"),
|
|
2913
|
+
contentDisposition: response.headers.get("content-disposition")
|
|
2914
|
+
};
|
|
2915
|
+
const unread = {
|
|
2916
|
+
...headers,
|
|
2917
|
+
bytes: null,
|
|
2918
|
+
zipEntries: null,
|
|
2919
|
+
zipEntryNames: null,
|
|
2920
|
+
headHex: null
|
|
2921
|
+
};
|
|
2922
|
+
if (response.status === 401) return {
|
|
2923
|
+
ok: false,
|
|
2924
|
+
reason: "unauthorized",
|
|
2925
|
+
entityCount: 0,
|
|
2926
|
+
...unread
|
|
2927
|
+
};
|
|
2928
|
+
if (response.status >= 400) return {
|
|
2929
|
+
ok: false,
|
|
2930
|
+
reason: "http-error",
|
|
2931
|
+
entityCount: 0,
|
|
2932
|
+
...unread
|
|
2933
|
+
};
|
|
2934
|
+
if (!response.arrayBuffer) return {
|
|
2935
|
+
ok: false,
|
|
2936
|
+
reason: "no-body",
|
|
2937
|
+
entityCount: 0,
|
|
2938
|
+
...unread
|
|
2939
|
+
};
|
|
2940
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
2941
|
+
const compact = parseIddfZipBuffer(body, haId);
|
|
2942
|
+
const read = () => {
|
|
2943
|
+
const names = [...extractZipEntries(body).keys()];
|
|
2944
|
+
return {
|
|
2945
|
+
...headers,
|
|
2946
|
+
bytes: body.length,
|
|
2947
|
+
zipEntries: names.length,
|
|
2948
|
+
zipEntryNames: names,
|
|
2949
|
+
headHex: body.subarray(0, HEAD_HEX_BYTES).toString("hex")
|
|
2950
|
+
};
|
|
2951
|
+
};
|
|
2952
|
+
if (compact === null) return {
|
|
2953
|
+
ok: false,
|
|
2954
|
+
reason: "unparsable-zip",
|
|
2955
|
+
entityCount: 0,
|
|
2956
|
+
...read()
|
|
2957
|
+
};
|
|
2958
|
+
const entityCount = entityCountForCompact(compact);
|
|
2959
|
+
if (entityCount === 0) return {
|
|
2960
|
+
ok: false,
|
|
2961
|
+
reason: "no-entities",
|
|
2962
|
+
entityCount,
|
|
2963
|
+
...read()
|
|
2964
|
+
};
|
|
2965
|
+
return {
|
|
2966
|
+
ok: true,
|
|
2967
|
+
compact,
|
|
2968
|
+
entityCount
|
|
2969
|
+
};
|
|
2766
2970
|
}
|
|
2767
2971
|
function compactDescriptionToStored(compact) {
|
|
2768
2972
|
return JSON.parse(JSON.stringify(compact));
|
|
2769
2973
|
}
|
|
2770
2974
|
function entityCountForCompact(compact) {
|
|
2771
|
-
if (compact === null) return 0;
|
|
2772
2975
|
return entityIndexFromDescription(JSON.parse(JSON.stringify(compact))).size;
|
|
2773
2976
|
}
|
|
2977
|
+
function refreshFailureMeta(failure) {
|
|
2978
|
+
return {
|
|
2979
|
+
reason: `refresh-${failure.reason}`,
|
|
2980
|
+
status: failure.status,
|
|
2981
|
+
...failure.error !== void 0 ? { error: failure.error } : {}
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
/** Refresh once; on failure the state is untouched (D49) and the reason is logged. */
|
|
2985
|
+
async function refreshOnce(state, trigger, fetchImpl, hooks) {
|
|
2986
|
+
if (state.refreshTried) return false;
|
|
2987
|
+
state.refreshTried = true;
|
|
2988
|
+
const result = await refreshHomeConnectToken(state.refresh, fetchImpl);
|
|
2989
|
+
if (!result.ok) {
|
|
2990
|
+
hooks.onWarn?.("Home Connect access token refresh failed — keeping the stored token", {
|
|
2991
|
+
trigger,
|
|
2992
|
+
...refreshFailureMeta(result)
|
|
2993
|
+
});
|
|
2994
|
+
return false;
|
|
2995
|
+
}
|
|
2996
|
+
state.token = result.token;
|
|
2997
|
+
state.refresh = result.refresh;
|
|
2998
|
+
state.rotated = true;
|
|
2999
|
+
hooks.onInfo?.("Home Connect access token refreshed", { trigger });
|
|
3000
|
+
return true;
|
|
3001
|
+
}
|
|
3002
|
+
function tokenNeedsRefresh(token, now) {
|
|
3003
|
+
const expiresAt = homeConnectTokenExpiresAtMs(token);
|
|
3004
|
+
return expiresAt !== null && expiresAt - now <= 3e5;
|
|
3005
|
+
}
|
|
3006
|
+
async function fetchWithRefresh(haId, state, fetchImpl, hooks) {
|
|
3007
|
+
const first = await fetchHomeConnectIddf(haId, state.token, fetchImpl);
|
|
3008
|
+
if (first.ok || first.reason !== "unauthorized") return first;
|
|
3009
|
+
if (!await refreshOnce(state, "unauthorized", fetchImpl, hooks)) return first;
|
|
3010
|
+
return fetchHomeConnectIddf(haId, state.token, fetchImpl);
|
|
3011
|
+
}
|
|
3012
|
+
function failureMeta(failure) {
|
|
3013
|
+
return {
|
|
3014
|
+
reason: failure.reason,
|
|
3015
|
+
status: failure.status,
|
|
3016
|
+
contentType: failure.contentType,
|
|
3017
|
+
bytes: failure.bytes,
|
|
3018
|
+
zipEntries: failure.zipEntries,
|
|
3019
|
+
zipEntryNames: failure.zipEntryNames,
|
|
3020
|
+
headHex: failure.headHex,
|
|
3021
|
+
contentEncoding: failure.contentEncoding,
|
|
3022
|
+
contentDisposition: failure.contentDisposition,
|
|
3023
|
+
entityCount: failure.entityCount
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
2774
3026
|
/**
|
|
2775
3027
|
* Fill missing or outdated IDDF entity maps on stored appliances (TLS first).
|
|
3028
|
+
* An access token at or near its expiry is refreshed first, and a 401 refreshes once and
|
|
3029
|
+
* retries; a rotated pair is returned on the connection (`tokenRotated`) for the caller to
|
|
3030
|
+
* persist. A failed refresh changes nothing (D49).
|
|
2776
3031
|
* An appliance whose IDDF cannot be fetched keeps whatever it had and is listed in
|
|
2777
3032
|
* `missing` so the caller can retry — the entity map of ANOTHER model is never
|
|
2778
3033
|
* substituted: the UIDs differ per model and a wrong map decodes values into the
|
|
@@ -2783,47 +3038,71 @@ async function enrichHomeConnectConnection(connection, fetchImpl, hooks = {}) {
|
|
|
2783
3038
|
if (appliances.length === 0) return {
|
|
2784
3039
|
connection,
|
|
2785
3040
|
changed: false,
|
|
3041
|
+
tokenRotated: false,
|
|
2786
3042
|
missing: []
|
|
2787
3043
|
};
|
|
2788
3044
|
const token = connection.token;
|
|
2789
3045
|
if (require_dist.isSecretMarker(token) || require_dist.isSecretMarkerLike(token)) return {
|
|
2790
3046
|
connection,
|
|
2791
3047
|
changed: false,
|
|
3048
|
+
tokenRotated: false,
|
|
2792
3049
|
missing: unmapped(appliances)
|
|
2793
3050
|
};
|
|
3051
|
+
const refreshStored = connection.refresh ?? "";
|
|
3052
|
+
const state = {
|
|
3053
|
+
token,
|
|
3054
|
+
refresh: require_dist.isSecretMarker(refreshStored) || require_dist.isSecretMarkerLike(refreshStored) ? "" : refreshStored,
|
|
3055
|
+
rotated: false,
|
|
3056
|
+
refreshTried: false
|
|
3057
|
+
};
|
|
3058
|
+
const pending = appliances.filter((row) => row.mode === "TLS" && !isCurrentDescription(row.deviceDescription));
|
|
3059
|
+
if (pending.length > 0 && tokenNeedsRefresh(state.token, (hooks.now ?? Date.now)())) await refreshOnce(state, "expired", fetchImpl, hooks);
|
|
2794
3060
|
let changed = false;
|
|
2795
3061
|
const nextAppliances = [];
|
|
2796
3062
|
for (const row of appliances) {
|
|
2797
|
-
if (!
|
|
3063
|
+
if (!pending.includes(row)) {
|
|
2798
3064
|
nextAppliances.push(row);
|
|
2799
3065
|
continue;
|
|
2800
3066
|
}
|
|
2801
|
-
let
|
|
3067
|
+
let result = null;
|
|
2802
3068
|
try {
|
|
2803
|
-
|
|
3069
|
+
result = await fetchWithRefresh(row.haId, state, fetchImpl, hooks);
|
|
2804
3070
|
} catch (err) {
|
|
2805
|
-
hooks.
|
|
3071
|
+
hooks.onWarn?.("Home Connect IDDF fetch failed", {
|
|
2806
3072
|
haId: row.haId,
|
|
2807
3073
|
error: require_dist.errMsg(err)
|
|
2808
3074
|
});
|
|
2809
3075
|
}
|
|
2810
|
-
if (
|
|
2811
|
-
hooks.
|
|
3076
|
+
if (result === null || !result.ok) {
|
|
3077
|
+
hooks.onWarn?.("Home Connect IDDF unavailable — appliance stays degraded until it loads", {
|
|
3078
|
+
haId: row.haId,
|
|
3079
|
+
...result === null ? { reason: "fetch-threw" } : failureMeta(result)
|
|
3080
|
+
});
|
|
2812
3081
|
nextAppliances.push(row);
|
|
2813
3082
|
continue;
|
|
2814
3083
|
}
|
|
2815
3084
|
changed = true;
|
|
3085
|
+
hooks.onInfo?.("Home Connect IDDF loaded", {
|
|
3086
|
+
haId: row.haId,
|
|
3087
|
+
entityCount: result.entityCount
|
|
3088
|
+
});
|
|
2816
3089
|
nextAppliances.push({
|
|
2817
3090
|
...row,
|
|
2818
|
-
deviceDescription: compactDescriptionToStored(compact)
|
|
3091
|
+
deviceDescription: compactDescriptionToStored(result.compact)
|
|
2819
3092
|
});
|
|
2820
3093
|
}
|
|
3094
|
+
const withAppliances = changed ? {
|
|
3095
|
+
...connection,
|
|
3096
|
+
appliances: nextAppliances
|
|
3097
|
+
} : connection;
|
|
2821
3098
|
return {
|
|
2822
|
-
connection:
|
|
2823
|
-
...
|
|
2824
|
-
|
|
2825
|
-
|
|
3099
|
+
connection: state.rotated ? {
|
|
3100
|
+
...withAppliances,
|
|
3101
|
+
token: state.token,
|
|
3102
|
+
refresh: state.refresh
|
|
3103
|
+
} : withAppliances,
|
|
2826
3104
|
changed,
|
|
3105
|
+
tokenRotated: state.rotated,
|
|
2827
3106
|
missing: unmapped(nextAppliances)
|
|
2828
3107
|
};
|
|
2829
3108
|
}
|
|
@@ -2885,6 +3164,19 @@ var HOME_CONNECT_IDDF_RETRY_MS = [
|
|
|
2885
3164
|
3e5,
|
|
2886
3165
|
6e5
|
|
2887
3166
|
];
|
|
3167
|
+
/** Route the enrich pass's lines to the logger, tagged with the integration (haId in meta). */
|
|
3168
|
+
function enrichLogHooks(logger, integrationId) {
|
|
3169
|
+
return {
|
|
3170
|
+
onInfo: (message, meta) => logger?.info(message, {
|
|
3171
|
+
tags: { integrationId },
|
|
3172
|
+
meta
|
|
3173
|
+
}),
|
|
3174
|
+
onWarn: (message, meta) => logger?.warn(message, {
|
|
3175
|
+
tags: { integrationId },
|
|
3176
|
+
meta
|
|
3177
|
+
})
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
2888
3180
|
async function openStoredOrAccount(connection, deps) {
|
|
2889
3181
|
let working = connection;
|
|
2890
3182
|
let missing = [];
|
|
@@ -2894,12 +3186,7 @@ async function openStoredOrAccount(connection, deps) {
|
|
|
2894
3186
|
tags: { integrationId: deps.integrationId },
|
|
2895
3187
|
meta: { tokenIsSecretMarker: require_dist.isSecretMarker(token) }
|
|
2896
3188
|
});
|
|
2897
|
-
const enriched = await enrichHomeConnectConnection(working, deps.fetchImpl ?? homeConnectLoginFetch,
|
|
2898
|
-
deps.logger?.info(message, {
|
|
2899
|
-
tags: { integrationId: deps.integrationId },
|
|
2900
|
-
meta
|
|
2901
|
-
});
|
|
2902
|
-
} });
|
|
3189
|
+
const enriched = await enrichHomeConnectConnection(working, deps.fetchImpl ?? homeConnectLoginFetch, enrichLogHooks(deps.logger, deps.integrationId));
|
|
2903
3190
|
working = enriched.connection;
|
|
2904
3191
|
missing = enriched.missing;
|
|
2905
3192
|
if (enriched.changed) deps.logger?.info("Home Connect enriched appliance IDDF from cloud", {
|
|
@@ -3085,14 +3372,13 @@ var HomeConnectIntegrationManager = class {
|
|
|
3085
3372
|
const client = this.#client;
|
|
3086
3373
|
if (client === null) return;
|
|
3087
3374
|
try {
|
|
3088
|
-
const result = await enrichHomeConnectConnection(this.#connection, this.#fetchImpl ?? homeConnectLoginFetch,
|
|
3089
|
-
this.#logger.info(message, {
|
|
3090
|
-
tags: { integrationId: this.#id },
|
|
3091
|
-
meta
|
|
3092
|
-
});
|
|
3093
|
-
} });
|
|
3375
|
+
const result = await enrichHomeConnectConnection(this.#connection, this.#fetchImpl ?? homeConnectLoginFetch, enrichLogHooks(this.#logger, this.#id));
|
|
3094
3376
|
if (this.#client !== client) return;
|
|
3095
3377
|
this.#missing = result.missing;
|
|
3378
|
+
if (result.tokenRotated && !result.changed) {
|
|
3379
|
+
this.#connection = result.connection;
|
|
3380
|
+
this.#onConnectionUpdated?.(this.#id, this.#connection);
|
|
3381
|
+
}
|
|
3096
3382
|
if (result.changed) {
|
|
3097
3383
|
this.#connection = result.connection;
|
|
3098
3384
|
for (const row of result.connection.appliances ?? []) {
|
|
@@ -3320,6 +3606,55 @@ function buildHomeConnectConnectionTestProvider(deps) {
|
|
|
3320
3606
|
};
|
|
3321
3607
|
}
|
|
3322
3608
|
//#endregion
|
|
3609
|
+
//#region src/home-connect/home-connect-connection-persist.ts
|
|
3610
|
+
function locked(value) {
|
|
3611
|
+
return require_dist.isSecretMarker(value) || require_dist.isSecretMarkerLike(value);
|
|
3612
|
+
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Persist what a connection learned at runtime — enriched IDDF entity maps and a rotated
|
|
3615
|
+
* access / refresh token — in ONE `setSettings` write. The tokens are named in `secretKeys`
|
|
3616
|
+
* so they land in the keychain, never on the plain blob. Writes nothing when a stored secret
|
|
3617
|
+
* cannot be read right now: the write would drop a key it cannot see or be refused. Never
|
|
3618
|
+
* throws and never logs a value.
|
|
3619
|
+
*/
|
|
3620
|
+
function buildHomeConnectConnectionPersister(deps) {
|
|
3621
|
+
return async (integrationId, connection) => {
|
|
3622
|
+
if (!connection.appliances || connection.appliances.length === 0) return;
|
|
3623
|
+
try {
|
|
3624
|
+
const settings = await deps.getSettings(integrationId);
|
|
3625
|
+
const stored = readApplianceSecrets(settings[APPLIANCE_SECRETS_KEY]);
|
|
3626
|
+
const lockedTokens = ["token", "refresh"].filter((key) => locked(settings[key]));
|
|
3627
|
+
if (stored.kind === "unresolved" || stored.kind === "invalid" || lockedTokens.length > 0) {
|
|
3628
|
+
deps.logger.warn("Home Connect not persisting the connection — stored secrets are locked or unreadable", {
|
|
3629
|
+
tags: { integrationId },
|
|
3630
|
+
meta: {
|
|
3631
|
+
applianceSecrets: stored.kind,
|
|
3632
|
+
lockedTokens
|
|
3633
|
+
}
|
|
3634
|
+
});
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
const refresh = connection.refresh ?? "";
|
|
3638
|
+
const merged = homeConnectSettingsForPersist({
|
|
3639
|
+
...settings,
|
|
3640
|
+
token: connection.token,
|
|
3641
|
+
...refresh.length > 0 ? { refresh } : {},
|
|
3642
|
+
appliances: mergeAppliancesForPersist(settings["appliances"], connection.appliances)
|
|
3643
|
+
});
|
|
3644
|
+
await deps.setSettings({
|
|
3645
|
+
id: integrationId,
|
|
3646
|
+
settings: merged,
|
|
3647
|
+
secretKeys: [...HOME_CONNECT_SECRET_KEYS]
|
|
3648
|
+
});
|
|
3649
|
+
} catch (err) {
|
|
3650
|
+
deps.logger.warn("Home Connect failed to persist the connection", {
|
|
3651
|
+
tags: { integrationId },
|
|
3652
|
+
meta: { error: require_dist.errMsg(err) }
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
//#endregion
|
|
3323
3658
|
//#region src/home-connect/home-connect-units.ts
|
|
3324
3659
|
/**
|
|
3325
3660
|
* Units are a property of the ENTITY, fixed for its whole series. A value is never
|
|
@@ -3434,14 +3769,20 @@ function operationStateOf(raw) {
|
|
|
3434
3769
|
const parsed = require_dist.DishwasherOperationStateSchema.safeParse(raw);
|
|
3435
3770
|
return parsed.success ? parsed.data : null;
|
|
3436
3771
|
}
|
|
3772
|
+
/** The reported state of EVERY listed event the description has — each read on its own. */
|
|
3773
|
+
function eventStates(view, shortNames) {
|
|
3774
|
+
return (view.description?.entities() ?? []).filter((e) => e.kind === "event" && shortNames.includes(featureShortName(e.name))).map((e) => eventActive(valueOf(view, e)));
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Any empty-class event raised → `empty`; else any nearly-empty one raised → `low`; else any of
|
|
3778
|
+
* them reading clear → `ok`; nothing reported → `null` (unknown, never `ok`).
|
|
3779
|
+
*/
|
|
3437
3780
|
function consumableFromEvents(view, nearlyEmpty, empty) {
|
|
3438
|
-
const
|
|
3439
|
-
const
|
|
3440
|
-
if (
|
|
3441
|
-
if (
|
|
3442
|
-
|
|
3443
|
-
if (nearlyActive === true) return "low";
|
|
3444
|
-
if (nearlyActive === false) return "ok";
|
|
3781
|
+
const gone = eventStates(view, empty);
|
|
3782
|
+
const nearly = eventStates(view, nearlyEmpty);
|
|
3783
|
+
if (gone.includes(true)) return "empty";
|
|
3784
|
+
if (nearly.includes(true)) return "low";
|
|
3785
|
+
if (gone.includes(false) || nearly.includes(false)) return "ok";
|
|
3445
3786
|
return null;
|
|
3446
3787
|
}
|
|
3447
3788
|
function programKey(raw) {
|
|
@@ -3531,8 +3872,8 @@ function dishwasherStatusFromFeatures(input) {
|
|
|
3531
3872
|
localControlActive: boolOf(valueOf(input, findEntity(description, ["status"], ["LocalControlActive"]))),
|
|
3532
3873
|
powerOn: powerRaw === null ? null : powerRaw === "On",
|
|
3533
3874
|
powerState: powerRaw,
|
|
3534
|
-
saltLevel: consumableFromEvents(input, ["SaltNearlyEmpty"], ["SaltEmpty"]),
|
|
3535
|
-
rinseAidLevel: consumableFromEvents(input, ["RinseAidNearlyEmpty"], ["RinseAidEmpty"]),
|
|
3875
|
+
saltLevel: consumableFromEvents(input, ["SaltNearlyEmpty"], ["SaltEmpty", "SaltLack"]),
|
|
3876
|
+
rinseAidLevel: consumableFromEvents(input, ["RinseAidNearlyEmpty"], ["RinseAidEmpty", "RinseAidLack"]),
|
|
3536
3877
|
halfLoad: optionBool(input, ["HalfLoad"]),
|
|
3537
3878
|
hygienePlus: optionBool(input, ["HygienePlus"]),
|
|
3538
3879
|
extraDry: optionBool(input, ["ExtraDry"]),
|
|
@@ -3942,22 +4283,38 @@ function legacyKindBinding(kind, name) {
|
|
|
3942
4283
|
function isSyntheticKey(key) {
|
|
3943
4284
|
return key.startsWith("HomeConnect.Info.") || key.startsWith("HomeConnect.Network.");
|
|
3944
4285
|
}
|
|
4286
|
+
/** Keys of persisted children the description does not list (info / network keys never count). */
|
|
4287
|
+
function keysMissingFromDescription(persisted, description) {
|
|
4288
|
+
return new Set(persisted.filter((b) => !isSyntheticKey(b.key) && description.entityByName(b.key) === void 0).map((b) => b.key));
|
|
4289
|
+
}
|
|
3945
4290
|
/**
|
|
3946
4291
|
* The bindings a container should carry: what the description derives now, plus what it
|
|
3947
|
-
* already had
|
|
3948
|
-
*
|
|
4292
|
+
* already had. A child leaves only when its entity is missing from TWO agreeing reads (D49):
|
|
4293
|
+
* `missingBefore` is what the previous read lacked — one description that omits an entity is
|
|
4294
|
+
* not enough to delete a device. A derived binding for a key the container already carries
|
|
4295
|
+
* keeps the existing child's `stableIdSuffix` and name, so a container saved before the
|
|
4296
|
+
* derived model (legacy children) keeps its device ids instead of being rebuilt.
|
|
3949
4297
|
*/
|
|
3950
|
-
function mergeChildBindings(persisted, derived, description) {
|
|
4298
|
+
function mergeChildBindings(persisted, derived, description, missingBefore = /* @__PURE__ */ new Set()) {
|
|
4299
|
+
const missingNow = keysMissingFromDescription(persisted, description);
|
|
3951
4300
|
const out = /* @__PURE__ */ new Map();
|
|
3952
|
-
|
|
3953
|
-
for (const
|
|
4301
|
+
const suffixByKey = /* @__PURE__ */ new Map();
|
|
4302
|
+
for (const kept of persisted) {
|
|
4303
|
+
if (missingNow.has(kept.key) && missingBefore.has(kept.key)) continue;
|
|
4304
|
+
out.set(kept.stableIdSuffix, kept);
|
|
4305
|
+
suffixByKey.set(kept.key, kept);
|
|
4306
|
+
}
|
|
4307
|
+
for (const binding of derived) {
|
|
4308
|
+
const existing = suffixByKey.get(binding.key);
|
|
4309
|
+
const merged = existing === void 0 ? binding : {
|
|
4310
|
+
...binding,
|
|
4311
|
+
stableIdSuffix: existing.stableIdSuffix,
|
|
4312
|
+
name: existing.name
|
|
4313
|
+
};
|
|
4314
|
+
out.set(merged.stableIdSuffix, merged);
|
|
4315
|
+
}
|
|
3954
4316
|
return [...out.values()].toSorted((a, b) => a.stableIdSuffix.localeCompare(b.stableIdSuffix));
|
|
3955
4317
|
}
|
|
3956
|
-
/**
|
|
3957
|
-
* Keeps each adopted appliance's child devices equal to what its description declares.
|
|
3958
|
-
* Runs on the addon, outside the container (rebuilding a container from inside itself
|
|
3959
|
-
* would tear down the code that asked for it).
|
|
3960
|
-
*/
|
|
3961
4318
|
var HomeConnectChildReconciler = class {
|
|
3962
4319
|
#deps;
|
|
3963
4320
|
#tracked = /* @__PURE__ */ new Map();
|
|
@@ -3970,6 +4327,8 @@ var HomeConnectChildReconciler = class {
|
|
|
3970
4327
|
const entry = {
|
|
3971
4328
|
a: appliance,
|
|
3972
4329
|
timer: null,
|
|
4330
|
+
missingBefore: /* @__PURE__ */ new Set(),
|
|
4331
|
+
waiting: null,
|
|
3973
4332
|
unwatch: this.#deps.watch(appliance.brokerId, appliance.haId, () => this.schedule(appliance.deviceId))
|
|
3974
4333
|
};
|
|
3975
4334
|
this.#tracked.set(appliance.deviceId, entry);
|
|
@@ -4005,8 +4364,18 @@ var HomeConnectChildReconciler = class {
|
|
|
4005
4364
|
const { brokerId, haId } = entry.a;
|
|
4006
4365
|
const client = this.#deps.getClient(brokerId);
|
|
4007
4366
|
const description = client?.getDescription(haId) ?? null;
|
|
4008
|
-
|
|
4009
|
-
if (
|
|
4367
|
+
const waiting = client === null || client.getLinkState(haId) !== "connected" ? "link-down" : description === null || description.state === "missing" ? "description-missing" : null;
|
|
4368
|
+
if (waiting !== entry.waiting) {
|
|
4369
|
+
entry.waiting = waiting;
|
|
4370
|
+
if (waiting !== null) this.#deps.logger.info("Home Connect child reconcile waiting", {
|
|
4371
|
+
tags: { deviceId },
|
|
4372
|
+
meta: {
|
|
4373
|
+
haId,
|
|
4374
|
+
reason: waiting
|
|
4375
|
+
}
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
if (client === null || description === null || waiting !== null) return;
|
|
4010
4379
|
try {
|
|
4011
4380
|
const { bindings: persisted, malformed } = await this.#deps.readPersisted(deviceId);
|
|
4012
4381
|
if (malformed > 0) {
|
|
@@ -4019,7 +4388,13 @@ var HomeConnectChildReconciler = class {
|
|
|
4019
4388
|
const merged = mergeChildBindings(persisted, deriveChildBindings({
|
|
4020
4389
|
description,
|
|
4021
4390
|
features: client.getFeatures(haId)
|
|
4022
|
-
}), description);
|
|
4391
|
+
}), description, entry.missingBefore);
|
|
4392
|
+
const missingNow = keysMissingFromDescription(merged, description);
|
|
4393
|
+
if (missingNow.size > 0 && missingNow.size !== entry.missingBefore.size) this.#deps.logger.info("Home Connect children kept until a second read agrees they left", {
|
|
4394
|
+
tags: { deviceId },
|
|
4395
|
+
meta: { missing: [...missingNow] }
|
|
4396
|
+
});
|
|
4397
|
+
entry.missingBefore = missingNow;
|
|
4023
4398
|
if (JSON.stringify(merged) === JSON.stringify(sorted(persisted))) return;
|
|
4024
4399
|
await this.#deps.applyEntities(deviceId, merged);
|
|
4025
4400
|
this.#deps.logger.info("Home Connect children re-derived from the appliance description", {
|
|
@@ -4041,6 +4416,19 @@ function sorted(list) {
|
|
|
4041
4416
|
return list.toSorted((a, b) => a.stableIdSuffix.localeCompare(b.stableIdSuffix));
|
|
4042
4417
|
}
|
|
4043
4418
|
/**
|
|
4419
|
+
* A container's persisted bindings from its config. One saved before the derived model has no
|
|
4420
|
+
* `entities`, only the legacy `children` the device publishes today: those ARE its bindings,
|
|
4421
|
+
* so the first derived write keeps them (same suffixes, same device ids) instead of reading
|
|
4422
|
+
* "no bindings" and rebuilding every child.
|
|
4423
|
+
*/
|
|
4424
|
+
function persistedChildBindingsFromConfig(cfg) {
|
|
4425
|
+
if (cfg["entities"] === void 0) return {
|
|
4426
|
+
bindings: legacyChildBindings(cfg["children"]),
|
|
4427
|
+
malformed: 0
|
|
4428
|
+
};
|
|
4429
|
+
return parsePersistedBindings(cfg["entities"]);
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4044
4432
|
* Parse a container's persisted `entities`. A row that does not parse is COUNTED, not dropped
|
|
4045
4433
|
* silently: the caller must not write a list that would lose it.
|
|
4046
4434
|
*/
|
|
@@ -4702,14 +5090,14 @@ var BoschHomeConnectProviderAddon = class extends require_dist.BaseDeviceProvide
|
|
|
4702
5090
|
} });
|
|
4703
5091
|
this.clients.setOnConnected((integrationId) => {
|
|
4704
5092
|
this.setIntegrationDevicesOnline(integrationId, true);
|
|
4705
|
-
this.trackAppliances();
|
|
5093
|
+
this.trackAppliances("connected");
|
|
4706
5094
|
});
|
|
4707
5095
|
this.clients.setOnDisconnected((integrationId) => {
|
|
4708
5096
|
this.setIntegrationDevicesOnline(integrationId, false);
|
|
4709
5097
|
});
|
|
4710
5098
|
this.reconciler = this.buildChildReconciler();
|
|
4711
5099
|
await this.reconcileIntegrations();
|
|
4712
|
-
await this.trackAppliances();
|
|
5100
|
+
await this.trackAppliances("init");
|
|
4713
5101
|
this.subscribeIntegrationLifecycle();
|
|
4714
5102
|
return {
|
|
4715
5103
|
providers: [
|
|
@@ -4784,30 +5172,13 @@ var BoschHomeConnectProviderAddon = class extends require_dist.BaseDeviceProvide
|
|
|
4784
5172
|
if (type === require_dist.DeviceType.Container) return `home-connect:${integrationId}:${haId}:appliance`;
|
|
4785
5173
|
return `home-connect:${integrationId}:${haId}`;
|
|
4786
5174
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
return;
|
|
4795
|
-
}
|
|
4796
|
-
const merged = homeConnectSettingsForPersist({
|
|
4797
|
-
...settings,
|
|
4798
|
-
appliances: mergeAppliancesForPersist(settings["appliances"], connection.appliances)
|
|
4799
|
-
});
|
|
4800
|
-
await this.ctx.api.integrations.setSettings.mutate({
|
|
4801
|
-
id: integrationId,
|
|
4802
|
-
settings: merged,
|
|
4803
|
-
secretKeys: [...HOME_CONNECT_SECRET_KEYS]
|
|
4804
|
-
});
|
|
4805
|
-
} catch (err) {
|
|
4806
|
-
this.ctx.logger.warn("Home Connect failed to persist enriched appliance IDDF", {
|
|
4807
|
-
tags: { integrationId },
|
|
4808
|
-
meta: { error: require_dist.errMsg(err) }
|
|
4809
|
-
});
|
|
4810
|
-
}
|
|
5175
|
+
/** Enriched IDDF maps and a rotated token pair, in one keychain-aware write. */
|
|
5176
|
+
persistIntegrationAppliances(integrationId, connection) {
|
|
5177
|
+
return buildHomeConnectConnectionPersister({
|
|
5178
|
+
getSettings: (id) => this.ctx.api.integrations.getSettings.query({ id }),
|
|
5179
|
+
setSettings: (input) => this.ctx.api.integrations.setSettings.mutate(input),
|
|
5180
|
+
logger: this.ctx.logger
|
|
5181
|
+
})(integrationId, connection);
|
|
4811
5182
|
}
|
|
4812
5183
|
async reconcileIntegrations() {
|
|
4813
5184
|
const reg = this.requireClients();
|
|
@@ -4913,7 +5284,7 @@ var BoschHomeConnectProviderAddon = class extends require_dist.BaseDeviceProvide
|
|
|
4913
5284
|
name,
|
|
4914
5285
|
integrationId
|
|
4915
5286
|
});
|
|
4916
|
-
this.trackAppliances();
|
|
5287
|
+
this.trackAppliances("adopt");
|
|
4917
5288
|
return { deviceId: device.id };
|
|
4918
5289
|
},
|
|
4919
5290
|
removeDevice: async (id) => {
|
|
@@ -4947,31 +5318,32 @@ var BoschHomeConnectProviderAddon = class extends require_dist.BaseDeviceProvide
|
|
|
4947
5318
|
watch: (integrationId, haId, listener) => homeConnectConnections.watch(integrationId, haId, listener),
|
|
4948
5319
|
getClient: (integrationId) => homeConnectConnections.getClient(integrationId),
|
|
4949
5320
|
readPersisted: async (deviceId) => {
|
|
4950
|
-
return
|
|
5321
|
+
return persistedChildBindingsFromConfig(await devices().loadConfig(deviceId));
|
|
4951
5322
|
},
|
|
4952
5323
|
applyEntities: (deviceId, entities) => devices().applyDerivedSpec(deviceId, { config: { entities: [...entities] } }),
|
|
4953
5324
|
logger: this.ctx.logger
|
|
4954
5325
|
});
|
|
4955
5326
|
}
|
|
5327
|
+
/**
|
|
5328
|
+
* The runner restores devices only AFTER `onInitialize` returned, so the init pass of
|
|
5329
|
+
* {@link trackAppliances} sees none: watch the restored containers here, or a description
|
|
5330
|
+
* that loads later never derives their children.
|
|
5331
|
+
*/
|
|
5332
|
+
async restoreDevices(savedDevices) {
|
|
5333
|
+
await super.restoreDevices(savedDevices);
|
|
5334
|
+
await this.trackAppliances("restore");
|
|
5335
|
+
}
|
|
4956
5336
|
/** Keep a reconciler watch on every adopted appliance container. */
|
|
4957
|
-
async trackAppliances() {
|
|
4958
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
5337
|
+
async trackAppliances(trigger) {
|
|
4959
5338
|
const devices = this.ctx.kernel.devices;
|
|
4960
5339
|
const reconciler = this.reconciler;
|
|
4961
|
-
if (!
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
if (typeof haId !== "string") continue;
|
|
4969
|
-
reconciler.track({
|
|
4970
|
-
deviceId: d.id,
|
|
4971
|
-
brokerId,
|
|
4972
|
-
haId
|
|
4973
|
-
});
|
|
4974
|
-
}
|
|
5340
|
+
if (!reconciler) return;
|
|
5341
|
+
await trackHomeConnectContainers({
|
|
5342
|
+
listDevices: () => devices.getAll(),
|
|
5343
|
+
loadConfig: (id) => devices.loadConfig(id),
|
|
5344
|
+
reconciler,
|
|
5345
|
+
logger: this.ctx.logger
|
|
5346
|
+
}, trigger);
|
|
4975
5347
|
}
|
|
4976
5348
|
globalSettingsSchema() {
|
|
4977
5349
|
return buildHomeConnectFormSchema();
|