@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/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 (typeof value === "object" && value !== null && "energy" in value) return asNumber(value.energy);
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$1(value) {
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$1(behaviorType)) return null;
198068
+ if (!isRecord$3(behaviorType)) return null;
197988
198069
  const cluster = behaviorType["cluster"];
197989
- if (!isRecord$1(cluster)) return null;
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$1(value)) return null;
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$1(behaviors) && "supported" in behaviors && typeof stateOf === "function" && typeof commandsOf === "function" && isIterable$1(parts)) return {
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$1(commands)) throw new Error(`matter-broker: endpoint ${endpointId} has no '${behaviorId}' commands`);
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$1(value) {
198056
- return isRecord$1(value) && typeof Reflect.get(value, Symbol.iterator) === "function";
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$1(supported)) for (const [behaviorId, behaviorType] of Object.entries(supported)) {
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$1(struct)) return;
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 v0.17 (`@matter/main` +
198112
- * `@matter/nodejs`).
198113
- *
198114
- * matter.js v0.17 uses the "node API": a {@link ServerNode} owns the controller
198115
- * fabric, and commissioned devices are {@link ClientNode} peers reachable via
198116
- * `serverNode.peers`. This class wraps exactly one `ServerNode` per broker
198117
- * (= one fabric), persisting it under the addon `dataDir` via the matter.js
198118
- * `Environment`'s `storage.path` variable.
198119
- *
198120
- * Bound API (matter.js 0.17.3):
198121
- * - `ServerNode.create({ environment, id })` — fabric owner; storage at dataDir.
198122
- * - `serverNode.peers.commission({ pairingCode })` — commission a node from a
198123
- * QR (`MT:…`) or manual pairing code; resolves to a `ClientNode`.
198124
- * - `serverNode.peers` — iterable of commissioned `ClientNode`s.
198125
- * - `clientNode.lifecycle.isOnline`, `clientNode.peerAddress.nodeId` — liveness
198126
- * + stable node id.
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 environment = this.#buildEnvironment();
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 the ServerNode FIRST (frees OS sockets/subscriptions), then clear. */
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.peers.commission({ pairingCode });
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) throw err;
198215
- throw new MatterCommissioningError(errMsg(err), { cause: err });
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 this.#peers(server)) {
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
- #peers(server) {
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 this.#peers(server)) {
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 this.#peers(server)) {
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 this.#peers(server)) {
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(opts, this.#logger));
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: "text",
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 }`. The
199029
- * Wi-Fi password is the only secret; it is redacted on settings reads and
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 = typeof payload === "object" && payload !== null && "pairingCode" in payload ? String(payload["pairingCode"] ?? "") : "";
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 () => null,
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
- features = [];
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.invoke(MatterClusterId.DoorLock, "lockDoor");
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.invoke(MatterClusterId.DoorLock, "unlockDoor");
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.invoke(MatterClusterId.DoorLock, "unlockDoor");
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) {