@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.
@@ -6028,7 +6028,7 @@ var ZodIssueCode = {
6028
6028
  var ZodFirstPartyTypeKind;
6029
6029
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6030
6030
  //#endregion
6031
- //#region ../types/dist/sleep-BKClVk0K.mjs
6031
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
6032
6032
  /**
6033
6033
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6034
6034
  * window to float samples (D455).
@@ -6628,7 +6628,7 @@ var NOT_RUN = {
6628
6628
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6629
6629
  * because the hub (and this addon's boot) must not hang on a migration.
6630
6630
  */
6631
- async function runAddonMigrations(host, migrations, logger) {
6631
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6632
6632
  const first = migrations[0];
6633
6633
  if (first === void 0) return NOT_RUN;
6634
6634
  const owner = first.meta.owner;
@@ -6657,7 +6657,9 @@ async function runAddonMigrations(host, migrations, logger) {
6657
6657
  else if (status === "blocked") blocked.push(id);
6658
6658
  else skipped.push(id);
6659
6659
  };
6660
+ const selected = options.only === void 0 ? null : new Set(options.only);
6660
6661
  for (const step of declaration.steps) {
6662
+ if (selected !== null && !selected.has(step.id)) continue;
6661
6663
  const bound = byId.get(step.id);
6662
6664
  if (bound === void 0 || step.action === "skip") {
6663
6665
  skipped.push(step.id);
@@ -6835,6 +6837,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6835
6837
  }
6836
6838
  return out;
6837
6839
  }
6840
+ /** Every scope, in the order the guard and the docs list them. */
6841
+ var SCOPED_SETTINGS_SCOPES = [
6842
+ "cluster",
6843
+ "device-defaults",
6844
+ "node"
6845
+ ];
6838
6846
  /**
6839
6847
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6840
6848
  *
@@ -7726,6 +7734,13 @@ var BaseAddon = class {
7726
7734
  */
7727
7735
  secretStatePaths = [];
7728
7736
  _addonSecretTree = null;
7737
+ /**
7738
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7739
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7740
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7741
+ */
7742
+ _scopedByCap = /* @__PURE__ */ new Map();
7743
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7729
7744
  _deviceSecretTree = null;
7730
7745
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7731
7746
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7795,6 +7810,7 @@ var BaseAddon = class {
7795
7810
  this._ctx = context;
7796
7811
  this._secretSettings = null;
7797
7812
  }
7813
+ await this.loadScopedSchemas();
7798
7814
  await this.resolveConfig();
7799
7815
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7800
7816
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7804,6 +7820,7 @@ var BaseAddon = class {
7804
7820
  const normalized = normalizeAddonInitResult(result);
7805
7821
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7806
7822
  this._registeredCapNames = providers.map((p) => p.capability.name);
7823
+ await this.loadScopedSchemas();
7807
7824
  return normalized;
7808
7825
  }
7809
7826
  /**
@@ -7817,14 +7834,18 @@ var BaseAddon = class {
7817
7834
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7818
7835
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7819
7836
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7837
+ *
7838
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7839
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7840
+ * omitted applied ids "newer").
7820
7841
  */
7821
- async runMigrations(migrations) {
7842
+ async runMigrations(migrations, options) {
7822
7843
  const api = this.ctx.api.migrations;
7823
7844
  const report = await runAddonMigrations({
7824
7845
  declareOwner: (input) => api.declareOwner.mutate(input),
7825
7846
  startMigration: (input) => api.startMigration.mutate(input),
7826
7847
  finishMigration: (input) => api.finishMigration.mutate(input)
7827
- }, migrations, this.ctx.logger);
7848
+ }, migrations, this.ctx.logger, options);
7828
7849
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7829
7850
  return report;
7830
7851
  }
@@ -7888,6 +7909,16 @@ var BaseAddon = class {
7888
7909
  schema(schema) {
7889
7910
  return schema;
7890
7911
  }
7912
+ /**
7913
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7914
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7915
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7916
+ * must already know which keys are per node. Names the providers register at init are added
7917
+ * once known.
7918
+ */
7919
+ scopedSettingsCapabilities() {
7920
+ return [];
7921
+ }
7891
7922
  /** Override to provide global-level settings UI schema. */
7892
7923
  globalSettingsSchema(_cap) {
7893
7924
  return null;
@@ -7931,7 +7962,8 @@ var BaseAddon = class {
7931
7962
  return [];
7932
7963
  }
7933
7964
  async getGlobalSettings(overlay, cap, nodeId) {
7934
- const schema = this.globalSettingsSchema(cap);
7965
+ await this.loadScopedSchemas(cap);
7966
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7935
7967
  if (!schema) return { sections: [] };
7936
7968
  const projected = await this.readRawProjectedStore(nodeId, cap);
7937
7969
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -7966,10 +7998,9 @@ var BaseAddon = class {
7966
7998
  *
7967
7999
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7968
8000
  * needs its connection fields, the default integration the addon's own sections. It is the
7969
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7970
- * unchanged (`integrations.setSettings` for an account integration).
8001
+ * SHAPE of the form only — the values live on the instance record.
7971
8002
  */
7972
- async integrationSettingsSchema(_instance) {
8003
+ async integrationFormSchema(_instance) {
7973
8004
  return null;
7974
8005
  }
7975
8006
  /**
@@ -8015,7 +8046,7 @@ var BaseAddon = class {
8015
8046
  * per-instance schema when it declares one, else its global schema (what they always showed).
8016
8047
  */
8017
8048
  async getIntegrationFormSchema(instance, nodeId) {
8018
- const scoped = await this.integrationSettingsSchema(instance);
8049
+ const scoped = await this.integrationFormSchema(instance);
8019
8050
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8020
8051
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8021
8052
  }
@@ -8063,6 +8094,7 @@ var BaseAddon = class {
8063
8094
  * resolved blob back is a no-op for every unchanged secret.
8064
8095
  */
8065
8096
  async resolveGlobalStore(nodeId, cap) {
8097
+ await this.loadScopedSchemas(cap);
8066
8098
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8067
8099
  return this.projectForNode(raw, nodeId, cap);
8068
8100
  }
@@ -8073,6 +8105,7 @@ var BaseAddon = class {
8073
8105
  }
8074
8106
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8075
8107
  async readRawProjectedStore(nodeId, cap) {
8108
+ await this.loadScopedSchemas(cap);
8076
8109
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8077
8110
  return this.projectForNode(raw, nodeId, cap);
8078
8111
  }
@@ -8165,12 +8198,17 @@ var BaseAddon = class {
8165
8198
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8166
8199
  const patch = {};
8167
8200
  for (const key of keys) {
8201
+ if (tree.get(key)?.kind === "array") {
8202
+ patch[key] = [];
8203
+ continue;
8204
+ }
8168
8205
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8169
8206
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8170
8207
  }
8171
8208
  await this.applyGlobalPatch(patch, nodeId);
8172
8209
  }
8173
8210
  async applyGlobalPatch(barePatch, nodeId) {
8211
+ await this.loadScopedSchemas();
8174
8212
  const keys = this.perNodeKeys();
8175
8213
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8176
8214
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8184,7 +8222,8 @@ var BaseAddon = class {
8184
8222
  await this.resolveConfig();
8185
8223
  await this.onConfigChanged();
8186
8224
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8187
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8225
+ const legacy = this.globalSettingsSchema();
8226
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8188
8227
  }
8189
8228
  /**
8190
8229
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8192,14 +8231,67 @@ var BaseAddon = class {
8192
8231
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8193
8232
  * settings API behaves exactly like the legacy node-agnostic one.
8194
8233
  */
8195
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8234
+ /**
8235
+ * Load the scoped schemas of every capability this addon declares (the override and the
8236
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8237
+ * derived from the previous set. Idempotent.
8238
+ */
8239
+ async loadScopedSchemas(extraCap) {
8240
+ const caps = new Set([
8241
+ ...this.scopedSettingsCapabilities(),
8242
+ ...this._registeredCapNames,
8243
+ ...extraCap === void 0 ? [] : [extraCap]
8244
+ ]);
8245
+ let added = false;
8246
+ for (const cap of caps) {
8247
+ if (this._scopedByCap.has(cap)) continue;
8248
+ const loaded = [];
8249
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8250
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8251
+ if (schema !== null) loaded.push({
8252
+ scope,
8253
+ schema
8254
+ });
8255
+ }
8256
+ this._scopedByCap.set(cap, loaded);
8257
+ added = true;
8258
+ }
8259
+ if (!added) return;
8260
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8261
+ this._addonSecretTree = null;
8262
+ }
8263
+ /**
8264
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8265
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8266
+ */
8267
+ scopedFormSchema(cap) {
8268
+ const byId = /* @__PURE__ */ new Map();
8269
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8270
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8271
+ }
8272
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8273
+ loadedScopedSchemas(cap) {
8274
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8275
+ return [...this._scopedByCap.values()].flat();
8276
+ }
8277
+ /**
8278
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8279
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8280
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8281
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8282
+ * Call {@link loadScopedSchemas} first (the async seams do).
8283
+ */
8196
8284
  perNodeKeys(cap) {
8197
8285
  const cacheKey = cap ?? "";
8198
- const cached = this._perNodeKeysCache.get(cacheKey);
8286
+ const cached = this._nodeKeysByCap.get(cacheKey);
8199
8287
  if (cached) return cached;
8200
- const schema = this.globalSettingsSchema(cap);
8201
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8202
- this._perNodeKeysCache.set(cacheKey, keys);
8288
+ const legacy = this.globalSettingsSchema(cap);
8289
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8290
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8291
+ if (scope !== "node") continue;
8292
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8293
+ }
8294
+ this._nodeKeysByCap.set(cacheKey, keys);
8203
8295
  return keys;
8204
8296
  }
8205
8297
  /**
@@ -8209,10 +8301,9 @@ var BaseAddon = class {
8209
8301
  * time to return its response before the addon is torn down and
8210
8302
  * re-initialised by `AddonRegistryService.restartAddon`.
8211
8303
  */
8212
- maybeAutoRestart(patchKeys, schema) {
8213
- if (!schema) return;
8304
+ maybeAutoRestart(patchKeys, schemas) {
8214
8305
  const restartKeys = /* @__PURE__ */ new Set();
8215
- for (const section of schema.sections) for (const field of section.fields) {
8306
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8216
8307
  if (field.type === "separator" || field.type === "info") continue;
8217
8308
  if (field.requiresRestart) restartKeys.add(field.key);
8218
8309
  }
@@ -8408,12 +8499,14 @@ var BaseAddon = class {
8408
8499
  * here, in the addon's own process, so the hub keeps no copy of it.
8409
8500
  */
8410
8501
  async getDeclaredSettingsKeys() {
8502
+ await this.loadScopedSchemas();
8411
8503
  return { keys: [...this.declaredKeySet()].toSorted() };
8412
8504
  }
8413
8505
  declaredKeySet() {
8414
8506
  const keys = new Set(Object.keys(this.defaults));
8415
8507
  const schema = this.globalSettingsSchema();
8416
8508
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8509
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8417
8510
  for (const key of this._declaredStateKeys) keys.add(key);
8418
8511
  for (const path of this.secretStatePaths) {
8419
8512
  const root = path.split(/[.[]/, 1)[0];
@@ -8509,7 +8602,11 @@ var BaseAddon = class {
8509
8602
  }
8510
8603
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8511
8604
  addonSecretTree() {
8512
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8605
+ if (this._addonSecretTree === null) {
8606
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8607
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8608
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8609
+ }
8513
8610
  return this._addonSecretTree;
8514
8611
  }
8515
8612
  deviceSecretTree() {
@@ -8759,6 +8856,23 @@ function dropPerNodeFields(fields) {
8759
8856
  }
8760
8857
  return kept;
8761
8858
  }
8859
+ /** The keys of the value-holding fields, recursing into layout containers. */
8860
+ function collectValueFieldKeys(fields) {
8861
+ const collected = [];
8862
+ for (const field of fields) {
8863
+ if (field.type === "group") {
8864
+ collected.push(...collectValueFieldKeys(field.fields));
8865
+ continue;
8866
+ }
8867
+ if (field.type === "sub-tabs") {
8868
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8869
+ continue;
8870
+ }
8871
+ if (field.type === "separator" || field.type === "info") continue;
8872
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8873
+ }
8874
+ return collected;
8875
+ }
8762
8876
  function collectFieldKeys(fields) {
8763
8877
  const collected = [];
8764
8878
  for (const field of fields) {
@@ -8860,6 +8974,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8860
8974
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8861
8975
  * (`auth.accessToken`, `headers[].value`).
8862
8976
  *
8977
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8978
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8979
+ * key releases exactly that key's secret.
8980
+ *
8863
8981
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8864
8982
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8865
8983
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8872,11 +8990,13 @@ function parseSecretPath(path) {
8872
8990
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8873
8991
  return path.split(".").map((raw) => {
8874
8992
  const each = raw.endsWith("[]");
8875
- const key = each ? raw.slice(0, -2) : raw;
8993
+ const map = raw.endsWith("{}");
8994
+ const key = each || map ? raw.slice(0, -2) : raw;
8876
8995
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8877
8996
  return {
8878
8997
  key,
8879
- each
8998
+ each,
8999
+ map
8880
9000
  };
8881
9001
  });
8882
9002
  }
@@ -8928,6 +9048,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8928
9048
  [segment.key]: mapped
8929
9049
  };
8930
9050
  }
9051
+ if (segment.map) {
9052
+ if (!isRecord$4(child)) return node;
9053
+ const kept = Object.entries(child).map(([mapKey, value]) => {
9054
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
9055
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
9056
+ return [mapKey, visit({
9057
+ declared,
9058
+ concrete,
9059
+ value
9060
+ })];
9061
+ }).filter(([, value]) => value !== REMOVE_LEAF);
9062
+ return {
9063
+ ...node,
9064
+ [segment.key]: Object.fromEntries(kept)
9065
+ };
9066
+ }
8931
9067
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8932
9068
  if (last) {
8933
9069
  const replacement = visit({
@@ -9069,7 +9205,8 @@ function isZodSecret(schema) {
9069
9205
  }
9070
9206
  /**
9071
9207
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9072
- * element of this array" (`connection.password`, `rows[].token`). A declared
9208
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9209
+ * map" (`applianceSecrets{}`). A declared
9073
9210
  * object or array is reported as one path and not descended into — everything
9074
9211
  * under it is secret.
9075
9212
  */
@@ -9080,6 +9217,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9080
9217
  const def = defOf(layer);
9081
9218
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9082
9219
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9220
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9083
9221
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9084
9222
  else if (def.type === "intersection") {
9085
9223
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9219,7 +9357,7 @@ function defineRecordKinds(specs) {
9219
9357
  retention: spec.retention,
9220
9358
  deviceRemoval: spec.deviceRemoval,
9221
9359
  maxDataBytes: spec.maxDataBytes,
9222
- secretPaths: [...secretPaths],
9360
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9223
9361
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9224
9362
  transactFamily: spec.transactFamily,
9225
9363
  entry: spec.entry === void 0 ? void 0 : {
@@ -9261,9 +9399,11 @@ function defineEntryKind(name, spec) {
9261
9399
  class: "config",
9262
9400
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9263
9401
  retention: { none: "an entry goes only when the operator deletes it" },
9402
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9264
9403
  ...spec.database === void 0 ? {} : { database: spec.database },
9265
9404
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9266
9405
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9406
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9267
9407
  entry: {
9268
9408
  title: spec.title,
9269
9409
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11925,7 +12065,7 @@ object({
11925
12065
  params: record(string(), union([string(), number()])).optional()
11926
12066
  });
11927
12067
  //#endregion
11928
- //#region ../types/dist/composition-BodkvOV0.mjs
12068
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11929
12069
  /**
11930
12070
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11931
12071
  *
@@ -12688,38 +12828,6 @@ function validateExpressionSource(src) {
12688
12828
  return null;
12689
12829
  }
12690
12830
  /**
12691
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12692
- *
12693
- * Standby of the control board is under 5 W; a motor or a heater is far
12694
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12695
- * a cycle, so running starts only after 30 s above the line.
12696
- *
12697
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12698
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12699
- * report "finished" in the middle of the program, so the stop dwell is
12700
- * 10 minutes on every type. The number is stored per type so one machine
12701
- * can move later without moving the others.
12702
- */
12703
- var APPLIANCE_TEMPLATE_ID = "appliance";
12704
- var APPLIANCE_KINDS = [
12705
- "washer",
12706
- "dryer",
12707
- "dishwasher"
12708
- ];
12709
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12710
- /**
12711
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12712
- *
12713
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12714
- * (D967), so the heater switches exactly as the plug does. Running is the
12715
- * plug being on; when a power reading is also named, running is the draw
12716
- * staying above the line instead (a plug left on with the stove's own
12717
- * thermostat open is not heating). 50 W is far above any standby and below
12718
- * the smallest radiator.
12719
- */
12720
- var HEATER_TEMPLATE_ID = "heater";
12721
- DeviceType.Heater;
12722
- /**
12723
12831
  * Statistics and integrals over a history read (D944).
12724
12832
  *
12725
12833
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12863,34 +12971,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12863
12971
  /** Actuator groups only. Readings leave this unset. */
12864
12972
  active: ActiveCriterionSchema.optional()
12865
12973
  }),
12866
- object({
12867
- kind: literal("appliance"),
12868
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12869
- appliance: _enum(APPLIANCE_KINDS),
12870
- thresholdWatts: number().positive(),
12871
- startDwellMs: number().int().nonnegative(),
12872
- stopDwellMs: number().int().nonnegative(),
12873
- member: HelperMemberSchema,
12874
- /**
12875
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12876
- * composition carries a `power-source` feature, so the hub can cut and restore
12877
- * that plug (power-source link of a NEW-target composition).
12878
- */
12879
- powerSwitch: HelperMemberSchema.optional(),
12880
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12881
- offSeconds: number().optional()
12882
- }),
12883
- object({
12884
- kind: literal("heater"),
12885
- templateId: literal(HEATER_TEMPLATE_ID),
12886
- /** The plug, read and commanded at `switch.on`. */
12887
- plug: HelperMemberSchema,
12888
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12889
- meter: HelperMemberSchema.optional(),
12890
- thresholdWatts: number().positive(),
12891
- startDwellMs: number().int().nonnegative(),
12892
- stopDwellMs: number().int().nonnegative()
12893
- }),
12894
12974
  object({
12895
12975
  kind: literal("statistic"),
12896
12976
  stat: _enum(STATISTIC_STATS),
@@ -13114,7 +13194,7 @@ var CompositionSchema = object({
13114
13194
  kind: literal("existing"),
13115
13195
  device: CompositionSourceRefSchema
13116
13196
  })]),
13117
- features: array(CompositionFeatureSchema).min(1).max(16),
13197
+ features: array(CompositionFeatureSchema).min(0).max(16),
13118
13198
  /**
13119
13199
  * Present when the block is a helper template. Absent on a free
13120
13200
  * composition (the room example, device 4603). The editor reads this,
@@ -13332,11 +13412,49 @@ var IntegrationInstanceRefSchema = object({
13332
13412
  ]),
13333
13413
  candidateStableId: string().optional()
13334
13414
  });
13415
+ /**
13416
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13417
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13418
+ *
13419
+ * Every instance is one record of the addon's own kind, keyed by the
13420
+ * `integrations` registry id. The records door does the secret work: a field the
13421
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13422
+ * deleting the row deletes the secret, and one row's value is never another's.
13423
+ *
13424
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13425
+ * token or the view the form was given KEEP the stored secret;
13426
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13427
+ */
13335
13428
  /** The most instances one addon lists (an integrations page, not a table). */
13336
13429
  var INSTANCES_LIST_MAX = 1e3;
13337
13430
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13338
13431
  function topLevelSecretKeys(paths) {
13339
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13432
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13433
+ }
13434
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13435
+ function secretMapKeys(paths) {
13436
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13437
+ }
13438
+ /**
13439
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13440
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13441
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13442
+ */
13443
+ function secretMapPatch(value) {
13444
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13445
+ const out = {};
13446
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13447
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13448
+ return Object.keys(out).length > 0 ? out : null;
13449
+ }
13450
+ /** Does the kind's body schema carry an `enabled` field? */
13451
+ function declaresEnabled(kind) {
13452
+ const json = toJSONSchema(kind.schema, {
13453
+ unrepresentable: "any",
13454
+ io: "input"
13455
+ });
13456
+ const properties = Reflect.get(json, "properties");
13457
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13340
13458
  }
13341
13459
  var RecordIntegrationInstances = class {
13342
13460
  records;
@@ -13344,14 +13462,20 @@ var RecordIntegrationInstances = class {
13344
13462
  hooks;
13345
13463
  handle;
13346
13464
  secretKeys;
13465
+ secretMaps;
13347
13466
  nameField;
13467
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13468
+ registryOwnsEnabled;
13348
13469
  constructor(records, kind, hooks) {
13349
13470
  this.records = records;
13350
13471
  this.kind = kind;
13351
13472
  this.hooks = hooks;
13352
13473
  this.handle = records.kind(kind);
13353
13474
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13475
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13354
13476
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13477
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13478
+ 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"`);
13355
13479
  }
13356
13480
  /**
13357
13481
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13396,15 +13520,23 @@ var RecordIntegrationInstances = class {
13396
13520
  viewOf(record) {
13397
13521
  const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13398
13522
  const rawName = record.data[this.nameField];
13523
+ const kind = this.hooks.kindOf?.(record.data);
13524
+ const health = this.hooks.health?.(record.data, record.id);
13399
13525
  return {
13400
13526
  id: record.id,
13401
13527
  name: typeof rawName === "string" ? rawName : record.id,
13402
13528
  configured: this.hooks.configured(record.data),
13403
- enabled: record.data.enabled !== false,
13404
- formValues: shown
13529
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13530
+ formValues: shown,
13531
+ ...kind !== void 0 ? { kind } : {},
13532
+ ...health?.status !== void 0 ? { status: health.status } : {},
13533
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13405
13534
  };
13406
13535
  }
13407
- describe = async () => this.hooks.description;
13536
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13537
+ ...this.hooks.description,
13538
+ mintsId: true
13539
+ };
13408
13540
  instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13409
13541
  listInstances = async () => {
13410
13542
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
@@ -13414,37 +13546,46 @@ var RecordIntegrationInstances = class {
13414
13546
  return record === null ? null : this.viewOf(record);
13415
13547
  };
13416
13548
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13417
- createInstance = async ({ id, name }) => {
13549
+ createInstance = async ({ id, name, kind }) => {
13550
+ const minted = id ?? this.hooks.mintId?.({
13551
+ name,
13552
+ ...kind !== void 0 ? { kind } : {}
13553
+ });
13554
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13418
13555
  await this.handle.put({
13419
- id,
13420
- data: this.hooks.initialBody(name)
13556
+ id: minted,
13557
+ data: this.hooks.initialBody(name, kind)
13421
13558
  }, { ifRev: 0 });
13422
- await this.hooks.onChanged?.(id);
13423
- return { id };
13559
+ await this.hooks.onChanged?.(minted, "created");
13560
+ return { id: minted };
13424
13561
  };
13425
13562
  updateInstance = async ({ id, name, values }) => {
13426
13563
  const patch = {};
13427
13564
  for (const [key, value] of Object.entries(values)) {
13428
13565
  if (value === void 0) continue;
13429
- if (!this.secretKeys.has(key)) patch[key] = value;
13566
+ if (this.secretMaps.has(key)) {
13567
+ const entries = secretMapPatch(value);
13568
+ if (entries !== null) patch[key] = entries;
13569
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13430
13570
  else if (isSecretClear(value)) patch[key] = null;
13431
13571
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13432
13572
  }
13433
13573
  if (name !== void 0) patch[this.nameField] = name;
13434
13574
  await this.handle.patch(id, patch);
13435
- await this.hooks.onChanged?.(id);
13575
+ await this.hooks.onChanged?.(id, "updated");
13436
13576
  return { ok: true };
13437
13577
  };
13438
13578
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13439
13579
  setInstanceEnabled = async ({ id, enabled }) => {
13580
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13440
13581
  await this.handle.patch(id, { enabled });
13441
- await this.hooks.onChanged?.(id);
13582
+ await this.hooks.onChanged?.(id, "enabled");
13442
13583
  return { ok: true };
13443
13584
  };
13444
13585
  /** The row and its keychain refs go together (the records door). */
13445
13586
  deleteInstance = async ({ id }) => {
13446
13587
  await this.handle.delete(id);
13447
- await this.hooks.onChanged?.(id);
13588
+ await this.hooks.onChanged?.(id, "deleted");
13448
13589
  return { ok: true };
13449
13590
  };
13450
13591
  testInstance = async (input) => {
@@ -14515,7 +14656,13 @@ var MigrationLedgerEntrySchema = object({
14515
14656
  * joined by the engine from the catalogues it knows this boot. Absent when no
14516
14657
  * catalogue lists the id (a newer database).
14517
14658
  */
14518
- meta: MigrationMetaSchema.optional()
14659
+ meta: MigrationMetaSchema.optional(),
14660
+ /**
14661
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14662
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14663
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14664
+ */
14665
+ ownerNotLoaded: boolean().optional()
14519
14666
  });
14520
14667
  /** A catalogued migration that is not applied, with where it stands. */
14521
14668
  var MigrationPendingEntrySchema = object({
@@ -48894,49 +49041,6 @@ function actuator(input) {
48894
49041
  };
48895
49042
  }
48896
49043
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48897
- var PowerSourceRefusalReasonSchema = _enum([
48898
- "no-power-source",
48899
- "switch-missing",
48900
- "switch-offline",
48901
- "switch-unknown",
48902
- "restart-in-progress",
48903
- "switch-call-failed"
48904
- ]);
48905
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48906
- accepted: literal(false),
48907
- reason: PowerSourceRefusalReasonSchema,
48908
- /** One line for the operator, naming the switch. */
48909
- message: string()
48910
- })]);
48911
- var PowerSourceInfoSchema = object({
48912
- /** False when the device has no power source linked; the rest is then empty. */
48913
- linked: boolean(),
48914
- switchDeviceId: number().nullable(),
48915
- switchName: string().nullable(),
48916
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48917
- switchOn: boolean().nullable(),
48918
- switchOnline: boolean().nullable(),
48919
- restartInProgress: boolean(),
48920
- offSeconds: number().int()
48921
- });
48922
- object({
48923
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48924
- switchOn: boolean().nullable(),
48925
- offSeconds: number().int(),
48926
- lastFetchedAt: number()
48927
- });
48928
- 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;
48929
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
48930
- systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48931
- kind: "mutation",
48932
- auth: "admin"
48933
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48934
- kind: "mutation",
48935
- auth: "admin"
48936
- }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48937
- kind: "mutation",
48938
- auth: "admin"
48939
- });
48940
49044
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48941
49045
  DeviceType.Camera;
48942
49046
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48964,6 +49068,168 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48964
49068
  }).optional()
48965
49069
  })) });
48966
49070
  /**
49071
+ * A saved Customize assignment (D936). The schema is the type: callers use
49072
+ * `z.infer`, they do not restate the shape.
49073
+ */
49074
+ var OverrideSourceSchema = object({
49075
+ addonId: string().min(1),
49076
+ stableId: string().min(1),
49077
+ cap: string().min(1),
49078
+ fieldPath: string().min(1)
49079
+ });
49080
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
49081
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
49082
+ var ThresholdTransformDraftSchema = object({
49083
+ kind: literal("threshold"),
49084
+ comparison: _enum(["gte", "lte"]),
49085
+ threshold: number(),
49086
+ startDwellMs: number().nonnegative(),
49087
+ stopDwellMs: number().nonnegative()
49088
+ });
49089
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
49090
+ IdentityTransformDraftSchema,
49091
+ InvertTransformDraftSchema,
49092
+ ThresholdTransformDraftSchema,
49093
+ object({
49094
+ kind: literal("linear"),
49095
+ scale: number(),
49096
+ offset: number(),
49097
+ clampMin: number().optional(),
49098
+ clampMax: number().optional()
49099
+ }),
49100
+ object({
49101
+ kind: literal("enum-map"),
49102
+ mapping: record(string(), string().nullable())
49103
+ }),
49104
+ object({ kind: literal("number") })
49105
+ ]);
49106
+ var ScalarOverrideDraftSchema = object({
49107
+ kind: literal("scalar"),
49108
+ cap: string().min(1),
49109
+ targetId: string().min(1),
49110
+ source: OverrideSourceSchema,
49111
+ transform: CustomizeTransformDraftSchema
49112
+ });
49113
+ var KeyedArrayOverrideDraftSchema = object({
49114
+ kind: literal("keyedArray"),
49115
+ cap: string().min(1),
49116
+ targetId: string().min(1),
49117
+ key: string().min(1),
49118
+ label: string().min(1),
49119
+ fieldPath: string().min(1),
49120
+ source: OverrideSourceSchema,
49121
+ transform: CustomizeTransformDraftSchema
49122
+ });
49123
+ var PulseOverrideDraftSchema = object({
49124
+ kind: literal("pulse"),
49125
+ cap: string().min(1),
49126
+ targetId: string().min(1),
49127
+ enabled: boolean(),
49128
+ debounceMs: number().nonnegative(),
49129
+ edges: array(_enum([
49130
+ "rising",
49131
+ "button",
49132
+ "native-event"
49133
+ ])).min(1).optional(),
49134
+ sources: array(OverrideSourceSchema).min(1)
49135
+ });
49136
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
49137
+ "in-addition",
49138
+ "confirm",
49139
+ "replace"
49140
+ ]).optional() });
49141
+ discriminatedUnion("kind", [
49142
+ ScalarOverrideDraftSchema,
49143
+ KeyedArrayOverrideDraftSchema,
49144
+ PulseOverrideDraftSchema,
49145
+ object({
49146
+ kind: literal("span"),
49147
+ cap: string().min(1),
49148
+ targetId: string().min(1),
49149
+ timeoutMs: number().nonnegative(),
49150
+ cooldownMs: number().nonnegative().optional(),
49151
+ sources: array(SpanSourceDraftSchema)
49152
+ })
49153
+ ]);
49154
+ object({
49155
+ targetId: string().min(1),
49156
+ configured: boolean(),
49157
+ source: _enum(["native", "assigned"]),
49158
+ state: _enum([
49159
+ "active",
49160
+ "stale",
49161
+ "released",
49162
+ "unavailable"
49163
+ ]),
49164
+ reason: string().nullable()
49165
+ });
49166
+ /**
49167
+ * What a running-threshold proposal says (planning#405). The schema is the
49168
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
49169
+ *
49170
+ * A proposal is read-only advice: it carries the numbers it was derived from
49171
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
49172
+ * WHY with a typed reason and the catalog defaults, never an invented number.
49173
+ */
49174
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
49175
+ var RunningProposalConfidenceSchema = _enum([
49176
+ "high",
49177
+ "medium",
49178
+ "low"
49179
+ ]);
49180
+ var RunningProposalInsufficientReasonSchema = _enum([
49181
+ "no-history",
49182
+ "span-too-short",
49183
+ "truncated",
49184
+ "no-idle-floor",
49185
+ "no-runs",
49186
+ "no-separation"
49187
+ ]);
49188
+ var RunningProposalReplaySchema = object({
49189
+ /** Intervals the real dwell expression held true over the window. */
49190
+ intervals: number().int().nonnegative(),
49191
+ /** Intervals outside every run, or a second interval inside one. */
49192
+ extra: number().int().nonnegative(),
49193
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
49194
+ missed: number().int().nonnegative()
49195
+ });
49196
+ var RunningProposalEvidenceSchema = object({
49197
+ tier: RunningProposalTierSchema,
49198
+ spanMs: number().nonnegative(),
49199
+ from: number(),
49200
+ to: number(),
49201
+ points: number().int().nonnegative(),
49202
+ runsFound: number().int().nonnegative(),
49203
+ pausesFound: number().int().nonnegative(),
49204
+ longestPauseMs: number().nonnegative(),
49205
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
49206
+ pulsesFound: number().int().nonnegative(),
49207
+ longestPulseMs: number().nonnegative(),
49208
+ idleFloorW: number(),
49209
+ noiseCeilingW: number(),
49210
+ plateauW: number(),
49211
+ replay: RunningProposalReplaySchema
49212
+ });
49213
+ var RunningProposalInsufficientEvidenceSchema = object({
49214
+ spanMs: number().nonnegative(),
49215
+ points: number().int().nonnegative(),
49216
+ runsFound: number().int().nonnegative()
49217
+ });
49218
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
49219
+ state: literal("proposal"),
49220
+ transform: ThresholdTransformDraftSchema,
49221
+ evidence: RunningProposalEvidenceSchema,
49222
+ confidence: RunningProposalConfidenceSchema,
49223
+ /** Why the confidence is not higher; empty when nothing held it back. */
49224
+ notes: array(string())
49225
+ }), object({
49226
+ state: literal("insufficient"),
49227
+ reason: RunningProposalInsufficientReasonSchema,
49228
+ evidence: RunningProposalInsufficientEvidenceSchema,
49229
+ /** The catalog defaults, offered as what stands when nothing better is known. */
49230
+ fallback: ThresholdTransformDraftSchema
49231
+ })]);
49232
+ /**
48967
49233
  * The report of a one-shot "move this owner's plaintext secrets into the
48968
49234
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48969
49235
  *
@@ -49253,6 +49519,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49253
49519
  kind: "mutation",
49254
49520
  auth: "admin",
49255
49521
  access: "view"
49522
+ }), method(object({
49523
+ deviceId: number().int(),
49524
+ cap: string().min(1),
49525
+ fieldPath: string().min(1),
49526
+ deviceType: _enum(DeviceType).optional()
49527
+ }), RunningThresholdProposalSchema, {
49528
+ auth: "admin",
49529
+ access: "view"
49256
49530
  }), method(object({
49257
49531
  blockId: string().min(1),
49258
49532
  trace: AutomationRunTraceSchema.optional(),
@@ -50061,7 +50335,27 @@ method(object({
50061
50335
  * exposes only the COMMON surface: link state, supported device
50062
50336
  * kinds (so the UI can filter the device picker), and per-device
50063
50337
  * expose/unexpose.
50064
- */
50338
+ *
50339
+ * INSTANCES. One export addon can serve several independent targets (a
50340
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
50341
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
50342
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
50343
+ * default instance's); present = that instance; an id the provider does not
50344
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
50345
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
50346
+ * change always names the target it changes. A single-instance provider
50347
+ * accepts an absent id on a read or its own id, and refuses any other.
50348
+ */
50349
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
50350
+ var InstanceIdSchema$1 = string().min(1);
50351
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
50352
+ /** One independent export target of an addon. */
50353
+ var ExportInstanceSchema = object({
50354
+ instanceId: InstanceIdSchema$1,
50355
+ name: string(),
50356
+ enabled: boolean(),
50357
+ kind: string().optional()
50358
+ });
50065
50359
  var LinkStateSchema = _enum([
50066
50360
  "unlinked",
50067
50361
  "linked",
@@ -50112,10 +50406,14 @@ var ExposedDeviceSchema = object({
50112
50406
  capabilities: array(string()).optional()
50113
50407
  });
50114
50408
  var ExposeInputSchema = object({
50409
+ instanceId: InstanceIdSchema$1,
50115
50410
  deviceId: string(),
50116
50411
  capabilities: array(string()).optional()
50117
50412
  });
50118
- var UnexposeInputSchema = object({ deviceId: string() });
50413
+ var UnexposeInputSchema = object({
50414
+ instanceId: InstanceIdSchema$1,
50415
+ deviceId: string()
50416
+ });
50119
50417
  /**
50120
50418
  * What an exporter may expose besides devices (D968). An action and a
50121
50419
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50144,15 +50442,21 @@ var ExposedEntitySchema = object({
50144
50442
  entity: ExportEntityRefSchema,
50145
50443
  exposedAs: string().optional()
50146
50444
  });
50147
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50148
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50149
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50445
+ var ExposeEntityInputSchema = object({
50446
+ instanceId: InstanceIdSchema$1,
50447
+ entity: ExportEntityRefSchema
50448
+ });
50449
+ var UnexposeEntityInputSchema = object({
50450
+ instanceId: InstanceIdSchema$1,
50451
+ entity: ExportEntityRefSchema
50452
+ });
50453
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50150
50454
  kind: "mutation",
50151
50455
  auth: "admin"
50152
50456
  }), method(UnexposeInputSchema, _void(), {
50153
50457
  kind: "mutation",
50154
50458
  auth: "admin"
50155
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50459
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50156
50460
  kind: "mutation",
50157
50461
  auth: "admin"
50158
50462
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -53353,15 +53657,15 @@ var FanPresetCaptureResultSchema = object({
53353
53657
  items: array(FanCaptureItemSchema),
53354
53658
  omitted: array(FanCaptureOmissionSchema)
53355
53659
  });
53356
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
53660
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53357
53661
  DeviceType.Fan;
53358
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53662
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53359
53663
  kind: "mutation",
53360
53664
  auth: "admin"
53361
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53665
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53362
53666
  kind: "mutation",
53363
53667
  auth: "protected"
53364
- }), method(deviceInput$1.extend({
53668
+ }), method(deviceInput$2.extend({
53365
53669
  fields: array(FanCaptureFieldSchema).optional(),
53366
53670
  steps: array(ActionStepSchema).max(20).default([])
53367
53671
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -53822,11 +54126,7 @@ var CreateIntegrationInputSchema = object({
53822
54126
  info: record(string(), unknown()).optional(),
53823
54127
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
53824
54128
  originStableId: string().optional(),
53825
- settings: record(string(), unknown()).optional(),
53826
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
53827
- * the keychain, not the table (D787). */
53828
- secretKeys: array(string()).optional(),
53829
- skipRestart: boolean().optional()
54129
+ settings: record(string(), unknown()).optional()
53830
54130
  });
53831
54131
  var UpdateIntegrationInputSchema = object({
53832
54132
  id: string(),
@@ -53847,16 +54147,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53847
54147
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
53848
54148
  kind: "mutation",
53849
54149
  auth: "admin"
53850
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
53851
- id: string(),
53852
- settings: record(string(), unknown()),
53853
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
53854
- secretKeys: array(string()).optional(),
53855
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
53856
- skipRestart: boolean().optional()
53857
- }), object({ success: boolean() }), {
53858
- kind: "mutation",
53859
- auth: "admin"
53860
54150
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
53861
54151
  addonId: string(),
53862
54152
  settings: record(string(), unknown())
@@ -53885,13 +54175,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53885
54175
  */
53886
54176
  var ConfigSchemaPassthrough$3 = custom();
53887
54177
  var IntegrationInstanceIdSchema = string().min(1);
54178
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54179
+ /**
54180
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
54181
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
54182
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
54183
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
54184
+ * instance id.
54185
+ */
54186
+ var IntegrationInstanceKindSchema = object({
54187
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
54188
+ id: string().min(1),
54189
+ label: string().min(1),
54190
+ /** The kind signs in through a server-held interactive session. */
54191
+ signIn: object({
54192
+ basePath: string().min(1),
54193
+ methods: array(_enum(["device-code", "proxy"])).optional()
54194
+ }).optional(),
54195
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
54196
+ connectPath: string().min(1).optional(),
54197
+ /** Relative hub path to disconnect that account. */
54198
+ disconnectPath: string().min(1).optional()
54199
+ });
53888
54200
  var IntegrationInstanceDescriptionSchema = object({
54201
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54202
+ role: _enum([
54203
+ "devices",
54204
+ "services",
54205
+ "both"
54206
+ ]),
54207
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
54208
+ discovery: object({
54209
+ mode: _enum([
54210
+ "none",
54211
+ "lan",
54212
+ "account"
54213
+ ]),
54214
+ scopedToInstance: boolean()
54215
+ }).optional(),
53889
54216
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
53890
- instanceMode: _enum(["single", "multiple"]),
54217
+ instanceMode: IntegrationInstanceModeSchema,
53891
54218
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
53892
54219
  kind: string(),
53893
54220
  /** True when `testInstance` really probes the remote. */
53894
- canTest: boolean()
54221
+ canTest: boolean(),
54222
+ /**
54223
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
54224
+ * Add flow asks which first and sends it as `createInstance.kind`.
54225
+ */
54226
+ kinds: array(IntegrationInstanceKindSchema).optional(),
54227
+ /**
54228
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
54229
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
54230
+ */
54231
+ allowRestoreId: boolean().optional(),
54232
+ /**
54233
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54234
+ * send an id (the UI then mints an opaque one).
54235
+ */
54236
+ mintsId: boolean().optional()
53895
54237
  });
53896
54238
  var IntegrationInstanceStatusSchema = object({
53897
54239
  state: _enum([
@@ -53900,7 +54242,9 @@ var IntegrationInstanceStatusSchema = object({
53900
54242
  "error",
53901
54243
  "unknown"
53902
54244
  ]),
53903
- detail: string().optional()
54245
+ detail: string().optional(),
54246
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
54247
+ label: string().optional()
53904
54248
  });
53905
54249
  var IntegrationInstanceSchema = object({
53906
54250
  id: IntegrationInstanceIdSchema,
@@ -53908,13 +54252,18 @@ var IntegrationInstanceSchema = object({
53908
54252
  /** The addon's own rule (an URL is present, a client id is present...). */
53909
54253
  configured: boolean(),
53910
54254
  /**
53911
- * The instance's own on/off switch (D996): always present, derived from the record body
53912
- * (`enabled !== false`, so a row written before the field existed is on).
54255
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
54256
+ * so a row written before the field existed is on). Absent when the `integrations` registry
54257
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
53913
54258
  */
53914
- enabled: boolean(),
54259
+ enabled: boolean().optional(),
54260
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
54261
+ kind: string().optional(),
53915
54262
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53916
54263
  formValues: record(string(), unknown()),
53917
- status: IntegrationInstanceStatusSchema.optional()
54264
+ status: IntegrationInstanceStatusSchema.optional(),
54265
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
54266
+ lastCheckedAt: number().optional()
53918
54267
  });
53919
54268
  var IntegrationInstancePatchSchema = object({
53920
54269
  id: IntegrationInstanceIdSchema,
@@ -53932,10 +54281,14 @@ var integrationInstancesCapability = {
53932
54281
  instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
53933
54282
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
53934
54283
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
53935
- /** Insert the typed record under the given id (a duplicate id is refused). */
54284
+ /**
54285
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
54286
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
54287
+ */
53936
54288
  createInstance: method(object({
53937
- id: IntegrationInstanceIdSchema,
53938
- name: string().min(1)
54289
+ id: IntegrationInstanceIdSchema.optional(),
54290
+ name: string().min(1),
54291
+ kind: string().min(1).optional()
53939
54292
  }), object({ id: string() }), {
53940
54293
  kind: "mutation",
53941
54294
  auth: "admin"
@@ -55767,6 +56120,11 @@ var LogEntrySchema = object({
55767
56120
  * flight, and not asking for the next slice ends the scan.
55768
56121
  */
55769
56122
  var LogReadQuerySchema = object({
56123
+ /**
56124
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
56125
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
56126
+ */
56127
+ instanceId: string().min(1).max(200).optional(),
55770
56128
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
55771
56129
  requestId: string().min(1).max(64).optional(),
55772
56130
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -55829,6 +56187,8 @@ var LogReadResultSchema = object({
55829
56187
  /** True when `cancelRead` ended this slice early. */
55830
56188
  cancelled: boolean().optional()
55831
56189
  });
56190
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
56191
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
55832
56192
  var LogReadCapabilitiesSchema = object({
55833
56193
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
55834
56194
  readable: boolean(),
@@ -55847,7 +56207,7 @@ var LogReadCapabilitiesSchema = object({
55847
56207
  /** How far back the store reaches, when the provider knows (days). */
55848
56208
  retentionDays: number().nullable()
55849
56209
  });
55850
- 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() }), {
56210
+ 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() }), {
55851
56211
  kind: "mutation",
55852
56212
  auth: "admin"
55853
56213
  });
@@ -56511,6 +56871,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
56511
56871
  }), {
56512
56872
  kind: "mutation",
56513
56873
  auth: "admin"
56874
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
56875
+ rows: number().int().nonnegative(),
56876
+ secrets: number().int().nonnegative()
56877
+ }), {
56878
+ kind: "mutation",
56879
+ auth: "admin"
56880
+ }), method(object({
56881
+ integrationId: string().min(1),
56882
+ patch: record(string(), unknown())
56883
+ }), object({ written: boolean() }), {
56884
+ kind: "mutation",
56885
+ auth: "admin"
56886
+ }), method(object({
56887
+ integrationId: string().min(1),
56888
+ keys: array(string().min(1))
56889
+ }), object({
56890
+ present: boolean(),
56891
+ removed: record(string(), unknown())
56892
+ }), {
56893
+ kind: "mutation",
56894
+ auth: "admin"
56514
56895
  });
56515
56896
  method(object({
56516
56897
  sourceUrl: string(),
@@ -67438,6 +67819,49 @@ object({
67438
67819
  DeviceType.PetFeeder,
67439
67820
  DeviceType.Heater
67440
67821
  ].filter((type) => type !== DeviceType.Sensor);
67822
+ var PowerSourceRefusalReasonSchema = _enum([
67823
+ "no-power-source",
67824
+ "switch-missing",
67825
+ "switch-offline",
67826
+ "switch-unknown",
67827
+ "restart-in-progress",
67828
+ "switch-call-failed"
67829
+ ]);
67830
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
67831
+ accepted: literal(false),
67832
+ reason: PowerSourceRefusalReasonSchema,
67833
+ /** One line for the operator, naming the switch. */
67834
+ message: string()
67835
+ })]);
67836
+ var PowerSourceInfoSchema = object({
67837
+ /** False when the device has no power source linked; the rest is then empty. */
67838
+ linked: boolean(),
67839
+ switchDeviceId: number().nullable(),
67840
+ switchName: string().nullable(),
67841
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
67842
+ switchOn: boolean().nullable(),
67843
+ switchOnline: boolean().nullable(),
67844
+ restartInProgress: boolean(),
67845
+ offSeconds: number().int()
67846
+ });
67847
+ object({
67848
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
67849
+ switchOn: boolean().nullable(),
67850
+ offSeconds: number().int(),
67851
+ lastFetchedAt: number()
67852
+ });
67853
+ 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;
67854
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
67855
+ systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67856
+ kind: "mutation",
67857
+ auth: "admin"
67858
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67859
+ kind: "mutation",
67860
+ auth: "admin"
67861
+ }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
67862
+ kind: "mutation",
67863
+ auth: "admin"
67864
+ });
67441
67865
  /**
67442
67866
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
67443
67867
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -75491,6 +75915,25 @@ function buildDescriptor(capName, kind) {
75491
75915
  * contributes one entry per bound cap present in this map.
75492
75916
  */
75493
75917
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
75918
+ /**
75919
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
75920
+ * shared by the admin UI and the viewer. A role is data on the wire; each
75921
+ * client resolves its name from `label` (English and Italian side by side) and
75922
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
75923
+ * server sends an id, a client maps it to its own icon set.
75924
+ *
75925
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
75926
+ * pictogram): a client that can draw text shows it, one that cannot falls back
75927
+ * to `icon`.
75928
+ */
75929
+ function digit(n) {
75930
+ return {
75931
+ label: t(n, n),
75932
+ icon: "hash",
75933
+ legend: n
75934
+ };
75935
+ }
75936
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
75494
75937
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
75495
75938
  var btn = (idSuffix, row, col, init) => ({
75496
75939
  idSuffix,
@@ -75618,6 +76061,42 @@ discriminatedUnion("kind", [object({
75618
76061
  kind: literal("pronto"),
75619
76062
  code: string()
75620
76063
  })]);
76064
+ /**
76065
+ * Propose a running threshold from a device's own power history (planning#405).
76066
+ *
76067
+ * Pure and deterministic: the same series gives the same answer. The series is
76068
+ * a HELD-VALUE step series — a point's value stands until the next point — which
76069
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
76070
+ * count of samples.
76071
+ *
76072
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
76073
+ * near F. A device that is not near one level most of the time has no floor.
76074
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
76075
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
76076
+ * display waking): it is neither a run nor a pause. Among the remaining
76077
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
76078
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
76079
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
76080
+ * is no run.
76081
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
76082
+ * grid a person would type; stop dwell covers the longest pause and the
76083
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
76084
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
76085
+ * run must hold exactly one true interval and nothing may flap outside.
76086
+ */
76087
+ var SECOND_MS = 1e3;
76088
+ var MINUTE_MS$1 = 60 * SECOND_MS;
76089
+ 24 * (60 * MINUTE_MS$1);
76090
+ 10 * MINUTE_MS$1;
76091
+ 30 * SECOND_MS;
76092
+ 60 * SECOND_MS;
76093
+ 20 * MINUTE_MS$1;
76094
+ 90 * SECOND_MS;
76095
+ 30 * MINUTE_MS$1;
76096
+ 10 * MINUTE_MS$1;
76097
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
76098
+ 10 * SECOND_MS;
76099
+ 10 * SECOND_MS;
75621
76100
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75622
76101
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75623
76102
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -77159,6 +77638,13 @@ Object.freeze({
77159
77638
  access: "create",
77160
77639
  role: "admin"
77161
77640
  },
77641
+ "coreBlocks.proposeRunningThreshold": {
77642
+ capName: "core-blocks",
77643
+ capScope: "system",
77644
+ addonId: null,
77645
+ access: "view",
77646
+ role: "admin"
77647
+ },
77162
77648
  "coreBlocks.reportAutomationRun": {
77163
77649
  capName: "core-blocks",
77164
77650
  capScope: "system",
@@ -77670,6 +78156,13 @@ Object.freeze({
77670
78156
  access: "view",
77671
78157
  role: "admin"
77672
78158
  },
78159
+ "deviceExport.listInstances": {
78160
+ capName: "device-export",
78161
+ capScope: "system",
78162
+ addonId: null,
78163
+ access: "view",
78164
+ role: "admin"
78165
+ },
77673
78166
  "deviceExport.listSupportedDeviceKinds": {
77674
78167
  capName: "device-export",
77675
78168
  capScope: "system",
@@ -79357,13 +79850,6 @@ Object.freeze({
79357
79850
  access: "view",
79358
79851
  role: "admin"
79359
79852
  },
79360
- "integrations.getSettings": {
79361
- capName: "integrations",
79362
- capScope: "system",
79363
- addonId: null,
79364
- access: "view",
79365
- role: "admin"
79366
- },
79367
79853
  "integrations.list": {
79368
79854
  capName: "integrations",
79369
79855
  capScope: "system",
@@ -79371,13 +79857,6 @@ Object.freeze({
79371
79857
  access: "view",
79372
79858
  role: "admin"
79373
79859
  },
79374
- "integrations.setSettings": {
79375
- capName: "integrations",
79376
- capScope: "system",
79377
- addonId: null,
79378
- access: "create",
79379
- role: "admin"
79380
- },
79381
79860
  "integrations.testConnection": {
79382
79861
  capName: "integrations",
79383
79862
  capScope: "system",
@@ -80372,6 +80851,13 @@ Object.freeze({
80372
80851
  access: "view",
80373
80852
  role: "admin"
80374
80853
  },
80854
+ "migrations.clearLegacyIntegrationRows": {
80855
+ capName: "migrations",
80856
+ capScope: "system",
80857
+ addonId: null,
80858
+ access: "delete",
80859
+ role: "admin"
80860
+ },
80375
80861
  "migrations.declareOwner": {
80376
80862
  capName: "migrations",
80377
80863
  capScope: "system",
@@ -80379,6 +80865,13 @@ Object.freeze({
80379
80865
  access: "create",
80380
80866
  role: "admin"
80381
80867
  },
80868
+ "migrations.deleteLegacyIntegrationRowKeys": {
80869
+ capName: "migrations",
80870
+ capScope: "system",
80871
+ addonId: null,
80872
+ access: "delete",
80873
+ role: "admin"
80874
+ },
80382
80875
  "migrations.dryRun": {
80383
80876
  capName: "migrations",
80384
80877
  capScope: "system",
@@ -80414,6 +80907,20 @@ Object.freeze({
80414
80907
  access: "create",
80415
80908
  role: "admin"
80416
80909
  },
80910
+ "migrations.patchLegacyIntegrationRows": {
80911
+ capName: "migrations",
80912
+ capScope: "system",
80913
+ addonId: null,
80914
+ access: "create",
80915
+ role: "admin"
80916
+ },
80917
+ "migrations.readLegacyIntegrationRows": {
80918
+ capName: "migrations",
80919
+ capScope: "system",
80920
+ addonId: null,
80921
+ access: "view",
80922
+ role: "admin"
80923
+ },
80417
80924
  "migrations.restorePreMigration": {
80418
80925
  capName: "migrations",
80419
80926
  capScope: "system",
@@ -86617,6 +87124,11 @@ Object.freeze({
86617
87124
  form: "single",
86618
87125
  optional: false
86619
87126
  }],
87127
+ "coreBlocks.proposeRunningThreshold": [{
87128
+ name: "deviceId",
87129
+ form: "single",
87130
+ optional: false
87131
+ }],
86620
87132
  "cover.close": [{
86621
87133
  name: "deviceId",
86622
87134
  form: "single",
@@ -89280,99 +89792,6 @@ Object.freeze({
89280
89792
  "network-access": "ingress",
89281
89793
  "smtp-provider": "email"
89282
89794
  });
89283
- /**
89284
- * A saved Customize assignment (D936). The schema is the type: callers use
89285
- * `z.infer`, they do not restate the shape.
89286
- */
89287
- var OverrideSourceSchema = object({
89288
- addonId: string().min(1),
89289
- stableId: string().min(1),
89290
- cap: string().min(1),
89291
- fieldPath: string().min(1)
89292
- });
89293
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
89294
- object({ kind: literal("identity") }),
89295
- object({ kind: literal("invert") }),
89296
- object({
89297
- kind: literal("threshold"),
89298
- comparison: _enum(["gte", "lte"]),
89299
- threshold: number(),
89300
- startDwellMs: number().nonnegative(),
89301
- stopDwellMs: number().nonnegative()
89302
- }),
89303
- object({
89304
- kind: literal("linear"),
89305
- scale: number(),
89306
- offset: number(),
89307
- clampMin: number().optional(),
89308
- clampMax: number().optional()
89309
- }),
89310
- object({
89311
- kind: literal("enum-map"),
89312
- mapping: record(string(), string().nullable())
89313
- }),
89314
- object({ kind: literal("number") })
89315
- ]);
89316
- var ScalarOverrideDraftSchema = object({
89317
- kind: literal("scalar"),
89318
- cap: string().min(1),
89319
- targetId: string().min(1),
89320
- source: OverrideSourceSchema,
89321
- transform: CustomizeTransformDraftSchema
89322
- });
89323
- var KeyedArrayOverrideDraftSchema = object({
89324
- kind: literal("keyedArray"),
89325
- cap: string().min(1),
89326
- targetId: string().min(1),
89327
- key: string().min(1),
89328
- label: string().min(1),
89329
- fieldPath: string().min(1),
89330
- source: OverrideSourceSchema,
89331
- transform: CustomizeTransformDraftSchema
89332
- });
89333
- var PulseOverrideDraftSchema = object({
89334
- kind: literal("pulse"),
89335
- cap: string().min(1),
89336
- targetId: string().min(1),
89337
- enabled: boolean(),
89338
- debounceMs: number().nonnegative(),
89339
- edges: array(_enum([
89340
- "rising",
89341
- "button",
89342
- "native-event"
89343
- ])).min(1).optional(),
89344
- sources: array(OverrideSourceSchema).min(1)
89345
- });
89346
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
89347
- "in-addition",
89348
- "confirm",
89349
- "replace"
89350
- ]).optional() });
89351
- discriminatedUnion("kind", [
89352
- ScalarOverrideDraftSchema,
89353
- KeyedArrayOverrideDraftSchema,
89354
- PulseOverrideDraftSchema,
89355
- object({
89356
- kind: literal("span"),
89357
- cap: string().min(1),
89358
- targetId: string().min(1),
89359
- timeoutMs: number().nonnegative(),
89360
- cooldownMs: number().nonnegative().optional(),
89361
- sources: array(SpanSourceDraftSchema)
89362
- })
89363
- ]);
89364
- object({
89365
- targetId: string().min(1),
89366
- configured: boolean(),
89367
- source: _enum(["native", "assigned"]),
89368
- state: _enum([
89369
- "active",
89370
- "stale",
89371
- "released",
89372
- "unavailable"
89373
- ]),
89374
- reason: string().nullable()
89375
- });
89376
89795
  var G711_SCALE_CORRECTION_DB = {
89377
89796
  PCMU: 20 * Math.log10(4),
89378
89797
  PCMA: 20 * Math.log10(8)
@@ -123197,7 +123616,7 @@ var MqttAclStore = class MqttAclStore {
123197
123616
  * The addon has NO standalone admin page. All configuration — external
123198
123617
  * broker entries, the embedded `aedes` broker, embedded credentials,
123199
123618
  * and the topic ACL — is exposed through the standard
123200
- * `globalSettingsSchema()` and rendered automatically inside the
123619
+ * `clusterSettingsSchema()` and rendered automatically inside the
123201
123620
  * Integrations → Brokers tab's docked Settings panel, like every other
123202
123621
  * integration-type addon.
123203
123622
  *
@@ -123384,6 +123803,7 @@ var MqttBrokerAddon = class extends BaseAddon {
123384
123803
  this.aclStore = MqttAclStore.of(records);
123385
123804
  this.instances = new RecordIntegrationInstances(records, mqttBrokersEntryKind, {
123386
123805
  description: {
123806
+ role: "services",
123387
123807
  instanceMode: "multiple",
123388
123808
  kind: "brokers",
123389
123809
  canTest: false
@@ -124267,11 +124687,15 @@ var MqttBrokerAddon = class extends BaseAddon {
124267
124687
  }
124268
124688
  };
124269
124689
  }
124690
+ scopedSettingsCapabilities() {
124691
+ return ["mqtt-broker", "broker"];
124692
+ }
124270
124693
  /**
124271
124694
  * The embedded broker, its credentials and the ACL. The external brokers are records, listed
124272
124695
  * on the Brokers page and edited as integration instances (`brokerForm`).
124273
124696
  */
124274
- globalSettingsSchema() {
124697
+ async clusterSettingsSchema(cap) {
124698
+ if (cap !== "mqtt-broker" && cap !== "broker") return null;
124275
124699
  return this.schema({ sections: [
124276
124700
  {
124277
124701
  id: "mqtt-broker-embedded",