@camstack/addon-matter-broker 0.2.149 → 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 +6 -0
- package/README.md +1 -1
- package/dist/addon.js +784 -110
- package/dist/addon.mjs +784 -110
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -6604,7 +6604,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
6604
6604
|
this.name = "SecretOverwriteRefusedError";
|
|
6605
6605
|
}
|
|
6606
6606
|
};
|
|
6607
|
-
function isRecord$4(value) {
|
|
6607
|
+
function isRecord$4$1(value) {
|
|
6608
6608
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6609
6609
|
}
|
|
6610
6610
|
function resolvedValue(pass, marker) {
|
|
@@ -6633,7 +6633,7 @@ function inArray(pass, incoming, stored) {
|
|
|
6633
6633
|
return incoming.map((item, index) => {
|
|
6634
6634
|
const twin = stored[index];
|
|
6635
6635
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
6636
|
-
if (isRecord$4(item) && isRecord$4(twin)) return inArrayRow(pass, item, twin);
|
|
6636
|
+
if (isRecord$4$1(item) && isRecord$4$1(twin)) return inArrayRow(pass, item, twin);
|
|
6637
6637
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
6638
6638
|
return item;
|
|
6639
6639
|
});
|
|
@@ -6644,7 +6644,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6644
6644
|
const twin = stored[key];
|
|
6645
6645
|
if (isSecretMarker(twin)) {
|
|
6646
6646
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
6647
|
-
} else if (isRecord$4(value) && isRecord$4(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
6647
|
+
} else if (isRecord$4$1(value) && isRecord$4$1(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
6648
6648
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
6649
6649
|
}
|
|
6650
6650
|
return out;
|
|
@@ -6652,7 +6652,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6652
6652
|
async function walk(pass, incoming, stored) {
|
|
6653
6653
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
6654
6654
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
6655
|
-
if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
|
|
6655
|
+
if (!isRecord$4$1(incoming) || !isRecord$4$1(stored)) return incoming;
|
|
6656
6656
|
const out = { ...incoming };
|
|
6657
6657
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
6658
6658
|
return out;
|
|
@@ -6683,7 +6683,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
6683
6683
|
function isJsonEqual(a, b) {
|
|
6684
6684
|
if (Object.is(a, b)) return true;
|
|
6685
6685
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
6686
|
-
if (isRecord$4(a) && isRecord$4(b)) {
|
|
6686
|
+
if (isRecord$4$1(a) && isRecord$4$1(b)) {
|
|
6687
6687
|
const keys = Object.keys(a);
|
|
6688
6688
|
if (keys.length !== Object.keys(b).length) return false;
|
|
6689
6689
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -6707,7 +6707,7 @@ function markerKeysOf(blob, owner) {
|
|
|
6707
6707
|
}
|
|
6708
6708
|
function nodeFromStored(value, owner) {
|
|
6709
6709
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
6710
|
-
if (!isRecord$4(value)) return null;
|
|
6710
|
+
if (!isRecord$4$1(value)) return null;
|
|
6711
6711
|
const children = /* @__PURE__ */ new Map();
|
|
6712
6712
|
for (const [key, child] of Object.entries(value)) {
|
|
6713
6713
|
const node = nodeFromStored(child, owner);
|
|
@@ -11258,7 +11258,7 @@ var ExpressionEvalError = class extends Error {
|
|
|
11258
11258
|
this.name = "ExpressionEvalError";
|
|
11259
11259
|
}
|
|
11260
11260
|
};
|
|
11261
|
-
function asFiniteNumber(value, name, index) {
|
|
11261
|
+
function asFiniteNumber$1(value, name, index) {
|
|
11262
11262
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
11263
11263
|
return value;
|
|
11264
11264
|
}
|
|
@@ -11271,7 +11271,7 @@ function finiteResult(value, name) {
|
|
|
11271
11271
|
return value;
|
|
11272
11272
|
}
|
|
11273
11273
|
function allFiniteNumbers(args, name) {
|
|
11274
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
11274
|
+
return args.map((a, idx) => asFiniteNumber$1(a, name, idx));
|
|
11275
11275
|
}
|
|
11276
11276
|
function asBoolean$2(value, name, index) {
|
|
11277
11277
|
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
@@ -11308,29 +11308,29 @@ var table = {
|
|
|
11308
11308
|
abs: {
|
|
11309
11309
|
minArgs: 1,
|
|
11310
11310
|
maxArgs: 1,
|
|
11311
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
11311
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber$1(args[0], "abs", 0)), "abs")
|
|
11312
11312
|
},
|
|
11313
11313
|
floor: {
|
|
11314
11314
|
minArgs: 1,
|
|
11315
11315
|
maxArgs: 1,
|
|
11316
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
11316
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber$1(args[0], "floor", 0)), "floor")
|
|
11317
11317
|
},
|
|
11318
11318
|
ceil: {
|
|
11319
11319
|
minArgs: 1,
|
|
11320
11320
|
maxArgs: 1,
|
|
11321
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
11321
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber$1(args[0], "ceil", 0)), "ceil")
|
|
11322
11322
|
},
|
|
11323
11323
|
sqrt: {
|
|
11324
11324
|
minArgs: 1,
|
|
11325
11325
|
maxArgs: 1,
|
|
11326
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
11326
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber$1(args[0], "sqrt", 0)), "sqrt")
|
|
11327
11327
|
},
|
|
11328
11328
|
round: {
|
|
11329
11329
|
minArgs: 1,
|
|
11330
11330
|
maxArgs: 2,
|
|
11331
11331
|
apply: (args) => {
|
|
11332
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
11333
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
11332
|
+
const x = asFiniteNumber$1(args[0], "round", 0);
|
|
11333
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber$1(args[1], "round", 1)) : 0;
|
|
11334
11334
|
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
11335
11335
|
const factor = 10 ** digits;
|
|
11336
11336
|
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
@@ -11339,15 +11339,15 @@ var table = {
|
|
|
11339
11339
|
pow: {
|
|
11340
11340
|
minArgs: 2,
|
|
11341
11341
|
maxArgs: 2,
|
|
11342
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
11342
|
+
apply: (args) => finiteResult(asFiniteNumber$1(args[0], "pow", 0) ** asFiniteNumber$1(args[1], "pow", 1), "pow")
|
|
11343
11343
|
},
|
|
11344
11344
|
clamp: {
|
|
11345
11345
|
minArgs: 3,
|
|
11346
11346
|
maxArgs: 3,
|
|
11347
11347
|
apply: (args) => {
|
|
11348
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
11349
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
11350
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
11348
|
+
const x = asFiniteNumber$1(args[0], "clamp", 0);
|
|
11349
|
+
const lo = asFiniteNumber$1(args[1], "clamp", 1);
|
|
11350
|
+
const hi = asFiniteNumber$1(args[2], "clamp", 2);
|
|
11351
11351
|
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
11352
11352
|
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
11353
11353
|
}
|
|
@@ -11376,13 +11376,13 @@ var table = {
|
|
|
11376
11376
|
age: {
|
|
11377
11377
|
minArgs: 2,
|
|
11378
11378
|
maxArgs: 2,
|
|
11379
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
11379
|
+
apply: (args) => finiteResult(asFiniteNumber$1(args[0], "age", 0) - asFiniteNumber$1(args[1], "age", 1), "age")
|
|
11380
11380
|
},
|
|
11381
11381
|
convert: {
|
|
11382
11382
|
minArgs: 3,
|
|
11383
11383
|
maxArgs: 3,
|
|
11384
11384
|
apply: (args, hooks) => {
|
|
11385
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
11385
|
+
const x = asFiniteNumber$1(args[0], "convert", 0);
|
|
11386
11386
|
const from = asString(args[1], "convert", 1).trim();
|
|
11387
11387
|
const to = asString(args[2], "convert", 2).trim();
|
|
11388
11388
|
if (hooks.convert) {
|
|
@@ -11408,11 +11408,11 @@ var table = {
|
|
|
11408
11408
|
minArgs: 2,
|
|
11409
11409
|
maxArgs: 2,
|
|
11410
11410
|
apply: (args, hooks) => {
|
|
11411
|
-
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
11411
|
+
const windowMs = asFiniteNumber$1(args[1], "within", 1);
|
|
11412
11412
|
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
11413
11413
|
const at = args[0];
|
|
11414
11414
|
if (at === null) return false;
|
|
11415
|
-
const ts = asFiniteNumber(at, "within", 0);
|
|
11415
|
+
const ts = asFiniteNumber$1(at, "within", 0);
|
|
11416
11416
|
const now = hooks.now;
|
|
11417
11417
|
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
11418
11418
|
const inside = now - ts <= windowMs;
|
|
@@ -11429,7 +11429,7 @@ var table = {
|
|
|
11429
11429
|
minArgs: 1,
|
|
11430
11430
|
maxArgs: INF,
|
|
11431
11431
|
apply: (args) => {
|
|
11432
|
-
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
|
|
11432
|
+
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber$1(a, "latest", i)]);
|
|
11433
11433
|
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
11434
11434
|
}
|
|
11435
11435
|
},
|
|
@@ -197516,6 +197516,7 @@ var StubMatterController = class {
|
|
|
197516
197516
|
async removeNode(_nodeId) {
|
|
197517
197517
|
throw new MatterControllerNotWiredError("removeNode");
|
|
197518
197518
|
}
|
|
197519
|
+
async syncSubscriptions() {}
|
|
197519
197520
|
};
|
|
197520
197521
|
/**
|
|
197521
197522
|
* Factory seam (stub default): build a {@link StubMatterController}.
|
|
@@ -197688,7 +197689,13 @@ var MatterAttributeId = {
|
|
|
197688
197689
|
/** ElectricalPowerMeasurement.ActivePower (int64, mW). */
|
|
197689
197690
|
ActivePower: 8,
|
|
197690
197691
|
/** ElectricalEnergyMeasurement.CumulativeEnergyImported (energy struct, mWh). */
|
|
197691
|
-
CumulativeEnergyImported: 1
|
|
197692
|
+
CumulativeEnergyImported: 1,
|
|
197693
|
+
/** DoorLock.LockState (enum: 0 NotFullyLocked, 1 Locked, 2 Unlocked, 3 Unlatched). */
|
|
197694
|
+
LockState: 0,
|
|
197695
|
+
/** DoorLock.DoorState (enum; 2 = DoorJammed). Present only with the door sensor. */
|
|
197696
|
+
DoorState: 3,
|
|
197697
|
+
/** DoorLock.RequirePinForRemoteOperation (bool). */
|
|
197698
|
+
RequirePinForRemoteOperation: 51
|
|
197692
197699
|
};
|
|
197693
197700
|
/** Thermostat.SystemMode enum → CamStack HvacMode (climate-control). */
|
|
197694
197701
|
var SYSTEM_MODE_TO_HVAC = new Map([
|
|
@@ -197720,8 +197727,11 @@ function asBoolean(value) {
|
|
|
197720
197727
|
* matter.js may surface a `MeasuredValue` either as a plain scaled int or wrapped
|
|
197721
197728
|
* in an energy-measurement struct (`{ energy }`). Pull the numeric payload out.
|
|
197722
197729
|
*/
|
|
197730
|
+
function isRecord$4(value) {
|
|
197731
|
+
return typeof value === "object" && value !== null;
|
|
197732
|
+
}
|
|
197723
197733
|
function asEnergyMilliWattHours(value) {
|
|
197724
|
-
if (
|
|
197734
|
+
if (isRecord$4(value) && "energy" in value) return asNumber(value["energy"]);
|
|
197725
197735
|
return asNumber(value);
|
|
197726
197736
|
}
|
|
197727
197737
|
/**
|
|
@@ -197743,9 +197753,77 @@ function decodeMatterAttribute(input) {
|
|
|
197743
197753
|
case MatterClusterId.Thermostat: return decodeThermostat(attributeId, value);
|
|
197744
197754
|
case MatterClusterId.ElectricalPowerMeasurement: return decodeElectricalPower(attributeId, value);
|
|
197745
197755
|
case MatterClusterId.ElectricalEnergyMeasurement: return decodeElectricalEnergy(attributeId, value);
|
|
197756
|
+
case MatterClusterId.DoorLock: return decodeDoorLock(attributeId, value);
|
|
197746
197757
|
default: return null;
|
|
197747
197758
|
}
|
|
197748
197759
|
}
|
|
197760
|
+
/** LockState enum → cap state. NotFullyLocked has no cap value of its own. */
|
|
197761
|
+
var LOCK_STATE_TO_CAP = new Map([
|
|
197762
|
+
[0, {
|
|
197763
|
+
state: "jammed",
|
|
197764
|
+
detail: "not-fully-locked"
|
|
197765
|
+
}],
|
|
197766
|
+
[1, { state: "locked" }],
|
|
197767
|
+
[2, { state: "unlocked" }],
|
|
197768
|
+
[3, {
|
|
197769
|
+
state: "unlocked",
|
|
197770
|
+
detail: "unlatched"
|
|
197771
|
+
}]
|
|
197772
|
+
]);
|
|
197773
|
+
/** DoorState enum → a detail. Only DoorJammed also moves the lock cap. */
|
|
197774
|
+
var DOOR_STATE_DETAIL = new Map([
|
|
197775
|
+
[0, "door-open"],
|
|
197776
|
+
[1, "door-closed"],
|
|
197777
|
+
[2, "door-jammed"],
|
|
197778
|
+
[3, "door-forced-open"],
|
|
197779
|
+
[4, "door-unspecified"],
|
|
197780
|
+
[5, "door-ajar"]
|
|
197781
|
+
]);
|
|
197782
|
+
function decodeDoorLock(attributeId, value) {
|
|
197783
|
+
if (attributeId === MatterAttributeId.LockState) {
|
|
197784
|
+
const raw = asNumber(value);
|
|
197785
|
+
if (raw === null) return null;
|
|
197786
|
+
const mapped = LOCK_STATE_TO_CAP.get(raw);
|
|
197787
|
+
if (mapped === void 0) return null;
|
|
197788
|
+
return {
|
|
197789
|
+
cap: "lock-control",
|
|
197790
|
+
patch: {
|
|
197791
|
+
state: mapped.state,
|
|
197792
|
+
lastChangedAt: Date.now()
|
|
197793
|
+
},
|
|
197794
|
+
...mapped.detail !== void 0 ? { detail: mapped.detail } : {}
|
|
197795
|
+
};
|
|
197796
|
+
}
|
|
197797
|
+
if (attributeId === MatterAttributeId.DoorState) {
|
|
197798
|
+
const raw = asNumber(value);
|
|
197799
|
+
if (raw === null) return null;
|
|
197800
|
+
const detail = DOOR_STATE_DETAIL.get(raw);
|
|
197801
|
+
if (detail === void 0) return null;
|
|
197802
|
+
if (detail === "door-jammed") return {
|
|
197803
|
+
cap: "lock-control",
|
|
197804
|
+
patch: {
|
|
197805
|
+
state: "jammed",
|
|
197806
|
+
lastChangedAt: Date.now()
|
|
197807
|
+
},
|
|
197808
|
+
detail
|
|
197809
|
+
};
|
|
197810
|
+
return {
|
|
197811
|
+
cap: "lock-control",
|
|
197812
|
+
patch: {},
|
|
197813
|
+
detail
|
|
197814
|
+
};
|
|
197815
|
+
}
|
|
197816
|
+
if (attributeId === MatterAttributeId.RequirePinForRemoteOperation) {
|
|
197817
|
+
const required = asBoolean(value);
|
|
197818
|
+
if (required === null) return null;
|
|
197819
|
+
return {
|
|
197820
|
+
cap: "lock-control",
|
|
197821
|
+
patch: {},
|
|
197822
|
+
detail: required ? "pin-required" : "pin-not-required"
|
|
197823
|
+
};
|
|
197824
|
+
}
|
|
197825
|
+
return null;
|
|
197826
|
+
}
|
|
197749
197827
|
function decodeOnOff(attributeId, value) {
|
|
197750
197828
|
if (attributeId !== MatterAttributeId.OnOff) return null;
|
|
197751
197829
|
const on = asBoolean(value);
|
|
@@ -197969,7 +198047,10 @@ var ATTR_NAME_TO_ID = new Map([
|
|
|
197969
198047
|
["voltage", MatterAttributeId.Voltage],
|
|
197970
198048
|
["activeCurrent", MatterAttributeId.ActiveCurrent],
|
|
197971
198049
|
["activePower", MatterAttributeId.ActivePower],
|
|
197972
|
-
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported]
|
|
198050
|
+
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported],
|
|
198051
|
+
["lockState", MatterAttributeId.LockState],
|
|
198052
|
+
["doorState", MatterAttributeId.DoorState],
|
|
198053
|
+
["requirePinForRemoteOperation", MatterAttributeId.RequirePinForRemoteOperation]
|
|
197973
198054
|
]);
|
|
197974
198055
|
/** clusterId → matter.js behavior id (camelCase) for outbound command clusters. */
|
|
197975
198056
|
var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
@@ -197981,14 +198062,14 @@ var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
|
197981
198062
|
[258, "windowCovering"]
|
|
197982
198063
|
]);
|
|
197983
198064
|
/** True when `value` is a non-null object (cast-free record narrowing). */
|
|
197984
|
-
function isRecord$
|
|
198065
|
+
function isRecord$3(value) {
|
|
197985
198066
|
return typeof value === "object" && value !== null;
|
|
197986
198067
|
}
|
|
197987
198068
|
/** Read a finite numeric cluster id from a `Behavior.Type`'s `.cluster.id`. */
|
|
197988
198069
|
function clusterIdOf(behaviorType) {
|
|
197989
|
-
if (!isRecord$
|
|
198070
|
+
if (!isRecord$3(behaviorType)) return null;
|
|
197990
198071
|
const cluster = behaviorType["cluster"];
|
|
197991
|
-
if (!isRecord$
|
|
198072
|
+
if (!isRecord$3(cluster)) return null;
|
|
197992
198073
|
const id = cluster["id"];
|
|
197993
198074
|
if (typeof id === "number" && Number.isFinite(id)) return id;
|
|
197994
198075
|
if (typeof id === "bigint") return Number(id);
|
|
@@ -197999,13 +198080,13 @@ function clusterIdOf(behaviorType) {
|
|
|
197999
198080
|
* the matter.js `Endpoint` interop at one typed boundary.
|
|
198000
198081
|
*/
|
|
198001
198082
|
function asDynamicEndpoint(value) {
|
|
198002
|
-
if (!isRecord$
|
|
198083
|
+
if (!isRecord$3(value)) return null;
|
|
198003
198084
|
const number = value["number"];
|
|
198004
198085
|
const behaviors = value["behaviors"];
|
|
198005
198086
|
const parts = value["parts"];
|
|
198006
198087
|
const stateOf = value["stateOf"];
|
|
198007
198088
|
const commandsOf = value["commandsOf"];
|
|
198008
|
-
if (typeof number === "number" && isRecord$
|
|
198089
|
+
if (typeof number === "number" && isRecord$3(behaviors) && "supported" in behaviors && typeof stateOf === "function" && typeof commandsOf === "function" && isIterable$2(parts)) return {
|
|
198009
198090
|
number,
|
|
198010
198091
|
behaviors: { supported: behaviors["supported"] },
|
|
198011
198092
|
parts: mapIterable(parts, asDynamicEndpointOrThrow),
|
|
@@ -198029,7 +198110,7 @@ async function invokeNodeCommand(input) {
|
|
|
198029
198110
|
const behaviorId = CLUSTER_ID_TO_BEHAVIOR_ID.get(clusterId);
|
|
198030
198111
|
if (behaviorId === void 0) throw new Error(`matter-broker: cluster 0x${clusterId.toString(16)} has no command binding`);
|
|
198031
198112
|
const commands = target.commandsOf(behaviorId);
|
|
198032
|
-
if (!isRecord$
|
|
198113
|
+
if (!isRecord$3(commands)) throw new Error(`matter-broker: endpoint ${endpointId} has no '${behaviorId}' commands`);
|
|
198033
198114
|
const command = commands[commandName];
|
|
198034
198115
|
const fn = asCommandFn(command);
|
|
198035
198116
|
if (fn === null) throw new Error(`matter-broker: command '${commandName}' unavailable on '${behaviorId}'`);
|
|
@@ -198054,8 +198135,8 @@ function asDynamicEndpointOrThrow(value) {
|
|
|
198054
198135
|
if (ep === null) throw new TypeError("matter-broker: unexpected non-endpoint child part");
|
|
198055
198136
|
return ep;
|
|
198056
198137
|
}
|
|
198057
|
-
function isIterable$
|
|
198058
|
-
return isRecord$
|
|
198138
|
+
function isIterable$2(value) {
|
|
198139
|
+
return isRecord$3(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
|
|
198059
198140
|
}
|
|
198060
198141
|
function* mapIterable(it, fn) {
|
|
198061
198142
|
for (const v of it) yield fn(v);
|
|
@@ -198076,7 +198157,7 @@ function collectEndpoint(endpoint, out) {
|
|
|
198076
198157
|
const supported = endpoint.behaviors.supported;
|
|
198077
198158
|
const clusterIds = [];
|
|
198078
198159
|
const attributes = [];
|
|
198079
|
-
if (isRecord$
|
|
198160
|
+
if (isRecord$3(supported)) for (const [behaviorId, behaviorType] of Object.entries(supported)) {
|
|
198080
198161
|
const clusterId = clusterIdOf(behaviorType);
|
|
198081
198162
|
if (clusterId === null) continue;
|
|
198082
198163
|
clusterIds.push(clusterId);
|
|
@@ -198096,7 +198177,7 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
198096
198177
|
} catch {
|
|
198097
198178
|
return;
|
|
198098
198179
|
}
|
|
198099
|
-
if (!isRecord$
|
|
198180
|
+
if (!isRecord$3(struct)) return;
|
|
198100
198181
|
for (const [attrName, value] of Object.entries(struct)) {
|
|
198101
198182
|
const attributeId = ATTR_NAME_TO_ID.get(attrName);
|
|
198102
198183
|
if (attributeId === void 0) continue;
|
|
@@ -198108,35 +198189,413 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
198108
198189
|
}
|
|
198109
198190
|
}
|
|
198110
198191
|
//#endregion
|
|
198192
|
+
//#region src/matter-commission-progress.ts
|
|
198193
|
+
/**
|
|
198194
|
+
* Commissioning progress the admin UI polls, plus the named failures a share
|
|
198195
|
+
* code can produce. Pure — no matter.js import.
|
|
198196
|
+
*
|
|
198197
|
+
* Phases follow the on-network share path: discover the device, establish
|
|
198198
|
+
* PASE, then commission onto this fabric. A missing or malformed code never
|
|
198199
|
+
* becomes a later phase.
|
|
198200
|
+
*/
|
|
198201
|
+
var CommissionPhase = {
|
|
198202
|
+
Idle: "idle",
|
|
198203
|
+
Discovering: "discovering",
|
|
198204
|
+
Pase: "pase",
|
|
198205
|
+
Commissioning: "commissioning",
|
|
198206
|
+
Done: "done",
|
|
198207
|
+
Failed: "failed"
|
|
198208
|
+
};
|
|
198209
|
+
var CommissionFailureCode = {
|
|
198210
|
+
CodeExpired: "code-expired",
|
|
198211
|
+
DeviceNotFound: "device-not-found",
|
|
198212
|
+
WrongCode: "wrong-code",
|
|
198213
|
+
InvalidCode: "invalid-code",
|
|
198214
|
+
Unknown: "unknown"
|
|
198215
|
+
};
|
|
198216
|
+
var IDLE_COMMISSION_PROGRESS = {
|
|
198217
|
+
phase: CommissionPhase.Idle,
|
|
198218
|
+
failure: null,
|
|
198219
|
+
message: null
|
|
198220
|
+
};
|
|
198221
|
+
/** Stable prefix the UI matches. The human text follows the colon. */
|
|
198222
|
+
function commissionFailureMessage(code, detail) {
|
|
198223
|
+
return `matter-commission:${code}: ${detail}`;
|
|
198224
|
+
}
|
|
198225
|
+
/**
|
|
198226
|
+
* Map a controller or matter.js error onto a named failure. Order matters:
|
|
198227
|
+
* an expired commissioning window is not "device not found", and a rejected
|
|
198228
|
+
* passcode is not a network miss.
|
|
198229
|
+
*/
|
|
198230
|
+
function classifyCommissionError(message) {
|
|
198231
|
+
const text = message.toLowerCase();
|
|
198232
|
+
if (text.includes("invalid pairing code") || text.includes("11 or 21")) return CommissionFailureCode.InvalidCode;
|
|
198233
|
+
if (text.includes("expired") || text.includes("commissioning window") || text.includes("window closed")) return CommissionFailureCode.CodeExpired;
|
|
198234
|
+
if (text.includes("passcode") || text.includes("wrong code") || text.includes("pase") || text.includes("authentication failed") || text.includes("status 0x7e") || text.includes("invalid code")) return CommissionFailureCode.WrongCode;
|
|
198235
|
+
if (text.includes("not found") || text.includes("no device") || text.includes("no commissionable") || text.includes("discovery") || text.includes("timed out") || text.includes("timeout") || text.includes("unreachable")) return CommissionFailureCode.DeviceNotFound;
|
|
198236
|
+
return CommissionFailureCode.Unknown;
|
|
198237
|
+
}
|
|
198238
|
+
//#endregion
|
|
198239
|
+
//#region src/matter-subscription.ts
|
|
198240
|
+
/**
|
|
198241
|
+
* Per-node Matter attribute subscriptions. The matter.js call itself lives in
|
|
198242
|
+
* {@link import('./matter-peer-subscribe.js')}; this module owns the lifecycle:
|
|
198243
|
+
* open, resubscribe after the session drops or the node returns, and a bounded
|
|
198244
|
+
* backoff. A report whose value is missing is not a value (D393) and is dropped.
|
|
198245
|
+
*/
|
|
198246
|
+
/** First wait after a dropped subscription. */
|
|
198247
|
+
var SUBSCRIPTION_BACKOFF_INITIAL_MS = 1e3;
|
|
198248
|
+
/** Cap so a sleepy Thread node cannot be hammered. */
|
|
198249
|
+
var SUBSCRIPTION_BACKOFF_MAX_MS = 6e4;
|
|
198250
|
+
/**
|
|
198251
|
+
* One supervisor per fabric. `sync` is safe to call on start, after commission,
|
|
198252
|
+
* and on the reconcile timer (the backstop when a lifecycle event was missed).
|
|
198253
|
+
*/
|
|
198254
|
+
var MatterSubscriptionSupervisor = class {
|
|
198255
|
+
#brokerId;
|
|
198256
|
+
#logger;
|
|
198257
|
+
#resolveDeviceId;
|
|
198258
|
+
#onReport;
|
|
198259
|
+
#slots = /* @__PURE__ */ new Map();
|
|
198260
|
+
#stopped = false;
|
|
198261
|
+
constructor(options) {
|
|
198262
|
+
this.#brokerId = options.brokerId;
|
|
198263
|
+
this.#logger = options.logger;
|
|
198264
|
+
this.#resolveDeviceId = options.resolveDeviceId;
|
|
198265
|
+
this.#onReport = options.onReport;
|
|
198266
|
+
}
|
|
198267
|
+
/** How many times `subscribe` has been entered for a node (tests). */
|
|
198268
|
+
subscribeAttempts(nodeId) {
|
|
198269
|
+
return this.#slots.get(nodeId)?.generation ?? 0;
|
|
198270
|
+
}
|
|
198271
|
+
phase(nodeId) {
|
|
198272
|
+
return this.#slots.get(nodeId)?.phase ?? null;
|
|
198273
|
+
}
|
|
198274
|
+
async sync(peers) {
|
|
198275
|
+
if (this.#stopped) return;
|
|
198276
|
+
const live = /* @__PURE__ */ new Set();
|
|
198277
|
+
for (const peer of peers) {
|
|
198278
|
+
live.add(peer.nodeId);
|
|
198279
|
+
const slot = this.#slots.get(peer.nodeId);
|
|
198280
|
+
if (slot === void 0) {
|
|
198281
|
+
await this.#open(peer);
|
|
198282
|
+
continue;
|
|
198283
|
+
}
|
|
198284
|
+
slot.peer = peer;
|
|
198285
|
+
if (slot.phase !== "subscribed" && peer.online() && slot.phase !== "resubscribing") {
|
|
198286
|
+
slot.attempt = 0;
|
|
198287
|
+
await this.#open(peer);
|
|
198288
|
+
}
|
|
198289
|
+
}
|
|
198290
|
+
for (const [nodeId, slot] of this.#slots) {
|
|
198291
|
+
if (live.has(nodeId)) continue;
|
|
198292
|
+
await this.#drop(slot);
|
|
198293
|
+
this.#slots.delete(nodeId);
|
|
198294
|
+
}
|
|
198295
|
+
}
|
|
198296
|
+
async stop() {
|
|
198297
|
+
this.#stopped = true;
|
|
198298
|
+
for (const slot of this.#slots.values()) await this.#drop(slot);
|
|
198299
|
+
this.#slots.clear();
|
|
198300
|
+
}
|
|
198301
|
+
async #open(peer) {
|
|
198302
|
+
if (this.#stopped) return;
|
|
198303
|
+
const existing = this.#slots.get(peer.nodeId);
|
|
198304
|
+
if (existing?.phase === "subscribed" && existing.subscription !== null) return;
|
|
198305
|
+
if (existing?.timer !== null && existing?.timer !== void 0) {
|
|
198306
|
+
clearTimeout(existing.timer);
|
|
198307
|
+
existing.timer = null;
|
|
198308
|
+
}
|
|
198309
|
+
const generation = (existing?.generation ?? 0) + 1;
|
|
198310
|
+
const slot = existing ?? this.#fresh(peer);
|
|
198311
|
+
slot.peer = peer;
|
|
198312
|
+
slot.generation = generation;
|
|
198313
|
+
slot.phase = "resubscribing";
|
|
198314
|
+
this.#slots.set(peer.nodeId, slot);
|
|
198315
|
+
this.#listen(slot);
|
|
198316
|
+
this.#log(peer.nodeId, "resubscribing");
|
|
198317
|
+
let subscription;
|
|
198318
|
+
try {
|
|
198319
|
+
subscription = await peer.subscribe({
|
|
198320
|
+
onReport: (report) => {
|
|
198321
|
+
if (report.value === void 0 || report.value === null) return;
|
|
198322
|
+
this.#onReport(report);
|
|
198323
|
+
},
|
|
198324
|
+
onClosed: () => {
|
|
198325
|
+
const current = this.#slots.get(peer.nodeId);
|
|
198326
|
+
if (current === void 0 || current.generation !== generation) return;
|
|
198327
|
+
this.#onClosed(peer.nodeId);
|
|
198328
|
+
}
|
|
198329
|
+
});
|
|
198330
|
+
} catch (err) {
|
|
198331
|
+
this.#log(peer.nodeId, "subscribe-failed", { error: errMsg(err) });
|
|
198332
|
+
const current = this.#slots.get(peer.nodeId);
|
|
198333
|
+
if (current === void 0 || current.generation !== generation) return;
|
|
198334
|
+
current.phase = "subscribe-failed";
|
|
198335
|
+
this.#schedule(peer.nodeId);
|
|
198336
|
+
return;
|
|
198337
|
+
}
|
|
198338
|
+
if (this.#stopped || this.#slots.get(peer.nodeId)?.generation !== generation) {
|
|
198339
|
+
await subscription.close().catch(() => void 0);
|
|
198340
|
+
return;
|
|
198341
|
+
}
|
|
198342
|
+
slot.subscription = subscription;
|
|
198343
|
+
slot.phase = "subscribed";
|
|
198344
|
+
slot.attempt = 0;
|
|
198345
|
+
this.#log(peer.nodeId, "subscribed");
|
|
198346
|
+
}
|
|
198347
|
+
#onClosed(nodeId) {
|
|
198348
|
+
const slot = this.#slots.get(nodeId);
|
|
198349
|
+
if (slot === void 0 || slot.dropping) return;
|
|
198350
|
+
if (slot.phase !== "subscribed" && slot.phase !== "resubscribing") return;
|
|
198351
|
+
slot.subscription = null;
|
|
198352
|
+
slot.phase = "closed";
|
|
198353
|
+
this.#log(nodeId, "closed");
|
|
198354
|
+
if (!slot.peer.online()) {
|
|
198355
|
+
slot.phase = "offline";
|
|
198356
|
+
this.#log(nodeId, "offline");
|
|
198357
|
+
return;
|
|
198358
|
+
}
|
|
198359
|
+
this.#schedule(nodeId);
|
|
198360
|
+
}
|
|
198361
|
+
#schedule(nodeId) {
|
|
198362
|
+
const slot = this.#slots.get(nodeId);
|
|
198363
|
+
if (slot === void 0 || this.#stopped) return;
|
|
198364
|
+
if (slot.timer !== null) clearTimeout(slot.timer);
|
|
198365
|
+
const delay = Math.min(SUBSCRIPTION_BACKOFF_MAX_MS, SUBSCRIPTION_BACKOFF_INITIAL_MS * 2 ** slot.attempt);
|
|
198366
|
+
slot.attempt += 1;
|
|
198367
|
+
slot.phase = "backing-off";
|
|
198368
|
+
this.#log(nodeId, "backing-off", {
|
|
198369
|
+
delayMs: delay,
|
|
198370
|
+
attempt: slot.attempt
|
|
198371
|
+
});
|
|
198372
|
+
slot.timer = setTimeout(() => {
|
|
198373
|
+
slot.timer = null;
|
|
198374
|
+
this.#open(slot.peer);
|
|
198375
|
+
}, delay);
|
|
198376
|
+
if (typeof slot.timer.unref === "function") slot.timer.unref();
|
|
198377
|
+
}
|
|
198378
|
+
#listen(slot) {
|
|
198379
|
+
if (slot.listening) return;
|
|
198380
|
+
slot.listening = true;
|
|
198381
|
+
slot.unlisten.push(slot.peer.onOffline(() => {
|
|
198382
|
+
const current = this.#slots.get(slot.peer.nodeId);
|
|
198383
|
+
if (current === void 0) return;
|
|
198384
|
+
if (current.phase === "offline") return;
|
|
198385
|
+
current.phase = "offline";
|
|
198386
|
+
this.#log(slot.peer.nodeId, "offline");
|
|
198387
|
+
}));
|
|
198388
|
+
slot.unlisten.push(slot.peer.onOnline(() => {
|
|
198389
|
+
const current = this.#slots.get(slot.peer.nodeId);
|
|
198390
|
+
if (current === void 0 || this.#stopped) return;
|
|
198391
|
+
current.attempt = 0;
|
|
198392
|
+
if (current.timer !== null) {
|
|
198393
|
+
clearTimeout(current.timer);
|
|
198394
|
+
current.timer = null;
|
|
198395
|
+
}
|
|
198396
|
+
this.#log(slot.peer.nodeId, "online");
|
|
198397
|
+
this.#open(current.peer);
|
|
198398
|
+
}));
|
|
198399
|
+
}
|
|
198400
|
+
async #drop(slot) {
|
|
198401
|
+
slot.dropping = true;
|
|
198402
|
+
if (slot.timer !== null) clearTimeout(slot.timer);
|
|
198403
|
+
slot.timer = null;
|
|
198404
|
+
for (const unlisten of slot.unlisten) unlisten();
|
|
198405
|
+
slot.unlisten = [];
|
|
198406
|
+
const subscription = slot.subscription;
|
|
198407
|
+
slot.subscription = null;
|
|
198408
|
+
if (subscription !== null) try {
|
|
198409
|
+
await subscription.close();
|
|
198410
|
+
} catch (err) {
|
|
198411
|
+
this.#logger.warn("Matter subscription close failed", {
|
|
198412
|
+
tags: this.#tags(slot.peer.nodeId),
|
|
198413
|
+
meta: {
|
|
198414
|
+
nodeId: slot.peer.nodeId,
|
|
198415
|
+
error: errMsg(err)
|
|
198416
|
+
}
|
|
198417
|
+
});
|
|
198418
|
+
}
|
|
198419
|
+
}
|
|
198420
|
+
#fresh(peer) {
|
|
198421
|
+
return {
|
|
198422
|
+
peer,
|
|
198423
|
+
phase: "resubscribing",
|
|
198424
|
+
attempt: 0,
|
|
198425
|
+
timer: null,
|
|
198426
|
+
subscription: null,
|
|
198427
|
+
unlisten: [],
|
|
198428
|
+
generation: 0,
|
|
198429
|
+
listening: false,
|
|
198430
|
+
dropping: false
|
|
198431
|
+
};
|
|
198432
|
+
}
|
|
198433
|
+
#tags(nodeId) {
|
|
198434
|
+
const deviceId = this.#resolveDeviceId(nodeId);
|
|
198435
|
+
if (deviceId !== null) return { deviceId };
|
|
198436
|
+
return { brokerId: this.#brokerId };
|
|
198437
|
+
}
|
|
198438
|
+
#log(nodeId, phase, extra) {
|
|
198439
|
+
const deviceId = this.#resolveDeviceId(nodeId);
|
|
198440
|
+
this.#logger.info("Matter subscription state", {
|
|
198441
|
+
tags: deviceId !== null ? { deviceId } : { brokerId: this.#brokerId },
|
|
198442
|
+
meta: {
|
|
198443
|
+
nodeId,
|
|
198444
|
+
phase,
|
|
198445
|
+
...extra
|
|
198446
|
+
}
|
|
198447
|
+
});
|
|
198448
|
+
}
|
|
198449
|
+
};
|
|
198450
|
+
function isRecord$2(value) {
|
|
198451
|
+
return typeof value === "object" && value !== null;
|
|
198452
|
+
}
|
|
198453
|
+
function asFiniteNumber(value) {
|
|
198454
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
198455
|
+
if (typeof value === "bigint") {
|
|
198456
|
+
const n = Number(value);
|
|
198457
|
+
return Number.isFinite(n) ? n : null;
|
|
198458
|
+
}
|
|
198459
|
+
return null;
|
|
198460
|
+
}
|
|
198461
|
+
function isIterable$1(value) {
|
|
198462
|
+
return isRecord$2(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
|
|
198463
|
+
}
|
|
198464
|
+
function isAsyncIterable(value) {
|
|
198465
|
+
return isRecord$2(value) && typeof Reflect.get(value, Symbol.asyncIterator) === "function";
|
|
198466
|
+
}
|
|
198467
|
+
/**
|
|
198468
|
+
* Pull attribute values out of a matter.js `ReadResult` (or a recorded fixture
|
|
198469
|
+
* of the same shape). Status reports and missing values are skipped — they are
|
|
198470
|
+
* not a measurement.
|
|
198471
|
+
*/
|
|
198472
|
+
async function collectAttributeReports(nodeId, update) {
|
|
198473
|
+
const out = [];
|
|
198474
|
+
const chunks = isAsyncIterable(update) ? update : isIterable$1(update) ? update : [];
|
|
198475
|
+
for await (const chunk of chunks) {
|
|
198476
|
+
const reports = isIterable$1(chunk) ? chunk : [];
|
|
198477
|
+
for (const report of reports) {
|
|
198478
|
+
const decoded = reportFromRecord(nodeId, report);
|
|
198479
|
+
if (decoded !== null) out.push(decoded);
|
|
198480
|
+
}
|
|
198481
|
+
}
|
|
198482
|
+
return out;
|
|
198483
|
+
}
|
|
198484
|
+
function reportFromRecord(nodeId, report) {
|
|
198485
|
+
if (!isRecord$2(report)) return null;
|
|
198486
|
+
if (report["kind"] !== "attr-value") return null;
|
|
198487
|
+
if (report["value"] === void 0 || report["value"] === null) return null;
|
|
198488
|
+
const path = report["path"];
|
|
198489
|
+
if (!isRecord$2(path)) return null;
|
|
198490
|
+
const endpointId = asFiniteNumber(path["endpointId"]);
|
|
198491
|
+
const clusterId = asFiniteNumber(path["clusterId"]);
|
|
198492
|
+
const attributeId = asFiniteNumber(path["attributeId"]);
|
|
198493
|
+
if (endpointId === null || clusterId === null || attributeId === null) return null;
|
|
198494
|
+
return {
|
|
198495
|
+
nodeId,
|
|
198496
|
+
endpointId,
|
|
198497
|
+
clusterId,
|
|
198498
|
+
attributeId,
|
|
198499
|
+
value: report["value"]
|
|
198500
|
+
};
|
|
198501
|
+
}
|
|
198502
|
+
function isRecord$1(value) {
|
|
198503
|
+
return typeof value === "object" && value !== null;
|
|
198504
|
+
}
|
|
198505
|
+
function peerOnline$1(peer) {
|
|
198506
|
+
const lifecycle = peer["lifecycle"];
|
|
198507
|
+
if (!isRecord$1(lifecycle)) return false;
|
|
198508
|
+
return lifecycle["isOnline"] === true;
|
|
198509
|
+
}
|
|
198510
|
+
function listenLifecycle(peer, event, handler) {
|
|
198511
|
+
const lifecycle = peer["lifecycle"];
|
|
198512
|
+
if (!isRecord$1(lifecycle)) return () => void 0;
|
|
198513
|
+
const observable = lifecycle[event];
|
|
198514
|
+
if (!isRecord$1(observable)) return () => void 0;
|
|
198515
|
+
const on = observable["on"];
|
|
198516
|
+
const off = observable["off"];
|
|
198517
|
+
if (typeof on !== "function" || typeof off !== "function") return () => void 0;
|
|
198518
|
+
Reflect.apply(on, observable, [handler]);
|
|
198519
|
+
return () => {
|
|
198520
|
+
Reflect.apply(off, observable, [handler]);
|
|
198521
|
+
};
|
|
198522
|
+
}
|
|
198523
|
+
/** Stop matter.js's own wildcard subscription so this addon owns the session. */
|
|
198524
|
+
function disableAutoSubscribe(peer) {
|
|
198525
|
+
const stateOf = peer["stateOf"];
|
|
198526
|
+
if (typeof stateOf !== "function") return;
|
|
198527
|
+
let network;
|
|
198528
|
+
try {
|
|
198529
|
+
network = Reflect.apply(stateOf, peer, ["network"]);
|
|
198530
|
+
} catch {
|
|
198531
|
+
return;
|
|
198532
|
+
}
|
|
198533
|
+
if (!isRecord$1(network)) return;
|
|
198534
|
+
if ("autoSubscribe" in network) network["autoSubscribe"] = false;
|
|
198535
|
+
}
|
|
198536
|
+
function subscriptionFrom(result) {
|
|
198537
|
+
return { close: async () => {
|
|
198538
|
+
if (!isRecord$1(result)) return;
|
|
198539
|
+
const fn = result["close"];
|
|
198540
|
+
if (typeof fn !== "function") return;
|
|
198541
|
+
await Promise.resolve(Reflect.apply(fn, result, []));
|
|
198542
|
+
} };
|
|
198543
|
+
}
|
|
198544
|
+
/**
|
|
198545
|
+
* Build a subscribable view of a commissioned peer, or null when the peer has
|
|
198546
|
+
* no `interaction.subscribe` (nothing to attach — the caller logs that).
|
|
198547
|
+
*/
|
|
198548
|
+
function subscribableFromPeer(peer, nodeId) {
|
|
198549
|
+
if (!isRecord$1(peer)) return null;
|
|
198550
|
+
const interaction = peer["interaction"];
|
|
198551
|
+
if (!isRecord$1(interaction) || typeof interaction["subscribe"] !== "function") return null;
|
|
198552
|
+
const subscribe = interaction["subscribe"];
|
|
198553
|
+
return {
|
|
198554
|
+
nodeId,
|
|
198555
|
+
online: () => peerOnline$1(peer),
|
|
198556
|
+
onOnline: (handler) => listenLifecycle(peer, "online", handler),
|
|
198557
|
+
onOffline: (handler) => listenLifecycle(peer, "offline", handler),
|
|
198558
|
+
subscribe: async (handlers) => {
|
|
198559
|
+
disableAutoSubscribe(peer);
|
|
198560
|
+
const request = {
|
|
198561
|
+
sustain: true,
|
|
198562
|
+
keepSubscriptions: false,
|
|
198563
|
+
fabricFilter: true,
|
|
198564
|
+
attributes: [{}],
|
|
198565
|
+
minIntervalFloor: require_esm.Seconds(0),
|
|
198566
|
+
maxIntervalCeiling: require_esm.Seconds(60),
|
|
198567
|
+
bootstrapWithRead: true,
|
|
198568
|
+
updated: async (update) => {
|
|
198569
|
+
const reports = await collectAttributeReports(nodeId, update);
|
|
198570
|
+
for (const report of reports) handlers.onReport(report);
|
|
198571
|
+
},
|
|
198572
|
+
closed: () => {
|
|
198573
|
+
handlers.onClosed();
|
|
198574
|
+
}
|
|
198575
|
+
};
|
|
198576
|
+
return subscriptionFrom(await Promise.resolve(Reflect.apply(subscribe, interaction, [request])));
|
|
198577
|
+
}
|
|
198578
|
+
};
|
|
198579
|
+
}
|
|
198580
|
+
//#endregion
|
|
198111
198581
|
//#region src/matter-controller-impl.ts
|
|
198112
198582
|
/**
|
|
198113
|
-
* Real {@link MatterController} backed by matter.js
|
|
198114
|
-
* `@matter/nodejs`).
|
|
198115
|
-
*
|
|
198116
|
-
*
|
|
198117
|
-
*
|
|
198118
|
-
*
|
|
198119
|
-
*
|
|
198120
|
-
*
|
|
198121
|
-
*
|
|
198122
|
-
*
|
|
198123
|
-
* - `
|
|
198124
|
-
* - `
|
|
198125
|
-
*
|
|
198126
|
-
* - `
|
|
198127
|
-
*
|
|
198128
|
-
*
|
|
198129
|
-
* - `clientNode.decommission()` — remove a node from the fabric.
|
|
198130
|
-
* - endpoint `commandsOf(behaviorId)[name](args)` — outbound cluster commands.
|
|
198131
|
-
*
|
|
198132
|
-
* The dynamic endpoint/cluster/attribute introspection lives in
|
|
198133
|
-
* `matter-endpoint-introspect.ts` (the single matter.js dynamic-shape boundary).
|
|
198134
|
-
* This class polls each node's endpoint state on reconcile + emits decoded
|
|
198135
|
-
* attribute reports, and re-emits on node online/offline transitions.
|
|
198136
|
-
*
|
|
198137
|
-
* BLE commissioning (`@matter/nodejs-ble`) is NOT wired — that package is not
|
|
198138
|
-
* installed. IP / on-network commissioning is the supported path; see the class
|
|
198139
|
-
* `commissionNode` note.
|
|
198583
|
+
* Real {@link MatterController} backed by matter.js (`@matter/main` +
|
|
198584
|
+
* `@matter/nodejs`). ServerNode / ClientNode is the API this controller uses.
|
|
198585
|
+
*
|
|
198586
|
+
* One `ServerNode` per broker (= one fabric). Storage is matter.js's own
|
|
198587
|
+
* files under the addon `dataDir` (`Environment` `storage.path`) — the
|
|
198588
|
+
* library-owned exception for fabric root / NOC / node registry, same class
|
|
198589
|
+
* as HAP pairing files. Not the keychain.
|
|
198590
|
+
*
|
|
198591
|
+
* Bound API:
|
|
198592
|
+
* - `ServerNode.create({ environment, id })` — fabric owner.
|
|
198593
|
+
* - `serverNode.peers.commission({ pairingCode, continueCommissioningAfterPase })`.
|
|
198594
|
+
* - `clientNode.interaction.subscribe({ sustain: true })` — attribute reports.
|
|
198595
|
+
* - `clientNode.lifecycle` online / offline — resubscribe.
|
|
198596
|
+
* - endpoint `commandsOf(behaviorId)[name](args)` — outbound commands.
|
|
198597
|
+
*
|
|
198598
|
+
* BLE commissioning is not wired. IP / on-network share codes only.
|
|
198140
198599
|
*/
|
|
198141
198600
|
/** A commissioning failure (bad code / device unreachable), distinct from runtime. */
|
|
198142
198601
|
var MatterCommissioningError = class extends Error {
|
|
@@ -198156,12 +198615,19 @@ var MatterJsController = class {
|
|
|
198156
198615
|
#options;
|
|
198157
198616
|
#logger;
|
|
198158
198617
|
#handlers = /* @__PURE__ */ new Set();
|
|
198618
|
+
#subscriptions;
|
|
198159
198619
|
#server = null;
|
|
198160
198620
|
/** Last emitted value per `node:endpoint:cluster:attribute` (change detection). */
|
|
198161
198621
|
#lastValues = /* @__PURE__ */ new Map();
|
|
198162
198622
|
constructor(options, logger) {
|
|
198163
198623
|
this.#options = options;
|
|
198164
198624
|
this.#logger = logger;
|
|
198625
|
+
this.#subscriptions = new MatterSubscriptionSupervisor({
|
|
198626
|
+
brokerId: options.brokerId,
|
|
198627
|
+
logger,
|
|
198628
|
+
resolveDeviceId: options.resolveDeviceId ?? (() => null),
|
|
198629
|
+
onReport: (report) => this.#emitIfChanged(report)
|
|
198630
|
+
});
|
|
198165
198631
|
}
|
|
198166
198632
|
get options() {
|
|
198167
198633
|
return this.#options;
|
|
@@ -198169,19 +198635,17 @@ var MatterJsController = class {
|
|
|
198169
198635
|
/** Boot the ServerNode fabric, loading/persisting state from `dataDir`. */
|
|
198170
198636
|
async start() {
|
|
198171
198637
|
if (this.#server !== null) return;
|
|
198172
|
-
const
|
|
198173
|
-
const server = await ServerNode.create({
|
|
198174
|
-
environment,
|
|
198175
|
-
id: this.#options.brokerId
|
|
198176
|
-
});
|
|
198638
|
+
const server = this.#options.openServer !== void 0 ? await this.#options.openServer() : await this.#openProductionServer();
|
|
198177
198639
|
this.#server = server;
|
|
198178
198640
|
await server.start();
|
|
198179
198641
|
this.#logger.info("MatterJsController: ServerNode online", { tags: { brokerId: this.#options.brokerId } });
|
|
198642
|
+
await this.syncSubscriptions();
|
|
198180
198643
|
this.#emitAllReports();
|
|
198181
198644
|
}
|
|
198182
|
-
/** Close
|
|
198645
|
+
/** Close subscriptions FIRST, then the ServerNode. */
|
|
198183
198646
|
async stop() {
|
|
198184
198647
|
this.#handlers.clear();
|
|
198648
|
+
await this.#subscriptions.stop();
|
|
198185
198649
|
const server = this.#server;
|
|
198186
198650
|
this.#server = null;
|
|
198187
198651
|
this.#lastValues.clear();
|
|
@@ -198202,19 +198666,49 @@ var MatterJsController = class {
|
|
|
198202
198666
|
*/
|
|
198203
198667
|
async commissionNode(pairingCode) {
|
|
198204
198668
|
const server = this.#requireServer("commissionNode");
|
|
198669
|
+
this.#progress({
|
|
198670
|
+
phase: CommissionPhase.Discovering,
|
|
198671
|
+
failure: null,
|
|
198672
|
+
message: null
|
|
198673
|
+
});
|
|
198205
198674
|
try {
|
|
198206
|
-
const node = await server.
|
|
198675
|
+
const node = await server.commission({
|
|
198676
|
+
pairingCode,
|
|
198677
|
+
onPase: () => {
|
|
198678
|
+
this.#progress({
|
|
198679
|
+
phase: CommissionPhase.Pase,
|
|
198680
|
+
failure: null,
|
|
198681
|
+
message: null
|
|
198682
|
+
});
|
|
198683
|
+
this.#progress({
|
|
198684
|
+
phase: CommissionPhase.Commissioning,
|
|
198685
|
+
failure: null,
|
|
198686
|
+
message: null
|
|
198687
|
+
});
|
|
198688
|
+
}
|
|
198689
|
+
});
|
|
198207
198690
|
const view = this.#viewPeer(node);
|
|
198208
198691
|
this.#logger.info("MatterJsController: node commissioned", {
|
|
198209
198692
|
tags: { brokerId: this.#options.brokerId },
|
|
198210
198693
|
meta: { nodeId: view?.id ?? "(unknown)" }
|
|
198211
198694
|
});
|
|
198695
|
+
await this.syncSubscriptions();
|
|
198212
198696
|
this.#emitAllReports();
|
|
198213
198697
|
if (view === null) throw new MatterCommissioningError("commissioned node exposed no operational id");
|
|
198698
|
+
this.#progress({
|
|
198699
|
+
phase: CommissionPhase.Done,
|
|
198700
|
+
failure: null,
|
|
198701
|
+
message: null
|
|
198702
|
+
});
|
|
198214
198703
|
return { nodeId: view.id };
|
|
198215
198704
|
} catch (err) {
|
|
198216
|
-
if (err instanceof MatterCommissioningError
|
|
198217
|
-
|
|
198705
|
+
if (err instanceof MatterCommissioningError && err.message.includes("no operational id")) {
|
|
198706
|
+
this.#failCommission(err.message);
|
|
198707
|
+
throw err;
|
|
198708
|
+
}
|
|
198709
|
+
const message = errMsg(err);
|
|
198710
|
+
this.#failCommission(message);
|
|
198711
|
+
throw new MatterCommissioningError(commissionFailureMessage(classifyCommissionError(message), message), { cause: err });
|
|
198218
198712
|
}
|
|
198219
198713
|
}
|
|
198220
198714
|
/** Pull the live commissioned-node inventory off `serverNode.peers`. */
|
|
@@ -198222,12 +198716,31 @@ var MatterJsController = class {
|
|
|
198222
198716
|
const server = this.#server;
|
|
198223
198717
|
if (server === null) return [];
|
|
198224
198718
|
const out = [];
|
|
198225
|
-
for (const peer of
|
|
198719
|
+
for (const peer of server.listPeers()) {
|
|
198226
198720
|
const snapshot = this.#snapshot(peer);
|
|
198227
198721
|
if (snapshot !== null) out.push(snapshot);
|
|
198228
198722
|
}
|
|
198229
198723
|
return out;
|
|
198230
198724
|
}
|
|
198725
|
+
async syncSubscriptions() {
|
|
198726
|
+
const server = this.#server;
|
|
198727
|
+
if (server === null) return;
|
|
198728
|
+
const peers = [];
|
|
198729
|
+
for (const peer of server.listPeers()) {
|
|
198730
|
+
const view = this.#viewPeer(peer);
|
|
198731
|
+
if (view === null) continue;
|
|
198732
|
+
const subscribable = subscribableFromPeer(peer, view.id);
|
|
198733
|
+
if (subscribable === null) {
|
|
198734
|
+
this.#logger.warn("MatterJsController: peer has no attribute subscription", {
|
|
198735
|
+
tags: { brokerId: this.#options.brokerId },
|
|
198736
|
+
meta: { nodeId: view.id }
|
|
198737
|
+
});
|
|
198738
|
+
continue;
|
|
198739
|
+
}
|
|
198740
|
+
peers.push(subscribable);
|
|
198741
|
+
}
|
|
198742
|
+
await this.#subscriptions.sync(peers);
|
|
198743
|
+
}
|
|
198231
198744
|
async getNode(nodeId) {
|
|
198232
198745
|
return (await this.listNodes()).find((n) => n.nodeId === nodeId) ?? null;
|
|
198233
198746
|
}
|
|
@@ -198268,6 +198781,25 @@ var MatterJsController = class {
|
|
|
198268
198781
|
throw err;
|
|
198269
198782
|
}
|
|
198270
198783
|
}
|
|
198784
|
+
async #openProductionServer() {
|
|
198785
|
+
const environment = this.#buildEnvironment();
|
|
198786
|
+
const server = await ServerNode.create({
|
|
198787
|
+
environment,
|
|
198788
|
+
id: this.#options.brokerId
|
|
198789
|
+
});
|
|
198790
|
+
return {
|
|
198791
|
+
start: () => server.start(),
|
|
198792
|
+
close: () => server.close(),
|
|
198793
|
+
listPeers: () => this.#serverPeers(server),
|
|
198794
|
+
commission: ({ pairingCode, onPase }) => server.peers.commission({
|
|
198795
|
+
pairingCode,
|
|
198796
|
+
continueCommissioningAfterPase: () => {
|
|
198797
|
+
onPase();
|
|
198798
|
+
return true;
|
|
198799
|
+
}
|
|
198800
|
+
})
|
|
198801
|
+
};
|
|
198802
|
+
}
|
|
198271
198803
|
#buildEnvironment() {
|
|
198272
198804
|
const env = require_esm.Environment.default;
|
|
198273
198805
|
env.vars.set("storage.path", this.#options.dataDir);
|
|
@@ -198280,13 +198812,13 @@ var MatterJsController = class {
|
|
|
198280
198812
|
return this.#server;
|
|
198281
198813
|
}
|
|
198282
198814
|
/** Iterate the commissioned peers of a ServerNode (cast-free). */
|
|
198283
|
-
#
|
|
198815
|
+
#serverPeers(server) {
|
|
198284
198816
|
const peers = server.peers;
|
|
198285
198817
|
if (isIterable(peers)) return peers;
|
|
198286
198818
|
return [];
|
|
198287
198819
|
}
|
|
198288
198820
|
#findPeer(server, nodeId) {
|
|
198289
|
-
for (const peer of
|
|
198821
|
+
for (const peer of server.listPeers()) {
|
|
198290
198822
|
const view = this.#viewPeer(peer);
|
|
198291
198823
|
if (view !== null && view.id === nodeId) return view;
|
|
198292
198824
|
}
|
|
@@ -198294,12 +198826,23 @@ var MatterJsController = class {
|
|
|
198294
198826
|
}
|
|
198295
198827
|
/** Raw (un-viewed) peer object for operations that call its methods. */
|
|
198296
198828
|
#rawPeer(server, nodeId) {
|
|
198297
|
-
for (const peer of
|
|
198829
|
+
for (const peer of server.listPeers()) {
|
|
198298
198830
|
const view = this.#viewPeer(peer);
|
|
198299
198831
|
if (view !== null && view.id === nodeId) return peer;
|
|
198300
198832
|
}
|
|
198301
198833
|
return null;
|
|
198302
198834
|
}
|
|
198835
|
+
#progress(progress) {
|
|
198836
|
+
this.#options.onCommissionProgress?.(progress);
|
|
198837
|
+
}
|
|
198838
|
+
#failCommission(message) {
|
|
198839
|
+
const failure = classifyCommissionError(message);
|
|
198840
|
+
this.#progress({
|
|
198841
|
+
phase: CommissionPhase.Failed,
|
|
198842
|
+
failure,
|
|
198843
|
+
message: commissionFailureMessage(failure, message)
|
|
198844
|
+
});
|
|
198845
|
+
}
|
|
198303
198846
|
/** Reduce a peer to the read-only view the addon models. */
|
|
198304
198847
|
#viewPeer(peer) {
|
|
198305
198848
|
if (!isRecord(peer)) return null;
|
|
@@ -198343,7 +198886,7 @@ var MatterJsController = class {
|
|
|
198343
198886
|
#emitAllReports() {
|
|
198344
198887
|
const server = this.#server;
|
|
198345
198888
|
if (server === null) return;
|
|
198346
|
-
for (const peer of
|
|
198889
|
+
for (const peer of server.listPeers()) {
|
|
198347
198890
|
const view = this.#viewPeer(peer);
|
|
198348
198891
|
if (view === null) continue;
|
|
198349
198892
|
for (const ep of readNodeEndpoints(view.rootEndpoint)) for (const attr of ep.attributes) {
|
|
@@ -198359,6 +198902,7 @@ var MatterJsController = class {
|
|
|
198359
198902
|
}
|
|
198360
198903
|
}
|
|
198361
198904
|
#emitIfChanged(report) {
|
|
198905
|
+
if (report.value === void 0 || report.value === null) return;
|
|
198362
198906
|
const key = `${report.nodeId}:${report.endpointId}:${report.clusterId}:${report.attributeId}`;
|
|
198363
198907
|
const prev = this.#lastValues.get(key);
|
|
198364
198908
|
if (prev !== void 0 && shallowEqual(prev, report.value)) return;
|
|
@@ -198444,12 +198988,24 @@ var MatterNodeResolver = class {
|
|
|
198444
198988
|
#attrs = /* @__PURE__ */ new Map();
|
|
198445
198989
|
/** Live attribute-report subscribers (endpoint device classes). */
|
|
198446
198990
|
#listeners = /* @__PURE__ */ new Set();
|
|
198991
|
+
/** Adopted container id per Matter node, for subscription logs. */
|
|
198992
|
+
#deviceIds = /* @__PURE__ */ new Map();
|
|
198447
198993
|
/**
|
|
198448
198994
|
* Subscribe to live attribute reports across all brokers. Endpoint device
|
|
198449
198995
|
* classes subscribe on activate (filtering to their own node/endpoint) so a
|
|
198450
198996
|
* push report decodes straight into their runtime-state slice. Returns a
|
|
198451
198997
|
* disposer.
|
|
198452
198998
|
*/
|
|
198999
|
+
/** Remember the CamStack container id for a commissioned node. */
|
|
199000
|
+
bindDevice(nodeId, deviceId) {
|
|
199001
|
+
this.#deviceIds.set(nodeId, deviceId);
|
|
199002
|
+
}
|
|
199003
|
+
unbindDevice(nodeId) {
|
|
199004
|
+
this.#deviceIds.delete(nodeId);
|
|
199005
|
+
}
|
|
199006
|
+
deviceIdFor(nodeId) {
|
|
199007
|
+
return this.#deviceIds.get(nodeId) ?? null;
|
|
199008
|
+
}
|
|
198453
199009
|
onAttributeReport(listener) {
|
|
198454
199010
|
this.#listeners.add(listener);
|
|
198455
199011
|
return () => {
|
|
@@ -198511,6 +199067,7 @@ var MatterNodeResolver = class {
|
|
|
198511
199067
|
this.#nodes.clear();
|
|
198512
199068
|
this.#attrs.clear();
|
|
198513
199069
|
this.#listeners.clear();
|
|
199070
|
+
this.#deviceIds.clear();
|
|
198514
199071
|
}
|
|
198515
199072
|
};
|
|
198516
199073
|
/** The single in-process per-broker node resolver shared across the addon. */
|
|
@@ -198570,6 +199127,7 @@ var MatterIntegrationManager = class {
|
|
|
198570
199127
|
#nodeCount = 0;
|
|
198571
199128
|
#lastCheckedAt = null;
|
|
198572
199129
|
#error = null;
|
|
199130
|
+
#commission = IDLE_COMMISSION_PROGRESS;
|
|
198573
199131
|
constructor(options) {
|
|
198574
199132
|
this.#id = options.id;
|
|
198575
199133
|
this.#name = options.name;
|
|
@@ -198580,7 +199138,17 @@ var MatterIntegrationManager = class {
|
|
|
198580
199138
|
this.#onDisconnected = options.onDisconnected;
|
|
198581
199139
|
this.#onNodesChanged = options.onNodesChanged;
|
|
198582
199140
|
this.#onAttributeReport = options.onAttributeReport;
|
|
198583
|
-
this.#makeController = options.makeController ?? ((opts) => new MatterJsController(
|
|
199141
|
+
this.#makeController = options.makeController ?? ((opts) => new MatterJsController({
|
|
199142
|
+
...opts,
|
|
199143
|
+
onCommissionProgress: (progress) => {
|
|
199144
|
+
this.#commission = progress;
|
|
199145
|
+
},
|
|
199146
|
+
resolveDeviceId: (nodeId) => matterNodes.deviceIdFor(nodeId)
|
|
199147
|
+
}, this.#logger));
|
|
199148
|
+
}
|
|
199149
|
+
/** Latest commissioning phase for `broker.getState({ key: 'commission' })`. */
|
|
199150
|
+
getCommissionProgress() {
|
|
199151
|
+
return this.#commission;
|
|
198584
199152
|
}
|
|
198585
199153
|
/** The connection settings the manager is currently configured with. */
|
|
198586
199154
|
getConnection() {
|
|
@@ -198655,6 +199223,7 @@ var MatterIntegrationManager = class {
|
|
|
198655
199223
|
if (controller === null) return;
|
|
198656
199224
|
try {
|
|
198657
199225
|
const nodes = await controller.listNodes();
|
|
199226
|
+
await controller.syncSubscriptions();
|
|
198658
199227
|
this.#nodeCount = nodes.length;
|
|
198659
199228
|
this.#lastCheckedAt = Date.now();
|
|
198660
199229
|
matterNodes.setNodes(this.#id, nodes);
|
|
@@ -198835,6 +199404,10 @@ var MatterBrokerRegistry = class {
|
|
|
198835
199404
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
198836
199405
|
return mgr.commissionNode(pairingCode);
|
|
198837
199406
|
}
|
|
199407
|
+
getCommissionProgress(brokerId) {
|
|
199408
|
+
const mgr = this.#managers.get(brokerId);
|
|
199409
|
+
return mgr ? mgr.getCommissionProgress() : null;
|
|
199410
|
+
}
|
|
198838
199411
|
async removeNode(brokerId, nodeId) {
|
|
198839
199412
|
const mgr = this.#managers.get(brokerId);
|
|
198840
199413
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
@@ -198937,6 +199510,26 @@ function toRecord(value) {
|
|
|
198937
199510
|
function settingsToMatterConnection(settings) {
|
|
198938
199511
|
return matterConnectionSchema.parse(toRecord(settings));
|
|
198939
199512
|
}
|
|
199513
|
+
/**
|
|
199514
|
+
* Settings readers see an empty string for both secrets. The stored value
|
|
199515
|
+
* stays in the keychain (records `secret` meta). An empty round-trip must
|
|
199516
|
+
* not wipe it — see {@link mergeMatterSecrets}.
|
|
199517
|
+
*/
|
|
199518
|
+
function publicMatterConnection(connection) {
|
|
199519
|
+
return {
|
|
199520
|
+
...connection,
|
|
199521
|
+
wifiPassword: "",
|
|
199522
|
+
threadDataset: ""
|
|
199523
|
+
};
|
|
199524
|
+
}
|
|
199525
|
+
/** Keep a stored secret when the form sent back the redacted empty string. */
|
|
199526
|
+
function mergeMatterSecrets(existing, incoming) {
|
|
199527
|
+
return {
|
|
199528
|
+
...incoming,
|
|
199529
|
+
wifiPassword: incoming.wifiPassword.length > 0 ? incoming.wifiPassword : existing.wifiPassword,
|
|
199530
|
+
threadDataset: incoming.threadDataset.length > 0 ? incoming.threadDataset : existing.threadDataset
|
|
199531
|
+
};
|
|
199532
|
+
}
|
|
198940
199533
|
/** Hand-written connection form for the broker/integration creation UI. */
|
|
198941
199534
|
function buildConnectionFormSchema() {
|
|
198942
199535
|
return { sections: [{
|
|
@@ -198948,9 +199541,10 @@ function buildConnectionFormSchema() {
|
|
|
198948
199541
|
columns: 1,
|
|
198949
199542
|
fields: [
|
|
198950
199543
|
{
|
|
198951
|
-
type: "
|
|
199544
|
+
type: "password",
|
|
198952
199545
|
key: "threadDataset",
|
|
198953
199546
|
secret: true,
|
|
199547
|
+
showToggle: true,
|
|
198954
199548
|
label: "Thread operational dataset (hex, optional)",
|
|
198955
199549
|
labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
|
|
198956
199550
|
placeholder: "0e08000000000001…"
|
|
@@ -199027,17 +199621,14 @@ var MATTER_KIND = "matter";
|
|
|
199027
199621
|
* Construct the `broker` cap provider for the Matter addon. Pure builder.
|
|
199028
199622
|
* Mirrors `buildEcowittBrokerProvider`. The defining Matter operation —
|
|
199029
199623
|
* commissioning — is surfaced through `publish` with
|
|
199030
|
-
* `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
|
|
199031
|
-
*
|
|
199032
|
-
* preserved on an empty round-trip.
|
|
199624
|
+
* `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
|
|
199625
|
+
* `threadDataset` and `wifiPassword` are redacted on settings reads and
|
|
199626
|
+
* preserved on an empty round-trip. Fabric keys stay in matter.js storage.
|
|
199033
199627
|
*/
|
|
199034
199628
|
function buildMatterBrokerProvider(deps) {
|
|
199035
199629
|
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
199036
199630
|
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
199037
|
-
const redact = (entry) => (
|
|
199038
|
-
...entry.connection,
|
|
199039
|
-
wifiPassword: ""
|
|
199040
|
-
});
|
|
199631
|
+
const redact = (entry) => publicMatterConnection(entry.connection);
|
|
199041
199632
|
return {
|
|
199042
199633
|
list: async ({ kind }) => {
|
|
199043
199634
|
if (kind && kind !== MATTER_KIND) return [];
|
|
@@ -199105,10 +199696,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199105
199696
|
const existing = getBrokers().find((b) => b.id === id);
|
|
199106
199697
|
if (!existing) return;
|
|
199107
199698
|
const parsed = settingsToMatterConnection(settings);
|
|
199108
|
-
const merged =
|
|
199109
|
-
...parsed,
|
|
199110
|
-
wifiPassword: parsed.wifiPassword.length > 0 ? parsed.wifiPassword : existing.connection.wifiPassword
|
|
199111
|
-
};
|
|
199699
|
+
const merged = mergeMatterSecrets(existing.connection, parsed);
|
|
199112
199700
|
const updated = await registry.updateEntry(id, merged);
|
|
199113
199701
|
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
199114
199702
|
},
|
|
@@ -199144,7 +199732,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199144
199732
|
if (!owns(brokerId)) throw new Error(`matter: unknown broker ${brokerId}`);
|
|
199145
199733
|
const action = typeof target["action"] === "string" ? target["action"] : void 0;
|
|
199146
199734
|
if (action === "commissionNode") {
|
|
199147
|
-
const code =
|
|
199735
|
+
const code = pairingCodeFromPayload(payload);
|
|
199148
199736
|
parsePairingCode(code);
|
|
199149
199737
|
return registry.commissionNode(brokerId, code);
|
|
199150
199738
|
}
|
|
@@ -199152,13 +199740,23 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199152
199740
|
},
|
|
199153
199741
|
subscribe: async () => ({ subscriptionId: "" }),
|
|
199154
199742
|
unsubscribe: async () => void 0,
|
|
199155
|
-
getState: async () =>
|
|
199743
|
+
getState: async ({ brokerId, key }) => {
|
|
199744
|
+
if (key !== "commission") return null;
|
|
199745
|
+
return registry.getCommissionProgress(brokerId);
|
|
199746
|
+
},
|
|
199156
199747
|
getStatus: async () => ({
|
|
199157
199748
|
brokerCount: registry.size(),
|
|
199158
199749
|
connectedCount: registry.connectedCount()
|
|
199159
199750
|
})
|
|
199160
199751
|
};
|
|
199161
199752
|
}
|
|
199753
|
+
function pairingCodeFromPayload(payload) {
|
|
199754
|
+
if (typeof payload !== "object" || payload === null || !("pairingCode" in payload)) return "";
|
|
199755
|
+
const code = payload.pairingCode;
|
|
199756
|
+
if (typeof code === "string") return code;
|
|
199757
|
+
if (typeof code === "number" && Number.isFinite(code)) return String(code);
|
|
199758
|
+
return "";
|
|
199759
|
+
}
|
|
199162
199760
|
//#endregion
|
|
199163
199761
|
//#region src/matter-discovery.ts
|
|
199164
199762
|
/**
|
|
@@ -199389,6 +199987,27 @@ function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
|
199389
199987
|
return count;
|
|
199390
199988
|
}
|
|
199391
199989
|
//#endregion
|
|
199990
|
+
//#region src/matter-lock-command.ts
|
|
199991
|
+
/**
|
|
199992
|
+
* Decide the command arguments. `pinRequired` comes from the live
|
|
199993
|
+
* RequirePinForRemoteOperation attribute, not from a stored secret.
|
|
199994
|
+
*/
|
|
199995
|
+
function planDoorLockCommand(input) {
|
|
199996
|
+
const code = input.code?.trim() ?? "";
|
|
199997
|
+
if (input.pinRequired && code.length === 0) return {
|
|
199998
|
+
ok: false,
|
|
199999
|
+
reason: "pin-required"
|
|
200000
|
+
};
|
|
200001
|
+
if (code.length > 0) return {
|
|
200002
|
+
ok: true,
|
|
200003
|
+
args: { pinCode: code }
|
|
200004
|
+
};
|
|
200005
|
+
return {
|
|
200006
|
+
ok: true,
|
|
200007
|
+
args: {}
|
|
200008
|
+
};
|
|
200009
|
+
}
|
|
200010
|
+
//#endregion
|
|
199392
200011
|
//#region src/devices/matter-endpoint-device.ts
|
|
199393
200012
|
/**
|
|
199394
200013
|
* Persisted config every Matter endpoint child carries. `nodeId`/`endpointId`
|
|
@@ -199480,7 +200099,8 @@ var COLD_POWER = { lastFetchedAt: 0 };
|
|
|
199480
200099
|
* arrives, each cap reads its cold-start default.
|
|
199481
200100
|
*/
|
|
199482
200101
|
var MatterEndpointDevice = class extends BaseDevice {
|
|
199483
|
-
|
|
200102
|
+
/** Live RequirePinForRemoteOperation. Commands read this, not a stored PIN. */
|
|
200103
|
+
pinRequired = false;
|
|
199484
200104
|
brokerId;
|
|
199485
200105
|
nodeId;
|
|
199486
200106
|
endpointId;
|
|
@@ -199494,12 +200114,17 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199494
200114
|
this.nodeId = persisted.nodeId;
|
|
199495
200115
|
this.endpointId = persisted.endpointId;
|
|
199496
200116
|
this.caps = persisted.capabilities.filter(isKnownCap);
|
|
200117
|
+
this.pinRequired = matterNodes.getAttribute(this.brokerId, this.nodeId, this.endpointId, MatterClusterId.DoorLock, MatterAttributeId.RequirePinForRemoteOperation) === true;
|
|
199497
200118
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? true;
|
|
199498
200119
|
this.updateSourceInfo({
|
|
199499
200120
|
id: `${this.nodeId}:${this.endpointId}`,
|
|
199500
200121
|
system: "matter"
|
|
199501
200122
|
});
|
|
199502
200123
|
}
|
|
200124
|
+
/** PIN-required locks advertise the feature the lock UI already understands. */
|
|
200125
|
+
get features() {
|
|
200126
|
+
return this.pinRequired ? [DeviceFeature.LockPinRequired] : [];
|
|
200127
|
+
}
|
|
199503
200128
|
async onActivate() {
|
|
199504
200129
|
await super.onActivate();
|
|
199505
200130
|
for (const cap of this.caps) {
|
|
@@ -199527,7 +200152,29 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199527
200152
|
value: report.value
|
|
199528
200153
|
});
|
|
199529
200154
|
if (decoded === null) return;
|
|
200155
|
+
if (decoded.detail === "pin-required" || decoded.detail === "pin-not-required") {
|
|
200156
|
+
const next = decoded.detail === "pin-required";
|
|
200157
|
+
if (next !== this.pinRequired) {
|
|
200158
|
+
this.pinRequired = next;
|
|
200159
|
+
this.ctx.logger.info("Matter lock PIN requirement changed", {
|
|
200160
|
+
tags: { deviceId: this.id },
|
|
200161
|
+
meta: {
|
|
200162
|
+
nodeId: this.nodeId,
|
|
200163
|
+
endpointId: this.endpointId,
|
|
200164
|
+
pinRequired: next
|
|
200165
|
+
}
|
|
200166
|
+
});
|
|
200167
|
+
}
|
|
200168
|
+
} else if (decoded.detail !== void 0) this.ctx.logger.info("Matter lock detail", {
|
|
200169
|
+
tags: { deviceId: this.id },
|
|
200170
|
+
meta: {
|
|
200171
|
+
nodeId: this.nodeId,
|
|
200172
|
+
endpointId: this.endpointId,
|
|
200173
|
+
detail: decoded.detail
|
|
200174
|
+
}
|
|
200175
|
+
});
|
|
199530
200176
|
if (!this.registeredCaps.has(decoded.cap)) return;
|
|
200177
|
+
if (Object.keys(decoded.patch).length === 0) return;
|
|
199531
200178
|
this.runtimeState.patchCapState(CAP_SLICE_NAME[decoded.cap], decoded.patch);
|
|
199532
200179
|
}
|
|
199533
200180
|
/** Invoke a cluster command on this endpoint via the live controller. */
|
|
@@ -199623,30 +200270,52 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199623
200270
|
};
|
|
199624
200271
|
this.ctx.registerNativeCap(lockControlCapability, {
|
|
199625
200272
|
getStatus: async () => this.runtimeState.getCapState("lock-control") ?? COLD,
|
|
199626
|
-
lock: async () => {
|
|
199627
|
-
await this.
|
|
199628
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199629
|
-
state: "locked",
|
|
199630
|
-
lastChangedAt: Date.now()
|
|
199631
|
-
});
|
|
200273
|
+
lock: async ({ code }) => {
|
|
200274
|
+
await this.invokeLock("lockDoor", "locking", code);
|
|
199632
200275
|
},
|
|
199633
|
-
unlock: async () => {
|
|
199634
|
-
await this.
|
|
199635
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199636
|
-
state: "unlocked",
|
|
199637
|
-
lastChangedAt: Date.now()
|
|
199638
|
-
});
|
|
200276
|
+
unlock: async ({ code }) => {
|
|
200277
|
+
await this.invokeLock("unlockDoor", "unlocking", code);
|
|
199639
200278
|
},
|
|
199640
200279
|
open: async () => {
|
|
199641
|
-
await this.
|
|
199642
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199643
|
-
state: "unlocked",
|
|
199644
|
-
lastChangedAt: Date.now()
|
|
199645
|
-
});
|
|
200280
|
+
await this.invokeLock("unlockDoor", "unlocking", void 0);
|
|
199646
200281
|
}
|
|
199647
200282
|
});
|
|
199648
200283
|
this.runtimeState.setCapState("lock-control", COLD);
|
|
199649
200284
|
}
|
|
200285
|
+
/**
|
|
200286
|
+
* Send lockDoor / unlockDoor. The final locked/unlocked state arrives on the
|
|
200287
|
+
* attribute subscription — this method only records the in-flight transition.
|
|
200288
|
+
* A PIN is forwarded on the command and nowhere else.
|
|
200289
|
+
*/
|
|
200290
|
+
async invokeLock(commandName, pending, code) {
|
|
200291
|
+
const plan = planDoorLockCommand({
|
|
200292
|
+
pinRequired: this.pinRequired,
|
|
200293
|
+
code
|
|
200294
|
+
});
|
|
200295
|
+
if (!plan.ok) {
|
|
200296
|
+
this.ctx.logger.warn("Matter lock command refused: PIN required and none was provided", {
|
|
200297
|
+
tags: { deviceId: this.id },
|
|
200298
|
+
meta: {
|
|
200299
|
+
nodeId: this.nodeId,
|
|
200300
|
+
endpointId: this.endpointId,
|
|
200301
|
+
commandName
|
|
200302
|
+
}
|
|
200303
|
+
});
|
|
200304
|
+
throw new Error("matter-broker: this lock requires a PIN for remote operation");
|
|
200305
|
+
}
|
|
200306
|
+
const previous = this.runtimeState.getCapState("lock-control");
|
|
200307
|
+
this.runtimeState.setCapState("lock-control", {
|
|
200308
|
+
state: pending,
|
|
200309
|
+
lastChangedAt: Date.now()
|
|
200310
|
+
});
|
|
200311
|
+
try {
|
|
200312
|
+
const args = "pinCode" in plan.args ? plan.args : void 0;
|
|
200313
|
+
await this.invoke(MatterClusterId.DoorLock, commandName, args);
|
|
200314
|
+
} catch (err) {
|
|
200315
|
+
if (previous !== void 0) this.runtimeState.setCapState("lock-control", previous);
|
|
200316
|
+
throw err;
|
|
200317
|
+
}
|
|
200318
|
+
}
|
|
199650
200319
|
registerCover() {
|
|
199651
200320
|
const COLD = {
|
|
199652
200321
|
state: "closed",
|
|
@@ -199867,11 +200536,16 @@ var MatterNodeDevice = class extends BaseDevice {
|
|
|
199867
200536
|
this.nodeId = cfg.nodeId;
|
|
199868
200537
|
this.integrationId = cfg.integrationId;
|
|
199869
200538
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? false;
|
|
200539
|
+
matterNodes.bindDevice(this.nodeId, this.id);
|
|
199870
200540
|
this.updateSourceInfo({
|
|
199871
200541
|
id: this.nodeId,
|
|
199872
200542
|
system: "matter"
|
|
199873
200543
|
});
|
|
199874
200544
|
}
|
|
200545
|
+
async removeDevice() {
|
|
200546
|
+
matterNodes.unbindDevice(this.nodeId);
|
|
200547
|
+
await super.removeDevice();
|
|
200548
|
+
}
|
|
199875
200549
|
getAccessoryChildren() {
|
|
199876
200550
|
const specs = [];
|
|
199877
200551
|
for (const endpoint of this.config.values.endpoints) {
|