@camstack/addon-provider-bosch 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-integration-offline-DAgg2Cgr.js → account-integration-offline-2MI94wOE.js} +16 -2
- package/dist/{account-integration-offline-iGgTPuq_.mjs → account-integration-offline-DKaazSyb.mjs} +16 -2
- package/dist/home-connect/addon.js +1 -1
- package/dist/home-connect/addon.mjs +1 -1
- package/dist/homecom/addon.js +11 -6
- package/dist/homecom/addon.mjs +11 -6
- package/dist/shc/addon.js +120 -19
- package/dist/shc/addon.mjs +120 -19
- package/package.json +1 -1
package/dist/{account-integration-offline-DAgg2Cgr.js → account-integration-offline-2MI94wOE.js}
RENAMED
|
@@ -10,15 +10,29 @@ var BARE_CODE = /^[A-Za-z0-9._~-]+$/;
|
|
|
10
10
|
function cleanPaste(input) {
|
|
11
11
|
return input.trim().replace(/^(?:location|request url)\s*:\s*/i, "").replace(/^["'<]+|["'>]+$/g, "").trim();
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function paramsFromUrl(input) {
|
|
14
14
|
try {
|
|
15
15
|
const url = new URL(input);
|
|
16
16
|
const merged = new URLSearchParams(url.search);
|
|
17
17
|
for (const [key, value] of new URLSearchParams(url.hash.replace(/^#/, ""))) if (!merged.has(key)) merged.set(key, value);
|
|
18
18
|
return merged;
|
|
19
19
|
} catch {
|
|
20
|
-
return
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function paramsOf(input) {
|
|
24
|
+
const direct = paramsFromUrl(input);
|
|
25
|
+
if (direct) return direct;
|
|
26
|
+
if (input.includes("%")) try {
|
|
27
|
+
const decoded = decodeURIComponent(input);
|
|
28
|
+
if (decoded !== input) {
|
|
29
|
+
const again = paramsFromUrl(decoded);
|
|
30
|
+
if (again) return again;
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (!(err instanceof URIError)) throw err;
|
|
21
34
|
}
|
|
35
|
+
return new URLSearchParams(input.replace(/^[?#]/, ""));
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
24
38
|
* The authorization code from whatever an operator pasted after a browser sign-in: the full
|
package/dist/{account-integration-offline-iGgTPuq_.mjs → account-integration-offline-DKaazSyb.mjs}
RENAMED
|
@@ -10,15 +10,29 @@ var BARE_CODE = /^[A-Za-z0-9._~-]+$/;
|
|
|
10
10
|
function cleanPaste(input) {
|
|
11
11
|
return input.trim().replace(/^(?:location|request url)\s*:\s*/i, "").replace(/^["'<]+|["'>]+$/g, "").trim();
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function paramsFromUrl(input) {
|
|
14
14
|
try {
|
|
15
15
|
const url = new URL(input);
|
|
16
16
|
const merged = new URLSearchParams(url.search);
|
|
17
17
|
for (const [key, value] of new URLSearchParams(url.hash.replace(/^#/, ""))) if (!merged.has(key)) merged.set(key, value);
|
|
18
18
|
return merged;
|
|
19
19
|
} catch {
|
|
20
|
-
return
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function paramsOf(input) {
|
|
24
|
+
const direct = paramsFromUrl(input);
|
|
25
|
+
if (direct) return direct;
|
|
26
|
+
if (input.includes("%")) try {
|
|
27
|
+
const decoded = decodeURIComponent(input);
|
|
28
|
+
if (decoded !== input) {
|
|
29
|
+
const again = paramsFromUrl(decoded);
|
|
30
|
+
if (again) return again;
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (!(err instanceof URIError)) throw err;
|
|
21
34
|
}
|
|
35
|
+
return new URLSearchParams(input.replace(/^[?#]/, ""));
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
24
38
|
* The authorization code from whatever an operator pasted after a browser sign-in: the full
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_dist = require("../dist-DEZDBpGW.js");
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
3
|
+
const require_account_integration_offline = require("../account-integration-offline-2MI94wOE.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_tls = require("node:tls");
|
|
6
6
|
let node_zlib = require("node:zlib");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { A as isSecretMarker, B as unknown, D as createEvent, E as errMsg, I as object, L as record, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, _ as powerMeterCapability, b as runningStateCapability, c as connectionTestCapability, d as dishwasherCapability, f as enumSensorCapability, g as numericSensorCapability, h as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarkerLike, k as defineCustomActions, l as consumablesCapability, n as BaseDeviceProvider, o as binaryCapability, p as eventEmitterCapability, r as DishwasherOperationStateSchema, t as BaseDevice, u as deviceAdoptionCapability, v as quantityFromUnit, w as DeviceRole, y as runningFromDishwasherOperation, z as union } from "../dist-Cbcvh8mB.mjs";
|
|
2
|
-
import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-
|
|
2
|
+
import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-DKaazSyb.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { connect } from "node:tls";
|
|
5
5
|
import { inflateRawSync } from "node:zlib";
|
package/dist/homecom/addon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_dist = require("../dist-DEZDBpGW.js");
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
3
|
+
const require_account_integration_offline = require("../account-integration-offline-2MI94wOE.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
//#region src/homecom/config.ts
|
|
6
6
|
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
@@ -322,16 +322,20 @@ function readNum(raw) {
|
|
|
322
322
|
//#endregion
|
|
323
323
|
//#region src/homecom/homecom-oauth.ts
|
|
324
324
|
/**
|
|
325
|
-
* Public OAuth client of the HomeCom Easy
|
|
325
|
+
* Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
|
|
326
326
|
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
327
|
+
* A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
|
|
328
|
+
* today: the same authorize query the MIT-licensed `homecom_alt` client records
|
|
329
|
+
* (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
|
|
330
|
+
* opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
|
|
331
|
+
* answers `misconfigured-application` and never shows the login page. The scope it does accept
|
|
332
|
+
* names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
|
|
330
333
|
*/
|
|
331
334
|
var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
|
|
332
335
|
var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
|
|
333
336
|
var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
|
|
334
337
|
var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
|
|
338
|
+
var HOMECOM_STYLE_ID = "tt_bsch";
|
|
335
339
|
var HOMECOM_SCOPE = [
|
|
336
340
|
"openid",
|
|
337
341
|
"email",
|
|
@@ -341,7 +345,7 @@ var HOMECOM_SCOPE = [
|
|
|
341
345
|
"pointt.gateway.removal",
|
|
342
346
|
"pointt.gateway.list",
|
|
343
347
|
"pointt.gateway.users",
|
|
344
|
-
"pointt.gateway.resource.
|
|
348
|
+
"pointt.gateway.resource.dashapp",
|
|
345
349
|
"pointt.castt.flow.token-exchange",
|
|
346
350
|
"bacon",
|
|
347
351
|
"hcc.tariff.read"
|
|
@@ -557,6 +561,7 @@ function beginHomeComSignIn() {
|
|
|
557
561
|
url.searchParams.set("state", state);
|
|
558
562
|
url.searchParams.set("nonce", b64url((0, node_crypto.randomBytes)(16)));
|
|
559
563
|
url.searchParams.set("prompt", "login");
|
|
564
|
+
url.searchParams.set("style_id", HOMECOM_STYLE_ID);
|
|
560
565
|
return {
|
|
561
566
|
authorizeUrl: url.toString(),
|
|
562
567
|
verifier,
|
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { A as isSecretMarker, C as waterHeaterCapability, E as errMsg, F as number, I as object, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, c as connectionTestCapability, j as isSecretMarkerLike, k as defineCustomActions, n as BaseDeviceProvider, s as climateControlCapability, t as BaseDevice, u as deviceAdoptionCapability } from "../dist-Cbcvh8mB.mjs";
|
|
2
|
-
import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-
|
|
2
|
+
import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-DKaazSyb.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
//#region src/homecom/config.ts
|
|
5
5
|
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
@@ -321,16 +321,20 @@ function readNum(raw) {
|
|
|
321
321
|
//#endregion
|
|
322
322
|
//#region src/homecom/homecom-oauth.ts
|
|
323
323
|
/**
|
|
324
|
-
* Public OAuth client of the HomeCom Easy
|
|
324
|
+
* Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
|
|
325
325
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
326
|
+
* A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
|
|
327
|
+
* today: the same authorize query the MIT-licensed `homecom_alt` client records
|
|
328
|
+
* (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
|
|
329
|
+
* opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
|
|
330
|
+
* answers `misconfigured-application` and never shows the login page. The scope it does accept
|
|
331
|
+
* names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
|
|
329
332
|
*/
|
|
330
333
|
var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
|
|
331
334
|
var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
|
|
332
335
|
var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
|
|
333
336
|
var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
|
|
337
|
+
var HOMECOM_STYLE_ID = "tt_bsch";
|
|
334
338
|
var HOMECOM_SCOPE = [
|
|
335
339
|
"openid",
|
|
336
340
|
"email",
|
|
@@ -340,7 +344,7 @@ var HOMECOM_SCOPE = [
|
|
|
340
344
|
"pointt.gateway.removal",
|
|
341
345
|
"pointt.gateway.list",
|
|
342
346
|
"pointt.gateway.users",
|
|
343
|
-
"pointt.gateway.resource.
|
|
347
|
+
"pointt.gateway.resource.dashapp",
|
|
344
348
|
"pointt.castt.flow.token-exchange",
|
|
345
349
|
"bacon",
|
|
346
350
|
"hcc.tariff.read"
|
|
@@ -556,6 +560,7 @@ function beginHomeComSignIn() {
|
|
|
556
560
|
url.searchParams.set("state", state);
|
|
557
561
|
url.searchParams.set("nonce", b64url(randomBytes(16)));
|
|
558
562
|
url.searchParams.set("prompt", "login");
|
|
563
|
+
url.searchParams.set("style_id", HOMECOM_STYLE_ID);
|
|
559
564
|
return {
|
|
560
565
|
authorizeUrl: url.toString(),
|
|
561
566
|
verifier,
|
package/dist/shc/addon.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
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;
|
package/dist/shc/addon.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
} catch (err) {
|
|
23832
|
-
this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: errMsg(err) } });
|
|
23833
|
-
return {
|
|
23834
|
-
hosts: [],
|
|
23835
|
-
summary: "No controller found on the network."
|
|
23836
|
-
};
|
|
23837
|
-
}
|
|
23868
|
+
const { proposals, summary } = await this.discoverShcControllers();
|
|
23869
|
+
return {
|
|
23870
|
+
hosts: proposals.map((found) => ({
|
|
23871
|
+
host: found.host,
|
|
23872
|
+
name: found.name,
|
|
23873
|
+
id: found.id
|
|
23874
|
+
})),
|
|
23875
|
+
summary
|
|
23876
|
+
};
|
|
23838
23877
|
},
|
|
23839
23878
|
registerClient: async (input) => {
|
|
23840
23879
|
const { sslCertificate, sslKey, token } = await registerShcClient({
|
|
@@ -23869,6 +23908,28 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
|
|
|
23869
23908
|
async supportsManualCreation() {
|
|
23870
23909
|
return false;
|
|
23871
23910
|
}
|
|
23911
|
+
/**
|
|
23912
|
+
* LAN mDNS probe. This addon is hub-only, so the socket is the hub's.
|
|
23913
|
+
* The operator's hub is on br0 (its own LAN address), the same path the
|
|
23914
|
+
* hub already uses to announce itself — multicast UDP 5353 reaches the
|
|
23915
|
+
* LAN. The probe asks for a unicast reply and does not bind 5353, which
|
|
23916
|
+
* the hub's announcer holds. Agents do not run this addon.
|
|
23917
|
+
*/
|
|
23918
|
+
async supportsDiscovery() {
|
|
23919
|
+
return true;
|
|
23920
|
+
}
|
|
23921
|
+
async discoverDevices() {
|
|
23922
|
+
const { proposals } = await this.discoverShcControllers();
|
|
23923
|
+
return shcDiscoveryCandidates(proposals).map((candidate) => ({
|
|
23924
|
+
stableId: candidate.stableId,
|
|
23925
|
+
type: DeviceType.Hub,
|
|
23926
|
+
suggestedName: candidate.suggestedName,
|
|
23927
|
+
prefilledConfig: { ...candidate.prefilledConfig }
|
|
23928
|
+
}));
|
|
23929
|
+
}
|
|
23930
|
+
async adoptDiscoveredDevice(_input) {
|
|
23931
|
+
throw new Error("Bosch SHC controllers are paired from the integration form");
|
|
23932
|
+
}
|
|
23872
23933
|
async onGetCreationSchema(_type) {
|
|
23873
23934
|
return null;
|
|
23874
23935
|
}
|
|
@@ -23882,6 +23943,46 @@ var BoschShcProviderAddon = class extends BaseDeviceProvider {
|
|
|
23882
23943
|
if (typeof shcDeviceId !== "string" || shcDeviceId.length === 0) throw new Error("bosch-shc generateStableId: shcDeviceId required");
|
|
23883
23944
|
return `bosch-shc:${integrationId}:${shcDeviceId}`;
|
|
23884
23945
|
}
|
|
23946
|
+
async discoverShcControllers() {
|
|
23947
|
+
let found = [];
|
|
23948
|
+
try {
|
|
23949
|
+
found = await browseShcControllers();
|
|
23950
|
+
} catch (err) {
|
|
23951
|
+
this.ctx.logger.warn("SHC controller discovery failed", { meta: { error: errMsg(err) } });
|
|
23952
|
+
}
|
|
23953
|
+
const known = await this.knownControllerHosts();
|
|
23954
|
+
const proposals = proposeShcControllers(found, known);
|
|
23955
|
+
if (found.length > proposals.length) this.ctx.logger.info("SHC discovery skipped controllers already added", { meta: {
|
|
23956
|
+
found: found.length,
|
|
23957
|
+
proposed: proposals.length
|
|
23958
|
+
} });
|
|
23959
|
+
return {
|
|
23960
|
+
proposals,
|
|
23961
|
+
summary: shcDiscoverySummary(found.length, proposals.length)
|
|
23962
|
+
};
|
|
23963
|
+
}
|
|
23964
|
+
async knownControllerHosts() {
|
|
23965
|
+
try {
|
|
23966
|
+
const all = await this.ctx.api.integrations.list.query();
|
|
23967
|
+
const hosts = [];
|
|
23968
|
+
for (const integration of all) {
|
|
23969
|
+
if (integration.addonId !== this.ctx.id) continue;
|
|
23970
|
+
try {
|
|
23971
|
+
const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
|
|
23972
|
+
if (connection) hosts.push(connection.host);
|
|
23973
|
+
} catch (err) {
|
|
23974
|
+
this.ctx.logger.warn("SHC discovery: settings read failed", { meta: {
|
|
23975
|
+
integrationId: integration.id,
|
|
23976
|
+
error: errMsg(err)
|
|
23977
|
+
} });
|
|
23978
|
+
}
|
|
23979
|
+
}
|
|
23980
|
+
return hosts;
|
|
23981
|
+
} catch (err) {
|
|
23982
|
+
this.ctx.logger.warn("SHC discovery: integration list failed", { meta: { error: errMsg(err) } });
|
|
23983
|
+
return [];
|
|
23984
|
+
}
|
|
23985
|
+
}
|
|
23885
23986
|
requireClients() {
|
|
23886
23987
|
if (!this.clients) throw new Error("Bosch SHC provider not initialised");
|
|
23887
23988
|
return this.clients;
|
package/package.json
CHANGED