@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.
- package/dist/{account-integration-offline-7DUtQrPy.js → account-integration-offline-CEg0JOfN.js} +55 -1
- package/dist/{account-integration-offline-DvuqXgc8.mjs → account-integration-offline-Ciq9CZa-.mjs} +44 -2
- package/dist/{dist-CYBUFKeW.js → dist-CFPbdUpa.js} +143 -88
- package/dist/{dist-B75tWQLF.mjs → dist-D9i3id5N.mjs} +132 -89
- package/dist/home-connect/addon.js +137 -106
- package/dist/home-connect/addon.mjs +137 -106
- package/dist/homecom/addon.js +557 -433
- package/dist/homecom/addon.mjs +557 -433
- package/dist/shc/addon.js +331 -38
- package/dist/shc/addon.mjs +331 -38
- package/i18n/en.json +23 -19
- package/i18n/it.json +23 -19
- package/package.json +4 -1
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,25 +1,210 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { n as cascadeRemoveDevicesForIntegration, r as
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
138
|
-
if (!isRecord
|
|
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/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
...
|
|
1514
|
+
...publishHomeComSignIn(homeComSignInAttempts, Date.now()),
|
|
1393
1515
|
summary: "Open the link in a browser and sign in."
|
|
1394
1516
|
}),
|
|
1395
|
-
finishSignIn: async (input) => {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
}
|