@camstack/addon-provider-bosch 0.1.20 → 0.1.22

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
@@ -2,27 +2,41 @@ 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-CYBUFKeW.js");
5
+ const require_dist = require("../dist-CRlugVId.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");
9
9
  let node_fs = require("node:fs");
10
10
  let node_os = require("node:os");
11
11
  let node_path = require("node:path");
12
+ let node_dgram = require("node:dgram");
12
13
  let node_net = require("node:net");
13
14
  //#region src/shc/custom-actions.ts
14
- var boschShcActions = require_dist.defineCustomActions({ registerClient: require_dist.customAction(require_dist.object({
15
- host: require_dist.string().min(1),
16
- systemPassword: require_dist.string().min(1)
17
- }), require_dist.object({
18
- sslCertificate: require_dist.string().min(1),
19
- sslKey: require_dist.string().min(1),
20
- token: require_dist.string().min(1),
21
- summary: require_dist.string()
22
- }), {
23
- kind: "mutation",
24
- auth: "admin"
25
- }) });
15
+ var boschShcActions = require_dist.defineCustomActions({
16
+ discoverController: require_dist.customAction(require_dist.object({}), require_dist.object({
17
+ hosts: require_dist.array(require_dist.object({
18
+ host: require_dist.string(),
19
+ name: require_dist.string()
20
+ })),
21
+ summary: require_dist.string()
22
+ }), {
23
+ kind: "mutation",
24
+ auth: "admin"
25
+ }),
26
+ registerClient: require_dist.customAction(require_dist.object({
27
+ host: require_dist.string().optional(),
28
+ hostManual: require_dist.string().optional(),
29
+ systemPassword: require_dist.string().min(1)
30
+ }), require_dist.object({
31
+ sslCertificate: require_dist.string().min(1).meta({ secret: true }),
32
+ sslKey: require_dist.string().min(1).meta({ secret: true }),
33
+ token: require_dist.string().min(1).meta({ secret: true }),
34
+ summary: require_dist.string()
35
+ }), {
36
+ kind: "mutation",
37
+ auth: "admin"
38
+ })
39
+ });
26
40
  //#endregion
27
41
  //#region src/shc/config.ts
28
42
  /** Operator settings for one SHC integration (local mTLS + optional token). */
@@ -46,30 +60,48 @@ function buildConnectionFormSchema() {
46
60
  return { sections: [{
47
61
  id: "connection",
48
62
  title: "Smart Home Controller",
49
- titleKey: "bosch-shc:connection.s_connection.title",
63
+ titleKey: "provider-bosch:shc.s_connection.title",
50
64
  description: "Local REST access to your Bosch Smart Home Controller.",
51
- descriptionKey: "bosch-shc:connection.s_connection.description",
65
+ descriptionKey: "provider-bosch:shc.s_connection.description",
52
66
  columns: 1,
53
67
  fields: [
68
+ {
69
+ type: "addon-action-select",
70
+ addonId: "bosch-shc",
71
+ key: "__shc-discovered",
72
+ writesKey: "host",
73
+ omitFromSettings: true,
74
+ label: "Controller",
75
+ labelKey: "provider-bosch:shc.s_connection.__shc-discovered.label",
76
+ action: "discoverController",
77
+ mapOption: {
78
+ value: "host",
79
+ label: "name"
80
+ },
81
+ hideWhenFewerThan: 2,
82
+ emptyResultsMessage: "No controller found on the network."
83
+ },
54
84
  {
55
85
  type: "text",
56
86
  key: "host",
57
87
  label: "Host",
58
- labelKey: "bosch-shc:connection.s_connection.host.label",
59
- required: true,
88
+ labelKey: "provider-bosch:shc.s_connection.host.label",
89
+ description: "Filled when one controller is found. Type the address if it is not.",
90
+ descriptionKey: "provider-bosch:shc.s_connection.host.description",
91
+ required: false,
60
92
  placeholder: "192.168.1.50"
61
93
  },
62
94
  {
63
95
  type: "info",
64
96
  key: "__shc-pairing-hint",
65
97
  label: "Pair with the controller",
66
- content: "Enter the password you created in the Bosch Smart Home app (not the code printed on the controller). Then hold the pairing button until the LED blinks, and press Register on controller."
98
+ content: "Press the button on the controller until the LED blinks. Enter the password you created in the Bosch Smart Home app, not the code printed on the controller, then register."
67
99
  },
68
100
  {
69
101
  type: "password",
70
102
  key: "systemPassword",
71
103
  label: "System password",
72
- labelKey: "bosch-shc:connection.s_connection.systemPassword.label",
104
+ labelKey: "provider-bosch:shc.s_connection.systemPassword.label",
73
105
  required: true,
74
106
  showToggle: true
75
107
  },
@@ -92,13 +124,46 @@ function buildConnectionFormSchema() {
92
124
  },
93
125
  successMessage: "{summary}"
94
126
  },
127
+ {
128
+ type: "group",
129
+ key: "__shc-advanced",
130
+ label: "Advanced",
131
+ labelKey: "provider-bosch:shc.s_connection.__shc-advanced.label",
132
+ style: "accordion",
133
+ defaultCollapsed: true,
134
+ fields: [{
135
+ type: "secret-presence",
136
+ key: "__shc-registered",
137
+ label: "Client registration",
138
+ sourceKey: "sslCertificate",
139
+ yesLabel: "Client registered: yes",
140
+ noLabel: "Client registered: no"
141
+ }, {
142
+ type: "addon-action-button",
143
+ addonId: "bosch-shc",
144
+ key: "__shc-reregister",
145
+ label: "Register again",
146
+ buttonLabel: "Register again",
147
+ action: "registerClient",
148
+ paramsFromForm: {
149
+ host: "host",
150
+ systemPassword: "systemPassword"
151
+ },
152
+ assignResult: {
153
+ sslCertificate: "sslCertificate",
154
+ sslKey: "sslKey",
155
+ token: "token"
156
+ },
157
+ successMessage: "{summary}"
158
+ }]
159
+ },
95
160
  {
96
161
  type: "password",
97
162
  key: "token",
98
163
  label: "Controller token",
99
- labelKey: "bosch-shc:connection.s_connection.token.label",
100
- required: true,
101
- showToggle: true,
164
+ labelKey: "provider-bosch:shc.s_connection.token.label",
165
+ required: false,
166
+ showToggle: false,
102
167
  showWhen: {
103
168
  field: "host",
104
169
  equals: SHC_PAIRING_ONLY_SHOW_WHEN
@@ -108,8 +173,8 @@ function buildConnectionFormSchema() {
108
173
  type: "password",
109
174
  key: "sslCertificate",
110
175
  label: "Client certificate (PEM)",
111
- labelKey: "bosch-shc:connection.s_connection.sslCertificate.label",
112
- required: true,
176
+ labelKey: "provider-bosch:shc.s_connection.sslCertificate.label",
177
+ required: false,
113
178
  showToggle: true,
114
179
  showWhen: {
115
180
  field: "host",
@@ -120,8 +185,8 @@ function buildConnectionFormSchema() {
120
185
  type: "password",
121
186
  key: "sslKey",
122
187
  label: "Client private key (PEM)",
123
- labelKey: "bosch-shc:connection.s_connection.sslKey.label",
124
- required: true,
188
+ labelKey: "provider-bosch:shc.s_connection.sslKey.label",
189
+ required: false,
125
190
  showToggle: true,
126
191
  showWhen: {
127
192
  field: "host",
@@ -265,6 +330,211 @@ async function registerShcClient(deps) {
265
330
  };
266
331
  }
267
332
  //#endregion
333
+ //#region src/shc/shc-controller-discovery.ts
334
+ var SHC_MDNS_SERVICE = "_http._tcp.local";
335
+ var SHC_CONTROLLER_NAME_PREFIX = "Bosch SHC";
336
+ var SHC_DISCOVERY_TIMEOUT_MS = 1500;
337
+ var MDNS_PORT = 5353;
338
+ var MDNS_ADDRESS = "224.0.0.251";
339
+ var PTR = 12;
340
+ var A = 1;
341
+ var SRV = 33;
342
+ function isShcControllerName(name) {
343
+ return (name.split("._")[0] ?? name).toLowerCase().startsWith(SHC_CONTROLLER_NAME_PREFIX.toLowerCase());
344
+ }
345
+ function readName(buf, start) {
346
+ const labels = [];
347
+ let pos = start;
348
+ let end = start;
349
+ let jumped = false;
350
+ for (let hops = 0; hops < 16; hops++) {
351
+ if (pos < 0 || pos >= buf.length) return null;
352
+ const len = buf[pos] ?? 0;
353
+ if (len === 0) {
354
+ if (!jumped) end = pos + 1;
355
+ return {
356
+ name: labels.join("."),
357
+ end
358
+ };
359
+ }
360
+ if ((len & 192) === 192) {
361
+ if (pos + 1 >= buf.length) return null;
362
+ if (!jumped) end = pos + 2;
363
+ jumped = true;
364
+ pos = (len & 63) << 8 | (buf[pos + 1] ?? 0);
365
+ continue;
366
+ }
367
+ if ((len & 192) !== 0 || pos + 1 + len > buf.length) return null;
368
+ labels.push(buf.subarray(pos + 1, pos + 1 + len).toString("utf8"));
369
+ pos += 1 + len;
370
+ if (!jumped) end = pos;
371
+ }
372
+ return null;
373
+ }
374
+ function readRecords(buf, offset, count) {
375
+ const records = [];
376
+ let pos = offset;
377
+ for (let i = 0; i < count; i++) {
378
+ const name = readName(buf, pos);
379
+ if (name === null || name.end + 10 > buf.length) return null;
380
+ const type = buf.readUInt16BE(name.end);
381
+ const dataLength = buf.readUInt16BE(name.end + 8);
382
+ const dataOffset = name.end + 10;
383
+ if (dataOffset + dataLength > buf.length) return null;
384
+ records.push({
385
+ name: name.name,
386
+ type,
387
+ message: buf,
388
+ dataOffset,
389
+ dataLength
390
+ });
391
+ pos = dataOffset + dataLength;
392
+ }
393
+ return {
394
+ records,
395
+ next: pos
396
+ };
397
+ }
398
+ function ipv4(buf, offset, length) {
399
+ if (length !== 4 || offset + 4 > buf.length) return null;
400
+ return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
401
+ }
402
+ function hostsFromRecords(records) {
403
+ const instances = [];
404
+ const srv = /* @__PURE__ */ new Map();
405
+ const addresses = /* @__PURE__ */ new Map();
406
+ for (const rec of records) {
407
+ const key = rec.name.toLowerCase();
408
+ if (rec.type === PTR) {
409
+ const target = readName(rec.message, rec.dataOffset);
410
+ if (target !== null && isShcControllerName(target.name)) instances.push(target.name);
411
+ } else if (rec.type === SRV && rec.dataLength >= 6) {
412
+ const target = readName(rec.message, rec.dataOffset + 6);
413
+ if (target !== null) srv.set(key, target.name);
414
+ } else if (rec.type === A) {
415
+ const ip = ipv4(rec.message, rec.dataOffset, rec.dataLength);
416
+ if (ip !== null) addresses.set(key, ip);
417
+ }
418
+ }
419
+ const out = [];
420
+ const seen = /* @__PURE__ */ new Set();
421
+ for (const instance of instances) {
422
+ const viaSrv = srv.get(instance.toLowerCase());
423
+ const host = (viaSrv !== void 0 ? addresses.get(viaSrv.toLowerCase()) : void 0) ?? addresses.get(instance.toLowerCase());
424
+ if (host === void 0 || seen.has(host)) continue;
425
+ seen.add(host);
426
+ const label = instance.split("._")[0] ?? instance;
427
+ out.push({
428
+ host,
429
+ name: label
430
+ });
431
+ }
432
+ return out;
433
+ }
434
+ /** Hosts advertised as a Bosch Smart Home Controller on one mDNS message. */
435
+ function shcHostsFromDnsMessage(buf) {
436
+ if (buf.length < 12) return [];
437
+ const questions = buf.readUInt16BE(4);
438
+ const answers = buf.readUInt16BE(6);
439
+ const authorities = buf.readUInt16BE(8);
440
+ const extra = buf.readUInt16BE(10);
441
+ let pos = 12;
442
+ for (let i = 0; i < questions; i++) {
443
+ const name = readName(buf, pos);
444
+ if (name === null) return [];
445
+ pos = name.end + 4;
446
+ if (pos > buf.length) return [];
447
+ }
448
+ const answerRecords = readRecords(buf, pos, answers);
449
+ if (answerRecords === null) return [];
450
+ const authorityRecords = readRecords(buf, answerRecords.next, authorities);
451
+ if (authorityRecords === null) return [];
452
+ const extraRecords = readRecords(buf, authorityRecords.next, extra);
453
+ if (extraRecords === null) return [];
454
+ return hostsFromRecords([
455
+ ...answerRecords.records,
456
+ ...authorityRecords.records,
457
+ ...extraRecords.records
458
+ ]);
459
+ }
460
+ function mdnsPtrQuery(service) {
461
+ const encoded = service.split(".").filter((part) => part.length > 0).map((label) => Buffer.concat([Buffer.from([label.length]), Buffer.from(label)]));
462
+ const header = Buffer.alloc(12);
463
+ header.writeUInt16BE(1, 4);
464
+ const tail = Buffer.alloc(4);
465
+ tail.writeUInt16BE(PTR, 0);
466
+ tail.writeUInt16BE(1, 2);
467
+ return Buffer.concat([
468
+ header,
469
+ ...encoded,
470
+ Buffer.from([0]),
471
+ tail
472
+ ]);
473
+ }
474
+ function closeQuietly(socket) {
475
+ try {
476
+ socket.close();
477
+ } catch {}
478
+ }
479
+ function exchangeMdns(query, timeoutMs) {
480
+ return new Promise((resolve) => {
481
+ let socket;
482
+ try {
483
+ socket = (0, node_dgram.createSocket)({
484
+ type: "udp4",
485
+ reuseAddr: true
486
+ });
487
+ } catch {
488
+ resolve([]);
489
+ return;
490
+ }
491
+ const packets = [];
492
+ let settled = false;
493
+ const finish = () => {
494
+ if (settled) return;
495
+ settled = true;
496
+ closeQuietly(socket);
497
+ resolve(packets);
498
+ };
499
+ socket.on("error", () => finish());
500
+ socket.on("message", (msg) => {
501
+ packets.push(Buffer.from(msg));
502
+ });
503
+ socket.bind(0, () => {
504
+ try {
505
+ socket.send(query, MDNS_PORT, MDNS_ADDRESS);
506
+ } catch {
507
+ finish();
508
+ }
509
+ });
510
+ setTimeout(finish, timeoutMs);
511
+ });
512
+ }
513
+ async function browseShcControllers(exchange = exchangeMdns) {
514
+ let packets;
515
+ try {
516
+ packets = await exchange(mdnsPtrQuery(SHC_MDNS_SERVICE), SHC_DISCOVERY_TIMEOUT_MS);
517
+ } catch {
518
+ return [];
519
+ }
520
+ const hosts = [];
521
+ const seen = /* @__PURE__ */ new Set();
522
+ for (const packet of packets) for (const found of shcHostsFromDnsMessage(packet)) {
523
+ if (seen.has(found.host)) continue;
524
+ seen.add(found.host);
525
+ hosts.push(found);
526
+ }
527
+ return hosts;
528
+ }
529
+ /** Manual host wins. Otherwise the address discovery filled in. */
530
+ function resolveShcPairingHost(input) {
531
+ const manual = input.hostManual?.trim() ?? "";
532
+ if (manual.length > 0) return manual;
533
+ const host = input.host?.trim() ?? "";
534
+ if (host.length > 0) return host;
535
+ throw new Error("Enter the controller host or IP address.");
536
+ }
537
+ //#endregion
268
538
  //#region src/shc/shc-domain-mapping.ts
269
539
  var ROOM_CLIMATE_MODEL = "ROOM_CLIMATE_CONTROL";
270
540
  /** Classify a normalised device summary into an adoption / driver kind. */
@@ -23546,18 +23816,41 @@ var BoschShcProviderAddon = class extends require_dist.BaseDeviceProvider {
23546
23816
  }
23547
23817
  ],
23548
23818
  customActions: boschShcActions,
23549
- actionHandlers: { registerClient: async (input) => {
23550
- const { sslCertificate, sslKey, token } = await registerShcClient({
23551
- host: input.host,
23552
- systemPassword: input.systemPassword
23553
- });
23554
- return {
23555
- sslCertificate,
23556
- sslKey,
23557
- token,
23558
- summary: "CamStack is registered with your Smart Home Controller."
23559
- };
23560
- } }
23819
+ actionHandlers: {
23820
+ 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
+ }
23837
+ },
23838
+ registerClient: async (input) => {
23839
+ const { sslCertificate, sslKey, token } = await registerShcClient({
23840
+ host: resolveShcPairingHost({
23841
+ host: input.host,
23842
+ hostManual: input.hostManual
23843
+ }),
23844
+ systemPassword: input.systemPassword
23845
+ });
23846
+ return {
23847
+ sslCertificate,
23848
+ sslKey,
23849
+ token,
23850
+ summary: "CamStack is registered with your Smart Home Controller."
23851
+ };
23852
+ }
23853
+ }
23561
23854
  };
23562
23855
  }
23563
23856
  async onShutdown() {