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