@camstack/addon-provider-bosch 0.1.25 → 0.1.27

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,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("../dist-Csx3t_1B.js");
3
- const require_account_integration_offline = require("../account-integration-offline-BPV9TTp1.js");
2
+ const require_dist = require("../dist-DEZDBpGW.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-2MI94wOE.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_tls = require("node:tls");
6
6
  let node_zlib = require("node:zlib");
@@ -1,5 +1,5 @@
1
- import { A as isSecretMarker, B as unknown, D as createEvent, E as errMsg, I as object, L as record, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, _ as powerMeterCapability, b as runningStateCapability, c as connectionTestCapability, d as dishwasherCapability, f as enumSensorCapability, g as numericSensorCapability, h as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarkerLike, k as defineCustomActions, l as consumablesCapability, n as BaseDeviceProvider, o as binaryCapability, p as eventEmitterCapability, r as DishwasherOperationStateSchema, t as BaseDevice, u as deviceAdoptionCapability, v as quantityFromUnit, w as DeviceRole, y as runningFromDishwasherOperation, z as union } from "../dist-DPDLG8_g.mjs";
2
- import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-BcUPRaQD.mjs";
1
+ import { A as isSecretMarker, B as unknown, D as createEvent, E as errMsg, I as object, L as record, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, _ as powerMeterCapability, b as runningStateCapability, c as connectionTestCapability, d as dishwasherCapability, f as enumSensorCapability, g as numericSensorCapability, h as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarkerLike, k as defineCustomActions, l as consumablesCapability, n as BaseDeviceProvider, o as binaryCapability, p as eventEmitterCapability, r as DishwasherOperationStateSchema, t as BaseDevice, u as deviceAdoptionCapability, v as quantityFromUnit, w as DeviceRole, y as runningFromDishwasherOperation, z as union } from "../dist-Cbcvh8mB.mjs";
2
+ import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-DKaazSyb.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { connect } from "node:tls";
5
5
  import { inflateRawSync } from "node:zlib";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("../dist-Csx3t_1B.js");
3
- const require_account_integration_offline = require("../account-integration-offline-BPV9TTp1.js");
2
+ const require_dist = require("../dist-DEZDBpGW.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-2MI94wOE.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  //#region src/homecom/config.ts
6
6
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -322,16 +322,20 @@ function readNum(raw) {
322
322
  //#endregion
323
323
  //#region src/homecom/homecom-oauth.ts
324
324
  /**
325
- * Public OAuth client of the HomeCom Easy / PointT mobile app, signed in through SingleKey ID.
325
+ * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
326
326
  *
327
- * These are the PUBLIC values the app itself ships (a PKCE client has no secret). They are not
328
- * derivable from this repository; they come from the community PointT client (`homecom_alt`) and
329
- * have to be confirmed against a live account — see docs/addons/addon-provider-bosch-homecom.md.
327
+ * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
328
+ * today: the same authorize query the MIT-licensed `homecom_alt` client records
329
+ * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
330
+ * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
331
+ * answers `misconfigured-application` and never shows the login page. The scope it does accept
332
+ * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
330
333
  */
331
334
  var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
332
335
  var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
333
336
  var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
334
337
  var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
338
+ var HOMECOM_STYLE_ID = "tt_bsch";
335
339
  var HOMECOM_SCOPE = [
336
340
  "openid",
337
341
  "email",
@@ -341,7 +345,7 @@ var HOMECOM_SCOPE = [
341
345
  "pointt.gateway.removal",
342
346
  "pointt.gateway.list",
343
347
  "pointt.gateway.users",
344
- "pointt.gateway.resource.dopair",
348
+ "pointt.gateway.resource.dashapp",
345
349
  "pointt.castt.flow.token-exchange",
346
350
  "bacon",
347
351
  "hcc.tariff.read"
@@ -557,6 +561,7 @@ function beginHomeComSignIn() {
557
561
  url.searchParams.set("state", state);
558
562
  url.searchParams.set("nonce", b64url((0, node_crypto.randomBytes)(16)));
559
563
  url.searchParams.set("prompt", "login");
564
+ url.searchParams.set("style_id", HOMECOM_STYLE_ID);
560
565
  return {
561
566
  authorizeUrl: url.toString(),
562
567
  verifier,
@@ -1,5 +1,5 @@
1
- import { A as isSecretMarker, C as waterHeaterCapability, E as errMsg, F as number, I as object, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, c as connectionTestCapability, j as isSecretMarkerLike, k as defineCustomActions, n as BaseDeviceProvider, s as climateControlCapability, t as BaseDevice, u as deviceAdoptionCapability } from "../dist-DPDLG8_g.mjs";
2
- import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-BcUPRaQD.mjs";
1
+ import { A as isSecretMarker, C as waterHeaterCapability, E as errMsg, F as number, I as object, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, c as connectionTestCapability, j as isSecretMarkerLike, k as defineCustomActions, n as BaseDeviceProvider, s as climateControlCapability, t as BaseDevice, u as deviceAdoptionCapability } from "../dist-Cbcvh8mB.mjs";
2
+ import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-DKaazSyb.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  //#region src/homecom/config.ts
5
5
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -321,16 +321,20 @@ function readNum(raw) {
321
321
  //#endregion
322
322
  //#region src/homecom/homecom-oauth.ts
323
323
  /**
324
- * Public OAuth client of the HomeCom Easy / PointT mobile app, signed in through SingleKey ID.
324
+ * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
325
325
  *
326
- * These are the PUBLIC values the app itself ships (a PKCE client has no secret). They are not
327
- * derivable from this repository; they come from the community PointT client (`homecom_alt`) and
328
- * have to be confirmed against a live account — see docs/addons/addon-provider-bosch-homecom.md.
326
+ * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
327
+ * today: the same authorize query the MIT-licensed `homecom_alt` client records
328
+ * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
329
+ * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
330
+ * answers `misconfigured-application` and never shows the login page. The scope it does accept
331
+ * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
329
332
  */
330
333
  var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
331
334
  var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
332
335
  var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
333
336
  var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
337
+ var HOMECOM_STYLE_ID = "tt_bsch";
334
338
  var HOMECOM_SCOPE = [
335
339
  "openid",
336
340
  "email",
@@ -340,7 +344,7 @@ var HOMECOM_SCOPE = [
340
344
  "pointt.gateway.removal",
341
345
  "pointt.gateway.list",
342
346
  "pointt.gateway.users",
343
- "pointt.gateway.resource.dopair",
347
+ "pointt.gateway.resource.dashapp",
344
348
  "pointt.castt.flow.token-exchange",
345
349
  "bacon",
346
350
  "hcc.tariff.read"
@@ -556,6 +560,7 @@ function beginHomeComSignIn() {
556
560
  url.searchParams.set("state", state);
557
561
  url.searchParams.set("nonce", b64url(randomBytes(16)));
558
562
  url.searchParams.set("prompt", "login");
563
+ url.searchParams.set("style_id", HOMECOM_STYLE_ID);
559
564
  return {
560
565
  authorizeUrl: url.toString(),
561
566
  verifier,
package/dist/shc/addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region \0rolldown/runtime.js
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  //#endregion
5
- const require_dist = require("../dist-Csx3t_1B.js");
5
+ const require_dist = require("../dist-DEZDBpGW.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_https = require("node:https");
8
8
  let node_child_process = require("node:child_process");
@@ -16,7 +16,8 @@ var boschShcActions = require_dist.defineCustomActions({
16
16
  discoverController: require_dist.customAction(require_dist.object({}), require_dist.object({
17
17
  hosts: require_dist.array(require_dist.object({
18
18
  host: require_dist.string(),
19
- name: require_dist.string()
19
+ name: require_dist.string(),
20
+ id: require_dist.string()
20
21
  })),
21
22
  summary: require_dist.string()
22
23
  }), {
@@ -331,6 +332,10 @@ async function registerShcClient(deps) {
331
332
  }
332
333
  //#endregion
333
334
  //#region src/shc/shc-controller-discovery.ts
335
+ /** `prefilledConfig` flag: the scan opens the integration form instead of adopting a device. */
336
+ var SHC_SETUP_INTEGRATION_KEY = "setupIntegration";
337
+ /** Top bit of the question class: ask for a unicast reply (RFC 6762). */
338
+ var MDNS_QU_CLASS = 32769;
334
339
  var SHC_MDNS_SERVICE = "_http._tcp.local";
335
340
  var SHC_CONTROLLER_NAME_PREFIX = "Bosch SHC";
336
341
  var SHC_DISCOVERY_TIMEOUT_MS = 1500;
@@ -399,6 +404,14 @@ function ipv4(buf, offset, length) {
399
404
  if (length !== 4 || offset + 4 > buf.length) return null;
400
405
  return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
401
406
  }
407
+ /** Identity used to recognise a controller already stored on an integration. */
408
+ function shcControllerId(input) {
409
+ const service = input.serviceHost?.trim().toLowerCase() ?? "";
410
+ if (service.length > 0) return service.endsWith(".local") ? service.slice(0, -6) : service;
411
+ const serial = /\[([^\]]+)\]/.exec(input.name)?.[1]?.trim().toLowerCase() ?? "";
412
+ if (serial.length > 0) return serial;
413
+ return input.host;
414
+ }
402
415
  function hostsFromRecords(records) {
403
416
  const instances = [];
404
417
  const srv = /* @__PURE__ */ new Map();
@@ -424,13 +437,46 @@ function hostsFromRecords(records) {
424
437
  if (host === void 0 || seen.has(host)) continue;
425
438
  seen.add(host);
426
439
  const label = instance.split("._")[0] ?? instance;
440
+ const id = shcControllerId({
441
+ name: label,
442
+ host,
443
+ ...viaSrv !== void 0 ? { serviceHost: viaSrv } : {}
444
+ });
427
445
  out.push({
428
446
  host,
429
- name: label
447
+ name: label,
448
+ id
430
449
  });
431
450
  }
432
451
  return out;
433
452
  }
453
+ function identityKeys(found) {
454
+ const host = found.host.trim().toLowerCase();
455
+ const id = found.id.trim().toLowerCase();
456
+ const keys = [host, id];
457
+ if (id.length > 0 && !id.endsWith(".local")) keys.push(`${id}.local`);
458
+ return keys;
459
+ }
460
+ /** Controllers whose host or id is already stored on an integration are not proposed again. */
461
+ function proposeShcControllers(found, existingHosts) {
462
+ const known = new Set(existingHosts.map((host) => host.trim().toLowerCase()).filter((host) => host.length > 0));
463
+ return found.filter((item) => !identityKeys(item).some((key) => known.has(key)));
464
+ }
465
+ function shcDiscoveryCandidates(proposals) {
466
+ return proposals.map((found) => ({
467
+ stableId: `bosch-shc:${found.id}`,
468
+ suggestedName: found.name,
469
+ prefilledConfig: {
470
+ host: found.host,
471
+ [SHC_SETUP_INTEGRATION_KEY]: true
472
+ }
473
+ }));
474
+ }
475
+ function shcDiscoverySummary(foundCount, proposedCount) {
476
+ if (proposedCount === 0 && foundCount > 0) return "Controller already added.";
477
+ if (proposedCount === 0) return "No controller found on the network.";
478
+ return `Found ${proposedCount} ${proposedCount === 1 ? "controller" : "controllers"}.`;
479
+ }
434
480
  /** Hosts advertised as a Bosch Smart Home Controller on one mDNS message. */
435
481
  function shcHostsFromDnsMessage(buf) {
436
482
  if (buf.length < 12) return [];
@@ -463,7 +509,7 @@ function mdnsPtrQuery(service) {
463
509
  header.writeUInt16BE(1, 4);
464
510
  const tail = Buffer.alloc(4);
465
511
  tail.writeUInt16BE(PTR, 0);
466
- tail.writeUInt16BE(1, 2);
512
+ tail.writeUInt16BE(MDNS_QU_CLASS, 2);
467
513
  return Buffer.concat([
468
514
  header,
469
515
  ...encoded,
@@ -23818,22 +23864,15 @@ var BoschShcProviderAddon = class extends require_dist.BaseDeviceProvider {
23818
23864
  customActions: boschShcActions,
23819
23865
  actionHandlers: {
23820
23866
  discoverController: async () => {
23821
- try {
23822
- const hosts = await browseShcControllers();
23823
- return {
23824
- hosts: hosts.map((found) => ({
23825
- host: found.host,
23826
- name: found.name
23827
- })),
23828
- summary: hosts.length === 0 ? "No controller found on the network." : `Found ${hosts.length} controller${hosts.length === 1 ? "" : "s"}.`
23829
- };
23830
- } catch (err) {
23831
- this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: require_dist.errMsg(err) } });
23832
- return {
23833
- hosts: [],
23834
- summary: "No controller found on the network."
23835
- };
23836
- }
23867
+ const { proposals, summary } = await this.discoverShcControllers();
23868
+ return {
23869
+ hosts: proposals.map((found) => ({
23870
+ host: found.host,
23871
+ name: found.name,
23872
+ id: found.id
23873
+ })),
23874
+ summary
23875
+ };
23837
23876
  },
23838
23877
  registerClient: async (input) => {
23839
23878
  const { sslCertificate, sslKey, token } = await registerShcClient({
@@ -23868,6 +23907,28 @@ var BoschShcProviderAddon = class extends require_dist.BaseDeviceProvider {
23868
23907
  async supportsManualCreation() {
23869
23908
  return false;
23870
23909
  }
23910
+ /**
23911
+ * LAN mDNS probe. This addon is hub-only, so the socket is the hub's.
23912
+ * The operator's hub is on br0 (its own LAN address), the same path the
23913
+ * hub already uses to announce itself — multicast UDP 5353 reaches the
23914
+ * LAN. The probe asks for a unicast reply and does not bind 5353, which
23915
+ * the hub's announcer holds. Agents do not run this addon.
23916
+ */
23917
+ async supportsDiscovery() {
23918
+ return true;
23919
+ }
23920
+ async discoverDevices() {
23921
+ const { proposals } = await this.discoverShcControllers();
23922
+ return shcDiscoveryCandidates(proposals).map((candidate) => ({
23923
+ stableId: candidate.stableId,
23924
+ type: require_dist.DeviceType.Hub,
23925
+ suggestedName: candidate.suggestedName,
23926
+ prefilledConfig: { ...candidate.prefilledConfig }
23927
+ }));
23928
+ }
23929
+ async adoptDiscoveredDevice(_input) {
23930
+ throw new Error("Bosch SHC controllers are paired from the integration form");
23931
+ }
23871
23932
  async onGetCreationSchema(_type) {
23872
23933
  return null;
23873
23934
  }
@@ -23881,6 +23942,46 @@ var BoschShcProviderAddon = class extends require_dist.BaseDeviceProvider {
23881
23942
  if (typeof shcDeviceId !== "string" || shcDeviceId.length === 0) throw new Error("bosch-shc generateStableId: shcDeviceId required");
23882
23943
  return `bosch-shc:${integrationId}:${shcDeviceId}`;
23883
23944
  }
23945
+ async discoverShcControllers() {
23946
+ let found = [];
23947
+ try {
23948
+ found = await browseShcControllers();
23949
+ } catch (err) {
23950
+ this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: require_dist.errMsg(err) } });
23951
+ }
23952
+ const known = await this.knownControllerHosts();
23953
+ const proposals = proposeShcControllers(found, known);
23954
+ if (found.length > proposals.length) this.ctx.logger.info("SHC discovery skipped controllers already added", { meta: {
23955
+ found: found.length,
23956
+ proposed: proposals.length
23957
+ } });
23958
+ return {
23959
+ proposals,
23960
+ summary: shcDiscoverySummary(found.length, proposals.length)
23961
+ };
23962
+ }
23963
+ async knownControllerHosts() {
23964
+ try {
23965
+ const all = await this.ctx.api.integrations.list.query();
23966
+ const hosts = [];
23967
+ for (const integration of all) {
23968
+ if (integration.addonId !== this.ctx.id) continue;
23969
+ try {
23970
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
23971
+ if (connection) hosts.push(connection.host);
23972
+ } catch (err) {
23973
+ this.ctx.logger.warn("SHC discovery: settings read failed", { meta: {
23974
+ integrationId: integration.id,
23975
+ error: require_dist.errMsg(err)
23976
+ } });
23977
+ }
23978
+ }
23979
+ return hosts;
23980
+ } catch (err) {
23981
+ this.ctx.logger.warn("SHC discovery: integration list failed", { meta: { error: require_dist.errMsg(err) } });
23982
+ return [];
23983
+ }
23984
+ }
23884
23985
  requireClients() {
23885
23986
  if (!this.clients) throw new Error("Bosch SHC provider not initialised");
23886
23987
  return this.clients;
@@ -1,4 +1,4 @@
1
- import { E as errMsg, F as number, I as object, M as _enum, N as array, O as customAction, P as literal, R as string, S as temperatureSensorCapability, T as DeviceType, V as EventCategory, a as batteryCapability, c as connectionTestCapability, k as defineCustomActions, m as heatingValveCapability, n as BaseDeviceProvider, s as climateControlCapability, t as BaseDevice, u as deviceAdoptionCapability, x as switchCapability } from "../dist-DPDLG8_g.mjs";
1
+ import { E as errMsg, F as number, I as object, M as _enum, N as array, O as customAction, P as literal, R as string, S as temperatureSensorCapability, T as DeviceType, V as EventCategory, a as batteryCapability, c as connectionTestCapability, k as defineCustomActions, m as heatingValveCapability, n as BaseDeviceProvider, s as climateControlCapability, t as BaseDevice, u as deviceAdoptionCapability, x as switchCapability } from "../dist-Cbcvh8mB.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { request } from "node:https";
@@ -17,7 +17,8 @@ var boschShcActions = defineCustomActions({
17
17
  discoverController: customAction(object({}), object({
18
18
  hosts: array(object({
19
19
  host: string(),
20
- name: string()
20
+ name: string(),
21
+ id: string()
21
22
  })),
22
23
  summary: string()
23
24
  }), {
@@ -332,6 +333,10 @@ async function registerShcClient(deps) {
332
333
  }
333
334
  //#endregion
334
335
  //#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;
335
340
  var SHC_MDNS_SERVICE = "_http._tcp.local";
336
341
  var SHC_CONTROLLER_NAME_PREFIX = "Bosch SHC";
337
342
  var SHC_DISCOVERY_TIMEOUT_MS = 1500;
@@ -400,6 +405,14 @@ function ipv4(buf, offset, length) {
400
405
  if (length !== 4 || offset + 4 > buf.length) return null;
401
406
  return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
402
407
  }
408
+ /** Identity used to recognise a controller already stored on an integration. */
409
+ function shcControllerId(input) {
410
+ const service = input.serviceHost?.trim().toLowerCase() ?? "";
411
+ if (service.length > 0) return service.endsWith(".local") ? service.slice(0, -6) : service;
412
+ const serial = /\[([^\]]+)\]/.exec(input.name)?.[1]?.trim().toLowerCase() ?? "";
413
+ if (serial.length > 0) return serial;
414
+ return input.host;
415
+ }
403
416
  function hostsFromRecords(records) {
404
417
  const instances = [];
405
418
  const srv = /* @__PURE__ */ new Map();
@@ -425,13 +438,46 @@ function hostsFromRecords(records) {
425
438
  if (host === void 0 || seen.has(host)) continue;
426
439
  seen.add(host);
427
440
  const label = instance.split("._")[0] ?? instance;
441
+ const id = shcControllerId({
442
+ name: label,
443
+ host,
444
+ ...viaSrv !== void 0 ? { serviceHost: viaSrv } : {}
445
+ });
428
446
  out.push({
429
447
  host,
430
- name: label
448
+ name: label,
449
+ id
431
450
  });
432
451
  }
433
452
  return out;
434
453
  }
454
+ function identityKeys(found) {
455
+ const host = found.host.trim().toLowerCase();
456
+ const id = found.id.trim().toLowerCase();
457
+ const keys = [host, id];
458
+ if (id.length > 0 && !id.endsWith(".local")) keys.push(`${id}.local`);
459
+ return keys;
460
+ }
461
+ /** Controllers whose host or id is already stored on an integration are not proposed again. */
462
+ function proposeShcControllers(found, existingHosts) {
463
+ const known = new Set(existingHosts.map((host) => host.trim().toLowerCase()).filter((host) => host.length > 0));
464
+ return found.filter((item) => !identityKeys(item).some((key) => known.has(key)));
465
+ }
466
+ function shcDiscoveryCandidates(proposals) {
467
+ return proposals.map((found) => ({
468
+ stableId: `bosch-shc:${found.id}`,
469
+ suggestedName: found.name,
470
+ prefilledConfig: {
471
+ host: found.host,
472
+ [SHC_SETUP_INTEGRATION_KEY]: true
473
+ }
474
+ }));
475
+ }
476
+ function shcDiscoverySummary(foundCount, proposedCount) {
477
+ if (proposedCount === 0 && foundCount > 0) return "Controller already added.";
478
+ if (proposedCount === 0) return "No controller found on the network.";
479
+ return `Found ${proposedCount} ${proposedCount === 1 ? "controller" : "controllers"}.`;
480
+ }
435
481
  /** Hosts advertised as a Bosch Smart Home Controller on one mDNS message. */
436
482
  function shcHostsFromDnsMessage(buf) {
437
483
  if (buf.length < 12) return [];
@@ -464,7 +510,7 @@ function mdnsPtrQuery(service) {
464
510
  header.writeUInt16BE(1, 4);
465
511
  const tail = Buffer.alloc(4);
466
512
  tail.writeUInt16BE(PTR, 0);
467
- tail.writeUInt16BE(1, 2);
513
+ tail.writeUInt16BE(MDNS_QU_CLASS, 2);
468
514
  return Buffer.concat([
469
515
  header,
470
516
  ...encoded,
@@ -23819,22 +23865,15 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23819
23865
  customActions: boschShcActions,
23820
23866
  actionHandlers: {
23821
23867
  discoverController: async () => {
23822
- try {
23823
- const hosts = await browseShcControllers();
23824
- return {
23825
- hosts: hosts.map((found) => ({
23826
- host: found.host,
23827
- name: found.name
23828
- })),
23829
- summary: hosts.length === 0 ? "No controller found on the network." : `Found ${hosts.length} controller${hosts.length === 1 ? "" : "s"}.`
23830
- };
23831
- } catch (err) {
23832
- this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: errMsg(err) } });
23833
- return {
23834
- hosts: [],
23835
- summary: "No controller found on the network."
23836
- };
23837
- }
23868
+ const { proposals, summary } = await this.discoverShcControllers();
23869
+ return {
23870
+ hosts: proposals.map((found) => ({
23871
+ host: found.host,
23872
+ name: found.name,
23873
+ id: found.id
23874
+ })),
23875
+ summary
23876
+ };
23838
23877
  },
23839
23878
  registerClient: async (input) => {
23840
23879
  const { sslCertificate, sslKey, token } = await registerShcClient({
@@ -23869,6 +23908,28 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23869
23908
  async supportsManualCreation() {
23870
23909
  return false;
23871
23910
  }
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
+ */
23918
+ async supportsDiscovery() {
23919
+ return true;
23920
+ }
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
+ }));
23929
+ }
23930
+ async adoptDiscoveredDevice(_input) {
23931
+ throw new Error("Bosch SHC controllers are paired from the integration form");
23932
+ }
23872
23933
  async onGetCreationSchema(_type) {
23873
23934
  return null;
23874
23935
  }
@@ -23882,6 +23943,46 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
23882
23943
  if (typeof shcDeviceId !== "string" || shcDeviceId.length === 0) throw new Error("bosch-shc generateStableId: shcDeviceId required");
23883
23944
  return `bosch-shc:${integrationId}:${shcDeviceId}`;
23884
23945
  }
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
+ async knownControllerHosts() {
23965
+ try {
23966
+ const all = await this.ctx.api.integrations.list.query();
23967
+ const hosts = [];
23968
+ for (const integration of all) {
23969
+ if (integration.addonId !== this.ctx.id) continue;
23970
+ try {
23971
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
23972
+ if (connection) hosts.push(connection.host);
23973
+ } catch (err) {
23974
+ this.ctx.logger.warn("SHC discovery: settings read failed", { meta: {
23975
+ integrationId: integration.id,
23976
+ error: errMsg(err)
23977
+ } });
23978
+ }
23979
+ }
23980
+ return hosts;
23981
+ } catch (err) {
23982
+ this.ctx.logger.warn("SHC discovery: integration list failed", { meta: { error: errMsg(err) } });
23983
+ return [];
23984
+ }
23985
+ }
23885
23986
  requireClients() {
23886
23987
  if (!this.clients) throw new Error("Bosch SHC provider not initialised");
23887
23988
  return this.clients;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-bosch",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -132,7 +132,9 @@
132
132
  "files": [
133
133
  "dist",
134
134
  "i18n",
135
- "assets"
135
+ "assets",
136
+ "README.md",
137
+ "CHANGELOG.md"
136
138
  ],
137
139
  "scripts": {
138
140
  "build": "vite build",