@camstack/addon-provider-bosch 0.1.20 → 0.1.22

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