@camstack/addon-provider-bosch 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/shc/addon.js CHANGED
@@ -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;
@@ -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/i18n/en.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "bosch-home-connect.name": "Bosch Home Connect",
4
4
  "bosch-homecom.name": "Bosch HomeCom",
5
5
  "homecom.s_account.title": "HomeCom account",
6
- "homecom.s_account.description": "Sign in with your SingleKey ID. Bosch opens its app address; paste that address here.",
6
+ "homecom.s_account.description": "Sign in with your SingleKey ID. Copy the code as described below, then paste it here.",
7
7
  "homecom.s_account.__homecom-sign-in.label": "HomeCom",
8
8
  "homecom.s_account.accountLabel.label": "Account",
9
9
  "homecom.s_account.gatewayCount.label": "Gateways",
@@ -31,5 +31,7 @@
31
31
  "shc.s_connection.__shc-advanced.label": "Advanced",
32
32
  "shc.s_connection.token.label": "Controller token",
33
33
  "shc.s_connection.sslCertificate.label": "Client certificate (PEM)",
34
- "shc.s_connection.sslKey.label": "Client private key (PEM)"
34
+ "shc.s_connection.sslKey.label": "Client private key (PEM)",
35
+ "homecom.s_account.__homecom-code-help.label": "Before you sign in, open the browser's developer tools, choose the Network tab, and turn on Preserve log.\n\nSign in with SingleKey. The page will say it has network problems. Ignore that: the browser tried to open the Bosch app and could not.\n\nIn the Network list, filter by code= and open the request whose address starts with com.bosch.tt.dashtt.pointt://app/login?code=\n\nCopy that whole address, or only the code, and paste it in the box under the sign-in button.\n\nOn a phone the same redirect fails unless the Bosch app is installed. Use a desktop browser to copy the code.",
36
+ "homecom.s_account.gatewayId.description": "Filled from the account. Type one only when you need to override the choice."
35
37
  }
package/i18n/it.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "bosch-home-connect.name": "Bosch Home Connect",
4
4
  "bosch-homecom.name": "Bosch HomeCom",
5
5
  "homecom.s_account.title": "Account HomeCom",
6
- "homecom.s_account.description": "Accedi con il tuo SingleKey ID. Bosch apre l'indirizzo dell'app; incolla qui quell'indirizzo.",
6
+ "homecom.s_account.description": "Accedi con il tuo SingleKey ID. Copia il codice come descritto sotto, poi incollalo qui.",
7
7
  "homecom.s_account.__homecom-sign-in.label": "HomeCom",
8
8
  "homecom.s_account.accountLabel.label": "Account",
9
9
  "homecom.s_account.gatewayCount.label": "Gateway",
@@ -31,5 +31,7 @@
31
31
  "shc.s_connection.__shc-advanced.label": "Avanzate",
32
32
  "shc.s_connection.token.label": "Token del controller",
33
33
  "shc.s_connection.sslCertificate.label": "Certificato client (PEM)",
34
- "shc.s_connection.sslKey.label": "Chiave privata client (PEM)"
34
+ "shc.s_connection.sslKey.label": "Chiave privata client (PEM)",
35
+ "homecom.s_account.__homecom-code-help.label": "Prima di accedere, apri gli strumenti per sviluppatori del browser, scegli la scheda Rete (Network) e attiva \"Conserva registro\" (Preserve log).\n\nAccedi con SingleKey. La pagina dirà che ci sono problemi di rete. Ignoralo: il browser ha provato ad aprire l'app Bosch e non c'è riuscito.\n\nNell'elenco Rete filtra per code= e apri la richiesta il cui indirizzo inizia con com.bosch.tt.dashtt.pointt://app/login?code=\n\nCopia l'indirizzo intero, oppure solo il codice, e incollalo nella casella sotto il pulsante di accesso.\n\nSu un telefono lo stesso reindirizzamento fallisce se l'app Bosch non è installata. Per copiare il codice usa un browser da computer.",
36
+ "homecom.s_account.gatewayId.description": "Compilato dall'account. Scrivilo solo se devi sostituire la scelta."
35
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-bosch",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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",