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