@camstack/addon-matter-broker 0.2.157 → 0.2.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.158] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - The Matter manifest declares multiple instances, as the integration already allowed.
15
+
10
16
  ## [0.2.157] - 2026-11-05
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -5995,7 +5995,7 @@ var ZodIssueCode = {
5995
5995
  var ZodFirstPartyTypeKind;
5996
5996
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5997
5997
  //#endregion
5998
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5998
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5999
5999
  /**
6000
6000
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6001
6001
  * window to float samples (D455).
@@ -6804,6 +6804,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6804
6804
  }
6805
6805
  return out;
6806
6806
  }
6807
+ /** Every scope, in the order the guard and the docs list them. */
6808
+ var SCOPED_SETTINGS_SCOPES = [
6809
+ "cluster",
6810
+ "device-defaults",
6811
+ "node"
6812
+ ];
6807
6813
  /**
6808
6814
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6809
6815
  *
@@ -7695,6 +7701,13 @@ var BaseAddon = class {
7695
7701
  */
7696
7702
  secretStatePaths = [];
7697
7703
  _addonSecretTree = null;
7704
+ /**
7705
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7706
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7707
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7708
+ */
7709
+ _scopedByCap = /* @__PURE__ */ new Map();
7710
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7698
7711
  _deviceSecretTree = null;
7699
7712
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7700
7713
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7764,6 +7777,7 @@ var BaseAddon = class {
7764
7777
  this._ctx = context;
7765
7778
  this._secretSettings = null;
7766
7779
  }
7780
+ await this.loadScopedSchemas();
7767
7781
  await this.resolveConfig();
7768
7782
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7769
7783
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7773,6 +7787,7 @@ var BaseAddon = class {
7773
7787
  const normalized = normalizeAddonInitResult(result);
7774
7788
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7775
7789
  this._registeredCapNames = providers.map((p) => p.capability.name);
7790
+ await this.loadScopedSchemas();
7776
7791
  return normalized;
7777
7792
  }
7778
7793
  /**
@@ -7861,6 +7876,16 @@ var BaseAddon = class {
7861
7876
  schema(schema) {
7862
7877
  return schema;
7863
7878
  }
7879
+ /**
7880
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7881
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7882
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7883
+ * must already know which keys are per node. Names the providers register at init are added
7884
+ * once known.
7885
+ */
7886
+ scopedSettingsCapabilities() {
7887
+ return [];
7888
+ }
7864
7889
  /** Override to provide global-level settings UI schema. */
7865
7890
  globalSettingsSchema(_cap) {
7866
7891
  return null;
@@ -7904,7 +7929,8 @@ var BaseAddon = class {
7904
7929
  return [];
7905
7930
  }
7906
7931
  async getGlobalSettings(overlay, cap, nodeId) {
7907
- const schema = this.globalSettingsSchema(cap);
7932
+ await this.loadScopedSchemas(cap);
7933
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7908
7934
  if (!schema) return { sections: [] };
7909
7935
  const projected = await this.readRawProjectedStore(nodeId, cap);
7910
7936
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8035,6 +8061,7 @@ var BaseAddon = class {
8035
8061
  * resolved blob back is a no-op for every unchanged secret.
8036
8062
  */
8037
8063
  async resolveGlobalStore(nodeId, cap) {
8064
+ await this.loadScopedSchemas(cap);
8038
8065
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8039
8066
  return this.projectForNode(raw, nodeId, cap);
8040
8067
  }
@@ -8045,6 +8072,7 @@ var BaseAddon = class {
8045
8072
  }
8046
8073
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8047
8074
  async readRawProjectedStore(nodeId, cap) {
8075
+ await this.loadScopedSchemas(cap);
8048
8076
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8049
8077
  return this.projectForNode(raw, nodeId, cap);
8050
8078
  }
@@ -8147,6 +8175,7 @@ var BaseAddon = class {
8147
8175
  await this.applyGlobalPatch(patch, nodeId);
8148
8176
  }
8149
8177
  async applyGlobalPatch(barePatch, nodeId) {
8178
+ await this.loadScopedSchemas();
8150
8179
  const keys = this.perNodeKeys();
8151
8180
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8152
8181
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8160,7 +8189,8 @@ var BaseAddon = class {
8160
8189
  await this.resolveConfig();
8161
8190
  await this.onConfigChanged();
8162
8191
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8163
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8192
+ const legacy = this.globalSettingsSchema();
8193
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8164
8194
  }
8165
8195
  /**
8166
8196
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8168,14 +8198,67 @@ var BaseAddon = class {
8168
8198
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8169
8199
  * settings API behaves exactly like the legacy node-agnostic one.
8170
8200
  */
8171
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8201
+ /**
8202
+ * Load the scoped schemas of every capability this addon declares (the override and the
8203
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8204
+ * derived from the previous set. Idempotent.
8205
+ */
8206
+ async loadScopedSchemas(extraCap) {
8207
+ const caps = new Set([
8208
+ ...this.scopedSettingsCapabilities(),
8209
+ ...this._registeredCapNames,
8210
+ ...extraCap === void 0 ? [] : [extraCap]
8211
+ ]);
8212
+ let added = false;
8213
+ for (const cap of caps) {
8214
+ if (this._scopedByCap.has(cap)) continue;
8215
+ const loaded = [];
8216
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8217
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8218
+ if (schema !== null) loaded.push({
8219
+ scope,
8220
+ schema
8221
+ });
8222
+ }
8223
+ this._scopedByCap.set(cap, loaded);
8224
+ added = true;
8225
+ }
8226
+ if (!added) return;
8227
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8228
+ this._addonSecretTree = null;
8229
+ }
8230
+ /**
8231
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8232
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8233
+ */
8234
+ scopedFormSchema(cap) {
8235
+ const byId = /* @__PURE__ */ new Map();
8236
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8237
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8238
+ }
8239
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8240
+ loadedScopedSchemas(cap) {
8241
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8242
+ return [...this._scopedByCap.values()].flat();
8243
+ }
8244
+ /**
8245
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8246
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8247
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8248
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8249
+ * Call {@link loadScopedSchemas} first (the async seams do).
8250
+ */
8172
8251
  perNodeKeys(cap) {
8173
8252
  const cacheKey = cap ?? "";
8174
- const cached = this._perNodeKeysCache.get(cacheKey);
8253
+ const cached = this._nodeKeysByCap.get(cacheKey);
8175
8254
  if (cached) return cached;
8176
- const schema = this.globalSettingsSchema(cap);
8177
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8178
- this._perNodeKeysCache.set(cacheKey, keys);
8255
+ const legacy = this.globalSettingsSchema(cap);
8256
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8257
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8258
+ if (scope !== "node") continue;
8259
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8260
+ }
8261
+ this._nodeKeysByCap.set(cacheKey, keys);
8179
8262
  return keys;
8180
8263
  }
8181
8264
  /**
@@ -8185,10 +8268,9 @@ var BaseAddon = class {
8185
8268
  * time to return its response before the addon is torn down and
8186
8269
  * re-initialised by `AddonRegistryService.restartAddon`.
8187
8270
  */
8188
- maybeAutoRestart(patchKeys, schema) {
8189
- if (!schema) return;
8271
+ maybeAutoRestart(patchKeys, schemas) {
8190
8272
  const restartKeys = /* @__PURE__ */ new Set();
8191
- for (const section of schema.sections) for (const field of section.fields) {
8273
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8192
8274
  if (field.type === "separator" || field.type === "info") continue;
8193
8275
  if (field.requiresRestart) restartKeys.add(field.key);
8194
8276
  }
@@ -8384,12 +8466,14 @@ var BaseAddon = class {
8384
8466
  * here, in the addon's own process, so the hub keeps no copy of it.
8385
8467
  */
8386
8468
  async getDeclaredSettingsKeys() {
8469
+ await this.loadScopedSchemas();
8387
8470
  return { keys: [...this.declaredKeySet()].toSorted() };
8388
8471
  }
8389
8472
  declaredKeySet() {
8390
8473
  const keys = new Set(Object.keys(this.defaults));
8391
8474
  const schema = this.globalSettingsSchema();
8392
8475
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8476
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8393
8477
  for (const key of this._declaredStateKeys) keys.add(key);
8394
8478
  for (const path of this.secretStatePaths) {
8395
8479
  const root = path.split(/[.[]/, 1)[0];
@@ -8485,7 +8569,11 @@ var BaseAddon = class {
8485
8569
  }
8486
8570
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8487
8571
  addonSecretTree() {
8488
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8572
+ if (this._addonSecretTree === null) {
8573
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8574
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8575
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8576
+ }
8489
8577
  return this._addonSecretTree;
8490
8578
  }
8491
8579
  deviceSecretTree() {
@@ -8735,6 +8823,23 @@ function dropPerNodeFields(fields) {
8735
8823
  }
8736
8824
  return kept;
8737
8825
  }
8826
+ /** The keys of the value-holding fields, recursing into layout containers. */
8827
+ function collectValueFieldKeys(fields) {
8828
+ const collected = [];
8829
+ for (const field of fields) {
8830
+ if (field.type === "group") {
8831
+ collected.push(...collectValueFieldKeys(field.fields));
8832
+ continue;
8833
+ }
8834
+ if (field.type === "sub-tabs") {
8835
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8836
+ continue;
8837
+ }
8838
+ if (field.type === "separator" || field.type === "info") continue;
8839
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8840
+ }
8841
+ return collected;
8842
+ }
8738
8843
  function collectFieldKeys(fields) {
8739
8844
  const collected = [];
8740
8845
  for (const field of fields) {
@@ -207386,8 +207491,8 @@ function sameConnection(a, b) {
207386
207491
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207387
207492
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207388
207493
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207389
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207390
- * fabric, but the namespace tolerates multiple.
207494
+ * registry. The integration is `instanceMode: multiple`: the common case is one
207495
+ * fabric, and an operator may add more.
207391
207496
  */
207392
207497
  var MatterBrokerRegistry = class {
207393
207498
  #logger;
package/dist/addon.mjs CHANGED
@@ -5993,7 +5993,7 @@ var ZodIssueCode = {
5993
5993
  var ZodFirstPartyTypeKind;
5994
5994
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5995
5995
  //#endregion
5996
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5996
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5997
5997
  /**
5998
5998
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5999
5999
  * window to float samples (D455).
@@ -6802,6 +6802,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6802
6802
  }
6803
6803
  return out;
6804
6804
  }
6805
+ /** Every scope, in the order the guard and the docs list them. */
6806
+ var SCOPED_SETTINGS_SCOPES = [
6807
+ "cluster",
6808
+ "device-defaults",
6809
+ "node"
6810
+ ];
6805
6811
  /**
6806
6812
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6807
6813
  *
@@ -7693,6 +7699,13 @@ var BaseAddon = class {
7693
7699
  */
7694
7700
  secretStatePaths = [];
7695
7701
  _addonSecretTree = null;
7702
+ /**
7703
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7704
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7705
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7706
+ */
7707
+ _scopedByCap = /* @__PURE__ */ new Map();
7708
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7696
7709
  _deviceSecretTree = null;
7697
7710
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7698
7711
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7762,6 +7775,7 @@ var BaseAddon = class {
7762
7775
  this._ctx = context;
7763
7776
  this._secretSettings = null;
7764
7777
  }
7778
+ await this.loadScopedSchemas();
7765
7779
  await this.resolveConfig();
7766
7780
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7767
7781
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7771,6 +7785,7 @@ var BaseAddon = class {
7771
7785
  const normalized = normalizeAddonInitResult(result);
7772
7786
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7773
7787
  this._registeredCapNames = providers.map((p) => p.capability.name);
7788
+ await this.loadScopedSchemas();
7774
7789
  return normalized;
7775
7790
  }
7776
7791
  /**
@@ -7859,6 +7874,16 @@ var BaseAddon = class {
7859
7874
  schema(schema) {
7860
7875
  return schema;
7861
7876
  }
7877
+ /**
7878
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7879
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7880
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7881
+ * must already know which keys are per node. Names the providers register at init are added
7882
+ * once known.
7883
+ */
7884
+ scopedSettingsCapabilities() {
7885
+ return [];
7886
+ }
7862
7887
  /** Override to provide global-level settings UI schema. */
7863
7888
  globalSettingsSchema(_cap) {
7864
7889
  return null;
@@ -7902,7 +7927,8 @@ var BaseAddon = class {
7902
7927
  return [];
7903
7928
  }
7904
7929
  async getGlobalSettings(overlay, cap, nodeId) {
7905
- const schema = this.globalSettingsSchema(cap);
7930
+ await this.loadScopedSchemas(cap);
7931
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7906
7932
  if (!schema) return { sections: [] };
7907
7933
  const projected = await this.readRawProjectedStore(nodeId, cap);
7908
7934
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8033,6 +8059,7 @@ var BaseAddon = class {
8033
8059
  * resolved blob back is a no-op for every unchanged secret.
8034
8060
  */
8035
8061
  async resolveGlobalStore(nodeId, cap) {
8062
+ await this.loadScopedSchemas(cap);
8036
8063
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8037
8064
  return this.projectForNode(raw, nodeId, cap);
8038
8065
  }
@@ -8043,6 +8070,7 @@ var BaseAddon = class {
8043
8070
  }
8044
8071
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8045
8072
  async readRawProjectedStore(nodeId, cap) {
8073
+ await this.loadScopedSchemas(cap);
8046
8074
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8047
8075
  return this.projectForNode(raw, nodeId, cap);
8048
8076
  }
@@ -8145,6 +8173,7 @@ var BaseAddon = class {
8145
8173
  await this.applyGlobalPatch(patch, nodeId);
8146
8174
  }
8147
8175
  async applyGlobalPatch(barePatch, nodeId) {
8176
+ await this.loadScopedSchemas();
8148
8177
  const keys = this.perNodeKeys();
8149
8178
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8150
8179
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8158,7 +8187,8 @@ var BaseAddon = class {
8158
8187
  await this.resolveConfig();
8159
8188
  await this.onConfigChanged();
8160
8189
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8161
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8190
+ const legacy = this.globalSettingsSchema();
8191
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8162
8192
  }
8163
8193
  /**
8164
8194
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8166,14 +8196,67 @@ var BaseAddon = class {
8166
8196
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8167
8197
  * settings API behaves exactly like the legacy node-agnostic one.
8168
8198
  */
8169
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8199
+ /**
8200
+ * Load the scoped schemas of every capability this addon declares (the override and the
8201
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8202
+ * derived from the previous set. Idempotent.
8203
+ */
8204
+ async loadScopedSchemas(extraCap) {
8205
+ const caps = new Set([
8206
+ ...this.scopedSettingsCapabilities(),
8207
+ ...this._registeredCapNames,
8208
+ ...extraCap === void 0 ? [] : [extraCap]
8209
+ ]);
8210
+ let added = false;
8211
+ for (const cap of caps) {
8212
+ if (this._scopedByCap.has(cap)) continue;
8213
+ const loaded = [];
8214
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8215
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8216
+ if (schema !== null) loaded.push({
8217
+ scope,
8218
+ schema
8219
+ });
8220
+ }
8221
+ this._scopedByCap.set(cap, loaded);
8222
+ added = true;
8223
+ }
8224
+ if (!added) return;
8225
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8226
+ this._addonSecretTree = null;
8227
+ }
8228
+ /**
8229
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8230
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8231
+ */
8232
+ scopedFormSchema(cap) {
8233
+ const byId = /* @__PURE__ */ new Map();
8234
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8235
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8236
+ }
8237
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8238
+ loadedScopedSchemas(cap) {
8239
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8240
+ return [...this._scopedByCap.values()].flat();
8241
+ }
8242
+ /**
8243
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8244
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8245
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8246
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8247
+ * Call {@link loadScopedSchemas} first (the async seams do).
8248
+ */
8170
8249
  perNodeKeys(cap) {
8171
8250
  const cacheKey = cap ?? "";
8172
- const cached = this._perNodeKeysCache.get(cacheKey);
8251
+ const cached = this._nodeKeysByCap.get(cacheKey);
8173
8252
  if (cached) return cached;
8174
- const schema = this.globalSettingsSchema(cap);
8175
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8176
- this._perNodeKeysCache.set(cacheKey, keys);
8253
+ const legacy = this.globalSettingsSchema(cap);
8254
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8255
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8256
+ if (scope !== "node") continue;
8257
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8258
+ }
8259
+ this._nodeKeysByCap.set(cacheKey, keys);
8177
8260
  return keys;
8178
8261
  }
8179
8262
  /**
@@ -8183,10 +8266,9 @@ var BaseAddon = class {
8183
8266
  * time to return its response before the addon is torn down and
8184
8267
  * re-initialised by `AddonRegistryService.restartAddon`.
8185
8268
  */
8186
- maybeAutoRestart(patchKeys, schema) {
8187
- if (!schema) return;
8269
+ maybeAutoRestart(patchKeys, schemas) {
8188
8270
  const restartKeys = /* @__PURE__ */ new Set();
8189
- for (const section of schema.sections) for (const field of section.fields) {
8271
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8190
8272
  if (field.type === "separator" || field.type === "info") continue;
8191
8273
  if (field.requiresRestart) restartKeys.add(field.key);
8192
8274
  }
@@ -8382,12 +8464,14 @@ var BaseAddon = class {
8382
8464
  * here, in the addon's own process, so the hub keeps no copy of it.
8383
8465
  */
8384
8466
  async getDeclaredSettingsKeys() {
8467
+ await this.loadScopedSchemas();
8385
8468
  return { keys: [...this.declaredKeySet()].toSorted() };
8386
8469
  }
8387
8470
  declaredKeySet() {
8388
8471
  const keys = new Set(Object.keys(this.defaults));
8389
8472
  const schema = this.globalSettingsSchema();
8390
8473
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8474
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8391
8475
  for (const key of this._declaredStateKeys) keys.add(key);
8392
8476
  for (const path of this.secretStatePaths) {
8393
8477
  const root = path.split(/[.[]/, 1)[0];
@@ -8483,7 +8567,11 @@ var BaseAddon = class {
8483
8567
  }
8484
8568
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8485
8569
  addonSecretTree() {
8486
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8570
+ if (this._addonSecretTree === null) {
8571
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8572
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8573
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8574
+ }
8487
8575
  return this._addonSecretTree;
8488
8576
  }
8489
8577
  deviceSecretTree() {
@@ -8733,6 +8821,23 @@ function dropPerNodeFields(fields) {
8733
8821
  }
8734
8822
  return kept;
8735
8823
  }
8824
+ /** The keys of the value-holding fields, recursing into layout containers. */
8825
+ function collectValueFieldKeys(fields) {
8826
+ const collected = [];
8827
+ for (const field of fields) {
8828
+ if (field.type === "group") {
8829
+ collected.push(...collectValueFieldKeys(field.fields));
8830
+ continue;
8831
+ }
8832
+ if (field.type === "sub-tabs") {
8833
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8834
+ continue;
8835
+ }
8836
+ if (field.type === "separator" || field.type === "info") continue;
8837
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8838
+ }
8839
+ return collected;
8840
+ }
8736
8841
  function collectFieldKeys(fields) {
8737
8842
  const collected = [];
8738
8843
  for (const field of fields) {
@@ -207384,8 +207489,8 @@ function sameConnection(a, b) {
207384
207489
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207385
207490
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207386
207491
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207387
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207388
- * fabric, but the namespace tolerates multiple.
207492
+ * registry. The integration is `instanceMode: multiple`: the common case is one
207493
+ * fabric, and an operator may add more.
207389
207494
  */
207390
207495
  var MatterBrokerRegistry = class {
207391
207496
  #logger;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.157",
3
+ "version": "0.2.158",
4
4
  "description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -43,7 +43,7 @@
43
43
  "entry": "./dist/addon.js",
44
44
  "icon": "assets/icon.svg",
45
45
  "color": "#3b6ea5",
46
- "instanceMode": "unique",
46
+ "instanceMode": "multiple",
47
47
  "brokerKind": "matter",
48
48
  "execution": {
49
49
  "placement": "hub-only"
@@ -65,7 +65,7 @@
65
65
  }
66
66
  ],
67
67
  "requires": {
68
- "server": ">=1.2.770"
68
+ "server": ">=1.2.776"
69
69
  }
70
70
  },
71
71
  "files": [