@camstack/addon-provider-bosch 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,1116 @@
1
+ import { D as EventCategory, S as object, a as connectionTestCapability, b as literal, d as waterHeaterCapability, f as DeviceType, i as climateControlCapability, n as BaseDeviceProvider, o as deviceAdoptionCapability, p as errMsg, t as BaseDevice, v as _enum, w as string } from "../dist-CY6qHAEx.mjs";
2
+ import { n as cascadeRemoveDevicesForIntegration, t as setIntegrationDevicesOnline } from "../account-integration-offline-CH7JBu8l.mjs";
3
+ //#region src/homecom/homecom-discovery.ts
4
+ function buildHomeComCandidates(input) {
5
+ const out = [];
6
+ for (const dev of input.devices) {
7
+ const type = dev.kind === "heating-circuit" ? DeviceType.Climate : DeviceType.WaterHeater;
8
+ const caps = dev.kind === "heating-circuit" ? ["climate-control"] : ["water-heater"];
9
+ const adoptedId = input.adopted.get(dev.nativeId) ?? null;
10
+ out.push({
11
+ childNativeId: dev.nativeId,
12
+ name: dev.name,
13
+ type,
14
+ status: "online",
15
+ metadata: { serialNumber: dev.nativeId },
16
+ alreadyAdopted: adoptedId !== null,
17
+ adoptedDeviceId: adoptedId,
18
+ capabilities: caps
19
+ });
20
+ }
21
+ return out;
22
+ }
23
+ //#endregion
24
+ //#region src/homecom/homecom-adoption-provider.ts
25
+ var DEVICES_FILTER = {
26
+ id: "devices",
27
+ label: "Devices",
28
+ isDefault: true
29
+ };
30
+ async function adoptedMapForIntegration(integrationId, listAdopted) {
31
+ const all = await listAdopted();
32
+ const map = /* @__PURE__ */ new Map();
33
+ for (const device of all) if (device.config["system"] === "homecom" && device.config["brokerId"] === integrationId) {
34
+ const nativeId = device.config["nativeId"];
35
+ if (typeof nativeId === "string") map.set(nativeId, device.id);
36
+ }
37
+ return map;
38
+ }
39
+ function buildHomeComAdoptionProvider(deps) {
40
+ const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
41
+ async function allCandidatesForIntegration(integrationId) {
42
+ return buildHomeComCandidates({
43
+ devices: registry.discoverables(integrationId),
44
+ adopted: await adoptedMapForIntegration(integrationId, listAdopted)
45
+ });
46
+ }
47
+ return {
48
+ listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
49
+ listCandidates: async ({ integrationId, page, pageSize }) => {
50
+ const cands = await allCandidatesForIntegration(integrationId);
51
+ const start = (page - 1) * pageSize;
52
+ return {
53
+ candidates: cands.slice(start, start + pageSize),
54
+ totalCount: cands.length,
55
+ page,
56
+ pageSize
57
+ };
58
+ },
59
+ getCandidate: async ({ integrationId, childNativeId }) => {
60
+ return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
61
+ },
62
+ getStatus: async () => {
63
+ try {
64
+ let candidateCount = 0;
65
+ for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
66
+ const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "homecom").length;
67
+ return {
68
+ lastDiscoveryAt: Date.now(),
69
+ candidateCount,
70
+ adoptedCount,
71
+ lastError: null
72
+ };
73
+ } catch (err) {
74
+ return {
75
+ lastDiscoveryAt: null,
76
+ candidateCount: 0,
77
+ adoptedCount: 0,
78
+ lastError: errMsg(err)
79
+ };
80
+ }
81
+ },
82
+ refresh: async ({ integrationId }) => {
83
+ const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
84
+ const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
85
+ return {
86
+ lastDiscoveryAt: Date.now(),
87
+ candidateCount,
88
+ adoptedCount,
89
+ lastError: null
90
+ };
91
+ },
92
+ adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
93
+ if (!registry.has(integrationId)) throw new Error(`homecom adopt: integration ${integrationId} not connected`);
94
+ const devices = registry.discoverables(integrationId);
95
+ const adopted = [];
96
+ let failures = 0;
97
+ for (const nativeId of childNativeIds) try {
98
+ const dev = devices.find((d) => d.nativeId === nativeId);
99
+ if (!dev) {
100
+ failures++;
101
+ continue;
102
+ }
103
+ const candidate = buildHomeComCandidates({
104
+ devices: [dev],
105
+ adopted: /* @__PURE__ */ new Map()
106
+ })[0];
107
+ if (!candidate) {
108
+ failures++;
109
+ continue;
110
+ }
111
+ const name = perCandidate?.[nativeId]?.name ?? dev.name;
112
+ const { deviceId } = await adoptDevice({
113
+ nativeId,
114
+ integrationId,
115
+ type: candidate.type,
116
+ name,
117
+ kind: dev.kind,
118
+ circuitId: dev.circuitId
119
+ });
120
+ adopted.push({
121
+ childNativeId: nativeId,
122
+ parentDeviceId: deviceId
123
+ });
124
+ } catch (err) {
125
+ logger.warn("homecom adopt failed", { meta: {
126
+ nativeId,
127
+ integrationId,
128
+ error: errMsg(err)
129
+ } });
130
+ failures++;
131
+ }
132
+ if (adopted.length === 0 && failures > 0) throw new Error(`homecom adopt: all ${failures} adopt(s) failed`);
133
+ return { adopted: adopted.map((a) => ({
134
+ ...a,
135
+ accessoryDeviceIds: []
136
+ })) };
137
+ },
138
+ release: async ({ camDeviceId }) => {
139
+ await removeDevice(camDeviceId);
140
+ },
141
+ resync: async ({ camDeviceId }) => {
142
+ const cfg = await findDeviceConfig(camDeviceId);
143
+ if (!cfg || cfg["system"] !== "homecom") throw new Error(`homecom resync: device ${camDeviceId} not found`);
144
+ const integrationId = String(cfg["brokerId"]);
145
+ const nativeId = String(cfg["nativeId"]);
146
+ if (!hasIntegration(integrationId)) throw new Error(`homecom resync: integration ${integrationId} not connected`);
147
+ if (!registry.discoverables(integrationId).some((d) => d.nativeId === nativeId)) throw new Error(`homecom resync: device ${nativeId} no longer present`);
148
+ return {
149
+ changed: false,
150
+ rebuiltChildren: 0
151
+ };
152
+ }
153
+ };
154
+ }
155
+ //#endregion
156
+ //#region src/homecom/config.ts
157
+ var HOMECOM_BRANDS = ["bosch", "buderus"];
158
+ var HomeComBrandSchema = _enum(HOMECOM_BRANDS);
159
+ var HOMECOM_REGIONS = ["eu", "us"];
160
+ var HomeComRegionSchema = _enum(HOMECOM_REGIONS);
161
+ var homeComConfigSchema = object({
162
+ username: string().min(1).describe("HomeCom / PointT account email").meta({ secret: true }),
163
+ token: string().min(1).describe("OAuth access token").meta({ secret: true }),
164
+ refresh: string().min(1).describe("OAuth refresh token").meta({ secret: true }),
165
+ brand: HomeComBrandSchema.default("bosch"),
166
+ region: HomeComRegionSchema.default("eu")
167
+ });
168
+ function connectionFromSettings(settings) {
169
+ const parsed = homeComConfigSchema.safeParse(settings);
170
+ return parsed.success ? parsed.data : null;
171
+ }
172
+ function sameHomeComConnection(a, b) {
173
+ if (a === null || b === null) return false;
174
+ return a.username === b.username && a.token === b.token && a.refresh === b.refresh && a.brand === b.brand && a.region === b.region;
175
+ }
176
+ /**
177
+ * PointT REST origin (matches homecom_alt / HomeCom Easy). US and EU accounts use
178
+ * the same host; `region` is retained for Bacon MQTT (`euc1` / `use1`) when wired.
179
+ */
180
+ var HOMECOM_POINTT_API_ORIGIN = "https://pointt-api.bosch-thermotechnology.com";
181
+ function homeComApiBase(_region) {
182
+ return HOMECOM_POINTT_API_ORIGIN;
183
+ }
184
+ function buildHomeComFormSchema() {
185
+ return { sections: [{
186
+ id: "account",
187
+ title: "HomeCom account",
188
+ columns: 1,
189
+ fields: [
190
+ {
191
+ type: "text",
192
+ key: "username",
193
+ label: "Username",
194
+ required: true
195
+ },
196
+ {
197
+ type: "password",
198
+ key: "token",
199
+ label: "Access token",
200
+ required: true,
201
+ showToggle: true
202
+ },
203
+ {
204
+ type: "password",
205
+ key: "refresh",
206
+ label: "Refresh token",
207
+ required: true,
208
+ showToggle: true
209
+ },
210
+ {
211
+ type: "select",
212
+ key: "brand",
213
+ label: "Brand",
214
+ default: "bosch",
215
+ options: HOMECOM_BRANDS.map((b) => ({
216
+ value: b,
217
+ label: b
218
+ }))
219
+ },
220
+ {
221
+ type: "select",
222
+ key: "region",
223
+ label: "Region",
224
+ default: "eu",
225
+ options: HOMECOM_REGIONS.map((r) => ({
226
+ value: r,
227
+ label: r.toUpperCase()
228
+ }))
229
+ }
230
+ ]
231
+ }] };
232
+ }
233
+ //#endregion
234
+ //#region src/homecom/homecom-gateway.ts
235
+ var HomeComConnectionResolver = class {
236
+ #surfaces = /* @__PURE__ */ new Map();
237
+ set(integrationId, surface) {
238
+ if (surface === null) {
239
+ this.#surfaces.delete(integrationId);
240
+ return;
241
+ }
242
+ this.#surfaces.set(integrationId, surface);
243
+ }
244
+ has(integrationId) {
245
+ return this.#surfaces.has(integrationId);
246
+ }
247
+ list() {
248
+ return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
249
+ }
250
+ discoverables(integrationId) {
251
+ return this.#surfaces.get(integrationId)?.discoverables ?? [];
252
+ }
253
+ getClient(integrationId) {
254
+ return this.#surfaces.get(integrationId)?.client ?? null;
255
+ }
256
+ getSnapshot(integrationId) {
257
+ return this.#surfaces.get(integrationId)?.snapshot ?? null;
258
+ }
259
+ updateSnapshot(integrationId, snapshot) {
260
+ const existing = this.#surfaces.get(integrationId);
261
+ if (!existing) return;
262
+ this.#surfaces.set(integrationId, {
263
+ ...existing,
264
+ snapshot,
265
+ discoverables: buildDiscoverables(snapshot)
266
+ });
267
+ }
268
+ clear() {
269
+ this.#surfaces.clear();
270
+ }
271
+ };
272
+ function buildDiscoverables(snapshot) {
273
+ const out = [];
274
+ for (const circuit of snapshot.circuits) out.push({
275
+ nativeId: `hc:${circuit.id}`,
276
+ name: circuit.name,
277
+ kind: "heating-circuit",
278
+ circuitId: circuit.id
279
+ });
280
+ if (snapshot.dhw) out.push({
281
+ nativeId: "dhw:0",
282
+ name: "Domestic hot water",
283
+ kind: "dhw"
284
+ });
285
+ return out;
286
+ }
287
+ var homeComConnections = new HomeComConnectionResolver();
288
+ //#endregion
289
+ //#region src/homecom/homecom-domain-mapping.ts
290
+ var MODE_MAP = {
291
+ Off: "off",
292
+ off: "off",
293
+ Heat: "heat",
294
+ heat: "heat",
295
+ Cool: "cool",
296
+ cool: "cool",
297
+ Auto: "auto",
298
+ auto: "auto"
299
+ };
300
+ function mapHvacMode(raw) {
301
+ return MODE_MAP[raw] ?? "off";
302
+ }
303
+ function climateSliceFromCircuit(circuit, nowMs) {
304
+ return {
305
+ mode: mapHvacMode(circuit.operationMode),
306
+ availableModes: [
307
+ "off",
308
+ "heat",
309
+ "auto"
310
+ ],
311
+ fanMode: "",
312
+ availableFanModes: [],
313
+ preset: "",
314
+ availablePresets: [],
315
+ target: circuit.setpoint,
316
+ targetHigh: null,
317
+ targetLow: null,
318
+ targetHumidity: null,
319
+ currentTemp: circuit.currentRoomTemp,
320
+ currentHumidity: null,
321
+ swingVertical: null,
322
+ swingHorizontal: null,
323
+ minTemp: circuit.minTemp,
324
+ maxTemp: circuit.maxTemp,
325
+ tempStep: .5,
326
+ lastFetchedAt: nowMs
327
+ };
328
+ }
329
+ function waterHeaterSliceFromDhw(dhw, nowMs) {
330
+ return {
331
+ currentTemp: dhw.currentTemp,
332
+ targetTemp: dhw.targetTemp,
333
+ operationMode: dhw.operationMode,
334
+ availableModes: [...dhw.availableModes],
335
+ away: null,
336
+ minTemp: dhw.minTemp,
337
+ maxTemp: dhw.maxTemp,
338
+ lastChangedAt: nowMs
339
+ };
340
+ }
341
+ /** Parse K40-style gateway JSON into heating circuits + DHW. Pure. */
342
+ function parseHomeComSystemPayload(payload) {
343
+ if (typeof payload !== "object" || payload === null) return {
344
+ circuits: [],
345
+ dhw: null
346
+ };
347
+ const root = payload;
348
+ const circuits = [];
349
+ const rawCircuits = root["heatingCircuits"];
350
+ if (Array.isArray(rawCircuits)) for (const item of rawCircuits) {
351
+ if (typeof item !== "object" || item === null) continue;
352
+ const row = item;
353
+ const id = row["id"];
354
+ if (typeof id !== "string") continue;
355
+ circuits.push({
356
+ id,
357
+ name: typeof row["name"] === "string" ? row["name"] : id,
358
+ operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : "off",
359
+ setpoint: readNum(row["setpoint"]),
360
+ currentRoomTemp: readNum(row["currentRoomTemperature"]),
361
+ minTemp: readNum(row["minTemp"]),
362
+ maxTemp: readNum(row["maxTemp"])
363
+ });
364
+ }
365
+ let dhw = null;
366
+ const rawDhw = root["domesticHotWater"];
367
+ if (typeof rawDhw === "object" && rawDhw !== null) {
368
+ const row = rawDhw;
369
+ const modes = [];
370
+ if (Array.isArray(row["operationModes"])) {
371
+ for (const m of row["operationModes"]) if (typeof m === "string") modes.push(m);
372
+ }
373
+ dhw = {
374
+ currentTemp: readNum(row["currentTemperature"]),
375
+ targetTemp: readNum(row["setpoint"]),
376
+ operationMode: typeof row["operationMode"] === "string" ? row["operationMode"] : null,
377
+ availableModes: modes.length > 0 ? modes : [
378
+ "off",
379
+ "eco",
380
+ "performance"
381
+ ],
382
+ minTemp: readNum(row["minTemp"]),
383
+ maxTemp: readNum(row["maxTemp"])
384
+ };
385
+ }
386
+ return {
387
+ circuits,
388
+ dhw
389
+ };
390
+ }
391
+ function readNum(raw) {
392
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
393
+ return null;
394
+ }
395
+ //#endregion
396
+ //#region src/homecom/homecom-api-client.ts
397
+ var HomeComAuthError = class extends Error {
398
+ name = "HomeComAuthError";
399
+ };
400
+ function createHomeComApiClient(config, gatewayId, deps = {}) {
401
+ const fetchFn = deps.fetchFn ?? globalThis.fetch.bind(globalThis);
402
+ let token = config.token;
403
+ let refresh = config.refresh;
404
+ const base = homeComApiBase(config.region);
405
+ async function authHeaders() {
406
+ return {
407
+ Authorization: `Bearer ${token}`,
408
+ Accept: "application/json",
409
+ "Content-Type": "application/json"
410
+ };
411
+ }
412
+ async function request(path, init = {}) {
413
+ const res = await fetchFn(`${base}${path}`, {
414
+ method: init.method ?? "GET",
415
+ headers: await authHeaders(),
416
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
417
+ });
418
+ if (res.status === 401) throw new HomeComAuthError("token expired");
419
+ if (!res.ok) throw new Error(`HomeCom API ${path} failed: HTTP ${res.status}`);
420
+ return res.json();
421
+ }
422
+ return {
423
+ async refreshTokens() {
424
+ const res = await fetchFn(`${base}/token/oauth2`, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
427
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refresh)}&client_id=homecom`
428
+ });
429
+ if (!res.ok) {
430
+ if (res.status === 401 || res.status === 403) throw new HomeComAuthError("refresh refused");
431
+ throw new Error(`HomeCom token refresh failed: HTTP ${res.status}`);
432
+ }
433
+ const body = await res.json();
434
+ if (typeof body !== "object" || body === null) throw new Error("HomeCom token refresh: bad body");
435
+ const row = body;
436
+ const newToken = row["access_token"];
437
+ const newRefresh = row["refresh_token"];
438
+ if (typeof newToken !== "string" || typeof newRefresh !== "string") throw new Error("HomeCom token refresh: missing tokens");
439
+ token = newToken;
440
+ refresh = newRefresh;
441
+ return {
442
+ token,
443
+ refresh
444
+ };
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-integration-manager.ts
486
+ var HomeComIntegrationManager = class {
487
+ #id;
488
+ #name;
489
+ #connection;
490
+ #logger;
491
+ #onConnected;
492
+ #onDisconnected;
493
+ #surfaceSink;
494
+ #makeClient;
495
+ #pollIntervalMs;
496
+ #client = null;
497
+ #gatewayId = null;
498
+ #pollTimer = null;
499
+ #status = "disconnected";
500
+ #error = null;
501
+ #lastCheckedAt = null;
502
+ constructor(options) {
503
+ this.#id = options.id;
504
+ this.#name = options.name;
505
+ this.#connection = options.connection;
506
+ this.#logger = options.logger;
507
+ this.#onConnected = options.onConnected;
508
+ this.#onDisconnected = options.onDisconnected;
509
+ this.#surfaceSink = options.surfaceSink;
510
+ const fetchFn = options.fetchFn;
511
+ this.#makeClient = options.makeClient ?? ((config, gatewayId) => createHomeComApiClient(config, gatewayId, { fetchFn }));
512
+ this.#pollIntervalMs = options.pollIntervalMs ?? 6e4;
513
+ }
514
+ getConnection() {
515
+ return this.#connection;
516
+ }
517
+ getInfo() {
518
+ return {
519
+ id: this.#id,
520
+ name: this.#name,
521
+ kind: "homecom",
522
+ status: this.#status,
523
+ info: {
524
+ username: this.#connection.username,
525
+ gatewayId: this.#gatewayId
526
+ },
527
+ lastCheckedAt: this.#lastCheckedAt,
528
+ error: this.#error
529
+ };
530
+ }
531
+ async start() {
532
+ if (this.#client !== null) return;
533
+ this.#status = "connecting";
534
+ this.#error = null;
535
+ const gatewayId = await this.resolveGatewayId(this.#connection);
536
+ this.#gatewayId = gatewayId;
537
+ const client = this.#makeClient(this.#connection, gatewayId);
538
+ this.#client = client;
539
+ try {
540
+ const snapshot = await this.fetchWithTokenRefresh(client);
541
+ const surface = {
542
+ gatewayId,
543
+ client,
544
+ snapshot,
545
+ discoverables: buildDiscoverables(snapshot)
546
+ };
547
+ this.#surfaceSink.set(this.#id, surface);
548
+ this.#status = "connected";
549
+ this.#lastCheckedAt = Date.now();
550
+ this.#onConnected(this.#id);
551
+ this.#pollTimer = setInterval(() => {
552
+ this.pollOnce();
553
+ }, this.#pollIntervalMs);
554
+ } catch (err) {
555
+ this.#status = "error";
556
+ this.#error = err instanceof HomeComAuthError ? "authentication failed — check tokens" : errMsg(err);
557
+ this.#surfaceSink.set(this.#id, null);
558
+ this.#onDisconnected(this.#id);
559
+ throw err;
560
+ }
561
+ }
562
+ async stop() {
563
+ if (this.#pollTimer) {
564
+ clearInterval(this.#pollTimer);
565
+ this.#pollTimer = null;
566
+ }
567
+ this.#client = null;
568
+ this.#gatewayId = null;
569
+ this.#status = "disconnected";
570
+ this.#surfaceSink.set(this.#id, null);
571
+ this.#onDisconnected(this.#id);
572
+ }
573
+ async applyConnection(conn) {
574
+ await this.stop();
575
+ this.#connection = conn;
576
+ await this.start();
577
+ }
578
+ async pollOnce() {
579
+ const client = this.#client;
580
+ if (!client) return;
581
+ try {
582
+ const snapshot = await this.fetchWithTokenRefresh(client);
583
+ this.#surfaceSink.updateSnapshot(this.#id, snapshot);
584
+ this.#lastCheckedAt = Date.now();
585
+ } catch (err) {
586
+ this.#logger.warn("HomeCom poll failed", { meta: {
587
+ integrationId: this.#id,
588
+ error: errMsg(err)
589
+ } });
590
+ }
591
+ }
592
+ async fetchWithTokenRefresh(client) {
593
+ try {
594
+ return await client.fetchSystem();
595
+ } catch (err) {
596
+ if (!(err instanceof HomeComAuthError)) throw err;
597
+ await client.refreshTokens();
598
+ return client.fetchSystem();
599
+ }
600
+ }
601
+ /** Discover K40 gateway id via account listing (simplified: username hash stub). */
602
+ async resolveGatewayId(config) {
603
+ return "gateway-k40";
604
+ }
605
+ };
606
+ //#endregion
607
+ //#region src/homecom/homecom-client-registry.ts
608
+ var HomeComClientRegistry = class {
609
+ #logger;
610
+ #onConnected;
611
+ #onDisconnected;
612
+ #makeManager;
613
+ #entries = /* @__PURE__ */ new Map();
614
+ constructor(logger, deps = {}) {
615
+ this.#logger = logger;
616
+ this.#onConnected = deps.onConnected ?? (() => void 0);
617
+ this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
618
+ const fetchFn = deps.fetchFn;
619
+ const makeClient = deps.makeClient;
620
+ this.#makeManager = deps.makeManager ?? ((opts) => new HomeComIntegrationManager({
621
+ id: opts.integrationId,
622
+ name: opts.name,
623
+ connection: opts.connection,
624
+ logger: opts.logger,
625
+ onConnected: opts.onConnected,
626
+ onDisconnected: opts.onDisconnected,
627
+ surfaceSink: homeComConnections,
628
+ fetchFn,
629
+ makeClient,
630
+ pollIntervalMs: 6e4
631
+ }));
632
+ }
633
+ setOnConnected(cb) {
634
+ this.#onConnected = cb;
635
+ }
636
+ setOnDisconnected(cb) {
637
+ this.#onDisconnected = cb;
638
+ }
639
+ async upsert(integrationId, name, connection) {
640
+ const existing = this.#entries.get(integrationId);
641
+ if (existing) {
642
+ if (sameHomeComConnection(existing.connection, connection)) return;
643
+ try {
644
+ await existing.manager.applyConnection(connection);
645
+ this.#entries.set(integrationId, {
646
+ manager: existing.manager,
647
+ connection
648
+ });
649
+ } catch (err) {
650
+ this.#logger.warn("HomeComClientRegistry: applyConnection failed", { meta: {
651
+ integrationId,
652
+ error: errMsg(err)
653
+ } });
654
+ }
655
+ return;
656
+ }
657
+ const manager = this.#makeManager({
658
+ integrationId,
659
+ name,
660
+ connection,
661
+ logger: this.#logger,
662
+ onConnected: (id) => this.#onConnected(id),
663
+ onDisconnected: (id) => this.#onDisconnected(id)
664
+ });
665
+ this.#entries.set(integrationId, {
666
+ manager,
667
+ connection
668
+ });
669
+ try {
670
+ await manager.start();
671
+ } catch (err) {
672
+ this.#logger.warn("HomeComClientRegistry: manager start failed", { meta: {
673
+ integrationId,
674
+ error: errMsg(err)
675
+ } });
676
+ }
677
+ }
678
+ async remove(integrationId) {
679
+ const entry = this.#entries.get(integrationId);
680
+ if (!entry) return;
681
+ this.#entries.delete(integrationId);
682
+ try {
683
+ await entry.manager.stop();
684
+ } catch (err) {
685
+ this.#logger.warn("HomeComClientRegistry: remove stop failed", { meta: {
686
+ integrationId,
687
+ error: errMsg(err)
688
+ } });
689
+ }
690
+ }
691
+ async retain(keep) {
692
+ for (const id of [...this.#entries.keys()].filter((k) => !keep.has(k))) await this.remove(id);
693
+ }
694
+ async shutdown() {
695
+ for (const id of [...this.#entries.keys()]) try {
696
+ await this.#entries.get(id)?.manager.stop();
697
+ } catch (err) {
698
+ this.#logger.warn("HomeComClientRegistry: shutdown stop failed", { meta: {
699
+ integrationId: id,
700
+ error: errMsg(err)
701
+ } });
702
+ }
703
+ this.#entries.clear();
704
+ homeComConnections.clear();
705
+ }
706
+ has(integrationId) {
707
+ return this.#entries.has(integrationId);
708
+ }
709
+ list() {
710
+ return [...this.#entries.keys()];
711
+ }
712
+ getInfo(integrationId) {
713
+ return this.#entries.get(integrationId)?.manager.getInfo() ?? null;
714
+ }
715
+ size() {
716
+ return this.#entries.size;
717
+ }
718
+ };
719
+ //#endregion
720
+ //#region src/homecom/homecom-connection-test.ts
721
+ async function defaultResolveGatewayId(_config) {
722
+ return "gateway-k40";
723
+ }
724
+ var TEST_LABEL = "Refreshes HomeCom OAuth tokens and reads the heat-pump system snapshot";
725
+ function buildHomeComConnectionTestProvider(deps) {
726
+ const { logger } = deps;
727
+ const makeClient = deps.makeClient ?? ((config, gatewayId) => createHomeComApiClient(config, gatewayId));
728
+ const resolveGatewayId = deps.resolveGatewayId ?? defaultResolveGatewayId;
729
+ return {
730
+ describeTest: async () => ({ label: TEST_LABEL }),
731
+ testSettings: async ({ settings }) => {
732
+ const parsed = homeComConfigSchema.safeParse(settings);
733
+ if (!parsed.success) return {
734
+ outcome: "rejected",
735
+ error: `HomeCom settings incomplete: ${parsed.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}`
736
+ };
737
+ const startedAt = Date.now();
738
+ const gatewayId = await resolveGatewayId(parsed.data);
739
+ const client = makeClient(parsed.data, gatewayId);
740
+ try {
741
+ await client.refreshTokens();
742
+ await client.fetchSystem();
743
+ return {
744
+ outcome: "validated",
745
+ latencyMs: Date.now() - startedAt
746
+ };
747
+ } catch (err) {
748
+ if (err instanceof HomeComAuthError) return {
749
+ outcome: "rejected",
750
+ error: "HomeCom refused these tokens — re-authenticate the PointT account."
751
+ };
752
+ logger.warn("HomeCom connection test inconclusive", { meta: {
753
+ username: parsed.data.username,
754
+ error: errMsg(err)
755
+ } });
756
+ return {
757
+ outcome: "inconclusive",
758
+ error: "Could not reach the HomeCom cloud API."
759
+ };
760
+ }
761
+ }
762
+ };
763
+ }
764
+ //#endregion
765
+ //#region src/homecom/devices/homecom-climate-device.ts
766
+ var homeComClimateSchema = object({
767
+ nativeId: string().min(1),
768
+ brokerId: string().min(1),
769
+ circuitId: string().min(1),
770
+ system: literal("homecom").optional(),
771
+ integrationId: string().optional()
772
+ });
773
+ var CLIMATE_CAP = "climate-control";
774
+ var COLD_START$1 = {
775
+ mode: "off",
776
+ availableModes: [],
777
+ fanMode: "",
778
+ availableFanModes: [],
779
+ preset: "",
780
+ availablePresets: [],
781
+ target: null,
782
+ targetHigh: null,
783
+ targetLow: null,
784
+ targetHumidity: null,
785
+ currentTemp: null,
786
+ currentHumidity: null,
787
+ swingVertical: null,
788
+ swingHorizontal: null,
789
+ lastFetchedAt: 0
790
+ };
791
+ var HomeComClimateDevice = class extends BaseDevice {
792
+ features = [];
793
+ #brokerId;
794
+ #circuitId;
795
+ constructor(ctx) {
796
+ const persisted = homeComClimateSchema.parse(ctx.persistedConfig ?? {});
797
+ super(ctx, homeComClimateSchema, { type: ctx.deviceMeta.type });
798
+ this.#brokerId = persisted.brokerId;
799
+ this.#circuitId = persisted.circuitId;
800
+ this.online = true;
801
+ this.updateSourceInfo({
802
+ id: persisted.nativeId,
803
+ system: "homecom"
804
+ });
805
+ }
806
+ async onActivate() {
807
+ await super.onActivate();
808
+ this.registerClimateCap();
809
+ this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$1);
810
+ this.refreshFromSnapshot();
811
+ }
812
+ refreshFromSnapshot() {
813
+ const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
814
+ if (!circuit) {
815
+ this.runtimeState.setCapState(CLIMATE_CAP, {
816
+ ...COLD_START$1,
817
+ lastFetchedAt: Date.now()
818
+ });
819
+ return;
820
+ }
821
+ this.runtimeState.setCapState(CLIMATE_CAP, climateSliceFromCircuit(circuit, Date.now()));
822
+ }
823
+ requireClient() {
824
+ const client = homeComConnections.getClient(this.#brokerId);
825
+ if (!client) throw new Error(`HomeCom client unavailable (circuit=${this.#circuitId})`);
826
+ return client;
827
+ }
828
+ registerClimateCap() {
829
+ this.ctx.registerNativeCap(climateControlCapability, {
830
+ getStatus: async () => {
831
+ this.refreshFromSnapshot();
832
+ return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$1;
833
+ },
834
+ setMode: async ({ mode }) => {
835
+ const apiMode = hvacModeToHomeCom(mode);
836
+ await this.requireClient().setCircuitMode(this.#circuitId, apiMode);
837
+ this.refreshFromSnapshot();
838
+ },
839
+ setFanMode: async () => {
840
+ throw new Error("HomeCom climate: fan mode not supported");
841
+ },
842
+ setPreset: async () => {
843
+ throw new Error("HomeCom climate: presets not supported");
844
+ },
845
+ setTarget: async ({ target }) => {
846
+ await this.requireClient().setCircuitSetpoint(this.#circuitId, target);
847
+ this.refreshFromSnapshot();
848
+ },
849
+ setTargetRange: async () => {
850
+ throw new Error("HomeCom climate: dual setpoint not supported");
851
+ },
852
+ setTargetHumidity: async () => {
853
+ throw new Error("HomeCom climate: humidity not supported");
854
+ },
855
+ setSwingVertical: async () => {
856
+ throw new Error("HomeCom climate: swing not supported");
857
+ },
858
+ setSwingHorizontal: async () => {
859
+ throw new Error("HomeCom climate: swing not supported");
860
+ }
861
+ });
862
+ }
863
+ };
864
+ function hvacModeToHomeCom(mode) {
865
+ switch (mode) {
866
+ case "heat": return "Heat";
867
+ case "cool": return "Cool";
868
+ case "auto": return "Auto";
869
+ default: return "Off";
870
+ }
871
+ }
872
+ //#endregion
873
+ //#region src/homecom/devices/homecom-water-heater-device.ts
874
+ var homeComWaterHeaterSchema = object({
875
+ nativeId: string().min(1),
876
+ brokerId: string().min(1),
877
+ system: literal("homecom").optional(),
878
+ integrationId: string().optional()
879
+ });
880
+ var WH_CAP = "water-heater";
881
+ var COLD_START = {
882
+ currentTemp: null,
883
+ targetTemp: null,
884
+ operationMode: null,
885
+ availableModes: [],
886
+ away: null,
887
+ minTemp: null,
888
+ maxTemp: null,
889
+ lastChangedAt: 0
890
+ };
891
+ var HomeComWaterHeaterDevice = class extends BaseDevice {
892
+ features = [];
893
+ #brokerId;
894
+ constructor(ctx) {
895
+ const persisted = homeComWaterHeaterSchema.parse(ctx.persistedConfig ?? {});
896
+ super(ctx, homeComWaterHeaterSchema, { type: ctx.deviceMeta.type });
897
+ this.#brokerId = persisted.brokerId;
898
+ this.online = true;
899
+ this.updateSourceInfo({
900
+ id: persisted.nativeId,
901
+ system: "homecom"
902
+ });
903
+ }
904
+ async onActivate() {
905
+ await super.onActivate();
906
+ this.registerWaterHeaterCap();
907
+ this.runtimeState.setCapState(WH_CAP, COLD_START);
908
+ this.refreshFromSnapshot();
909
+ }
910
+ refreshFromSnapshot() {
911
+ const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
912
+ if (!dhw) {
913
+ this.runtimeState.setCapState(WH_CAP, {
914
+ ...COLD_START,
915
+ lastChangedAt: Date.now()
916
+ });
917
+ return;
918
+ }
919
+ this.runtimeState.setCapState(WH_CAP, waterHeaterSliceFromDhw(dhw, Date.now()));
920
+ }
921
+ requireClient() {
922
+ const client = homeComConnections.getClient(this.#brokerId);
923
+ if (!client) throw new Error("HomeCom client unavailable (DHW)");
924
+ return client;
925
+ }
926
+ registerWaterHeaterCap() {
927
+ this.ctx.registerNativeCap(waterHeaterCapability, {
928
+ getStatus: async () => {
929
+ this.refreshFromSnapshot();
930
+ return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
931
+ },
932
+ setTargetTemp: async ({ temp }) => {
933
+ await this.requireClient().setDhwTarget(temp);
934
+ this.refreshFromSnapshot();
935
+ },
936
+ setOperationMode: async ({ mode }) => {
937
+ await this.requireClient().setDhwMode(mode);
938
+ this.refreshFromSnapshot();
939
+ },
940
+ setAway: async () => {
941
+ throw new Error("HomeCom DHW: away mode not supported");
942
+ }
943
+ });
944
+ }
945
+ };
946
+ //#endregion
947
+ //#region src/homecom/addon.ts
948
+ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
949
+ addonId = "bosch-homecom";
950
+ providerName = "Bosch HomeCom";
951
+ deviceClasses = {
952
+ [DeviceType.Climate]: HomeComClimateDevice,
953
+ [DeviceType.WaterHeater]: HomeComWaterHeaterDevice
954
+ };
955
+ clients = null;
956
+ constructor() {
957
+ super({});
958
+ }
959
+ async onInitialize() {
960
+ const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
961
+ this.clients = new HomeComClientRegistry(this.ctx.logger);
962
+ this.clients.setOnConnected((integrationId) => {
963
+ this.setIntegrationDevicesOnline(integrationId, true);
964
+ });
965
+ this.clients.setOnDisconnected((integrationId) => {
966
+ this.setIntegrationDevicesOnline(integrationId, false);
967
+ });
968
+ await this.reconcileIntegrations();
969
+ this.subscribeIntegrationLifecycle();
970
+ return [
971
+ ...regs,
972
+ {
973
+ capability: deviceAdoptionCapability,
974
+ provider: this.buildAdoptionProvider()
975
+ },
976
+ {
977
+ capability: connectionTestCapability,
978
+ provider: this.buildConnectionTestProvider()
979
+ }
980
+ ];
981
+ }
982
+ async onShutdown() {
983
+ try {
984
+ await this.clients?.shutdown();
985
+ } catch (err) {
986
+ this.ctx.logger.warn("HomeCom shutdown error", { meta: { error: errMsg(err) } });
987
+ }
988
+ this.clients = null;
989
+ await super.onShutdown();
990
+ }
991
+ requireClients() {
992
+ if (!this.clients) throw new Error("HomeCom provider not initialised");
993
+ return this.clients;
994
+ }
995
+ generateStableId(_type, config) {
996
+ const integrationId = config?.["brokerId"];
997
+ if (typeof integrationId !== "string" || integrationId.length === 0) throw new Error("homecom generateStableId: brokerId required");
998
+ const nativeId = config?.["nativeId"];
999
+ if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("homecom generateStableId: nativeId required");
1000
+ return `homecom:${integrationId}:${nativeId}`;
1001
+ }
1002
+ async reconcileIntegrations() {
1003
+ const reg = this.requireClients();
1004
+ try {
1005
+ const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
1006
+ const surviving = /* @__PURE__ */ new Set();
1007
+ for (const integration of mine) try {
1008
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
1009
+ if (!connection) continue;
1010
+ await reg.upsert(integration.id, integration.name, connection);
1011
+ surviving.add(integration.id);
1012
+ } catch (err) {
1013
+ this.ctx.logger.warn("HomeCom reconcile failed for integration", { meta: {
1014
+ integrationId: integration.id,
1015
+ error: errMsg(err)
1016
+ } });
1017
+ }
1018
+ const vanished = reg.list().filter((id) => !surviving.has(id));
1019
+ await reg.retain(surviving);
1020
+ const deviceReg = this.ctx.kernel.deviceRegistry;
1021
+ const deviceApi = this.ctx.kernel.devices;
1022
+ if (deviceReg && deviceApi) for (const integrationId of vanished) await cascadeRemoveDevicesForIntegration({
1023
+ reg: deviceReg,
1024
+ devices: deviceApi,
1025
+ addonId: this.addonId,
1026
+ system: "homecom",
1027
+ integrationId,
1028
+ logger: this.ctx.logger
1029
+ });
1030
+ } catch (err) {
1031
+ this.ctx.logger.warn("HomeCom integration reconcile failed", { meta: { error: errMsg(err) } });
1032
+ }
1033
+ }
1034
+ subscribeIntegrationLifecycle() {
1035
+ const handler = (event) => {
1036
+ const addonId = event.data["addonId"];
1037
+ if (typeof addonId === "string" && addonId !== this.ctx.id) return;
1038
+ this.reconcileIntegrations();
1039
+ };
1040
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
1041
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
1042
+ this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
1043
+ }
1044
+ buildConnectionTestProvider() {
1045
+ return buildHomeComConnectionTestProvider({ logger: this.ctx.logger });
1046
+ }
1047
+ buildAdoptionProvider() {
1048
+ return buildHomeComAdoptionProvider({
1049
+ registry: homeComConnections,
1050
+ hasIntegration: (id) => this.requireClients().has(id),
1051
+ listIntegrations: () => this.requireClients().list(),
1052
+ listAdopted: async () => {
1053
+ const reg = this.ctx.kernel.deviceRegistry;
1054
+ const devices = this.ctx.kernel.devices;
1055
+ if (!reg || !devices) return [];
1056
+ const out = [];
1057
+ for (const d of reg.getAllForAddon(this.addonId)) {
1058
+ const config = await devices.loadConfig(d.id).catch(() => ({}));
1059
+ out.push({
1060
+ id: d.id,
1061
+ config
1062
+ });
1063
+ }
1064
+ return out;
1065
+ },
1066
+ adoptDevice: async ({ nativeId, integrationId, type, name, kind, circuitId }) => {
1067
+ const devices = this.ctx.kernel.devices;
1068
+ if (!devices) throw new Error("homecom adopt: devices unavailable");
1069
+ const config = {
1070
+ nativeId,
1071
+ brokerId: integrationId,
1072
+ system: "homecom",
1073
+ integrationId,
1074
+ name
1075
+ };
1076
+ if (kind === "heating-circuit" && circuitId) config["circuitId"] = circuitId;
1077
+ const stableId = this.generateStableId(type, config);
1078
+ const DeviceClass = this.deviceClasses[type];
1079
+ if (!DeviceClass) throw new Error(`homecom adopt: unsupported type ${type}`);
1080
+ return { deviceId: (await devices.create(stableId, DeviceClass, config, null, {
1081
+ type,
1082
+ name,
1083
+ integrationId
1084
+ })).id };
1085
+ },
1086
+ removeDevice: async (id) => {
1087
+ await this.ctx.kernel.devices?.remove(id);
1088
+ },
1089
+ findDeviceConfig: async (id) => {
1090
+ const devices = this.ctx.kernel.devices;
1091
+ if (!devices) return null;
1092
+ return devices.loadConfig(id).catch(() => null);
1093
+ },
1094
+ logger: this.ctx.logger
1095
+ });
1096
+ }
1097
+ globalSettingsSchema() {
1098
+ return buildHomeComFormSchema();
1099
+ }
1100
+ async supportsManualCreation() {
1101
+ return false;
1102
+ }
1103
+ async onGetCreationSchema(_type) {
1104
+ return null;
1105
+ }
1106
+ async onCreateDevice(_type, _config) {
1107
+ throw new Error("HomeCom devices are adopted from the cloud");
1108
+ }
1109
+ setIntegrationDevicesOnline(integrationId, online) {
1110
+ const reg = this.ctx.kernel.deviceRegistry;
1111
+ if (!reg) return;
1112
+ setIntegrationDevicesOnline(reg.getAllForAddon(this.addonId), "homecom", integrationId, online);
1113
+ }
1114
+ };
1115
+ //#endregion
1116
+ export { BoschHomeComProviderAddon };