@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.mjs
CHANGED
|
@@ -6602,7 +6602,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
6602
6602
|
this.name = "SecretOverwriteRefusedError";
|
|
6603
6603
|
}
|
|
6604
6604
|
};
|
|
6605
|
-
function isRecord$4(value) {
|
|
6605
|
+
function isRecord$4$1(value) {
|
|
6606
6606
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6607
6607
|
}
|
|
6608
6608
|
function resolvedValue(pass, marker) {
|
|
@@ -6631,7 +6631,7 @@ function inArray(pass, incoming, stored) {
|
|
|
6631
6631
|
return incoming.map((item, index) => {
|
|
6632
6632
|
const twin = stored[index];
|
|
6633
6633
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
6634
|
-
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);
|
|
6635
6635
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
6636
6636
|
return item;
|
|
6637
6637
|
});
|
|
@@ -6642,7 +6642,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6642
6642
|
const twin = stored[key];
|
|
6643
6643
|
if (isSecretMarker(twin)) {
|
|
6644
6644
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
6645
|
-
} 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);
|
|
6646
6646
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
6647
6647
|
}
|
|
6648
6648
|
return out;
|
|
@@ -6650,7 +6650,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
6650
6650
|
async function walk(pass, incoming, stored) {
|
|
6651
6651
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
6652
6652
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
6653
|
-
if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
|
|
6653
|
+
if (!isRecord$4$1(incoming) || !isRecord$4$1(stored)) return incoming;
|
|
6654
6654
|
const out = { ...incoming };
|
|
6655
6655
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
6656
6656
|
return out;
|
|
@@ -6681,7 +6681,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
6681
6681
|
function isJsonEqual(a, b) {
|
|
6682
6682
|
if (Object.is(a, b)) return true;
|
|
6683
6683
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
6684
|
-
if (isRecord$4(a) && isRecord$4(b)) {
|
|
6684
|
+
if (isRecord$4$1(a) && isRecord$4$1(b)) {
|
|
6685
6685
|
const keys = Object.keys(a);
|
|
6686
6686
|
if (keys.length !== Object.keys(b).length) return false;
|
|
6687
6687
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -6705,7 +6705,7 @@ function markerKeysOf(blob, owner) {
|
|
|
6705
6705
|
}
|
|
6706
6706
|
function nodeFromStored(value, owner) {
|
|
6707
6707
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
6708
|
-
if (!isRecord$4(value)) return null;
|
|
6708
|
+
if (!isRecord$4$1(value)) return null;
|
|
6709
6709
|
const children = /* @__PURE__ */ new Map();
|
|
6710
6710
|
for (const [key, child] of Object.entries(value)) {
|
|
6711
6711
|
const node = nodeFromStored(child, owner);
|
|
@@ -11256,7 +11256,7 @@ var ExpressionEvalError = class extends Error {
|
|
|
11256
11256
|
this.name = "ExpressionEvalError";
|
|
11257
11257
|
}
|
|
11258
11258
|
};
|
|
11259
|
-
function asFiniteNumber(value, name, index) {
|
|
11259
|
+
function asFiniteNumber$1(value, name, index) {
|
|
11260
11260
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
11261
11261
|
return value;
|
|
11262
11262
|
}
|
|
@@ -11269,7 +11269,7 @@ function finiteResult(value, name) {
|
|
|
11269
11269
|
return value;
|
|
11270
11270
|
}
|
|
11271
11271
|
function allFiniteNumbers(args, name) {
|
|
11272
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
11272
|
+
return args.map((a, idx) => asFiniteNumber$1(a, name, idx));
|
|
11273
11273
|
}
|
|
11274
11274
|
function asBoolean$2(value, name, index) {
|
|
11275
11275
|
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
@@ -11306,29 +11306,29 @@ var table = {
|
|
|
11306
11306
|
abs: {
|
|
11307
11307
|
minArgs: 1,
|
|
11308
11308
|
maxArgs: 1,
|
|
11309
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
11309
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber$1(args[0], "abs", 0)), "abs")
|
|
11310
11310
|
},
|
|
11311
11311
|
floor: {
|
|
11312
11312
|
minArgs: 1,
|
|
11313
11313
|
maxArgs: 1,
|
|
11314
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
11314
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber$1(args[0], "floor", 0)), "floor")
|
|
11315
11315
|
},
|
|
11316
11316
|
ceil: {
|
|
11317
11317
|
minArgs: 1,
|
|
11318
11318
|
maxArgs: 1,
|
|
11319
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
11319
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber$1(args[0], "ceil", 0)), "ceil")
|
|
11320
11320
|
},
|
|
11321
11321
|
sqrt: {
|
|
11322
11322
|
minArgs: 1,
|
|
11323
11323
|
maxArgs: 1,
|
|
11324
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
11324
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber$1(args[0], "sqrt", 0)), "sqrt")
|
|
11325
11325
|
},
|
|
11326
11326
|
round: {
|
|
11327
11327
|
minArgs: 1,
|
|
11328
11328
|
maxArgs: 2,
|
|
11329
11329
|
apply: (args) => {
|
|
11330
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
11331
|
-
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;
|
|
11332
11332
|
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
11333
11333
|
const factor = 10 ** digits;
|
|
11334
11334
|
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
@@ -11337,15 +11337,15 @@ var table = {
|
|
|
11337
11337
|
pow: {
|
|
11338
11338
|
minArgs: 2,
|
|
11339
11339
|
maxArgs: 2,
|
|
11340
|
-
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")
|
|
11341
11341
|
},
|
|
11342
11342
|
clamp: {
|
|
11343
11343
|
minArgs: 3,
|
|
11344
11344
|
maxArgs: 3,
|
|
11345
11345
|
apply: (args) => {
|
|
11346
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
11347
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
11348
|
-
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);
|
|
11349
11349
|
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
11350
11350
|
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
11351
11351
|
}
|
|
@@ -11374,13 +11374,13 @@ var table = {
|
|
|
11374
11374
|
age: {
|
|
11375
11375
|
minArgs: 2,
|
|
11376
11376
|
maxArgs: 2,
|
|
11377
|
-
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")
|
|
11378
11378
|
},
|
|
11379
11379
|
convert: {
|
|
11380
11380
|
minArgs: 3,
|
|
11381
11381
|
maxArgs: 3,
|
|
11382
11382
|
apply: (args, hooks) => {
|
|
11383
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
11383
|
+
const x = asFiniteNumber$1(args[0], "convert", 0);
|
|
11384
11384
|
const from = asString(args[1], "convert", 1).trim();
|
|
11385
11385
|
const to = asString(args[2], "convert", 2).trim();
|
|
11386
11386
|
if (hooks.convert) {
|
|
@@ -11406,11 +11406,11 @@ var table = {
|
|
|
11406
11406
|
minArgs: 2,
|
|
11407
11407
|
maxArgs: 2,
|
|
11408
11408
|
apply: (args, hooks) => {
|
|
11409
|
-
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
11409
|
+
const windowMs = asFiniteNumber$1(args[1], "within", 1);
|
|
11410
11410
|
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
11411
11411
|
const at = args[0];
|
|
11412
11412
|
if (at === null) return false;
|
|
11413
|
-
const ts = asFiniteNumber(at, "within", 0);
|
|
11413
|
+
const ts = asFiniteNumber$1(at, "within", 0);
|
|
11414
11414
|
const now = hooks.now;
|
|
11415
11415
|
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
11416
11416
|
const inside = now - ts <= windowMs;
|
|
@@ -11427,7 +11427,7 @@ var table = {
|
|
|
11427
11427
|
minArgs: 1,
|
|
11428
11428
|
maxArgs: INF,
|
|
11429
11429
|
apply: (args) => {
|
|
11430
|
-
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)]);
|
|
11431
11431
|
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
11432
11432
|
}
|
|
11433
11433
|
},
|
|
@@ -197514,6 +197514,7 @@ var StubMatterController = class {
|
|
|
197514
197514
|
async removeNode(_nodeId) {
|
|
197515
197515
|
throw new MatterControllerNotWiredError("removeNode");
|
|
197516
197516
|
}
|
|
197517
|
+
async syncSubscriptions() {}
|
|
197517
197518
|
};
|
|
197518
197519
|
/**
|
|
197519
197520
|
* Factory seam (stub default): build a {@link StubMatterController}.
|
|
@@ -197686,7 +197687,13 @@ var MatterAttributeId = {
|
|
|
197686
197687
|
/** ElectricalPowerMeasurement.ActivePower (int64, mW). */
|
|
197687
197688
|
ActivePower: 8,
|
|
197688
197689
|
/** ElectricalEnergyMeasurement.CumulativeEnergyImported (energy struct, mWh). */
|
|
197689
|
-
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
|
|
197690
197697
|
};
|
|
197691
197698
|
/** Thermostat.SystemMode enum → CamStack HvacMode (climate-control). */
|
|
197692
197699
|
var SYSTEM_MODE_TO_HVAC = new Map([
|
|
@@ -197718,8 +197725,11 @@ function asBoolean(value) {
|
|
|
197718
197725
|
* matter.js may surface a `MeasuredValue` either as a plain scaled int or wrapped
|
|
197719
197726
|
* in an energy-measurement struct (`{ energy }`). Pull the numeric payload out.
|
|
197720
197727
|
*/
|
|
197728
|
+
function isRecord$4(value) {
|
|
197729
|
+
return typeof value === "object" && value !== null;
|
|
197730
|
+
}
|
|
197721
197731
|
function asEnergyMilliWattHours(value) {
|
|
197722
|
-
if (
|
|
197732
|
+
if (isRecord$4(value) && "energy" in value) return asNumber(value["energy"]);
|
|
197723
197733
|
return asNumber(value);
|
|
197724
197734
|
}
|
|
197725
197735
|
/**
|
|
@@ -197741,9 +197751,77 @@ function decodeMatterAttribute(input) {
|
|
|
197741
197751
|
case MatterClusterId.Thermostat: return decodeThermostat(attributeId, value);
|
|
197742
197752
|
case MatterClusterId.ElectricalPowerMeasurement: return decodeElectricalPower(attributeId, value);
|
|
197743
197753
|
case MatterClusterId.ElectricalEnergyMeasurement: return decodeElectricalEnergy(attributeId, value);
|
|
197754
|
+
case MatterClusterId.DoorLock: return decodeDoorLock(attributeId, value);
|
|
197744
197755
|
default: return null;
|
|
197745
197756
|
}
|
|
197746
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
|
+
}
|
|
197747
197825
|
function decodeOnOff(attributeId, value) {
|
|
197748
197826
|
if (attributeId !== MatterAttributeId.OnOff) return null;
|
|
197749
197827
|
const on = asBoolean(value);
|
|
@@ -197967,7 +198045,10 @@ var ATTR_NAME_TO_ID = new Map([
|
|
|
197967
198045
|
["voltage", MatterAttributeId.Voltage],
|
|
197968
198046
|
["activeCurrent", MatterAttributeId.ActiveCurrent],
|
|
197969
198047
|
["activePower", MatterAttributeId.ActivePower],
|
|
197970
|
-
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported]
|
|
198048
|
+
["cumulativeEnergyImported", MatterAttributeId.CumulativeEnergyImported],
|
|
198049
|
+
["lockState", MatterAttributeId.LockState],
|
|
198050
|
+
["doorState", MatterAttributeId.DoorState],
|
|
198051
|
+
["requirePinForRemoteOperation", MatterAttributeId.RequirePinForRemoteOperation]
|
|
197971
198052
|
]);
|
|
197972
198053
|
/** clusterId → matter.js behavior id (camelCase) for outbound command clusters. */
|
|
197973
198054
|
var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
@@ -197979,14 +198060,14 @@ var CLUSTER_ID_TO_BEHAVIOR_ID = new Map([
|
|
|
197979
198060
|
[258, "windowCovering"]
|
|
197980
198061
|
]);
|
|
197981
198062
|
/** True when `value` is a non-null object (cast-free record narrowing). */
|
|
197982
|
-
function isRecord$
|
|
198063
|
+
function isRecord$3(value) {
|
|
197983
198064
|
return typeof value === "object" && value !== null;
|
|
197984
198065
|
}
|
|
197985
198066
|
/** Read a finite numeric cluster id from a `Behavior.Type`'s `.cluster.id`. */
|
|
197986
198067
|
function clusterIdOf(behaviorType) {
|
|
197987
|
-
if (!isRecord$
|
|
198068
|
+
if (!isRecord$3(behaviorType)) return null;
|
|
197988
198069
|
const cluster = behaviorType["cluster"];
|
|
197989
|
-
if (!isRecord$
|
|
198070
|
+
if (!isRecord$3(cluster)) return null;
|
|
197990
198071
|
const id = cluster["id"];
|
|
197991
198072
|
if (typeof id === "number" && Number.isFinite(id)) return id;
|
|
197992
198073
|
if (typeof id === "bigint") return Number(id);
|
|
@@ -197997,13 +198078,13 @@ function clusterIdOf(behaviorType) {
|
|
|
197997
198078
|
* the matter.js `Endpoint` interop at one typed boundary.
|
|
197998
198079
|
*/
|
|
197999
198080
|
function asDynamicEndpoint(value) {
|
|
198000
|
-
if (!isRecord$
|
|
198081
|
+
if (!isRecord$3(value)) return null;
|
|
198001
198082
|
const number = value["number"];
|
|
198002
198083
|
const behaviors = value["behaviors"];
|
|
198003
198084
|
const parts = value["parts"];
|
|
198004
198085
|
const stateOf = value["stateOf"];
|
|
198005
198086
|
const commandsOf = value["commandsOf"];
|
|
198006
|
-
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 {
|
|
198007
198088
|
number,
|
|
198008
198089
|
behaviors: { supported: behaviors["supported"] },
|
|
198009
198090
|
parts: mapIterable(parts, asDynamicEndpointOrThrow),
|
|
@@ -198027,7 +198108,7 @@ async function invokeNodeCommand(input) {
|
|
|
198027
198108
|
const behaviorId = CLUSTER_ID_TO_BEHAVIOR_ID.get(clusterId);
|
|
198028
198109
|
if (behaviorId === void 0) throw new Error(`matter-broker: cluster 0x${clusterId.toString(16)} has no command binding`);
|
|
198029
198110
|
const commands = target.commandsOf(behaviorId);
|
|
198030
|
-
if (!isRecord$
|
|
198111
|
+
if (!isRecord$3(commands)) throw new Error(`matter-broker: endpoint ${endpointId} has no '${behaviorId}' commands`);
|
|
198031
198112
|
const command = commands[commandName];
|
|
198032
198113
|
const fn = asCommandFn(command);
|
|
198033
198114
|
if (fn === null) throw new Error(`matter-broker: command '${commandName}' unavailable on '${behaviorId}'`);
|
|
@@ -198052,8 +198133,8 @@ function asDynamicEndpointOrThrow(value) {
|
|
|
198052
198133
|
if (ep === null) throw new TypeError("matter-broker: unexpected non-endpoint child part");
|
|
198053
198134
|
return ep;
|
|
198054
198135
|
}
|
|
198055
|
-
function isIterable$
|
|
198056
|
-
return isRecord$
|
|
198136
|
+
function isIterable$2(value) {
|
|
198137
|
+
return isRecord$3(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
|
|
198057
198138
|
}
|
|
198058
198139
|
function* mapIterable(it, fn) {
|
|
198059
198140
|
for (const v of it) yield fn(v);
|
|
@@ -198074,7 +198155,7 @@ function collectEndpoint(endpoint, out) {
|
|
|
198074
198155
|
const supported = endpoint.behaviors.supported;
|
|
198075
198156
|
const clusterIds = [];
|
|
198076
198157
|
const attributes = [];
|
|
198077
|
-
if (isRecord$
|
|
198158
|
+
if (isRecord$3(supported)) for (const [behaviorId, behaviorType] of Object.entries(supported)) {
|
|
198078
198159
|
const clusterId = clusterIdOf(behaviorType);
|
|
198079
198160
|
if (clusterId === null) continue;
|
|
198080
198161
|
clusterIds.push(clusterId);
|
|
@@ -198094,7 +198175,7 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
198094
198175
|
} catch {
|
|
198095
198176
|
return;
|
|
198096
198177
|
}
|
|
198097
|
-
if (!isRecord$
|
|
198178
|
+
if (!isRecord$3(struct)) return;
|
|
198098
198179
|
for (const [attrName, value] of Object.entries(struct)) {
|
|
198099
198180
|
const attributeId = ATTR_NAME_TO_ID.get(attrName);
|
|
198100
198181
|
if (attributeId === void 0) continue;
|
|
@@ -198106,35 +198187,413 @@ function readBehaviorAttributes(endpoint, behaviorId, clusterId, attributes) {
|
|
|
198106
198187
|
}
|
|
198107
198188
|
}
|
|
198108
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
|
|
198109
198579
|
//#region src/matter-controller-impl.ts
|
|
198110
198580
|
/**
|
|
198111
|
-
* Real {@link MatterController} backed by matter.js
|
|
198112
|
-
* `@matter/nodejs`).
|
|
198113
|
-
*
|
|
198114
|
-
*
|
|
198115
|
-
*
|
|
198116
|
-
*
|
|
198117
|
-
*
|
|
198118
|
-
*
|
|
198119
|
-
*
|
|
198120
|
-
*
|
|
198121
|
-
* - `
|
|
198122
|
-
* - `
|
|
198123
|
-
*
|
|
198124
|
-
* - `
|
|
198125
|
-
*
|
|
198126
|
-
*
|
|
198127
|
-
* - `clientNode.decommission()` — remove a node from the fabric.
|
|
198128
|
-
* - endpoint `commandsOf(behaviorId)[name](args)` — outbound cluster commands.
|
|
198129
|
-
*
|
|
198130
|
-
* The dynamic endpoint/cluster/attribute introspection lives in
|
|
198131
|
-
* `matter-endpoint-introspect.ts` (the single matter.js dynamic-shape boundary).
|
|
198132
|
-
* This class polls each node's endpoint state on reconcile + emits decoded
|
|
198133
|
-
* attribute reports, and re-emits on node online/offline transitions.
|
|
198134
|
-
*
|
|
198135
|
-
* BLE commissioning (`@matter/nodejs-ble`) is NOT wired — that package is not
|
|
198136
|
-
* installed. IP / on-network commissioning is the supported path; see the class
|
|
198137
|
-
* `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.
|
|
198138
198597
|
*/
|
|
198139
198598
|
/** A commissioning failure (bad code / device unreachable), distinct from runtime. */
|
|
198140
198599
|
var MatterCommissioningError = class extends Error {
|
|
@@ -198154,12 +198613,19 @@ var MatterJsController = class {
|
|
|
198154
198613
|
#options;
|
|
198155
198614
|
#logger;
|
|
198156
198615
|
#handlers = /* @__PURE__ */ new Set();
|
|
198616
|
+
#subscriptions;
|
|
198157
198617
|
#server = null;
|
|
198158
198618
|
/** Last emitted value per `node:endpoint:cluster:attribute` (change detection). */
|
|
198159
198619
|
#lastValues = /* @__PURE__ */ new Map();
|
|
198160
198620
|
constructor(options, logger) {
|
|
198161
198621
|
this.#options = options;
|
|
198162
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
|
+
});
|
|
198163
198629
|
}
|
|
198164
198630
|
get options() {
|
|
198165
198631
|
return this.#options;
|
|
@@ -198167,19 +198633,17 @@ var MatterJsController = class {
|
|
|
198167
198633
|
/** Boot the ServerNode fabric, loading/persisting state from `dataDir`. */
|
|
198168
198634
|
async start() {
|
|
198169
198635
|
if (this.#server !== null) return;
|
|
198170
|
-
const
|
|
198171
|
-
const server = await ServerNode.create({
|
|
198172
|
-
environment,
|
|
198173
|
-
id: this.#options.brokerId
|
|
198174
|
-
});
|
|
198636
|
+
const server = this.#options.openServer !== void 0 ? await this.#options.openServer() : await this.#openProductionServer();
|
|
198175
198637
|
this.#server = server;
|
|
198176
198638
|
await server.start();
|
|
198177
198639
|
this.#logger.info("MatterJsController: ServerNode online", { tags: { brokerId: this.#options.brokerId } });
|
|
198640
|
+
await this.syncSubscriptions();
|
|
198178
198641
|
this.#emitAllReports();
|
|
198179
198642
|
}
|
|
198180
|
-
/** Close
|
|
198643
|
+
/** Close subscriptions FIRST, then the ServerNode. */
|
|
198181
198644
|
async stop() {
|
|
198182
198645
|
this.#handlers.clear();
|
|
198646
|
+
await this.#subscriptions.stop();
|
|
198183
198647
|
const server = this.#server;
|
|
198184
198648
|
this.#server = null;
|
|
198185
198649
|
this.#lastValues.clear();
|
|
@@ -198200,19 +198664,49 @@ var MatterJsController = class {
|
|
|
198200
198664
|
*/
|
|
198201
198665
|
async commissionNode(pairingCode) {
|
|
198202
198666
|
const server = this.#requireServer("commissionNode");
|
|
198667
|
+
this.#progress({
|
|
198668
|
+
phase: CommissionPhase.Discovering,
|
|
198669
|
+
failure: null,
|
|
198670
|
+
message: null
|
|
198671
|
+
});
|
|
198203
198672
|
try {
|
|
198204
|
-
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
|
+
});
|
|
198205
198688
|
const view = this.#viewPeer(node);
|
|
198206
198689
|
this.#logger.info("MatterJsController: node commissioned", {
|
|
198207
198690
|
tags: { brokerId: this.#options.brokerId },
|
|
198208
198691
|
meta: { nodeId: view?.id ?? "(unknown)" }
|
|
198209
198692
|
});
|
|
198693
|
+
await this.syncSubscriptions();
|
|
198210
198694
|
this.#emitAllReports();
|
|
198211
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
|
+
});
|
|
198212
198701
|
return { nodeId: view.id };
|
|
198213
198702
|
} catch (err) {
|
|
198214
|
-
if (err instanceof MatterCommissioningError
|
|
198215
|
-
|
|
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 });
|
|
198216
198710
|
}
|
|
198217
198711
|
}
|
|
198218
198712
|
/** Pull the live commissioned-node inventory off `serverNode.peers`. */
|
|
@@ -198220,12 +198714,31 @@ var MatterJsController = class {
|
|
|
198220
198714
|
const server = this.#server;
|
|
198221
198715
|
if (server === null) return [];
|
|
198222
198716
|
const out = [];
|
|
198223
|
-
for (const peer of
|
|
198717
|
+
for (const peer of server.listPeers()) {
|
|
198224
198718
|
const snapshot = this.#snapshot(peer);
|
|
198225
198719
|
if (snapshot !== null) out.push(snapshot);
|
|
198226
198720
|
}
|
|
198227
198721
|
return out;
|
|
198228
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
|
+
}
|
|
198229
198742
|
async getNode(nodeId) {
|
|
198230
198743
|
return (await this.listNodes()).find((n) => n.nodeId === nodeId) ?? null;
|
|
198231
198744
|
}
|
|
@@ -198266,6 +198779,25 @@ var MatterJsController = class {
|
|
|
198266
198779
|
throw err;
|
|
198267
198780
|
}
|
|
198268
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
|
+
}
|
|
198269
198801
|
#buildEnvironment() {
|
|
198270
198802
|
const env = Environment.default;
|
|
198271
198803
|
env.vars.set("storage.path", this.#options.dataDir);
|
|
@@ -198278,13 +198810,13 @@ var MatterJsController = class {
|
|
|
198278
198810
|
return this.#server;
|
|
198279
198811
|
}
|
|
198280
198812
|
/** Iterate the commissioned peers of a ServerNode (cast-free). */
|
|
198281
|
-
#
|
|
198813
|
+
#serverPeers(server) {
|
|
198282
198814
|
const peers = server.peers;
|
|
198283
198815
|
if (isIterable(peers)) return peers;
|
|
198284
198816
|
return [];
|
|
198285
198817
|
}
|
|
198286
198818
|
#findPeer(server, nodeId) {
|
|
198287
|
-
for (const peer of
|
|
198819
|
+
for (const peer of server.listPeers()) {
|
|
198288
198820
|
const view = this.#viewPeer(peer);
|
|
198289
198821
|
if (view !== null && view.id === nodeId) return view;
|
|
198290
198822
|
}
|
|
@@ -198292,12 +198824,23 @@ var MatterJsController = class {
|
|
|
198292
198824
|
}
|
|
198293
198825
|
/** Raw (un-viewed) peer object for operations that call its methods. */
|
|
198294
198826
|
#rawPeer(server, nodeId) {
|
|
198295
|
-
for (const peer of
|
|
198827
|
+
for (const peer of server.listPeers()) {
|
|
198296
198828
|
const view = this.#viewPeer(peer);
|
|
198297
198829
|
if (view !== null && view.id === nodeId) return peer;
|
|
198298
198830
|
}
|
|
198299
198831
|
return null;
|
|
198300
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
|
+
}
|
|
198301
198844
|
/** Reduce a peer to the read-only view the addon models. */
|
|
198302
198845
|
#viewPeer(peer) {
|
|
198303
198846
|
if (!isRecord(peer)) return null;
|
|
@@ -198341,7 +198884,7 @@ var MatterJsController = class {
|
|
|
198341
198884
|
#emitAllReports() {
|
|
198342
198885
|
const server = this.#server;
|
|
198343
198886
|
if (server === null) return;
|
|
198344
|
-
for (const peer of
|
|
198887
|
+
for (const peer of server.listPeers()) {
|
|
198345
198888
|
const view = this.#viewPeer(peer);
|
|
198346
198889
|
if (view === null) continue;
|
|
198347
198890
|
for (const ep of readNodeEndpoints(view.rootEndpoint)) for (const attr of ep.attributes) {
|
|
@@ -198357,6 +198900,7 @@ var MatterJsController = class {
|
|
|
198357
198900
|
}
|
|
198358
198901
|
}
|
|
198359
198902
|
#emitIfChanged(report) {
|
|
198903
|
+
if (report.value === void 0 || report.value === null) return;
|
|
198360
198904
|
const key = `${report.nodeId}:${report.endpointId}:${report.clusterId}:${report.attributeId}`;
|
|
198361
198905
|
const prev = this.#lastValues.get(key);
|
|
198362
198906
|
if (prev !== void 0 && shallowEqual(prev, report.value)) return;
|
|
@@ -198442,12 +198986,24 @@ var MatterNodeResolver = class {
|
|
|
198442
198986
|
#attrs = /* @__PURE__ */ new Map();
|
|
198443
198987
|
/** Live attribute-report subscribers (endpoint device classes). */
|
|
198444
198988
|
#listeners = /* @__PURE__ */ new Set();
|
|
198989
|
+
/** Adopted container id per Matter node, for subscription logs. */
|
|
198990
|
+
#deviceIds = /* @__PURE__ */ new Map();
|
|
198445
198991
|
/**
|
|
198446
198992
|
* Subscribe to live attribute reports across all brokers. Endpoint device
|
|
198447
198993
|
* classes subscribe on activate (filtering to their own node/endpoint) so a
|
|
198448
198994
|
* push report decodes straight into their runtime-state slice. Returns a
|
|
198449
198995
|
* disposer.
|
|
198450
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
|
+
}
|
|
198451
199007
|
onAttributeReport(listener) {
|
|
198452
199008
|
this.#listeners.add(listener);
|
|
198453
199009
|
return () => {
|
|
@@ -198509,6 +199065,7 @@ var MatterNodeResolver = class {
|
|
|
198509
199065
|
this.#nodes.clear();
|
|
198510
199066
|
this.#attrs.clear();
|
|
198511
199067
|
this.#listeners.clear();
|
|
199068
|
+
this.#deviceIds.clear();
|
|
198512
199069
|
}
|
|
198513
199070
|
};
|
|
198514
199071
|
/** The single in-process per-broker node resolver shared across the addon. */
|
|
@@ -198568,6 +199125,7 @@ var MatterIntegrationManager = class {
|
|
|
198568
199125
|
#nodeCount = 0;
|
|
198569
199126
|
#lastCheckedAt = null;
|
|
198570
199127
|
#error = null;
|
|
199128
|
+
#commission = IDLE_COMMISSION_PROGRESS;
|
|
198571
199129
|
constructor(options) {
|
|
198572
199130
|
this.#id = options.id;
|
|
198573
199131
|
this.#name = options.name;
|
|
@@ -198578,7 +199136,17 @@ var MatterIntegrationManager = class {
|
|
|
198578
199136
|
this.#onDisconnected = options.onDisconnected;
|
|
198579
199137
|
this.#onNodesChanged = options.onNodesChanged;
|
|
198580
199138
|
this.#onAttributeReport = options.onAttributeReport;
|
|
198581
|
-
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;
|
|
198582
199150
|
}
|
|
198583
199151
|
/** The connection settings the manager is currently configured with. */
|
|
198584
199152
|
getConnection() {
|
|
@@ -198653,6 +199221,7 @@ var MatterIntegrationManager = class {
|
|
|
198653
199221
|
if (controller === null) return;
|
|
198654
199222
|
try {
|
|
198655
199223
|
const nodes = await controller.listNodes();
|
|
199224
|
+
await controller.syncSubscriptions();
|
|
198656
199225
|
this.#nodeCount = nodes.length;
|
|
198657
199226
|
this.#lastCheckedAt = Date.now();
|
|
198658
199227
|
matterNodes.setNodes(this.#id, nodes);
|
|
@@ -198833,6 +199402,10 @@ var MatterBrokerRegistry = class {
|
|
|
198833
199402
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
198834
199403
|
return mgr.commissionNode(pairingCode);
|
|
198835
199404
|
}
|
|
199405
|
+
getCommissionProgress(brokerId) {
|
|
199406
|
+
const mgr = this.#managers.get(brokerId);
|
|
199407
|
+
return mgr ? mgr.getCommissionProgress() : null;
|
|
199408
|
+
}
|
|
198836
199409
|
async removeNode(brokerId, nodeId) {
|
|
198837
199410
|
const mgr = this.#managers.get(brokerId);
|
|
198838
199411
|
if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${brokerId}"`);
|
|
@@ -198935,6 +199508,26 @@ function toRecord(value) {
|
|
|
198935
199508
|
function settingsToMatterConnection(settings) {
|
|
198936
199509
|
return matterConnectionSchema.parse(toRecord(settings));
|
|
198937
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
|
+
}
|
|
198938
199531
|
/** Hand-written connection form for the broker/integration creation UI. */
|
|
198939
199532
|
function buildConnectionFormSchema() {
|
|
198940
199533
|
return { sections: [{
|
|
@@ -198946,9 +199539,10 @@ function buildConnectionFormSchema() {
|
|
|
198946
199539
|
columns: 1,
|
|
198947
199540
|
fields: [
|
|
198948
199541
|
{
|
|
198949
|
-
type: "
|
|
199542
|
+
type: "password",
|
|
198950
199543
|
key: "threadDataset",
|
|
198951
199544
|
secret: true,
|
|
199545
|
+
showToggle: true,
|
|
198952
199546
|
label: "Thread operational dataset (hex, optional)",
|
|
198953
199547
|
labelKey: "matter-broker:connection.s_matter-fabric.threadDataset.label",
|
|
198954
199548
|
placeholder: "0e08000000000001…"
|
|
@@ -199025,17 +199619,14 @@ var MATTER_KIND = "matter";
|
|
|
199025
199619
|
* Construct the `broker` cap provider for the Matter addon. Pure builder.
|
|
199026
199620
|
* Mirrors `buildEcowittBrokerProvider`. The defining Matter operation —
|
|
199027
199621
|
* commissioning — is surfaced through `publish` with
|
|
199028
|
-
* `target: { action: 'commissionNode' }` and `payload: { pairingCode }`.
|
|
199029
|
-
*
|
|
199030
|
-
* 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.
|
|
199031
199625
|
*/
|
|
199032
199626
|
function buildMatterBrokerProvider(deps) {
|
|
199033
199627
|
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
199034
199628
|
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
199035
|
-
const redact = (entry) => (
|
|
199036
|
-
...entry.connection,
|
|
199037
|
-
wifiPassword: ""
|
|
199038
|
-
});
|
|
199629
|
+
const redact = (entry) => publicMatterConnection(entry.connection);
|
|
199039
199630
|
return {
|
|
199040
199631
|
list: async ({ kind }) => {
|
|
199041
199632
|
if (kind && kind !== MATTER_KIND) return [];
|
|
@@ -199103,10 +199694,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199103
199694
|
const existing = getBrokers().find((b) => b.id === id);
|
|
199104
199695
|
if (!existing) return;
|
|
199105
199696
|
const parsed = settingsToMatterConnection(settings);
|
|
199106
|
-
const merged =
|
|
199107
|
-
...parsed,
|
|
199108
|
-
wifiPassword: parsed.wifiPassword.length > 0 ? parsed.wifiPassword : existing.connection.wifiPassword
|
|
199109
|
-
};
|
|
199697
|
+
const merged = mergeMatterSecrets(existing.connection, parsed);
|
|
199110
199698
|
const updated = await registry.updateEntry(id, merged);
|
|
199111
199699
|
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
199112
199700
|
},
|
|
@@ -199142,7 +199730,7 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199142
199730
|
if (!owns(brokerId)) throw new Error(`matter: unknown broker ${brokerId}`);
|
|
199143
199731
|
const action = typeof target["action"] === "string" ? target["action"] : void 0;
|
|
199144
199732
|
if (action === "commissionNode") {
|
|
199145
|
-
const code =
|
|
199733
|
+
const code = pairingCodeFromPayload(payload);
|
|
199146
199734
|
parsePairingCode(code);
|
|
199147
199735
|
return registry.commissionNode(brokerId, code);
|
|
199148
199736
|
}
|
|
@@ -199150,13 +199738,23 @@ function buildMatterBrokerProvider(deps) {
|
|
|
199150
199738
|
},
|
|
199151
199739
|
subscribe: async () => ({ subscriptionId: "" }),
|
|
199152
199740
|
unsubscribe: async () => void 0,
|
|
199153
|
-
getState: async () =>
|
|
199741
|
+
getState: async ({ brokerId, key }) => {
|
|
199742
|
+
if (key !== "commission") return null;
|
|
199743
|
+
return registry.getCommissionProgress(brokerId);
|
|
199744
|
+
},
|
|
199154
199745
|
getStatus: async () => ({
|
|
199155
199746
|
brokerCount: registry.size(),
|
|
199156
199747
|
connectedCount: registry.connectedCount()
|
|
199157
199748
|
})
|
|
199158
199749
|
};
|
|
199159
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
|
+
}
|
|
199160
199758
|
//#endregion
|
|
199161
199759
|
//#region src/matter-discovery.ts
|
|
199162
199760
|
/**
|
|
@@ -199387,6 +199985,27 @@ function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
|
199387
199985
|
return count;
|
|
199388
199986
|
}
|
|
199389
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
|
|
199390
200009
|
//#region src/devices/matter-endpoint-device.ts
|
|
199391
200010
|
/**
|
|
199392
200011
|
* Persisted config every Matter endpoint child carries. `nodeId`/`endpointId`
|
|
@@ -199478,7 +200097,8 @@ var COLD_POWER = { lastFetchedAt: 0 };
|
|
|
199478
200097
|
* arrives, each cap reads its cold-start default.
|
|
199479
200098
|
*/
|
|
199480
200099
|
var MatterEndpointDevice = class extends BaseDevice {
|
|
199481
|
-
|
|
200100
|
+
/** Live RequirePinForRemoteOperation. Commands read this, not a stored PIN. */
|
|
200101
|
+
pinRequired = false;
|
|
199482
200102
|
brokerId;
|
|
199483
200103
|
nodeId;
|
|
199484
200104
|
endpointId;
|
|
@@ -199492,12 +200112,17 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199492
200112
|
this.nodeId = persisted.nodeId;
|
|
199493
200113
|
this.endpointId = persisted.endpointId;
|
|
199494
200114
|
this.caps = persisted.capabilities.filter(isKnownCap);
|
|
200115
|
+
this.pinRequired = matterNodes.getAttribute(this.brokerId, this.nodeId, this.endpointId, MatterClusterId.DoorLock, MatterAttributeId.RequirePinForRemoteOperation) === true;
|
|
199495
200116
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? true;
|
|
199496
200117
|
this.updateSourceInfo({
|
|
199497
200118
|
id: `${this.nodeId}:${this.endpointId}`,
|
|
199498
200119
|
system: "matter"
|
|
199499
200120
|
});
|
|
199500
200121
|
}
|
|
200122
|
+
/** PIN-required locks advertise the feature the lock UI already understands. */
|
|
200123
|
+
get features() {
|
|
200124
|
+
return this.pinRequired ? [DeviceFeature.LockPinRequired] : [];
|
|
200125
|
+
}
|
|
199501
200126
|
async onActivate() {
|
|
199502
200127
|
await super.onActivate();
|
|
199503
200128
|
for (const cap of this.caps) {
|
|
@@ -199525,7 +200150,29 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199525
200150
|
value: report.value
|
|
199526
200151
|
});
|
|
199527
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
|
+
});
|
|
199528
200174
|
if (!this.registeredCaps.has(decoded.cap)) return;
|
|
200175
|
+
if (Object.keys(decoded.patch).length === 0) return;
|
|
199529
200176
|
this.runtimeState.patchCapState(CAP_SLICE_NAME[decoded.cap], decoded.patch);
|
|
199530
200177
|
}
|
|
199531
200178
|
/** Invoke a cluster command on this endpoint via the live controller. */
|
|
@@ -199621,30 +200268,52 @@ var MatterEndpointDevice = class extends BaseDevice {
|
|
|
199621
200268
|
};
|
|
199622
200269
|
this.ctx.registerNativeCap(lockControlCapability, {
|
|
199623
200270
|
getStatus: async () => this.runtimeState.getCapState("lock-control") ?? COLD,
|
|
199624
|
-
lock: async () => {
|
|
199625
|
-
await this.
|
|
199626
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199627
|
-
state: "locked",
|
|
199628
|
-
lastChangedAt: Date.now()
|
|
199629
|
-
});
|
|
200271
|
+
lock: async ({ code }) => {
|
|
200272
|
+
await this.invokeLock("lockDoor", "locking", code);
|
|
199630
200273
|
},
|
|
199631
|
-
unlock: async () => {
|
|
199632
|
-
await this.
|
|
199633
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199634
|
-
state: "unlocked",
|
|
199635
|
-
lastChangedAt: Date.now()
|
|
199636
|
-
});
|
|
200274
|
+
unlock: async ({ code }) => {
|
|
200275
|
+
await this.invokeLock("unlockDoor", "unlocking", code);
|
|
199637
200276
|
},
|
|
199638
200277
|
open: async () => {
|
|
199639
|
-
await this.
|
|
199640
|
-
this.runtimeState.setCapState("lock-control", {
|
|
199641
|
-
state: "unlocked",
|
|
199642
|
-
lastChangedAt: Date.now()
|
|
199643
|
-
});
|
|
200278
|
+
await this.invokeLock("unlockDoor", "unlocking", void 0);
|
|
199644
200279
|
}
|
|
199645
200280
|
});
|
|
199646
200281
|
this.runtimeState.setCapState("lock-control", COLD);
|
|
199647
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
|
+
}
|
|
199648
200317
|
registerCover() {
|
|
199649
200318
|
const COLD = {
|
|
199650
200319
|
state: "closed",
|
|
@@ -199865,11 +200534,16 @@ var MatterNodeDevice = class extends BaseDevice {
|
|
|
199865
200534
|
this.nodeId = cfg.nodeId;
|
|
199866
200535
|
this.integrationId = cfg.integrationId;
|
|
199867
200536
|
this.online = matterNodes.getNode(this.brokerId, this.nodeId)?.online ?? false;
|
|
200537
|
+
matterNodes.bindDevice(this.nodeId, this.id);
|
|
199868
200538
|
this.updateSourceInfo({
|
|
199869
200539
|
id: this.nodeId,
|
|
199870
200540
|
system: "matter"
|
|
199871
200541
|
});
|
|
199872
200542
|
}
|
|
200543
|
+
async removeDevice() {
|
|
200544
|
+
matterNodes.unbindDevice(this.nodeId);
|
|
200545
|
+
await super.removeDevice();
|
|
200546
|
+
}
|
|
199873
200547
|
getAccessoryChildren() {
|
|
199874
200548
|
const specs = [];
|
|
199875
200549
|
for (const endpoint of this.config.values.endpoints) {
|