@camstack/addon-provider-bosch 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,210 @@
1
- import { C as customAction, D as _enum, E as isSecretMarkerLike, I as EventCategory, N as string, T as isSecretMarker, b as DeviceType, j as object, k as literal, l as deviceAdoptionCapability, n as BaseDeviceProvider, o as climateControlCapability, s as connectionTestCapability, t as BaseDevice, v as waterHeaterCapability, w as defineCustomActions, x as errMsg } from "../dist-B75tWQLF.mjs";
2
- import { n as cascadeRemoveDevicesForIntegration, r as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-DvuqXgc8.mjs";
1
+ import { A as array, C as errMsg, D as isSecretMarker, E as defineCustomActions, F as string, M as number, N as object, O as isSecretMarkerLike, R as EventCategory, S as DeviceType, T as customAction, b as waterHeaterCapability, j as literal, k as _enum, l as deviceAdoptionCapability, n as BaseDeviceProvider, o as climateControlCapability, s as connectionTestCapability, t as BaseDevice } from "../dist-D9i3id5N.mjs";
2
+ import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-Ciq9CZa-.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
+ //#region src/homecom/config.ts
5
+ var HOMECOM_BRANDS = ["bosch", "buderus"];
6
+ var HomeComBrandSchema = _enum(HOMECOM_BRANDS);
7
+ var HOMECOM_REGIONS = ["eu", "us"];
8
+ var HomeComRegionSchema = _enum(HOMECOM_REGIONS);
9
+ var homeComConfigSchema = object({
10
+ /** Account email; filled by the sign-in, informational only. */
11
+ username: string().optional().describe("HomeCom / PointT account email").meta({ secret: true }),
12
+ token: string().min(1).describe("OAuth access token").meta({ secret: true }),
13
+ refresh: string().min(1).describe("OAuth refresh token").meta({ secret: true }),
14
+ brand: HomeComBrandSchema.default("bosch"),
15
+ region: HomeComRegionSchema.default("eu"),
16
+ /** The gateway this integration controls; chosen once from the account's gateways. */
17
+ gatewayId: string().min(1).optional(),
18
+ /** Shown on the sign-in field. Not a credential. */
19
+ accountLabel: string().optional(),
20
+ /** How many gateways the last sign-in listed. */
21
+ gatewayCount: number().optional()
22
+ });
23
+ function connectionFromSettings(settings) {
24
+ const parsed = homeComConfigSchema.safeParse(settings);
25
+ return parsed.success ? parsed.data : null;
26
+ }
27
+ function sameHomeComConnection(a, b) {
28
+ if (a === null || b === null) return false;
29
+ 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;
30
+ }
31
+ /**
32
+ * PointT REST origin (matches homecom_alt / HomeCom Easy). US and EU accounts use
33
+ * the same host; `region` is retained for Bacon MQTT (`euc1` / `use1`) when wired.
34
+ */
35
+ var HOMECOM_POINTT_API_ORIGIN = "https://pointt-api.bosch-thermotechnology.com";
36
+ function homeComApiBase(_region) {
37
+ return HOMECOM_POINTT_API_ORIGIN;
38
+ }
39
+ var HOMECOM_SECRET_KEYS = [
40
+ "username",
41
+ "token",
42
+ "refresh"
43
+ ];
44
+ var HIDDEN_UNTIL_ADVANCED = {
45
+ field: "__hidden__",
46
+ equals: "yes"
47
+ };
48
+ function buildHomeComFormSchema() {
49
+ return { sections: [{
50
+ id: "account",
51
+ title: "HomeCom account",
52
+ titleKey: "provider-bosch:homecom.s_account.title",
53
+ description: "Sign in with your SingleKey ID. Bosch opens its app address; paste that address here.",
54
+ descriptionKey: "provider-bosch:homecom.s_account.description",
55
+ columns: 1,
56
+ fields: [
57
+ {
58
+ type: "account-sign-in",
59
+ addonId: "bosch-homecom",
60
+ key: "__homecom-sign-in",
61
+ label: "HomeCom",
62
+ labelKey: "provider-bosch:homecom.s_account.__homecom-sign-in.label",
63
+ providerLabel: "SingleKey ID",
64
+ beginAction: "beginSignIn",
65
+ completeAction: "finishSignIn",
66
+ signOutAction: "signOut",
67
+ accountKey: "accountLabel",
68
+ countKey: "gatewayCount",
69
+ countNoun: "gateway",
70
+ choiceKey: "gatewayId",
71
+ choiceLabel: "Gateway",
72
+ pastePlaceholder: "com.bosch.tt.dashtt.pointt://app/login?code=…&state=…",
73
+ secretKeys: [
74
+ "token",
75
+ "refresh",
76
+ "username"
77
+ ],
78
+ assignResult: {
79
+ token: "token",
80
+ refresh: "refresh",
81
+ username: "username",
82
+ accountLabel: "accountLabel",
83
+ gatewayId: "gatewayId",
84
+ brand: "brand",
85
+ region: "region",
86
+ gatewayCount: "gatewayCount"
87
+ }
88
+ },
89
+ {
90
+ type: "text",
91
+ key: "accountLabel",
92
+ label: "Account",
93
+ labelKey: "provider-bosch:homecom.s_account.accountLabel.label",
94
+ required: false,
95
+ showWhen: HIDDEN_UNTIL_ADVANCED
96
+ },
97
+ {
98
+ type: "number",
99
+ key: "gatewayCount",
100
+ label: "Gateways",
101
+ labelKey: "provider-bosch:homecom.s_account.gatewayCount.label",
102
+ required: false,
103
+ showWhen: HIDDEN_UNTIL_ADVANCED
104
+ },
105
+ {
106
+ type: "password",
107
+ key: "username",
108
+ label: "Account email",
109
+ labelKey: "provider-bosch:homecom.s_account.username.label",
110
+ required: false,
111
+ showWhen: HIDDEN_UNTIL_ADVANCED
112
+ },
113
+ {
114
+ type: "group",
115
+ key: "__homecom-advanced",
116
+ label: "Advanced",
117
+ labelKey: "provider-bosch:homecom.s_account.__homecom-advanced.label",
118
+ style: "accordion",
119
+ defaultCollapsed: true,
120
+ fields: [
121
+ {
122
+ type: "password",
123
+ key: "token",
124
+ label: "Access token",
125
+ labelKey: "provider-bosch:homecom.s_account.token.label",
126
+ required: false,
127
+ showToggle: false
128
+ },
129
+ {
130
+ type: "password",
131
+ key: "refresh",
132
+ label: "Refresh token",
133
+ labelKey: "provider-bosch:homecom.s_account.refresh.label",
134
+ required: false,
135
+ showToggle: false
136
+ },
137
+ {
138
+ type: "text",
139
+ key: "gatewayId",
140
+ label: "Gateway id",
141
+ labelKey: "provider-bosch:homecom.s_account.gatewayId.label",
142
+ required: false
143
+ },
144
+ {
145
+ type: "select",
146
+ key: "brand",
147
+ label: "Brand",
148
+ labelKey: "provider-bosch:homecom.s_account.brand.label",
149
+ default: "bosch",
150
+ options: HOMECOM_BRANDS.map((b) => ({
151
+ value: b,
152
+ label: b
153
+ }))
154
+ },
155
+ {
156
+ type: "select",
157
+ key: "region",
158
+ label: "Region",
159
+ labelKey: "provider-bosch:homecom.s_account.region.label",
160
+ default: "eu",
161
+ options: HOMECOM_REGIONS.map((r) => ({
162
+ value: r,
163
+ label: r.toUpperCase()
164
+ }))
165
+ }
166
+ ]
167
+ }
168
+ ]
169
+ }] };
170
+ }
171
+ //#endregion
4
172
  //#region src/homecom/custom-actions.ts
173
+ var gatewayChoice = object({
174
+ value: string(),
175
+ label: string(),
176
+ brand: HomeComBrandSchema,
177
+ region: HomeComRegionSchema
178
+ });
5
179
  var homeComActions = defineCustomActions({
6
180
  beginSignIn: customAction(object({}), object({
181
+ attemptId: string().min(1),
7
182
  authorizeUrl: string().min(1),
8
- verifier: string().min(1),
9
- state: string().min(1),
10
183
  summary: string()
11
184
  }), {
12
185
  kind: "mutation",
13
186
  auth: "admin"
14
187
  }),
15
188
  finishSignIn: customAction(object({
16
- verifier: string().min(1),
17
- callback: string().min(1),
18
- state: string().min(1)
189
+ attemptId: string().min(1),
190
+ callback: string().min(1)
19
191
  }), object({
20
192
  token: string().min(1).meta({ secret: true }),
21
193
  refresh: string().min(1).meta({ secret: true }),
22
194
  username: string().meta({ secret: true }),
195
+ accountLabel: string(),
196
+ gatewayCount: number(),
197
+ summary: string(),
198
+ gatewayId: string().optional(),
199
+ brand: HomeComBrandSchema.optional(),
200
+ region: HomeComRegionSchema.optional(),
201
+ choices: array(gatewayChoice).optional()
202
+ }), {
203
+ kind: "mutation",
204
+ auth: "admin"
205
+ }),
206
+ signOut: customAction(object({}), object({
207
+ signedOut: literal(true),
23
208
  summary: string()
24
209
  }), {
25
210
  kind: "mutation",
@@ -27,6 +212,113 @@ var homeComActions = defineCustomActions({
27
212
  })
28
213
  });
29
214
  //#endregion
215
+ //#region src/homecom/homecom-domain-mapping.ts
216
+ var MODE_MAP = {
217
+ Off: "off",
218
+ off: "off",
219
+ Heat: "heat",
220
+ heat: "heat",
221
+ Cool: "cool",
222
+ cool: "cool",
223
+ Auto: "auto",
224
+ auto: "auto"
225
+ };
226
+ function mapHvacMode(raw) {
227
+ return MODE_MAP[raw] ?? "off";
228
+ }
229
+ function climateSliceFromCircuit(circuit, nowMs) {
230
+ return {
231
+ mode: mapHvacMode(circuit.operationMode),
232
+ availableModes: [
233
+ "off",
234
+ "heat",
235
+ "auto"
236
+ ],
237
+ fanMode: "",
238
+ availableFanModes: [],
239
+ preset: "",
240
+ availablePresets: [],
241
+ target: circuit.setpoint,
242
+ targetHigh: null,
243
+ targetLow: null,
244
+ targetHumidity: null,
245
+ currentTemp: circuit.currentRoomTemp,
246
+ currentHumidity: null,
247
+ swingVertical: null,
248
+ swingHorizontal: null,
249
+ minTemp: circuit.minTemp,
250
+ maxTemp: circuit.maxTemp,
251
+ tempStep: .5,
252
+ lastFetchedAt: nowMs
253
+ };
254
+ }
255
+ function waterHeaterSliceFromDhw(dhw, nowMs) {
256
+ return {
257
+ currentTemp: dhw.currentTemp,
258
+ targetTemp: dhw.targetTemp,
259
+ operationMode: dhw.operationMode,
260
+ availableModes: [...dhw.availableModes],
261
+ away: null,
262
+ minTemp: dhw.minTemp,
263
+ maxTemp: dhw.maxTemp,
264
+ lastChangedAt: nowMs
265
+ };
266
+ }
267
+ /** Parse K40-style gateway JSON into heating circuits + DHW. Pure. */
268
+ function parseHomeComSystemPayload(payload) {
269
+ if (typeof payload !== "object" || payload === null) return {
270
+ circuits: [],
271
+ dhw: null
272
+ };
273
+ const root = payload;
274
+ const circuits = [];
275
+ const rawCircuits = root["heatingCircuits"];
276
+ if (Array.isArray(rawCircuits)) for (const item of rawCircuits) {
277
+ if (typeof item !== "object" || item === null) continue;
278
+ const row = item;
279
+ const id = row["id"];
280
+ if (typeof id !== "string") continue;
281
+ circuits.push({
282
+ id,
283
+ name: typeof row["name"] === "string" ? row["name"] : id,
284
+ operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : "off",
285
+ setpoint: readNum(row["setpoint"]),
286
+ currentRoomTemp: readNum(row["currentRoomTemperature"]),
287
+ minTemp: readNum(row["minTemp"]),
288
+ maxTemp: readNum(row["maxTemp"])
289
+ });
290
+ }
291
+ let dhw = null;
292
+ const rawDhw = root["domesticHotWater"];
293
+ if (typeof rawDhw === "object" && rawDhw !== null) {
294
+ const row = rawDhw;
295
+ const modes = [];
296
+ if (Array.isArray(row["operationModes"])) {
297
+ for (const m of row["operationModes"]) if (typeof m === "string") modes.push(m);
298
+ }
299
+ dhw = {
300
+ currentTemp: readNum(row["currentTemperature"]),
301
+ targetTemp: readNum(row["setpoint"]),
302
+ operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : null,
303
+ availableModes: modes.length > 0 ? modes : [
304
+ "off",
305
+ "eco",
306
+ "performance"
307
+ ],
308
+ minTemp: readNum(row["minTemp"]),
309
+ maxTemp: readNum(row["maxTemp"])
310
+ };
311
+ }
312
+ return {
313
+ circuits,
314
+ dhw
315
+ };
316
+ }
317
+ function readNum(raw) {
318
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
319
+ return null;
320
+ }
321
+ //#endregion
30
322
  //#region src/homecom/homecom-oauth.ts
31
323
  /**
32
324
  * Public OAuth client of the HomeCom Easy / PointT mobile app, signed in through SingleKey ID.
@@ -54,6 +346,179 @@ var HOMECOM_SCOPE = [
54
346
  "hcc.tariff.read"
55
347
  ].join(" ");
56
348
  //#endregion
349
+ //#region src/homecom/homecom-api-client.ts
350
+ var HomeComAuthError = class extends Error {
351
+ name = "HomeComAuthError";
352
+ };
353
+ function isRecord$1(value) {
354
+ return typeof value === "object" && value !== null && !Array.isArray(value);
355
+ }
356
+ function rowId(row) {
357
+ for (const key of [
358
+ "deviceId",
359
+ "gatewayId",
360
+ "id"
361
+ ]) {
362
+ const value = row[key];
363
+ if (typeof value === "string" && value.length > 0) return value;
364
+ if (typeof value === "number") return String(value);
365
+ }
366
+ return null;
367
+ }
368
+ /** Gateways from the account listing: a bare array or `{ gateways: [...] }`. */
369
+ function parseHomeComGatewayList(payload) {
370
+ const rows = isRecord$1(payload) ? payload["gateways"] : payload;
371
+ if (!Array.isArray(rows)) return [];
372
+ const out = [];
373
+ for (const row of rows) {
374
+ if (!isRecord$1(row)) continue;
375
+ const gatewayId = rowId(row);
376
+ if (gatewayId === null) continue;
377
+ const name = row["name"];
378
+ const brand = row["brand"];
379
+ const region = row["region"];
380
+ out.push({
381
+ gatewayId,
382
+ name: typeof name === "string" && name.length > 0 ? name : gatewayId,
383
+ ...typeof brand === "string" && brand.length > 0 ? { brand } : {},
384
+ ...typeof region === "string" && region.length > 0 ? { region } : {}
385
+ });
386
+ }
387
+ return out;
388
+ }
389
+ function createHomeComApiClient(config, gatewayId, deps = {}) {
390
+ const fetchFn = deps.fetchFn ?? globalThis.fetch.bind(globalThis);
391
+ let token = config.token;
392
+ let refresh = config.refresh;
393
+ const base = homeComApiBase(config.region);
394
+ async function authHeaders() {
395
+ return {
396
+ Authorization: `Bearer ${token}`,
397
+ Accept: "application/json",
398
+ "Content-Type": "application/json"
399
+ };
400
+ }
401
+ async function request(path, init = {}) {
402
+ const res = await fetchFn(`${base}${path}`, {
403
+ method: init.method ?? "GET",
404
+ headers: await authHeaders(),
405
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
406
+ });
407
+ if (res.status === 401) throw new HomeComAuthError("token expired");
408
+ if (!res.ok) throw new Error(`HomeCom API ${path} failed: HTTP ${res.status}`);
409
+ return res.json();
410
+ }
411
+ return {
412
+ async refreshTokens() {
413
+ const res = await fetchFn(HOMECOM_TOKEN_URL, {
414
+ method: "POST",
415
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
416
+ body: new URLSearchParams({
417
+ grant_type: "refresh_token",
418
+ refresh_token: refresh,
419
+ client_id: HOMECOM_CLIENT_ID
420
+ }).toString()
421
+ });
422
+ if (!res.ok) {
423
+ if (res.status === 401 || res.status === 403) throw new HomeComAuthError("refresh refused");
424
+ throw new Error(`HomeCom token refresh failed: HTTP ${res.status}`);
425
+ }
426
+ const body = await res.json();
427
+ if (typeof body !== "object" || body === null) throw new Error("HomeCom token refresh: bad body");
428
+ const row = body;
429
+ const newToken = row["access_token"];
430
+ const newRefresh = row["refresh_token"];
431
+ if (typeof newToken !== "string" || typeof newRefresh !== "string") throw new Error("HomeCom token refresh: missing tokens");
432
+ token = newToken;
433
+ refresh = newRefresh;
434
+ await deps.onTokensRotated?.({
435
+ token,
436
+ refresh
437
+ });
438
+ return {
439
+ token,
440
+ refresh
441
+ };
442
+ },
443
+ async listGateways() {
444
+ return parseHomeComGatewayList(await request("/v2/gateways"));
445
+ },
446
+ async fetchSystem() {
447
+ const parsed = parseHomeComSystemPayload(await request(`/v2/gateways/${gatewayId}/system`));
448
+ return {
449
+ gateway: {
450
+ gatewayId,
451
+ name: "HomeCom heat pump",
452
+ model: "K40"
453
+ },
454
+ circuits: parsed.circuits,
455
+ dhw: parsed.dhw
456
+ };
457
+ },
458
+ async setCircuitSetpoint(circuitId, temp) {
459
+ await request(`/v2/gateways/${gatewayId}/heatingCircuits/${circuitId}/setpoint`, {
460
+ method: "PUT",
461
+ body: { value: temp }
462
+ });
463
+ },
464
+ async setCircuitMode(circuitId, mode) {
465
+ await request(`/v2/gateways/${gatewayId}/heatingCircuits/${circuitId}/operationMode`, {
466
+ method: "PUT",
467
+ body: { value: mode }
468
+ });
469
+ },
470
+ async setDhwTarget(temp) {
471
+ await request(`/v2/gateways/${gatewayId}/domesticHotWater/setpoint`, {
472
+ method: "PUT",
473
+ body: { value: temp }
474
+ });
475
+ },
476
+ async setDhwMode(mode) {
477
+ await request(`/v2/gateways/${gatewayId}/domesticHotWater/operationMode`, {
478
+ method: "PUT",
479
+ body: { value: mode }
480
+ });
481
+ }
482
+ };
483
+ }
484
+ //#endregion
485
+ //#region src/homecom/homecom-gateway-pick.ts
486
+ function brandOf(value) {
487
+ for (const brand of HOMECOM_BRANDS) if (value === brand) return brand;
488
+ return "bosch";
489
+ }
490
+ function regionOf(value) {
491
+ for (const region of HOMECOM_REGIONS) if (value === region) return region;
492
+ return "eu";
493
+ }
494
+ function choiceOf(gateway) {
495
+ return {
496
+ value: gateway.gatewayId,
497
+ label: gateway.name,
498
+ brand: brandOf(gateway.brand),
499
+ region: regionOf(gateway.region)
500
+ };
501
+ }
502
+ /**
503
+ * One gateway is filled in. Several stay a choice. None leaves the id empty
504
+ * for Advanced.
505
+ */
506
+ function resolveHomeComGateway(gateways) {
507
+ if (gateways.length === 0) return { kind: "none" };
508
+ const options = gateways.map(choiceOf);
509
+ const only = options[0];
510
+ if (gateways.length === 1 && only !== void 0) return {
511
+ kind: "one",
512
+ gatewayId: only.value,
513
+ brand: only.brand,
514
+ region: only.region
515
+ };
516
+ return {
517
+ kind: "many",
518
+ options
519
+ };
520
+ }
521
+ //#endregion
57
522
  //#region src/homecom/homecom-login.ts
58
523
  var MESSAGES = {
59
524
  empty: "Nothing was pasted. Paste the address the browser tried to open after you signed in.",
@@ -106,7 +571,7 @@ function parseHomeComCallback(input, expectedState) {
106
571
  if (!parsed.ok) throw new HomeComLoginError(parsed.reason);
107
572
  return parsed.code;
108
573
  }
109
- function isRecord$1(value) {
574
+ function isRecord(value) {
110
575
  return typeof value === "object" && value !== null && !Array.isArray(value);
111
576
  }
112
577
  function emailFromIdToken(idToken) {
@@ -115,7 +580,7 @@ function emailFromIdToken(idToken) {
115
580
  if (!payload) return "";
116
581
  try {
117
582
  const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
118
- return isRecord$1(parsed) && typeof parsed["email"] === "string" ? parsed["email"] : "";
583
+ return isRecord(parsed) && typeof parsed["email"] === "string" ? parsed["email"] : "";
119
584
  } catch {
120
585
  return "";
121
586
  }
@@ -134,189 +599,85 @@ async function completeHomeComSignIn(input, fetchFn = globalThis.fetch.bind(glob
134
599
  }).toString()
135
600
  });
136
601
  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");
602
+ if (!res.ok) throw new HomeComLoginError((isRecord(body) ? body["error"] : void 0) === "invalid_grant" ? "code-rejected" : "rejected");
603
+ if (!isRecord(body) || typeof body["access_token"] !== "string" || body["access_token"] === "") throw new HomeComLoginError("rejected");
139
604
  const refresh = body["refresh_token"];
140
605
  if (typeof refresh !== "string" || refresh === "") throw new HomeComLoginError("no-refresh");
141
606
  return {
142
607
  token: body["access_token"],
143
608
  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: "Sign in with your SingleKey ID, then paste the address the browser stops on (com.bosch.tt.dashtt.pointt://app/login?code=…). Find it in DevTools > Network.",
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
- }] };
609
+ username: emailFromIdToken(body["id_token"])
610
+ };
611
+ }
612
+ //#endregion
613
+ //#region src/homecom/homecom-sign-in.ts
614
+ /** What the form is allowed to see: the URL and an attempt id. Not the verifier. */
615
+ function publishHomeComSignIn(store, nowMs) {
616
+ const started = beginHomeComSignIn();
617
+ return {
618
+ attemptId: store.put(started.verifier, started.state, nowMs),
619
+ authorizeUrl: started.authorizeUrl
620
+ };
621
+ }
622
+ async function listGatewaysDefault(tokens, fetchFn) {
623
+ const res = await fetchFn(`${HOMECOM_POINTT_API_ORIGIN}/v2/gateways`, { headers: {
624
+ Authorization: `Bearer ${tokens.token}`,
625
+ Accept: "application/json"
626
+ } });
627
+ if (!res.ok) return [];
628
+ return parseHomeComGatewayList(await res.json());
629
+ }
630
+ async function finishHomeComSignIn(input, store, deps = {}) {
631
+ const taken = store.take(input.attemptId, input.nowMs);
632
+ if (!taken.ok) throw new HomeComLoginError(loginFailureForAttempt(taken.reason));
633
+ const fetchFn = deps.fetchFn ?? globalThis.fetch.bind(globalThis);
634
+ const tokens = await completeHomeComSignIn({
635
+ verifier: taken.attempt.verifier,
636
+ callback: input.callback,
637
+ state: taken.attempt.state
638
+ }, fetchFn);
639
+ let gateways = [];
640
+ let listFailed = false;
641
+ try {
642
+ gateways = await (deps.listGateways ?? ((next) => listGatewaysDefault(next, fetchFn)))(tokens);
643
+ } catch {
644
+ listFailed = true;
645
+ gateways = [];
646
+ }
647
+ return finishedFromGateways(tokens, resolveHomeComGateway(gateways), listFailed);
648
+ }
649
+ function finishedFromGateways(tokens, resolved, listFailed) {
650
+ const who = tokens.username.length > 0 ? `Signed in as ${tokens.username}` : "Signed in";
651
+ const base = {
652
+ ...tokens,
653
+ accountLabel: tokens.username
654
+ };
655
+ if (listFailed) return {
656
+ ...base,
657
+ gatewayCount: 0,
658
+ summary: `${who}. The gateway list could not be read. Enter the gateway id under Advanced.`
659
+ };
660
+ if (resolved.kind === "one") return {
661
+ ...base,
662
+ gatewayCount: 1,
663
+ gatewayId: resolved.gatewayId,
664
+ brand: resolved.brand,
665
+ region: resolved.region,
666
+ summary: `${who}. Gateway ${resolved.gatewayId} selected.`
667
+ };
668
+ if (resolved.kind === "many") return {
669
+ ...base,
670
+ gatewayCount: resolved.options.length,
671
+ choices: resolved.options.map((row) => ({ ...row })),
672
+ summary: `${who}. Choose a gateway.`
673
+ };
674
+ return {
675
+ ...base,
676
+ gatewayCount: 0,
677
+ summary: `${who}. No gateway was listed. Enter the gateway id under Advanced.`
678
+ };
319
679
  }
680
+ var homeComSignInAttempts = createSignInAttemptStore();
320
681
  //#endregion
321
682
  //#region src/homecom/homecom-token-persist.ts
322
683
  function unreadable(value) {
@@ -590,245 +951,6 @@ function buildDiscoverables(snapshot) {
590
951
  }
591
952
  var homeComConnections = new HomeComConnectionResolver();
592
953
  //#endregion
593
- //#region src/homecom/homecom-domain-mapping.ts
594
- var MODE_MAP = {
595
- Off: "off",
596
- off: "off",
597
- Heat: "heat",
598
- heat: "heat",
599
- Cool: "cool",
600
- cool: "cool",
601
- Auto: "auto",
602
- auto: "auto"
603
- };
604
- function mapHvacMode(raw) {
605
- return MODE_MAP[raw] ?? "off";
606
- }
607
- function climateSliceFromCircuit(circuit, nowMs) {
608
- return {
609
- mode: mapHvacMode(circuit.operationMode),
610
- availableModes: [
611
- "off",
612
- "heat",
613
- "auto"
614
- ],
615
- fanMode: "",
616
- availableFanModes: [],
617
- preset: "",
618
- availablePresets: [],
619
- target: circuit.setpoint,
620
- targetHigh: null,
621
- targetLow: null,
622
- targetHumidity: null,
623
- currentTemp: circuit.currentRoomTemp,
624
- currentHumidity: null,
625
- swingVertical: null,
626
- swingHorizontal: null,
627
- minTemp: circuit.minTemp,
628
- maxTemp: circuit.maxTemp,
629
- tempStep: .5,
630
- lastFetchedAt: nowMs
631
- };
632
- }
633
- function waterHeaterSliceFromDhw(dhw, nowMs) {
634
- return {
635
- currentTemp: dhw.currentTemp,
636
- targetTemp: dhw.targetTemp,
637
- operationMode: dhw.operationMode,
638
- availableModes: [...dhw.availableModes],
639
- away: null,
640
- minTemp: dhw.minTemp,
641
- maxTemp: dhw.maxTemp,
642
- lastChangedAt: nowMs
643
- };
644
- }
645
- /** Parse K40-style gateway JSON into heating circuits + DHW. Pure. */
646
- function parseHomeComSystemPayload(payload) {
647
- if (typeof payload !== "object" || payload === null) return {
648
- circuits: [],
649
- dhw: null
650
- };
651
- const root = payload;
652
- const circuits = [];
653
- const rawCircuits = root["heatingCircuits"];
654
- if (Array.isArray(rawCircuits)) for (const item of rawCircuits) {
655
- if (typeof item !== "object" || item === null) continue;
656
- const row = item;
657
- const id = row["id"];
658
- if (typeof id !== "string") continue;
659
- circuits.push({
660
- id,
661
- name: typeof row["name"] === "string" ? row["name"] : id,
662
- operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : "off",
663
- setpoint: readNum(row["setpoint"]),
664
- currentRoomTemp: readNum(row["currentRoomTemperature"]),
665
- minTemp: readNum(row["minTemp"]),
666
- maxTemp: readNum(row["maxTemp"])
667
- });
668
- }
669
- let dhw = null;
670
- const rawDhw = root["domesticHotWater"];
671
- if (typeof rawDhw === "object" && rawDhw !== null) {
672
- const row = rawDhw;
673
- const modes = [];
674
- if (Array.isArray(row["operationModes"])) {
675
- for (const m of row["operationModes"]) if (typeof m === "string") modes.push(m);
676
- }
677
- dhw = {
678
- currentTemp: readNum(row["currentTemperature"]),
679
- targetTemp: readNum(row["setpoint"]),
680
- operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : null,
681
- availableModes: modes.length > 0 ? modes : [
682
- "off",
683
- "eco",
684
- "performance"
685
- ],
686
- minTemp: readNum(row["minTemp"]),
687
- maxTemp: readNum(row["maxTemp"])
688
- };
689
- }
690
- return {
691
- circuits,
692
- dhw
693
- };
694
- }
695
- function readNum(raw) {
696
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
697
- return null;
698
- }
699
- //#endregion
700
- //#region src/homecom/homecom-api-client.ts
701
- var HomeComAuthError = class extends Error {
702
- name = "HomeComAuthError";
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
- }
736
- function createHomeComApiClient(config, gatewayId, deps = {}) {
737
- const fetchFn = deps.fetchFn ?? globalThis.fetch.bind(globalThis);
738
- let token = config.token;
739
- let refresh = config.refresh;
740
- const base = homeComApiBase(config.region);
741
- async function authHeaders() {
742
- return {
743
- Authorization: `Bearer ${token}`,
744
- Accept: "application/json",
745
- "Content-Type": "application/json"
746
- };
747
- }
748
- async function request(path, init = {}) {
749
- const res = await fetchFn(`${base}${path}`, {
750
- method: init.method ?? "GET",
751
- headers: await authHeaders(),
752
- body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
753
- });
754
- if (res.status === 401) throw new HomeComAuthError("token expired");
755
- if (!res.ok) throw new Error(`HomeCom API ${path} failed: HTTP ${res.status}`);
756
- return res.json();
757
- }
758
- return {
759
- async refreshTokens() {
760
- const res = await fetchFn(HOMECOM_TOKEN_URL, {
761
- method: "POST",
762
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
763
- body: new URLSearchParams({
764
- grant_type: "refresh_token",
765
- refresh_token: refresh,
766
- client_id: HOMECOM_CLIENT_ID
767
- }).toString()
768
- });
769
- if (!res.ok) {
770
- if (res.status === 401 || res.status === 403) throw new HomeComAuthError("refresh refused");
771
- throw new Error(`HomeCom token refresh failed: HTTP ${res.status}`);
772
- }
773
- const body = await res.json();
774
- if (typeof body !== "object" || body === null) throw new Error("HomeCom token refresh: bad body");
775
- const row = body;
776
- const newToken = row["access_token"];
777
- const newRefresh = row["refresh_token"];
778
- if (typeof newToken !== "string" || typeof newRefresh !== "string") throw new Error("HomeCom token refresh: missing tokens");
779
- token = newToken;
780
- refresh = newRefresh;
781
- await deps.onTokensRotated?.({
782
- token,
783
- refresh
784
- });
785
- return {
786
- token,
787
- refresh
788
- };
789
- },
790
- async listGateways() {
791
- return parseHomeComGatewayList(await request("/v2/gateways"));
792
- },
793
- async fetchSystem() {
794
- const parsed = parseHomeComSystemPayload(await request(`/v2/gateways/${gatewayId}/system`));
795
- return {
796
- gateway: {
797
- gatewayId,
798
- name: "HomeCom heat pump",
799
- model: "K40"
800
- },
801
- circuits: parsed.circuits,
802
- dhw: parsed.dhw
803
- };
804
- },
805
- async setCircuitSetpoint(circuitId, temp) {
806
- await request(`/v2/gateways/${gatewayId}/heatingCircuits/${circuitId}/setpoint`, {
807
- method: "PUT",
808
- body: { value: temp }
809
- });
810
- },
811
- async setCircuitMode(circuitId, mode) {
812
- await request(`/v2/gateways/${gatewayId}/heatingCircuits/${circuitId}/operationMode`, {
813
- method: "PUT",
814
- body: { value: mode }
815
- });
816
- },
817
- async setDhwTarget(temp) {
818
- await request(`/v2/gateways/${gatewayId}/domesticHotWater/setpoint`, {
819
- method: "PUT",
820
- body: { value: temp }
821
- });
822
- },
823
- async setDhwMode(mode) {
824
- await request(`/v2/gateways/${gatewayId}/domesticHotWater/operationMode`, {
825
- method: "PUT",
826
- body: { value: mode }
827
- });
828
- }
829
- };
830
- }
831
- //#endregion
832
954
  //#region src/homecom/homecom-integration-manager.ts
833
955
  var HomeComIntegrationManager = class {
834
956
  #id;
@@ -1389,16 +1511,18 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
1389
1511
  customActions: homeComActions,
1390
1512
  actionHandlers: {
1391
1513
  beginSignIn: async () => ({
1392
- ...beginHomeComSignIn(),
1514
+ ...publishHomeComSignIn(homeComSignInAttempts, Date.now()),
1393
1515
  summary: "Open the link in a browser and sign in."
1394
1516
  }),
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
- }
1517
+ finishSignIn: async (input) => finishHomeComSignIn({
1518
+ attemptId: input.attemptId,
1519
+ callback: input.callback,
1520
+ nowMs: Date.now()
1521
+ }, homeComSignInAttempts),
1522
+ signOut: async () => ({
1523
+ signedOut: true,
1524
+ summary: "Signed out."
1525
+ })
1402
1526
  }
1403
1527
  };
1404
1528
  }