@camstack/addon-matter-broker 0.2.156 → 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,18 @@ 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
+
16
+ ## [0.2.157] - 2026-11-05
17
+
18
+ ### Changed
19
+
20
+ - A Matter integration links to its fabric through the fabric record
21
+
10
22
  ## [0.2.156] - 2026-11-04
11
23
 
12
24
  ### 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-BaWhXZFm.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).
@@ -6595,7 +6595,7 @@ var NOT_RUN = {
6595
6595
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6596
6596
  * because the hub (and this addon's boot) must not hang on a migration.
6597
6597
  */
6598
- async function runAddonMigrations(host, migrations, logger) {
6598
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6599
6599
  const first = migrations[0];
6600
6600
  if (first === void 0) return NOT_RUN;
6601
6601
  const owner = first.meta.owner;
@@ -6624,7 +6624,9 @@ async function runAddonMigrations(host, migrations, logger) {
6624
6624
  else if (status === "blocked") blocked.push(id);
6625
6625
  else skipped.push(id);
6626
6626
  };
6627
+ const selected = options.only === void 0 ? null : new Set(options.only);
6627
6628
  for (const step of declaration.steps) {
6629
+ if (selected !== null && !selected.has(step.id)) continue;
6628
6630
  const bound = byId.get(step.id);
6629
6631
  if (bound === void 0 || step.action === "skip") {
6630
6632
  skipped.push(step.id);
@@ -6802,6 +6804,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6802
6804
  }
6803
6805
  return out;
6804
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
+ ];
6805
6813
  /**
6806
6814
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6807
6815
  *
@@ -7693,6 +7701,13 @@ var BaseAddon = class {
7693
7701
  */
7694
7702
  secretStatePaths = [];
7695
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();
7696
7711
  _deviceSecretTree = null;
7697
7712
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7698
7713
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7762,6 +7777,7 @@ var BaseAddon = class {
7762
7777
  this._ctx = context;
7763
7778
  this._secretSettings = null;
7764
7779
  }
7780
+ await this.loadScopedSchemas();
7765
7781
  await this.resolveConfig();
7766
7782
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7767
7783
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7771,6 +7787,7 @@ var BaseAddon = class {
7771
7787
  const normalized = normalizeAddonInitResult(result);
7772
7788
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7773
7789
  this._registeredCapNames = providers.map((p) => p.capability.name);
7790
+ await this.loadScopedSchemas();
7774
7791
  return normalized;
7775
7792
  }
7776
7793
  /**
@@ -7784,14 +7801,18 @@ var BaseAddon = class {
7784
7801
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7785
7802
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7786
7803
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7804
+ *
7805
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7806
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7807
+ * omitted applied ids "newer").
7787
7808
  */
7788
- async runMigrations(migrations) {
7809
+ async runMigrations(migrations, options) {
7789
7810
  const api = this.ctx.api.migrations;
7790
7811
  const report = await runAddonMigrations({
7791
7812
  declareOwner: (input) => api.declareOwner.mutate(input),
7792
7813
  startMigration: (input) => api.startMigration.mutate(input),
7793
7814
  finishMigration: (input) => api.finishMigration.mutate(input)
7794
- }, migrations, this.ctx.logger);
7815
+ }, migrations, this.ctx.logger, options);
7795
7816
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7796
7817
  return report;
7797
7818
  }
@@ -7855,6 +7876,16 @@ var BaseAddon = class {
7855
7876
  schema(schema) {
7856
7877
  return schema;
7857
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
+ }
7858
7889
  /** Override to provide global-level settings UI schema. */
7859
7890
  globalSettingsSchema(_cap) {
7860
7891
  return null;
@@ -7898,7 +7929,8 @@ var BaseAddon = class {
7898
7929
  return [];
7899
7930
  }
7900
7931
  async getGlobalSettings(overlay, cap, nodeId) {
7901
- const schema = this.globalSettingsSchema(cap);
7932
+ await this.loadScopedSchemas(cap);
7933
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7902
7934
  if (!schema) return { sections: [] };
7903
7935
  const projected = await this.readRawProjectedStore(nodeId, cap);
7904
7936
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7933,10 +7965,9 @@ var BaseAddon = class {
7933
7965
  *
7934
7966
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7935
7967
  * needs its connection fields, the default integration the addon's own sections. It is the
7936
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7937
- * unchanged (`integrations.setSettings` for an account integration).
7968
+ * SHAPE of the form only — the values live on the instance record.
7938
7969
  */
7939
- async integrationSettingsSchema(_instance) {
7970
+ async integrationFormSchema(_instance) {
7940
7971
  return null;
7941
7972
  }
7942
7973
  /**
@@ -7982,7 +8013,7 @@ var BaseAddon = class {
7982
8013
  * per-instance schema when it declares one, else its global schema (what they always showed).
7983
8014
  */
7984
8015
  async getIntegrationFormSchema(instance, nodeId) {
7985
- const scoped = await this.integrationSettingsSchema(instance);
8016
+ const scoped = await this.integrationFormSchema(instance);
7986
8017
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7987
8018
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7988
8019
  }
@@ -8030,6 +8061,7 @@ var BaseAddon = class {
8030
8061
  * resolved blob back is a no-op for every unchanged secret.
8031
8062
  */
8032
8063
  async resolveGlobalStore(nodeId, cap) {
8064
+ await this.loadScopedSchemas(cap);
8033
8065
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8034
8066
  return this.projectForNode(raw, nodeId, cap);
8035
8067
  }
@@ -8040,6 +8072,7 @@ var BaseAddon = class {
8040
8072
  }
8041
8073
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8042
8074
  async readRawProjectedStore(nodeId, cap) {
8075
+ await this.loadScopedSchemas(cap);
8043
8076
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8044
8077
  return this.projectForNode(raw, nodeId, cap);
8045
8078
  }
@@ -8142,6 +8175,7 @@ var BaseAddon = class {
8142
8175
  await this.applyGlobalPatch(patch, nodeId);
8143
8176
  }
8144
8177
  async applyGlobalPatch(barePatch, nodeId) {
8178
+ await this.loadScopedSchemas();
8145
8179
  const keys = this.perNodeKeys();
8146
8180
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8147
8181
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8155,7 +8189,8 @@ var BaseAddon = class {
8155
8189
  await this.resolveConfig();
8156
8190
  await this.onConfigChanged();
8157
8191
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8158
- 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)]);
8159
8194
  }
8160
8195
  /**
8161
8196
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8163,14 +8198,67 @@ var BaseAddon = class {
8163
8198
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8164
8199
  * settings API behaves exactly like the legacy node-agnostic one.
8165
8200
  */
8166
- _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
+ */
8167
8251
  perNodeKeys(cap) {
8168
8252
  const cacheKey = cap ?? "";
8169
- const cached = this._perNodeKeysCache.get(cacheKey);
8253
+ const cached = this._nodeKeysByCap.get(cacheKey);
8170
8254
  if (cached) return cached;
8171
- const schema = this.globalSettingsSchema(cap);
8172
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8173
- 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);
8174
8262
  return keys;
8175
8263
  }
8176
8264
  /**
@@ -8180,10 +8268,9 @@ var BaseAddon = class {
8180
8268
  * time to return its response before the addon is torn down and
8181
8269
  * re-initialised by `AddonRegistryService.restartAddon`.
8182
8270
  */
8183
- maybeAutoRestart(patchKeys, schema) {
8184
- if (!schema) return;
8271
+ maybeAutoRestart(patchKeys, schemas) {
8185
8272
  const restartKeys = /* @__PURE__ */ new Set();
8186
- 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) {
8187
8274
  if (field.type === "separator" || field.type === "info") continue;
8188
8275
  if (field.requiresRestart) restartKeys.add(field.key);
8189
8276
  }
@@ -8379,12 +8466,14 @@ var BaseAddon = class {
8379
8466
  * here, in the addon's own process, so the hub keeps no copy of it.
8380
8467
  */
8381
8468
  async getDeclaredSettingsKeys() {
8469
+ await this.loadScopedSchemas();
8382
8470
  return { keys: [...this.declaredKeySet()].toSorted() };
8383
8471
  }
8384
8472
  declaredKeySet() {
8385
8473
  const keys = new Set(Object.keys(this.defaults));
8386
8474
  const schema = this.globalSettingsSchema();
8387
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);
8388
8477
  for (const key of this._declaredStateKeys) keys.add(key);
8389
8478
  for (const path of this.secretStatePaths) {
8390
8479
  const root = path.split(/[.[]/, 1)[0];
@@ -8480,7 +8569,11 @@ var BaseAddon = class {
8480
8569
  }
8481
8570
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8482
8571
  addonSecretTree() {
8483
- 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
+ }
8484
8577
  return this._addonSecretTree;
8485
8578
  }
8486
8579
  deviceSecretTree() {
@@ -8730,6 +8823,23 @@ function dropPerNodeFields(fields) {
8730
8823
  }
8731
8824
  return kept;
8732
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
+ }
8733
8843
  function collectFieldKeys(fields) {
8734
8844
  const collected = [];
8735
8845
  for (const field of fields) {
@@ -9256,6 +9366,7 @@ function defineEntryKind(name, spec) {
9256
9366
  class: "config",
9257
9367
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9258
9368
  retention: { none: "an entry goes only when the operator deletes it" },
9369
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9259
9370
  ...spec.database === void 0 ? {} : { database: spec.database },
9260
9371
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9261
9372
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11925,7 +12036,7 @@ object({
11925
12036
  params: record(string$2(), union([string$2(), number()])).optional()
11926
12037
  });
11927
12038
  //#endregion
11928
- //#region ../types/dist/composition-CRNwcFYY.mjs
12039
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11929
12040
  /**
11930
12041
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11931
12042
  *
@@ -14506,7 +14617,13 @@ var MigrationLedgerEntrySchema = object({
14506
14617
  * joined by the engine from the catalogues it knows this boot. Absent when no
14507
14618
  * catalogue lists the id (a newer database).
14508
14619
  */
14509
- meta: MigrationMetaSchema.optional()
14620
+ meta: MigrationMetaSchema.optional(),
14621
+ /**
14622
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14623
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14624
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14625
+ */
14626
+ ownerNotLoaded: boolean().optional()
14510
14627
  });
14511
14628
  /** A catalogued migration that is not applied, with where it stands. */
14512
14629
  var MigrationPendingEntrySchema = object({
@@ -50241,6 +50358,9 @@ var RunningProposalEvidenceSchema = object({
50241
50358
  runsFound: number().int().nonnegative(),
50242
50359
  pausesFound: number().int().nonnegative(),
50243
50360
  longestPauseMs: number().nonnegative(),
50361
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50362
+ pulsesFound: number().int().nonnegative(),
50363
+ longestPulseMs: number().nonnegative(),
50244
50364
  idleFloorW: number(),
50245
50365
  noiseCeilingW: number(),
50246
50366
  plateauW: number(),
@@ -56086,11 +56206,7 @@ var CreateIntegrationInputSchema = object({
56086
56206
  info: record(string$2(), unknown()).optional(),
56087
56207
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56088
56208
  originStableId: string$2().optional(),
56089
- settings: record(string$2(), unknown()).optional(),
56090
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56091
- * the keychain, not the table (D787). */
56092
- secretKeys: array(string$2()).optional(),
56093
- skipRestart: boolean().optional()
56209
+ settings: record(string$2(), unknown()).optional()
56094
56210
  });
56095
56211
  var UpdateIntegrationInputSchema = object({
56096
56212
  id: string$2(),
@@ -56111,16 +56227,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
56111
56227
  }), method(object({ id: string$2() }), DeleteIntegrationResultSchema, {
56112
56228
  kind: "mutation",
56113
56229
  auth: "admin"
56114
- }), method(object({ id: string$2() }), record(string$2(), unknown()), { auth: "admin" }), method(object({
56115
- id: string$2(),
56116
- settings: record(string$2(), unknown()),
56117
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56118
- secretKeys: array(string$2()).optional(),
56119
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56120
- skipRestart: boolean().optional()
56121
- }), object({ success: boolean() }), {
56122
- kind: "mutation",
56123
- auth: "admin"
56124
56230
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56125
56231
  addonId: string$2(),
56126
56232
  settings: record(string$2(), unknown())
@@ -59097,6 +59203,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59097
59203
  }), {
59098
59204
  kind: "mutation",
59099
59205
  auth: "admin"
59206
+ }), method(object({ integrationId: string$2().min(1) }), record(string$2(), unknown()), { auth: "admin" }), method(object({ integrationId: string$2().min(1) }), object({
59207
+ rows: number().int().nonnegative(),
59208
+ secrets: number().int().nonnegative()
59209
+ }), {
59210
+ kind: "mutation",
59211
+ auth: "admin"
59212
+ }), method(object({
59213
+ integrationId: string$2().min(1),
59214
+ patch: record(string$2(), unknown())
59215
+ }), object({ written: boolean() }), {
59216
+ kind: "mutation",
59217
+ auth: "admin"
59218
+ }), method(object({
59219
+ integrationId: string$2().min(1),
59220
+ keys: array(string$2().min(1))
59221
+ }), object({
59222
+ present: boolean(),
59223
+ removed: record(string$2(), unknown())
59224
+ }), {
59225
+ kind: "mutation",
59226
+ auth: "admin"
59100
59227
  });
59101
59228
  method(object({
59102
59229
  sourceUrl: string$2(),
@@ -80423,11 +80550,15 @@ discriminatedUnion("kind", [object({
80423
80550
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80424
80551
  * near F. A device that is not near one level most of the time has no floor.
80425
80552
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80426
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80427
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80553
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80554
+ * display waking): it is neither a run nor a pause. Among the remaining
80555
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80556
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80557
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80558
+ * is no run.
80428
80559
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80429
- * grid a person would type; stop dwell covers the longest pause; start dwell
80430
- * covers the longest standby blip above T.
80560
+ * grid a person would type; stop dwell covers the longest pause and the
80561
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80431
80562
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80432
80563
  * run must hold exactly one true interval and nothing may flap outside.
80433
80564
  */
@@ -80438,6 +80569,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80438
80569
  30 * SECOND_MS;
80439
80570
  60 * SECOND_MS;
80440
80571
  20 * MINUTE_MS$1;
80572
+ 90 * SECOND_MS;
80573
+ 30 * MINUTE_MS$1;
80574
+ 10 * MINUTE_MS$1;
80575
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80441
80576
  10 * SECOND_MS;
80442
80577
  10 * SECOND_MS;
80443
80578
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85757,13 +85892,6 @@ Object.freeze({
85757
85892
  access: "view",
85758
85893
  role: "admin"
85759
85894
  },
85760
- "integrations.getSettings": {
85761
- capName: "integrations",
85762
- capScope: "system",
85763
- addonId: null,
85764
- access: "view",
85765
- role: "admin"
85766
- },
85767
85895
  "integrations.list": {
85768
85896
  capName: "integrations",
85769
85897
  capScope: "system",
@@ -85771,13 +85899,6 @@ Object.freeze({
85771
85899
  access: "view",
85772
85900
  role: "admin"
85773
85901
  },
85774
- "integrations.setSettings": {
85775
- capName: "integrations",
85776
- capScope: "system",
85777
- addonId: null,
85778
- access: "create",
85779
- role: "admin"
85780
- },
85781
85902
  "integrations.testConnection": {
85782
85903
  capName: "integrations",
85783
85904
  capScope: "system",
@@ -86772,6 +86893,13 @@ Object.freeze({
86772
86893
  access: "view",
86773
86894
  role: "admin"
86774
86895
  },
86896
+ "migrations.clearLegacyIntegrationRows": {
86897
+ capName: "migrations",
86898
+ capScope: "system",
86899
+ addonId: null,
86900
+ access: "delete",
86901
+ role: "admin"
86902
+ },
86775
86903
  "migrations.declareOwner": {
86776
86904
  capName: "migrations",
86777
86905
  capScope: "system",
@@ -86779,6 +86907,13 @@ Object.freeze({
86779
86907
  access: "create",
86780
86908
  role: "admin"
86781
86909
  },
86910
+ "migrations.deleteLegacyIntegrationRowKeys": {
86911
+ capName: "migrations",
86912
+ capScope: "system",
86913
+ addonId: null,
86914
+ access: "delete",
86915
+ role: "admin"
86916
+ },
86782
86917
  "migrations.dryRun": {
86783
86918
  capName: "migrations",
86784
86919
  capScope: "system",
@@ -86814,6 +86949,20 @@ Object.freeze({
86814
86949
  access: "create",
86815
86950
  role: "admin"
86816
86951
  },
86952
+ "migrations.patchLegacyIntegrationRows": {
86953
+ capName: "migrations",
86954
+ capScope: "system",
86955
+ addonId: null,
86956
+ access: "create",
86957
+ role: "admin"
86958
+ },
86959
+ "migrations.readLegacyIntegrationRows": {
86960
+ capName: "migrations",
86961
+ capScope: "system",
86962
+ addonId: null,
86963
+ access: "view",
86964
+ role: "admin"
86965
+ },
86817
86966
  "migrations.restorePreMigration": {
86818
86967
  capName: "migrations",
86819
86968
  capScope: "system",
@@ -207342,8 +207491,8 @@ function sameConnection(a, b) {
207342
207491
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207343
207492
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207344
207493
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207345
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207346
- * 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.
207347
207496
  */
207348
207497
  var MatterBrokerRegistry = class {
207349
207498
  #logger;
@@ -209100,18 +209249,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209100
209249
  try {
209101
209250
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
209102
209251
  const surviving = new Set(mine.map((i) => i.id));
209103
- for (const integration of mine) {
209104
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
209105
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
209106
- if (brokerId === void 0) continue;
209107
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
209108
- this.ctx.logger.warn("Matter integration→broker: linked broker not found", { meta: {
209109
- integrationId: integration.id,
209110
- brokerId
209111
- } });
209112
- continue;
209113
- }
209114
- this.requireRegistry().linkIntegration(integration.id, brokerId);
209252
+ for (const broker of this.activeBrokers) {
209253
+ const integrationId = broker.integrationId;
209254
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
209255
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
209115
209256
  }
209116
209257
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
209117
209258
  if (toRemove.length === 0) return;
@@ -209152,9 +209293,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209152
209293
  },
209153
209294
  logger: this.ctx.logger,
209154
209295
  getBrokerIdForIntegration: async (id) => {
209155
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
209156
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
209157
- return brokerId;
209296
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
209297
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
209298
+ return broker.id;
209158
209299
  },
209159
209300
  listAdoptedMatter: async () => {
209160
209301
  const reg = this.ctx.kernel.deviceRegistry;
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-BaWhXZFm.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).
@@ -6593,7 +6593,7 @@ var NOT_RUN = {
6593
6593
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6594
6594
  * because the hub (and this addon's boot) must not hang on a migration.
6595
6595
  */
6596
- async function runAddonMigrations(host, migrations, logger) {
6596
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6597
6597
  const first = migrations[0];
6598
6598
  if (first === void 0) return NOT_RUN;
6599
6599
  const owner = first.meta.owner;
@@ -6622,7 +6622,9 @@ async function runAddonMigrations(host, migrations, logger) {
6622
6622
  else if (status === "blocked") blocked.push(id);
6623
6623
  else skipped.push(id);
6624
6624
  };
6625
+ const selected = options.only === void 0 ? null : new Set(options.only);
6625
6626
  for (const step of declaration.steps) {
6627
+ if (selected !== null && !selected.has(step.id)) continue;
6626
6628
  const bound = byId.get(step.id);
6627
6629
  if (bound === void 0 || step.action === "skip") {
6628
6630
  skipped.push(step.id);
@@ -6800,6 +6802,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6800
6802
  }
6801
6803
  return out;
6802
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
+ ];
6803
6811
  /**
6804
6812
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6805
6813
  *
@@ -7691,6 +7699,13 @@ var BaseAddon = class {
7691
7699
  */
7692
7700
  secretStatePaths = [];
7693
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();
7694
7709
  _deviceSecretTree = null;
7695
7710
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7696
7711
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7760,6 +7775,7 @@ var BaseAddon = class {
7760
7775
  this._ctx = context;
7761
7776
  this._secretSettings = null;
7762
7777
  }
7778
+ await this.loadScopedSchemas();
7763
7779
  await this.resolveConfig();
7764
7780
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7765
7781
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7769,6 +7785,7 @@ var BaseAddon = class {
7769
7785
  const normalized = normalizeAddonInitResult(result);
7770
7786
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7771
7787
  this._registeredCapNames = providers.map((p) => p.capability.name);
7788
+ await this.loadScopedSchemas();
7772
7789
  return normalized;
7773
7790
  }
7774
7791
  /**
@@ -7782,14 +7799,18 @@ var BaseAddon = class {
7782
7799
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7783
7800
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7784
7801
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7802
+ *
7803
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7804
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7805
+ * omitted applied ids "newer").
7785
7806
  */
7786
- async runMigrations(migrations) {
7807
+ async runMigrations(migrations, options) {
7787
7808
  const api = this.ctx.api.migrations;
7788
7809
  const report = await runAddonMigrations({
7789
7810
  declareOwner: (input) => api.declareOwner.mutate(input),
7790
7811
  startMigration: (input) => api.startMigration.mutate(input),
7791
7812
  finishMigration: (input) => api.finishMigration.mutate(input)
7792
- }, migrations, this.ctx.logger);
7813
+ }, migrations, this.ctx.logger, options);
7793
7814
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7794
7815
  return report;
7795
7816
  }
@@ -7853,6 +7874,16 @@ var BaseAddon = class {
7853
7874
  schema(schema) {
7854
7875
  return schema;
7855
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
+ }
7856
7887
  /** Override to provide global-level settings UI schema. */
7857
7888
  globalSettingsSchema(_cap) {
7858
7889
  return null;
@@ -7896,7 +7927,8 @@ var BaseAddon = class {
7896
7927
  return [];
7897
7928
  }
7898
7929
  async getGlobalSettings(overlay, cap, nodeId) {
7899
- const schema = this.globalSettingsSchema(cap);
7930
+ await this.loadScopedSchemas(cap);
7931
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7900
7932
  if (!schema) return { sections: [] };
7901
7933
  const projected = await this.readRawProjectedStore(nodeId, cap);
7902
7934
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7931,10 +7963,9 @@ var BaseAddon = class {
7931
7963
  *
7932
7964
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7933
7965
  * needs its connection fields, the default integration the addon's own sections. It is the
7934
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7935
- * unchanged (`integrations.setSettings` for an account integration).
7966
+ * SHAPE of the form only — the values live on the instance record.
7936
7967
  */
7937
- async integrationSettingsSchema(_instance) {
7968
+ async integrationFormSchema(_instance) {
7938
7969
  return null;
7939
7970
  }
7940
7971
  /**
@@ -7980,7 +8011,7 @@ var BaseAddon = class {
7980
8011
  * per-instance schema when it declares one, else its global schema (what they always showed).
7981
8012
  */
7982
8013
  async getIntegrationFormSchema(instance, nodeId) {
7983
- const scoped = await this.integrationSettingsSchema(instance);
8014
+ const scoped = await this.integrationFormSchema(instance);
7984
8015
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7985
8016
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7986
8017
  }
@@ -8028,6 +8059,7 @@ var BaseAddon = class {
8028
8059
  * resolved blob back is a no-op for every unchanged secret.
8029
8060
  */
8030
8061
  async resolveGlobalStore(nodeId, cap) {
8062
+ await this.loadScopedSchemas(cap);
8031
8063
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8032
8064
  return this.projectForNode(raw, nodeId, cap);
8033
8065
  }
@@ -8038,6 +8070,7 @@ var BaseAddon = class {
8038
8070
  }
8039
8071
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8040
8072
  async readRawProjectedStore(nodeId, cap) {
8073
+ await this.loadScopedSchemas(cap);
8041
8074
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8042
8075
  return this.projectForNode(raw, nodeId, cap);
8043
8076
  }
@@ -8140,6 +8173,7 @@ var BaseAddon = class {
8140
8173
  await this.applyGlobalPatch(patch, nodeId);
8141
8174
  }
8142
8175
  async applyGlobalPatch(barePatch, nodeId) {
8176
+ await this.loadScopedSchemas();
8143
8177
  const keys = this.perNodeKeys();
8144
8178
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8145
8179
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8153,7 +8187,8 @@ var BaseAddon = class {
8153
8187
  await this.resolveConfig();
8154
8188
  await this.onConfigChanged();
8155
8189
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8156
- 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)]);
8157
8192
  }
8158
8193
  /**
8159
8194
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8161,14 +8196,67 @@ var BaseAddon = class {
8161
8196
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8162
8197
  * settings API behaves exactly like the legacy node-agnostic one.
8163
8198
  */
8164
- _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
+ */
8165
8249
  perNodeKeys(cap) {
8166
8250
  const cacheKey = cap ?? "";
8167
- const cached = this._perNodeKeysCache.get(cacheKey);
8251
+ const cached = this._nodeKeysByCap.get(cacheKey);
8168
8252
  if (cached) return cached;
8169
- const schema = this.globalSettingsSchema(cap);
8170
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8171
- 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);
8172
8260
  return keys;
8173
8261
  }
8174
8262
  /**
@@ -8178,10 +8266,9 @@ var BaseAddon = class {
8178
8266
  * time to return its response before the addon is torn down and
8179
8267
  * re-initialised by `AddonRegistryService.restartAddon`.
8180
8268
  */
8181
- maybeAutoRestart(patchKeys, schema) {
8182
- if (!schema) return;
8269
+ maybeAutoRestart(patchKeys, schemas) {
8183
8270
  const restartKeys = /* @__PURE__ */ new Set();
8184
- 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) {
8185
8272
  if (field.type === "separator" || field.type === "info") continue;
8186
8273
  if (field.requiresRestart) restartKeys.add(field.key);
8187
8274
  }
@@ -8377,12 +8464,14 @@ var BaseAddon = class {
8377
8464
  * here, in the addon's own process, so the hub keeps no copy of it.
8378
8465
  */
8379
8466
  async getDeclaredSettingsKeys() {
8467
+ await this.loadScopedSchemas();
8380
8468
  return { keys: [...this.declaredKeySet()].toSorted() };
8381
8469
  }
8382
8470
  declaredKeySet() {
8383
8471
  const keys = new Set(Object.keys(this.defaults));
8384
8472
  const schema = this.globalSettingsSchema();
8385
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);
8386
8475
  for (const key of this._declaredStateKeys) keys.add(key);
8387
8476
  for (const path of this.secretStatePaths) {
8388
8477
  const root = path.split(/[.[]/, 1)[0];
@@ -8478,7 +8567,11 @@ var BaseAddon = class {
8478
8567
  }
8479
8568
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8480
8569
  addonSecretTree() {
8481
- 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
+ }
8482
8575
  return this._addonSecretTree;
8483
8576
  }
8484
8577
  deviceSecretTree() {
@@ -8728,6 +8821,23 @@ function dropPerNodeFields(fields) {
8728
8821
  }
8729
8822
  return kept;
8730
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
+ }
8731
8841
  function collectFieldKeys(fields) {
8732
8842
  const collected = [];
8733
8843
  for (const field of fields) {
@@ -9254,6 +9364,7 @@ function defineEntryKind(name, spec) {
9254
9364
  class: "config",
9255
9365
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9256
9366
  retention: { none: "an entry goes only when the operator deletes it" },
9367
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9257
9368
  ...spec.database === void 0 ? {} : { database: spec.database },
9258
9369
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9259
9370
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -11923,7 +12034,7 @@ object({
11923
12034
  params: record(string$2(), union([string$2(), number()])).optional()
11924
12035
  });
11925
12036
  //#endregion
11926
- //#region ../types/dist/composition-CRNwcFYY.mjs
12037
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11927
12038
  /**
11928
12039
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11929
12040
  *
@@ -14504,7 +14615,13 @@ var MigrationLedgerEntrySchema = object({
14504
14615
  * joined by the engine from the catalogues it knows this boot. Absent when no
14505
14616
  * catalogue lists the id (a newer database).
14506
14617
  */
14507
- meta: MigrationMetaSchema.optional()
14618
+ meta: MigrationMetaSchema.optional(),
14619
+ /**
14620
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14621
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14622
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14623
+ */
14624
+ ownerNotLoaded: boolean().optional()
14508
14625
  });
14509
14626
  /** A catalogued migration that is not applied, with where it stands. */
14510
14627
  var MigrationPendingEntrySchema = object({
@@ -50239,6 +50356,9 @@ var RunningProposalEvidenceSchema = object({
50239
50356
  runsFound: number().int().nonnegative(),
50240
50357
  pausesFound: number().int().nonnegative(),
50241
50358
  longestPauseMs: number().nonnegative(),
50359
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50360
+ pulsesFound: number().int().nonnegative(),
50361
+ longestPulseMs: number().nonnegative(),
50242
50362
  idleFloorW: number(),
50243
50363
  noiseCeilingW: number(),
50244
50364
  plateauW: number(),
@@ -56084,11 +56204,7 @@ var CreateIntegrationInputSchema = object({
56084
56204
  info: record(string$2(), unknown()).optional(),
56085
56205
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56086
56206
  originStableId: string$2().optional(),
56087
- settings: record(string$2(), unknown()).optional(),
56088
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56089
- * the keychain, not the table (D787). */
56090
- secretKeys: array(string$2()).optional(),
56091
- skipRestart: boolean().optional()
56207
+ settings: record(string$2(), unknown()).optional()
56092
56208
  });
56093
56209
  var UpdateIntegrationInputSchema = object({
56094
56210
  id: string$2(),
@@ -56109,16 +56225,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
56109
56225
  }), method(object({ id: string$2() }), DeleteIntegrationResultSchema, {
56110
56226
  kind: "mutation",
56111
56227
  auth: "admin"
56112
- }), method(object({ id: string$2() }), record(string$2(), unknown()), { auth: "admin" }), method(object({
56113
- id: string$2(),
56114
- settings: record(string$2(), unknown()),
56115
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56116
- secretKeys: array(string$2()).optional(),
56117
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56118
- skipRestart: boolean().optional()
56119
- }), object({ success: boolean() }), {
56120
- kind: "mutation",
56121
- auth: "admin"
56122
56228
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56123
56229
  addonId: string$2(),
56124
56230
  settings: record(string$2(), unknown())
@@ -59095,6 +59201,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59095
59201
  }), {
59096
59202
  kind: "mutation",
59097
59203
  auth: "admin"
59204
+ }), method(object({ integrationId: string$2().min(1) }), record(string$2(), unknown()), { auth: "admin" }), method(object({ integrationId: string$2().min(1) }), object({
59205
+ rows: number().int().nonnegative(),
59206
+ secrets: number().int().nonnegative()
59207
+ }), {
59208
+ kind: "mutation",
59209
+ auth: "admin"
59210
+ }), method(object({
59211
+ integrationId: string$2().min(1),
59212
+ patch: record(string$2(), unknown())
59213
+ }), object({ written: boolean() }), {
59214
+ kind: "mutation",
59215
+ auth: "admin"
59216
+ }), method(object({
59217
+ integrationId: string$2().min(1),
59218
+ keys: array(string$2().min(1))
59219
+ }), object({
59220
+ present: boolean(),
59221
+ removed: record(string$2(), unknown())
59222
+ }), {
59223
+ kind: "mutation",
59224
+ auth: "admin"
59098
59225
  });
59099
59226
  method(object({
59100
59227
  sourceUrl: string$2(),
@@ -80421,11 +80548,15 @@ discriminatedUnion("kind", [object({
80421
80548
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80422
80549
  * near F. A device that is not near one level most of the time has no floor.
80423
80550
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80424
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80425
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80551
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80552
+ * display waking): it is neither a run nor a pause. Among the remaining
80553
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80554
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80555
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80556
+ * is no run.
80426
80557
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80427
- * grid a person would type; stop dwell covers the longest pause; start dwell
80428
- * covers the longest standby blip above T.
80558
+ * grid a person would type; stop dwell covers the longest pause and the
80559
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80429
80560
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80430
80561
  * run must hold exactly one true interval and nothing may flap outside.
80431
80562
  */
@@ -80436,6 +80567,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80436
80567
  30 * SECOND_MS;
80437
80568
  60 * SECOND_MS;
80438
80569
  20 * MINUTE_MS$1;
80570
+ 90 * SECOND_MS;
80571
+ 30 * MINUTE_MS$1;
80572
+ 10 * MINUTE_MS$1;
80573
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80439
80574
  10 * SECOND_MS;
80440
80575
  10 * SECOND_MS;
80441
80576
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85755,13 +85890,6 @@ Object.freeze({
85755
85890
  access: "view",
85756
85891
  role: "admin"
85757
85892
  },
85758
- "integrations.getSettings": {
85759
- capName: "integrations",
85760
- capScope: "system",
85761
- addonId: null,
85762
- access: "view",
85763
- role: "admin"
85764
- },
85765
85893
  "integrations.list": {
85766
85894
  capName: "integrations",
85767
85895
  capScope: "system",
@@ -85769,13 +85897,6 @@ Object.freeze({
85769
85897
  access: "view",
85770
85898
  role: "admin"
85771
85899
  },
85772
- "integrations.setSettings": {
85773
- capName: "integrations",
85774
- capScope: "system",
85775
- addonId: null,
85776
- access: "create",
85777
- role: "admin"
85778
- },
85779
85900
  "integrations.testConnection": {
85780
85901
  capName: "integrations",
85781
85902
  capScope: "system",
@@ -86770,6 +86891,13 @@ Object.freeze({
86770
86891
  access: "view",
86771
86892
  role: "admin"
86772
86893
  },
86894
+ "migrations.clearLegacyIntegrationRows": {
86895
+ capName: "migrations",
86896
+ capScope: "system",
86897
+ addonId: null,
86898
+ access: "delete",
86899
+ role: "admin"
86900
+ },
86773
86901
  "migrations.declareOwner": {
86774
86902
  capName: "migrations",
86775
86903
  capScope: "system",
@@ -86777,6 +86905,13 @@ Object.freeze({
86777
86905
  access: "create",
86778
86906
  role: "admin"
86779
86907
  },
86908
+ "migrations.deleteLegacyIntegrationRowKeys": {
86909
+ capName: "migrations",
86910
+ capScope: "system",
86911
+ addonId: null,
86912
+ access: "delete",
86913
+ role: "admin"
86914
+ },
86780
86915
  "migrations.dryRun": {
86781
86916
  capName: "migrations",
86782
86917
  capScope: "system",
@@ -86812,6 +86947,20 @@ Object.freeze({
86812
86947
  access: "create",
86813
86948
  role: "admin"
86814
86949
  },
86950
+ "migrations.patchLegacyIntegrationRows": {
86951
+ capName: "migrations",
86952
+ capScope: "system",
86953
+ addonId: null,
86954
+ access: "create",
86955
+ role: "admin"
86956
+ },
86957
+ "migrations.readLegacyIntegrationRows": {
86958
+ capName: "migrations",
86959
+ capScope: "system",
86960
+ addonId: null,
86961
+ access: "view",
86962
+ role: "admin"
86963
+ },
86815
86964
  "migrations.restorePreMigration": {
86816
86965
  capName: "migrations",
86817
86966
  capScope: "system",
@@ -207340,8 +207489,8 @@ function sameConnection(a, b) {
207340
207489
  * Manages N live Matter fabric connections ("brokers"), each backed by a
207341
207490
  * {@link MatterIntegrationManager}. Allocates stable ids (`matter_001`, …), CRUD,
207342
207491
  * an integration FK index, lifecycle helpers. Mirrors the Ecowitt / Dreame
207343
- * registry. Because `instanceMode` is `unique`, the common case is exactly one
207344
- * 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.
207345
207494
  */
207346
207495
  var MatterBrokerRegistry = class {
207347
207496
  #logger;
@@ -209098,18 +209247,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209098
209247
  try {
209099
209248
  const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
209100
209249
  const surviving = new Set(mine.map((i) => i.id));
209101
- for (const integration of mine) {
209102
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
209103
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
209104
- if (brokerId === void 0) continue;
209105
- if (!this.activeBrokers.some((b) => b.id === brokerId)) {
209106
- this.ctx.logger.warn("Matter integration→broker: linked broker not found", { meta: {
209107
- integrationId: integration.id,
209108
- brokerId
209109
- } });
209110
- continue;
209111
- }
209112
- this.requireRegistry().linkIntegration(integration.id, brokerId);
209250
+ for (const broker of this.activeBrokers) {
209251
+ const integrationId = broker.integrationId;
209252
+ if (integrationId === void 0 || !surviving.has(integrationId)) continue;
209253
+ this.requireRegistry().linkIntegration(integrationId, broker.id);
209113
209254
  }
209114
209255
  const toRemove = this.activeBrokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
209115
209256
  if (toRemove.length === 0) return;
@@ -209150,9 +209291,9 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
209150
209291
  },
209151
209292
  logger: this.ctx.logger,
209152
209293
  getBrokerIdForIntegration: async (id) => {
209153
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
209154
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
209155
- return brokerId;
209294
+ const broker = this.activeBrokers.find((entry) => entry.integrationId === id);
209295
+ if (broker === void 0) throw new Error(`integration ${id} has no linked brokerId`);
209296
+ return broker.id;
209156
209297
  },
209157
209298
  listAdoptedMatter: async () => {
209158
209299
  const reg = this.ctx.kernel.deviceRegistry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.156",
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.762"
68
+ "server": ">=1.2.776"
69
69
  }
70
70
  },
71
71
  "files": [