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