@camstack/addon-matter-broker 0.2.148 → 0.2.150
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 +18 -0
- package/README.md +18 -0
- package/dist/addon.js +1433 -126
- package/dist/addon.mjs +1433 -126
- package/package.json +4 -2
package/dist/addon.js
CHANGED
|
@@ -13,7 +13,7 @@ let node_stream_promises = require("node:stream/promises");
|
|
|
13
13
|
let node_net = require("node:net");
|
|
14
14
|
let node_dgram = require("node:dgram");
|
|
15
15
|
node_dgram = require_esm.__toESM(node_dgram, 1);
|
|
16
|
-
//#region ../types/dist/event-category-
|
|
16
|
+
//#region ../types/dist/event-category-CT-hz5Rb.mjs
|
|
17
17
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
18
18
|
EventCategory["SystemBoot"] = "system.boot";
|
|
19
19
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -339,6 +339,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
339
339
|
/** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
|
|
340
340
|
EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
|
|
341
341
|
/**
|
|
342
|
+
* A camera's package LLM confirm has failed closed three times in a row
|
|
343
|
+
* (D931, 2026-10-26). One edge per camera until
|
|
344
|
+
* {@link PipelinePackageLlmAnswered}.
|
|
345
|
+
*/
|
|
346
|
+
EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
|
|
347
|
+
/** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
|
|
348
|
+
EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
|
|
349
|
+
/**
|
|
342
350
|
* Per-camera pipeline config was mutated by the orchestrator
|
|
343
351
|
* (a settings change via `setAgentInferenceDevices` /
|
|
344
352
|
* `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
|
|
@@ -5581,7 +5589,7 @@ var ZodIssueCode = {
|
|
|
5581
5589
|
var ZodFirstPartyTypeKind;
|
|
5582
5590
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5583
5591
|
//#endregion
|
|
5584
|
-
//#region ../types/dist/sleep-
|
|
5592
|
+
//#region ../types/dist/sleep-bykKrTgu.mjs
|
|
5585
5593
|
/**
|
|
5586
5594
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5587
5595
|
* window to float samples (D455).
|
|
@@ -6596,7 +6604,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
6596
6604
|
this.name = "SecretOverwriteRefusedError";
|
|
6597
6605
|
}
|
|
6598
6606
|
};
|
|
6599
|
-
function isRecord$4(value) {
|
|
6607
|
+
function isRecord$4$1(value) {
|
|
6600
6608
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6601
6609
|
}
|
|
6602
6610
|
function resolvedValue(pass, marker) {
|
|
@@ -6625,7 +6633,7 @@ function inArray(pass, incoming, stored) {
|
|
|
6625
6633
|
return incoming.map((item, index) => {
|
|
6626
6634
|
const twin = stored[index];
|
|
6627
6635
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
6628
|
-
if (isRecord$4(item) && isRecord$4(twin)) return inArrayRow(pass, item, twin);
|
|
6636
|
+
if (isRecord$4$1(item) && isRecord$4$1(twin)) return inArrayRow(pass, item, twin);
|
|
6629
6637
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
6630
6638
|
return item;
|
|
6631
6639
|
});
|
|
@@ -6636,7 +6644,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6636
6644
|
const twin = stored[key];
|
|
6637
6645
|
if (isSecretMarker(twin)) {
|
|
6638
6646
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
6639
|
-
} else if (isRecord$4(value) && isRecord$4(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
6647
|
+
} else if (isRecord$4$1(value) && isRecord$4$1(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
6640
6648
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
6641
6649
|
}
|
|
6642
6650
|
return out;
|
|
@@ -6644,7 +6652,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6644
6652
|
async function walk(pass, incoming, stored) {
|
|
6645
6653
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
6646
6654
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
6647
|
-
if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
|
|
6655
|
+
if (!isRecord$4$1(incoming) || !isRecord$4$1(stored)) return incoming;
|
|
6648
6656
|
const out = { ...incoming };
|
|
6649
6657
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
6650
6658
|
return out;
|
|
@@ -6675,7 +6683,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
6675
6683
|
function isJsonEqual(a, b) {
|
|
6676
6684
|
if (Object.is(a, b)) return true;
|
|
6677
6685
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
6678
|
-
if (isRecord$4(a) && isRecord$4(b)) {
|
|
6686
|
+
if (isRecord$4$1(a) && isRecord$4$1(b)) {
|
|
6679
6687
|
const keys = Object.keys(a);
|
|
6680
6688
|
if (keys.length !== Object.keys(b).length) return false;
|
|
6681
6689
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -6699,7 +6707,7 @@ function markerKeysOf(blob, owner) {
|
|
|
6699
6707
|
}
|
|
6700
6708
|
function nodeFromStored(value, owner) {
|
|
6701
6709
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
6702
|
-
if (!isRecord$4(value)) return null;
|
|
6710
|
+
if (!isRecord$4$1(value)) return null;
|
|
6703
6711
|
const children = /* @__PURE__ */ new Map();
|
|
6704
6712
|
for (const [key, child] of Object.entries(value)) {
|
|
6705
6713
|
const node = nodeFromStored(child, owner);
|
|
@@ -8362,6 +8370,53 @@ var READING_QUANTITIES = [
|
|
|
8362
8370
|
];
|
|
8363
8371
|
var ReadingQuantitySchema = _enum(READING_QUANTITIES);
|
|
8364
8372
|
new Set(READING_QUANTITIES);
|
|
8373
|
+
/**
|
|
8374
|
+
* Stored unit symbol for a quantity. `none` means the number is dimensionless
|
|
8375
|
+
* (or an index / ratio / count). `device` means the cap has no single unit.
|
|
8376
|
+
*/
|
|
8377
|
+
var QUANTITY_BASE = {
|
|
8378
|
+
temperature: "°C",
|
|
8379
|
+
pressure: "hPa",
|
|
8380
|
+
speed: "m/s",
|
|
8381
|
+
precipitation: "mm",
|
|
8382
|
+
"precipitation-rate": "mm/h",
|
|
8383
|
+
length: "m",
|
|
8384
|
+
area: "m²",
|
|
8385
|
+
illuminance: "lx",
|
|
8386
|
+
irradiance: "W/m²",
|
|
8387
|
+
power: "W",
|
|
8388
|
+
energy: "kWh",
|
|
8389
|
+
voltage: "V",
|
|
8390
|
+
current: "A",
|
|
8391
|
+
"apparent-power": "VA",
|
|
8392
|
+
humidity: "%",
|
|
8393
|
+
percentage: "%",
|
|
8394
|
+
"concentration-volume": "ppm",
|
|
8395
|
+
"concentration-mass": "µg/m³",
|
|
8396
|
+
"concentration-ppb": "ppb",
|
|
8397
|
+
volume: "L",
|
|
8398
|
+
duration: "s",
|
|
8399
|
+
"duration-ms": "ms",
|
|
8400
|
+
information: "B",
|
|
8401
|
+
"sound-level": "dBFS",
|
|
8402
|
+
"signal-level": "dBm",
|
|
8403
|
+
"colour-temperature": "mired",
|
|
8404
|
+
angle: "°",
|
|
8405
|
+
bitrate: "kbps",
|
|
8406
|
+
rate: "fps",
|
|
8407
|
+
mass: "kg",
|
|
8408
|
+
index: "none",
|
|
8409
|
+
ratio: "none",
|
|
8410
|
+
count: "none",
|
|
8411
|
+
none: "none",
|
|
8412
|
+
device: "device"
|
|
8413
|
+
};
|
|
8414
|
+
/** The stored unit, or `undefined` when the quantity has no unit symbol. */
|
|
8415
|
+
function baseUnitOf(quantity) {
|
|
8416
|
+
const symbol = QUANTITY_BASE[quantity];
|
|
8417
|
+
if (symbol === "none" || symbol === "device") return void 0;
|
|
8418
|
+
return symbol;
|
|
8419
|
+
}
|
|
8365
8420
|
function readingMeta(quantity, extras) {
|
|
8366
8421
|
return {
|
|
8367
8422
|
quantity,
|
|
@@ -8915,6 +8970,8 @@ var CAP_NAMES = Object.freeze([
|
|
|
8915
8970
|
"filesystem-browse",
|
|
8916
8971
|
"flood",
|
|
8917
8972
|
"gas",
|
|
8973
|
+
"group-commands",
|
|
8974
|
+
"group-status",
|
|
8918
8975
|
"heating-valve",
|
|
8919
8976
|
"humidifier",
|
|
8920
8977
|
"humidity-sensor",
|
|
@@ -9733,6 +9790,20 @@ Object.freeze({
|
|
|
9733
9790
|
"path": "detected",
|
|
9734
9791
|
"valueType": "boolean"
|
|
9735
9792
|
}],
|
|
9793
|
+
"group-status": [
|
|
9794
|
+
{
|
|
9795
|
+
"path": "included",
|
|
9796
|
+
"valueType": "number"
|
|
9797
|
+
},
|
|
9798
|
+
{
|
|
9799
|
+
"path": "excluded",
|
|
9800
|
+
"valueType": "number"
|
|
9801
|
+
},
|
|
9802
|
+
{
|
|
9803
|
+
"path": "total",
|
|
9804
|
+
"valueType": "number"
|
|
9805
|
+
}
|
|
9806
|
+
],
|
|
9736
9807
|
"heating-valve": [{
|
|
9737
9808
|
"path": "position",
|
|
9738
9809
|
"valueType": "number"
|
|
@@ -11074,7 +11145,7 @@ function errMsg(err) {
|
|
|
11074
11145
|
return String(err);
|
|
11075
11146
|
}
|
|
11076
11147
|
//#endregion
|
|
11077
|
-
//#region ../types/dist/composition-
|
|
11148
|
+
//#region ../types/dist/composition-OhpuespA.mjs
|
|
11078
11149
|
/**
|
|
11079
11150
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11080
11151
|
*
|
|
@@ -11187,7 +11258,7 @@ var ExpressionEvalError = class extends Error {
|
|
|
11187
11258
|
this.name = "ExpressionEvalError";
|
|
11188
11259
|
}
|
|
11189
11260
|
};
|
|
11190
|
-
function asFiniteNumber(value, name, index) {
|
|
11261
|
+
function asFiniteNumber$1(value, name, index) {
|
|
11191
11262
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
11192
11263
|
return value;
|
|
11193
11264
|
}
|
|
@@ -11200,7 +11271,7 @@ function finiteResult(value, name) {
|
|
|
11200
11271
|
return value;
|
|
11201
11272
|
}
|
|
11202
11273
|
function allFiniteNumbers(args, name) {
|
|
11203
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
11274
|
+
return args.map((a, idx) => asFiniteNumber$1(a, name, idx));
|
|
11204
11275
|
}
|
|
11205
11276
|
function asBoolean$2(value, name, index) {
|
|
11206
11277
|
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
@@ -11237,29 +11308,29 @@ var table = {
|
|
|
11237
11308
|
abs: {
|
|
11238
11309
|
minArgs: 1,
|
|
11239
11310
|
maxArgs: 1,
|
|
11240
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
11311
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber$1(args[0], "abs", 0)), "abs")
|
|
11241
11312
|
},
|
|
11242
11313
|
floor: {
|
|
11243
11314
|
minArgs: 1,
|
|
11244
11315
|
maxArgs: 1,
|
|
11245
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
11316
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber$1(args[0], "floor", 0)), "floor")
|
|
11246
11317
|
},
|
|
11247
11318
|
ceil: {
|
|
11248
11319
|
minArgs: 1,
|
|
11249
11320
|
maxArgs: 1,
|
|
11250
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
11321
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber$1(args[0], "ceil", 0)), "ceil")
|
|
11251
11322
|
},
|
|
11252
11323
|
sqrt: {
|
|
11253
11324
|
minArgs: 1,
|
|
11254
11325
|
maxArgs: 1,
|
|
11255
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
11326
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber$1(args[0], "sqrt", 0)), "sqrt")
|
|
11256
11327
|
},
|
|
11257
11328
|
round: {
|
|
11258
11329
|
minArgs: 1,
|
|
11259
11330
|
maxArgs: 2,
|
|
11260
11331
|
apply: (args) => {
|
|
11261
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
11262
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
11332
|
+
const x = asFiniteNumber$1(args[0], "round", 0);
|
|
11333
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber$1(args[1], "round", 1)) : 0;
|
|
11263
11334
|
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
11264
11335
|
const factor = 10 ** digits;
|
|
11265
11336
|
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
@@ -11268,15 +11339,15 @@ var table = {
|
|
|
11268
11339
|
pow: {
|
|
11269
11340
|
minArgs: 2,
|
|
11270
11341
|
maxArgs: 2,
|
|
11271
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
11342
|
+
apply: (args) => finiteResult(asFiniteNumber$1(args[0], "pow", 0) ** asFiniteNumber$1(args[1], "pow", 1), "pow")
|
|
11272
11343
|
},
|
|
11273
11344
|
clamp: {
|
|
11274
11345
|
minArgs: 3,
|
|
11275
11346
|
maxArgs: 3,
|
|
11276
11347
|
apply: (args) => {
|
|
11277
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
11278
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
11279
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
11348
|
+
const x = asFiniteNumber$1(args[0], "clamp", 0);
|
|
11349
|
+
const lo = asFiniteNumber$1(args[1], "clamp", 1);
|
|
11350
|
+
const hi = asFiniteNumber$1(args[2], "clamp", 2);
|
|
11280
11351
|
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
11281
11352
|
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
11282
11353
|
}
|
|
@@ -11305,13 +11376,13 @@ var table = {
|
|
|
11305
11376
|
age: {
|
|
11306
11377
|
minArgs: 2,
|
|
11307
11378
|
maxArgs: 2,
|
|
11308
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
11379
|
+
apply: (args) => finiteResult(asFiniteNumber$1(args[0], "age", 0) - asFiniteNumber$1(args[1], "age", 1), "age")
|
|
11309
11380
|
},
|
|
11310
11381
|
convert: {
|
|
11311
11382
|
minArgs: 3,
|
|
11312
11383
|
maxArgs: 3,
|
|
11313
11384
|
apply: (args, hooks) => {
|
|
11314
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
11385
|
+
const x = asFiniteNumber$1(args[0], "convert", 0);
|
|
11315
11386
|
const from = asString(args[1], "convert", 1).trim();
|
|
11316
11387
|
const to = asString(args[2], "convert", 2).trim();
|
|
11317
11388
|
if (hooks.convert) {
|
|
@@ -11337,11 +11408,11 @@ var table = {
|
|
|
11337
11408
|
minArgs: 2,
|
|
11338
11409
|
maxArgs: 2,
|
|
11339
11410
|
apply: (args, hooks) => {
|
|
11340
|
-
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
11411
|
+
const windowMs = asFiniteNumber$1(args[1], "within", 1);
|
|
11341
11412
|
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
11342
11413
|
const at = args[0];
|
|
11343
11414
|
if (at === null) return false;
|
|
11344
|
-
const ts = asFiniteNumber(at, "within", 0);
|
|
11415
|
+
const ts = asFiniteNumber$1(at, "within", 0);
|
|
11345
11416
|
const now = hooks.now;
|
|
11346
11417
|
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
11347
11418
|
const inside = now - ts <= windowMs;
|
|
@@ -11358,7 +11429,7 @@ var table = {
|
|
|
11358
11429
|
minArgs: 1,
|
|
11359
11430
|
maxArgs: INF,
|
|
11360
11431
|
apply: (args) => {
|
|
11361
|
-
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
|
|
11432
|
+
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber$1(a, "latest", i)]);
|
|
11362
11433
|
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
11363
11434
|
}
|
|
11364
11435
|
},
|
|
@@ -11829,15 +11900,74 @@ function validateExpressionSource(src) {
|
|
|
11829
11900
|
}
|
|
11830
11901
|
return null;
|
|
11831
11902
|
}
|
|
11832
|
-
/**
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11903
|
+
/**
|
|
11904
|
+
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
11905
|
+
* id. Split out so helper schemas can name it without a cycle through
|
|
11906
|
+
* `composition.ts` (D659).
|
|
11907
|
+
*/
|
|
11836
11908
|
var CompositionSourceRefSchema = object({
|
|
11837
11909
|
/** The owning addon; `stableId` is unique only within it. */
|
|
11838
11910
|
addonId: string$2().min(1),
|
|
11839
11911
|
stableId: string$2().min(1)
|
|
11840
11912
|
});
|
|
11913
|
+
var GROUP_TEMPLATE_IDS = [
|
|
11914
|
+
"temperature",
|
|
11915
|
+
"humidity",
|
|
11916
|
+
"pressure",
|
|
11917
|
+
"illuminance",
|
|
11918
|
+
"power",
|
|
11919
|
+
"energy",
|
|
11920
|
+
"battery",
|
|
11921
|
+
"numeric",
|
|
11922
|
+
"motion",
|
|
11923
|
+
"contact",
|
|
11924
|
+
"leak",
|
|
11925
|
+
"smoke",
|
|
11926
|
+
"occupancy",
|
|
11927
|
+
"binary",
|
|
11928
|
+
"switch",
|
|
11929
|
+
"light"
|
|
11930
|
+
];
|
|
11931
|
+
var NUMERIC_AGGREGATES = [
|
|
11932
|
+
"mean",
|
|
11933
|
+
"median",
|
|
11934
|
+
"min",
|
|
11935
|
+
"max",
|
|
11936
|
+
"sum"
|
|
11937
|
+
];
|
|
11938
|
+
var BINARY_AGGREGATES = ["any", "all"];
|
|
11939
|
+
/** How an actuator group decides it is on. Unknown members never count as off. */
|
|
11940
|
+
var ACTIVE_CRITERIA = ["at-least-one", "all"];
|
|
11941
|
+
var AGGREGATE_REDUCERS = [
|
|
11942
|
+
"mean",
|
|
11943
|
+
"median",
|
|
11944
|
+
"min",
|
|
11945
|
+
"max",
|
|
11946
|
+
"sum",
|
|
11947
|
+
"any",
|
|
11948
|
+
"all"
|
|
11949
|
+
];
|
|
11950
|
+
var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
|
|
11951
|
+
var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
|
|
11952
|
+
var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
|
|
11953
|
+
/** One member. The field is stored so occupancy can name motion or binary. */
|
|
11954
|
+
var HelperMemberSchema = object({
|
|
11955
|
+
source: CompositionSourceRefSchema,
|
|
11956
|
+
cap: string$2().min(1),
|
|
11957
|
+
fieldPath: string$2().min(1)
|
|
11958
|
+
});
|
|
11959
|
+
var HelperSpecSchema = discriminatedUnion("kind", [object({
|
|
11960
|
+
kind: literal("group"),
|
|
11961
|
+
templateId: GroupTemplateIdSchema,
|
|
11962
|
+
aggregate: AggregateReducerSchema,
|
|
11963
|
+
members: array(HelperMemberSchema).min(1).max(64),
|
|
11964
|
+
/** Actuator groups only. Readings leave this unset. */
|
|
11965
|
+
active: ActiveCriterionSchema.optional()
|
|
11966
|
+
})]);
|
|
11967
|
+
/** A composed device's stableId is this prefix plus the block id. */
|
|
11968
|
+
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
11969
|
+
/** Longest snippet a `code` source may carry (slice 4). */
|
|
11970
|
+
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
11841
11971
|
/** One field of one capability of one source device. */
|
|
11842
11972
|
var CompositionFieldReadSchema = object({
|
|
11843
11973
|
source: CompositionSourceRefSchema,
|
|
@@ -11881,7 +12011,33 @@ var CompositionCodeSourceSchema = object({
|
|
|
11881
12011
|
var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
11882
12012
|
CompositionFromSourceSchema,
|
|
11883
12013
|
CompositionExpressionSourceSchema,
|
|
11884
|
-
CompositionCodeSourceSchema
|
|
12014
|
+
CompositionCodeSourceSchema,
|
|
12015
|
+
object({
|
|
12016
|
+
kind: literal("aggregate"),
|
|
12017
|
+
reducer: _enum([
|
|
12018
|
+
"mean",
|
|
12019
|
+
"median",
|
|
12020
|
+
"min",
|
|
12021
|
+
"max",
|
|
12022
|
+
"sum",
|
|
12023
|
+
"any",
|
|
12024
|
+
"all"
|
|
12025
|
+
]),
|
|
12026
|
+
reads: array(CompositionFieldReadSchema).min(1).max(64),
|
|
12027
|
+
/** `value` is the reading; the others are the member counts. */
|
|
12028
|
+
publish: _enum([
|
|
12029
|
+
"value",
|
|
12030
|
+
"included",
|
|
12031
|
+
"excluded",
|
|
12032
|
+
"total"
|
|
12033
|
+
]),
|
|
12034
|
+
/** What a `value` publish does when nobody is included. Counts ignore this. */
|
|
12035
|
+
empty: _enum([
|
|
12036
|
+
"null",
|
|
12037
|
+
"omit",
|
|
12038
|
+
"offline"
|
|
12039
|
+
])
|
|
12040
|
+
})
|
|
11885
12041
|
]);
|
|
11886
12042
|
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
11887
12043
|
kind: literal("forward"),
|
|
@@ -11927,7 +12083,13 @@ var CompositionSchema = object({
|
|
|
11927
12083
|
kind: literal("existing"),
|
|
11928
12084
|
device: CompositionSourceRefSchema
|
|
11929
12085
|
})]),
|
|
11930
|
-
features: array(CompositionFeatureSchema).min(1).max(16)
|
|
12086
|
+
features: array(CompositionFeatureSchema).min(1).max(16),
|
|
12087
|
+
/**
|
|
12088
|
+
* Present when the block is a helper template. Absent on a free
|
|
12089
|
+
* composition (the room example, device 4603). The editor reads this,
|
|
12090
|
+
* never the compiled features.
|
|
12091
|
+
*/
|
|
12092
|
+
helper: HelperSpecSchema.optional()
|
|
11931
12093
|
}).superRefine((composition, ctx) => {
|
|
11932
12094
|
const seen = /* @__PURE__ */ new Set();
|
|
11933
12095
|
composition.features.forEach((feature, index) => {
|
|
@@ -18185,7 +18347,10 @@ var CompositionProblemSchema = object({
|
|
|
18185
18347
|
"target-missing",
|
|
18186
18348
|
"target-unreadable",
|
|
18187
18349
|
"not-replaceable",
|
|
18188
|
-
"native-capability-replace"
|
|
18350
|
+
"native-capability-replace",
|
|
18351
|
+
"mixed-quantity",
|
|
18352
|
+
"helper-mismatch",
|
|
18353
|
+
"member-constraint"
|
|
18189
18354
|
]),
|
|
18190
18355
|
path: string$2(),
|
|
18191
18356
|
message: string$2()
|
|
@@ -18206,7 +18371,8 @@ var CompositionUnavailableModeSchema = _enum([
|
|
|
18206
18371
|
"null",
|
|
18207
18372
|
"offline",
|
|
18208
18373
|
"unknown",
|
|
18209
|
-
"release"
|
|
18374
|
+
"release",
|
|
18375
|
+
"omit"
|
|
18210
18376
|
]);
|
|
18211
18377
|
/**
|
|
18212
18378
|
* `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
|
|
@@ -18420,6 +18586,275 @@ Object.freeze({
|
|
|
18420
18586
|
result: ["number", "null"]
|
|
18421
18587
|
}
|
|
18422
18588
|
});
|
|
18589
|
+
/**
|
|
18590
|
+
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18591
|
+
* Data, not a form default written into the block.
|
|
18592
|
+
*/
|
|
18593
|
+
var NOW = {
|
|
18594
|
+
fieldPath: "lastFetchedAt",
|
|
18595
|
+
expr: "now"
|
|
18596
|
+
};
|
|
18597
|
+
var CHANGED = {
|
|
18598
|
+
fieldPath: "lastChangedAt",
|
|
18599
|
+
expr: "now"
|
|
18600
|
+
};
|
|
18601
|
+
function numeric(input) {
|
|
18602
|
+
return {
|
|
18603
|
+
...input,
|
|
18604
|
+
family: "numeric",
|
|
18605
|
+
deviceType: DeviceType.Sensor,
|
|
18606
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18607
|
+
commandCaps: [],
|
|
18608
|
+
baseUnit: baseUnitOf(input.quantity),
|
|
18609
|
+
aggregates: NUMERIC_AGGREGATES,
|
|
18610
|
+
accept: [{
|
|
18611
|
+
cap: input.outputCap,
|
|
18612
|
+
fieldPath: input.outputField
|
|
18613
|
+
}]
|
|
18614
|
+
};
|
|
18615
|
+
}
|
|
18616
|
+
function binary(input) {
|
|
18617
|
+
return {
|
|
18618
|
+
...input,
|
|
18619
|
+
family: "binary",
|
|
18620
|
+
deviceType: DeviceType.Sensor,
|
|
18621
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18622
|
+
commandCaps: [],
|
|
18623
|
+
quantity: "none",
|
|
18624
|
+
baseUnit: void 0,
|
|
18625
|
+
aggregates: BINARY_AGGREGATES,
|
|
18626
|
+
empty: "offline"
|
|
18627
|
+
};
|
|
18628
|
+
}
|
|
18629
|
+
var GROUP_TEMPLATES = [
|
|
18630
|
+
numeric({
|
|
18631
|
+
id: "temperature",
|
|
18632
|
+
summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
|
|
18633
|
+
role: DeviceRole.TemperatureSensor,
|
|
18634
|
+
outputCap: "temperature-sensor",
|
|
18635
|
+
outputField: "celsius",
|
|
18636
|
+
quantity: "temperature",
|
|
18637
|
+
defaultAggregate: "mean",
|
|
18638
|
+
empty: "null",
|
|
18639
|
+
companions: [NOW]
|
|
18640
|
+
}),
|
|
18641
|
+
numeric({
|
|
18642
|
+
id: "humidity",
|
|
18643
|
+
summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18644
|
+
role: DeviceRole.HumiditySensor,
|
|
18645
|
+
outputCap: "humidity-sensor",
|
|
18646
|
+
outputField: "percent",
|
|
18647
|
+
quantity: "humidity",
|
|
18648
|
+
defaultAggregate: "mean",
|
|
18649
|
+
empty: "null",
|
|
18650
|
+
companions: [NOW]
|
|
18651
|
+
}),
|
|
18652
|
+
numeric({
|
|
18653
|
+
id: "pressure",
|
|
18654
|
+
summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
|
|
18655
|
+
role: DeviceRole.PressureSensor,
|
|
18656
|
+
outputCap: "pressure-sensor",
|
|
18657
|
+
outputField: "hpa",
|
|
18658
|
+
quantity: "pressure",
|
|
18659
|
+
defaultAggregate: "mean",
|
|
18660
|
+
empty: "null",
|
|
18661
|
+
companions: [NOW]
|
|
18662
|
+
}),
|
|
18663
|
+
numeric({
|
|
18664
|
+
id: "illuminance",
|
|
18665
|
+
summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
|
|
18666
|
+
role: DeviceRole.AmbientLightSensor,
|
|
18667
|
+
outputCap: "ambient-light-sensor",
|
|
18668
|
+
outputField: "lux",
|
|
18669
|
+
quantity: "illuminance",
|
|
18670
|
+
defaultAggregate: "mean",
|
|
18671
|
+
empty: "null",
|
|
18672
|
+
companions: [NOW]
|
|
18673
|
+
}),
|
|
18674
|
+
numeric({
|
|
18675
|
+
id: "power",
|
|
18676
|
+
summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
|
|
18677
|
+
role: DeviceRole.PowerSensor,
|
|
18678
|
+
outputCap: "power-meter",
|
|
18679
|
+
outputField: "watts",
|
|
18680
|
+
quantity: "power",
|
|
18681
|
+
defaultAggregate: "sum",
|
|
18682
|
+
empty: "omit",
|
|
18683
|
+
companions: [NOW]
|
|
18684
|
+
}),
|
|
18685
|
+
numeric({
|
|
18686
|
+
id: "energy",
|
|
18687
|
+
summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
|
|
18688
|
+
role: DeviceRole.EnergySensor,
|
|
18689
|
+
outputCap: "power-meter",
|
|
18690
|
+
outputField: "kwhTotal",
|
|
18691
|
+
quantity: "energy",
|
|
18692
|
+
defaultAggregate: "sum",
|
|
18693
|
+
empty: "omit",
|
|
18694
|
+
companions: [NOW]
|
|
18695
|
+
}),
|
|
18696
|
+
numeric({
|
|
18697
|
+
id: "battery",
|
|
18698
|
+
summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18699
|
+
role: DeviceRole.BatterySensor,
|
|
18700
|
+
outputCap: "battery",
|
|
18701
|
+
outputField: "percentage",
|
|
18702
|
+
quantity: "percentage",
|
|
18703
|
+
defaultAggregate: "mean",
|
|
18704
|
+
empty: "null",
|
|
18705
|
+
companions: [
|
|
18706
|
+
{
|
|
18707
|
+
fieldPath: "charging",
|
|
18708
|
+
expr: "\"unknown\""
|
|
18709
|
+
},
|
|
18710
|
+
{
|
|
18711
|
+
fieldPath: "sleeping",
|
|
18712
|
+
expr: "false"
|
|
18713
|
+
},
|
|
18714
|
+
{
|
|
18715
|
+
fieldPath: "lastUpdated",
|
|
18716
|
+
expr: "now"
|
|
18717
|
+
}
|
|
18718
|
+
]
|
|
18719
|
+
}),
|
|
18720
|
+
numeric({
|
|
18721
|
+
id: "numeric",
|
|
18722
|
+
summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
|
|
18723
|
+
role: DeviceRole.NumericSensor,
|
|
18724
|
+
outputCap: "numeric-sensor",
|
|
18725
|
+
outputField: "value",
|
|
18726
|
+
quantity: "device",
|
|
18727
|
+
defaultAggregate: "mean",
|
|
18728
|
+
empty: "null",
|
|
18729
|
+
companions: [NOW]
|
|
18730
|
+
}),
|
|
18731
|
+
binary({
|
|
18732
|
+
id: "motion",
|
|
18733
|
+
summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
|
|
18734
|
+
role: DeviceRole.MotionSensor,
|
|
18735
|
+
outputCap: "motion",
|
|
18736
|
+
outputField: "detected",
|
|
18737
|
+
defaultAggregate: "any",
|
|
18738
|
+
accept: [{
|
|
18739
|
+
cap: "motion",
|
|
18740
|
+
fieldPath: "detected"
|
|
18741
|
+
}],
|
|
18742
|
+
companions: [{
|
|
18743
|
+
fieldPath: "lastDetectedAt",
|
|
18744
|
+
expr: "null"
|
|
18745
|
+
}, {
|
|
18746
|
+
fieldPath: "autoClearAfterMs",
|
|
18747
|
+
expr: "null"
|
|
18748
|
+
}]
|
|
18749
|
+
}),
|
|
18750
|
+
binary({
|
|
18751
|
+
id: "contact",
|
|
18752
|
+
summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
|
|
18753
|
+
role: DeviceRole.ContactSensor,
|
|
18754
|
+
outputCap: "contact",
|
|
18755
|
+
outputField: "entryOpen",
|
|
18756
|
+
defaultAggregate: "any",
|
|
18757
|
+
accept: [{
|
|
18758
|
+
cap: "contact",
|
|
18759
|
+
fieldPath: "entryOpen"
|
|
18760
|
+
}],
|
|
18761
|
+
companions: [CHANGED]
|
|
18762
|
+
}),
|
|
18763
|
+
binary({
|
|
18764
|
+
id: "leak",
|
|
18765
|
+
summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
|
|
18766
|
+
role: DeviceRole.LeakSensor,
|
|
18767
|
+
outputCap: "flood",
|
|
18768
|
+
outputField: "flooded",
|
|
18769
|
+
defaultAggregate: "any",
|
|
18770
|
+
accept: [{
|
|
18771
|
+
cap: "flood",
|
|
18772
|
+
fieldPath: "flooded"
|
|
18773
|
+
}],
|
|
18774
|
+
companions: [CHANGED]
|
|
18775
|
+
}),
|
|
18776
|
+
binary({
|
|
18777
|
+
id: "smoke",
|
|
18778
|
+
summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
|
|
18779
|
+
role: DeviceRole.SmokeSensor,
|
|
18780
|
+
outputCap: "smoke",
|
|
18781
|
+
outputField: "detected",
|
|
18782
|
+
defaultAggregate: "any",
|
|
18783
|
+
accept: [{
|
|
18784
|
+
cap: "smoke",
|
|
18785
|
+
fieldPath: "detected"
|
|
18786
|
+
}],
|
|
18787
|
+
companions: [CHANGED]
|
|
18788
|
+
}),
|
|
18789
|
+
binary({
|
|
18790
|
+
id: "occupancy",
|
|
18791
|
+
summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
|
|
18792
|
+
role: DeviceRole.BinarySensor,
|
|
18793
|
+
outputCap: "binary",
|
|
18794
|
+
outputField: "on",
|
|
18795
|
+
defaultAggregate: "any",
|
|
18796
|
+
accept: [{
|
|
18797
|
+
cap: "motion",
|
|
18798
|
+
fieldPath: "detected"
|
|
18799
|
+
}, {
|
|
18800
|
+
cap: "binary",
|
|
18801
|
+
fieldPath: "on"
|
|
18802
|
+
}],
|
|
18803
|
+
companions: [CHANGED]
|
|
18804
|
+
}),
|
|
18805
|
+
binary({
|
|
18806
|
+
id: "binary",
|
|
18807
|
+
summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
|
|
18808
|
+
role: DeviceRole.BinarySensor,
|
|
18809
|
+
outputCap: "binary",
|
|
18810
|
+
outputField: "on",
|
|
18811
|
+
defaultAggregate: "any",
|
|
18812
|
+
accept: [{
|
|
18813
|
+
cap: "binary",
|
|
18814
|
+
fieldPath: "on"
|
|
18815
|
+
}],
|
|
18816
|
+
companions: [CHANGED]
|
|
18817
|
+
}),
|
|
18818
|
+
actuator({
|
|
18819
|
+
id: "switch",
|
|
18820
|
+
summary: "On when at least one switch is on, or when all of them are. Offline and unknown members are left out of that decision, never read as off, and still receive the command.",
|
|
18821
|
+
deviceType: DeviceType.Switch,
|
|
18822
|
+
outputCap: "switch",
|
|
18823
|
+
commandCaps: ["switch"]
|
|
18824
|
+
}),
|
|
18825
|
+
actuator({
|
|
18826
|
+
id: "light",
|
|
18827
|
+
summary: "On when at least one light is on, or when all of them are. Brightness and colour are offered only when every member has them. A command is not sent to a member that lacks it.",
|
|
18828
|
+
deviceType: DeviceType.Light,
|
|
18829
|
+
outputCap: "switch",
|
|
18830
|
+
commandCaps: [
|
|
18831
|
+
"switch",
|
|
18832
|
+
"brightness",
|
|
18833
|
+
"color"
|
|
18834
|
+
]
|
|
18835
|
+
})
|
|
18836
|
+
];
|
|
18837
|
+
function actuator(input) {
|
|
18838
|
+
return {
|
|
18839
|
+
...input,
|
|
18840
|
+
family: "actuator",
|
|
18841
|
+
role: void 0,
|
|
18842
|
+
memberDeviceType: input.deviceType,
|
|
18843
|
+
outputField: "on",
|
|
18844
|
+
quantity: "none",
|
|
18845
|
+
baseUnit: void 0,
|
|
18846
|
+
aggregates: BINARY_AGGREGATES,
|
|
18847
|
+
defaultAggregate: "any",
|
|
18848
|
+
empty: "offline",
|
|
18849
|
+
accept: [{
|
|
18850
|
+
cap: "switch",
|
|
18851
|
+
fieldPath: "on"
|
|
18852
|
+
}],
|
|
18853
|
+
companions: [CHANGED]
|
|
18854
|
+
};
|
|
18855
|
+
}
|
|
18856
|
+
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
18857
|
+
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
18423
18858
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
18424
18859
|
/**
|
|
18425
18860
|
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
@@ -45239,6 +45674,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
45239
45674
|
"detection-blind",
|
|
45240
45675
|
"motion-noisy",
|
|
45241
45676
|
"motion-calm",
|
|
45677
|
+
"package-llm-silent",
|
|
45678
|
+
"package-llm-answered",
|
|
45242
45679
|
"recording-stalled",
|
|
45243
45680
|
"recording-healthy",
|
|
45244
45681
|
"recording-degraded",
|
|
@@ -56330,6 +56767,28 @@ var PackageVersionInfoSchema = object({
|
|
|
56330
56767
|
deprecated: string$2().optional(),
|
|
56331
56768
|
distTags: array(string$2()).readonly()
|
|
56332
56769
|
});
|
|
56770
|
+
/**
|
|
56771
|
+
* One published document. `ok` is markdown from the registry. `missing` means
|
|
56772
|
+
* the registry answered and the file was not there. `error` is a registry or
|
|
56773
|
+
* tarball failure — never an empty string standing in for either.
|
|
56774
|
+
*/
|
|
56775
|
+
var PackageDocBodySchema = discriminatedUnion("status", [
|
|
56776
|
+
object({
|
|
56777
|
+
status: literal("ok"),
|
|
56778
|
+
markdown: string$2()
|
|
56779
|
+
}),
|
|
56780
|
+
object({ status: literal("missing") }),
|
|
56781
|
+
object({
|
|
56782
|
+
status: literal("error"),
|
|
56783
|
+
message: string$2()
|
|
56784
|
+
})
|
|
56785
|
+
]);
|
|
56786
|
+
var PackageDocsSchema = object({
|
|
56787
|
+
name: string$2(),
|
|
56788
|
+
version: string$2(),
|
|
56789
|
+
readme: PackageDocBodySchema,
|
|
56790
|
+
changelog: PackageDocBodySchema
|
|
56791
|
+
});
|
|
56333
56792
|
var SearchResultSchema = object({
|
|
56334
56793
|
name: string$2(),
|
|
56335
56794
|
version: string$2(),
|
|
@@ -56548,7 +57007,10 @@ method(object({ nodeId: string$2().optional() }).optional(), array(AddonListItem
|
|
|
56548
57007
|
}), object({ success: literal(true) }), {
|
|
56549
57008
|
kind: "mutation",
|
|
56550
57009
|
auth: "admin"
|
|
56551
|
-
}), method(object({ name: string$2() }), array(PackageVersionInfoSchema).readonly()), method(object({
|
|
57010
|
+
}), method(object({ name: string$2() }), array(PackageVersionInfoSchema).readonly()), method(object({
|
|
57011
|
+
name: string$2().min(1),
|
|
57012
|
+
version: string$2().min(1).optional()
|
|
57013
|
+
}), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string$2() }), RestartAddonResultSchema, {
|
|
56552
57014
|
kind: "mutation",
|
|
56553
57015
|
auth: "admin"
|
|
56554
57016
|
}), method(object({ packageName: string$2() }), object({ success: literal(true) }), {
|
|
@@ -59884,6 +60346,88 @@ var gasCapability = {
|
|
|
59884
60346
|
* whether persisting is worth a SQLite commit. */
|
|
59885
60347
|
volatileStateFields: ["lastChangedAt"]
|
|
59886
60348
|
};
|
|
60349
|
+
var GroupCommandsSchema = object({
|
|
60350
|
+
/** One row per member that has been commanded. A later command replaces that member's row. */
|
|
60351
|
+
results: array(object({
|
|
60352
|
+
deviceId: number().int().nonnegative(),
|
|
60353
|
+
cap: string$2().min(1),
|
|
60354
|
+
method: string$2().min(1),
|
|
60355
|
+
ok: boolean(),
|
|
60356
|
+
error: string$2().optional(),
|
|
60357
|
+
/** Ms epoch of the attempt. */
|
|
60358
|
+
at: number()
|
|
60359
|
+
})) });
|
|
60360
|
+
var groupCommandsCapability = {
|
|
60361
|
+
name: "group-commands",
|
|
60362
|
+
scope: "device",
|
|
60363
|
+
deviceNative: true,
|
|
60364
|
+
mode: "singleton",
|
|
60365
|
+
deviceTypes: [DeviceType.Switch, DeviceType.Light],
|
|
60366
|
+
methods: {},
|
|
60367
|
+
status: {
|
|
60368
|
+
schema: GroupCommandsSchema,
|
|
60369
|
+
kind: "push"
|
|
60370
|
+
},
|
|
60371
|
+
runtimeState: GroupCommandsSchema,
|
|
60372
|
+
/**
|
|
60373
|
+
* Runtime-state durability: **session** — the last attempt. A restored
|
|
60374
|
+
* failure would outlive the command that caused it.
|
|
60375
|
+
*
|
|
60376
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
60377
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
60378
|
+
*/
|
|
60379
|
+
durability: "session",
|
|
60380
|
+
history: "none"
|
|
60381
|
+
};
|
|
60382
|
+
/**
|
|
60383
|
+
* How many members of a helper group answered (D944). Counts are integers,
|
|
60384
|
+
* not measurements: zero included is a real count, never a stand-in reading.
|
|
60385
|
+
* The reading itself stays on the output capability. History is none — the
|
|
60386
|
+
* series that later statistics read is the output cap's own history block.
|
|
60387
|
+
*/
|
|
60388
|
+
var GroupStatusSchema = object({
|
|
60389
|
+
/** Members whose current value entered the aggregate. */
|
|
60390
|
+
included: number().int().nonnegative().meta(readingMeta("count", {
|
|
60391
|
+
precision: 0,
|
|
60392
|
+
label: "Reporting"
|
|
60393
|
+
})),
|
|
60394
|
+
/** Members left out: offline, unknown, or not a finite reading (D393). */
|
|
60395
|
+
excluded: number().int().nonnegative().meta(readingMeta("count", {
|
|
60396
|
+
precision: 0,
|
|
60397
|
+
label: "Excluded"
|
|
60398
|
+
})),
|
|
60399
|
+
/** `included` + `excluded`. */
|
|
60400
|
+
total: number().int().nonnegative().meta(readingMeta("count", {
|
|
60401
|
+
precision: 0,
|
|
60402
|
+
label: "Members"
|
|
60403
|
+
}))
|
|
60404
|
+
});
|
|
60405
|
+
var groupStatusCapability = {
|
|
60406
|
+
name: "group-status",
|
|
60407
|
+
scope: "device",
|
|
60408
|
+
deviceNative: true,
|
|
60409
|
+
mode: "singleton",
|
|
60410
|
+
deviceTypes: [
|
|
60411
|
+
DeviceType.Sensor,
|
|
60412
|
+
DeviceType.Switch,
|
|
60413
|
+
DeviceType.Light
|
|
60414
|
+
],
|
|
60415
|
+
methods: {},
|
|
60416
|
+
status: {
|
|
60417
|
+
schema: GroupStatusSchema,
|
|
60418
|
+
kind: "push"
|
|
60419
|
+
},
|
|
60420
|
+
runtimeState: GroupStatusSchema,
|
|
60421
|
+
/**
|
|
60422
|
+
* Runtime-state durability: **session** — recomputed from members on the
|
|
60423
|
+
* next source event; a restored count would outlive the readings it named.
|
|
60424
|
+
*
|
|
60425
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
60426
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
60427
|
+
*/
|
|
60428
|
+
durability: "session",
|
|
60429
|
+
history: "none"
|
|
60430
|
+
};
|
|
59887
60431
|
var HeatingValveStatusSchema = object({
|
|
59888
60432
|
/** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
|
|
59889
60433
|
position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
@@ -61653,12 +62197,18 @@ var motionCapability = {
|
|
|
61653
62197
|
methods: {
|
|
61654
62198
|
/**
|
|
61655
62199
|
* Pull the current motion state synchronously. Convenience shortcut
|
|
61656
|
-
* for consumers that don't need the full status object
|
|
61657
|
-
*
|
|
61658
|
-
* `getStatus` once the auto-injected status surface lands in every
|
|
61659
|
-
* consumer.
|
|
62200
|
+
* for consumers that don't need the full status object. A composed
|
|
62201
|
+
* group answers from its slice (D944): unknown throws, it is not off.
|
|
61660
62202
|
*/
|
|
61661
62203
|
isDetected: method(object({ deviceId: number() }), boolean()) },
|
|
62204
|
+
/**
|
|
62205
|
+
* A composed motion group serves `isDetected` from `detected` (D944).
|
|
62206
|
+
* Without this the cap stays uncomposable (`has-commands`).
|
|
62207
|
+
*/
|
|
62208
|
+
composedMethods: { isDetected: {
|
|
62209
|
+
kind: "slice",
|
|
62210
|
+
field: "detected"
|
|
62211
|
+
} },
|
|
61662
62212
|
events: {
|
|
61663
62213
|
/**
|
|
61664
62214
|
* Fires every time the runner transitions a camera between
|
|
@@ -69466,6 +70016,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
69466
70016
|
featureProbe: featureProbeCapability,
|
|
69467
70017
|
flood: floodCapability,
|
|
69468
70018
|
gas: gasCapability,
|
|
70019
|
+
groupCommands: groupCommandsCapability,
|
|
70020
|
+
groupStatus: groupStatusCapability,
|
|
69469
70021
|
heatingValve: heatingValveCapability,
|
|
69470
70022
|
humidifier: humidifierCapability,
|
|
69471
70023
|
humiditySensor: humiditySensorCapability,
|
|
@@ -70842,6 +71394,13 @@ Object.freeze({
|
|
|
70842
71394
|
access: "view",
|
|
70843
71395
|
role: "admin"
|
|
70844
71396
|
},
|
|
71397
|
+
"addons.getPackageDocs": {
|
|
71398
|
+
capName: "addons",
|
|
71399
|
+
capScope: "system",
|
|
71400
|
+
addonId: null,
|
|
71401
|
+
access: "view",
|
|
71402
|
+
role: "admin"
|
|
71403
|
+
},
|
|
70845
71404
|
"addons.getVersions": {
|
|
70846
71405
|
capName: "addons",
|
|
70847
71406
|
capScope: "system",
|
|
@@ -73950,6 +74509,20 @@ Object.freeze({
|
|
|
73950
74509
|
access: "view",
|
|
73951
74510
|
role: "viewer"
|
|
73952
74511
|
},
|
|
74512
|
+
"groupCommands.getStatus": {
|
|
74513
|
+
capName: "group-commands",
|
|
74514
|
+
capScope: "device",
|
|
74515
|
+
addonId: null,
|
|
74516
|
+
access: "view",
|
|
74517
|
+
role: "viewer"
|
|
74518
|
+
},
|
|
74519
|
+
"groupStatus.getStatus": {
|
|
74520
|
+
capName: "group-status",
|
|
74521
|
+
capScope: "device",
|
|
74522
|
+
addonId: null,
|
|
74523
|
+
access: "view",
|
|
74524
|
+
role: "viewer"
|
|
74525
|
+
},
|
|
73953
74526
|
"heatingValve.getStatus": {
|
|
73954
74527
|
capName: "heating-valve",
|
|
73955
74528
|
capScope: "device",
|
|
@@ -83772,6 +84345,37 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
83772
84345
|
icon: "flame"
|
|
83773
84346
|
}]
|
|
83774
84347
|
},
|
|
84348
|
+
"group-commands": {
|
|
84349
|
+
scope: "device",
|
|
84350
|
+
noun: t("Group command", "Comando di gruppo")
|
|
84351
|
+
},
|
|
84352
|
+
"group-status": {
|
|
84353
|
+
scope: "device",
|
|
84354
|
+
noun: t("Group", "Gruppo"),
|
|
84355
|
+
state: [
|
|
84356
|
+
{
|
|
84357
|
+
kind: "numeric",
|
|
84358
|
+
field: "included",
|
|
84359
|
+
quantity: t("the members reporting", "i membri che rispondono"),
|
|
84360
|
+
label: t("Reporting", "Rispondono"),
|
|
84361
|
+
icon: "users"
|
|
84362
|
+
},
|
|
84363
|
+
{
|
|
84364
|
+
kind: "numeric",
|
|
84365
|
+
field: "excluded",
|
|
84366
|
+
quantity: t("the members left out", "i membri esclusi"),
|
|
84367
|
+
label: t("Excluded", "Esclusi"),
|
|
84368
|
+
icon: "users"
|
|
84369
|
+
},
|
|
84370
|
+
{
|
|
84371
|
+
kind: "numeric",
|
|
84372
|
+
field: "total",
|
|
84373
|
+
quantity: t("the member count", "il numero di membri"),
|
|
84374
|
+
label: t("Members", "Membri"),
|
|
84375
|
+
icon: "sigma"
|
|
84376
|
+
}
|
|
84377
|
+
]
|
|
84378
|
+
},
|
|
83775
84379
|
"humidity-sensor": {
|
|
83776
84380
|
scope: "device",
|
|
83777
84381
|
noun: t("Humidity", "Umidità"),
|
|
@@ -85237,6 +85841,16 @@ Object.freeze({
|
|
|
85237
85841
|
form: "single",
|
|
85238
85842
|
optional: false
|
|
85239
85843
|
}],
|
|
85844
|
+
"groupCommands.getStatus": [{
|
|
85845
|
+
name: "deviceId",
|
|
85846
|
+
form: "single",
|
|
85847
|
+
optional: false
|
|
85848
|
+
}],
|
|
85849
|
+
"groupStatus.getStatus": [{
|
|
85850
|
+
name: "deviceId",
|
|
85851
|
+
form: "single",
|
|
85852
|
+
optional: false
|
|
85853
|
+
}],
|
|
85240
85854
|
"heatingValve.getStatus": [{
|
|
85241
85855
|
name: "deviceId",
|
|
85242
85856
|
form: "single",
|
|
@@ -87221,6 +87835,8 @@ new Map(Object.entries({
|
|
|
87221
87835
|
"detection-blind": "device-events",
|
|
87222
87836
|
"motion-noisy": "device-events",
|
|
87223
87837
|
"motion-calm": "device-events",
|
|
87838
|
+
"package-llm-silent": "device-events",
|
|
87839
|
+
"package-llm-answered": "device-events",
|
|
87224
87840
|
"recording-stalled": "device-events",
|
|
87225
87841
|
"recording-healthy": "device-events",
|
|
87226
87842
|
"recording-degraded": "device-events",
|
|
@@ -87304,6 +87920,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
87304
87920
|
label: "Motion without a subject",
|
|
87305
87921
|
kinds: ["motion-noisy", "motion-calm"]
|
|
87306
87922
|
},
|
|
87923
|
+
{
|
|
87924
|
+
id: "package-llm",
|
|
87925
|
+
family: "device-events",
|
|
87926
|
+
label: "Package confirm",
|
|
87927
|
+
kinds: ["package-llm-silent", "package-llm-answered"]
|
|
87928
|
+
},
|
|
87307
87929
|
{
|
|
87308
87930
|
id: "recording",
|
|
87309
87931
|
family: "device-events",
|
|
@@ -87485,6 +88107,8 @@ var DEVICE_EVENTS = [
|
|
|
87485
88107
|
"detection-blind",
|
|
87486
88108
|
"motion-noisy",
|
|
87487
88109
|
"motion-calm",
|
|
88110
|
+
"package-llm-silent",
|
|
88111
|
+
"package-llm-answered",
|
|
87488
88112
|
"recording-stalled",
|
|
87489
88113
|
"recording-healthy",
|
|
87490
88114
|
"recording-degraded",
|
|
@@ -87753,6 +88377,15 @@ new Map([
|
|
|
87753
88377
|
snapshotLabel: "Cameras with constant motion now",
|
|
87754
88378
|
snapshotSection: null
|
|
87755
88379
|
},
|
|
88380
|
+
{
|
|
88381
|
+
id: "package-llm-confirm",
|
|
88382
|
+
family: "device-events",
|
|
88383
|
+
label: "Package confirm (not answering / answering again)",
|
|
88384
|
+
bad: ["package-llm-silent"],
|
|
88385
|
+
recovery: ["package-llm-answered"],
|
|
88386
|
+
snapshotLabel: "Cameras whose package confirm is not answering",
|
|
88387
|
+
snapshotSection: null
|
|
88388
|
+
},
|
|
87756
88389
|
{
|
|
87757
88390
|
id: "recording-health",
|
|
87758
88391
|
family: "device-events",
|
|
@@ -196883,6 +197516,7 @@ var StubMatterController = class {
|
|
|
196883
197516
|
async removeNode(_nodeId) {
|
|
196884
197517
|
throw new MatterControllerNotWiredError("removeNode");
|
|
196885
197518
|
}
|
|
197519
|
+
async syncSubscriptions() {}
|
|
196886
197520
|
};
|
|
196887
197521
|
/**
|
|
196888
197522
|
* Factory seam (stub default): build a {@link StubMatterController}.
|
|
@@ -197055,7 +197689,13 @@ var MatterAttributeId = {
|
|
|
197055
197689
|
/** ElectricalPowerMeasurement.ActivePower (int64, mW). */
|
|
197056
197690
|
ActivePower: 8,
|
|
197057
197691
|
/** ElectricalEnergyMeasurement.CumulativeEnergyImported (energy struct, mWh). */
|
|
197058
|
-
CumulativeEnergyImported: 1
|
|
197692
|
+
CumulativeEnergyImported: 1,
|
|
197693
|
+
/** DoorLock.LockState (enum: 0 NotFullyLocked, 1 Locked, 2 Unlocked, 3 Unlatched). */
|
|
197694
|
+
LockState: 0,
|
|
197695
|
+
/** DoorLock.DoorState (enum; 2 = DoorJammed). Present only with the door sensor. */
|
|
197696
|
+
DoorState: 3,
|
|
197697
|
+
/** DoorLock.RequirePinForRemoteOperation (bool). */
|
|
197698
|
+
RequirePinForRemoteOperation: 51
|
|
197059
197699
|
};
|
|
197060
197700
|
/** Thermostat.SystemMode enum → CamStack HvacMode (climate-control). */
|
|
197061
197701
|
var SYSTEM_MODE_TO_HVAC = new Map([
|
|
@@ -197087,8 +197727,11 @@ function asBoolean(value) {
|
|
|
197087
197727
|
* matter.js may surface a `MeasuredValue` either as a plain scaled int or wrapped
|
|
197088
197728
|
* in an energy-measurement struct (`{ energy }`). Pull the numeric payload out.
|
|
197089
197729
|
*/
|
|
197730
|
+
function isRecord$4(value) {
|
|
197731
|
+
return typeof value === "object" && value !== null;
|
|
197732
|
+
}
|
|
197090
197733
|
function asEnergyMilliWattHours(value) {
|
|
197091
|
-
if (
|
|
197734
|
+
if (isRecord$4(value) && "energy" in value) return asNumber(value["energy"]);
|
|
197092
197735
|
return asNumber(value);
|
|
197093
197736
|
}
|
|
197094
197737
|
/**
|
|
@@ -197110,9 +197753,77 @@ function decodeMatterAttribute(input) {
|
|
|
197110
197753
|
case MatterClusterId.Thermostat: return decodeThermostat(attributeId, value);
|
|
197111
197754
|
case MatterClusterId.ElectricalPowerMeasurement: return decodeElectricalPower(attributeId, value);
|
|
197112
197755
|
case MatterClusterId.ElectricalEnergyMeasurement: return decodeElectricalEnergy(attributeId, value);
|
|
197756
|
+
case MatterClusterId.DoorLock: return decodeDoorLock(attributeId, value);
|
|
197113
197757
|
default: return null;
|
|
197114
197758
|
}
|
|
197115
197759
|
}
|
|
197760
|
+
/** LockState enum → cap state. NotFullyLocked has no cap value of its own. */
|
|
197761
|
+
var LOCK_STATE_TO_CAP = new Map([
|
|
197762
|
+
[0, {
|
|
197763
|
+
state: "jammed",
|
|
197764
|
+
detail: "not-fully-locked"
|
|
197765
|
+
}],
|
|
197766
|
+
[1, { state: "locked" }],
|
|
197767
|
+
[2, { state: "unlocked" }],
|
|
197768
|
+
[3, {
|
|
197769
|
+
state: "unlocked",
|
|
197770
|
+
detail: "unlatched"
|
|
197771
|
+
}]
|
|
197772
|
+
]);
|
|
197773
|
+
/** DoorState enum → a detail. Only DoorJammed also moves the lock cap. */
|
|
197774
|
+
var DOOR_STATE_DETAIL = new Map([
|
|
197775
|
+
[0, "door-open"],
|
|
197776
|
+
[1, "door-closed"],
|
|
197777
|
+
[2, "door-jammed"],
|
|
197778
|
+
[3, "door-forced-open"],
|
|
197779
|
+
[4, "door-unspecified"],
|
|
197780
|
+
[5, "door-ajar"]
|
|
197781
|
+
]);
|
|
197782
|
+
function decodeDoorLock(attributeId, value) {
|
|
197783
|
+
if (attributeId === MatterAttributeId.LockState) {
|
|
197784
|
+
const raw = asNumber(value);
|
|
197785
|
+
if (raw === null) return null;
|
|
197786
|
+
const mapped = LOCK_STATE_TO_CAP.get(raw);
|
|
197787
|
+
if (mapped === void 0) return null;
|
|
197788
|
+
return {
|
|
197789
|
+
cap: "lock-control",
|
|
197790
|
+
patch: {
|
|
197791
|
+
state: mapped.state,
|
|
197792
|
+
lastChangedAt: Date.now()
|
|
197793
|
+
},
|
|
197794
|
+
...mapped.detail !== void 0 ? { detail: mapped.detail } : {}
|
|
197795
|
+
};
|
|
197796
|
+
}
|
|
197797
|
+
if (attributeId === MatterAttributeId.DoorState) {
|
|
197798
|
+
const raw = asNumber(value);
|
|
197799
|
+
if (raw === null) return null;
|
|
197800
|
+
const detail = DOOR_STATE_DETAIL.get(raw);
|
|
197801
|
+
if (detail === void 0) return null;
|
|
197802
|
+
if (detail === "door-jammed") return {
|
|
197803
|
+
cap: "lock-control",
|
|
197804
|
+
patch: {
|
|
197805
|
+
state: "jammed",
|
|
197806
|
+
lastChangedAt: Date.now()
|
|
197807
|
+
},
|
|
197808
|
+
detail
|
|
197809
|
+
};
|
|
197810
|
+
return {
|
|
197811
|
+
cap: "lock-control",
|
|
197812
|
+
patch: {},
|
|
197813
|
+
detail
|
|
197814
|
+
};
|
|
197815
|
+
}
|
|
197816
|
+
if (attributeId === MatterAttributeId.RequirePinForRemoteOperation) {
|
|
197817
|
+
const required = asBoolean(value);
|
|
197818
|
+
if (required === null) return null;
|
|
197819
|
+
return {
|
|
197820
|
+
cap: "lock-control",
|
|
197821
|
+
patch: {},
|
|
197822
|
+
detail: required ? "pin-required" : "pin-not-required"
|
|
197823
|
+
};
|
|
197824
|
+
}
|
|
197825
|
+
return null;
|
|
197826
|
+
}
|
|
197116
197827
|
function decodeOnOff(attributeId, value) {
|
|
197117
197828
|
if (attributeId !== MatterAttributeId.OnOff) return null;
|
|
197118
197829
|
const on = asBoolean(value);
|
|
@@ -197336,7 +198047,10 @@ var ATTR_NAME_TO_ID = new Map([
|
|
|
197336
198047
|
["voltage", MatterAttributeId.Voltage],
|
|
197337
198048
|
["activeCurrent", MatterAttributeId.ActiveCurrent],
|
|
197338
198049
|
["activePower", MatterAttributeId.ActivePower],
|
|
197339
|
-
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported]
|
|
198050
|
+
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported],
|
|
198051
|
+
["lockState", MatterAttributeId.LockState],
|
|
198052
|
+
["doorState", MatterAttributeId.DoorState],
|
|
198053
|
+
["requirePinForRemoteOperation", MatterAttributeId.RequirePinForRemoteOperation]
|
|
197340
198054
|
]);
|
|
197341
198055
|
/** clusterId → matter.js behavior id (camelCase) for outbound command clusters. */
|
|
197342
198056
|
var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
@@ -197348,14 +198062,14 @@ var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
|
197348
198062
|
[258, "windowCovering"]
|
|
197349
198063
|
]);
|
|
197350
198064
|
/** True when `value` is a non-null object (cast-free record narrowing). */
|
|
197351
|
-
function isRecord$
|
|
198065
|
+
function isRecord$3(value) {
|
|
197352
198066
|
return typeof value === "object" && value !== null;
|
|
197353
198067
|
}
|
|
197354
198068
|
/** Read a finite numeric cluster id from a `Behavior.Type`'s `.cluster.id`. */
|
|
197355
198069
|
function clusterIdOf(behaviorType) {
|
|
197356
|
-
if (!isRecord$
|
|
198070
|
+
if (!isRecord$3(behaviorType)) return null;
|
|
197357
198071
|
const cluster = behaviorType["cluster"];
|
|
197358
|
-
if (!isRecord$
|
|
198072
|
+
if (!isRecord$3(cluster)) return null;
|
|
197359
198073
|
const id = cluster["id"];
|
|
197360
198074
|
if (typeof id === "number" && Number.isFinite(id)) return id;
|
|
197361
198075
|
if (typeof id === "bigint") return Number(id);
|
|
@@ -197366,13 +198080,13 @@ function clusterIdOf(behaviorType) {
|
|
|
197366
198080
|
* the matter.js `Endpoint` interop at one typed boundary.
|
|
197367
198081
|
*/
|
|
197368
198082
|
function asDynamicEndpoint(value) {
|
|
197369
|
-
if (!isRecord$
|
|
198083
|
+
if (!isRecord$3(value)) return null;
|
|
197370
198084
|
const number = value["number"];
|
|
197371
198085
|
const behaviors = value["behaviors"];
|
|
197372
198086
|
const parts = value["parts"];
|
|
197373
198087
|
const stateOf = value["stateOf"];
|
|
197374
198088
|
const commandsOf = value["commandsOf"];
|
|
197375
|
-
if (typeof number === "number" && isRecord$
|
|
198089
|
+
if (typeof number === "number" && isRecord$3(behaviors) && "supported" in behaviors && typeof stateOf === "function" && typeof commandsOf === "function" && isIterable$2(parts)) return {
|
|
197376
198090
|
number,
|
|
197377
198091
|
behaviors: { supported: behaviors["supported"] },
|
|
197378
198092
|
parts: mapIterable(parts, asDynamicEndpointOrThrow),
|
|
@@ -197396,7 +198110,7 @@ async function invokeNodeCommand(input) {
|
|
|
197396
198110
|
const behaviorId = CLUSTER_ID_TO_BEHAVIOR_ID.get(clusterId);
|
|
197397
198111
|
if (behaviorId === void 0) throw new Error(`matter-broker: cluster 0x${clusterId.toString(16)} has no command binding`);
|
|
197398
198112
|
const commands = target.commandsOf(behaviorId);
|
|
197399
|
-
if (!isRecord$
|
|
198113
|
+
if (!isRecord$3(commands)) throw new Error(`matter-broker: endpoint ${endpointId} has no '${behaviorId}' commands`);
|
|
197400
198114
|
const command = commands[commandName];
|
|
197401
198115
|
const fn = asCommandFn(command);
|
|
197402
198116
|
if (fn === null) throw new Error(`matter-broker: command '${commandName}' unavailable on '${behaviorId}'`);
|
|
@@ -197421,8 +198135,8 @@ function asDynamicEndpointOrThrow(value) {
|
|
|
197421
198135
|
if (ep === null) throw new TypeError("matter-broker: unexpected non-endpoint child part");
|
|
197422
198136
|
return ep;
|
|
197423
198137
|
}
|
|
197424
|
-
function isIterable$
|
|
197425
|
-
return isRecord$
|
|
198138
|
+
function isIterable$2(value) {
|
|
198139
|
+
return isRecord$3(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
|
|
197426
198140
|
}
|
|
197427
198141
|
function* mapIterable(it, fn) {
|
|
197428
198142
|
for (const v of it) yield fn(v);
|
|
@@ -197443,7 +198157,7 @@ function collectEndpoint(endpoint, out) {
|
|
|
197443
198157
|
const supported = endpoint.behaviors.supported;
|
|
197444
198158
|
const clusterIds = [];
|
|
197445
198159
|
const attributes = [];
|
|
197446
|
-
if (isRecord$
|
|
198160
|
+
if (isRecord$3(supported)) for (const [behaviorId, behaviorType] of Object.entries(supported)) {
|
|
197447
198161
|
const clusterId = clusterIdOf(behaviorType);
|
|
197448
198162
|
if (clusterId === null) continue;
|
|
197449
198163
|
clusterIds.push(clusterId);
|
|
@@ -197463,7 +198177,7 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
197463
198177
|
} catch {
|
|
197464
198178
|
return;
|
|
197465
198179
|
}
|
|
197466
|
-
if (!isRecord$
|
|
198180
|
+
if (!isRecord$3(struct)) return;
|
|
197467
198181
|
for (const [attrName, value] of Object.entries(struct)) {
|
|
197468
198182
|
const attributeId = ATTR_NAME_TO_ID.get(attrName);
|
|
197469
198183
|
if (attributeId === void 0) continue;
|
|
@@ -197475,35 +198189,413 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
197475
198189
|
}
|
|
197476
198190
|
}
|
|
197477
198191
|
//#endregion
|
|
198192
|
+
//#region src/matter-commission-progress.ts
|
|
198193
|
+
/**
|
|
198194
|
+
* Commissioning progress the admin UI polls, plus the named failures a share
|
|
198195
|
+
* code can produce. Pure — no matter.js import.
|
|
198196
|
+
*
|
|
198197
|
+
* Phases follow the on-network share path: discover the device, establish
|
|
198198
|
+
* PASE, then commission onto this fabric. A missing or malformed code never
|
|
198199
|
+
* becomes a later phase.
|
|
198200
|
+
*/
|
|
198201
|
+
var CommissionPhase = {
|
|
198202
|
+
Idle: "idle",
|
|
198203
|
+
Discovering: "discovering",
|
|
198204
|
+
Pase: "pase",
|
|
198205
|
+
Commissioning: "commissioning",
|
|
198206
|
+
Done: "done",
|
|
198207
|
+
Failed: "failed"
|
|
198208
|
+
};
|
|
198209
|
+
var CommissionFailureCode = {
|
|
198210
|
+
CodeExpired: "code-expired",
|
|
198211
|
+
DeviceNotFound: "device-not-found",
|
|
198212
|
+
WrongCode: "wrong-code",
|
|
198213
|
+
InvalidCode: "invalid-code",
|
|
198214
|
+
Unknown: "unknown"
|
|
198215
|
+
};
|
|
198216
|
+
var IDLE_COMMISSION_PROGRESS = {
|
|
198217
|
+
phase: CommissionPhase.Idle,
|
|
198218
|
+
failure: null,
|
|
198219
|
+
message: null
|
|
198220
|
+
};
|
|
198221
|
+
/** Stable prefix the UI matches. The human text follows the colon. */
|
|
198222
|
+
function commissionFailureMessage(code, detail) {
|
|
198223
|
+
return `matter-commission:${code}: ${detail}`;
|
|
198224
|
+
}
|
|
198225
|
+
/**
|
|
198226
|
+
* Map a controller or matter.js error onto a named failure. Order matters:
|
|
198227
|
+
* an expired commissioning window is not "device not found", and a rejected
|
|
198228
|
+
* passcode is not a network miss.
|
|
198229
|
+
*/
|
|
198230
|
+
function classifyCommissionError(message) {
|
|
198231
|
+
const text = message.toLowerCase();
|
|
198232
|
+
if (text.includes("invalid pairing code") || text.includes("11 or 21")) return CommissionFailureCode.InvalidCode;
|
|
198233
|
+
if (text.includes("expired") || text.includes("commissioning window") || text.includes("window closed")) return CommissionFailureCode.CodeExpired;
|
|
198234
|
+
if (text.includes("passcode") || text.includes("wrong code") || text.includes("pase") || text.includes("authentication failed") || text.includes("status 0x7e") || text.includes("invalid code")) return CommissionFailureCode.WrongCode;
|
|
198235
|
+
if (text.includes("not found") || text.includes("no device") || text.includes("no commissionable") || text.includes("discovery") || text.includes("timed out") || text.includes("timeout") || text.includes("unreachable")) return CommissionFailureCode.DeviceNotFound;
|
|
198236
|
+
return CommissionFailureCode.Unknown;
|
|
198237
|
+
}
|
|
198238
|
+
//#endregion
|
|
198239
|
+
//#region src/matter-subscription.ts
|
|
198240
|
+
/**
|
|
198241
|
+
* Per-node Matter attribute subscriptions. The matter.js call itself lives in
|
|
198242
|
+
* {@link import('./matter-peer-subscribe.js')}; this module owns the lifecycle:
|
|
198243
|
+
* open, resubscribe after the session drops or the node returns, and a bounded
|
|
198244
|
+
* backoff. A report whose value is missing is not a value (D393) and is dropped.
|
|
198245
|
+
*/
|
|
198246
|
+
/** First wait after a dropped subscription. */
|
|
198247
|
+
var SUBSCRIPTION_BACKOFF_INITIAL_MS = 1e3;
|
|
198248
|
+
/** Cap so a sleepy Thread node cannot be hammered. */
|
|
198249
|
+
var SUBSCRIPTION_BACKOFF_MAX_MS = 6e4;
|
|
198250
|
+
/**
|
|
198251
|
+
* One supervisor per fabric. `sync` is safe to call on start, after commission,
|
|
198252
|
+
* and on the reconcile timer (the backstop when a lifecycle event was missed).
|
|
198253
|
+
*/
|
|
198254
|
+
var MatterSubscriptionSupervisor = class {
|
|
198255
|
+
#brokerId;
|
|
198256
|
+
#logger;
|
|
198257
|
+
#resolveDeviceId;
|
|
198258
|
+
#onReport;
|
|
198259
|
+
#slots = /* @__PURE__ */ new Map();
|
|
198260
|
+
#stopped = false;
|
|
198261
|
+
constructor(options) {
|
|
198262
|
+
this.#brokerId = options.brokerId;
|
|
198263
|
+
this.#logger = options.logger;
|
|
198264
|
+
this.#resolveDeviceId = options.resolveDeviceId;
|
|
198265
|
+
this.#onReport = options.onReport;
|
|
198266
|
+
}
|
|
198267
|
+
/** How many times `subscribe` has been entered for a node (tests). */
|
|
198268
|
+
subscribeAttempts(nodeId) {
|
|
198269
|
+
return this.#slots.get(nodeId)?.generation ?? 0;
|
|
198270
|
+
}
|
|
198271
|
+
phase(nodeId) {
|
|
198272
|
+
return this.#slots.get(nodeId)?.phase ?? null;
|
|
198273
|
+
}
|
|
198274
|
+
async sync(peers) {
|
|
198275
|
+
if (this.#stopped) return;
|
|
198276
|
+
const live = /* @__PURE__ */ new Set();
|
|
198277
|
+
for (const peer of peers) {
|
|
198278
|
+
live.add(peer.nodeId);
|
|
198279
|
+
const slot = this.#slots.get(peer.nodeId);
|
|
198280
|
+
if (slot === void 0) {
|
|
198281
|
+
await this.#open(peer);
|
|
198282
|
+
continue;
|
|
198283
|
+
}
|
|
198284
|
+
slot.peer = peer;
|
|
198285
|
+
if (slot.phase !== "subscribed" && peer.online() && slot.phase !== "resubscribing") {
|
|
198286
|
+
slot.attempt = 0;
|
|
198287
|
+
await this.#open(peer);
|
|
198288
|
+
}
|
|
198289
|
+
}
|
|
198290
|
+
for (const [nodeId, slot] of this.#slots) {
|
|
198291
|
+
if (live.has(nodeId)) continue;
|
|
198292
|
+
await this.#drop(slot);
|
|
198293
|
+
this.#slots.delete(nodeId);
|
|
198294
|
+
}
|
|
198295
|
+
}
|
|
198296
|
+
async stop() {
|
|
198297
|
+
this.#stopped = true;
|
|
198298
|
+
for (const slot of this.#slots.values()) await this.#drop(slot);
|
|
198299
|
+
this.#slots.clear();
|
|
198300
|
+
}
|
|
198301
|
+
async #open(peer) {
|
|
198302
|
+
if (this.#stopped) return;
|
|
198303
|
+
const existing = this.#slots.get(peer.nodeId);
|
|
198304
|
+
if (existing?.phase === "subscribed" && existing.subscription !== null) return;
|
|
198305
|
+
if (existing?.timer !== null && existing?.timer !== void 0) {
|
|
198306
|
+
clearTimeout(existing.timer);
|
|
198307
|
+
existing.timer = null;
|
|
198308
|
+
}
|
|
198309
|
+
const generation = (existing?.generation ?? 0) + 1;
|
|
198310
|
+
const slot = existing ?? this.#fresh(peer);
|
|
198311
|
+
slot.peer = peer;
|
|
198312
|
+
slot.generation = generation;
|
|
198313
|
+
slot.phase = "resubscribing";
|
|
198314
|
+
this.#slots.set(peer.nodeId, slot);
|
|
198315
|
+
this.#listen(slot);
|
|
198316
|
+
this.#log(peer.nodeId, "resubscribing");
|
|
198317
|
+
let subscription;
|
|
198318
|
+
try {
|
|
198319
|
+
subscription = await peer.subscribe({
|
|
198320
|
+
onReport: (report) => {
|
|
198321
|
+
if (report.value === void 0 || report.value === null) return;
|
|
198322
|
+
this.#onReport(report);
|
|
198323
|
+
},
|
|
198324
|
+
onClosed: () => {
|
|
198325
|
+
const current = this.#slots.get(peer.nodeId);
|
|
198326
|
+
if (current === void 0 || current.generation !== generation) return;
|
|
198327
|
+
this.#onClosed(peer.nodeId);
|
|
198328
|
+
}
|
|
198329
|
+
});
|
|
198330
|
+
} catch (err) {
|
|
198331
|
+
this.#log(peer.nodeId, "subscribe-failed", { error: errMsg(err) });
|
|
198332
|
+
const current = this.#slots.get(peer.nodeId);
|
|
198333
|
+
if (current === void 0 || current.generation !== generation) return;
|
|
198334
|
+
current.phase = "subscribe-failed";
|
|
198335
|
+
this.#schedule(peer.nodeId);
|
|
198336
|
+
return;
|
|
198337
|
+
}
|
|
198338
|
+
if (this.#stopped || this.#slots.get(peer.nodeId)?.generation !== generation) {
|
|
198339
|
+
await subscription.close().catch(() => void 0);
|
|
198340
|
+
return;
|
|
198341
|
+
}
|
|
198342
|
+
slot.subscription = subscription;
|
|
198343
|
+
slot.phase = "subscribed";
|
|
198344
|
+
slot.attempt = 0;
|
|
198345
|
+
this.#log(peer.nodeId, "subscribed");
|
|
198346
|
+
}
|
|
198347
|
+
#onClosed(nodeId) {
|
|
198348
|
+
const slot = this.#slots.get(nodeId);
|
|
198349
|
+
if (slot === void 0 || slot.dropping) return;
|
|
198350
|
+
if (slot.phase !== "subscribed" && slot.phase !== "resubscribing") return;
|
|
198351
|
+
slot.subscription = null;
|
|
198352
|
+
slot.phase = "closed";
|
|
198353
|
+
this.#log(nodeId, "closed");
|
|
198354
|
+
if (!slot.peer.online()) {
|
|
198355
|
+
slot.phase = "offline";
|
|
198356
|
+
this.#log(nodeId, "offline");
|
|
198357
|
+
return;
|
|
198358
|
+
}
|
|
198359
|
+
this.#schedule(nodeId);
|
|
198360
|
+
}
|
|
198361
|
+
#schedule(nodeId) {
|
|
198362
|
+
const slot = this.#slots.get(nodeId);
|
|
198363
|
+
if (slot === void 0 || this.#stopped) return;
|
|
198364
|
+
if (slot.timer !== null) clearTimeout(slot.timer);
|
|
198365
|
+
const delay = Math.min(SUBSCRIPTION_BACKOFF_MAX_MS, SUBSCRIPTION_BACKOFF_INITIAL_MS * 2 ** slot.attempt);
|
|
198366
|
+
slot.attempt += 1;
|
|
198367
|
+
slot.phase = "backing-off";
|
|
198368
|
+
this.#log(nodeId, "backing-off", {
|
|
198369
|
+
delayMs: delay,
|
|
198370
|
+
attempt: slot.attempt
|
|
198371
|
+
});
|
|
198372
|
+
slot.timer = setTimeout(() => {
|
|
198373
|
+
slot.timer = null;
|
|
198374
|
+
this.#open(slot.peer);
|
|
198375
|
+
}, delay);
|
|
198376
|
+
if (typeof slot.timer.unref === "function") slot.timer.unref();
|
|
198377
|
+
}
|
|
198378
|
+
#listen(slot) {
|
|
198379
|
+
if (slot.listening) return;
|
|
198380
|
+
slot.listening = true;
|
|
198381
|
+
slot.unlisten.push(slot.peer.onOffline(() => {
|
|
198382
|
+
const current = this.#slots.get(slot.peer.nodeId);
|
|
198383
|
+
if (current === void 0) return;
|
|
198384
|
+
if (current.phase === "offline") return;
|
|
198385
|
+
current.phase = "offline";
|
|
198386
|
+
this.#log(slot.peer.nodeId, "offline");
|
|
198387
|
+
}));
|
|
198388
|
+
slot.unlisten.push(slot.peer.onOnline(() => {
|
|
198389
|
+
const current = this.#slots.get(slot.peer.nodeId);
|
|
198390
|
+
if (current === void 0 || this.#stopped) return;
|
|
198391
|
+
current.attempt = 0;
|
|
198392
|
+
if (current.timer !== null) {
|
|
198393
|
+
clearTimeout(current.timer);
|
|
198394
|
+
current.timer = null;
|
|
198395
|
+
}
|
|
198396
|
+
this.#log(slot.peer.nodeId, "online");
|
|
198397
|
+
this.#open(current.peer);
|
|
198398
|
+
}));
|
|
198399
|
+
}
|
|
198400
|
+
async #drop(slot) {
|
|
198401
|
+
slot.dropping = true;
|
|
198402
|
+
if (slot.timer !== null) clearTimeout(slot.timer);
|
|
198403
|
+
slot.timer = null;
|
|
198404
|
+
for (const unlisten of slot.unlisten) unlisten();
|
|
198405
|
+
slot.unlisten = [];
|
|
198406
|
+
const subscription = slot.subscription;
|
|
198407
|
+
slot.subscription = null;
|
|
198408
|
+
if (subscription !== null) try {
|
|
198409
|
+
await subscription.close();
|
|
198410
|
+
} catch (err) {
|
|
198411
|
+
this.#logger.warn("Matter subscription close failed", {
|
|
198412
|
+
tags: this.#tags(slot.peer.nodeId),
|
|
198413
|
+
meta: {
|
|
198414
|
+
nodeId: slot.peer.nodeId,
|
|
198415
|
+
error: errMsg(err)
|
|
198416
|
+
}
|
|
198417
|
+
});
|
|
198418
|
+
}
|
|
198419
|
+
}
|
|
198420
|
+
#fresh(peer) {
|
|
198421
|
+
return {
|
|
198422
|
+
peer,
|
|
198423
|
+
phase: "resubscribing",
|
|
198424
|
+
attempt: 0,
|
|
198425
|
+
timer: null,
|
|
198426
|
+
subscription: null,
|
|
198427
|
+
unlisten: [],
|
|
198428
|
+
generation: 0,
|
|
198429
|
+
listening: false,
|
|
198430
|
+
dropping: false
|
|
198431
|
+
};
|
|
198432
|
+
}
|
|
198433
|
+
#tags(nodeId) {
|
|
198434
|
+
const deviceId = this.#resolveDeviceId(nodeId);
|
|
198435
|
+
if (deviceId !== null) return { deviceId };
|
|
198436
|
+
return { brokerId: this.#brokerId };
|
|
198437
|
+
}
|
|
198438
|
+
#log(nodeId, phase, extra) {
|
|
198439
|
+
const deviceId = this.#resolveDeviceId(nodeId);
|
|
198440
|
+
this.#logger.info("Matter subscription state", {
|
|
198441
|
+
tags: deviceId !== null ? { deviceId } : { brokerId: this.#brokerId },
|
|
198442
|
+
meta: {
|
|
198443
|
+
nodeId,
|
|
198444
|
+
phase,
|
|
198445
|
+
...extra
|
|
198446
|
+
}
|
|
198447
|
+
});
|
|
198448
|
+
}
|
|
198449
|
+
};
|
|
198450
|
+
function isRecord$2(value) {
|
|
198451
|
+
return typeof value === "object" && value !== null;
|
|
198452
|
+
}
|
|
198453
|
+
function asFiniteNumber(value) {
|
|
198454
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
198455
|
+
if (typeof value === "bigint") {
|
|
198456
|
+
const n = Number(value);
|
|
198457
|
+
return Number.isFinite(n) ? n : null;
|
|
198458
|
+
}
|
|
198459
|
+
return null;
|
|
198460
|
+
}
|
|
198461
|
+
function isIterable$1(value) {
|
|
198462
|
+
return isRecord$2(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
|
|
198463
|
+
}
|
|
198464
|
+
function isAsyncIterable(value) {
|
|
198465
|
+
return isRecord$2(value) && typeof Reflect.get(value, Symbol.asyncIterator) === "function";
|
|
198466
|
+
}
|
|
198467
|
+
/**
|
|
198468
|
+
* Pull attribute values out of a matter.js `ReadResult` (or a recorded fixture
|
|
198469
|
+
* of the same shape). Status reports and missing values are skipped — they are
|
|
198470
|
+
* not a measurement.
|
|
198471
|
+
*/
|
|
198472
|
+
async function collectAttributeReports(nodeId, update) {
|
|
198473
|
+
const out = [];
|
|
198474
|
+
const chunks = isAsyncIterable(update) ? update : isIterable$1(update) ? update : [];
|
|
198475
|
+
for await (const chunk of chunks) {
|
|
198476
|
+
const reports = isIterable$1(chunk) ? chunk : [];
|
|
198477
|
+
for (const report of reports) {
|
|
198478
|
+
const decoded = reportFromRecord(nodeId, report);
|
|
198479
|
+
if (decoded !== null) out.push(decoded);
|
|
198480
|
+
}
|
|
198481
|
+
}
|
|
198482
|
+
return out;
|
|
198483
|
+
}
|
|
198484
|
+
function reportFromRecord(nodeId, report) {
|
|
198485
|
+
if (!isRecord$2(report)) return null;
|
|
198486
|
+
if (report["kind"] !== "attr-value") return null;
|
|
198487
|
+
if (report["value"] === void 0 || report["value"] === null) return null;
|
|
198488
|
+
const path = report["path"];
|
|
198489
|
+
if (!isRecord$2(path)) return null;
|
|
198490
|
+
const endpointId = asFiniteNumber(path["endpointId"]);
|
|
198491
|
+
const clusterId = asFiniteNumber(path["clusterId"]);
|
|
198492
|
+
const attributeId = asFiniteNumber(path["attributeId"]);
|
|
198493
|
+
if (endpointId === null || clusterId === null || attributeId === null) return null;
|
|
198494
|
+
return {
|
|
198495
|
+
nodeId,
|
|
198496
|
+
endpointId,
|
|
198497
|
+
clusterId,
|
|
198498
|
+
attributeId,
|
|
198499
|
+
value: report["value"]
|
|
198500
|
+
};
|
|
198501
|
+
}
|
|
198502
|
+
function isRecord$1(value) {
|
|
198503
|
+
return typeof value === "object" && value !== null;
|
|
198504
|
+
}
|
|
198505
|
+
function peerOnline$1(peer) {
|
|
198506
|
+
const lifecycle = peer["lifecycle"];
|
|
198507
|
+
if (!isRecord$1(lifecycle)) return false;
|
|
198508
|
+
return lifecycle["isOnline"] === true;
|
|
198509
|
+
}
|
|
198510
|
+
function listenLifecycle(peer, event, handler) {
|
|
198511
|
+
const lifecycle = peer["lifecycle"];
|
|
198512
|
+
if (!isRecord$1(lifecycle)) return () => void 0;
|
|
198513
|
+
const observable = lifecycle[event];
|
|
198514
|
+
if (!isRecord$1(observable)) return () => void 0;
|
|
198515
|
+
const on = observable["on"];
|
|
198516
|
+
const off = observable["off"];
|
|
198517
|
+
if (typeof on !== "function" || typeof off !== "function") return () => void 0;
|
|
198518
|
+
Reflect.apply(on, observable, [handler]);
|
|
198519
|
+
return () => {
|
|
198520
|
+
Reflect.apply(off, observable, [handler]);
|
|
198521
|
+
};
|
|
198522
|
+
}
|
|
198523
|
+
/** Stop matter.js's own wildcard subscription so this addon owns the session. */
|
|
198524
|
+
function disableAutoSubscribe(peer) {
|
|
198525
|
+
const stateOf = peer["stateOf"];
|
|
198526
|
+
if (typeof stateOf !== "function") return;
|
|
198527
|
+
let network;
|
|
198528
|
+
try {
|
|
198529
|
+
network = Reflect.apply(stateOf, peer, ["network"]);
|
|
198530
|
+
} catch {
|
|
198531
|
+
return;
|
|
198532
|
+
}
|
|
198533
|
+
if (!isRecord$1(network)) return;
|
|
198534
|
+
if ("autoSubscribe" in network) network["autoSubscribe"] = false;
|
|
198535
|
+
}
|
|
198536
|
+
function subscriptionFrom(result) {
|
|
198537
|
+
return { close: async () => {
|
|
198538
|
+
if (!isRecord$1(result)) return;
|
|
198539
|
+
const fn = result["close"];
|
|
198540
|
+
if (typeof fn !== "function") return;
|
|
198541
|
+
await Promise.resolve(Reflect.apply(fn, result, []));
|
|
198542
|
+
} };
|
|
198543
|
+
}
|
|
198544
|
+
/**
|
|
198545
|
+
* Build a subscribable view of a commissioned peer, or null when the peer has
|
|
198546
|
+
* no `interaction.subscribe` (nothing to attach — the caller logs that).
|
|
198547
|
+
*/
|
|
198548
|
+
function subscribableFromPeer(peer, nodeId) {
|
|
198549
|
+
if (!isRecord$1(peer)) return null;
|
|
198550
|
+
const interaction = peer["interaction"];
|
|
198551
|
+
if (!isRecord$1(interaction) || typeof interaction["subscribe"] !== "function") return null;
|
|
198552
|
+
const subscribe = interaction["subscribe"];
|
|
198553
|
+
return {
|
|
198554
|
+
nodeId,
|
|
198555
|
+
online: () => peerOnline$1(peer),
|
|
198556
|
+
onOnline: (handler) => listenLifecycle(peer, "online", handler),
|
|
198557
|
+
onOffline: (handler) => listenLifecycle(peer, "offline", handler),
|
|
198558
|
+
subscribe: async (handlers) => {
|
|
198559
|
+
disableAutoSubscribe(peer);
|
|
198560
|
+
const request = {
|
|
198561
|
+
sustain: true,
|
|
198562
|
+
keepSubscriptions: false,
|
|
198563
|
+
fabricFilter: true,
|
|
198564
|
+
attributes: [{}],
|
|
198565
|
+
minIntervalFloor: require_esm.Seconds(0),
|
|
198566
|
+
maxIntervalCeiling: require_esm.Seconds(60),
|
|
198567
|
+
bootstrapWithRead: true,
|
|
198568
|
+
updated: async (update) => {
|
|
198569
|
+
const reports = await collectAttributeReports(nodeId, update);
|
|
198570
|
+
for (const report of reports) handlers.onReport(report);
|
|
198571
|
+
},
|
|
198572
|
+
closed: () => {
|
|
198573
|
+
handlers.onClosed();
|
|
198574
|
+
}
|
|
198575
|
+
};
|
|
198576
|
+
return subscriptionFrom(await Promise.resolve(Reflect.apply(subscribe, interaction, [request])));
|
|
198577
|
+
}
|
|
198578
|
+
};
|
|
198579
|
+
}
|
|
198580
|
+
//#endregion
|
|
197478
198581
|
//#region src/matter-controller-impl.ts
|
|
197479
198582
|
/**
|
|
197480
|
-
* Real {@link MatterController} backed by matter.js
|
|
197481
|
-
* `@matter/nodejs`).
|
|
197482
|
-
*
|
|
197483
|
-
*
|
|
197484
|
-
*
|
|
197485
|
-
*
|
|
197486
|
-
*
|
|
197487
|
-
*
|
|
197488
|
-
*
|
|
197489
|
-
*
|
|
197490
|
-
* - `
|
|
197491
|
-
* - `
|
|
197492
|
-
*
|
|
197493
|
-
* - `
|
|
197494
|
-
*
|
|
197495
|
-
*
|
|
197496
|
-
* - `clientNode.decommission()` — remove a node from the fabric.
|
|
197497
|
-
* - endpoint `commandsOf(behaviorId)[name](args)` — outbound cluster commands.
|
|
197498
|
-
*
|
|
197499
|
-
* The dynamic endpoint/cluster/attribute introspection lives in
|
|
197500
|
-
* `matter-endpoint-introspect.ts` (the single matter.js dynamic-shape boundary).
|
|
197501
|
-
* This class polls each node's endpoint state on reconcile + emits decoded
|
|
197502
|
-
* attribute reports, and re-emits on node online/offline transitions.
|
|
197503
|
-
*
|
|
197504
|
-
* BLE commissioning (`@matter/nodejs-ble`) is NOT wired — that package is not
|
|
197505
|
-
* installed. IP / on-network commissioning is the supported path; see the class
|
|
197506
|
-
* `commissionNode` note.
|
|
198583
|
+
* Real {@link MatterController} backed by matter.js (`@matter/main` +
|
|
198584
|
+
* `@matter/nodejs`). ServerNode / ClientNode is the API this controller uses.
|
|
198585
|
+
*
|
|
198586
|
+
* One `ServerNode` per broker (= one fabric). Storage is matter.js's own
|
|
198587
|
+
* files under the addon `dataDir` (`Environment` `storage.path`) — the
|
|
198588
|
+
* library-owned exception for fabric root / NOC / node registry, same class
|
|
198589
|
+
* as HAP pairing files. Not the keychain.
|
|
198590
|
+
*
|
|
198591
|
+
* Bound API:
|
|
198592
|
+
* - `ServerNode.create({ environment, id })` — fabric owner.
|
|
198593
|
+
* - `serverNode.peers.commission({ pairingCode, continueCommissioningAfterPase })`.
|
|
198594
|
+
* - `clientNode.interaction.subscribe({ sustain: true })` — attribute reports.
|
|
198595
|
+
* - `clientNode.lifecycle` online / offline — resubscribe.
|
|
198596
|
+
* - endpoint `commandsOf(behaviorId)[name](args)` — outbound commands.
|
|
198597
|
+
*
|
|
198598
|
+
* BLE commissioning is not wired. IP / on-network share codes only.
|
|
197507
198599
|
*/
|
|
197508
198600
|
/** A commissioning failure (bad code / device unreachable), distinct from runtime. */
|
|
197509
198601
|
var MatterCommissioningError = class extends Error {
|
|
@@ -197523,12 +198615,19 @@ var MatterJsController = class {
|
|
|
197523
198615
|
#options;
|
|
197524
198616
|
#logger;
|
|
197525
198617
|
#handlers = /* @__PURE__ */ new Set();
|
|
198618
|
+
#subscriptions;
|
|
197526
198619
|
#server = null;
|
|
197527
198620
|
/** Last emitted value per `node:endpoint:cluster:attribute` (change detection). */
|
|
197528
198621
|
#lastValues = /* @__PURE__ */ new Map();
|
|
197529
198622
|
constructor(options, logger) {
|
|
197530
198623
|
this.#options = options;
|
|
197531
198624
|
this.#logger = logger;
|
|
198625
|
+
this.#subscriptions = new MatterSubscriptionSupervisor({
|
|
198626
|
+
brokerId: options.brokerId,
|
|
198627
|
+
logger,
|
|
198628
|
+
resolveDeviceId: options.resolveDeviceId ?? (() => null),
|
|
198629
|
+
onReport: (report) => this.#emitIfChanged(report)
|
|
198630
|
+
});
|
|
197532
198631
|
}
|
|
197533
198632
|
get options() {
|
|
197534
198633
|
return this.#options;
|
|
@@ -197536,19 +198635,17 @@ var MatterJsController = class {
|
|
|
197536
198635
|
/** Boot the ServerNode fabric, loading/persisting state from `dataDir`. */
|
|
197537
198636
|
async start() {
|
|
197538
198637
|
if (this.#server !== null) return;
|
|
197539
|
-
const
|
|
197540
|
-
const server = await ServerNode.create({
|
|
197541
|
-
environment,
|
|
197542
|
-
id: this.#options.brokerId
|
|
197543
|
-
});
|
|
198638
|
+
const server = this.#options.openServer !== void 0 ? await this.#options.openServer() : await this.#openProductionServer();
|
|
197544
198639
|
this.#server = server;
|
|
197545
198640
|
await server.start();
|
|
197546
198641
|
this.#logger.info("MatterJsController: ServerNode online", { tags: { brokerId: this.#options.brokerId } });
|
|
198642
|
+
await this.syncSubscriptions();
|
|
197547
198643
|
this.#emitAllReports();
|
|
197548
198644
|
}
|
|
197549
|
-
/** Close
|
|
198645
|
+
/** Close subscriptions FIRST, then the ServerNode. */
|
|
197550
198646
|
async stop() {
|
|
197551
198647
|
this.#handlers.clear();
|
|
198648
|
+
await this.#subscriptions.stop();
|
|
197552
198649
|
const server = this.#server;
|
|
197553
198650
|
this.#server = null;
|
|
197554
198651
|
this.#lastValues.clear();
|
|
@@ -197569,19 +198666,49 @@ var MatterJsController = class {
|
|
|
197569
198666
|
*/
|
|
197570
198667
|
async commissionNode(pairingCode) {
|
|
197571
198668
|
const server = this.#requireServer("commissionNode");
|
|
198669
|
+
this.#progress({
|
|
198670
|
+
phase: CommissionPhase.Discovering,
|
|
198671
|
+
failure: null,
|
|
198672
|
+
message: null
|
|
198673
|
+
});
|
|
197572
198674
|
try {
|
|
197573
|
-
const node = await server.
|
|
198675
|
+
const node = await server.commission({
|
|
198676
|
+
pairingCode,
|
|
198677
|
+
onPase: () => {
|
|
198678
|
+
this.#progress({
|
|
198679
|
+
phase: CommissionPhase.Pase,
|
|
198680
|
+
failure: null,
|
|
198681
|
+
message: null
|
|
198682
|
+
});
|
|
198683
|
+
this.#progress({
|
|
198684
|
+
phase: CommissionPhase.Commissioning,
|
|
198685
|
+
failure: null,
|
|
198686
|
+
message: null
|
|
198687
|
+
});
|
|
198688
|
+
}
|
|
198689
|
+
});
|
|
197574
198690
|
const view = this.#viewPeer(node);
|
|
197575
198691
|
this.#logger.info("MatterJsController: node commissioned", {
|
|
197576
198692
|
tags: { brokerId: this.#options.brokerId },
|
|
197577
198693
|
meta: { nodeId: view?.id ?? "(unknown)" }
|
|
197578
198694
|
});
|
|
198695
|
+
await this.syncSubscriptions();
|
|
197579
198696
|
this.#emitAllReports();
|
|
197580
198697
|
if (view === null) throw new MatterCommissioningError("commissioned node exposed no operational id");
|
|
198698
|
+
this.#progress({
|
|
198699
|
+
phase: CommissionPhase.Done,
|
|
198700
|
+
failure: null,
|
|
198701
|
+
message: null
|
|
198702
|
+
});
|
|
197581
198703
|
return { nodeId: view.id };
|
|
197582
198704
|
} catch (err) {
|
|
197583
|
-
if (err instanceof MatterCommissioningError
|
|
197584
|
-
|
|
198705
|
+
if (err instanceof MatterCommissioningError && err.message.includes("no operational id")) {
|
|
198706
|
+
this.#failCommission(err.message);
|
|
198707
|
+
throw err;
|
|
198708
|
+
}
|
|
198709
|
+
const message = errMsg(err);
|
|
198710
|
+
this.#failCommission(message);
|
|
198711
|
+
throw new MatterCommissioningError(commissionFailureMessage(classifyCommissionError(message), message), { cause: err });
|
|
197585
198712
|
}
|
|
197586
198713
|
}
|
|
197587
198714
|
/** Pull the live commissioned-node inventory off `serverNode.peers`. */
|
|
@@ -197589,12 +198716,31 @@ var MatterJsController = class {
|
|
|
197589
198716
|
const server = this.#server;
|
|
197590
198717
|
if (server === null) return [];
|
|
197591
198718
|
const out = [];
|
|
197592
|
-
for (const peer of
|
|
198719
|
+
for (const peer of server.listPeers()) {
|
|
197593
198720
|
const snapshot = this.#snapshot(peer);
|
|
197594
198721
|
if (snapshot !== null) out.push(snapshot);
|
|
197595
198722
|
}
|
|
197596
198723
|
return out;
|
|
197597
198724
|
}
|
|
198725
|
+
async syncSubscriptions() {
|
|
198726
|
+
const server = this.#server;
|
|
198727
|
+
if (server === null) return;
|
|
198728
|
+
const peers = [];
|
|
198729
|
+
for (const peer of server.listPeers()) {
|
|
198730
|
+
const view = this.#viewPeer(peer);
|
|
198731
|
+
if (view === null) continue;
|
|
198732
|
+
const subscribable = subscribableFromPeer(peer, view.id);
|
|
198733
|
+
if (subscribable === null) {
|
|
198734
|
+
this.#logger.warn("MatterJsController: peer has no attribute subscription", {
|
|
198735
|
+
tags: { brokerId: this.#options.brokerId },
|
|
198736
|
+
meta: { nodeId: view.id }
|
|
198737
|
+
});
|
|
198738
|
+
continue;
|
|
198739
|
+
}
|
|
198740
|
+
peers.push(subscribable);
|
|
198741
|
+
}
|
|
198742
|
+
await this.#subscriptions.sync(peers);
|
|
198743
|
+
}
|
|
197598
198744
|
async getNode(nodeId) {
|
|
197599
198745
|
return (await this.listNodes()).find((n) => n.nodeId === nodeId) ?? null;
|
|
197600
198746
|
}
|
|
@@ -197635,6 +198781,25 @@ var MatterJsController = class {
|
|
|
197635
198781
|
throw err;
|
|
197636
198782
|
}
|
|
197637
198783
|
}
|
|
198784
|
+
async #openProductionServer() {
|
|
198785
|
+
const environment = this.#buildEnvironment();
|
|
198786
|
+
const server = await ServerNode.create({
|
|
198787
|
+
environment,
|
|
198788
|
+
id: this.#options.brokerId
|
|
198789
|
+
});
|
|
198790
|
+
return {
|
|
198791
|
+
start: () => server.start(),
|
|
198792
|
+
close: () => server.close(),
|
|
198793
|
+
listPeers: () => this.#serverPeers(server),
|
|
198794
|
+
commission: ({ pairingCode, onPase }) => server.peers.commission({
|
|
198795
|
+
pairingCode,
|
|
198796
|
+
continueCommissioningAfterPase: () => {
|
|
198797
|
+
onPase();
|
|
198798
|
+
return true;
|
|
198799
|
+
}
|
|
198800
|
+
})
|
|
198801
|
+
};
|
|
198802
|
+
}
|
|
197638
198803
|
#buildEnvironment() {
|
|
197639
198804
|
const env = require_esm.Environment.default;
|
|
197640
198805
|
env.vars.set("storage.path", this.#options.dataDir);
|
|
@@ -197647,13 +198812,13 @@ var MatterJsController = class {
|
|
|
197647
198812
|
return this.#server;
|
|
197648
198813
|
}
|
|
197649
198814
|
/** Iterate the commissioned peers of a ServerNode (cast-free). */
|
|
197650
|
-
#
|
|
198815
|
+
#serverPeers(server) {
|
|
197651
198816
|
const peers = server.peers;
|
|
197652
198817
|
if (isIterable(peers)) return peers;
|
|
197653
198818
|
return [];
|
|
197654
198819
|
}
|
|
197655
198820
|
#findPeer(server, nodeId) {
|
|
197656
|
-
for (const peer of
|
|
198821
|
+
for (const peer of server.listPeers()) {
|
|
197657
198822
|
const view = this.#viewPeer(peer);
|
|
197658
198823
|
if (view !== null && view.id === nodeId) return view;
|
|
197659
198824
|
}
|
|
@@ -197661,12 +198826,23 @@ var MatterJsController = class {
|
|
|
197661
198826
|
}
|
|
197662
198827
|
/** Raw (un-viewed) peer object for operations that call its methods. */
|
|
197663
198828
|
#rawPeer(server, nodeId) {
|
|
197664
|
-
for (const peer of
|
|
198829
|
+
for (const peer of server.listPeers()) {
|
|
197665
198830
|
const view = this.#viewPeer(peer);
|
|
197666
198831
|
if (view !== null && view.id === nodeId) return peer;
|
|
197667
198832
|
}
|
|
197668
198833
|
return null;
|
|
197669
198834
|
}
|
|
198835
|
+
#progress(progress) {
|
|
198836
|
+
this.#options.onCommissionProgress?.(progress);
|
|
198837
|
+
}
|
|
198838
|
+
#failCommission(message) {
|
|
198839
|
+
const failure = classifyCommissionError(message);
|
|
198840
|
+
this.#progress({
|
|
198841
|
+
phase: CommissionPhase.Failed,
|
|
198842
|
+
failure,
|
|
198843
|
+
message: commissionFailureMessage(failure, message)
|
|
198844
|
+
});
|
|
198845
|
+
}
|
|
197670
198846
|
/** Reduce a peer to the read-only view the addon models. */
|
|
197671
198847
|
#viewPeer(peer) {
|
|
197672
198848
|
if (!isRecord(peer)) return null;
|
|
@@ -197710,7 +198886,7 @@ var MatterJsController = class {
|
|
|
197710
198886
|
#emitAllReports() {
|
|
197711
198887
|
const server = this.#server;
|
|
197712
198888
|
if (server === null) return;
|
|
197713
|
-
for (const peer of
|
|
198889
|
+
for (const peer of server.listPeers()) {
|
|
197714
198890
|
const view = this.#viewPeer(peer);
|
|
197715
198891
|
if (view === null) continue;
|
|
197716
198892
|
for (const ep of readNodeEndpoints(view.rootEndpoint)) for (const attr of ep.attributes) {
|
|
@@ -197726,6 +198902,7 @@ var MatterJsController = class {
|
|
|
197726
198902
|
}
|
|
197727
198903
|
}
|
|
197728
198904
|
#emitIfChanged(report) {
|
|
198905
|
+
if (report.value === void 0 || report.value === null) return;
|
|
197729
198906
|
const key = `${report.nodeId}:${report.endpointId}:${report.clusterId}:${report.attributeId}`;
|
|
197730
198907
|
const prev = this.#lastValues.get(key);
|
|
197731
198908
|
if (prev !== void 0 && shallowEqual(prev, report.value)) return;
|
|
@@ -197811,12 +198988,24 @@ var MatterNodeResolver = class {
|
|
|
197811
198988
|
#attrs = /* @__PURE__ */ new Map();
|
|
197812
198989
|
/** Live attribute-report subscribers (endpoint device classes). */
|
|
197813
198990
|
#listeners = /* @__PURE__ */ new Set();
|
|
198991
|
+
/** Adopted container id per Matter node, for subscription logs. */
|
|
198992
|
+
#deviceIds = /* @__PURE__ */ new Map();
|
|
197814
198993
|
/**
|
|
197815
198994
|
* Subscribe to live attribute reports across all brokers. Endpoint device
|
|
197816
198995
|
* classes subscribe on activate (filtering to their own node/endpoint) so a
|
|
197817
198996
|
* push report decodes straight into their runtime-state slice. Returns a
|
|
197818
198997
|
* disposer.
|
|
197819
198998
|
*/
|
|
198999
|
+
/** Remember the CamStack container id for a commissioned node. */
|
|
199000
|
+
bindDevice(nodeId, deviceId) {
|
|
199001
|
+
this.#deviceIds.set(nodeId, deviceId);
|
|
199002
|
+
}
|
|
199003
|
+
unbindDevice(nodeId) {
|
|
199004
|
+
this.#deviceIds.delete(nodeId);
|
|
199005
|
+
}
|
|
199006
|
+
deviceIdFor(nodeId) {
|
|
199007
|
+
return this.#deviceIds.get(nodeId) ?? null;
|
|
199008
|
+
}
|
|
197820
199009
|
onAttributeReport(listener) {
|
|
197821
199010
|
this.#listeners.add(listener);
|
|
197822
199011
|
return () => {
|
|
@@ -197878,6 +199067,7 @@ var MatterNodeResolver = class {
|
|
|
197878
199067
|
this.#nodes.clear();
|
|
197879
199068
|
this.#attrs.clear();
|
|
197880
199069
|
this.#listeners.clear();
|
|
199070
|
+
this.#deviceIds.clear();
|
|
197881
199071
|
}
|
|
197882
199072
|
};
|
|
197883
199073
|
/** The single in-process per-broker node resolver shared across the addon. */
|
|
@@ -197937,6 +199127,7 @@ var MatterIntegrationManager = class {
|
|
|
197937
199127
|
#nodeCount = 0;
|
|
197938
199128
|
#lastCheckedAt = null;
|
|
197939
199129
|
#error = null;
|
|
199130
|
+
#commission = IDLE_COMMISSION_PROGRESS;
|
|
197940
199131
|
constructor(options) {
|
|
197941
199132
|
this.#id = options.id;
|
|
197942
199133
|
this.#name = options.name;
|
|
@@ -197947,7 +199138,17 @@ var MatterIntegrationManager = class {
|
|
|
197947
199138
|
this.#onDisconnected = options.onDisconnected;
|
|
197948
199139
|
this.#onNodesChanged = options.onNodesChanged;
|
|
197949
199140
|
this.#onAttributeReport = options.onAttributeReport;
|
|
197950
|
-
this.#makeController = options.makeController ?? ((opts) => new MatterJsController(
|
|
199141
|
+
this.#makeController = options.makeController ?? ((opts) => new MatterJsController({
|
|
199142
|
+
...opts,
|
|
199143
|
+
onCommissionProgress: (progress) => {
|
|
199144
|
+
this.#commission = progress;
|
|
199145
|
+
},
|
|
199146
|
+
resolveDeviceId: (nodeId) => matterNodes.deviceIdFor(nodeId)
|
|
199147
|
+
}, this.#logger));
|
|
199148
|
+
}
|
|
199149
|
+
/** Latest commissioning phase for `broker.getState({ key: 'commission' })`. */
|
|
199150
|
+
getCommissionProgress() {
|
|
199151
|
+
return this.#commission;
|
|
197951
199152
|
}
|
|
197952
199153
|
/** The connection settings the manager is currently configured with. */
|
|
197953
199154
|
getConnection() {
|
|
@@ -198022,6 +199223,7 @@ var MatterIntegrationManager = class {
|
|
|
198022
199223
|
if (controller === null) return;
|
|
198023
199224
|
try {
|
|
198024
199225
|
const nodes = await controller.listNodes();
|
|
199226
|
+
await controller.syncSubscriptions();
|
|
198025
199227
|
this.#nodeCount = nodes.length;
|
|
198026
199228
|
this.#lastCheckedAt = Date.now();
|
|
198027
199229
|
matterNodes.setNodes(this.#id, nodes);
|
|
@@ -198202,6 +199404,10 @@ var MatterBrokerRegistry = class {
|
|
|
198202
199404
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
198203
199405
|
return mgr.commissionNode(pairingCode);
|
|
198204
199406
|
}
|
|
199407
|
+
getCommissionProgress(brokerId) {
|
|
199408
|
+
const mgr = this.#managers.get(brokerId);
|
|
199409
|
+
return mgr ? mgr.getCommissionProgress() : null;
|
|
199410
|
+
}
|
|
198205
199411
|
async removeNode(brokerId, nodeId) {
|
|
198206
199412
|
const mgr = this.#managers.get(brokerId);
|
|
198207
199413
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
@@ -198304,6 +199510,26 @@ function toRecord(value) {
|
|
|
198304
199510
|
function settingsToMatterConnection(settings) {
|
|
198305
199511
|
return matterConnectionSchema.parse(toRecord(settings));
|
|
198306
199512
|
}
|
|
199513
|
+
/**
|
|
199514
|
+
* Settings readers see an empty string for both secrets. The stored value
|
|
199515
|
+
* stays in the keychain (records `secret` meta). An empty round-trip must
|
|
199516
|
+
* not wipe it — see {@link mergeMatterSecrets}.
|
|
199517
|
+
*/
|
|
199518
|
+
function publicMatterConnection(connection) {
|
|
199519
|
+
return {
|
|
199520
|
+
...connection,
|
|
199521
|
+
wifiPassword: "",
|
|
199522
|
+
threadDataset: ""
|
|
199523
|
+
};
|
|
199524
|
+
}
|
|
199525
|
+
/** Keep a stored secret when the form sent back the redacted empty string. */
|
|
199526
|
+
function mergeMatterSecrets(existing, incoming) {
|
|
199527
|
+
return {
|
|
199528
|
+
...incoming,
|
|
199529
|
+
wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
|
|
199530
|
+
threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
|
|
199531
|
+
};
|
|
199532
|
+
}
|
|
198307
199533
|
/** Hand-written connection form for the broker/integration creation UI. */
|
|
198308
199534
|
function buildConnectionFormSchema() {
|
|
198309
199535
|
return { sections: [{
|
|
@@ -198315,9 +199541,10 @@ function buildConnectionFormSchema() {
|
|
|
198315
199541
|
columns: 1,
|
|
198316
199542
|
fields: [
|
|
198317
199543
|
{
|
|
198318
|
-
type: "
|
|
199544
|
+
type: "password",
|
|
198319
199545
|
key: "threadDataset",
|
|
198320
199546
|
secret: true,
|
|
199547
|
+
showToggle: true,
|
|
198321
199548
|
label: "Thread operational dataset (hex, optional)",
|
|
198322
199549
|
labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
|
|
198323
199550
|
placeholder: "0e08000000000001…"
|
|
@@ -198394,17 +199621,14 @@ var MATTER_KIND = "matter";
|
|
|
198394
199621
|
* Construct the `broker` cap provider for the Matter addon. Pure builder.
|
|
198395
199622
|
* Mirrors `buildEcowittBrokerProvider`. The defining Matter operation —
|
|
198396
199623
|
* commissioning — is surfaced through `publish` with
|
|
198397
|
-
* `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
|
|
198398
|
-
*
|
|
198399
|
-
* preserved on an empty round-trip.
|
|
199624
|
+
* `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
|
|
199625
|
+
* `threadDataset` and `wifiPassword` are redacted on settings reads and
|
|
199626
|
+
* preserved on an empty round-trip. Fabric keys stay in matter.js storage.
|
|
198400
199627
|
*/
|
|
198401
199628
|
function buildMatterBrokerProvider(deps) {
|
|
198402
199629
|
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
198403
199630
|
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
198404
|
-
const redact = (entry) => (
|
|
198405
|
-
...entry.connection,
|
|
198406
|
-
wifiPassword: ""
|
|
198407
|
-
});
|
|
199631
|
+
const redact = (entry) => publicMatterConnection(entry.connection);
|
|
198408
199632
|
return {
|
|
198409
199633
|
list: async ({ kind }) => {
|
|
198410
199634
|
if (kind && kind !== MATTER_KIND) return [];
|
|
@@ -198472,10 +199696,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
198472
199696
|
const existing = getBrokers().find((b) => b.id === id);
|
|
198473
199697
|
if (!existing) return;
|
|
198474
199698
|
const parsed = settingsToMatterConnection(settings);
|
|
198475
|
-
const merged =
|
|
198476
|
-
...parsed,
|
|
198477
|
-
wifiPassword: parsed.wifiPassword.length > 0 ? parsed.wifiPassword : existing.connection.wifiPassword
|
|
198478
|
-
};
|
|
199699
|
+
const merged = mergeMatterSecrets(existing.connection, parsed);
|
|
198479
199700
|
const updated = await registry.updateEntry(id, merged);
|
|
198480
199701
|
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
198481
199702
|
},
|
|
@@ -198511,7 +199732,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
198511
199732
|
if (!owns(brokerId)) throw new Error(`matter: unknown broker ${brokerId}`);
|
|
198512
199733
|
const action = typeof target["action"] === "string" ? target["action"] : void 0;
|
|
198513
199734
|
if (action === "commissionNode") {
|
|
198514
|
-
const code =
|
|
199735
|
+
const code = pairingCodeFromPayload(payload);
|
|
198515
199736
|
parsePairingCode(code);
|
|
198516
199737
|
return registry.commissionNode(brokerId, code);
|
|
198517
199738
|
}
|
|
@@ -198519,13 +199740,23 @@ function buildMatterBrokerProvider(deps) {
|
|
|
198519
199740
|
},
|
|
198520
199741
|
subscribe: async () => ({ subscriptionId: "" }),
|
|
198521
199742
|
unsubscribe: async () => void 0,
|
|
198522
|
-
getState: async () =>
|
|
199743
|
+
getState: async ({ brokerId, key }) => {
|
|
199744
|
+
if (key !== "commission") return null;
|
|
199745
|
+
return registry.getCommissionProgress(brokerId);
|
|
199746
|
+
},
|
|
198523
199747
|
getStatus: async () => ({
|
|
198524
199748
|
brokerCount: registry.size(),
|
|
198525
199749
|
connectedCount: registry.connectedCount()
|
|
198526
199750
|
})
|
|
198527
199751
|
};
|
|
198528
199752
|
}
|
|
199753
|
+
function pairingCodeFromPayload(payload) {
|
|
199754
|
+
if (typeof payload !== "object" || payload === null || !("pairingCode" in payload)) return "";
|
|
199755
|
+
const code = payload.pairingCode;
|
|
199756
|
+
if (typeof code === "string") return code;
|
|
199757
|
+
if (typeof code === "number" && Number.isFinite(code)) return String(code);
|
|
199758
|
+
return "";
|
|
199759
|
+
}
|
|
198529
199760
|
//#endregion
|
|
198530
199761
|
//#region src/matter-discovery.ts
|
|
198531
199762
|
/**
|
|
@@ -198756,6 +199987,27 @@ function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
|
198756
199987
|
return count;
|
|
198757
199988
|
}
|
|
198758
199989
|
//#endregion
|
|
199990
|
+
//#region src/matter-lock-command.ts
|
|
199991
|
+
/**
|
|
199992
|
+
* Decide the command arguments. `pinRequired` comes from the live
|
|
199993
|
+
* RequirePinForRemoteOperation attribute, not from a stored secret.
|
|
199994
|
+
*/
|
|
199995
|
+
function planDoorLockCommand(input) {
|
|
199996
|
+
const code = input.code?.trim() ?? "";
|
|
199997
|
+
if (input.pinRequired && code.length === 0) return {
|
|
199998
|
+
ok: false,
|
|
199999
|
+
reason: "pin-required"
|
|
200000
|
+
};
|
|
200001
|
+
if (code.length > 0) return {
|
|
200002
|
+
ok: true,
|
|
200003
|
+
args: { pinCode: code }
|
|
200004
|
+
};
|
|
200005
|
+
return {
|
|
200006
|
+
ok: true,
|
|
200007
|
+
args: {}
|
|
200008
|
+
};
|
|
200009
|
+
}
|
|
200010
|
+
//#endregion
|
|
198759
200011
|
//#region src/devices/matter-endpoint-device.ts
|
|
198760
200012
|
/**
|
|
198761
200013
|
* Persisted config every Matter endpoint child carries. `nodeId`/`endpointId`
|
|
@@ -198847,7 +200099,8 @@ var COLD_POWER = { lastFetchedAt: 0 };
|
|
|
198847
200099
|
* arrives, each cap reads its cold-start default.
|
|
198848
200100
|
*/
|
|
198849
200101
|
var MatterEndpointDevice = class extends BaseDevice {
|
|
198850
|
-
|
|
200102
|
+
/** Live RequirePinForRemoteOperation. Commands read this, not a stored PIN. */
|
|
200103
|
+
pinRequired = false;
|
|
198851
200104
|
brokerId;
|
|
198852
200105
|
nodeId;
|
|
198853
200106
|
endpointId;
|
|
@@ -198861,12 +200114,17 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
198861
200114
|
this.nodeId = persisted.nodeId;
|
|
198862
200115
|
this.endpointId = persisted.endpointId;
|
|
198863
200116
|
this.caps = persisted.capabilities.filter(isKnownCap);
|
|
200117
|
+
this.pinRequired = matterNodes.getAttribute(this.brokerId, this.nodeId, this.endpointId, MatterClusterId.DoorLock, MatterAttributeId.RequirePinForRemoteOperation) === true;
|
|
198864
200118
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? true;
|
|
198865
200119
|
this.updateSourceInfo({
|
|
198866
200120
|
id: `${this.nodeId}:${this.endpointId}`,
|
|
198867
200121
|
system: "matter"
|
|
198868
200122
|
});
|
|
198869
200123
|
}
|
|
200124
|
+
/** PIN-required locks advertise the feature the lock UI already understands. */
|
|
200125
|
+
get features() {
|
|
200126
|
+
return this.pinRequired ? [DeviceFeature.LockPinRequired] : [];
|
|
200127
|
+
}
|
|
198870
200128
|
async onActivate() {
|
|
198871
200129
|
await super.onActivate();
|
|
198872
200130
|
for (const cap of this.caps) {
|
|
@@ -198894,7 +200152,29 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
198894
200152
|
value: report.value
|
|
198895
200153
|
});
|
|
198896
200154
|
if (decoded === null) return;
|
|
200155
|
+
if (decoded.detail === "pin-required" || decoded.detail === "pin-not-required") {
|
|
200156
|
+
const next = decoded.detail === "pin-required";
|
|
200157
|
+
if (next !== this.pinRequired) {
|
|
200158
|
+
this.pinRequired = next;
|
|
200159
|
+
this.ctx.logger.info("Matter lock PIN requirement changed", {
|
|
200160
|
+
tags: { deviceId: this.id },
|
|
200161
|
+
meta: {
|
|
200162
|
+
nodeId: this.nodeId,
|
|
200163
|
+
endpointId: this.endpointId,
|
|
200164
|
+
pinRequired: next
|
|
200165
|
+
}
|
|
200166
|
+
});
|
|
200167
|
+
}
|
|
200168
|
+
} else if (decoded.detail !== void 0) this.ctx.logger.info("Matter lock detail", {
|
|
200169
|
+
tags: { deviceId: this.id },
|
|
200170
|
+
meta: {
|
|
200171
|
+
nodeId: this.nodeId,
|
|
200172
|
+
endpointId: this.endpointId,
|
|
200173
|
+
detail: decoded.detail
|
|
200174
|
+
}
|
|
200175
|
+
});
|
|
198897
200176
|
if (!this.registeredCaps.has(decoded.cap)) return;
|
|
200177
|
+
if (Object.keys(decoded.patch).length === 0) return;
|
|
198898
200178
|
this.runtimeState.patchCapState(CAP_SLICE_NAME[decoded.cap], decoded.patch);
|
|
198899
200179
|
}
|
|
198900
200180
|
/** Invoke a cluster command on this endpoint via the live controller. */
|
|
@@ -198990,30 +200270,52 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
198990
200270
|
};
|
|
198991
200271
|
this.ctx.registerNativeCap(lockControlCapability, {
|
|
198992
200272
|
getStatus: async () => this.runtimeState.getCapState("lock-control") ?? COLD,
|
|
198993
|
-
lock: async () => {
|
|
198994
|
-
await this.
|
|
198995
|
-
this.runtimeState.setCapState("lock-control", {
|
|
198996
|
-
state: "locked",
|
|
198997
|
-
lastChangedAt: Date.now()
|
|
198998
|
-
});
|
|
200273
|
+
lock: async ({ code }) => {
|
|
200274
|
+
await this.invokeLock("lockDoor", "locking", code);
|
|
198999
200275
|
},
|
|
199000
|
-
unlock: async () => {
|
|
199001
|
-
await this.
|
|
199002
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199003
|
-
state: "unlocked",
|
|
199004
|
-
lastChangedAt: Date.now()
|
|
199005
|
-
});
|
|
200276
|
+
unlock: async ({ code }) => {
|
|
200277
|
+
await this.invokeLock("unlockDoor", "unlocking", code);
|
|
199006
200278
|
},
|
|
199007
200279
|
open: async () => {
|
|
199008
|
-
await this.
|
|
199009
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199010
|
-
state: "unlocked",
|
|
199011
|
-
lastChangedAt: Date.now()
|
|
199012
|
-
});
|
|
200280
|
+
await this.invokeLock("unlockDoor", "unlocking", void 0);
|
|
199013
200281
|
}
|
|
199014
200282
|
});
|
|
199015
200283
|
this.runtimeState.setCapState("lock-control", COLD);
|
|
199016
200284
|
}
|
|
200285
|
+
/**
|
|
200286
|
+
* Send lockDoor / unlockDoor. The final locked/unlocked state arrives on the
|
|
200287
|
+
* attribute subscription — this method only records the in-flight transition.
|
|
200288
|
+
* A PIN is forwarded on the command and nowhere else.
|
|
200289
|
+
*/
|
|
200290
|
+
async invokeLock(commandName, pending, code) {
|
|
200291
|
+
const plan = planDoorLockCommand({
|
|
200292
|
+
pinRequired: this.pinRequired,
|
|
200293
|
+
code
|
|
200294
|
+
});
|
|
200295
|
+
if (!plan.ok) {
|
|
200296
|
+
this.ctx.logger.warn("Matter lock command refused: PIN required and none was provided", {
|
|
200297
|
+
tags: { deviceId: this.id },
|
|
200298
|
+
meta: {
|
|
200299
|
+
nodeId: this.nodeId,
|
|
200300
|
+
endpointId: this.endpointId,
|
|
200301
|
+
commandName
|
|
200302
|
+
}
|
|
200303
|
+
});
|
|
200304
|
+
throw new Error("matter-broker: this lock requires a PIN for remote operation");
|
|
200305
|
+
}
|
|
200306
|
+
const previous = this.runtimeState.getCapState("lock-control");
|
|
200307
|
+
this.runtimeState.setCapState("lock-control", {
|
|
200308
|
+
state: pending,
|
|
200309
|
+
lastChangedAt: Date.now()
|
|
200310
|
+
});
|
|
200311
|
+
try {
|
|
200312
|
+
const args = "pinCode" in plan.args ? plan.args : void 0;
|
|
200313
|
+
await this.invoke(MatterClusterId.DoorLock, commandName, args);
|
|
200314
|
+
} catch (err) {
|
|
200315
|
+
if (previous !== void 0) this.runtimeState.setCapState("lock-control", previous);
|
|
200316
|
+
throw err;
|
|
200317
|
+
}
|
|
200318
|
+
}
|
|
199017
200319
|
registerCover() {
|
|
199018
200320
|
const COLD = {
|
|
199019
200321
|
state: "closed",
|
|
@@ -199234,11 +200536,16 @@ var MatterNodeDevice = class extends BaseDevice {
|
|
|
199234
200536
|
this.nodeId = cfg.nodeId;
|
|
199235
200537
|
this.integrationId = cfg.integrationId;
|
|
199236
200538
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? false;
|
|
200539
|
+
matterNodes.bindDevice(this.nodeId, this.id);
|
|
199237
200540
|
this.updateSourceInfo({
|
|
199238
200541
|
id: this.nodeId,
|
|
199239
200542
|
system: "matter"
|
|
199240
200543
|
});
|
|
199241
200544
|
}
|
|
200545
|
+
async removeDevice() {
|
|
200546
|
+
matterNodes.unbindDevice(this.nodeId);
|
|
200547
|
+
await super.removeDevice();
|
|
200548
|
+
}
|
|
199242
200549
|
getAccessoryChildren() {
|
|
199243
200550
|
const specs = [];
|
|
199244
200551
|
for (const endpoint of this.config.values.endpoints) {
|