@camstack/addon-provider-bosch 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { A as customAction, C as switchCapability, D as DeviceType, F as array, L as literal, O as errMsg, P as _enum, R as number, V as string, W as EventCategory, f as deviceAdoptionCapability, g as heatingValveCapability, j as defineCustomActions, l as climateControlCapability, n as BaseDeviceProvider, s as batteryCapability, t as BaseDevice, u as connectionTestCapability, w as temperatureSensorCapability, z as object } from "../dist-n4Mk3Uu1.mjs";
1
+ import { A as customAction, C as switchCapability, D as DeviceType, F as array, L as literal, O as errMsg, P as _enum, R as number, V as string, W as EventCategory, f as deviceAdoptionCapability, g as heatingValveCapability, j as defineCustomActions, l as climateControlCapability, n as BaseDeviceProvider, s as batteryCapability, t as BaseDevice, u as connectionTestCapability, w as temperatureSensorCapability, z as object } from "../dist-BV6iLwfA.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { request } from "node:https";
@@ -6,7 +6,6 @@ import { execFileSync } from "node:child_process";
6
6
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { createSocket } from "node:dgram";
10
9
  import { connect } from "node:net";
11
10
  //#region \0rolldown/runtime.js
12
11
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
@@ -333,78 +332,18 @@ async function registerShcClient(deps) {
333
332
  }
334
333
  //#endregion
335
334
  //#region src/shc/shc-controller-discovery.ts
336
- /** `prefilledConfig` flag: the scan opens the integration form instead of adopting a device. */
337
- var SHC_SETUP_INTEGRATION_KEY = "setupIntegration";
338
- /** Top bit of the question class: ask for a unicast reply (RFC 6762). */
339
- var MDNS_QU_CLASS = 32769;
340
- var SHC_MDNS_SERVICE = "_http._tcp.local";
335
+ /** The controller announces itself as an `_http._tcp` service named `Bosch SHC`. */
336
+ var SHC_MDNS_SERVICE = "_http._tcp";
341
337
  var SHC_CONTROLLER_NAME_PREFIX = "Bosch SHC";
342
- var SHC_DISCOVERY_TIMEOUT_MS = 1500;
343
- var MDNS_PORT = 5353;
344
- var MDNS_ADDRESS = "224.0.0.251";
345
- var PTR = 12;
346
- var A = 1;
347
- var SRV = 33;
338
+ /** How the scan engine finds a controller: one mDNS browse, filtered on the instance name. */
339
+ var SHC_DISCOVERY_MODES = [{
340
+ kind: "mdns",
341
+ serviceType: SHC_MDNS_SERVICE,
342
+ instancePattern: `^${SHC_CONTROLLER_NAME_PREFIX}`
343
+ }];
348
344
  function isShcControllerName(name) {
349
345
  return (name.split("._")[0] ?? name).toLowerCase().startsWith(SHC_CONTROLLER_NAME_PREFIX.toLowerCase());
350
346
  }
351
- function readName(buf, start) {
352
- const labels = [];
353
- let pos = start;
354
- let end = start;
355
- let jumped = false;
356
- for (let hops = 0; hops < 16; hops++) {
357
- if (pos < 0 || pos >= buf.length) return null;
358
- const len = buf[pos] ?? 0;
359
- if (len === 0) {
360
- if (!jumped) end = pos + 1;
361
- return {
362
- name: labels.join("."),
363
- end
364
- };
365
- }
366
- if ((len & 192) === 192) {
367
- if (pos + 1 >= buf.length) return null;
368
- if (!jumped) end = pos + 2;
369
- jumped = true;
370
- pos = (len & 63) << 8 | (buf[pos + 1] ?? 0);
371
- continue;
372
- }
373
- if ((len & 192) !== 0 || pos + 1 + len > buf.length) return null;
374
- labels.push(buf.subarray(pos + 1, pos + 1 + len).toString("utf8"));
375
- pos += 1 + len;
376
- if (!jumped) end = pos;
377
- }
378
- return null;
379
- }
380
- function readRecords(buf, offset, count) {
381
- const records = [];
382
- let pos = offset;
383
- for (let i = 0; i < count; i++) {
384
- const name = readName(buf, pos);
385
- if (name === null || name.end + 10 > buf.length) return null;
386
- const type = buf.readUInt16BE(name.end);
387
- const dataLength = buf.readUInt16BE(name.end + 8);
388
- const dataOffset = name.end + 10;
389
- if (dataOffset + dataLength > buf.length) return null;
390
- records.push({
391
- name: name.name,
392
- type,
393
- message: buf,
394
- dataOffset,
395
- dataLength
396
- });
397
- pos = dataOffset + dataLength;
398
- }
399
- return {
400
- records,
401
- next: pos
402
- };
403
- }
404
- function ipv4(buf, offset, length) {
405
- if (length !== 4 || offset + 4 > buf.length) return null;
406
- return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
407
- }
408
347
  /** Identity used to recognise a controller already stored on an integration. */
409
348
  function shcControllerId(input) {
410
349
  const service = input.serviceHost?.trim().toLowerCase() ?? "";
@@ -413,40 +352,24 @@ function shcControllerId(input) {
413
352
  if (serial.length > 0) return serial;
414
353
  return input.host;
415
354
  }
416
- function hostsFromRecords(records) {
417
- const instances = [];
418
- const srv = /* @__PURE__ */ new Map();
419
- const addresses = /* @__PURE__ */ new Map();
420
- for (const rec of records) {
421
- const key = rec.name.toLowerCase();
422
- if (rec.type === PTR) {
423
- const target = readName(rec.message, rec.dataOffset);
424
- if (target !== null && isShcControllerName(target.name)) instances.push(target.name);
425
- } else if (rec.type === SRV && rec.dataLength >= 6) {
426
- const target = readName(rec.message, rec.dataOffset + 6);
427
- if (target !== null) srv.set(key, target.name);
428
- } else if (rec.type === A) {
429
- const ip = ipv4(rec.message, rec.dataOffset, rec.dataLength);
430
- if (ip !== null) addresses.set(key, ip);
431
- }
432
- }
355
+ /** Controllers among the engine's mDNS hits (a `Bosch SHC…` instance), one per host. */
356
+ function shcHostsFromHits(hits) {
433
357
  const out = [];
434
358
  const seen = /* @__PURE__ */ new Set();
435
- for (const instance of instances) {
436
- const viaSrv = srv.get(instance.toLowerCase());
437
- const host = (viaSrv !== void 0 ? addresses.get(viaSrv.toLowerCase()) : void 0) ?? addresses.get(instance.toLowerCase());
438
- if (host === void 0 || seen.has(host)) continue;
439
- seen.add(host);
440
- const label = instance.split("._")[0] ?? instance;
441
- const id = shcControllerId({
442
- name: label,
443
- host,
444
- ...viaSrv !== void 0 ? { serviceHost: viaSrv } : {}
445
- });
359
+ for (const hit of hits) {
360
+ const mdns = hit.mdns;
361
+ if (mdns === void 0 || !isShcControllerName(mdns.instance)) continue;
362
+ if (hit.host.length === 0 || seen.has(hit.host)) continue;
363
+ seen.add(hit.host);
364
+ const name = mdns.instance.split("._")[0] ?? mdns.instance;
446
365
  out.push({
447
- host,
448
- name: label,
449
- id
366
+ host: hit.host,
367
+ name,
368
+ id: shcControllerId({
369
+ name,
370
+ host: hit.host,
371
+ serviceHost: mdns.host
372
+ })
450
373
  });
451
374
  }
452
375
  return out;
@@ -466,11 +389,10 @@ function proposeShcControllers(found, existingHosts) {
466
389
  function shcDiscoveryCandidates(proposals) {
467
390
  return proposals.map((found) => ({
468
391
  stableId: `bosch-shc:${found.id}`,
392
+ type: DeviceType.Gateway,
469
393
  suggestedName: found.name,
470
- prefilledConfig: {
471
- host: found.host,
472
- [SHC_SETUP_INTEGRATION_KEY]: true
473
- }
394
+ prefilledConfig: { host: found.host },
395
+ setupIntegration: true
474
396
  }));
475
397
  }
476
398
  function shcDiscoverySummary(foundCount, proposedCount) {
@@ -478,101 +400,6 @@ function shcDiscoverySummary(foundCount, proposedCount) {
478
400
  if (proposedCount === 0) return "No controller found on the network.";
479
401
  return `Found ${proposedCount} ${proposedCount === 1 ? "controller" : "controllers"}.`;
480
402
  }
481
- /** Hosts advertised as a Bosch Smart Home Controller on one mDNS message. */
482
- function shcHostsFromDnsMessage(buf) {
483
- if (buf.length < 12) return [];
484
- const questions = buf.readUInt16BE(4);
485
- const answers = buf.readUInt16BE(6);
486
- const authorities = buf.readUInt16BE(8);
487
- const extra = buf.readUInt16BE(10);
488
- let pos = 12;
489
- for (let i = 0; i < questions; i++) {
490
- const name = readName(buf, pos);
491
- if (name === null) return [];
492
- pos = name.end + 4;
493
- if (pos > buf.length) return [];
494
- }
495
- const answerRecords = readRecords(buf, pos, answers);
496
- if (answerRecords === null) return [];
497
- const authorityRecords = readRecords(buf, answerRecords.next, authorities);
498
- if (authorityRecords === null) return [];
499
- const extraRecords = readRecords(buf, authorityRecords.next, extra);
500
- if (extraRecords === null) return [];
501
- return hostsFromRecords([
502
- ...answerRecords.records,
503
- ...authorityRecords.records,
504
- ...extraRecords.records
505
- ]);
506
- }
507
- function mdnsPtrQuery(service) {
508
- const encoded = service.split(".").filter((part) => part.length > 0).map((label) => Buffer.concat([Buffer.from([label.length]), Buffer.from(label)]));
509
- const header = Buffer.alloc(12);
510
- header.writeUInt16BE(1, 4);
511
- const tail = Buffer.alloc(4);
512
- tail.writeUInt16BE(PTR, 0);
513
- tail.writeUInt16BE(MDNS_QU_CLASS, 2);
514
- return Buffer.concat([
515
- header,
516
- ...encoded,
517
- Buffer.from([0]),
518
- tail
519
- ]);
520
- }
521
- function closeQuietly(socket) {
522
- try {
523
- socket.close();
524
- } catch {}
525
- }
526
- function exchangeMdns(query, timeoutMs) {
527
- return new Promise((resolve) => {
528
- let socket;
529
- try {
530
- socket = createSocket({
531
- type: "udp4",
532
- reuseAddr: true
533
- });
534
- } catch {
535
- resolve([]);
536
- return;
537
- }
538
- const packets = [];
539
- let settled = false;
540
- const finish = () => {
541
- if (settled) return;
542
- settled = true;
543
- closeQuietly(socket);
544
- resolve(packets);
545
- };
546
- socket.on("error", () => finish());
547
- socket.on("message", (msg) => {
548
- packets.push(Buffer.from(msg));
549
- });
550
- socket.bind(0, () => {
551
- try {
552
- socket.send(query, MDNS_PORT, MDNS_ADDRESS);
553
- } catch {
554
- finish();
555
- }
556
- });
557
- setTimeout(finish, timeoutMs);
558
- });
559
- }
560
- async function browseShcControllers(exchange = exchangeMdns) {
561
- let packets;
562
- try {
563
- packets = await exchange(mdnsPtrQuery(SHC_MDNS_SERVICE), SHC_DISCOVERY_TIMEOUT_MS);
564
- } catch {
565
- return [];
566
- }
567
- const hosts = [];
568
- const seen = /* @__PURE__ */ new Set();
569
- for (const packet of packets) for (const found of shcHostsFromDnsMessage(packet)) {
570
- if (seen.has(found.host)) continue;
571
- seen.add(found.host);
572
- hosts.push(found);
573
- }
574
- return hosts;
575
- }
576
403
  /** Manual host wins. Otherwise the address discovery filled in. */
577
404
  function resolveShcPairingHost(input) {
578
405
  const manual = input.hostManual?.trim() ?? "";
@@ -582,6 +409,137 @@ function resolveShcPairingHost(input) {
582
409
  throw new Error("Enter the controller host or IP address.");
583
410
  }
584
411
  //#endregion
412
+ //#region src/shc/shc-gateway.ts
413
+ /** In-process resolver: integrationId → live SHC connection surface. */
414
+ var ShcConnectionResolver = class {
415
+ #surfaces = /* @__PURE__ */ new Map();
416
+ set(integrationId, surface) {
417
+ if (surface === null) {
418
+ this.#surfaces.delete(integrationId);
419
+ return;
420
+ }
421
+ this.#surfaces.set(integrationId, surface);
422
+ }
423
+ has(integrationId) {
424
+ return this.#surfaces.has(integrationId);
425
+ }
426
+ devices(integrationId) {
427
+ return this.#surfaces.get(integrationId)?.devices ?? [];
428
+ }
429
+ getClient(integrationId) {
430
+ return this.#surfaces.get(integrationId)?.client ?? null;
431
+ }
432
+ list() {
433
+ return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
434
+ }
435
+ clear() {
436
+ for (const surface of this.#surfaces.values()) surface.client.close();
437
+ this.#surfaces.clear();
438
+ }
439
+ };
440
+ var shcConnections = new ShcConnectionResolver();
441
+ //#endregion
442
+ //#region src/shc/devices/shc-device-base.ts
443
+ var shcDeviceSchema = object({
444
+ shcDeviceId: string().min(1).describe("Bosch SHC device id"),
445
+ brokerId: string().min(1).describe("Integration id"),
446
+ deviceModel: string().optional(),
447
+ system: literal("bosch-shc").optional(),
448
+ integrationId: string().optional(),
449
+ tempStep: number().positive().optional()
450
+ });
451
+ var ShcBaseDevice = class extends BaseDevice {
452
+ features = [];
453
+ shcDeviceId;
454
+ brokerId;
455
+ deviceModel;
456
+ stateUnsub = null;
457
+ constructor(ctx) {
458
+ const persisted = shcDeviceSchema.parse(ctx.persistedConfig ?? {});
459
+ super(ctx, shcDeviceSchema, { type: ctx.deviceMeta.type });
460
+ this.shcDeviceId = persisted.shcDeviceId;
461
+ this.brokerId = persisted.brokerId;
462
+ this.deviceModel = persisted.deviceModel;
463
+ this.online = shcConnections.has(this.brokerId);
464
+ this.updateSourceInfo({
465
+ id: this.shcDeviceId,
466
+ system: "bosch-shc"
467
+ });
468
+ }
469
+ resolveClient() {
470
+ return shcConnections.getClient(this.brokerId);
471
+ }
472
+ requireClient() {
473
+ const client = this.resolveClient();
474
+ if (client === null) throw new Error(`SHC client unavailable (device=${this.shcDeviceId})`);
475
+ return client;
476
+ }
477
+ async onActivate() {
478
+ await super.onActivate();
479
+ this.registerCaps();
480
+ this.attachStateListener();
481
+ await this.refreshFromController();
482
+ }
483
+ async removeDevice() {
484
+ if (this.stateUnsub) {
485
+ try {
486
+ this.stateUnsub();
487
+ } catch {}
488
+ this.stateUnsub = null;
489
+ }
490
+ }
491
+ attachStateListener() {
492
+ const client = this.resolveClient();
493
+ if (client === null) return;
494
+ const onEvent = (event) => {
495
+ if (event.deviceId !== this.shcDeviceId) return;
496
+ this.refreshFromController().catch((err) => {
497
+ this.deviceLog("warn", "SHC refresh after controller event failed", {
498
+ shcDeviceId: this.shcDeviceId,
499
+ error: errMsg(err)
500
+ });
501
+ });
502
+ };
503
+ this.stateUnsub = client.onStateChange(onEvent);
504
+ }
505
+ deviceLog(level, message, meta) {
506
+ const extras = {
507
+ tags: { deviceId: this.ctx.id },
508
+ ...meta === void 0 ? {} : { meta }
509
+ };
510
+ if (level === "warn") this.ctx.logger.warn(message, extras);
511
+ else this.ctx.logger.info(message, extras);
512
+ }
513
+ unsupported(surface) {
514
+ throw new Error(`bosch-shc ${this.shcDeviceId}: ${surface} not supported`);
515
+ }
516
+ };
517
+ //#endregion
518
+ //#region src/shc/devices/shc-gateway-device.ts
519
+ /**
520
+ * The Smart Home Controller box, as a {@link DeviceType.Gateway}. Its connection
521
+ * lives on the INTEGRATION; this device is the box's presence in the device list
522
+ * and follows the session's online state (the integration's connect and
523
+ * disconnect callbacks flip it).
524
+ *
525
+ * It registers no action: the controller's local API offers no reboot or update
526
+ * and the box serves no web page, so there is nothing real to expose. Rooms and
527
+ * devices are NOT children of it; they are adopted as their own top-level
528
+ * devices of the same integration.
529
+ */
530
+ var ShcGatewayDevice = class extends BaseDevice {
531
+ features = [];
532
+ constructor(ctx) {
533
+ const cfg = shcDeviceSchema.parse(ctx.persistedConfig ?? {});
534
+ super(ctx, shcDeviceSchema, { type: DeviceType.Gateway });
535
+ this.online = shcConnections.has(cfg.brokerId);
536
+ this.updateSourceInfo({
537
+ id: cfg.shcDeviceId,
538
+ system: "bosch-shc"
539
+ });
540
+ }
541
+ };
542
+ //#endregion
585
543
  //#region src/shc/shc-domain-mapping.ts
586
544
  var ROOM_CLIMATE_MODEL = "ROOM_CLIMATE_CONTROL";
587
545
  /** Classify a normalised device summary into an adoption / driver kind. */
@@ -704,6 +662,11 @@ function buildRoomClimatePutBody(input) {
704
662
  }
705
663
  //#endregion
706
664
  //#region src/shc/shc-discovery.ts
665
+ var GATEWAY_SUFFIX = ":controller";
666
+ /** The `childNativeId` (and `shcDeviceId`) of the controller box itself, one per integration. */
667
+ function shcGatewayNativeId(integrationId) {
668
+ return `${integrationId}${GATEWAY_SUFFIX}`;
669
+ }
707
670
  function shouldSkipShcDevice(device) {
708
671
  if (classifyShcDevice(device) === "unsupported") return true;
709
672
  if (device.deviceModel.startsWith("HOMECONNECT_")) return true;
@@ -798,7 +761,21 @@ function leafCandidate(device, kind, adoptedId) {
798
761
  /** Map live SHC discovery rows to adoption candidates. Pure — no I/O. */
799
762
  function buildShcCandidates(input) {
800
763
  const linkedTrvs = collectTrvsLinkedToRoomClimate(input.devices);
801
- const out = [];
764
+ const gatewayId = shcGatewayNativeId(input.integrationId);
765
+ const gatewayAdopted = input.adopted.get(gatewayId) ?? null;
766
+ const out = [{
767
+ childNativeId: gatewayId,
768
+ name: input.integrationName,
769
+ type: DeviceType.Gateway,
770
+ status: "online",
771
+ metadata: {
772
+ manufacturer: "Bosch",
773
+ model: "Smart Home Controller"
774
+ },
775
+ alreadyAdopted: gatewayAdopted !== null,
776
+ adoptedDeviceId: gatewayAdopted,
777
+ capabilities: []
778
+ }];
802
779
  for (const device of input.devices) {
803
780
  if (shouldSkipShcDevice(device)) continue;
804
781
  const kind = classifyShcDevice(device);
@@ -828,112 +805,6 @@ function shcStableIdSuffixFromDeviceId(shcDeviceId) {
828
805
  return shcDeviceId.replace(/[^a-zA-Z0-9._-]+/g, "_");
829
806
  }
830
807
  //#endregion
831
- //#region src/shc/shc-gateway.ts
832
- /** In-process resolver: integrationId → live SHC connection surface. */
833
- var ShcConnectionResolver = class {
834
- #surfaces = /* @__PURE__ */ new Map();
835
- set(integrationId, surface) {
836
- if (surface === null) {
837
- this.#surfaces.delete(integrationId);
838
- return;
839
- }
840
- this.#surfaces.set(integrationId, surface);
841
- }
842
- has(integrationId) {
843
- return this.#surfaces.has(integrationId);
844
- }
845
- devices(integrationId) {
846
- return this.#surfaces.get(integrationId)?.devices ?? [];
847
- }
848
- getClient(integrationId) {
849
- return this.#surfaces.get(integrationId)?.client ?? null;
850
- }
851
- list() {
852
- return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
853
- }
854
- clear() {
855
- for (const surface of this.#surfaces.values()) surface.client.close();
856
- this.#surfaces.clear();
857
- }
858
- };
859
- var shcConnections = new ShcConnectionResolver();
860
- //#endregion
861
- //#region src/shc/devices/shc-device-base.ts
862
- var shcDeviceSchema = object({
863
- shcDeviceId: string().min(1).describe("Bosch SHC device id"),
864
- brokerId: string().min(1).describe("Integration id"),
865
- deviceModel: string().optional(),
866
- system: literal("bosch-shc").optional(),
867
- integrationId: string().optional(),
868
- tempStep: number().positive().optional()
869
- });
870
- var ShcBaseDevice = class extends BaseDevice {
871
- features = [];
872
- shcDeviceId;
873
- brokerId;
874
- deviceModel;
875
- stateUnsub = null;
876
- constructor(ctx) {
877
- const persisted = shcDeviceSchema.parse(ctx.persistedConfig ?? {});
878
- super(ctx, shcDeviceSchema, { type: ctx.deviceMeta.type });
879
- this.shcDeviceId = persisted.shcDeviceId;
880
- this.brokerId = persisted.brokerId;
881
- this.deviceModel = persisted.deviceModel;
882
- this.online = shcConnections.has(this.brokerId);
883
- this.updateSourceInfo({
884
- id: this.shcDeviceId,
885
- system: "bosch-shc"
886
- });
887
- }
888
- resolveClient() {
889
- return shcConnections.getClient(this.brokerId);
890
- }
891
- requireClient() {
892
- const client = this.resolveClient();
893
- if (client === null) throw new Error(`SHC client unavailable (device=${this.shcDeviceId})`);
894
- return client;
895
- }
896
- async onActivate() {
897
- await super.onActivate();
898
- this.registerCaps();
899
- this.attachStateListener();
900
- await this.refreshFromController();
901
- }
902
- async removeDevice() {
903
- if (this.stateUnsub) {
904
- try {
905
- this.stateUnsub();
906
- } catch {}
907
- this.stateUnsub = null;
908
- }
909
- }
910
- attachStateListener() {
911
- const client = this.resolveClient();
912
- if (client === null) return;
913
- const onEvent = (event) => {
914
- if (event.deviceId !== this.shcDeviceId) return;
915
- this.refreshFromController().catch((err) => {
916
- this.deviceLog("warn", "SHC refresh after controller event failed", {
917
- shcDeviceId: this.shcDeviceId,
918
- error: errMsg(err)
919
- });
920
- });
921
- };
922
- this.stateUnsub = client.onStateChange(onEvent);
923
- }
924
- deviceLog(level, message, meta) {
925
- const extras = {
926
- tags: { deviceId: this.ctx.id },
927
- ...meta === void 0 ? {} : { meta }
928
- };
929
- if (level === "warn") this.ctx.logger.warn(message, extras);
930
- else this.ctx.logger.info(message, extras);
931
- }
932
- unsupported(surface) {
933
- throw new Error(`bosch-shc ${this.shcDeviceId}: ${surface} not supported`);
934
- }
935
- };
936
- //#endregion
937
808
  //#region src/shc/shc-climate-schedules.ts
938
809
  /** Heating profiles from `GET /climate/schedule/{id}/HEATING`. */
939
810
  function heatingProfilesFromSchedule(body) {
@@ -1301,11 +1172,15 @@ async function adoptedMapForIntegration(integrationId, listAdoptedShc) {
1301
1172
  return map;
1302
1173
  }
1303
1174
  function buildShcAdoptionProvider(deps) {
1304
- const { registry, hasIntegration, listIntegrations, listAdoptedShc, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
1175
+ const { registry, hasIntegration, listIntegrations, integrationName, listAdoptedShc, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
1305
1176
  async function allCandidatesForIntegration(integrationId) {
1177
+ const devices = registry.devices(integrationId);
1178
+ const adopted = await adoptedMapForIntegration(integrationId, listAdoptedShc);
1306
1179
  return buildShcCandidates({
1307
- devices: registry.devices(integrationId),
1308
- adopted: await adoptedMapForIntegration(integrationId, listAdoptedShc)
1180
+ integrationId,
1181
+ integrationName: integrationName(integrationId) ?? "Bosch SHC",
1182
+ devices,
1183
+ adopted
1309
1184
  });
1310
1185
  }
1311
1186
  return {
@@ -1366,6 +1241,22 @@ function buildShcAdoptionProvider(deps) {
1366
1241
  const adopted = [];
1367
1242
  let failures = 0;
1368
1243
  for (const shcDeviceId of childNativeIds) try {
1244
+ if (shcDeviceId === shcGatewayNativeId(integrationId)) {
1245
+ const name = perCandidate?.[shcDeviceId]?.name ?? integrationName(integrationId) ?? "Bosch SHC";
1246
+ const { deviceId } = await adoptDevice({
1247
+ shcDeviceId,
1248
+ integrationId,
1249
+ type: DeviceType.Gateway,
1250
+ name,
1251
+ deviceModel: "Smart Home Controller"
1252
+ });
1253
+ adopted.push({
1254
+ childNativeId: shcDeviceId,
1255
+ parentDeviceId: deviceId,
1256
+ accessoryDeviceIds: []
1257
+ });
1258
+ continue;
1259
+ }
1369
1260
  const dev = devices.find((d) => d.id === shcDeviceId);
1370
1261
  if (dev === void 0) {
1371
1262
  failures++;
@@ -1410,7 +1301,7 @@ function buildShcAdoptionProvider(deps) {
1410
1301
  const integrationId = String(cfg["brokerId"]);
1411
1302
  const shcDeviceId = String(cfg["shcDeviceId"]);
1412
1303
  if (!hasIntegration(integrationId)) throw new Error(`bosch-shc resync: integration ${integrationId} not connected`);
1413
- if (!registry.devices(integrationId).some((d) => d.id === shcDeviceId)) throw new Error(`bosch-shc resync: device ${shcDeviceId} no longer on integration`);
1304
+ if (!(shcDeviceId === shcGatewayNativeId(integrationId) || registry.devices(integrationId).some((d) => d.id === shcDeviceId))) throw new Error(`bosch-shc resync: device ${shcDeviceId} no longer on integration`);
1414
1305
  return {
1415
1306
  changed: false,
1416
1307
  rebuiltChildren: 0
@@ -23635,6 +23526,10 @@ var ShcClientRegistry = class {
23635
23526
  makeClient: opts.makeClient ?? this.#makeClient
23636
23527
  }));
23637
23528
  }
23529
+ /** The integration's display name, or null when it is not registered. */
23530
+ nameOf(integrationId) {
23531
+ return this.#entries.get(integrationId)?.manager.getInfo().name ?? null;
23532
+ }
23638
23533
  setOnConnected(cb) {
23639
23534
  this.#onConnected = cb;
23640
23535
  }
@@ -23830,6 +23725,7 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23830
23725
  addonId = "bosch-shc";
23831
23726
  providerName = "Bosch SHC";
23832
23727
  deviceClasses = {
23728
+ [DeviceType.Gateway]: ShcGatewayDevice,
23833
23729
  [DeviceType.Container]: ShcClimateRoomDevice,
23834
23730
  [DeviceType.Climate]: ShcRoomClimateDevice,
23835
23731
  [DeviceType.Thermostat]: ShcTrvDevice,
@@ -23865,14 +23761,15 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23865
23761
  customActions: boschShcActions,
23866
23762
  actionHandlers: {
23867
23763
  discoverController: async () => {
23868
- const { proposals, summary } = await this.discoverShcControllers();
23764
+ const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
23765
+ const hosts = candidates.map((candidate) => ({
23766
+ host: String(candidate.prefilledConfig["host"] ?? ""),
23767
+ name: candidate.suggestedName,
23768
+ id: candidate.stableId.replace(/^bosch-shc:/, "")
23769
+ }));
23869
23770
  return {
23870
- hosts: proposals.map((found) => ({
23871
- host: found.host,
23872
- name: found.name,
23873
- id: found.id
23874
- })),
23875
- summary
23771
+ hosts,
23772
+ summary: shcDiscoverySummary(hosts.length, hosts.length)
23876
23773
  };
23877
23774
  },
23878
23775
  registerClient: async (input) => {
@@ -23908,24 +23805,21 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23908
23805
  async supportsManualCreation() {
23909
23806
  return false;
23910
23807
  }
23911
- /**
23912
- * LAN mDNS probe. This addon is hub-only, so the socket is the hub's.
23913
- * The operator's hub is on br0 (its own LAN address), the same path the
23914
- * hub already uses to announce itself — multicast UDP 5353 reaches the
23915
- * LAN. The probe asks for a unicast reply and does not bind 5353, which
23916
- * the hub's announcer holds. Agents do not run this addon.
23917
- */
23808
+ /** A controller is found by the scan engine's mDNS browse; this addon only classifies the hits. */
23918
23809
  async supportsDiscovery() {
23919
23810
  return true;
23920
23811
  }
23921
- async discoverDevices() {
23922
- const { proposals } = await this.discoverShcControllers();
23923
- return shcDiscoveryCandidates(proposals).map((candidate) => ({
23924
- stableId: candidate.stableId,
23925
- type: DeviceType.Hub,
23926
- suggestedName: candidate.suggestedName,
23927
- prefilledConfig: { ...candidate.prefilledConfig }
23928
- }));
23812
+ async getDiscoveryModes() {
23813
+ return SHC_DISCOVERY_MODES;
23814
+ }
23815
+ async classifyDiscoveryHits(input) {
23816
+ const found = shcHostsFromHits(input.hits);
23817
+ const proposals = proposeShcControllers(found, await this.knownControllerHosts());
23818
+ if (found.length > proposals.length) this.ctx.logger.info("SHC discovery skipped controllers already added", { meta: {
23819
+ found: found.length,
23820
+ proposed: proposals.length
23821
+ } });
23822
+ return shcDiscoveryCandidates(proposals);
23929
23823
  }
23930
23824
  async adoptDiscoveredDevice(_input) {
23931
23825
  throw new Error("Bosch SHC controllers are paired from the integration form");
@@ -23943,24 +23837,6 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23943
23837
  if (typeof shcDeviceId !== "string" || shcDeviceId.length === 0) throw new Error("bosch-shc generateStableId: shcDeviceId required");
23944
23838
  return `bosch-shc:${integrationId}:${shcDeviceId}`;
23945
23839
  }
23946
- async discoverShcControllers() {
23947
- let found = [];
23948
- try {
23949
- found = await browseShcControllers();
23950
- } catch (err) {
23951
- this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: errMsg(err) } });
23952
- }
23953
- const known = await this.knownControllerHosts();
23954
- const proposals = proposeShcControllers(found, known);
23955
- if (found.length > proposals.length) this.ctx.logger.info("SHC discovery skipped controllers already added", { meta: {
23956
- found: found.length,
23957
- proposed: proposals.length
23958
- } });
23959
- return {
23960
- proposals,
23961
- summary: shcDiscoverySummary(found.length, proposals.length)
23962
- };
23963
- }
23964
23840
  async knownControllerHosts() {
23965
23841
  try {
23966
23842
  const all = await this.ctx.api.integrations.list.query();
@@ -24041,6 +23917,7 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
24041
23917
  logger: this.ctx.logger,
24042
23918
  hasIntegration: (integrationId) => this.requireClients().has(integrationId),
24043
23919
  listIntegrations: () => this.requireClients().list(),
23920
+ integrationName: (integrationId) => this.requireClients().nameOf(integrationId),
24044
23921
  listAdoptedShc: async () => {
24045
23922
  const reg = this.ctx.kernel.deviceRegistry;
24046
23923
  const devices = this.ctx.kernel.devices;
@@ -24060,8 +23937,25 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
24060
23937
  const devices = this.ctx.kernel.devices;
24061
23938
  if (!devices) throw new Error("bosch-shc adopt: kernel.devices unavailable");
24062
23939
  const summary = shcConnections.devices(integrationId).find((d) => d.id === shcDeviceId);
24063
- const kind = summary ? classifyShcDevice(summary) : "unsupported";
23940
+ const kind = type === DeviceType.Gateway ? "gateway" : summary ? classifyShcDevice(summary) : "unsupported";
24064
23941
  if (kind === "unsupported") throw new Error(`bosch-shc adopt: unsupported device ${shcDeviceId}`);
23942
+ if (type === DeviceType.Gateway) {
23943
+ const config = {
23944
+ shcDeviceId,
23945
+ brokerId: integrationId,
23946
+ deviceModel,
23947
+ system: "bosch-shc",
23948
+ integrationId,
23949
+ name
23950
+ };
23951
+ const stableId = this.generateStableId(DeviceType.Gateway, config);
23952
+ const meta = {
23953
+ type: DeviceType.Gateway,
23954
+ name,
23955
+ integrationId
23956
+ };
23957
+ return { deviceId: (await devices.create(stableId, ShcGatewayDevice, config, null, meta)).id };
23958
+ }
24065
23959
  const baseConfig = {
24066
23960
  shcDeviceId,
24067
23961
  brokerId: integrationId,