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