@camstack/addon-mqtt-broker 1.2.149 → 1.2.151

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.
@@ -6021,7 +6021,7 @@ var ZodIssueCode = {
6021
6021
  var ZodFirstPartyTypeKind;
6022
6022
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6023
6023
  //#endregion
6024
- //#region ../types/dist/sleep-BKClVk0K.mjs
6024
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
6025
6025
  /**
6026
6026
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6027
6027
  * window to float samples (D455).
@@ -6621,7 +6621,7 @@ var NOT_RUN = {
6621
6621
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6622
6622
  * because the hub (and this addon's boot) must not hang on a migration.
6623
6623
  */
6624
- async function runAddonMigrations(host, migrations, logger) {
6624
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6625
6625
  const first = migrations[0];
6626
6626
  if (first === void 0) return NOT_RUN;
6627
6627
  const owner = first.meta.owner;
@@ -6650,7 +6650,9 @@ async function runAddonMigrations(host, migrations, logger) {
6650
6650
  else if (status === "blocked") blocked.push(id);
6651
6651
  else skipped.push(id);
6652
6652
  };
6653
+ const selected = options.only === void 0 ? null : new Set(options.only);
6653
6654
  for (const step of declaration.steps) {
6655
+ if (selected !== null && !selected.has(step.id)) continue;
6654
6656
  const bound = byId.get(step.id);
6655
6657
  if (bound === void 0 || step.action === "skip") {
6656
6658
  skipped.push(step.id);
@@ -6828,6 +6830,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6828
6830
  }
6829
6831
  return out;
6830
6832
  }
6833
+ /** Every scope, in the order the guard and the docs list them. */
6834
+ var SCOPED_SETTINGS_SCOPES = [
6835
+ "cluster",
6836
+ "device-defaults",
6837
+ "node"
6838
+ ];
6831
6839
  /**
6832
6840
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6833
6841
  *
@@ -7719,6 +7727,13 @@ var BaseAddon = class {
7719
7727
  */
7720
7728
  secretStatePaths = [];
7721
7729
  _addonSecretTree = null;
7730
+ /**
7731
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7732
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7733
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7734
+ */
7735
+ _scopedByCap = /* @__PURE__ */ new Map();
7736
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7722
7737
  _deviceSecretTree = null;
7723
7738
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7724
7739
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7788,6 +7803,7 @@ var BaseAddon = class {
7788
7803
  this._ctx = context;
7789
7804
  this._secretSettings = null;
7790
7805
  }
7806
+ await this.loadScopedSchemas();
7791
7807
  await this.resolveConfig();
7792
7808
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7793
7809
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7797,6 +7813,7 @@ var BaseAddon = class {
7797
7813
  const normalized = normalizeAddonInitResult(result);
7798
7814
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7799
7815
  this._registeredCapNames = providers.map((p) => p.capability.name);
7816
+ await this.loadScopedSchemas();
7800
7817
  return normalized;
7801
7818
  }
7802
7819
  /**
@@ -7810,14 +7827,18 @@ var BaseAddon = class {
7810
7827
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7811
7828
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7812
7829
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7830
+ *
7831
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7832
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7833
+ * omitted applied ids "newer").
7813
7834
  */
7814
- async runMigrations(migrations) {
7835
+ async runMigrations(migrations, options) {
7815
7836
  const api = this.ctx.api.migrations;
7816
7837
  const report = await runAddonMigrations({
7817
7838
  declareOwner: (input) => api.declareOwner.mutate(input),
7818
7839
  startMigration: (input) => api.startMigration.mutate(input),
7819
7840
  finishMigration: (input) => api.finishMigration.mutate(input)
7820
- }, migrations, this.ctx.logger);
7841
+ }, migrations, this.ctx.logger, options);
7821
7842
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7822
7843
  return report;
7823
7844
  }
@@ -7881,6 +7902,16 @@ var BaseAddon = class {
7881
7902
  schema(schema) {
7882
7903
  return schema;
7883
7904
  }
7905
+ /**
7906
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7907
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7908
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7909
+ * must already know which keys are per node. Names the providers register at init are added
7910
+ * once known.
7911
+ */
7912
+ scopedSettingsCapabilities() {
7913
+ return [];
7914
+ }
7884
7915
  /** Override to provide global-level settings UI schema. */
7885
7916
  globalSettingsSchema(_cap) {
7886
7917
  return null;
@@ -7924,7 +7955,8 @@ var BaseAddon = class {
7924
7955
  return [];
7925
7956
  }
7926
7957
  async getGlobalSettings(overlay, cap, nodeId) {
7927
- const schema = this.globalSettingsSchema(cap);
7958
+ await this.loadScopedSchemas(cap);
7959
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7928
7960
  if (!schema) return { sections: [] };
7929
7961
  const projected = await this.readRawProjectedStore(nodeId, cap);
7930
7962
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7959,10 +7991,9 @@ var BaseAddon = class {
7959
7991
  *
7960
7992
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7961
7993
  * needs its connection fields, the default integration the addon's own sections. It is the
7962
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7963
- * unchanged (`integrations.setSettings` for an account integration).
7994
+ * SHAPE of the form only — the values live on the instance record.
7964
7995
  */
7965
- async integrationSettingsSchema(_instance) {
7996
+ async integrationFormSchema(_instance) {
7966
7997
  return null;
7967
7998
  }
7968
7999
  /**
@@ -8008,7 +8039,7 @@ var BaseAddon = class {
8008
8039
  * per-instance schema when it declares one, else its global schema (what they always showed).
8009
8040
  */
8010
8041
  async getIntegrationFormSchema(instance, nodeId) {
8011
- const scoped = await this.integrationSettingsSchema(instance);
8042
+ const scoped = await this.integrationFormSchema(instance);
8012
8043
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8013
8044
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8014
8045
  }
@@ -8056,6 +8087,7 @@ var BaseAddon = class {
8056
8087
  * resolved blob back is a no-op for every unchanged secret.
8057
8088
  */
8058
8089
  async resolveGlobalStore(nodeId, cap) {
8090
+ await this.loadScopedSchemas(cap);
8059
8091
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8060
8092
  return this.projectForNode(raw, nodeId, cap);
8061
8093
  }
@@ -8066,6 +8098,7 @@ var BaseAddon = class {
8066
8098
  }
8067
8099
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8068
8100
  async readRawProjectedStore(nodeId, cap) {
8101
+ await this.loadScopedSchemas(cap);
8069
8102
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8070
8103
  return this.projectForNode(raw, nodeId, cap);
8071
8104
  }
@@ -8158,12 +8191,17 @@ var BaseAddon = class {
8158
8191
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8159
8192
  const patch = {};
8160
8193
  for (const key of keys) {
8194
+ if (tree.get(key)?.kind === "array") {
8195
+ patch[key] = [];
8196
+ continue;
8197
+ }
8161
8198
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8162
8199
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8163
8200
  }
8164
8201
  await this.applyGlobalPatch(patch, nodeId);
8165
8202
  }
8166
8203
  async applyGlobalPatch(barePatch, nodeId) {
8204
+ await this.loadScopedSchemas();
8167
8205
  const keys = this.perNodeKeys();
8168
8206
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8169
8207
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8177,7 +8215,8 @@ var BaseAddon = class {
8177
8215
  await this.resolveConfig();
8178
8216
  await this.onConfigChanged();
8179
8217
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8180
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8218
+ const legacy = this.globalSettingsSchema();
8219
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8181
8220
  }
8182
8221
  /**
8183
8222
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8185,14 +8224,67 @@ var BaseAddon = class {
8185
8224
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8186
8225
  * settings API behaves exactly like the legacy node-agnostic one.
8187
8226
  */
8188
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8227
+ /**
8228
+ * Load the scoped schemas of every capability this addon declares (the override and the
8229
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8230
+ * derived from the previous set. Idempotent.
8231
+ */
8232
+ async loadScopedSchemas(extraCap) {
8233
+ const caps = new Set([
8234
+ ...this.scopedSettingsCapabilities(),
8235
+ ...this._registeredCapNames,
8236
+ ...extraCap === void 0 ? [] : [extraCap]
8237
+ ]);
8238
+ let added = false;
8239
+ for (const cap of caps) {
8240
+ if (this._scopedByCap.has(cap)) continue;
8241
+ const loaded = [];
8242
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8243
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8244
+ if (schema !== null) loaded.push({
8245
+ scope,
8246
+ schema
8247
+ });
8248
+ }
8249
+ this._scopedByCap.set(cap, loaded);
8250
+ added = true;
8251
+ }
8252
+ if (!added) return;
8253
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8254
+ this._addonSecretTree = null;
8255
+ }
8256
+ /**
8257
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8258
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8259
+ */
8260
+ scopedFormSchema(cap) {
8261
+ const byId = /* @__PURE__ */ new Map();
8262
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8263
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8264
+ }
8265
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8266
+ loadedScopedSchemas(cap) {
8267
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8268
+ return [...this._scopedByCap.values()].flat();
8269
+ }
8270
+ /**
8271
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8272
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8273
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8274
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8275
+ * Call {@link loadScopedSchemas} first (the async seams do).
8276
+ */
8189
8277
  perNodeKeys(cap) {
8190
8278
  const cacheKey = cap ?? "";
8191
- const cached = this._perNodeKeysCache.get(cacheKey);
8279
+ const cached = this._nodeKeysByCap.get(cacheKey);
8192
8280
  if (cached) return cached;
8193
- const schema = this.globalSettingsSchema(cap);
8194
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8195
- this._perNodeKeysCache.set(cacheKey, keys);
8281
+ const legacy = this.globalSettingsSchema(cap);
8282
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8283
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8284
+ if (scope !== "node") continue;
8285
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8286
+ }
8287
+ this._nodeKeysByCap.set(cacheKey, keys);
8196
8288
  return keys;
8197
8289
  }
8198
8290
  /**
@@ -8202,10 +8294,9 @@ var BaseAddon = class {
8202
8294
  * time to return its response before the addon is torn down and
8203
8295
  * re-initialised by `AddonRegistryService.restartAddon`.
8204
8296
  */
8205
- maybeAutoRestart(patchKeys, schema) {
8206
- if (!schema) return;
8297
+ maybeAutoRestart(patchKeys, schemas) {
8207
8298
  const restartKeys = /* @__PURE__ */ new Set();
8208
- for (const section of schema.sections) for (const field of section.fields) {
8299
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8209
8300
  if (field.type === "separator" || field.type === "info") continue;
8210
8301
  if (field.requiresRestart) restartKeys.add(field.key);
8211
8302
  }
@@ -8401,12 +8492,14 @@ var BaseAddon = class {
8401
8492
  * here, in the addon's own process, so the hub keeps no copy of it.
8402
8493
  */
8403
8494
  async getDeclaredSettingsKeys() {
8495
+ await this.loadScopedSchemas();
8404
8496
  return { keys: [...this.declaredKeySet()].toSorted() };
8405
8497
  }
8406
8498
  declaredKeySet() {
8407
8499
  const keys = new Set(Object.keys(this.defaults));
8408
8500
  const schema = this.globalSettingsSchema();
8409
8501
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8502
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8410
8503
  for (const key of this._declaredStateKeys) keys.add(key);
8411
8504
  for (const path of this.secretStatePaths) {
8412
8505
  const root = path.split(/[.[]/, 1)[0];
@@ -8502,7 +8595,11 @@ var BaseAddon = class {
8502
8595
  }
8503
8596
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8504
8597
  addonSecretTree() {
8505
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8598
+ if (this._addonSecretTree === null) {
8599
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8600
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8601
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8602
+ }
8506
8603
  return this._addonSecretTree;
8507
8604
  }
8508
8605
  deviceSecretTree() {
@@ -8752,6 +8849,23 @@ function dropPerNodeFields(fields) {
8752
8849
  }
8753
8850
  return kept;
8754
8851
  }
8852
+ /** The keys of the value-holding fields, recursing into layout containers. */
8853
+ function collectValueFieldKeys(fields) {
8854
+ const collected = [];
8855
+ for (const field of fields) {
8856
+ if (field.type === "group") {
8857
+ collected.push(...collectValueFieldKeys(field.fields));
8858
+ continue;
8859
+ }
8860
+ if (field.type === "sub-tabs") {
8861
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8862
+ continue;
8863
+ }
8864
+ if (field.type === "separator" || field.type === "info") continue;
8865
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8866
+ }
8867
+ return collected;
8868
+ }
8755
8869
  function collectFieldKeys(fields) {
8756
8870
  const collected = [];
8757
8871
  for (const field of fields) {
@@ -8853,6 +8967,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8853
8967
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8854
8968
  * (`auth.accessToken`, `headers[].value`).
8855
8969
  *
8970
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8971
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8972
+ * key releases exactly that key's secret.
8973
+ *
8856
8974
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8857
8975
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8858
8976
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8865,11 +8983,13 @@ function parseSecretPath(path) {
8865
8983
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8866
8984
  return path.split(".").map((raw) => {
8867
8985
  const each = raw.endsWith("[]");
8868
- const key = each ? raw.slice(0, -2) : raw;
8986
+ const map = raw.endsWith("{}");
8987
+ const key = each || map ? raw.slice(0, -2) : raw;
8869
8988
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8870
8989
  return {
8871
8990
  key,
8872
- each
8991
+ each,
8992
+ map
8873
8993
  };
8874
8994
  });
8875
8995
  }
@@ -8921,6 +9041,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8921
9041
  [segment.key]: mapped
8922
9042
  };
8923
9043
  }
9044
+ if (segment.map) {
9045
+ if (!isRecord$4(child)) return node;
9046
+ const kept = Object.entries(child).map(([mapKey, value]) => {
9047
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
9048
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
9049
+ return [mapKey, visit({
9050
+ declared,
9051
+ concrete,
9052
+ value
9053
+ })];
9054
+ }).filter(([, value]) => value !== REMOVE_LEAF);
9055
+ return {
9056
+ ...node,
9057
+ [segment.key]: Object.fromEntries(kept)
9058
+ };
9059
+ }
8924
9060
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8925
9061
  if (last) {
8926
9062
  const replacement = visit({
@@ -9062,7 +9198,8 @@ function isZodSecret(schema) {
9062
9198
  }
9063
9199
  /**
9064
9200
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9065
- * element of this array" (`connection.password`, `rows[].token`). A declared
9201
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9202
+ * map" (`applianceSecrets{}`). A declared
9066
9203
  * object or array is reported as one path and not descended into — everything
9067
9204
  * under it is secret.
9068
9205
  */
@@ -9073,6 +9210,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9073
9210
  const def = defOf(layer);
9074
9211
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9075
9212
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9213
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9076
9214
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9077
9215
  else if (def.type === "intersection") {
9078
9216
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9212,7 +9350,7 @@ function defineRecordKinds(specs) {
9212
9350
  retention: spec.retention,
9213
9351
  deviceRemoval: spec.deviceRemoval,
9214
9352
  maxDataBytes: spec.maxDataBytes,
9215
- secretPaths: [...secretPaths],
9353
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9216
9354
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9217
9355
  transactFamily: spec.transactFamily,
9218
9356
  entry: spec.entry === void 0 ? void 0 : {
@@ -9254,9 +9392,11 @@ function defineEntryKind(name, spec) {
9254
9392
  class: "config",
9255
9393
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9256
9394
  retention: { none: "an entry goes only when the operator deletes it" },
9395
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9257
9396
  ...spec.database === void 0 ? {} : { database: spec.database },
9258
9397
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9259
9398
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9399
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9260
9400
  entry: {
9261
9401
  title: spec.title,
9262
9402
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11918,7 +12058,7 @@ object({
11918
12058
  params: record(string(), union([string(), number()])).optional()
11919
12059
  });
11920
12060
  //#endregion
11921
- //#region ../types/dist/composition-BodkvOV0.mjs
12061
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11922
12062
  /**
11923
12063
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11924
12064
  *
@@ -12681,38 +12821,6 @@ function validateExpressionSource(src) {
12681
12821
  return null;
12682
12822
  }
12683
12823
  /**
12684
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12685
- *
12686
- * Standby of the control board is under 5 W; a motor or a heater is far
12687
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12688
- * a cycle, so running starts only after 30 s above the line.
12689
- *
12690
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12691
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12692
- * report "finished" in the middle of the program, so the stop dwell is
12693
- * 10 minutes on every type. The number is stored per type so one machine
12694
- * can move later without moving the others.
12695
- */
12696
- var APPLIANCE_TEMPLATE_ID = "appliance";
12697
- var APPLIANCE_KINDS = [
12698
- "washer",
12699
- "dryer",
12700
- "dishwasher"
12701
- ];
12702
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12703
- /**
12704
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12705
- *
12706
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12707
- * (D967), so the heater switches exactly as the plug does. Running is the
12708
- * plug being on; when a power reading is also named, running is the draw
12709
- * staying above the line instead (a plug left on with the stove's own
12710
- * thermostat open is not heating). 50 W is far above any standby and below
12711
- * the smallest radiator.
12712
- */
12713
- var HEATER_TEMPLATE_ID = "heater";
12714
- DeviceType.Heater;
12715
- /**
12716
12824
  * Statistics and integrals over a history read (D944).
12717
12825
  *
12718
12826
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12856,34 +12964,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12856
12964
  /** Actuator groups only. Readings leave this unset. */
12857
12965
  active: ActiveCriterionSchema.optional()
12858
12966
  }),
12859
- object({
12860
- kind: literal("appliance"),
12861
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12862
- appliance: _enum(APPLIANCE_KINDS),
12863
- thresholdWatts: number().positive(),
12864
- startDwellMs: number().int().nonnegative(),
12865
- stopDwellMs: number().int().nonnegative(),
12866
- member: HelperMemberSchema,
12867
- /**
12868
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12869
- * composition carries a `power-source` feature, so the hub can cut and restore
12870
- * that plug (power-source link of a NEW-target composition).
12871
- */
12872
- powerSwitch: HelperMemberSchema.optional(),
12873
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12874
- offSeconds: number().optional()
12875
- }),
12876
- object({
12877
- kind: literal("heater"),
12878
- templateId: literal(HEATER_TEMPLATE_ID),
12879
- /** The plug, read and commanded at `switch.on`. */
12880
- plug: HelperMemberSchema,
12881
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12882
- meter: HelperMemberSchema.optional(),
12883
- thresholdWatts: number().positive(),
12884
- startDwellMs: number().int().nonnegative(),
12885
- stopDwellMs: number().int().nonnegative()
12886
- }),
12887
12967
  object({
12888
12968
  kind: literal("statistic"),
12889
12969
  stat: _enum(STATISTIC_STATS),
@@ -13107,7 +13187,7 @@ var CompositionSchema = object({
13107
13187
  kind: literal("existing"),
13108
13188
  device: CompositionSourceRefSchema
13109
13189
  })]),
13110
- features: array(CompositionFeatureSchema).min(1).max(16),
13190
+ features: array(CompositionFeatureSchema).min(0).max(16),
13111
13191
  /**
13112
13192
  * Present when the block is a helper template. Absent on a free
13113
13193
  * composition (the room example, device 4603). The editor reads this,
@@ -13325,11 +13405,49 @@ var IntegrationInstanceRefSchema = object({
13325
13405
  ]),
13326
13406
  candidateStableId: string().optional()
13327
13407
  });
13408
+ /**
13409
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13410
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13411
+ *
13412
+ * Every instance is one record of the addon's own kind, keyed by the
13413
+ * `integrations` registry id. The records door does the secret work: a field the
13414
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13415
+ * deleting the row deletes the secret, and one row's value is never another's.
13416
+ *
13417
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13418
+ * token or the view the form was given KEEP the stored secret;
13419
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13420
+ */
13328
13421
  /** The most instances one addon lists (an integrations page, not a table). */
13329
13422
  var INSTANCES_LIST_MAX = 1e3;
13330
13423
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13331
13424
  function topLevelSecretKeys(paths) {
13332
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13425
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13426
+ }
13427
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13428
+ function secretMapKeys(paths) {
13429
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13430
+ }
13431
+ /**
13432
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13433
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13434
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13435
+ */
13436
+ function secretMapPatch(value) {
13437
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13438
+ const out = {};
13439
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13440
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13441
+ return Object.keys(out).length > 0 ? out : null;
13442
+ }
13443
+ /** Does the kind's body schema carry an `enabled` field? */
13444
+ function declaresEnabled(kind) {
13445
+ const json = toJSONSchema(kind.schema, {
13446
+ unrepresentable: "any",
13447
+ io: "input"
13448
+ });
13449
+ const properties = Reflect.get(json, "properties");
13450
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13333
13451
  }
13334
13452
  var RecordIntegrationInstances = class {
13335
13453
  records;
@@ -13337,14 +13455,20 @@ var RecordIntegrationInstances = class {
13337
13455
  hooks;
13338
13456
  handle;
13339
13457
  secretKeys;
13458
+ secretMaps;
13340
13459
  nameField;
13460
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13461
+ registryOwnsEnabled;
13341
13462
  constructor(records, kind, hooks) {
13342
13463
  this.records = records;
13343
13464
  this.kind = kind;
13344
13465
  this.hooks = hooks;
13345
13466
  this.handle = records.kind(kind);
13346
13467
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13468
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13347
13469
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13470
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13471
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13348
13472
  }
13349
13473
  /**
13350
13474
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13389,15 +13513,23 @@ var RecordIntegrationInstances = class {
13389
13513
  viewOf(record) {
13390
13514
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13391
13515
  const rawName = record.data[this.nameField];
13516
+ const kind = this.hooks.kindOf?.(record.data);
13517
+ const health = this.hooks.health?.(record.data, record.id);
13392
13518
  return {
13393
13519
  id: record.id,
13394
13520
  name: typeof rawName === "string" ? rawName : record.id,
13395
13521
  configured: this.hooks.configured(record.data),
13396
- enabled: record.data.enabled !== false,
13397
- formValues: shown
13522
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13523
+ formValues: shown,
13524
+ ...kind !== void 0 ? { kind } : {},
13525
+ ...health?.status !== void 0 ? { status: health.status } : {},
13526
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13398
13527
  };
13399
13528
  }
13400
- describe = async () => this.hooks.description;
13529
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13530
+ ...this.hooks.description,
13531
+ mintsId: true
13532
+ };
13401
13533
  instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13402
13534
  listInstances = async () => {
13403
13535
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
@@ -13407,37 +13539,46 @@ var RecordIntegrationInstances = class {
13407
13539
  return record === null ? null : this.viewOf(record);
13408
13540
  };
13409
13541
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13410
- createInstance = async ({ id, name }) => {
13542
+ createInstance = async ({ id, name, kind }) => {
13543
+ const minted = id ?? this.hooks.mintId?.({
13544
+ name,
13545
+ ...kind !== void 0 ? { kind } : {}
13546
+ });
13547
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13411
13548
  await this.handle.put({
13412
- id,
13413
- data: this.hooks.initialBody(name)
13549
+ id: minted,
13550
+ data: this.hooks.initialBody(name, kind)
13414
13551
  }, { ifRev: 0 });
13415
- await this.hooks.onChanged?.(id);
13416
- return { id };
13552
+ await this.hooks.onChanged?.(minted, "created");
13553
+ return { id: minted };
13417
13554
  };
13418
13555
  updateInstance = async ({ id, name, values }) => {
13419
13556
  const patch = {};
13420
13557
  for (const [key, value] of Object.entries(values)) {
13421
13558
  if (value === void 0) continue;
13422
- if (!this.secretKeys.has(key)) patch[key] = value;
13559
+ if (this.secretMaps.has(key)) {
13560
+ const entries = secretMapPatch(value);
13561
+ if (entries !== null) patch[key] = entries;
13562
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13423
13563
  else if (isSecretClear(value)) patch[key] = null;
13424
13564
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13425
13565
  }
13426
13566
  if (name !== void 0) patch[this.nameField] = name;
13427
13567
  await this.handle.patch(id, patch);
13428
- await this.hooks.onChanged?.(id);
13568
+ await this.hooks.onChanged?.(id, "updated");
13429
13569
  return { ok: true };
13430
13570
  };
13431
13571
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13432
13572
  setInstanceEnabled = async ({ id, enabled }) => {
13573
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13433
13574
  await this.handle.patch(id, { enabled });
13434
- await this.hooks.onChanged?.(id);
13575
+ await this.hooks.onChanged?.(id, "enabled");
13435
13576
  return { ok: true };
13436
13577
  };
13437
13578
  /** The row and its keychain refs go together (the records door). */
13438
13579
  deleteInstance = async ({ id }) => {
13439
13580
  await this.handle.delete(id);
13440
- await this.hooks.onChanged?.(id);
13581
+ await this.hooks.onChanged?.(id, "deleted");
13441
13582
  return { ok: true };
13442
13583
  };
13443
13584
  testInstance = async (input) => {
@@ -14508,7 +14649,13 @@ var MigrationLedgerEntrySchema = object({
14508
14649
  * joined by the engine from the catalogues it knows this boot. Absent when no
14509
14650
  * catalogue lists the id (a newer database).
14510
14651
  */
14511
- meta: MigrationMetaSchema.optional()
14652
+ meta: MigrationMetaSchema.optional(),
14653
+ /**
14654
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14655
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14656
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14657
+ */
14658
+ ownerNotLoaded: boolean().optional()
14512
14659
  });
14513
14660
  /** A catalogued migration that is not applied, with where it stands. */
14514
14661
  var MigrationPendingEntrySchema = object({
@@ -48887,49 +49034,6 @@ function actuator(input) {
48887
49034
  };
48888
49035
  }
48889
49036
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48890
- var PowerSourceRefusalReasonSchema = _enum([
48891
- "no-power-source",
48892
- "switch-missing",
48893
- "switch-offline",
48894
- "switch-unknown",
48895
- "restart-in-progress",
48896
- "switch-call-failed"
48897
- ]);
48898
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48899
- accepted: literal(false),
48900
- reason: PowerSourceRefusalReasonSchema,
48901
- /** One line for the operator, naming the switch. */
48902
- message: string()
48903
- })]);
48904
- var PowerSourceInfoSchema = object({
48905
- /** False when the device has no power source linked; the rest is then empty. */
48906
- linked: boolean(),
48907
- switchDeviceId: number().nullable(),
48908
- switchName: string().nullable(),
48909
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48910
- switchOn: boolean().nullable(),
48911
- switchOnline: boolean().nullable(),
48912
- restartInProgress: boolean(),
48913
- offSeconds: number().int()
48914
- });
48915
- object({
48916
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48917
- switchOn: boolean().nullable(),
48918
- offSeconds: number().int(),
48919
- lastFetchedAt: number()
48920
- });
48921
- DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
48922
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
48923
- systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48924
- kind: "mutation",
48925
- auth: "admin"
48926
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48927
- kind: "mutation",
48928
- auth: "admin"
48929
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48930
- kind: "mutation",
48931
- auth: "admin"
48932
- });
48933
49037
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48934
49038
  DeviceType.Camera;
48935
49039
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48957,6 +49061,168 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48957
49061
  }).optional()
48958
49062
  })) });
48959
49063
  /**
49064
+ * A saved Customize assignment (D936). The schema is the type: callers use
49065
+ * `z.infer`, they do not restate the shape.
49066
+ */
49067
+ var OverrideSourceSchema = object({
49068
+ addonId: string().min(1),
49069
+ stableId: string().min(1),
49070
+ cap: string().min(1),
49071
+ fieldPath: string().min(1)
49072
+ });
49073
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
49074
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
49075
+ var ThresholdTransformDraftSchema = object({
49076
+ kind: literal("threshold"),
49077
+ comparison: _enum(["gte", "lte"]),
49078
+ threshold: number(),
49079
+ startDwellMs: number().nonnegative(),
49080
+ stopDwellMs: number().nonnegative()
49081
+ });
49082
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
49083
+ IdentityTransformDraftSchema,
49084
+ InvertTransformDraftSchema,
49085
+ ThresholdTransformDraftSchema,
49086
+ object({
49087
+ kind: literal("linear"),
49088
+ scale: number(),
49089
+ offset: number(),
49090
+ clampMin: number().optional(),
49091
+ clampMax: number().optional()
49092
+ }),
49093
+ object({
49094
+ kind: literal("enum-map"),
49095
+ mapping: record(string(), string().nullable())
49096
+ }),
49097
+ object({ kind: literal("number") })
49098
+ ]);
49099
+ var ScalarOverrideDraftSchema = object({
49100
+ kind: literal("scalar"),
49101
+ cap: string().min(1),
49102
+ targetId: string().min(1),
49103
+ source: OverrideSourceSchema,
49104
+ transform: CustomizeTransformDraftSchema
49105
+ });
49106
+ var KeyedArrayOverrideDraftSchema = object({
49107
+ kind: literal("keyedArray"),
49108
+ cap: string().min(1),
49109
+ targetId: string().min(1),
49110
+ key: string().min(1),
49111
+ label: string().min(1),
49112
+ fieldPath: string().min(1),
49113
+ source: OverrideSourceSchema,
49114
+ transform: CustomizeTransformDraftSchema
49115
+ });
49116
+ var PulseOverrideDraftSchema = object({
49117
+ kind: literal("pulse"),
49118
+ cap: string().min(1),
49119
+ targetId: string().min(1),
49120
+ enabled: boolean(),
49121
+ debounceMs: number().nonnegative(),
49122
+ edges: array(_enum([
49123
+ "rising",
49124
+ "button",
49125
+ "native-event"
49126
+ ])).min(1).optional(),
49127
+ sources: array(OverrideSourceSchema).min(1)
49128
+ });
49129
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
49130
+ "in-addition",
49131
+ "confirm",
49132
+ "replace"
49133
+ ]).optional() });
49134
+ discriminatedUnion("kind", [
49135
+ ScalarOverrideDraftSchema,
49136
+ KeyedArrayOverrideDraftSchema,
49137
+ PulseOverrideDraftSchema,
49138
+ object({
49139
+ kind: literal("span"),
49140
+ cap: string().min(1),
49141
+ targetId: string().min(1),
49142
+ timeoutMs: number().nonnegative(),
49143
+ cooldownMs: number().nonnegative().optional(),
49144
+ sources: array(SpanSourceDraftSchema)
49145
+ })
49146
+ ]);
49147
+ object({
49148
+ targetId: string().min(1),
49149
+ configured: boolean(),
49150
+ source: _enum(["native", "assigned"]),
49151
+ state: _enum([
49152
+ "active",
49153
+ "stale",
49154
+ "released",
49155
+ "unavailable"
49156
+ ]),
49157
+ reason: string().nullable()
49158
+ });
49159
+ /**
49160
+ * What a running-threshold proposal says (planning#405). The schema is the
49161
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
49162
+ *
49163
+ * A proposal is read-only advice: it carries the numbers it was derived from
49164
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
49165
+ * WHY with a typed reason and the catalog defaults, never an invented number.
49166
+ */
49167
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
49168
+ var RunningProposalConfidenceSchema = _enum([
49169
+ "high",
49170
+ "medium",
49171
+ "low"
49172
+ ]);
49173
+ var RunningProposalInsufficientReasonSchema = _enum([
49174
+ "no-history",
49175
+ "span-too-short",
49176
+ "truncated",
49177
+ "no-idle-floor",
49178
+ "no-runs",
49179
+ "no-separation"
49180
+ ]);
49181
+ var RunningProposalReplaySchema = object({
49182
+ /** Intervals the real dwell expression held true over the window. */
49183
+ intervals: number().int().nonnegative(),
49184
+ /** Intervals outside every run, or a second interval inside one. */
49185
+ extra: number().int().nonnegative(),
49186
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
49187
+ missed: number().int().nonnegative()
49188
+ });
49189
+ var RunningProposalEvidenceSchema = object({
49190
+ tier: RunningProposalTierSchema,
49191
+ spanMs: number().nonnegative(),
49192
+ from: number(),
49193
+ to: number(),
49194
+ points: number().int().nonnegative(),
49195
+ runsFound: number().int().nonnegative(),
49196
+ pausesFound: number().int().nonnegative(),
49197
+ longestPauseMs: number().nonnegative(),
49198
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49199
+ pulsesFound: number().int().nonnegative(),
49200
+ longestPulseMs: number().nonnegative(),
49201
+ idleFloorW: number(),
49202
+ noiseCeilingW: number(),
49203
+ plateauW: number(),
49204
+ replay: RunningProposalReplaySchema
49205
+ });
49206
+ var RunningProposalInsufficientEvidenceSchema = object({
49207
+ spanMs: number().nonnegative(),
49208
+ points: number().int().nonnegative(),
49209
+ runsFound: number().int().nonnegative()
49210
+ });
49211
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
49212
+ state: literal("proposal"),
49213
+ transform: ThresholdTransformDraftSchema,
49214
+ evidence: RunningProposalEvidenceSchema,
49215
+ confidence: RunningProposalConfidenceSchema,
49216
+ /** Why the confidence is not higher; empty when nothing held it back. */
49217
+ notes: array(string())
49218
+ }), object({
49219
+ state: literal("insufficient"),
49220
+ reason: RunningProposalInsufficientReasonSchema,
49221
+ evidence: RunningProposalInsufficientEvidenceSchema,
49222
+ /** The catalog defaults, offered as what stands when nothing better is known. */
49223
+ fallback: ThresholdTransformDraftSchema
49224
+ })]);
49225
+ /**
48960
49226
  * The report of a one-shot "move this owner's plaintext secrets into the
48961
49227
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48962
49228
  *
@@ -49246,6 +49512,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49246
49512
  kind: "mutation",
49247
49513
  auth: "admin",
49248
49514
  access: "view"
49515
+ }), method(object({
49516
+ deviceId: number().int(),
49517
+ cap: string().min(1),
49518
+ fieldPath: string().min(1),
49519
+ deviceType: _enum(DeviceType).optional()
49520
+ }), RunningThresholdProposalSchema, {
49521
+ auth: "admin",
49522
+ access: "view"
49249
49523
  }), method(object({
49250
49524
  blockId: string().min(1),
49251
49525
  trace: AutomationRunTraceSchema.optional(),
@@ -50054,7 +50328,27 @@ method(object({
50054
50328
  * exposes only the COMMON surface: link state, supported device
50055
50329
  * kinds (so the UI can filter the device picker), and per-device
50056
50330
  * expose/unexpose.
50057
- */
50331
+ *
50332
+ * INSTANCES. One export addon can serve several independent targets (a
50333
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
50334
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
50335
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
50336
+ * default instance's); present = that instance; an id the provider does not
50337
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
50338
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
50339
+ * change always names the target it changes. A single-instance provider
50340
+ * accepts an absent id on a read or its own id, and refuses any other.
50341
+ */
50342
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
50343
+ var InstanceIdSchema$1 = string().min(1);
50344
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
50345
+ /** One independent export target of an addon. */
50346
+ var ExportInstanceSchema = object({
50347
+ instanceId: InstanceIdSchema$1,
50348
+ name: string(),
50349
+ enabled: boolean(),
50350
+ kind: string().optional()
50351
+ });
50058
50352
  var LinkStateSchema = _enum([
50059
50353
  "unlinked",
50060
50354
  "linked",
@@ -50105,10 +50399,14 @@ var ExposedDeviceSchema = object({
50105
50399
  capabilities: array(string()).optional()
50106
50400
  });
50107
50401
  var ExposeInputSchema = object({
50402
+ instanceId: InstanceIdSchema$1,
50108
50403
  deviceId: string(),
50109
50404
  capabilities: array(string()).optional()
50110
50405
  });
50111
- var UnexposeInputSchema = object({ deviceId: string() });
50406
+ var UnexposeInputSchema = object({
50407
+ instanceId: InstanceIdSchema$1,
50408
+ deviceId: string()
50409
+ });
50112
50410
  /**
50113
50411
  * What an exporter may expose besides devices (D968). An action and a
50114
50412
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50137,15 +50435,21 @@ var ExposedEntitySchema = object({
50137
50435
  entity: ExportEntityRefSchema,
50138
50436
  exposedAs: string().optional()
50139
50437
  });
50140
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50141
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50142
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50438
+ var ExposeEntityInputSchema = object({
50439
+ instanceId: InstanceIdSchema$1,
50440
+ entity: ExportEntityRefSchema
50441
+ });
50442
+ var UnexposeEntityInputSchema = object({
50443
+ instanceId: InstanceIdSchema$1,
50444
+ entity: ExportEntityRefSchema
50445
+ });
50446
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50143
50447
  kind: "mutation",
50144
50448
  auth: "admin"
50145
50449
  }), method(UnexposeInputSchema, _void(), {
50146
50450
  kind: "mutation",
50147
50451
  auth: "admin"
50148
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50452
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50149
50453
  kind: "mutation",
50150
50454
  auth: "admin"
50151
50455
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -53346,15 +53650,15 @@ var FanPresetCaptureResultSchema = object({
53346
53650
  items: array(FanCaptureItemSchema),
53347
53651
  omitted: array(FanCaptureOmissionSchema)
53348
53652
  });
53349
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
53653
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53350
53654
  DeviceType.Fan;
53351
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53655
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53352
53656
  kind: "mutation",
53353
53657
  auth: "admin"
53354
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53658
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53355
53659
  kind: "mutation",
53356
53660
  auth: "protected"
53357
- }), method(deviceInput$1.extend({
53661
+ }), method(deviceInput$2.extend({
53358
53662
  fields: array(FanCaptureFieldSchema).optional(),
53359
53663
  steps: array(ActionStepSchema).max(20).default([])
53360
53664
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -53815,11 +54119,7 @@ var CreateIntegrationInputSchema = object({
53815
54119
  info: record(string(), unknown()).optional(),
53816
54120
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
53817
54121
  originStableId: string().optional(),
53818
- settings: record(string(), unknown()).optional(),
53819
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
53820
- * the keychain, not the table (D787). */
53821
- secretKeys: array(string()).optional(),
53822
- skipRestart: boolean().optional()
54122
+ settings: record(string(), unknown()).optional()
53823
54123
  });
53824
54124
  var UpdateIntegrationInputSchema = object({
53825
54125
  id: string(),
@@ -53840,16 +54140,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53840
54140
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
53841
54141
  kind: "mutation",
53842
54142
  auth: "admin"
53843
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
53844
- id: string(),
53845
- settings: record(string(), unknown()),
53846
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
53847
- secretKeys: array(string()).optional(),
53848
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
53849
- skipRestart: boolean().optional()
53850
- }), object({ success: boolean() }), {
53851
- kind: "mutation",
53852
- auth: "admin"
53853
54143
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
53854
54144
  addonId: string(),
53855
54145
  settings: record(string(), unknown())
@@ -53878,13 +54168,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53878
54168
  */
53879
54169
  var ConfigSchemaPassthrough$3 = custom();
53880
54170
  var IntegrationInstanceIdSchema = string().min(1);
54171
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54172
+ /**
54173
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54174
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54175
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
54176
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
54177
+ * instance id.
54178
+ */
54179
+ var IntegrationInstanceKindSchema = object({
54180
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
54181
+ id: string().min(1),
54182
+ label: string().min(1),
54183
+ /** The kind signs in through a server-held interactive session. */
54184
+ signIn: object({
54185
+ basePath: string().min(1),
54186
+ methods: array(_enum(["device-code", "proxy"])).optional()
54187
+ }).optional(),
54188
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
54189
+ connectPath: string().min(1).optional(),
54190
+ /** Relative hub path to disconnect that account. */
54191
+ disconnectPath: string().min(1).optional()
54192
+ });
53881
54193
  var IntegrationInstanceDescriptionSchema = object({
54194
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54195
+ role: _enum([
54196
+ "devices",
54197
+ "services",
54198
+ "both"
54199
+ ]),
54200
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54201
+ discovery: object({
54202
+ mode: _enum([
54203
+ "none",
54204
+ "lan",
54205
+ "account"
54206
+ ]),
54207
+ scopedToInstance: boolean()
54208
+ }).optional(),
53882
54209
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
53883
- instanceMode: _enum(["single", "multiple"]),
54210
+ instanceMode: IntegrationInstanceModeSchema,
53884
54211
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
53885
54212
  kind: string(),
53886
54213
  /** True when `testInstance` really probes the remote. */
53887
- canTest: boolean()
54214
+ canTest: boolean(),
54215
+ /**
54216
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54217
+ * Add flow asks which first and sends it as `createInstance.kind`.
54218
+ */
54219
+ kinds: array(IntegrationInstanceKindSchema).optional(),
54220
+ /**
54221
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54222
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
54223
+ */
54224
+ allowRestoreId: boolean().optional(),
54225
+ /**
54226
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54227
+ * send an id (the UI then mints an opaque one).
54228
+ */
54229
+ mintsId: boolean().optional()
53888
54230
  });
53889
54231
  var IntegrationInstanceStatusSchema = object({
53890
54232
  state: _enum([
@@ -53893,7 +54235,9 @@ var IntegrationInstanceStatusSchema = object({
53893
54235
  "error",
53894
54236
  "unknown"
53895
54237
  ]),
53896
- detail: string().optional()
54238
+ detail: string().optional(),
54239
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
54240
+ label: string().optional()
53897
54241
  });
53898
54242
  var IntegrationInstanceSchema = object({
53899
54243
  id: IntegrationInstanceIdSchema,
@@ -53901,13 +54245,18 @@ var IntegrationInstanceSchema = object({
53901
54245
  /** The addon's own rule (an URL is present, a client id is present...). */
53902
54246
  configured: boolean(),
53903
54247
  /**
53904
- * The instance's own on/off switch (D996): always present, derived from the record body
53905
- * (`enabled !== false`, so a row written before the field existed is on).
54248
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
54249
+ * so a row written before the field existed is on). Absent when the `integrations` registry
54250
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
53906
54251
  */
53907
- enabled: boolean(),
54252
+ enabled: boolean().optional(),
54253
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54254
+ kind: string().optional(),
53908
54255
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53909
54256
  formValues: record(string(), unknown()),
53910
- status: IntegrationInstanceStatusSchema.optional()
54257
+ status: IntegrationInstanceStatusSchema.optional(),
54258
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
54259
+ lastCheckedAt: number().optional()
53911
54260
  });
53912
54261
  var IntegrationInstancePatchSchema = object({
53913
54262
  id: IntegrationInstanceIdSchema,
@@ -53925,10 +54274,14 @@ var integrationInstancesCapability = {
53925
54274
  instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
53926
54275
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
53927
54276
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
53928
- /** Insert the typed record under the given id (a duplicate id is refused). */
54277
+ /**
54278
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
54279
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
54280
+ */
53929
54281
  createInstance: method(object({
53930
- id: IntegrationInstanceIdSchema,
53931
- name: string().min(1)
54282
+ id: IntegrationInstanceIdSchema.optional(),
54283
+ name: string().min(1),
54284
+ kind: string().min(1).optional()
53932
54285
  }), object({ id: string() }), {
53933
54286
  kind: "mutation",
53934
54287
  auth: "admin"
@@ -55760,6 +56113,11 @@ var LogEntrySchema = object({
55760
56113
  * flight, and not asking for the next slice ends the scan.
55761
56114
  */
55762
56115
  var LogReadQuerySchema = object({
56116
+ /**
56117
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
56118
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
56119
+ */
56120
+ instanceId: string().min(1).max(200).optional(),
55763
56121
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
55764
56122
  requestId: string().min(1).max(64).optional(),
55765
56123
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -55822,6 +56180,8 @@ var LogReadResultSchema = object({
55822
56180
  /** True when `cancelRead` ended this slice early. */
55823
56181
  cancelled: boolean().optional()
55824
56182
  });
56183
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
56184
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
55825
56185
  var LogReadCapabilitiesSchema = object({
55826
56186
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
55827
56187
  readable: boolean(),
@@ -55840,7 +56200,7 @@ var LogReadCapabilitiesSchema = object({
55840
56200
  /** How far back the store reaches, when the provider knows (days). */
55841
56201
  retentionDays: number().nullable()
55842
56202
  });
55843
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
56203
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
55844
56204
  kind: "mutation",
55845
56205
  auth: "admin"
55846
56206
  });
@@ -56504,6 +56864,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56504
56864
  }), {
56505
56865
  kind: "mutation",
56506
56866
  auth: "admin"
56867
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
56868
+ rows: number().int().nonnegative(),
56869
+ secrets: number().int().nonnegative()
56870
+ }), {
56871
+ kind: "mutation",
56872
+ auth: "admin"
56873
+ }), method(object({
56874
+ integrationId: string().min(1),
56875
+ patch: record(string(), unknown())
56876
+ }), object({ written: boolean() }), {
56877
+ kind: "mutation",
56878
+ auth: "admin"
56879
+ }), method(object({
56880
+ integrationId: string().min(1),
56881
+ keys: array(string().min(1))
56882
+ }), object({
56883
+ present: boolean(),
56884
+ removed: record(string(), unknown())
56885
+ }), {
56886
+ kind: "mutation",
56887
+ auth: "admin"
56507
56888
  });
56508
56889
  method(object({
56509
56890
  sourceUrl: string(),
@@ -67431,6 +67812,49 @@ object({
67431
67812
  DeviceType.PetFeeder,
67432
67813
  DeviceType.Heater
67433
67814
  ].filter((type) => type !== DeviceType.Sensor);
67815
+ var PowerSourceRefusalReasonSchema = _enum([
67816
+ "no-power-source",
67817
+ "switch-missing",
67818
+ "switch-offline",
67819
+ "switch-unknown",
67820
+ "restart-in-progress",
67821
+ "switch-call-failed"
67822
+ ]);
67823
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67824
+ accepted: literal(false),
67825
+ reason: PowerSourceRefusalReasonSchema,
67826
+ /** One line for the operator, naming the switch. */
67827
+ message: string()
67828
+ })]);
67829
+ var PowerSourceInfoSchema = object({
67830
+ /** False when the device has no power source linked; the rest is then empty. */
67831
+ linked: boolean(),
67832
+ switchDeviceId: number().nullable(),
67833
+ switchName: string().nullable(),
67834
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67835
+ switchOn: boolean().nullable(),
67836
+ switchOnline: boolean().nullable(),
67837
+ restartInProgress: boolean(),
67838
+ offSeconds: number().int()
67839
+ });
67840
+ object({
67841
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67842
+ switchOn: boolean().nullable(),
67843
+ offSeconds: number().int(),
67844
+ lastFetchedAt: number()
67845
+ });
67846
+ DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
67847
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
67848
+ systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67849
+ kind: "mutation",
67850
+ auth: "admin"
67851
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67852
+ kind: "mutation",
67853
+ auth: "admin"
67854
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67855
+ kind: "mutation",
67856
+ auth: "admin"
67857
+ });
67434
67858
  /**
67435
67859
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67436
67860
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -75484,6 +75908,25 @@ function buildDescriptor(capName, kind) {
75484
75908
  * contributes one entry per bound cap present in this map.
75485
75909
  */
75486
75910
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
75911
+ /**
75912
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
75913
+ * shared by the admin UI and the viewer. A role is data on the wire; each
75914
+ * client resolves its name from `label` (English and Italian side by side) and
75915
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
75916
+ * server sends an id, a client maps it to its own icon set.
75917
+ *
75918
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
75919
+ * pictogram): a client that can draw text shows it, one that cannot falls back
75920
+ * to `icon`.
75921
+ */
75922
+ function digit(n) {
75923
+ return {
75924
+ label: t(n, n),
75925
+ icon: "hash",
75926
+ legend: n
75927
+ };
75928
+ }
75929
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
75487
75930
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
75488
75931
  var btn = (idSuffix, row, col, init) => ({
75489
75932
  idSuffix,
@@ -75611,6 +76054,42 @@ discriminatedUnion("kind", [object({
75611
76054
  kind: literal("pronto"),
75612
76055
  code: string()
75613
76056
  })]);
76057
+ /**
76058
+ * Propose a running threshold from a device's own power history (planning#405).
76059
+ *
76060
+ * Pure and deterministic: the same series gives the same answer. The series is
76061
+ * a HELD-VALUE step series — a point's value stands until the next point — which
76062
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
76063
+ * count of samples.
76064
+ *
76065
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
76066
+ * near F. A device that is not near one level most of the time has no floor.
76067
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
76068
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76069
+ * display waking): it is neither a run nor a pause. Among the remaining
76070
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76071
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76072
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76073
+ * is no run.
76074
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
76075
+ * grid a person would type; stop dwell covers the longest pause and the
76076
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
76077
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
76078
+ * run must hold exactly one true interval and nothing may flap outside.
76079
+ */
76080
+ var SECOND_MS = 1e3;
76081
+ var MINUTE_MS$1 = 60 * SECOND_MS;
76082
+ 24 * (60 * MINUTE_MS$1);
76083
+ 10 * MINUTE_MS$1;
76084
+ 30 * SECOND_MS;
76085
+ 60 * SECOND_MS;
76086
+ 20 * MINUTE_MS$1;
76087
+ 90 * SECOND_MS;
76088
+ 30 * MINUTE_MS$1;
76089
+ 10 * MINUTE_MS$1;
76090
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
76091
+ 10 * SECOND_MS;
76092
+ 10 * SECOND_MS;
75614
76093
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75615
76094
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75616
76095
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -77152,6 +77631,13 @@ Object.freeze({
77152
77631
  access: "create",
77153
77632
  role: "admin"
77154
77633
  },
77634
+ "coreBlocks.proposeRunningThreshold": {
77635
+ capName: "core-blocks",
77636
+ capScope: "system",
77637
+ addonId: null,
77638
+ access: "view",
77639
+ role: "admin"
77640
+ },
77155
77641
  "coreBlocks.reportAutomationRun": {
77156
77642
  capName: "core-blocks",
77157
77643
  capScope: "system",
@@ -77663,6 +78149,13 @@ Object.freeze({
77663
78149
  access: "view",
77664
78150
  role: "admin"
77665
78151
  },
78152
+ "deviceExport.listInstances": {
78153
+ capName: "device-export",
78154
+ capScope: "system",
78155
+ addonId: null,
78156
+ access: "view",
78157
+ role: "admin"
78158
+ },
77666
78159
  "deviceExport.listSupportedDeviceKinds": {
77667
78160
  capName: "device-export",
77668
78161
  capScope: "system",
@@ -79350,13 +79843,6 @@ Object.freeze({
79350
79843
  access: "view",
79351
79844
  role: "admin"
79352
79845
  },
79353
- "integrations.getSettings": {
79354
- capName: "integrations",
79355
- capScope: "system",
79356
- addonId: null,
79357
- access: "view",
79358
- role: "admin"
79359
- },
79360
79846
  "integrations.list": {
79361
79847
  capName: "integrations",
79362
79848
  capScope: "system",
@@ -79364,13 +79850,6 @@ Object.freeze({
79364
79850
  access: "view",
79365
79851
  role: "admin"
79366
79852
  },
79367
- "integrations.setSettings": {
79368
- capName: "integrations",
79369
- capScope: "system",
79370
- addonId: null,
79371
- access: "create",
79372
- role: "admin"
79373
- },
79374
79853
  "integrations.testConnection": {
79375
79854
  capName: "integrations",
79376
79855
  capScope: "system",
@@ -80365,6 +80844,13 @@ Object.freeze({
80365
80844
  access: "view",
80366
80845
  role: "admin"
80367
80846
  },
80847
+ "migrations.clearLegacyIntegrationRows": {
80848
+ capName: "migrations",
80849
+ capScope: "system",
80850
+ addonId: null,
80851
+ access: "delete",
80852
+ role: "admin"
80853
+ },
80368
80854
  "migrations.declareOwner": {
80369
80855
  capName: "migrations",
80370
80856
  capScope: "system",
@@ -80372,6 +80858,13 @@ Object.freeze({
80372
80858
  access: "create",
80373
80859
  role: "admin"
80374
80860
  },
80861
+ "migrations.deleteLegacyIntegrationRowKeys": {
80862
+ capName: "migrations",
80863
+ capScope: "system",
80864
+ addonId: null,
80865
+ access: "delete",
80866
+ role: "admin"
80867
+ },
80375
80868
  "migrations.dryRun": {
80376
80869
  capName: "migrations",
80377
80870
  capScope: "system",
@@ -80407,6 +80900,20 @@ Object.freeze({
80407
80900
  access: "create",
80408
80901
  role: "admin"
80409
80902
  },
80903
+ "migrations.patchLegacyIntegrationRows": {
80904
+ capName: "migrations",
80905
+ capScope: "system",
80906
+ addonId: null,
80907
+ access: "create",
80908
+ role: "admin"
80909
+ },
80910
+ "migrations.readLegacyIntegrationRows": {
80911
+ capName: "migrations",
80912
+ capScope: "system",
80913
+ addonId: null,
80914
+ access: "view",
80915
+ role: "admin"
80916
+ },
80410
80917
  "migrations.restorePreMigration": {
80411
80918
  capName: "migrations",
80412
80919
  capScope: "system",
@@ -86610,6 +87117,11 @@ Object.freeze({
86610
87117
  form: "single",
86611
87118
  optional: false
86612
87119
  }],
87120
+ "coreBlocks.proposeRunningThreshold": [{
87121
+ name: "deviceId",
87122
+ form: "single",
87123
+ optional: false
87124
+ }],
86613
87125
  "cover.close": [{
86614
87126
  name: "deviceId",
86615
87127
  form: "single",
@@ -89273,99 +89785,6 @@ Object.freeze({
89273
89785
  "network-access": "ingress",
89274
89786
  "smtp-provider": "email"
89275
89787
  });
89276
- /**
89277
- * A saved Customize assignment (D936). The schema is the type: callers use
89278
- * `z.infer`, they do not restate the shape.
89279
- */
89280
- var OverrideSourceSchema = object({
89281
- addonId: string().min(1),
89282
- stableId: string().min(1),
89283
- cap: string().min(1),
89284
- fieldPath: string().min(1)
89285
- });
89286
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
89287
- object({ kind: literal("identity") }),
89288
- object({ kind: literal("invert") }),
89289
- object({
89290
- kind: literal("threshold"),
89291
- comparison: _enum(["gte", "lte"]),
89292
- threshold: number(),
89293
- startDwellMs: number().nonnegative(),
89294
- stopDwellMs: number().nonnegative()
89295
- }),
89296
- object({
89297
- kind: literal("linear"),
89298
- scale: number(),
89299
- offset: number(),
89300
- clampMin: number().optional(),
89301
- clampMax: number().optional()
89302
- }),
89303
- object({
89304
- kind: literal("enum-map"),
89305
- mapping: record(string(), string().nullable())
89306
- }),
89307
- object({ kind: literal("number") })
89308
- ]);
89309
- var ScalarOverrideDraftSchema = object({
89310
- kind: literal("scalar"),
89311
- cap: string().min(1),
89312
- targetId: string().min(1),
89313
- source: OverrideSourceSchema,
89314
- transform: CustomizeTransformDraftSchema
89315
- });
89316
- var KeyedArrayOverrideDraftSchema = object({
89317
- kind: literal("keyedArray"),
89318
- cap: string().min(1),
89319
- targetId: string().min(1),
89320
- key: string().min(1),
89321
- label: string().min(1),
89322
- fieldPath: string().min(1),
89323
- source: OverrideSourceSchema,
89324
- transform: CustomizeTransformDraftSchema
89325
- });
89326
- var PulseOverrideDraftSchema = object({
89327
- kind: literal("pulse"),
89328
- cap: string().min(1),
89329
- targetId: string().min(1),
89330
- enabled: boolean(),
89331
- debounceMs: number().nonnegative(),
89332
- edges: array(_enum([
89333
- "rising",
89334
- "button",
89335
- "native-event"
89336
- ])).min(1).optional(),
89337
- sources: array(OverrideSourceSchema).min(1)
89338
- });
89339
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
89340
- "in-addition",
89341
- "confirm",
89342
- "replace"
89343
- ]).optional() });
89344
- discriminatedUnion("kind", [
89345
- ScalarOverrideDraftSchema,
89346
- KeyedArrayOverrideDraftSchema,
89347
- PulseOverrideDraftSchema,
89348
- object({
89349
- kind: literal("span"),
89350
- cap: string().min(1),
89351
- targetId: string().min(1),
89352
- timeoutMs: number().nonnegative(),
89353
- cooldownMs: number().nonnegative().optional(),
89354
- sources: array(SpanSourceDraftSchema)
89355
- })
89356
- ]);
89357
- object({
89358
- targetId: string().min(1),
89359
- configured: boolean(),
89360
- source: _enum(["native", "assigned"]),
89361
- state: _enum([
89362
- "active",
89363
- "stale",
89364
- "released",
89365
- "unavailable"
89366
- ]),
89367
- reason: string().nullable()
89368
- });
89369
89788
  var G711_SCALE_CORRECTION_DB = {
89370
89789
  PCMU: 20 * Math.log10(4),
89371
89790
  PCMA: 20 * Math.log10(8)
@@ -123190,7 +123609,7 @@ var MqttAclStore = class MqttAclStore {
123190
123609
  * The addon has NO standalone admin page. All configuration — external
123191
123610
  * broker entries, the embedded `aedes` broker, embedded credentials,
123192
123611
  * and the topic ACL — is exposed through the standard
123193
- * `globalSettingsSchema()` and rendered automatically inside the
123612
+ * `clusterSettingsSchema()` and rendered automatically inside the
123194
123613
  * Integrations → Brokers tab's docked Settings panel, like every other
123195
123614
  * integration-type addon.
123196
123615
  *
@@ -123377,6 +123796,7 @@ var MqttBrokerAddon = class extends BaseAddon {
123377
123796
  this.aclStore = MqttAclStore.of(records);
123378
123797
  this.instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
123379
123798
  description: {
123799
+ role: "services",
123380
123800
  instanceMode: "multiple",
123381
123801
  kind: "brokers",
123382
123802
  canTest: false
@@ -124260,11 +124680,15 @@ var MqttBrokerAddon = class extends BaseAddon {
124260
124680
  }
124261
124681
  };
124262
124682
  }
124683
+ scopedSettingsCapabilities() {
124684
+ return ["mqtt-broker", "broker"];
124685
+ }
124263
124686
  /**
124264
124687
  * The embedded broker, its credentials and the ACL. The external brokers are records, listed
124265
124688
  * on the Brokers page and edited as integration instances (`brokerForm`).
124266
124689
  */
124267
- globalSettingsSchema() {
124690
+ async clusterSettingsSchema(cap) {
124691
+ if (cap !== "mqtt-broker" && cap !== "broker") return null;
124268
124692
  return this.schema({ sections: [
124269
124693
  {
124270
124694
  id: "mqtt-broker-embedded",