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