@camstack/addon-provider-bosch 0.1.3 → 0.1.5
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-BhjIPfv5.js +119 -0
- package/dist/account-integration-offline-DW2D0vf8.mjs +102 -0
- package/dist/{dist-B-Un7bqT.js → dist-CIIror0W.js} +390 -294
- package/dist/{dist-BiZi19Lw.mjs → dist-DNznrlG_.mjs} +390 -294
- package/dist/home-connect/addon.js +50 -21
- package/dist/home-connect/addon.mjs +50 -21
- package/dist/homecom/addon.js +546 -123
- package/dist/homecom/addon.mjs +546 -123
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +27 -1
- package/i18n/it.json +27 -1
- package/package.json +1 -1
- package/dist/account-integration-offline-BBqEtCgz.js +0 -50
- package/dist/account-integration-offline-BfoLYzxz.mjs +0 -39
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,5 +1,387 @@
|
|
|
1
|
-
import { A as object, E as _enum, F as EventCategory, M as string, O as literal, _ as waterHeaterCapability, b as errMsg, c as deviceAdoptionCapability, n as BaseDeviceProvider, o as climateControlCapability, s as connectionTestCapability, t as BaseDevice, y as DeviceType } from "../dist-
|
|
2
|
-
import { n as cascadeRemoveDevicesForIntegration, t as setIntegrationDevicesOnline } from "../account-integration-offline-
|
|
1
|
+
import { A as object, C as defineCustomActions, E as _enum, F as EventCategory, M as string, O as literal, S as customAction, T as isSecretMarkerLike, _ as waterHeaterCapability, b as errMsg, c as deviceAdoptionCapability, n as BaseDeviceProvider, o as climateControlCapability, s as connectionTestCapability, t as BaseDevice, w as isSecretMarker, y as DeviceType } from "../dist-DNznrlG_.mjs";
|
|
2
|
+
import { n as cascadeRemoveDevicesForIntegration, r as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-DW2D0vf8.mjs";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
//#region src/homecom/custom-actions.ts
|
|
5
|
+
var homeComActions = defineCustomActions({
|
|
6
|
+
beginSignIn: customAction(object({}), object({
|
|
7
|
+
authorizeUrl: string().min(1),
|
|
8
|
+
verifier: string().min(1),
|
|
9
|
+
state: string().min(1),
|
|
10
|
+
summary: string()
|
|
11
|
+
}), {
|
|
12
|
+
kind: "mutation",
|
|
13
|
+
auth: "admin"
|
|
14
|
+
}),
|
|
15
|
+
finishSignIn: customAction(object({
|
|
16
|
+
verifier: string().min(1),
|
|
17
|
+
callback: string().min(1),
|
|
18
|
+
state: string().min(1)
|
|
19
|
+
}), object({
|
|
20
|
+
token: string().min(1).meta({ secret: true }),
|
|
21
|
+
refresh: string().min(1).meta({ secret: true }),
|
|
22
|
+
username: string().meta({ secret: true }),
|
|
23
|
+
summary: string()
|
|
24
|
+
}), {
|
|
25
|
+
kind: "mutation",
|
|
26
|
+
auth: "admin"
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/homecom/homecom-oauth.ts
|
|
31
|
+
/**
|
|
32
|
+
* Public OAuth client of the HomeCom Easy / PointT mobile app, signed in through SingleKey ID.
|
|
33
|
+
*
|
|
34
|
+
* These are the PUBLIC values the app itself ships (a PKCE client has no secret). They are not
|
|
35
|
+
* derivable from this repository; they come from the community PointT client (`homecom_alt`) and
|
|
36
|
+
* have to be confirmed against a live account — see docs/addons/addon-provider-bosch-homecom.md.
|
|
37
|
+
*/
|
|
38
|
+
var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
|
|
39
|
+
var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
|
|
40
|
+
var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
|
|
41
|
+
var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
|
|
42
|
+
var HOMECOM_SCOPE = [
|
|
43
|
+
"openid",
|
|
44
|
+
"email",
|
|
45
|
+
"profile",
|
|
46
|
+
"offline_access",
|
|
47
|
+
"pointt.gateway.claiming",
|
|
48
|
+
"pointt.gateway.removal",
|
|
49
|
+
"pointt.gateway.list",
|
|
50
|
+
"pointt.gateway.users",
|
|
51
|
+
"pointt.gateway.resource.dopair",
|
|
52
|
+
"pointt.castt.flow.token-exchange",
|
|
53
|
+
"bacon",
|
|
54
|
+
"hcc.tariff.read"
|
|
55
|
+
].join(" ");
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/homecom/homecom-login.ts
|
|
58
|
+
var MESSAGES = {
|
|
59
|
+
empty: "Nothing was pasted. Paste the address the browser tried to open after you signed in.",
|
|
60
|
+
"no-code": "No sign-in code was found in what you pasted. Copy the whole address that starts with com.bosch.tt.dashtt.pointt://, or just the code.",
|
|
61
|
+
"state-missing": "The pasted address has no state. Copy the complete address, or create the login link again.",
|
|
62
|
+
"state-mismatch": "This address belongs to a different sign-in attempt. Create the login link again and use the address that follows it.",
|
|
63
|
+
denied: "Bosch refused the sign-in. Create the login link again and approve the access.",
|
|
64
|
+
"code-rejected": "The code was already used or has expired. Create the login link again; a code works once and only for a few minutes.",
|
|
65
|
+
rejected: "Bosch did not accept the sign-in. Create the login link again.",
|
|
66
|
+
"no-refresh": "Bosch signed you in but returned no long-lived token. Create the login link again."
|
|
67
|
+
};
|
|
68
|
+
/** A failure the operator can act on; `reason` is stable, the message is for the form. */
|
|
69
|
+
var HomeComLoginError = class extends Error {
|
|
70
|
+
name = "HomeComLoginError";
|
|
71
|
+
reason;
|
|
72
|
+
constructor(reason) {
|
|
73
|
+
super(MESSAGES[reason]);
|
|
74
|
+
this.reason = reason;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function b64url(bytes) {
|
|
78
|
+
return bytes.toString("base64url");
|
|
79
|
+
}
|
|
80
|
+
function beginHomeComSignIn() {
|
|
81
|
+
const verifier = b64url(randomBytes(32));
|
|
82
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
83
|
+
const state = b64url(randomBytes(16));
|
|
84
|
+
const url = new URL(HOMECOM_AUTHORIZE_URL);
|
|
85
|
+
url.searchParams.set("client_id", HOMECOM_CLIENT_ID);
|
|
86
|
+
url.searchParams.set("redirect_uri", HOMECOM_REDIRECT_URI);
|
|
87
|
+
url.searchParams.set("response_type", "code");
|
|
88
|
+
url.searchParams.set("scope", HOMECOM_SCOPE);
|
|
89
|
+
url.searchParams.set("code_challenge", challenge);
|
|
90
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
91
|
+
url.searchParams.set("state", state);
|
|
92
|
+
url.searchParams.set("nonce", b64url(randomBytes(16)));
|
|
93
|
+
url.searchParams.set("prompt", "login");
|
|
94
|
+
return {
|
|
95
|
+
authorizeUrl: url.toString(),
|
|
96
|
+
verifier,
|
|
97
|
+
state
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The sign-in code from whatever the operator pasted: the full app address, an address copied
|
|
102
|
+
* from the browser's developer tools, a bare `code=…&state=…` query, or the bare code.
|
|
103
|
+
*/
|
|
104
|
+
function parseHomeComCallback(input, expectedState) {
|
|
105
|
+
const parsed = parseOAuthCallback(input, expectedState);
|
|
106
|
+
if (!parsed.ok) throw new HomeComLoginError(parsed.reason);
|
|
107
|
+
return parsed.code;
|
|
108
|
+
}
|
|
109
|
+
function isRecord$1(value) {
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
function emailFromIdToken(idToken) {
|
|
113
|
+
if (typeof idToken !== "string") return "";
|
|
114
|
+
const payload = idToken.split(".")[1];
|
|
115
|
+
if (!payload) return "";
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
118
|
+
return isRecord$1(parsed) && typeof parsed["email"] === "string" ? parsed["email"] : "";
|
|
119
|
+
} catch {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function completeHomeComSignIn(input, fetchFn = globalThis.fetch.bind(globalThis)) {
|
|
124
|
+
const code = parseHomeComCallback(input.callback, input.state);
|
|
125
|
+
const res = await fetchFn(HOMECOM_TOKEN_URL, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
128
|
+
body: new URLSearchParams({
|
|
129
|
+
grant_type: "authorization_code",
|
|
130
|
+
client_id: HOMECOM_CLIENT_ID,
|
|
131
|
+
code,
|
|
132
|
+
code_verifier: input.verifier,
|
|
133
|
+
redirect_uri: HOMECOM_REDIRECT_URI
|
|
134
|
+
}).toString()
|
|
135
|
+
});
|
|
136
|
+
const body = await res.json().catch(() => null);
|
|
137
|
+
if (!res.ok) throw new HomeComLoginError((isRecord$1(body) ? body["error"] : void 0) === "invalid_grant" ? "code-rejected" : "rejected");
|
|
138
|
+
if (!isRecord$1(body) || typeof body["access_token"] !== "string" || body["access_token"] === "") throw new HomeComLoginError("rejected");
|
|
139
|
+
const refresh = body["refresh_token"];
|
|
140
|
+
if (typeof refresh !== "string" || refresh === "") throw new HomeComLoginError("no-refresh");
|
|
141
|
+
return {
|
|
142
|
+
token: body["access_token"],
|
|
143
|
+
refresh,
|
|
144
|
+
username: emailFromIdToken(body["id_token"])
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/homecom/config.ts
|
|
149
|
+
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
150
|
+
var HomeComBrandSchema = _enum(HOMECOM_BRANDS);
|
|
151
|
+
var HOMECOM_REGIONS = ["eu", "us"];
|
|
152
|
+
var HomeComRegionSchema = _enum(HOMECOM_REGIONS);
|
|
153
|
+
var homeComConfigSchema = object({
|
|
154
|
+
/** Account email; filled by the sign-in, informational only. */
|
|
155
|
+
username: string().optional().describe("HomeCom / PointT account email").meta({ secret: true }),
|
|
156
|
+
token: string().min(1).describe("OAuth access token").meta({ secret: true }),
|
|
157
|
+
refresh: string().min(1).describe("OAuth refresh token").meta({ secret: true }),
|
|
158
|
+
brand: HomeComBrandSchema.default("bosch"),
|
|
159
|
+
region: HomeComRegionSchema.default("eu"),
|
|
160
|
+
/** The gateway this integration controls; chosen once from the account's gateways. */
|
|
161
|
+
gatewayId: string().min(1).optional()
|
|
162
|
+
});
|
|
163
|
+
function connectionFromSettings(settings) {
|
|
164
|
+
const parsed = homeComConfigSchema.safeParse(settings);
|
|
165
|
+
return parsed.success ? parsed.data : null;
|
|
166
|
+
}
|
|
167
|
+
function sameHomeComConnection(a, b) {
|
|
168
|
+
if (a === null || b === null) return false;
|
|
169
|
+
return a.username === b.username && a.token === b.token && a.refresh === b.refresh && a.brand === b.brand && a.region === b.region && a.gatewayId === b.gatewayId;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* PointT REST origin (matches homecom_alt / HomeCom Easy). US and EU accounts use
|
|
173
|
+
* the same host; `region` is retained for Bacon MQTT (`euc1` / `use1`) when wired.
|
|
174
|
+
*/
|
|
175
|
+
var HOMECOM_POINTT_API_ORIGIN = "https://pointt-api.bosch-thermotechnology.com";
|
|
176
|
+
function homeComApiBase(_region) {
|
|
177
|
+
return HOMECOM_POINTT_API_ORIGIN;
|
|
178
|
+
}
|
|
179
|
+
var HOMECOM_SECRET_KEYS = [
|
|
180
|
+
"username",
|
|
181
|
+
"token",
|
|
182
|
+
"refresh"
|
|
183
|
+
];
|
|
184
|
+
function buildHomeComFormSchema() {
|
|
185
|
+
return { sections: [{
|
|
186
|
+
id: "account",
|
|
187
|
+
title: "HomeCom account",
|
|
188
|
+
titleKey: "provider-bosch:homecom.s_account.title",
|
|
189
|
+
description: "Create the login link and open it in a browser, then sign in with your SingleKey ID. The browser cannot open the app and stops on an address starting with com.bosch.tt.dashtt.pointt://app/login?code=. Paste that whole address below (or only the code) and press Complete sign-in. The address is shown in Chrome and Edge under DevTools (F12) > Network, with Preserve log ticked before you sign in: select the last redirect and copy its Location header. In Firefox use Network > Headers > Location; in Safari use Develop > Show Web Inspector > Network. A code works once and expires within minutes. Tokens can also be pasted by hand in the fields below.",
|
|
190
|
+
descriptionKey: "provider-bosch:homecom.s_account.description",
|
|
191
|
+
columns: 1,
|
|
192
|
+
fields: [
|
|
193
|
+
{
|
|
194
|
+
type: "addon-action-button",
|
|
195
|
+
addonId: "bosch-homecom",
|
|
196
|
+
key: "__homecom-begin",
|
|
197
|
+
label: "HomeCom",
|
|
198
|
+
labelKey: "provider-bosch:homecom.s_account.__homecom-begin.label",
|
|
199
|
+
buttonLabel: "Create login link",
|
|
200
|
+
action: "beginSignIn",
|
|
201
|
+
variant: "primary",
|
|
202
|
+
assignResult: {
|
|
203
|
+
authorizeUrl: "authorizeUrl",
|
|
204
|
+
verifier: "verifier",
|
|
205
|
+
state: "state"
|
|
206
|
+
},
|
|
207
|
+
successMessage: "Open the link in a browser and sign in."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: "textarea",
|
|
211
|
+
key: "authorizeUrl",
|
|
212
|
+
label: "Login link",
|
|
213
|
+
labelKey: "provider-bosch:homecom.s_account.authorizeUrl.label",
|
|
214
|
+
required: false,
|
|
215
|
+
omitFromSettings: true
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "textarea",
|
|
219
|
+
key: "callback",
|
|
220
|
+
label: "Redirect address or code",
|
|
221
|
+
labelKey: "provider-bosch:homecom.s_account.callback.label",
|
|
222
|
+
required: false,
|
|
223
|
+
placeholder: "com.bosch.tt.dashtt.pointt://app/login?code=…&state=…",
|
|
224
|
+
omitFromSettings: true
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: "password",
|
|
228
|
+
key: "state",
|
|
229
|
+
label: "Login state",
|
|
230
|
+
labelKey: "provider-bosch:homecom.s_account.state.label",
|
|
231
|
+
required: false,
|
|
232
|
+
omitFromSettings: true
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: "password",
|
|
236
|
+
key: "verifier",
|
|
237
|
+
label: "Login verifier",
|
|
238
|
+
labelKey: "provider-bosch:homecom.s_account.verifier.label",
|
|
239
|
+
required: false,
|
|
240
|
+
omitFromSettings: true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
type: "addon-action-button",
|
|
244
|
+
addonId: "bosch-homecom",
|
|
245
|
+
key: "__homecom-finish",
|
|
246
|
+
label: "Finish",
|
|
247
|
+
labelKey: "provider-bosch:homecom.s_account.__homecom-finish.label",
|
|
248
|
+
buttonLabel: "Complete sign-in",
|
|
249
|
+
action: "finishSignIn",
|
|
250
|
+
variant: "primary",
|
|
251
|
+
paramsFromForm: {
|
|
252
|
+
verifier: "verifier",
|
|
253
|
+
callback: "callback",
|
|
254
|
+
state: "state"
|
|
255
|
+
},
|
|
256
|
+
assignResult: {
|
|
257
|
+
token: "token",
|
|
258
|
+
refresh: "refresh",
|
|
259
|
+
username: "username"
|
|
260
|
+
},
|
|
261
|
+
successMessage: "{summary}"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
type: "text",
|
|
265
|
+
key: "username",
|
|
266
|
+
label: "Account",
|
|
267
|
+
labelKey: "provider-bosch:homecom.s_account.username.label",
|
|
268
|
+
required: false
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: "password",
|
|
272
|
+
key: "token",
|
|
273
|
+
label: "Access token (filled by sign-in, or paste one)",
|
|
274
|
+
labelKey: "provider-bosch:homecom.s_account.token.label",
|
|
275
|
+
required: true,
|
|
276
|
+
showToggle: true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "password",
|
|
280
|
+
key: "refresh",
|
|
281
|
+
label: "Refresh token (filled by sign-in, or paste one)",
|
|
282
|
+
labelKey: "provider-bosch:homecom.s_account.refresh.label",
|
|
283
|
+
required: true,
|
|
284
|
+
showToggle: true
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
key: "gatewayId",
|
|
289
|
+
label: "Gateway id",
|
|
290
|
+
labelKey: "provider-bosch:homecom.s_account.gatewayId.label",
|
|
291
|
+
description: "Leave empty to use the first gateway of the account; the chosen one is stored here. For a second gateway add another HomeCom account entry and enter its id.",
|
|
292
|
+
descriptionKey: "provider-bosch:homecom.s_account.gatewayId.description",
|
|
293
|
+
required: false
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: "select",
|
|
297
|
+
key: "brand",
|
|
298
|
+
label: "Brand",
|
|
299
|
+
labelKey: "provider-bosch:homecom.s_account.brand.label",
|
|
300
|
+
default: "bosch",
|
|
301
|
+
options: HOMECOM_BRANDS.map((b) => ({
|
|
302
|
+
value: b,
|
|
303
|
+
label: b
|
|
304
|
+
}))
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: "select",
|
|
308
|
+
key: "region",
|
|
309
|
+
label: "Region",
|
|
310
|
+
labelKey: "provider-bosch:homecom.s_account.region.label",
|
|
311
|
+
default: "eu",
|
|
312
|
+
options: HOMECOM_REGIONS.map((r) => ({
|
|
313
|
+
value: r,
|
|
314
|
+
label: r.toUpperCase()
|
|
315
|
+
}))
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
}] };
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/homecom/homecom-token-persist.ts
|
|
322
|
+
function unreadable(value) {
|
|
323
|
+
return isSecretMarker(value) || isSecretMarkerLike(value);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Merge `patch` into the integration's stored settings in ONE `setSettings` write, one at a
|
|
327
|
+
* time per integration. Refuses (and says so, without values) when a secret the write
|
|
328
|
+
* touches cannot be read back from the keychain: the hub refuses an overwrite of a secret the
|
|
329
|
+
* addon could not read, so the write would only fail.
|
|
330
|
+
*/
|
|
331
|
+
function buildSerialPatcher(deps) {
|
|
332
|
+
const tails = /* @__PURE__ */ new Map();
|
|
333
|
+
return (integrationId, patch) => {
|
|
334
|
+
const run = async () => {
|
|
335
|
+
try {
|
|
336
|
+
const stored = await deps.getSettings(integrationId);
|
|
337
|
+
const locked = HOMECOM_SECRET_KEYS.filter((key) => unreadable(stored[key]));
|
|
338
|
+
if (locked.length > 0) {
|
|
339
|
+
deps.logger.warn(`HomeCom not persisting ${deps.what} — stored secrets are locked`, {
|
|
340
|
+
tags: { integrationId },
|
|
341
|
+
meta: { locked }
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
await deps.setSettings({
|
|
346
|
+
id: integrationId,
|
|
347
|
+
settings: {
|
|
348
|
+
...stored,
|
|
349
|
+
...patch
|
|
350
|
+
},
|
|
351
|
+
secretKeys: [...HOMECOM_SECRET_KEYS]
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
deps.logger.warn(`HomeCom failed to persist ${deps.what}`, {
|
|
355
|
+
tags: { integrationId },
|
|
356
|
+
meta: { error: errMsg(err) }
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const next = (tails.get(integrationId) ?? Promise.resolve()).then(run);
|
|
361
|
+
tails.set(integrationId, next);
|
|
362
|
+
return next;
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** Persist a rotated token pair; never throws, never logs a value. */
|
|
366
|
+
function buildHomeComTokenPersister(deps) {
|
|
367
|
+
const patch = buildSerialPatcher({
|
|
368
|
+
...deps,
|
|
369
|
+
what: "rotated tokens"
|
|
370
|
+
});
|
|
371
|
+
return (integrationId, tokens) => patch(integrationId, {
|
|
372
|
+
token: tokens.token,
|
|
373
|
+
refresh: tokens.refresh
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** Persist the gateway this integration settled on. */
|
|
377
|
+
function buildHomeComGatewayPersister(deps) {
|
|
378
|
+
const patch = buildSerialPatcher({
|
|
379
|
+
...deps,
|
|
380
|
+
what: "chosen gateway"
|
|
381
|
+
});
|
|
382
|
+
return (integrationId, gatewayId) => patch(integrationId, { gatewayId });
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
3
385
|
//#region src/homecom/homecom-discovery.ts
|
|
4
386
|
function buildHomeComCandidates(input) {
|
|
5
387
|
const out = [];
|
|
@@ -153,84 +535,6 @@ function buildHomeComAdoptionProvider(deps) {
|
|
|
153
535
|
};
|
|
154
536
|
}
|
|
155
537
|
//#endregion
|
|
156
|
-
//#region src/homecom/config.ts
|
|
157
|
-
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
158
|
-
var HomeComBrandSchema = _enum(HOMECOM_BRANDS);
|
|
159
|
-
var HOMECOM_REGIONS = ["eu", "us"];
|
|
160
|
-
var HomeComRegionSchema = _enum(HOMECOM_REGIONS);
|
|
161
|
-
var homeComConfigSchema = object({
|
|
162
|
-
username: string().min(1).describe("HomeCom / PointT account email").meta({ secret: true }),
|
|
163
|
-
token: string().min(1).describe("OAuth access token").meta({ secret: true }),
|
|
164
|
-
refresh: string().min(1).describe("OAuth refresh token").meta({ secret: true }),
|
|
165
|
-
brand: HomeComBrandSchema.default("bosch"),
|
|
166
|
-
region: HomeComRegionSchema.default("eu")
|
|
167
|
-
});
|
|
168
|
-
function connectionFromSettings(settings) {
|
|
169
|
-
const parsed = homeComConfigSchema.safeParse(settings);
|
|
170
|
-
return parsed.success ? parsed.data : null;
|
|
171
|
-
}
|
|
172
|
-
function sameHomeComConnection(a, b) {
|
|
173
|
-
if (a === null || b === null) return false;
|
|
174
|
-
return a.username === b.username && a.token === b.token && a.refresh === b.refresh && a.brand === b.brand && a.region === b.region;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* PointT REST origin (matches homecom_alt / HomeCom Easy). US and EU accounts use
|
|
178
|
-
* the same host; `region` is retained for Bacon MQTT (`euc1` / `use1`) when wired.
|
|
179
|
-
*/
|
|
180
|
-
var HOMECOM_POINTT_API_ORIGIN = "https://pointt-api.bosch-thermotechnology.com";
|
|
181
|
-
function homeComApiBase(_region) {
|
|
182
|
-
return HOMECOM_POINTT_API_ORIGIN;
|
|
183
|
-
}
|
|
184
|
-
function buildHomeComFormSchema() {
|
|
185
|
-
return { sections: [{
|
|
186
|
-
id: "account",
|
|
187
|
-
title: "HomeCom account",
|
|
188
|
-
columns: 1,
|
|
189
|
-
fields: [
|
|
190
|
-
{
|
|
191
|
-
type: "text",
|
|
192
|
-
key: "username",
|
|
193
|
-
label: "Username",
|
|
194
|
-
required: true
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
type: "password",
|
|
198
|
-
key: "token",
|
|
199
|
-
label: "Access token",
|
|
200
|
-
required: true,
|
|
201
|
-
showToggle: true
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
type: "password",
|
|
205
|
-
key: "refresh",
|
|
206
|
-
label: "Refresh token",
|
|
207
|
-
required: true,
|
|
208
|
-
showToggle: true
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
type: "select",
|
|
212
|
-
key: "brand",
|
|
213
|
-
label: "Brand",
|
|
214
|
-
default: "bosch",
|
|
215
|
-
options: HOMECOM_BRANDS.map((b) => ({
|
|
216
|
-
value: b,
|
|
217
|
-
label: b
|
|
218
|
-
}))
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
type: "select",
|
|
222
|
-
key: "region",
|
|
223
|
-
label: "Region",
|
|
224
|
-
default: "eu",
|
|
225
|
-
options: HOMECOM_REGIONS.map((r) => ({
|
|
226
|
-
value: r,
|
|
227
|
-
label: r.toUpperCase()
|
|
228
|
-
}))
|
|
229
|
-
}
|
|
230
|
-
]
|
|
231
|
-
}] };
|
|
232
|
-
}
|
|
233
|
-
//#endregion
|
|
234
538
|
//#region src/homecom/homecom-gateway.ts
|
|
235
539
|
var HomeComConnectionResolver = class {
|
|
236
540
|
#surfaces = /* @__PURE__ */ new Map();
|
|
@@ -397,6 +701,38 @@ function readNum(raw) {
|
|
|
397
701
|
var HomeComAuthError = class extends Error {
|
|
398
702
|
name = "HomeComAuthError";
|
|
399
703
|
};
|
|
704
|
+
function isRecord(value) {
|
|
705
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
706
|
+
}
|
|
707
|
+
function rowId(row) {
|
|
708
|
+
for (const key of [
|
|
709
|
+
"deviceId",
|
|
710
|
+
"gatewayId",
|
|
711
|
+
"id"
|
|
712
|
+
]) {
|
|
713
|
+
const value = row[key];
|
|
714
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
715
|
+
if (typeof value === "number") return String(value);
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
/** Gateways from the account listing: a bare array or `{ gateways: [...] }`. */
|
|
720
|
+
function parseHomeComGatewayList(payload) {
|
|
721
|
+
const rows = isRecord(payload) ? payload["gateways"] : payload;
|
|
722
|
+
if (!Array.isArray(rows)) return [];
|
|
723
|
+
const out = [];
|
|
724
|
+
for (const row of rows) {
|
|
725
|
+
if (!isRecord(row)) continue;
|
|
726
|
+
const gatewayId = rowId(row);
|
|
727
|
+
if (gatewayId === null) continue;
|
|
728
|
+
const name = row["name"];
|
|
729
|
+
out.push({
|
|
730
|
+
gatewayId,
|
|
731
|
+
name: typeof name === "string" && name.length > 0 ? name : gatewayId
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
400
736
|
function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
401
737
|
const fetchFn = deps.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
402
738
|
let token = config.token;
|
|
@@ -421,10 +757,14 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
421
757
|
}
|
|
422
758
|
return {
|
|
423
759
|
async refreshTokens() {
|
|
424
|
-
const res = await fetchFn(
|
|
760
|
+
const res = await fetchFn(HOMECOM_TOKEN_URL, {
|
|
425
761
|
method: "POST",
|
|
426
762
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
427
|
-
body:
|
|
763
|
+
body: new URLSearchParams({
|
|
764
|
+
grant_type: "refresh_token",
|
|
765
|
+
refresh_token: refresh,
|
|
766
|
+
client_id: HOMECOM_CLIENT_ID
|
|
767
|
+
}).toString()
|
|
428
768
|
});
|
|
429
769
|
if (!res.ok) {
|
|
430
770
|
if (res.status === 401 || res.status === 403) throw new HomeComAuthError("refresh refused");
|
|
@@ -438,11 +778,18 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
438
778
|
if (typeof newToken !== "string" || typeof newRefresh !== "string") throw new Error("HomeCom token refresh: missing tokens");
|
|
439
779
|
token = newToken;
|
|
440
780
|
refresh = newRefresh;
|
|
781
|
+
await deps.onTokensRotated?.({
|
|
782
|
+
token,
|
|
783
|
+
refresh
|
|
784
|
+
});
|
|
441
785
|
return {
|
|
442
786
|
token,
|
|
443
787
|
refresh
|
|
444
788
|
};
|
|
445
789
|
},
|
|
790
|
+
async listGateways() {
|
|
791
|
+
return parseHomeComGatewayList(await request("/v2/gateways"));
|
|
792
|
+
},
|
|
446
793
|
async fetchSystem() {
|
|
447
794
|
const parsed = parseHomeComSystemPayload(await request(`/v2/gateways/${gatewayId}/system`));
|
|
448
795
|
return {
|
|
@@ -493,6 +840,8 @@ var HomeComIntegrationManager = class {
|
|
|
493
840
|
#surfaceSink;
|
|
494
841
|
#makeClient;
|
|
495
842
|
#pollIntervalMs;
|
|
843
|
+
#onGatewayChosen;
|
|
844
|
+
#gateways = [];
|
|
496
845
|
#client = null;
|
|
497
846
|
#gatewayId = null;
|
|
498
847
|
#pollTimer = null;
|
|
@@ -508,7 +857,26 @@ var HomeComIntegrationManager = class {
|
|
|
508
857
|
this.#onDisconnected = options.onDisconnected;
|
|
509
858
|
this.#surfaceSink = options.surfaceSink;
|
|
510
859
|
const fetchFn = options.fetchFn;
|
|
511
|
-
this.#
|
|
860
|
+
this.#onGatewayChosen = options.onGatewayChosen ?? (() => void 0);
|
|
861
|
+
const onTokensRotated = async (tokens) => {
|
|
862
|
+
this.#connection = {
|
|
863
|
+
...this.#connection,
|
|
864
|
+
token: tokens.token,
|
|
865
|
+
refresh: tokens.refresh
|
|
866
|
+
};
|
|
867
|
+
try {
|
|
868
|
+
await options.onTokensRotated?.(tokens);
|
|
869
|
+
} catch (err) {
|
|
870
|
+
this.#logger.warn("HomeCom could not store the rotated tokens", {
|
|
871
|
+
tags: { integrationId: this.#id },
|
|
872
|
+
meta: { error: errMsg(err) }
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
this.#makeClient = options.makeClient ?? ((config, gatewayId) => createHomeComApiClient(config, gatewayId, {
|
|
877
|
+
fetchFn,
|
|
878
|
+
onTokensRotated
|
|
879
|
+
}));
|
|
512
880
|
this.#pollIntervalMs = options.pollIntervalMs ?? 6e4;
|
|
513
881
|
}
|
|
514
882
|
getConnection() {
|
|
@@ -521,8 +889,9 @@ var HomeComIntegrationManager = class {
|
|
|
521
889
|
kind: "homecom",
|
|
522
890
|
status: this.#status,
|
|
523
891
|
info: {
|
|
524
|
-
username: this.#connection.username,
|
|
525
|
-
gatewayId: this.#gatewayId
|
|
892
|
+
username: this.#connection.username ?? "",
|
|
893
|
+
gatewayId: this.#gatewayId,
|
|
894
|
+
gateways: this.#gateways
|
|
526
895
|
},
|
|
527
896
|
lastCheckedAt: this.#lastCheckedAt,
|
|
528
897
|
error: this.#error
|
|
@@ -532,14 +901,15 @@ var HomeComIntegrationManager = class {
|
|
|
532
901
|
if (this.#client !== null) return;
|
|
533
902
|
this.#status = "connecting";
|
|
534
903
|
this.#error = null;
|
|
535
|
-
|
|
536
|
-
this.#gatewayId = gatewayId;
|
|
537
|
-
const client = this.#makeClient(this.#connection, gatewayId);
|
|
538
|
-
this.#client = client;
|
|
904
|
+
let client = null;
|
|
539
905
|
try {
|
|
906
|
+
const gatewayId = await this.resolveGatewayId();
|
|
907
|
+
this.#gatewayId = gatewayId;
|
|
908
|
+
client = this.#makeClient(this.#connection, gatewayId);
|
|
909
|
+
this.#client = client;
|
|
540
910
|
const snapshot = await this.fetchWithTokenRefresh(client);
|
|
541
911
|
const surface = {
|
|
542
|
-
gatewayId,
|
|
912
|
+
gatewayId: this.#gatewayId ?? "",
|
|
543
913
|
client,
|
|
544
914
|
snapshot,
|
|
545
915
|
discoverables: buildDiscoverables(snapshot)
|
|
@@ -553,7 +923,8 @@ var HomeComIntegrationManager = class {
|
|
|
553
923
|
}, this.#pollIntervalMs);
|
|
554
924
|
} catch (err) {
|
|
555
925
|
this.#status = "error";
|
|
556
|
-
this.#error = err instanceof HomeComAuthError ? "authentication failed —
|
|
926
|
+
this.#error = err instanceof HomeComAuthError ? "authentication failed — sign in again" : errMsg(err);
|
|
927
|
+
this.#client = null;
|
|
557
928
|
this.#surfaceSink.set(this.#id, null);
|
|
558
929
|
this.#onDisconnected(this.#id);
|
|
559
930
|
throw err;
|
|
@@ -566,6 +937,7 @@ var HomeComIntegrationManager = class {
|
|
|
566
937
|
}
|
|
567
938
|
this.#client = null;
|
|
568
939
|
this.#gatewayId = null;
|
|
940
|
+
this.#gateways = [];
|
|
569
941
|
this.#status = "disconnected";
|
|
570
942
|
this.#surfaceSink.set(this.#id, null);
|
|
571
943
|
this.#onDisconnected(this.#id);
|
|
@@ -590,17 +962,34 @@ var HomeComIntegrationManager = class {
|
|
|
590
962
|
}
|
|
591
963
|
}
|
|
592
964
|
async fetchWithTokenRefresh(client) {
|
|
965
|
+
return this.withTokenRefresh(client, () => client.fetchSystem());
|
|
966
|
+
}
|
|
967
|
+
async withTokenRefresh(client, call) {
|
|
593
968
|
try {
|
|
594
|
-
return await
|
|
969
|
+
return await call();
|
|
595
970
|
} catch (err) {
|
|
596
971
|
if (!(err instanceof HomeComAuthError)) throw err;
|
|
597
972
|
await client.refreshTokens();
|
|
598
|
-
return
|
|
973
|
+
return call();
|
|
599
974
|
}
|
|
600
975
|
}
|
|
601
|
-
/**
|
|
602
|
-
|
|
603
|
-
|
|
976
|
+
/**
|
|
977
|
+
* The gateway to poll: the stored choice when the account still has it, otherwise the
|
|
978
|
+
* account's first gateway, which is then stored. A stored choice that vanished is an error —
|
|
979
|
+
* silently switching to another gateway would control a different house.
|
|
980
|
+
*/
|
|
981
|
+
async resolveGatewayId() {
|
|
982
|
+
const lister = this.#makeClient(this.#connection, "");
|
|
983
|
+
this.#gateways = await this.withTokenRefresh(lister, () => lister.listGateways());
|
|
984
|
+
const stored = this.#connection.gatewayId;
|
|
985
|
+
if (stored !== void 0) {
|
|
986
|
+
if (!this.#gateways.some((g) => g.gatewayId === stored)) throw new Error(`HomeCom gateway ${stored} is no longer on this account`);
|
|
987
|
+
return stored;
|
|
988
|
+
}
|
|
989
|
+
const first = this.#gateways[0];
|
|
990
|
+
if (first === void 0) throw new Error("HomeCom account has no gateway");
|
|
991
|
+
this.#onGatewayChosen(first.gatewayId);
|
|
992
|
+
return first.gatewayId;
|
|
604
993
|
}
|
|
605
994
|
};
|
|
606
995
|
//#endregion
|
|
@@ -610,11 +999,15 @@ var HomeComClientRegistry = class {
|
|
|
610
999
|
#onConnected;
|
|
611
1000
|
#onDisconnected;
|
|
612
1001
|
#makeManager;
|
|
1002
|
+
#onTokensRotated;
|
|
1003
|
+
#onGatewayChosen;
|
|
613
1004
|
#entries = /* @__PURE__ */ new Map();
|
|
614
1005
|
constructor(logger, deps = {}) {
|
|
615
1006
|
this.#logger = logger;
|
|
616
1007
|
this.#onConnected = deps.onConnected ?? (() => void 0);
|
|
617
1008
|
this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
|
|
1009
|
+
this.#onTokensRotated = deps.onTokensRotated ?? (async () => void 0);
|
|
1010
|
+
this.#onGatewayChosen = deps.onGatewayChosen ?? (() => void 0);
|
|
618
1011
|
const fetchFn = deps.fetchFn;
|
|
619
1012
|
const makeClient = deps.makeClient;
|
|
620
1013
|
this.#makeManager = deps.makeManager ?? ((opts) => new HomeComIntegrationManager({
|
|
@@ -624,6 +1017,8 @@ var HomeComClientRegistry = class {
|
|
|
624
1017
|
logger: opts.logger,
|
|
625
1018
|
onConnected: opts.onConnected,
|
|
626
1019
|
onDisconnected: opts.onDisconnected,
|
|
1020
|
+
onTokensRotated: opts.onTokensRotated,
|
|
1021
|
+
onGatewayChosen: opts.onGatewayChosen,
|
|
627
1022
|
surfaceSink: homeComConnections,
|
|
628
1023
|
fetchFn,
|
|
629
1024
|
makeClient,
|
|
@@ -639,7 +1034,7 @@ var HomeComClientRegistry = class {
|
|
|
639
1034
|
async upsert(integrationId, name, connection) {
|
|
640
1035
|
const existing = this.#entries.get(integrationId);
|
|
641
1036
|
if (existing) {
|
|
642
|
-
if (sameHomeComConnection(existing.connection, connection)) return;
|
|
1037
|
+
if (sameHomeComConnection(existing.connection, connection) || sameHomeComConnection(existing.manager.getConnection(), connection)) return;
|
|
643
1038
|
try {
|
|
644
1039
|
await existing.manager.applyConnection(connection);
|
|
645
1040
|
this.#entries.set(integrationId, {
|
|
@@ -660,7 +1055,9 @@ var HomeComClientRegistry = class {
|
|
|
660
1055
|
connection,
|
|
661
1056
|
logger: this.#logger,
|
|
662
1057
|
onConnected: (id) => this.#onConnected(id),
|
|
663
|
-
onDisconnected: (id) => this.#onDisconnected(id)
|
|
1058
|
+
onDisconnected: (id) => this.#onDisconnected(id),
|
|
1059
|
+
onTokensRotated: (tokens) => this.#onTokensRotated(integrationId, tokens),
|
|
1060
|
+
onGatewayChosen: (gatewayId) => this.#onGatewayChosen(integrationId, gatewayId)
|
|
664
1061
|
});
|
|
665
1062
|
this.#entries.set(integrationId, {
|
|
666
1063
|
manager,
|
|
@@ -718,14 +1115,10 @@ var HomeComClientRegistry = class {
|
|
|
718
1115
|
};
|
|
719
1116
|
//#endregion
|
|
720
1117
|
//#region src/homecom/homecom-connection-test.ts
|
|
721
|
-
|
|
722
|
-
return "gateway-k40";
|
|
723
|
-
}
|
|
724
|
-
var TEST_LABEL = "Refreshes HomeCom OAuth tokens and reads the heat-pump system snapshot";
|
|
1118
|
+
var TEST_LABEL = "Lists the account gateways and reads the heat-pump system snapshot";
|
|
725
1119
|
function buildHomeComConnectionTestProvider(deps) {
|
|
726
1120
|
const { logger } = deps;
|
|
727
1121
|
const makeClient = deps.makeClient ?? ((config, gatewayId) => createHomeComApiClient(config, gatewayId));
|
|
728
|
-
const resolveGatewayId = deps.resolveGatewayId ?? defaultResolveGatewayId;
|
|
729
1122
|
return {
|
|
730
1123
|
describeTest: async () => ({ label: TEST_LABEL }),
|
|
731
1124
|
testSettings: async ({ settings }) => {
|
|
@@ -735,11 +1128,18 @@ function buildHomeComConnectionTestProvider(deps) {
|
|
|
735
1128
|
error: `HomeCom settings incomplete: ${parsed.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}`
|
|
736
1129
|
};
|
|
737
1130
|
const startedAt = Date.now();
|
|
738
|
-
const gatewayId = await resolveGatewayId(parsed.data);
|
|
739
|
-
const client = makeClient(parsed.data, gatewayId);
|
|
740
1131
|
try {
|
|
741
|
-
await
|
|
742
|
-
|
|
1132
|
+
const gateways = await makeClient(parsed.data, "").listGateways();
|
|
1133
|
+
const gatewayId = parsed.data.gatewayId ?? gateways[0]?.gatewayId;
|
|
1134
|
+
if (gatewayId === void 0) return {
|
|
1135
|
+
outcome: "rejected",
|
|
1136
|
+
error: "This HomeCom account has no gateway."
|
|
1137
|
+
};
|
|
1138
|
+
if (!gateways.some((g) => g.gatewayId === gatewayId)) return {
|
|
1139
|
+
outcome: "rejected",
|
|
1140
|
+
error: `Gateway ${gatewayId} is not on this account.`
|
|
1141
|
+
};
|
|
1142
|
+
await makeClient(parsed.data, gatewayId).fetchSystem();
|
|
743
1143
|
return {
|
|
744
1144
|
outcome: "validated",
|
|
745
1145
|
latencyMs: Date.now() - startedAt
|
|
@@ -747,12 +1147,9 @@ function buildHomeComConnectionTestProvider(deps) {
|
|
|
747
1147
|
} catch (err) {
|
|
748
1148
|
if (err instanceof HomeComAuthError) return {
|
|
749
1149
|
outcome: "rejected",
|
|
750
|
-
error: "HomeCom refused
|
|
1150
|
+
error: "HomeCom refused the access token — sign in again to get a fresh one."
|
|
751
1151
|
};
|
|
752
|
-
logger.warn("HomeCom connection test inconclusive", { meta: {
|
|
753
|
-
username: parsed.data.username,
|
|
754
|
-
error: errMsg(err)
|
|
755
|
-
} });
|
|
1152
|
+
logger.warn("HomeCom connection test inconclusive", { meta: { error: errMsg(err) } });
|
|
756
1153
|
return {
|
|
757
1154
|
outcome: "inconclusive",
|
|
758
1155
|
error: "Could not reach the HomeCom cloud API."
|
|
@@ -958,7 +1355,17 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
958
1355
|
}
|
|
959
1356
|
async onInitialize() {
|
|
960
1357
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
961
|
-
|
|
1358
|
+
const persistDeps = {
|
|
1359
|
+
getSettings: (id) => this.ctx.api.integrations.getSettings.query({ id }),
|
|
1360
|
+
setSettings: (input) => this.ctx.api.integrations.setSettings.mutate(input),
|
|
1361
|
+
logger: this.ctx.logger
|
|
1362
|
+
};
|
|
1363
|
+
const persistTokens = buildHomeComTokenPersister(persistDeps);
|
|
1364
|
+
const persistGateway = buildHomeComGatewayPersister(persistDeps);
|
|
1365
|
+
this.clients = new HomeComClientRegistry(this.ctx.logger, {
|
|
1366
|
+
onTokensRotated: (integrationId, tokens) => persistTokens(integrationId, tokens),
|
|
1367
|
+
onGatewayChosen: (integrationId, gatewayId) => void persistGateway(integrationId, gatewayId)
|
|
1368
|
+
});
|
|
962
1369
|
this.clients.setOnConnected((integrationId) => {
|
|
963
1370
|
this.setIntegrationDevicesOnline(integrationId, true);
|
|
964
1371
|
});
|
|
@@ -967,17 +1374,33 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
967
1374
|
});
|
|
968
1375
|
await this.reconcileIntegrations();
|
|
969
1376
|
this.subscribeIntegrationLifecycle();
|
|
970
|
-
return
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1377
|
+
return {
|
|
1378
|
+
providers: [
|
|
1379
|
+
...regs,
|
|
1380
|
+
{
|
|
1381
|
+
capability: deviceAdoptionCapability,
|
|
1382
|
+
provider: this.buildAdoptionProvider()
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
capability: connectionTestCapability,
|
|
1386
|
+
provider: this.buildConnectionTestProvider()
|
|
1387
|
+
}
|
|
1388
|
+
],
|
|
1389
|
+
customActions: homeComActions,
|
|
1390
|
+
actionHandlers: {
|
|
1391
|
+
beginSignIn: async () => ({
|
|
1392
|
+
...beginHomeComSignIn(),
|
|
1393
|
+
summary: "Open the link in a browser and sign in."
|
|
1394
|
+
}),
|
|
1395
|
+
finishSignIn: async (input) => {
|
|
1396
|
+
const tokens = await completeHomeComSignIn(input);
|
|
1397
|
+
return {
|
|
1398
|
+
...tokens,
|
|
1399
|
+
summary: tokens.username.length > 0 ? `Signed in as ${tokens.username}. Save to connect.` : "Signed in. Save to connect."
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
979
1402
|
}
|
|
980
|
-
|
|
1403
|
+
};
|
|
981
1404
|
}
|
|
982
1405
|
async onShutdown() {
|
|
983
1406
|
try {
|