@camstack/addon-matter-broker 0.2.154 → 0.2.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -799,7 +799,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
799
799
  return EventCategory;
800
800
  }({});
801
801
  //#endregion
802
- //#region ../types/dist/device-type-CfHw_quR.mjs
802
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
803
803
  /**
804
804
  import { errMsg } from '@camstack/types'
805
805
  * Extract a human-readable message from an unknown error value.
@@ -1030,6 +1030,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1030
1030
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1031
1031
  /** Media player exposes stop distinct from pause. */
1032
1032
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1033
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1034
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1035
+ /** Media player exposes channel up / down in steps. */
1036
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1037
+ /** Media player exposes power on / off. */
1038
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1033
1039
  /** Alarm panel requires a PIN code on arm/disarm. */
1034
1040
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1035
1041
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -5989,7 +5995,7 @@ var ZodIssueCode = {
5989
5995
  var ZodFirstPartyTypeKind;
5990
5996
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5991
5997
  //#endregion
5992
- //#region ../types/dist/sleep-QsoHs66J.mjs
5998
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
5993
5999
  /**
5994
6000
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5995
6001
  * window to float samples (D455).
@@ -6068,6 +6074,22 @@ function buildAlawTable() {
6068
6074
  }
6069
6075
  buildUlawTable();
6070
6076
  buildAlawTable();
6077
+ /**
6078
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
6079
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
6080
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
6081
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
6082
+ */
6083
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
6084
+ var AddonMigrationsBlockedError = class extends Error {
6085
+ /** The ledger ids that did not apply and block the owner. */
6086
+ migrationIds;
6087
+ constructor(migrationIds) {
6088
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
6089
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
6090
+ this.migrationIds = migrationIds;
6091
+ }
6092
+ };
6071
6093
  /** Is this field's stored value a credential? */
6072
6094
  function isSecretConfigField(field) {
6073
6095
  if (field.type === "password") return true;
@@ -6121,9 +6143,16 @@ function isSecretMarker(value) {
6121
6143
  const ref = value["$secret"];
6122
6144
  return typeof ref === "string" && ref.length > 0;
6123
6145
  }
6146
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6147
+ function isSecretMarkerLike(value) {
6148
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6149
+ }
6124
6150
  function isSecretClear(value) {
6125
6151
  return isRecord$7(value) && value["$secretClear"] === true;
6126
6152
  }
6153
+ function isSecretFieldView(value) {
6154
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6155
+ }
6127
6156
  /**
6128
6157
  * The UI view of a stored secret field value: a marker becomes "set", inline
6129
6158
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6505,6 +6534,53 @@ function emitReadiness(bus, params) {
6505
6534
  ts
6506
6535
  }));
6507
6536
  }
6537
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6538
+ var AddonMigrationDefinitionError = class extends Error {
6539
+ id;
6540
+ constructor(id, message) {
6541
+ super(`${id}: ${message}`);
6542
+ this.id = id;
6543
+ this.name = "AddonMigrationDefinitionError";
6544
+ }
6545
+ };
6546
+ /**
6547
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6548
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6549
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6550
+ */
6551
+ function defineAddonMigrations(catalogue, ports) {
6552
+ return catalogue.map((entry) => {
6553
+ const { meta } = entry;
6554
+ if ("plan" in entry) {
6555
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6556
+ return {
6557
+ meta,
6558
+ execute: () => guarded(async () => ({
6559
+ kind: "plan",
6560
+ plan: await entry.plan(ports)
6561
+ }))
6562
+ };
6563
+ }
6564
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6565
+ return {
6566
+ meta,
6567
+ execute: () => guarded(async () => ({
6568
+ kind: "procedure",
6569
+ result: await entry.procedure(ports)
6570
+ }))
6571
+ };
6572
+ });
6573
+ }
6574
+ async function guarded(work) {
6575
+ try {
6576
+ return await work();
6577
+ } catch (err) {
6578
+ return {
6579
+ kind: "failed",
6580
+ error: errMsg(err)
6581
+ };
6582
+ }
6583
+ }
6508
6584
  var NOT_RUN = {
6509
6585
  declared: false,
6510
6586
  snapshot: null,
@@ -6726,11 +6802,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6726
6802
  }
6727
6803
  return out;
6728
6804
  }
6805
+ /**
6806
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6807
+ *
6808
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6809
+ * scope (global, one node, one device, one integration, one row of a business
6810
+ * collection) and the field path inside that scope — so saving the same field
6811
+ * twice updates the same ref, and the ref itself says which settings blob
6812
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6813
+ * copied onto another ref does not decrypt.
6814
+ *
6815
+ * Array items are addressed by ITEM ID, never by index
6816
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6817
+ *
6818
+ * Pure and browser-safe: no node builtins, no I/O.
6819
+ */
6820
+ /** The URI scheme of every keychain ref. */
6821
+ var SECRET_REF_SCHEME = "secret://";
6729
6822
  var GLOBAL_SCOPE = { kind: "global" };
6730
6823
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6731
6824
  function globalSecretScope() {
6732
6825
  return GLOBAL_SCOPE;
6733
6826
  }
6827
+ /**
6828
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6829
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6830
+ * addon running forked.
6831
+ */
6832
+ function normalizeSecretOwner(addonId) {
6833
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6834
+ }
6734
6835
  function encodeSegment(value) {
6735
6836
  return encodeURIComponent(value);
6736
6837
  }
@@ -6741,6 +6842,16 @@ function decodeSegment(value) {
6741
6842
  return null;
6742
6843
  }
6743
6844
  }
6845
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6846
+ function formatSecretScope(scope) {
6847
+ switch (scope.kind) {
6848
+ case "global": return "global";
6849
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6850
+ case "device": return `device/${scope.deviceId}`;
6851
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6852
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6853
+ }
6854
+ }
6744
6855
  /**
6745
6856
  * The field path of a value inside an array item, addressed by the item's id:
6746
6857
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6749,6 +6860,13 @@ function decodeSegment(value) {
6749
6860
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6750
6861
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6751
6862
  }
6863
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6864
+ function buildSecretRef(parts) {
6865
+ const owner = normalizeSecretOwner(parts.owner);
6866
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6867
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6868
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6869
+ }
6752
6870
  function parseScope(segments) {
6753
6871
  switch (segments[0]) {
6754
6872
  case "global": return {
@@ -7674,7 +7792,7 @@ var BaseAddon = class {
7674
7792
  startMigration: (input) => api.startMigration.mutate(input),
7675
7793
  finishMigration: (input) => api.finishMigration.mutate(input)
7676
7794
  }, migrations, this.ctx.logger);
7677
- if (report.blockOwner.length > 0) throw new Error(`migration(s) ${report.blockOwner.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
7795
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7678
7796
  return report;
7679
7797
  }
7680
7798
  /**
@@ -8014,6 +8132,10 @@ var BaseAddon = class {
8014
8132
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8015
8133
  const patch = {};
8016
8134
  for (const key of keys) {
8135
+ if (tree.get(key)?.kind === "array") {
8136
+ patch[key] = [];
8137
+ continue;
8138
+ }
8017
8139
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8018
8140
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8019
8141
  }
@@ -8702,11 +8824,17 @@ var RECORD_BATCH_MAX = 1e3;
8702
8824
  var RECORD_QUERY_MAX = 1e3;
8703
8825
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8704
8826
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8827
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8828
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8705
8829
  /**
8706
8830
  * Secret fields of a record, addressed the way a Zod schema declares them
8707
8831
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8708
8832
  * (`auth.accessToken`, `headers[].value`).
8709
8833
  *
8834
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8835
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8836
+ * key releases exactly that key's secret.
8837
+ *
8710
8838
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8711
8839
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8712
8840
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8719,11 +8847,13 @@ function parseSecretPath(path) {
8719
8847
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8720
8848
  return path.split(".").map((raw) => {
8721
8849
  const each = raw.endsWith("[]");
8722
- const key = each ? raw.slice(0, -2) : raw;
8850
+ const map = raw.endsWith("{}");
8851
+ const key = each || map ? raw.slice(0, -2) : raw;
8723
8852
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8724
8853
  return {
8725
8854
  key,
8726
- each
8855
+ each,
8856
+ map
8727
8857
  };
8728
8858
  });
8729
8859
  }
@@ -8738,6 +8868,128 @@ function secretArrayPrefixes(path) {
8738
8868
  }
8739
8869
  return prefixes;
8740
8870
  }
8871
+ function isRecord$4$1(value) {
8872
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8873
+ }
8874
+ function elementId(element, idKey, declared, prefix) {
8875
+ const id = isRecord$4$1(element) ? element[idKey] : void 0;
8876
+ if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
8877
+ return id;
8878
+ }
8879
+ /**
8880
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8881
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8882
+ * result is a new body, `data` is untouched.
8883
+ */
8884
+ var REMOVE_LEAF = Symbol("remove-leaf");
8885
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8886
+ if (!isRecord$4$1(node)) return node;
8887
+ const segment = segments[index];
8888
+ if (segment === void 0) return node;
8889
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8890
+ const child = node[segment.key];
8891
+ const keys = [...walkedKeys, segment.key];
8892
+ const last = index === segments.length - 1;
8893
+ if (segment.each) {
8894
+ if (!Array.isArray(child)) return node;
8895
+ const prefix = keys.join(".");
8896
+ const idKey = arrayKeys[prefix];
8897
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8898
+ const mapped = child.map((element) => {
8899
+ const id = elementId(element, idKey, declared, prefix);
8900
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8901
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8902
+ });
8903
+ return {
8904
+ ...node,
8905
+ [segment.key]: mapped
8906
+ };
8907
+ }
8908
+ if (segment.map) {
8909
+ if (!isRecord$4$1(child)) return node;
8910
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8911
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8912
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8913
+ return [mapKey, visit({
8914
+ declared,
8915
+ concrete,
8916
+ value
8917
+ })];
8918
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8919
+ return {
8920
+ ...node,
8921
+ [segment.key]: Object.fromEntries(kept)
8922
+ };
8923
+ }
8924
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8925
+ if (last) {
8926
+ const replacement = visit({
8927
+ declared,
8928
+ concrete,
8929
+ value: child
8930
+ });
8931
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8932
+ return {
8933
+ ...node,
8934
+ [segment.key]: replacement
8935
+ };
8936
+ }
8937
+ return {
8938
+ ...node,
8939
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8940
+ };
8941
+ }
8942
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8943
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8944
+ let current = data;
8945
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8946
+ return current;
8947
+ }
8948
+ /**
8949
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8950
+ * record's refs never collide with the refs a legacy collection of the same name
8951
+ * already holds during an adoption.
8952
+ */
8953
+ function recordSecretCollection(kind) {
8954
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8955
+ }
8956
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8957
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8958
+ return buildSecretRef({
8959
+ owner,
8960
+ scope: {
8961
+ kind: "row",
8962
+ collection: recordSecretCollection(kind),
8963
+ rowId
8964
+ },
8965
+ fieldPath: concretePath
8966
+ });
8967
+ }
8968
+ /**
8969
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8970
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8971
+ * token. The form hands it to the secret field, which reveals the value by ref
8972
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8973
+ * stored secret (the records door reads a view as KEEP).
8974
+ *
8975
+ * ONE implementation for the entries service and for every addon that serves
8976
+ * its own instances (`RecordIntegrationInstances`).
8977
+ */
8978
+ function isRecord$3$1(value) {
8979
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8980
+ }
8981
+ function secretViewsOf(owner, kind, id, decl, data) {
8982
+ if (decl.secretPaths.length === 0) return data;
8983
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8984
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8985
+ return {
8986
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8987
+ v: 1,
8988
+ set: true
8989
+ };
8990
+ });
8991
+ return isRecord$3$1(shown) ? shown : data;
8992
+ }
8741
8993
  /**
8742
8994
  * The share-view scope kinds a share-link token can carry. The hub mints
8743
8995
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8810,7 +9062,8 @@ function isZodSecret(schema) {
8810
9062
  }
8811
9063
  /**
8812
9064
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8813
- * element of this array" (`connection.password`, `rows[].token`). A declared
9065
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9066
+ * map" (`applianceSecrets{}`). A declared
8814
9067
  * object or array is reported as one path and not descended into — everything
8815
9068
  * under it is secret.
8816
9069
  */
@@ -8821,6 +9074,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8821
9074
  const def = defOf(layer);
8822
9075
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8823
9076
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9077
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8824
9078
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8825
9079
  else if (def.type === "intersection") {
8826
9080
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -8960,7 +9214,7 @@ function defineRecordKinds(specs) {
8960
9214
  retention: spec.retention,
8961
9215
  deviceRemoval: spec.deviceRemoval,
8962
9216
  maxDataBytes: spec.maxDataBytes,
8963
- secretPaths: [...secretPaths],
9217
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
8964
9218
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
8965
9219
  transactFamily: spec.transactFamily,
8966
9220
  entry: spec.entry === void 0 ? void 0 : {
@@ -9005,6 +9259,7 @@ function defineEntryKind(name, spec) {
9005
9259
  ...spec.database === void 0 ? {} : { database: spec.database },
9006
9260
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9007
9261
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9262
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9008
9263
  entry: {
9009
9264
  title: spec.title,
9010
9265
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -9669,6 +9924,7 @@ var CAP_NAMES = Object.freeze([
9669
9924
  "backup",
9670
9925
  "battery",
9671
9926
  "binary",
9927
+ "board-key-source",
9672
9928
  "brightness",
9673
9929
  "broker",
9674
9930
  "button",
@@ -9790,6 +10046,7 @@ var CAP_NAMES = Object.freeze([
9790
10046
  "privacy-mask",
9791
10047
  "ptz",
9792
10048
  "ptz-autotrack",
10049
+ "pump-cycle",
9793
10050
  "reboot",
9794
10051
  "record-entries",
9795
10052
  "recording",
@@ -11082,18 +11339,6 @@ Object.freeze({
11082
11339
  "path": "pumpRunning",
11083
11340
  "valueType": "boolean"
11084
11341
  },
11085
- {
11086
- "path": "pumpIntervalEnabled",
11087
- "valueType": "boolean"
11088
- },
11089
- {
11090
- "path": "pumpOnTime",
11091
- "valueType": "number"
11092
- },
11093
- {
11094
- "path": "pumpOffTime",
11095
- "valueType": "number"
11096
- },
11097
11342
  {
11098
11343
  "path": "lastDrink",
11099
11344
  "valueType": "number"
@@ -11217,6 +11462,20 @@ Object.freeze({
11217
11462
  "valueType": "number"
11218
11463
  }
11219
11464
  ],
11465
+ "pump-cycle": [
11466
+ {
11467
+ "path": "enabled",
11468
+ "valueType": "boolean"
11469
+ },
11470
+ {
11471
+ "path": "onTime",
11472
+ "valueType": "number"
11473
+ },
11474
+ {
11475
+ "path": "offTime",
11476
+ "valueType": "number"
11477
+ }
11478
+ ],
11220
11479
  "recording-archive": [
11221
11480
  {
11222
11481
  "path": "deviceId",
@@ -11666,7 +11925,7 @@ object({
11666
11925
  params: record(string$2(), union([string$2(), number()])).optional()
11667
11926
  });
11668
11927
  //#endregion
11669
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11928
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11670
11929
  /**
11671
11930
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11672
11931
  *
@@ -12429,38 +12688,6 @@ function validateExpressionSource(src) {
12429
12688
  return null;
12430
12689
  }
12431
12690
  /**
12432
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12433
- *
12434
- * Standby of the control board is under 5 W; a motor or a heater is far
12435
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12436
- * a cycle, so running starts only after 30 s above the line.
12437
- *
12438
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12439
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12440
- * report "finished" in the middle of the program, so the stop dwell is
12441
- * 10 minutes on every type. The number is stored per type so one machine
12442
- * can move later without moving the others.
12443
- */
12444
- var APPLIANCE_TEMPLATE_ID = "appliance";
12445
- var APPLIANCE_KINDS = [
12446
- "washer",
12447
- "dryer",
12448
- "dishwasher"
12449
- ];
12450
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12451
- /**
12452
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12453
- *
12454
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12455
- * (D967), so the heater switches exactly as the plug does. Running is the
12456
- * plug being on; when a power reading is also named, running is the draw
12457
- * staying above the line instead (a plug left on with the stove's own
12458
- * thermostat open is not heating). 50 W is far above any standby and below
12459
- * the smallest radiator.
12460
- */
12461
- var HEATER_TEMPLATE_ID = "heater";
12462
- DeviceType.Heater;
12463
- /**
12464
12691
  * Statistics and integrals over a history read (D944).
12465
12692
  *
12466
12693
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12604,34 +12831,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12604
12831
  /** Actuator groups only. Readings leave this unset. */
12605
12832
  active: ActiveCriterionSchema.optional()
12606
12833
  }),
12607
- object({
12608
- kind: literal("appliance"),
12609
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12610
- appliance: _enum(APPLIANCE_KINDS),
12611
- thresholdWatts: number().positive(),
12612
- startDwellMs: number().int().nonnegative(),
12613
- stopDwellMs: number().int().nonnegative(),
12614
- member: HelperMemberSchema,
12615
- /**
12616
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12617
- * composition carries a `power-source` feature, so the hub can cut and restore
12618
- * that plug (power-source link of a NEW-target composition).
12619
- */
12620
- powerSwitch: HelperMemberSchema.optional(),
12621
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12622
- offSeconds: number().optional()
12623
- }),
12624
- object({
12625
- kind: literal("heater"),
12626
- templateId: literal(HEATER_TEMPLATE_ID),
12627
- /** The plug, read and commanded at `switch.on`. */
12628
- plug: HelperMemberSchema,
12629
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12630
- meter: HelperMemberSchema.optional(),
12631
- thresholdWatts: number().positive(),
12632
- startDwellMs: number().int().nonnegative(),
12633
- stopDwellMs: number().int().nonnegative()
12634
- }),
12635
12834
  object({
12636
12835
  kind: literal("statistic"),
12637
12836
  stat: _enum(STATISTIC_STATS),
@@ -12803,7 +13002,14 @@ var CompositionForwardCommandSchema = object({
12803
13002
  method: string$2().min(1),
12804
13003
  args: record(string$2().min(1), CompositionForwardArgSchema).optional()
12805
13004
  });
12806
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
13005
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
13006
+ CompositionForwardCommandSchema,
13007
+ CompositionCodeSourceSchema,
13008
+ object({
13009
+ kind: literal("extension"),
13010
+ target: literal("remote")
13011
+ })
13012
+ ]);
12807
13013
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12808
13014
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12809
13015
  /**
@@ -12848,7 +13054,7 @@ var CompositionSchema = object({
12848
13054
  kind: literal("existing"),
12849
13055
  device: CompositionSourceRefSchema
12850
13056
  })]),
12851
- features: array(CompositionFeatureSchema).min(1).max(16),
13057
+ features: array(CompositionFeatureSchema).min(0).max(16),
12852
13058
  /**
12853
13059
  * Present when the block is a helper template. Absent on a free
12854
13060
  * composition (the room example, device 4603). The editor reads this,
@@ -12880,6 +13086,73 @@ var NodeColorIdSchema = _enum([
12880
13086
  "node-g",
12881
13087
  "node-h"
12882
13088
  ]);
13089
+ //#endregion
13090
+ //#region ../types/dist/index.mjs
13091
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
13092
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13093
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13094
+ /** The text excerpt a message carries on the bus. */
13095
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13096
+ var PortSchema = number().int().min(1).max(65535);
13097
+ /** A recipient local-part: what stands before the `@`. */
13098
+ var SmtpRecipientSchema = string$2().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13099
+ object({
13100
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13101
+ bindHost: string$2().min(1).default("0.0.0.0"),
13102
+ domain: string$2().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13103
+ requireAuth: boolean().default(true),
13104
+ username: string$2().default(""),
13105
+ password: string$2().default("").meta({ secret: true }),
13106
+ starttls: boolean().default(false),
13107
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13108
+ });
13109
+ object({
13110
+ host: string$2().min(1).nullable(),
13111
+ hostReason: string$2().nullable(),
13112
+ port: PortSchema,
13113
+ domain: string$2().min(1),
13114
+ requireAuth: boolean(),
13115
+ username: string$2(),
13116
+ password: string$2(),
13117
+ starttls: boolean()
13118
+ });
13119
+ object({
13120
+ recipients: array(SmtpRecipientSchema).min(1),
13121
+ /**
13122
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13123
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13124
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13125
+ * Absent, the subscriber is anonymous and unique.
13126
+ */
13127
+ subscriberId: string$2().min(1).max(64).optional()
13128
+ });
13129
+ var SmtpInboundAttachmentSchema = object({
13130
+ filename: string$2(),
13131
+ contentType: string$2(),
13132
+ bytes: number().int().nonnegative()
13133
+ });
13134
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13135
+ var SmtpInboundMessageSchema = object({
13136
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13137
+ seq: number().int().positive(),
13138
+ messageId: string$2(),
13139
+ receivedAt: number().int().nonnegative(),
13140
+ from: string$2(),
13141
+ to: string$2(),
13142
+ subject: string$2(),
13143
+ text: string$2().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13144
+ attachments: array(SmtpInboundAttachmentSchema)
13145
+ });
13146
+ object({
13147
+ /** The newest seq issued so far (0 before any mail). */
13148
+ lastSeq: number().int().nonnegative(),
13149
+ /** The oldest seq still retained; messages below it are gone. */
13150
+ oldestSeq: number().int().nonnegative(),
13151
+ messages: array(object({
13152
+ key: string$2(),
13153
+ message: SmtpInboundMessageSchema
13154
+ }))
13155
+ });
12883
13156
  new Set([
12884
13157
  "track",
12885
13158
  "face",
@@ -12999,6 +13272,205 @@ var IntegrationInstanceRefSchema = object({
12999
13272
  ]),
13000
13273
  candidateStableId: string$2().optional()
13001
13274
  });
13275
+ /**
13276
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13277
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13278
+ *
13279
+ * Every instance is one record of the addon's own kind, keyed by the
13280
+ * `integrations` registry id. The records door does the secret work: a field the
13281
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13282
+ * deleting the row deletes the secret, and one row's value is never another's.
13283
+ *
13284
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13285
+ * token or the view the form was given KEEP the stored secret;
13286
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13287
+ */
13288
+ /** The most instances one addon lists (an integrations page, not a table). */
13289
+ var INSTANCES_LIST_MAX = 1e3;
13290
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13291
+ function topLevelSecretKeys(paths) {
13292
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13293
+ }
13294
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13295
+ function secretMapKeys(paths) {
13296
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13297
+ }
13298
+ /**
13299
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13300
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13301
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13302
+ */
13303
+ function secretMapPatch(value) {
13304
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13305
+ const out = {};
13306
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13307
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13308
+ return Object.keys(out).length > 0 ? out : null;
13309
+ }
13310
+ /** Does the kind's body schema carry an `enabled` field? */
13311
+ function declaresEnabled(kind) {
13312
+ const json = toJSONSchema(kind.schema, {
13313
+ unrepresentable: "any",
13314
+ io: "input"
13315
+ });
13316
+ const properties = Reflect.get(json, "properties");
13317
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13318
+ }
13319
+ var RecordIntegrationInstances = class {
13320
+ records;
13321
+ kind;
13322
+ hooks;
13323
+ handle;
13324
+ secretKeys;
13325
+ secretMaps;
13326
+ nameField;
13327
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13328
+ registryOwnsEnabled;
13329
+ constructor(records, kind, hooks) {
13330
+ this.records = records;
13331
+ this.kind = kind;
13332
+ this.hooks = hooks;
13333
+ this.handle = records.kind(kind);
13334
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13335
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13336
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13337
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13338
+ 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"`);
13339
+ }
13340
+ /**
13341
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13342
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13343
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13344
+ */
13345
+ async readResolved(id) {
13346
+ try {
13347
+ const record = await this.handle.getResolved(id);
13348
+ return record === null ? null : {
13349
+ id,
13350
+ body: record.data,
13351
+ enabled: record.data.enabled !== false,
13352
+ secrets: "resolved"
13353
+ };
13354
+ } catch (err) {
13355
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13356
+ const stored = await this.handle.get(id);
13357
+ if (stored === null) return null;
13358
+ return {
13359
+ id,
13360
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13361
+ enabled: stored.data.enabled !== false,
13362
+ secrets: "unavailable"
13363
+ };
13364
+ }
13365
+ }
13366
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13367
+ async getResolved(id) {
13368
+ return (await this.readResolved(id))?.body ?? null;
13369
+ }
13370
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13371
+ async listResolved() {
13372
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13373
+ const out = [];
13374
+ for (const record of page.records) {
13375
+ const read = await this.readResolved(record.id);
13376
+ if (read !== null) out.push(read);
13377
+ }
13378
+ return out;
13379
+ }
13380
+ viewOf(record) {
13381
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13382
+ const rawName = record.data[this.nameField];
13383
+ const kind = this.hooks.kindOf?.(record.data);
13384
+ const health = this.hooks.health?.(record.data, record.id);
13385
+ return {
13386
+ id: record.id,
13387
+ name: typeof rawName === "string" ? rawName : record.id,
13388
+ configured: this.hooks.configured(record.data),
13389
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13390
+ formValues: shown,
13391
+ ...kind !== void 0 ? { kind } : {},
13392
+ ...health?.status !== void 0 ? { status: health.status } : {},
13393
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13394
+ };
13395
+ }
13396
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13397
+ ...this.hooks.description,
13398
+ mintsId: true
13399
+ };
13400
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13401
+ listInstances = async () => {
13402
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13403
+ };
13404
+ getInstance = async ({ id }) => {
13405
+ const record = await this.handle.get(id);
13406
+ return record === null ? null : this.viewOf(record);
13407
+ };
13408
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13409
+ createInstance = async ({ id, name, kind }) => {
13410
+ const minted = id ?? this.hooks.mintId?.({
13411
+ name,
13412
+ ...kind !== void 0 ? { kind } : {}
13413
+ });
13414
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13415
+ await this.handle.put({
13416
+ id: minted,
13417
+ data: this.hooks.initialBody(name, kind)
13418
+ }, { ifRev: 0 });
13419
+ await this.hooks.onChanged?.(minted, "created");
13420
+ return { id: minted };
13421
+ };
13422
+ updateInstance = async ({ id, name, values }) => {
13423
+ const patch = {};
13424
+ for (const [key, value] of Object.entries(values)) {
13425
+ if (value === void 0) continue;
13426
+ if (this.secretMaps.has(key)) {
13427
+ const entries = secretMapPatch(value);
13428
+ if (entries !== null) patch[key] = entries;
13429
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13430
+ else if (isSecretClear(value)) patch[key] = null;
13431
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13432
+ }
13433
+ if (name !== void 0) patch[this.nameField] = name;
13434
+ await this.handle.patch(id, patch);
13435
+ await this.hooks.onChanged?.(id, "updated");
13436
+ return { ok: true };
13437
+ };
13438
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13439
+ setInstanceEnabled = async ({ id, enabled }) => {
13440
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13441
+ await this.handle.patch(id, { enabled });
13442
+ await this.hooks.onChanged?.(id, "enabled");
13443
+ return { ok: true };
13444
+ };
13445
+ /** The row and its keychain refs go together (the records door). */
13446
+ deleteInstance = async ({ id }) => {
13447
+ await this.handle.delete(id);
13448
+ await this.hooks.onChanged?.(id, "deleted");
13449
+ return { ok: true };
13450
+ };
13451
+ testInstance = async (input) => {
13452
+ if (this.hooks.test === void 0) return {
13453
+ success: false,
13454
+ status: "unsupported",
13455
+ error: "This integration declares no connection test"
13456
+ };
13457
+ return this.hooks.test(input);
13458
+ };
13459
+ /** The flat provider object to hand to `registerProvider`. */
13460
+ toProvider() {
13461
+ return {
13462
+ describe: this.describe,
13463
+ instanceFormSchema: this.instanceFormSchema,
13464
+ listInstances: this.listInstances,
13465
+ getInstance: this.getInstance,
13466
+ createInstance: this.createInstance,
13467
+ updateInstance: this.updateInstance,
13468
+ setInstanceEnabled: this.setInstanceEnabled,
13469
+ deleteInstance: this.deleteInstance,
13470
+ testInstance: this.testInstance
13471
+ };
13472
+ }
13473
+ };
13002
13474
  var EncodeProfileSchema = object({
13003
13475
  video: object({
13004
13476
  codec: _enum([
@@ -14067,6 +14539,58 @@ var MigrationOwnerStepSchema = object({
14067
14539
  /** The ledger state the engine found: `pending` has no row yet. */
14068
14540
  state: union([literal("pending"), MigrationStatusSchema])
14069
14541
  });
14542
+ function isPlain(value) {
14543
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14544
+ }
14545
+ async function flattenNestedRecords(ports, spec) {
14546
+ const ids = await ports.listNested(spec.legacyKey);
14547
+ if (ids.length === 0) return {
14548
+ outcome: "already-done",
14549
+ summary: { converted: 0 }
14550
+ };
14551
+ const rows = /* @__PURE__ */ new Map();
14552
+ for (const id of ids) try {
14553
+ rows.set(id, await ports.readResolved(id));
14554
+ } catch (err) {
14555
+ const why = err instanceof Error ? err.message : String(err);
14556
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14557
+ }
14558
+ for (const [id, row] of rows) {
14559
+ const legacy = row[spec.legacyKey];
14560
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14561
+ const flat = spec.toFlat(legacy, row);
14562
+ await ports.patch(id, flat);
14563
+ const back = await ports.readResolved(id);
14564
+ const wrong = spec.flatSecretKeys.filter((key) => {
14565
+ const sent = flat[key];
14566
+ const got = back[key];
14567
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14568
+ });
14569
+ if (wrong.length > 0) {
14570
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14571
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14572
+ }
14573
+ await ports.patch(id, { [spec.legacyKey]: null });
14574
+ }
14575
+ return {
14576
+ outcome: "done",
14577
+ summary: { converted: rows.size }
14578
+ };
14579
+ }
14580
+ /** The ports over one kind's records handle. */
14581
+ function flattenPortsOver(handle) {
14582
+ return {
14583
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14584
+ readResolved: async (id) => {
14585
+ const row = await handle.getResolved(id);
14586
+ if (row === null) throw new Error("the record is gone");
14587
+ return row.data;
14588
+ },
14589
+ patch: async (id, patch) => {
14590
+ await handle.patch(id, patch);
14591
+ }
14592
+ };
14593
+ }
14070
14594
  /**
14071
14595
  * The trainer contract — what the hub hands the cloud trainer and what it
14072
14596
  * gets back (cloud training, D805+).
@@ -19152,24 +19676,52 @@ var APPLIANCE_CAP_VOCABULARY = {
19152
19676
  args: { on: false }
19153
19677
  },
19154
19678
  {
19155
- id: "petFountain.intervalOn",
19156
- method: "setPumpInterval",
19679
+ id: "petFountain.resetDailyConsumption",
19680
+ method: "resetDailyConsumption",
19681
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19682
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19683
+ icon: "rotate-ccw"
19684
+ }
19685
+ ],
19686
+ state: [{
19687
+ kind: "boolean",
19688
+ field: "empty",
19689
+ label: t("Tank empty", "Serbatoio vuoto"),
19690
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19691
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19692
+ icon: "droplet"
19693
+ }, {
19694
+ kind: "numeric",
19695
+ field: "waterLevel",
19696
+ quantity: t("the water level", "il livello dell’acqua"),
19697
+ label: t("Water level", "Livello dell’acqua"),
19698
+ unit: "%",
19699
+ icon: "droplets"
19700
+ }]
19701
+ },
19702
+ "pump-cycle": {
19703
+ scope: "device",
19704
+ noun: t("Pump cycle", "Ciclo della pompa"),
19705
+ actions: [
19706
+ {
19707
+ id: "pumpCycle.on",
19708
+ method: "setEnabled",
19157
19709
  label: t("Pump cycle on", "Ciclo pompa attivo"),
19158
19710
  phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
19159
19711
  icon: "timer",
19160
19712
  args: { on: true }
19161
19713
  },
19162
19714
  {
19163
- id: "petFountain.intervalOff",
19164
- method: "setPumpInterval",
19715
+ id: "pumpCycle.off",
19716
+ method: "setEnabled",
19165
19717
  label: t("Pump cycle off", "Ciclo pompa spento"),
19166
19718
  phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
19167
19719
  icon: "timer-off",
19168
19720
  args: { on: false }
19169
19721
  },
19170
19722
  {
19171
- id: "petFountain.pumpOnMinutes",
19172
- method: "setPumpOnMinutes",
19723
+ id: "pumpCycle.onMinutes",
19724
+ method: "setOnMinutes",
19173
19725
  label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
19174
19726
  phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
19175
19727
  icon: "timer",
@@ -19185,8 +19737,8 @@ var APPLIANCE_CAP_VOCABULARY = {
19185
19737
  }]
19186
19738
  },
19187
19739
  {
19188
- id: "petFountain.pumpOffMinutes",
19189
- method: "setPumpOffMinutes",
19740
+ id: "pumpCycle.offMinutes",
19741
+ method: "setOffMinutes",
19190
19742
  label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
19191
19743
  phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
19192
19744
  icon: "timer-off",
@@ -19200,35 +19752,63 @@ var APPLIANCE_CAP_VOCABULARY = {
19200
19752
  unit: "min"
19201
19753
  }
19202
19754
  }]
19203
- },
19204
- {
19205
- id: "petFountain.resetDailyConsumption",
19206
- method: "resetDailyConsumption",
19207
- label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19208
- phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19209
- icon: "rotate-ccw"
19210
19755
  }
19211
19756
  ],
19212
19757
  state: [{
19213
19758
  kind: "boolean",
19214
- field: "empty",
19215
- label: t("Tank empty", "Serbatoio vuoto"),
19216
- whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19217
- whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19218
- icon: "droplet"
19219
- }, {
19220
- kind: "numeric",
19221
- field: "waterLevel",
19222
- quantity: t("the water level", "il livello dell’acqua"),
19223
- label: t("Water level", "Livello dell’acqua"),
19224
- unit: "%",
19225
- icon: "droplets"
19759
+ field: "enabled",
19760
+ label: t("Pump cycle", "Ciclo pompa"),
19761
+ whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
19762
+ whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
19763
+ icon: "timer"
19226
19764
  }]
19227
19765
  },
19228
19766
  "media-player": {
19229
19767
  scope: "device",
19230
19768
  noun: t("Media player", "Lettore multimediale"),
19231
19769
  actions: [
19770
+ {
19771
+ id: "media.turnOn",
19772
+ method: "turnOn",
19773
+ label: t("Turn on", "Accendi"),
19774
+ phrase: t("Turn on {device}", "Accendi {device}"),
19775
+ icon: "power"
19776
+ },
19777
+ {
19778
+ id: "media.turnOff",
19779
+ method: "turnOff",
19780
+ label: t("Turn off", "Spegni"),
19781
+ phrase: t("Turn off {device}", "Spegni {device}"),
19782
+ icon: "power"
19783
+ },
19784
+ {
19785
+ id: "media.volumeUp",
19786
+ method: "volumeUp",
19787
+ label: t("Volume up", "Alza il volume"),
19788
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
19789
+ icon: "volume-2"
19790
+ },
19791
+ {
19792
+ id: "media.volumeDown",
19793
+ method: "volumeDown",
19794
+ label: t("Volume down", "Abbassa il volume"),
19795
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
19796
+ icon: "volume-2"
19797
+ },
19798
+ {
19799
+ id: "media.channelUp",
19800
+ method: "channelUp",
19801
+ label: t("Channel up", "Canale successivo"),
19802
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
19803
+ icon: "skip-forward"
19804
+ },
19805
+ {
19806
+ id: "media.channelDown",
19807
+ method: "channelDown",
19808
+ label: t("Channel down", "Canale precedente"),
19809
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
19810
+ icon: "skip-back"
19811
+ },
19232
19812
  {
19233
19813
  id: "media.play",
19234
19814
  method: "play",
@@ -21115,6 +21695,7 @@ var SYSTEM_CAP_VOCABULARY = {
21115
21695
  "audio-analyzer": system(PIPELINE),
21116
21696
  "audio-codec": system(INTERNAL$1),
21117
21697
  "auth-provider": system(IDENTITY),
21698
+ "board-key-source": system(INTERNAL$1),
21118
21699
  backup: system("system backups — run and restored from the Backups page"),
21119
21700
  broker: system("message-broker connections — configured, not actuated"),
21120
21701
  "calendar-source": system(INTERNAL$1),
@@ -46024,6 +46605,49 @@ var binaryCapability = {
46024
46605
  volatileStateFields: ["lastChangedAt"]
46025
46606
  };
46026
46607
  /**
46608
+ * Why a board key could not be read. Typed so a caller reports the cause and
46609
+ * never treats "could not read" as "has no key" (D49).
46610
+ */
46611
+ var BoardKeyFailureReasonSchema = _enum([
46612
+ "unknown-source",
46613
+ "not-admin",
46614
+ "no-builder",
46615
+ "no-ingress-url",
46616
+ "unreachable",
46617
+ "unknown-board",
46618
+ "no-encryption-key",
46619
+ "unreadable-config"
46620
+ ]);
46621
+ var BoardKeySourceSchema = object({
46622
+ sourceId: string$2(),
46623
+ name: string$2()
46624
+ });
46625
+ var BoardKeyBoardSchema = object({
46626
+ /** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
46627
+ configuration: string$2(),
46628
+ /** The node name the builder gives the board. */
46629
+ name: string$2(),
46630
+ /** The address the builder knows the board by, when it has one. */
46631
+ address: string$2().nullable()
46632
+ });
46633
+ var BoardKeyFailureSchema = object({
46634
+ ok: literal(false),
46635
+ reason: BoardKeyFailureReasonSchema,
46636
+ detail: string$2().optional()
46637
+ });
46638
+ var ListBoardsResultSchema = discriminatedUnion("ok", [object({
46639
+ ok: literal(true),
46640
+ boards: array(BoardKeyBoardSchema).readonly()
46641
+ }), BoardKeyFailureSchema]);
46642
+ var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
46643
+ ok: literal(true),
46644
+ key: string$2()
46645
+ }), BoardKeyFailureSchema]);
46646
+ method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string$2().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
46647
+ sourceId: string$2().min(1),
46648
+ configuration: string$2().min(1)
46649
+ }), GetBoardKeyResultSchema, { auth: "admin" });
46650
+ /**
46027
46651
  * Dimmable-light brightness control. Co-exists with `switch` on the
46028
46652
  * same device — the switch toggles on/off, this cap sets the level
46029
46653
  * applied when the light is on. Drivers map their per-vendor dim
@@ -48771,6 +49395,244 @@ var controlCapability = {
48771
49395
  */
48772
49396
  history: "none"
48773
49397
  };
49398
+ /**
49399
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49400
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49401
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49402
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49403
+ *
49404
+ * The cap declares a wide method surface; not every backend supports
49405
+ * every method. Drivers signal sub-capability via the
49406
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49407
+ * corresponding method) so the UI gates buttons without a slice
49408
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49409
+ */
49410
+ var MediaPlayerStateSchema = _enum([
49411
+ "off",
49412
+ "on",
49413
+ "idle",
49414
+ "playing",
49415
+ "paused",
49416
+ "buffering",
49417
+ "standby"
49418
+ ]);
49419
+ var MediaPlayerRepeatSchema = _enum([
49420
+ "off",
49421
+ "all",
49422
+ "one"
49423
+ ]);
49424
+ var MediaInfoSchema = object({
49425
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49426
+ * `podcast`, …). */
49427
+ type: string$2(),
49428
+ /** Human-readable title. */
49429
+ title: string$2(),
49430
+ /** Optional artist / channel / station label. */
49431
+ artist: string$2().optional(),
49432
+ /** Optional album / season / show name. */
49433
+ album: string$2().optional(),
49434
+ /** Optional cover-art / thumbnail URL. */
49435
+ imageUrl: string$2().optional()
49436
+ });
49437
+ /**
49438
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49439
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49440
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49441
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49442
+ * guess, never as a reading.
49443
+ */
49444
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49445
+ /** The fields of a media-player status a driver can only guess. */
49446
+ var MediaPlayerAssumedFieldSchema = _enum([
49447
+ "state",
49448
+ "volumeLevel",
49449
+ "isMuted",
49450
+ "source"
49451
+ ]);
49452
+ var MediaPlayerStatusSchema = object({
49453
+ /** Playback lifecycle state. */
49454
+ state: MediaPlayerStateSchema,
49455
+ /**
49456
+ * Whether the state fields are readings or the driver's own bookkeeping.
49457
+ * A slice written before this field existed parses as `reported`.
49458
+ */
49459
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49460
+ /**
49461
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49462
+ * this is not empty; a field not listed here is a reading even then (power
49463
+ * from a running-state source, mute guessed from the last send).
49464
+ */
49465
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49466
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49467
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49468
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
49469
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49470
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49471
+ isMuted: boolean().nullable(),
49472
+ /** Active source / input. Empty when no source surface. */
49473
+ source: string$2(),
49474
+ /** Selectable sources. Empty when no source surface.
49475
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49476
+ availableSources: array(string$2()),
49477
+ /** Currently-playing media info. Null when nothing is playing. */
49478
+ currentMedia: MediaInfoSchema.nullable(),
49479
+ /** Current playback position in ms. Null when not seekable or
49480
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
49481
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49482
+ /** Total duration of the current media in ms. Null when unknown
49483
+ * (live stream) or nothing is playing. */
49484
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49485
+ /** Shuffle toggle. Null when no shuffle surface.
49486
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
49487
+ shuffle: boolean().nullable(),
49488
+ /** Repeat mode. Null when no repeat surface.
49489
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
49490
+ repeat: MediaPlayerRepeatSchema.nullable(),
49491
+ /** Ms epoch when the slice was last updated. */
49492
+ lastChangedAt: number()
49493
+ });
49494
+ var stepInput = object({
49495
+ deviceId: number().int().nonnegative(),
49496
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
49497
+ steps: number().int().min(1).max(50).default(1)
49498
+ });
49499
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
49500
+ var mediaPlayerCapability = {
49501
+ name: "media-player",
49502
+ scope: "device",
49503
+ deviceNative: true,
49504
+ mode: "singleton",
49505
+ deviceTypes: [DeviceType.MediaPlayer],
49506
+ methods: {
49507
+ turnOn: method(deviceOnly, _void(), {
49508
+ kind: "mutation",
49509
+ auth: "protected"
49510
+ }),
49511
+ turnOff: method(deviceOnly, _void(), {
49512
+ kind: "mutation",
49513
+ auth: "protected"
49514
+ }),
49515
+ volumeUp: method(stepInput, _void(), {
49516
+ kind: "mutation",
49517
+ auth: "protected"
49518
+ }),
49519
+ volumeDown: method(stepInput, _void(), {
49520
+ kind: "mutation",
49521
+ auth: "protected"
49522
+ }),
49523
+ channelUp: method(stepInput, _void(), {
49524
+ kind: "mutation",
49525
+ auth: "protected"
49526
+ }),
49527
+ channelDown: method(stepInput, _void(), {
49528
+ kind: "mutation",
49529
+ auth: "protected"
49530
+ }),
49531
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49532
+ kind: "mutation",
49533
+ auth: "protected"
49534
+ }),
49535
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49536
+ kind: "mutation",
49537
+ auth: "protected"
49538
+ }),
49539
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49540
+ kind: "mutation",
49541
+ auth: "protected"
49542
+ }),
49543
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49544
+ kind: "mutation",
49545
+ auth: "protected"
49546
+ }),
49547
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49548
+ kind: "mutation",
49549
+ auth: "protected"
49550
+ }),
49551
+ seek: method(object({
49552
+ deviceId: number().int().nonnegative(),
49553
+ position: number().int().nonnegative()
49554
+ }), _void(), {
49555
+ kind: "mutation",
49556
+ auth: "protected"
49557
+ }),
49558
+ setVolume: method(object({
49559
+ deviceId: number().int().nonnegative(),
49560
+ volumeLevel: number().min(0).max(100)
49561
+ }), _void(), {
49562
+ kind: "mutation",
49563
+ auth: "protected"
49564
+ }),
49565
+ setMute: method(object({
49566
+ deviceId: number().int().nonnegative(),
49567
+ muted: boolean()
49568
+ }), _void(), {
49569
+ kind: "mutation",
49570
+ auth: "protected"
49571
+ }),
49572
+ setShuffle: method(object({
49573
+ deviceId: number().int().nonnegative(),
49574
+ shuffle: boolean()
49575
+ }), _void(), {
49576
+ kind: "mutation",
49577
+ auth: "protected"
49578
+ }),
49579
+ setRepeat: method(object({
49580
+ deviceId: number().int().nonnegative(),
49581
+ repeat: MediaPlayerRepeatSchema
49582
+ }), _void(), {
49583
+ kind: "mutation",
49584
+ auth: "protected"
49585
+ }),
49586
+ selectSource: method(object({
49587
+ deviceId: number().int().nonnegative(),
49588
+ source: string$2().min(1)
49589
+ }), _void(), {
49590
+ kind: "mutation",
49591
+ auth: "protected"
49592
+ }),
49593
+ playMedia: method(object({
49594
+ deviceId: number().int().nonnegative(),
49595
+ /** Media identifier / URL. */
49596
+ mediaId: string$2().min(1),
49597
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
49598
+ * passes it through to the upstream service. */
49599
+ mediaType: string$2().min(1)
49600
+ }), _void(), {
49601
+ kind: "mutation",
49602
+ auth: "protected"
49603
+ })
49604
+ },
49605
+ status: {
49606
+ schema: MediaPlayerStatusSchema,
49607
+ kind: "push"
49608
+ },
49609
+ /**
49610
+ * Runtime-state slice — mirrored by the kernel. UI players read the
49611
+ * full slice for live now-playing, volume, and progress updates
49612
+ * without polling.
49613
+ */
49614
+ runtimeState: MediaPlayerStatusSchema,
49615
+ /**
49616
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
49617
+ *
49618
+ * See `RuntimeStateDurability`. Enforced by
49619
+ * `scripts/check-runtime-state-durability.ts`.
49620
+ */
49621
+ durability: "session",
49622
+ /**
49623
+ * History (D850): volume (a level an operator or an automation sets; the
49624
+ * default 10 s floor folds a slider drag into its end value) and mute.
49625
+ * `position` is not recorded: it moves continuously while playing.
49626
+ */
49627
+ history: { fields: {
49628
+ volumeLevel: {
49629
+ kind: "gauge",
49630
+ unit: "%",
49631
+ deadband: 1
49632
+ },
49633
+ isMuted: { kind: "boolean" }
49634
+ } }
49635
+ };
48774
49636
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, 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, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
48775
49637
  var NUMBER = {
48776
49638
  kind: "fixed",
@@ -49218,117 +50080,6 @@ function actuator(input) {
49218
50080
  };
49219
50081
  }
49220
50082
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49221
- var PowerSourceRefusalReasonSchema = _enum([
49222
- "no-power-source",
49223
- "switch-missing",
49224
- "switch-offline",
49225
- "switch-unknown",
49226
- "restart-in-progress",
49227
- "switch-call-failed"
49228
- ]);
49229
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
49230
- accepted: literal(false),
49231
- reason: PowerSourceRefusalReasonSchema,
49232
- /** One line for the operator, naming the switch. */
49233
- message: string$2()
49234
- })]);
49235
- var PowerSourceInfoSchema = object({
49236
- /** False when the device has no power source linked; the rest is then empty. */
49237
- linked: boolean(),
49238
- switchDeviceId: number().nullable(),
49239
- switchName: string$2().nullable(),
49240
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
49241
- switchOn: boolean().nullable(),
49242
- switchOnline: boolean().nullable(),
49243
- restartInProgress: boolean(),
49244
- offSeconds: number().int()
49245
- });
49246
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
49247
- var PowerSourceStateSchema = object({
49248
- /** Null while the switch has not reported: unknown is never `false` (D393). */
49249
- switchOn: boolean().nullable(),
49250
- offSeconds: number().int(),
49251
- lastFetchedAt: number()
49252
- });
49253
- var POWER_SOURCE_DEVICE_TYPES = [
49254
- DeviceType.Gateway,
49255
- DeviceType.Hub,
49256
- DeviceType.Camera,
49257
- DeviceType.Sensor,
49258
- DeviceType.MediaPlayer,
49259
- DeviceType.Vacuum,
49260
- DeviceType.Generic,
49261
- DeviceType.Washer,
49262
- DeviceType.Dryer,
49263
- DeviceType.Dishwasher,
49264
- DeviceType.PetFountain,
49265
- DeviceType.Fan,
49266
- DeviceType.Humidifier,
49267
- DeviceType.Climate,
49268
- DeviceType.WaterHeater
49269
- ];
49270
- var powerSourceCustomize = {
49271
- version: 1,
49272
- targets: [{
49273
- id: "switch",
49274
- cardinality: "scalar",
49275
- vocabulary: {
49276
- cap: "power-source",
49277
- stateField: "switchOn"
49278
- },
49279
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49280
- apply: "add",
49281
- path: "switchOn",
49282
- sources: [{
49283
- kind: "switch",
49284
- transforms: [{ kind: "identity" }]
49285
- }],
49286
- companions: [{
49287
- kind: "constant",
49288
- path: "offSeconds",
49289
- value: 10,
49290
- when: "add"
49291
- }, {
49292
- kind: "clock",
49293
- path: "lastFetchedAt",
49294
- when: "always"
49295
- }],
49296
- sourceReport: { status: "assignment-status" }
49297
- }]
49298
- };
49299
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
49300
- var powerSourceCapability = {
49301
- name: "power-source",
49302
- scope: "device",
49303
- mode: "singleton",
49304
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49305
- methods: {
49306
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
49307
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49308
- kind: "mutation",
49309
- auth: "admin"
49310
- }),
49311
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49312
- kind: "mutation",
49313
- auth: "admin"
49314
- }),
49315
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49316
- kind: "mutation",
49317
- auth: "admin"
49318
- })
49319
- },
49320
- runtimeState: PowerSourceStateSchema,
49321
- customize: powerSourceCustomize,
49322
- /**
49323
- * Runtime-state durability: **session** — mirrored from the linked switch on
49324
- * its next report; a restored value would outlive the reading it names.
49325
- *
49326
- * See `RuntimeStateDurability`. Enforced by
49327
- * `scripts/check-runtime-state-durability.ts`.
49328
- */
49329
- durability: "session",
49330
- history: "none"
49331
- };
49332
50083
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
49333
50084
  DeviceType.Camera;
49334
50085
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -49356,6 +50107,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
49356
50107
  }).optional()
49357
50108
  })) });
49358
50109
  /**
50110
+ * A saved Customize assignment (D936). The schema is the type: callers use
50111
+ * `z.infer`, they do not restate the shape.
50112
+ */
50113
+ var OverrideSourceSchema = object({
50114
+ addonId: string$2().min(1),
50115
+ stableId: string$2().min(1),
50116
+ cap: string$2().min(1),
50117
+ fieldPath: string$2().min(1)
50118
+ });
50119
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50120
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50121
+ var ThresholdTransformDraftSchema = object({
50122
+ kind: literal("threshold"),
50123
+ comparison: _enum(["gte", "lte"]),
50124
+ threshold: number(),
50125
+ startDwellMs: number().nonnegative(),
50126
+ stopDwellMs: number().nonnegative()
50127
+ });
50128
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50129
+ IdentityTransformDraftSchema,
50130
+ InvertTransformDraftSchema,
50131
+ ThresholdTransformDraftSchema,
50132
+ object({
50133
+ kind: literal("linear"),
50134
+ scale: number(),
50135
+ offset: number(),
50136
+ clampMin: number().optional(),
50137
+ clampMax: number().optional()
50138
+ }),
50139
+ object({
50140
+ kind: literal("enum-map"),
50141
+ mapping: record(string$2(), string$2().nullable())
50142
+ }),
50143
+ object({ kind: literal("number") })
50144
+ ]);
50145
+ var ScalarOverrideDraftSchema = object({
50146
+ kind: literal("scalar"),
50147
+ cap: string$2().min(1),
50148
+ targetId: string$2().min(1),
50149
+ source: OverrideSourceSchema,
50150
+ transform: CustomizeTransformDraftSchema
50151
+ });
50152
+ var KeyedArrayOverrideDraftSchema = object({
50153
+ kind: literal("keyedArray"),
50154
+ cap: string$2().min(1),
50155
+ targetId: string$2().min(1),
50156
+ key: string$2().min(1),
50157
+ label: string$2().min(1),
50158
+ fieldPath: string$2().min(1),
50159
+ source: OverrideSourceSchema,
50160
+ transform: CustomizeTransformDraftSchema
50161
+ });
50162
+ var PulseOverrideDraftSchema = object({
50163
+ kind: literal("pulse"),
50164
+ cap: string$2().min(1),
50165
+ targetId: string$2().min(1),
50166
+ enabled: boolean(),
50167
+ debounceMs: number().nonnegative(),
50168
+ edges: array(_enum([
50169
+ "rising",
50170
+ "button",
50171
+ "native-event"
50172
+ ])).min(1).optional(),
50173
+ sources: array(OverrideSourceSchema).min(1)
50174
+ });
50175
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50176
+ "in-addition",
50177
+ "confirm",
50178
+ "replace"
50179
+ ]).optional() });
50180
+ discriminatedUnion("kind", [
50181
+ ScalarOverrideDraftSchema,
50182
+ KeyedArrayOverrideDraftSchema,
50183
+ PulseOverrideDraftSchema,
50184
+ object({
50185
+ kind: literal("span"),
50186
+ cap: string$2().min(1),
50187
+ targetId: string$2().min(1),
50188
+ timeoutMs: number().nonnegative(),
50189
+ cooldownMs: number().nonnegative().optional(),
50190
+ sources: array(SpanSourceDraftSchema)
50191
+ })
50192
+ ]);
50193
+ object({
50194
+ targetId: string$2().min(1),
50195
+ configured: boolean(),
50196
+ source: _enum(["native", "assigned"]),
50197
+ state: _enum([
50198
+ "active",
50199
+ "stale",
50200
+ "released",
50201
+ "unavailable"
50202
+ ]),
50203
+ reason: string$2().nullable()
50204
+ });
50205
+ /**
50206
+ * What a running-threshold proposal says (planning#405). The schema is the
50207
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50208
+ *
50209
+ * A proposal is read-only advice: it carries the numbers it was derived from
50210
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50211
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50212
+ */
50213
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50214
+ var RunningProposalConfidenceSchema = _enum([
50215
+ "high",
50216
+ "medium",
50217
+ "low"
50218
+ ]);
50219
+ var RunningProposalInsufficientReasonSchema = _enum([
50220
+ "no-history",
50221
+ "span-too-short",
50222
+ "truncated",
50223
+ "no-idle-floor",
50224
+ "no-runs",
50225
+ "no-separation"
50226
+ ]);
50227
+ var RunningProposalReplaySchema = object({
50228
+ /** Intervals the real dwell expression held true over the window. */
50229
+ intervals: number().int().nonnegative(),
50230
+ /** Intervals outside every run, or a second interval inside one. */
50231
+ extra: number().int().nonnegative(),
50232
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50233
+ missed: number().int().nonnegative()
50234
+ });
50235
+ var RunningProposalEvidenceSchema = object({
50236
+ tier: RunningProposalTierSchema,
50237
+ spanMs: number().nonnegative(),
50238
+ from: number(),
50239
+ to: number(),
50240
+ points: number().int().nonnegative(),
50241
+ runsFound: number().int().nonnegative(),
50242
+ pausesFound: number().int().nonnegative(),
50243
+ longestPauseMs: number().nonnegative(),
50244
+ idleFloorW: number(),
50245
+ noiseCeilingW: number(),
50246
+ plateauW: number(),
50247
+ replay: RunningProposalReplaySchema
50248
+ });
50249
+ var RunningProposalInsufficientEvidenceSchema = object({
50250
+ spanMs: number().nonnegative(),
50251
+ points: number().int().nonnegative(),
50252
+ runsFound: number().int().nonnegative()
50253
+ });
50254
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50255
+ state: literal("proposal"),
50256
+ transform: ThresholdTransformDraftSchema,
50257
+ evidence: RunningProposalEvidenceSchema,
50258
+ confidence: RunningProposalConfidenceSchema,
50259
+ /** Why the confidence is not higher; empty when nothing held it back. */
50260
+ notes: array(string$2())
50261
+ }), object({
50262
+ state: literal("insufficient"),
50263
+ reason: RunningProposalInsufficientReasonSchema,
50264
+ evidence: RunningProposalInsufficientEvidenceSchema,
50265
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50266
+ fallback: ThresholdTransformDraftSchema
50267
+ })]);
50268
+ /**
49359
50269
  * The report of a one-shot "move this owner's plaintext secrets into the
49360
50270
  * keychain" run (D878). COUNTS only - never a value, never a ref.
49361
50271
  *
@@ -49645,6 +50555,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49645
50555
  kind: "mutation",
49646
50556
  auth: "admin",
49647
50557
  access: "view"
50558
+ }), method(object({
50559
+ deviceId: number().int(),
50560
+ cap: string$2().min(1),
50561
+ fieldPath: string$2().min(1),
50562
+ deviceType: _enum(DeviceType).optional()
50563
+ }), RunningThresholdProposalSchema, {
50564
+ auth: "admin",
50565
+ access: "view"
49648
50566
  }), method(object({
49649
50567
  blockId: string$2().min(1),
49650
50568
  trace: AutomationRunTraceSchema.optional(),
@@ -50600,7 +51518,27 @@ method(object({
50600
51518
  * exposes only the COMMON surface: link state, supported device
50601
51519
  * kinds (so the UI can filter the device picker), and per-device
50602
51520
  * expose/unexpose.
50603
- */
51521
+ *
51522
+ * INSTANCES. One export addon can serve several independent targets (a
51523
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51524
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51525
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51526
+ * default instance's); present = that instance; an id the provider does not
51527
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51528
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51529
+ * change always names the target it changes. A single-instance provider
51530
+ * accepts an absent id on a read or its own id, and refuses any other.
51531
+ */
51532
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51533
+ var InstanceIdSchema$1 = string$2().min(1);
51534
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51535
+ /** One independent export target of an addon. */
51536
+ var ExportInstanceSchema = object({
51537
+ instanceId: InstanceIdSchema$1,
51538
+ name: string$2(),
51539
+ enabled: boolean(),
51540
+ kind: string$2().optional()
51541
+ });
50604
51542
  var LinkStateSchema = _enum([
50605
51543
  "unlinked",
50606
51544
  "linked",
@@ -50651,10 +51589,14 @@ var ExposedDeviceSchema = object({
50651
51589
  capabilities: array(string$2()).optional()
50652
51590
  });
50653
51591
  var ExposeInputSchema = object({
51592
+ instanceId: InstanceIdSchema$1,
50654
51593
  deviceId: string$2(),
50655
51594
  capabilities: array(string$2()).optional()
50656
51595
  });
50657
- var UnexposeInputSchema = object({ deviceId: string$2() });
51596
+ var UnexposeInputSchema = object({
51597
+ instanceId: InstanceIdSchema$1,
51598
+ deviceId: string$2()
51599
+ });
50658
51600
  /**
50659
51601
  * What an exporter may expose besides devices (D968). An action and a
50660
51602
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50683,15 +51625,21 @@ var ExposedEntitySchema = object({
50683
51625
  entity: ExportEntityRefSchema,
50684
51626
  exposedAs: string$2().optional()
50685
51627
  });
50686
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50687
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50688
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51628
+ var ExposeEntityInputSchema = object({
51629
+ instanceId: InstanceIdSchema$1,
51630
+ entity: ExportEntityRefSchema
51631
+ });
51632
+ var UnexposeEntityInputSchema = object({
51633
+ instanceId: InstanceIdSchema$1,
51634
+ entity: ExportEntityRefSchema
51635
+ });
51636
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50689
51637
  kind: "mutation",
50690
51638
  auth: "admin"
50691
51639
  }), method(UnexposeInputSchema, _void(), {
50692
51640
  kind: "mutation",
50693
51641
  auth: "admin"
50694
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51642
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50695
51643
  kind: "mutation",
50696
51644
  auth: "admin"
50697
51645
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -52693,7 +53641,7 @@ var DishwasherStatusSchema = object({
52693
53641
  /** Ms epoch of the last value received from the appliance; 0 = never. */
52694
53642
  lastFetchedAt: number()
52695
53643
  });
52696
- var DeviceIdSchema$1 = number().int().nonnegative();
53644
+ var DeviceIdSchema$2 = number().int().nonnegative();
52697
53645
  /** A value a program option or setting can take. */
52698
53646
  var DishwasherValueSchema = union([
52699
53647
  boolean(),
@@ -52768,77 +53716,77 @@ var dishwasherCapability = {
52768
53716
  * option feature keys; each is checked against the program's constraints.
52769
53717
  */
52770
53718
  start: method(object({
52771
- deviceId: DeviceIdSchema$1,
53719
+ deviceId: DeviceIdSchema$2,
52772
53720
  program: string$2().min(1).optional(),
52773
53721
  options: record(string$2(), DishwasherValueSchema).optional()
52774
53722
  }), _void(), {
52775
53723
  kind: "mutation",
52776
53724
  auth: "protected"
52777
53725
  }),
52778
- pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53726
+ pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52779
53727
  kind: "mutation",
52780
53728
  auth: "protected"
52781
53729
  }),
52782
- resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53730
+ resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52783
53731
  kind: "mutation",
52784
53732
  auth: "protected"
52785
53733
  }),
52786
- stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53734
+ stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52787
53735
  kind: "mutation",
52788
53736
  auth: "protected"
52789
53737
  }),
52790
53738
  /** Select (not start) a program; refused when the appliance does not offer it. */
52791
53739
  setProgram: method(object({
52792
- deviceId: DeviceIdSchema$1,
53740
+ deviceId: DeviceIdSchema$2,
52793
53741
  program: string$2().min(1)
52794
53742
  }), _void(), {
52795
53743
  kind: "mutation",
52796
53744
  auth: "protected"
52797
53745
  }),
52798
53746
  setPower: method(object({
52799
- deviceId: DeviceIdSchema$1,
53747
+ deviceId: DeviceIdSchema$2,
52800
53748
  on: boolean()
52801
53749
  }), _void(), {
52802
53750
  kind: "mutation",
52803
53751
  auth: "protected"
52804
53752
  }),
52805
53753
  setHalfLoad: method(object({
52806
- deviceId: DeviceIdSchema$1,
53754
+ deviceId: DeviceIdSchema$2,
52807
53755
  on: boolean()
52808
53756
  }), _void(), {
52809
53757
  kind: "mutation",
52810
53758
  auth: "protected"
52811
53759
  }),
52812
53760
  setHygienePlus: method(object({
52813
- deviceId: DeviceIdSchema$1,
53761
+ deviceId: DeviceIdSchema$2,
52814
53762
  on: boolean()
52815
53763
  }), _void(), {
52816
53764
  kind: "mutation",
52817
53765
  auth: "protected"
52818
53766
  }),
52819
53767
  setExtraDry: method(object({
52820
- deviceId: DeviceIdSchema$1,
53768
+ deviceId: DeviceIdSchema$2,
52821
53769
  on: boolean()
52822
53770
  }), _void(), {
52823
53771
  kind: "mutation",
52824
53772
  auth: "protected"
52825
53773
  }),
52826
53774
  setIntensiveZone: method(object({
52827
- deviceId: DeviceIdSchema$1,
53775
+ deviceId: DeviceIdSchema$2,
52828
53776
  on: boolean()
52829
53777
  }), _void(), {
52830
53778
  kind: "mutation",
52831
53779
  auth: "protected"
52832
53780
  }),
52833
53781
  setVarioSpeedPlus: method(object({
52834
- deviceId: DeviceIdSchema$1,
53782
+ deviceId: DeviceIdSchema$2,
52835
53783
  on: boolean()
52836
53784
  }), _void(), {
52837
53785
  kind: "mutation",
52838
53786
  auth: "protected"
52839
53787
  }),
52840
53788
  setChildLock: method(object({
52841
- deviceId: DeviceIdSchema$1,
53789
+ deviceId: DeviceIdSchema$2,
52842
53790
  on: boolean()
52843
53791
  }), _void(), {
52844
53792
  kind: "mutation",
@@ -52850,7 +53798,7 @@ var dishwasherCapability = {
52850
53798
  * its declared range / members.
52851
53799
  */
52852
53800
  setOption: method(object({
52853
- deviceId: DeviceIdSchema$1,
53801
+ deviceId: DeviceIdSchema$2,
52854
53802
  key: string$2().min(1),
52855
53803
  value: DishwasherValueSchema
52856
53804
  }), _void(), {
@@ -52858,7 +53806,7 @@ var dishwasherCapability = {
52858
53806
  auth: "protected"
52859
53807
  }),
52860
53808
  /** What this appliance can do, from its own description — the UI is derived from it. */
52861
- getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
53809
+ getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
52862
53810
  },
52863
53811
  status: {
52864
53812
  schema: DishwasherStatusSchema,
@@ -54282,15 +55230,15 @@ var FanPresetCaptureResultSchema = object({
54282
55230
  items: array(FanCaptureItemSchema),
54283
55231
  omitted: array(FanCaptureOmissionSchema)
54284
55232
  });
54285
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55233
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
54286
55234
  DeviceType.Fan;
54287
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55235
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54288
55236
  kind: "mutation",
54289
55237
  auth: "admin"
54290
- }), method(deviceInput$1.extend({ presetId: string$2().min(1) }), FanPresetApplyResultSchema, {
55238
+ }), method(deviceInput$2.extend({ presetId: string$2().min(1) }), FanPresetApplyResultSchema, {
54291
55239
  kind: "mutation",
54292
55240
  auth: "protected"
54293
- }), method(deviceInput$1.extend({
55241
+ }), method(deviceInput$2.extend({
54294
55242
  fields: array(FanCaptureFieldSchema).optional(),
54295
55243
  steps: array(ActionStepSchema).max(20).default([])
54296
55244
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -55201,13 +56149,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
55201
56149
  */
55202
56150
  var ConfigSchemaPassthrough$3 = custom();
55203
56151
  var IntegrationInstanceIdSchema = string$2().min(1);
56152
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56153
+ /**
56154
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56155
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56156
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56157
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56158
+ * instance id.
56159
+ */
56160
+ var IntegrationInstanceKindSchema = object({
56161
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56162
+ id: string$2().min(1),
56163
+ label: string$2().min(1),
56164
+ /** The kind signs in through a server-held interactive session. */
56165
+ signIn: object({
56166
+ basePath: string$2().min(1),
56167
+ methods: array(_enum(["device-code", "proxy"])).optional()
56168
+ }).optional(),
56169
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56170
+ connectPath: string$2().min(1).optional(),
56171
+ /** Relative hub path to disconnect that account. */
56172
+ disconnectPath: string$2().min(1).optional()
56173
+ });
55204
56174
  var IntegrationInstanceDescriptionSchema = object({
56175
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56176
+ role: _enum([
56177
+ "devices",
56178
+ "services",
56179
+ "both"
56180
+ ]),
56181
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56182
+ discovery: object({
56183
+ mode: _enum([
56184
+ "none",
56185
+ "lan",
56186
+ "account"
56187
+ ]),
56188
+ scopedToInstance: boolean()
56189
+ }).optional(),
55205
56190
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55206
- instanceMode: _enum(["single", "multiple"]),
56191
+ instanceMode: IntegrationInstanceModeSchema,
55207
56192
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55208
56193
  kind: string$2(),
55209
56194
  /** True when `testInstance` really probes the remote. */
55210
- canTest: boolean()
56195
+ canTest: boolean(),
56196
+ /**
56197
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56198
+ * Add flow asks which first and sends it as `createInstance.kind`.
56199
+ */
56200
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56201
+ /**
56202
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56203
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56204
+ */
56205
+ allowRestoreId: boolean().optional(),
56206
+ /**
56207
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56208
+ * send an id (the UI then mints an opaque one).
56209
+ */
56210
+ mintsId: boolean().optional()
55211
56211
  });
55212
56212
  var IntegrationInstanceStatusSchema = object({
55213
56213
  state: _enum([
@@ -55216,16 +56216,28 @@ var IntegrationInstanceStatusSchema = object({
55216
56216
  "error",
55217
56217
  "unknown"
55218
56218
  ]),
55219
- detail: string$2().optional()
56219
+ detail: string$2().optional(),
56220
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56221
+ label: string$2().optional()
55220
56222
  });
55221
56223
  var IntegrationInstanceSchema = object({
55222
56224
  id: IntegrationInstanceIdSchema,
55223
56225
  name: string$2(),
55224
56226
  /** The addon's own rule (an URL is present, a client id is present...). */
55225
56227
  configured: boolean(),
56228
+ /**
56229
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56230
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56231
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56232
+ */
56233
+ enabled: boolean().optional(),
56234
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56235
+ kind: string$2().optional(),
55226
56236
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55227
56237
  formValues: record(string$2(), unknown()),
55228
- status: IntegrationInstanceStatusSchema.optional()
56238
+ status: IntegrationInstanceStatusSchema.optional(),
56239
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56240
+ lastCheckedAt: number().optional()
55229
56241
  });
55230
56242
  var IntegrationInstancePatchSchema = object({
55231
56243
  id: IntegrationInstanceIdSchema,
@@ -55233,25 +56245,57 @@ var IntegrationInstancePatchSchema = object({
55233
56245
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55234
56246
  values: record(string$2(), unknown())
55235
56247
  });
55236
- method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
55237
- id: IntegrationInstanceIdSchema,
55238
- name: string$2().min(1)
55239
- }), object({ id: string$2() }), {
55240
- kind: "mutation",
55241
- auth: "admin"
55242
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55243
- kind: "mutation",
55244
- auth: "admin"
55245
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55246
- kind: "mutation",
55247
- auth: "admin"
55248
- }), optionalMethod(object({
55249
- id: IntegrationInstanceIdSchema.nullable(),
55250
- values: record(string$2(), unknown())
55251
- }), TestConnectionResultSchema$1, {
55252
- kind: "mutation",
55253
- auth: "admin"
55254
- });
56248
+ var integrationInstancesCapability = {
56249
+ name: "integration-instances",
56250
+ scope: "system",
56251
+ mode: "collection",
56252
+ methods: {
56253
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56254
+ /** The form; `id: null` is the create form. */
56255
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56256
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56257
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56258
+ /**
56259
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56260
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56261
+ */
56262
+ createInstance: method(object({
56263
+ id: IntegrationInstanceIdSchema.optional(),
56264
+ name: string$2().min(1),
56265
+ kind: string$2().min(1).optional()
56266
+ }), object({ id: string$2() }), {
56267
+ kind: "mutation",
56268
+ auth: "admin"
56269
+ }),
56270
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56271
+ kind: "mutation",
56272
+ auth: "admin"
56273
+ }),
56274
+ /**
56275
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56276
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56277
+ */
56278
+ setInstanceEnabled: method(object({
56279
+ id: IntegrationInstanceIdSchema,
56280
+ enabled: boolean()
56281
+ }), object({ ok: literal(true) }), {
56282
+ kind: "mutation",
56283
+ auth: "admin"
56284
+ }),
56285
+ /** Delete the record and its keychain refs together. */
56286
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56287
+ kind: "mutation",
56288
+ auth: "admin"
56289
+ }),
56290
+ testInstance: optionalMethod(object({
56291
+ id: IntegrationInstanceIdSchema.nullable(),
56292
+ values: record(string$2(), unknown())
56293
+ }), TestConnectionResultSchema$1, {
56294
+ kind: "mutation",
56295
+ auth: "admin"
56296
+ })
56297
+ }
56298
+ };
55255
56299
  /**
55256
56300
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55257
56301
  * session (server-initiated offer pattern, identical to the main
@@ -57269,6 +58313,11 @@ var LogEntrySchema = object({
57269
58313
  * flight, and not asking for the next slice ends the scan.
57270
58314
  */
57271
58315
  var LogReadQuerySchema = object({
58316
+ /**
58317
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58318
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58319
+ */
58320
+ instanceId: string$2().min(1).max(200).optional(),
57272
58321
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57273
58322
  requestId: string$2().min(1).max(64).optional(),
57274
58323
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -57331,6 +58380,8 @@ var LogReadResultSchema = object({
57331
58380
  /** True when `cancelRead` ended this slice early. */
57332
58381
  cancelled: boolean().optional()
57333
58382
  });
58383
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58384
+ var LogInstanceSelectorSchema = object({ instanceId: string$2().min(1).max(200).optional() });
57334
58385
  var LogReadCapabilitiesSchema = object({
57335
58386
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
57336
58387
  readable: boolean(),
@@ -57349,7 +58400,7 @@ var LogReadCapabilitiesSchema = object({
57349
58400
  /** How far back the store reaches, when the provider knows (days). */
57350
58401
  retentionDays: number().nullable()
57351
58402
  });
57352
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string$2().min(1).max(64) }), object({ cancelled: boolean() }), {
58403
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string$2().min(1).max(64) }), object({ cancelled: boolean() }), {
57353
58404
  kind: "mutation",
57354
58405
  auth: "admin"
57355
58406
  });
@@ -57453,214 +58504,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
57453
58504
  ]);
57454
58505
  method(_void(), array(LoginMethodContributionSchema).readonly());
57455
58506
  /**
57456
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
57457
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
57458
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
57459
- * surface — those use `DeviceType.Camera` + the broker pipeline.
57460
- *
57461
- * The cap declares a wide method surface; not every backend supports
57462
- * every method. Drivers signal sub-capability via the
57463
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
57464
- * corresponding method) so the UI gates buttons without a slice
57465
- * fetch — same pattern as `PanTiltZoom` for cameras.
57466
- */
57467
- var MediaPlayerStateSchema = _enum([
57468
- "off",
57469
- "on",
57470
- "idle",
57471
- "playing",
57472
- "paused",
57473
- "buffering",
57474
- "standby"
57475
- ]);
57476
- var MediaPlayerRepeatSchema = _enum([
57477
- "off",
57478
- "all",
57479
- "one"
57480
- ]);
57481
- var MediaInfoSchema = object({
57482
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
57483
- * `podcast`, …). */
57484
- type: string$2(),
57485
- /** Human-readable title. */
57486
- title: string$2(),
57487
- /** Optional artist / channel / station label. */
57488
- artist: string$2().optional(),
57489
- /** Optional album / season / show name. */
57490
- album: string$2().optional(),
57491
- /** Optional cover-art / thumbnail URL. */
57492
- imageUrl: string$2().optional()
57493
- });
57494
- /**
57495
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
57496
- * the device said so. `assumed`: the driver has no feedback (infrared) and
57497
- * tracks what it last SENT; it moves only after a send was accepted, and a
57498
- * human at the real remote makes it wrong. A UI shows an assumed value as a
57499
- * guess, never as a reading.
57500
- */
57501
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
57502
- /** The fields of a media-player status a driver can only guess. */
57503
- var MediaPlayerAssumedFieldSchema = _enum([
57504
- "state",
57505
- "volumeLevel",
57506
- "isMuted",
57507
- "source"
57508
- ]);
57509
- var MediaPlayerStatusSchema = object({
57510
- /** Playback lifecycle state. */
57511
- state: MediaPlayerStateSchema,
57512
- /**
57513
- * Whether the state fields are readings or the driver's own bookkeeping.
57514
- * A slice written before this field existed parses as `reported`.
57515
- */
57516
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
57517
- /**
57518
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
57519
- * this is not empty; a field not listed here is a reading even then (power
57520
- * from a running-state source, mute guessed from the last send).
57521
- */
57522
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
57523
- /** Volume as 0..100 inclusive. Null when the device has no volume
57524
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
57525
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57526
- /** Mute toggle distinct from volume=0. Null when no mute surface.
57527
- * Pair with `DeviceFeature.MediaPlayerMute`. */
57528
- isMuted: boolean().nullable(),
57529
- /** Active source / input. Empty when no source surface. */
57530
- source: string$2(),
57531
- /** Selectable sources. Empty when no source surface.
57532
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
57533
- availableSources: array(string$2()),
57534
- /** Currently-playing media info. Null when nothing is playing. */
57535
- currentMedia: MediaInfoSchema.nullable(),
57536
- /** Current playback position in ms. Null when not seekable or
57537
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
57538
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57539
- /** Total duration of the current media in ms. Null when unknown
57540
- * (live stream) or nothing is playing. */
57541
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57542
- /** Shuffle toggle. Null when no shuffle surface.
57543
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
57544
- shuffle: boolean().nullable(),
57545
- /** Repeat mode. Null when no repeat surface.
57546
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
57547
- repeat: MediaPlayerRepeatSchema.nullable(),
57548
- /** Ms epoch when the slice was last updated. */
57549
- lastChangedAt: number()
57550
- });
57551
- var mediaPlayerCapability = {
57552
- name: "media-player",
57553
- scope: "device",
57554
- deviceNative: true,
57555
- mode: "singleton",
57556
- deviceTypes: [DeviceType.MediaPlayer],
57557
- methods: {
57558
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57559
- kind: "mutation",
57560
- auth: "protected"
57561
- }),
57562
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57563
- kind: "mutation",
57564
- auth: "protected"
57565
- }),
57566
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57567
- kind: "mutation",
57568
- auth: "protected"
57569
- }),
57570
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57571
- kind: "mutation",
57572
- auth: "protected"
57573
- }),
57574
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57575
- kind: "mutation",
57576
- auth: "protected"
57577
- }),
57578
- seek: method(object({
57579
- deviceId: number().int().nonnegative(),
57580
- position: number().int().nonnegative()
57581
- }), _void(), {
57582
- kind: "mutation",
57583
- auth: "protected"
57584
- }),
57585
- setVolume: method(object({
57586
- deviceId: number().int().nonnegative(),
57587
- volumeLevel: number().min(0).max(100)
57588
- }), _void(), {
57589
- kind: "mutation",
57590
- auth: "protected"
57591
- }),
57592
- setMute: method(object({
57593
- deviceId: number().int().nonnegative(),
57594
- muted: boolean()
57595
- }), _void(), {
57596
- kind: "mutation",
57597
- auth: "protected"
57598
- }),
57599
- setShuffle: method(object({
57600
- deviceId: number().int().nonnegative(),
57601
- shuffle: boolean()
57602
- }), _void(), {
57603
- kind: "mutation",
57604
- auth: "protected"
57605
- }),
57606
- setRepeat: method(object({
57607
- deviceId: number().int().nonnegative(),
57608
- repeat: MediaPlayerRepeatSchema
57609
- }), _void(), {
57610
- kind: "mutation",
57611
- auth: "protected"
57612
- }),
57613
- selectSource: method(object({
57614
- deviceId: number().int().nonnegative(),
57615
- source: string$2().min(1)
57616
- }), _void(), {
57617
- kind: "mutation",
57618
- auth: "protected"
57619
- }),
57620
- playMedia: method(object({
57621
- deviceId: number().int().nonnegative(),
57622
- /** Media identifier / URL. */
57623
- mediaId: string$2().min(1),
57624
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
57625
- * passes it through to the upstream service. */
57626
- mediaType: string$2().min(1)
57627
- }), _void(), {
57628
- kind: "mutation",
57629
- auth: "protected"
57630
- })
57631
- },
57632
- status: {
57633
- schema: MediaPlayerStatusSchema,
57634
- kind: "push"
57635
- },
57636
- /**
57637
- * Runtime-state slice — mirrored by the kernel. UI players read the
57638
- * full slice for live now-playing, volume, and progress updates
57639
- * without polling.
57640
- */
57641
- runtimeState: MediaPlayerStatusSchema,
57642
- /**
57643
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
57644
- *
57645
- * See `RuntimeStateDurability`. Enforced by
57646
- * `scripts/check-runtime-state-durability.ts`.
57647
- */
57648
- durability: "session",
57649
- /**
57650
- * History (D850): volume (a level an operator or an automation sets; the
57651
- * default 10 s floor folds a slider drag into its end value) and mute.
57652
- * `position` is not recorded: it moves continuously while playing.
57653
- */
57654
- history: { fields: {
57655
- volumeLevel: {
57656
- kind: "gauge",
57657
- unit: "%",
57658
- deadband: 1
57659
- },
57660
- isMuted: { kind: "boolean" }
57661
- } }
57662
- };
57663
- /**
57664
58507
  * mesh-network — collection cap for mesh-VPN providers.
57665
58508
  *
57666
58509
  * Different shape from `network-access` (public tunnels):
@@ -66419,6 +67262,21 @@ var NcTemplateVarKindSchema = _enum([
66419
67262
  "sound",
66420
67263
  "system"
66421
67264
  ]);
67265
+ /**
67266
+ * The default title/body a rule of one SITUATION is delivered with when the
67267
+ * operator wrote no template — served so an editor can pre-fill it (and the
67268
+ * operator can rebuild it from placeholders) instead of starting from a blank
67269
+ * box that, once saved, silently replaces it. `id` names the situation inside
67270
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
67271
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
67272
+ * the hub's current language and with its overrides applied.
67273
+ */
67274
+ var NcTemplateDefaultSchema = object({
67275
+ id: _enum(["appliance-cycle"]),
67276
+ kind: NcRuleKindSchema,
67277
+ title: string$2(),
67278
+ body: string$2()
67279
+ });
66422
67280
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
66423
67281
  var NcTemplateFamilySchema = _enum([
66424
67282
  "rule",
@@ -67274,7 +68132,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
67274
68132
  }), method(object({}), object({
67275
68133
  catalog: array(NcConditionDescriptorSchema),
67276
68134
  taxonomy: NcTaxonomySchema.optional()
67277
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
68135
+ })), method(object({}), object({
68136
+ vars: array(NcTemplateVarDescriptorSchema),
68137
+ /** Per-situation default texts; absent on a hub that predates them. */
68138
+ defaults: array(NcTemplateDefaultSchema)
68139
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
67278
68140
  kind: "mutation",
67279
68141
  access: "view"
67280
68142
  }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string$2().min(1) }), object({ url: string$2().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
@@ -68170,7 +69032,7 @@ var petFeederCapability = {
68170
69032
  /**
68171
69033
  * Pet water-fountain cap. Models the telemetry and the control surface of a
68172
69034
  * circulating drinking fountain: how much water is in the tank, how much the
68173
- * pet drank, whether the pump runs and on what duty cycle.
69035
+ * pet drank, whether the pump runs.
68174
69036
  *
68175
69037
  * It has no native vendor provider of its own: a fountain reaches CamStack as
68176
69038
  * a container of loose entities (a pump switch, two number boxes, a "filter
@@ -68186,8 +69048,10 @@ var petFeederCapability = {
68186
69048
  * never here. Every reading may be `null`: a source that is offline or does
68187
69049
  * not report is unknown, never zero.
68188
69050
  *
68189
- * The filter is not a field of this cap: it is an item of the `consumables`
68190
- * cap on the same device (kind `wear`), reset by the same forward mechanism.
69051
+ * The pump's on/off duty cycle is model-specific and lives in the optional
69052
+ * `pump-cycle` cap. The filter, the cleaning and the water change are not
69053
+ * fields of this cap: they are items of the `consumables` cap on the same
69054
+ * device (kind `wear`), reset by the same forward mechanism.
68191
69055
  */
68192
69056
  var PetFountainStatusSchema = object({
68193
69057
  /** Water in the tank, 0..100 (%). Null while the source has not reported. */
@@ -68198,27 +69062,20 @@ var PetFountainStatusSchema = object({
68198
69062
  empty: boolean().nullable(),
68199
69063
  /** True while the pump is running. Null while unknown. */
68200
69064
  pumpRunning: boolean().nullable(),
68201
- /** True when the pump works on an on/off duty cycle rather than continuously. */
68202
- pumpIntervalEnabled: boolean().nullable(),
68203
- /** How long the pump runs per cycle, in seconds. */
68204
- pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
68205
- /** How long the pump rests per cycle, in seconds. */
68206
- pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
68207
69065
  /** Water drunk in the last drink, in litres. */
68208
69066
  lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
68209
69067
  /** Water drunk in the current hour, in litres. */
68210
69068
  currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
68211
69069
  /** Water drunk over the last 24 hours (rolling), in litres. */
68212
69070
  last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
68213
- /** Water drunk since local midnight, in litres. */
69071
+ /** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
68214
69072
  todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
68215
69073
  /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
68216
69074
  error: string$2().nullable(),
68217
69075
  /** Ms epoch when the slice was last refreshed. */
68218
69076
  lastFetchedAt: number()
68219
69077
  });
68220
- var DeviceIdSchema = number().int().nonnegative();
68221
- var MinutesSchema = number().int().min(0);
69078
+ var DeviceIdSchema$1 = number().int().nonnegative();
68222
69079
  var petFountainCapability = {
68223
69080
  name: "pet-fountain",
68224
69081
  scope: "device",
@@ -68228,38 +69085,14 @@ var petFountainCapability = {
68228
69085
  methods: {
68229
69086
  /** Start or stop the pump. */
68230
69087
  setPump: method(object({
68231
- deviceId: DeviceIdSchema,
68232
- on: boolean()
68233
- }), _void(), {
68234
- kind: "mutation",
68235
- auth: "protected"
68236
- }),
68237
- /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
68238
- setPumpInterval: method(object({
68239
- deviceId: DeviceIdSchema,
69088
+ deviceId: DeviceIdSchema$1,
68240
69089
  on: boolean()
68241
69090
  }), _void(), {
68242
69091
  kind: "mutation",
68243
69092
  auth: "protected"
68244
69093
  }),
68245
- /** Set how many minutes the pump runs per cycle. */
68246
- setPumpOnMinutes: method(object({
68247
- deviceId: DeviceIdSchema,
68248
- minutes: MinutesSchema
68249
- }), _void(), {
68250
- kind: "mutation",
68251
- auth: "protected"
68252
- }),
68253
- /** Set how many minutes the pump rests per cycle. */
68254
- setPumpOffMinutes: method(object({
68255
- deviceId: DeviceIdSchema,
68256
- minutes: MinutesSchema
68257
- }), _void(), {
68258
- kind: "mutation",
68259
- auth: "protected"
68260
- }),
68261
69094
  /** Clear the rolling daily consumption counters on the device. */
68262
- resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69095
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
68263
69096
  kind: "mutation",
68264
69097
  auth: "protected"
68265
69098
  })
@@ -69894,6 +70727,14 @@ var powerMeterCapability = {
69894
70727
  deviceNative: true,
69895
70728
  mode: "singleton",
69896
70729
  deviceTypes: POWER_METER_DEVICE_TYPES,
70730
+ deviceConfig: { ui: {
70731
+ kind: "widget",
70732
+ widgetId: "host/power-meter-line",
70733
+ tab: "power",
70734
+ placement: "hero",
70735
+ label: "Power",
70736
+ order: 4
70737
+ } },
69897
70738
  methods: {},
69898
70739
  status: {
69899
70740
  schema: PowerMeterStatusSchema,
@@ -69953,6 +70794,117 @@ var powerMeterCapability = {
69953
70794
  volatileStateFields: ["lastFetchedAt"],
69954
70795
  customize: powerMeterCustomize
69955
70796
  };
70797
+ var PowerSourceRefusalReasonSchema = _enum([
70798
+ "no-power-source",
70799
+ "switch-missing",
70800
+ "switch-offline",
70801
+ "switch-unknown",
70802
+ "restart-in-progress",
70803
+ "switch-call-failed"
70804
+ ]);
70805
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
70806
+ accepted: literal(false),
70807
+ reason: PowerSourceRefusalReasonSchema,
70808
+ /** One line for the operator, naming the switch. */
70809
+ message: string$2()
70810
+ })]);
70811
+ var PowerSourceInfoSchema = object({
70812
+ /** False when the device has no power source linked; the rest is then empty. */
70813
+ linked: boolean(),
70814
+ switchDeviceId: number().nullable(),
70815
+ switchName: string$2().nullable(),
70816
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
70817
+ switchOn: boolean().nullable(),
70818
+ switchOnline: boolean().nullable(),
70819
+ restartInProgress: boolean(),
70820
+ offSeconds: number().int()
70821
+ });
70822
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
70823
+ var PowerSourceStateSchema = object({
70824
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
70825
+ switchOn: boolean().nullable(),
70826
+ offSeconds: number().int(),
70827
+ lastFetchedAt: number()
70828
+ });
70829
+ var POWER_SOURCE_DEVICE_TYPES = [
70830
+ DeviceType.Gateway,
70831
+ DeviceType.Hub,
70832
+ DeviceType.Camera,
70833
+ DeviceType.Sensor,
70834
+ DeviceType.MediaPlayer,
70835
+ DeviceType.Vacuum,
70836
+ DeviceType.Generic,
70837
+ DeviceType.Washer,
70838
+ DeviceType.Dryer,
70839
+ DeviceType.Dishwasher,
70840
+ DeviceType.PetFountain,
70841
+ DeviceType.Fan,
70842
+ DeviceType.Humidifier,
70843
+ DeviceType.Climate,
70844
+ DeviceType.WaterHeater
70845
+ ];
70846
+ var powerSourceCustomize = {
70847
+ version: 1,
70848
+ targets: [{
70849
+ id: "switch",
70850
+ cardinality: "scalar",
70851
+ vocabulary: {
70852
+ cap: "power-source",
70853
+ stateField: "switchOn"
70854
+ },
70855
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70856
+ apply: "add",
70857
+ path: "switchOn",
70858
+ sources: [{
70859
+ kind: "switch",
70860
+ transforms: [{ kind: "identity" }]
70861
+ }],
70862
+ companions: [{
70863
+ kind: "constant",
70864
+ path: "offSeconds",
70865
+ value: 10,
70866
+ when: "add"
70867
+ }, {
70868
+ kind: "clock",
70869
+ path: "lastFetchedAt",
70870
+ when: "always"
70871
+ }],
70872
+ sourceReport: { status: "assignment-status" }
70873
+ }]
70874
+ };
70875
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
70876
+ var powerSourceCapability = {
70877
+ name: "power-source",
70878
+ scope: "device",
70879
+ mode: "singleton",
70880
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70881
+ methods: {
70882
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
70883
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70884
+ kind: "mutation",
70885
+ auth: "admin"
70886
+ }),
70887
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70888
+ kind: "mutation",
70889
+ auth: "admin"
70890
+ }),
70891
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70892
+ kind: "mutation",
70893
+ auth: "admin"
70894
+ })
70895
+ },
70896
+ runtimeState: PowerSourceStateSchema,
70897
+ customize: powerSourceCustomize,
70898
+ /**
70899
+ * Runtime-state durability: **session** — mirrored from the linked switch on
70900
+ * its next report; a restored value would outlive the reading it names.
70901
+ *
70902
+ * See `RuntimeStateDurability`. Enforced by
70903
+ * `scripts/check-runtime-state-durability.ts`.
70904
+ */
70905
+ durability: "session",
70906
+ history: "none"
70907
+ };
69956
70908
  /**
69957
70909
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
69958
70910
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -70592,6 +71544,79 @@ var ptzAutotrackCapability = {
70592
71544
  */
70593
71545
  history: "none"
70594
71546
  };
71547
+ /**
71548
+ * The on/off duty cycle of a pet fountain's pump. Some models run the pump
71549
+ * continuously and have nothing to configure; others can alternate a run time
71550
+ * and a rest time. That is model-specific, so it is its own optional cap on the
71551
+ * fountain device: a fountain without cycle entities simply never composes it
71552
+ * (planning#366), and the `pet-fountain` cap stays the model-independent core.
71553
+ *
71554
+ * Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
71555
+ * cycle from loose entities (a cycle switch and two number boxes) and every
71556
+ * method is a forward to one source entity (D659, D967). Durations are stored
71557
+ * in SECONDS (D944); a number box that cannot be read back leaves them `null`
71558
+ * — unknown, never zero (D393).
71559
+ */
71560
+ var PumpCycleStatusSchema = object({
71561
+ /** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
71562
+ enabled: boolean().nullable(),
71563
+ /** How long the pump runs per cycle, in seconds. Null while unknown. */
71564
+ onTime: number().min(0).nullable().meta(readingMeta("duration")),
71565
+ /** How long the pump rests per cycle, in seconds. Null while unknown. */
71566
+ offTime: number().min(0).nullable().meta(readingMeta("duration")),
71567
+ /** Ms epoch when the slice was last refreshed. */
71568
+ lastFetchedAt: number()
71569
+ });
71570
+ var DeviceIdSchema = number().int().nonnegative();
71571
+ var MinutesSchema = number().int().min(0);
71572
+ var pumpCycleCapability = {
71573
+ name: "pump-cycle",
71574
+ scope: "device",
71575
+ deviceNative: true,
71576
+ mode: "singleton",
71577
+ deviceTypes: [DeviceType.PetFountain],
71578
+ methods: {
71579
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
71580
+ setEnabled: method(object({
71581
+ deviceId: DeviceIdSchema,
71582
+ on: boolean()
71583
+ }), _void(), {
71584
+ kind: "mutation",
71585
+ auth: "protected"
71586
+ }),
71587
+ /** Set how many minutes the pump runs per cycle. */
71588
+ setOnMinutes: method(object({
71589
+ deviceId: DeviceIdSchema,
71590
+ minutes: MinutesSchema
71591
+ }), _void(), {
71592
+ kind: "mutation",
71593
+ auth: "protected"
71594
+ }),
71595
+ /** Set how many minutes the pump rests per cycle. */
71596
+ setOffMinutes: method(object({
71597
+ deviceId: DeviceIdSchema,
71598
+ minutes: MinutesSchema
71599
+ }), _void(), {
71600
+ kind: "mutation",
71601
+ auth: "protected"
71602
+ })
71603
+ },
71604
+ status: {
71605
+ schema: PumpCycleStatusSchema,
71606
+ kind: "push"
71607
+ },
71608
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
71609
+ runtimeState: PumpCycleStatusSchema,
71610
+ /**
71611
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
71612
+ *
71613
+ * See `RuntimeStateDurability`. Enforced by
71614
+ * `scripts/check-runtime-state-durability.ts`.
71615
+ */
71616
+ durability: "session",
71617
+ /** History (D850): a duty-cycle setting is not a series worth recording. */
71618
+ history: "none"
71619
+ };
70595
71620
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
70596
71621
  kind: "mutation",
70597
71622
  auth: "admin"
@@ -73382,7 +74407,13 @@ var RemoteMediaCommandSchema = _enum([
73382
74407
  "next",
73383
74408
  "previous",
73384
74409
  "setMute",
73385
- "selectSource"
74410
+ "selectSource",
74411
+ "turnOn",
74412
+ "turnOff",
74413
+ "volumeUp",
74414
+ "volumeDown",
74415
+ "channelUp",
74416
+ "channelDown"
73386
74417
  ]);
73387
74418
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
73388
74419
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -73403,7 +74434,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
73403
74434
  }), RemoteSendResultSchema, {
73404
74435
  kind: "mutation",
73405
74436
  auth: "protected"
73406
- }), method(deviceInput.extend({
74437
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
73407
74438
  command: RemoteMediaCommandSchema,
73408
74439
  muted: boolean().optional(),
73409
74440
  source: string$2().optional(),
@@ -75649,7 +76680,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
75649
76680
  * `getSettingsUISchema()` surface.
75650
76681
  */
75651
76682
  var SwitchStatusSchema = object({
75652
- on: boolean().meta({ label: "On" }),
76683
+ /**
76684
+ * `null` = the device has never reported its state (a measurement that
76685
+ * has not happened is unknown, never `false`).
76686
+ */
76687
+ on: boolean().nullable().meta({ label: "On" }),
75653
76688
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
75654
76689
  lastChangedAt: number()
75655
76690
  });
@@ -75674,7 +76709,7 @@ var switchCapability = {
75674
76709
  }) },
75675
76710
  events: { onStateChanged: { data: object({
75676
76711
  deviceId: number(),
75677
- on: boolean(),
76712
+ on: boolean().nullable(),
75678
76713
  lastChangedAt: number()
75679
76714
  }) } },
75680
76715
  status: {
@@ -79231,6 +80266,26 @@ function buildDescriptor(capName, kind) {
79231
80266
  * contributes one entry per bound cap present in this map.
79232
80267
  */
79233
80268
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80269
+ /**
80270
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80271
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80272
+ * client resolves its name from `label` (English and Italian side by side) and
80273
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80274
+ * server sends an id, a client maps it to its own icon set.
80275
+ *
80276
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80277
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80278
+ * to `icon`.
80279
+ */
80280
+ function digit(n) {
80281
+ return {
80282
+ label: t(n, n),
80283
+ icon: "hash",
80284
+ legend: n
80285
+ };
80286
+ }
80287
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
80288
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79234
80289
  var btn = (idSuffix, row, col, init) => ({
79235
80290
  idSuffix,
79236
80291
  row,
@@ -79357,6 +80412,34 @@ discriminatedUnion("kind", [object({
79357
80412
  kind: literal("pronto"),
79358
80413
  code: string$2()
79359
80414
  })]);
80415
+ /**
80416
+ * Propose a running threshold from a device's own power history (planning#405).
80417
+ *
80418
+ * Pure and deterministic: the same series gives the same answer. The series is
80419
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80420
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80421
+ * count of samples.
80422
+ *
80423
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80424
+ * near F. A device that is not near one level most of the time has no floor.
80425
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80426
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80427
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80428
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80429
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80430
+ * covers the longest standby blip above T.
80431
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80432
+ * run must hold exactly one true interval and nothing may flap outside.
80433
+ */
80434
+ var SECOND_MS = 1e3;
80435
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80436
+ 24 * (60 * MINUTE_MS$1);
80437
+ 10 * MINUTE_MS$1;
80438
+ 30 * SECOND_MS;
80439
+ 60 * SECOND_MS;
80440
+ 20 * MINUTE_MS$1;
80441
+ 10 * SECOND_MS;
80442
+ 10 * SECOND_MS;
79360
80443
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79361
80444
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79362
80445
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -79717,6 +80800,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79717
80800
  pressureSensor: pressureSensorCapability,
79718
80801
  privacyMask: privacyMaskCapability,
79719
80802
  ptzAutotrack: ptzAutotrackCapability,
80803
+ pumpCycle: pumpCycleCapability,
79720
80804
  recordingOnboard: recordingOnboardCapability,
79721
80805
  recordingSignal: recordingSignalCapability,
79722
80806
  runningState: runningStateCapability,
@@ -81901,6 +82985,27 @@ Object.freeze({
81901
82985
  access: "view",
81902
82986
  role: "viewer"
81903
82987
  },
82988
+ "boardKeySource.getBoardKey": {
82989
+ capName: "board-key-source",
82990
+ capScope: "system",
82991
+ addonId: null,
82992
+ access: "view",
82993
+ role: "admin"
82994
+ },
82995
+ "boardKeySource.listBoards": {
82996
+ capName: "board-key-source",
82997
+ capScope: "system",
82998
+ addonId: null,
82999
+ access: "view",
83000
+ role: "admin"
83001
+ },
83002
+ "boardKeySource.listSources": {
83003
+ capName: "board-key-source",
83004
+ capScope: "system",
83005
+ addonId: null,
83006
+ access: "view",
83007
+ role: "admin"
83008
+ },
81904
83009
  "brightness.getStatus": {
81905
83010
  capName: "brightness",
81906
83011
  capScope: "device",
@@ -82440,6 +83545,13 @@ Object.freeze({
82440
83545
  access: "create",
82441
83546
  role: "admin"
82442
83547
  },
83548
+ "coreBlocks.proposeRunningThreshold": {
83549
+ capName: "core-blocks",
83550
+ capScope: "system",
83551
+ addonId: null,
83552
+ access: "view",
83553
+ role: "admin"
83554
+ },
82443
83555
  "coreBlocks.reportAutomationRun": {
82444
83556
  capName: "core-blocks",
82445
83557
  capScope: "system",
@@ -82951,6 +84063,13 @@ Object.freeze({
82951
84063
  access: "view",
82952
84064
  role: "admin"
82953
84065
  },
84066
+ "deviceExport.listInstances": {
84067
+ capName: "device-export",
84068
+ capScope: "system",
84069
+ addonId: null,
84070
+ access: "view",
84071
+ role: "admin"
84072
+ },
82954
84073
  "deviceExport.listSupportedDeviceKinds": {
82955
84074
  capName: "device-export",
82956
84075
  capScope: "system",
@@ -84582,6 +85701,13 @@ Object.freeze({
84582
85701
  access: "view",
84583
85702
  role: "admin"
84584
85703
  },
85704
+ "integrationInstances.setInstanceEnabled": {
85705
+ capName: "integration-instances",
85706
+ capScope: "system",
85707
+ addonId: null,
85708
+ access: "create",
85709
+ role: "admin"
85710
+ },
84585
85711
  "integrationInstances.testInstance": {
84586
85712
  capName: "integration-instances",
84587
85713
  capScope: "system",
@@ -85380,6 +86506,20 @@ Object.freeze({
85380
86506
  access: "view",
85381
86507
  role: "admin"
85382
86508
  },
86509
+ "mediaPlayer.channelDown": {
86510
+ capName: "media-player",
86511
+ capScope: "device",
86512
+ addonId: null,
86513
+ access: "create",
86514
+ role: "viewer"
86515
+ },
86516
+ "mediaPlayer.channelUp": {
86517
+ capName: "media-player",
86518
+ capScope: "device",
86519
+ addonId: null,
86520
+ access: "create",
86521
+ role: "viewer"
86522
+ },
85383
86523
  "mediaPlayer.getStatus": {
85384
86524
  capName: "media-player",
85385
86525
  capScope: "device",
@@ -85471,6 +86611,34 @@ Object.freeze({
85471
86611
  access: "create",
85472
86612
  role: "viewer"
85473
86613
  },
86614
+ "mediaPlayer.turnOff": {
86615
+ capName: "media-player",
86616
+ capScope: "device",
86617
+ addonId: null,
86618
+ access: "create",
86619
+ role: "viewer"
86620
+ },
86621
+ "mediaPlayer.turnOn": {
86622
+ capName: "media-player",
86623
+ capScope: "device",
86624
+ addonId: null,
86625
+ access: "create",
86626
+ role: "viewer"
86627
+ },
86628
+ "mediaPlayer.volumeDown": {
86629
+ capName: "media-player",
86630
+ capScope: "device",
86631
+ addonId: null,
86632
+ access: "create",
86633
+ role: "viewer"
86634
+ },
86635
+ "mediaPlayer.volumeUp": {
86636
+ capName: "media-player",
86637
+ capScope: "device",
86638
+ addonId: null,
86639
+ access: "create",
86640
+ role: "viewer"
86641
+ },
85474
86642
  "meshNetwork.getStatus": {
85475
86643
  capName: "mesh-network",
85476
86644
  capScope: "system",
@@ -86717,27 +87885,6 @@ Object.freeze({
86717
87885
  access: "create",
86718
87886
  role: "viewer"
86719
87887
  },
86720
- "petFountain.setPumpInterval": {
86721
- capName: "pet-fountain",
86722
- capScope: "device",
86723
- addonId: null,
86724
- access: "create",
86725
- role: "viewer"
86726
- },
86727
- "petFountain.setPumpOffMinutes": {
86728
- capName: "pet-fountain",
86729
- capScope: "device",
86730
- addonId: null,
86731
- access: "create",
86732
- role: "viewer"
86733
- },
86734
- "petFountain.setPumpOnMinutes": {
86735
- capName: "pet-fountain",
86736
- capScope: "device",
86737
- addonId: null,
86738
- access: "create",
86739
- role: "viewer"
86740
- },
86741
87888
  "pipelineAnalytics.addRetrainFrameFromRecording": {
86742
87889
  capName: "pipeline-analytics",
86743
87890
  capScope: "device",
@@ -88516,6 +89663,34 @@ Object.freeze({
88516
89663
  access: "create",
88517
89664
  role: "admin"
88518
89665
  },
89666
+ "pumpCycle.getStatus": {
89667
+ capName: "pump-cycle",
89668
+ capScope: "device",
89669
+ addonId: null,
89670
+ access: "view",
89671
+ role: "viewer"
89672
+ },
89673
+ "pumpCycle.setEnabled": {
89674
+ capName: "pump-cycle",
89675
+ capScope: "device",
89676
+ addonId: null,
89677
+ access: "create",
89678
+ role: "viewer"
89679
+ },
89680
+ "pumpCycle.setOffMinutes": {
89681
+ capName: "pump-cycle",
89682
+ capScope: "device",
89683
+ addonId: null,
89684
+ access: "create",
89685
+ role: "viewer"
89686
+ },
89687
+ "pumpCycle.setOnMinutes": {
89688
+ capName: "pump-cycle",
89689
+ capScope: "device",
89690
+ addonId: null,
89691
+ access: "create",
89692
+ role: "viewer"
89693
+ },
88519
89694
  "reboot.reboot": {
88520
89695
  capName: "reboot",
88521
89696
  capScope: "device",
@@ -89020,6 +90195,13 @@ Object.freeze({
89020
90195
  access: "create",
89021
90196
  role: "admin"
89022
90197
  },
90198
+ "remote.getAssumedStatus": {
90199
+ capName: "remote",
90200
+ capScope: "system",
90201
+ addonId: null,
90202
+ access: "view",
90203
+ role: "admin"
90204
+ },
89023
90205
  "remote.getConfig": {
89024
90206
  capName: "remote",
89025
90207
  capScope: "system",
@@ -91835,6 +93017,11 @@ Object.freeze({
91835
93017
  form: "single",
91836
93018
  optional: false
91837
93019
  }],
93020
+ "coreBlocks.proposeRunningThreshold": [{
93021
+ name: "deviceId",
93022
+ form: "single",
93023
+ optional: false
93024
+ }],
91838
93025
  "cover.close": [{
91839
93026
  name: "deviceId",
91840
93027
  form: "single",
@@ -92686,6 +93873,16 @@ Object.freeze({
92686
93873
  form: "single",
92687
93874
  optional: true
92688
93875
  }],
93876
+ "mediaPlayer.channelDown": [{
93877
+ name: "deviceId",
93878
+ form: "single",
93879
+ optional: false
93880
+ }],
93881
+ "mediaPlayer.channelUp": [{
93882
+ name: "deviceId",
93883
+ form: "single",
93884
+ optional: false
93885
+ }],
92689
93886
  "mediaPlayer.getStatus": [{
92690
93887
  name: "deviceId",
92691
93888
  form: "single",
@@ -92751,6 +93948,26 @@ Object.freeze({
92751
93948
  form: "single",
92752
93949
  optional: false
92753
93950
  }],
93951
+ "mediaPlayer.turnOff": [{
93952
+ name: "deviceId",
93953
+ form: "single",
93954
+ optional: false
93955
+ }],
93956
+ "mediaPlayer.turnOn": [{
93957
+ name: "deviceId",
93958
+ form: "single",
93959
+ optional: false
93960
+ }],
93961
+ "mediaPlayer.volumeDown": [{
93962
+ name: "deviceId",
93963
+ form: "single",
93964
+ optional: false
93965
+ }],
93966
+ "mediaPlayer.volumeUp": [{
93967
+ name: "deviceId",
93968
+ form: "single",
93969
+ optional: false
93970
+ }],
92754
93971
  "meter.getStatus": [{
92755
93972
  name: "deviceId",
92756
93973
  form: "single",
@@ -93060,21 +94277,6 @@ Object.freeze({
93060
94277
  form: "single",
93061
94278
  optional: false
93062
94279
  }],
93063
- "petFountain.setPumpInterval": [{
93064
- name: "deviceId",
93065
- form: "single",
93066
- optional: false
93067
- }],
93068
- "petFountain.setPumpOffMinutes": [{
93069
- name: "deviceId",
93070
- form: "single",
93071
- optional: false
93072
- }],
93073
- "petFountain.setPumpOnMinutes": [{
93074
- name: "deviceId",
93075
- form: "single",
93076
- optional: false
93077
- }],
93078
94280
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
93079
94281
  name: "deviceId",
93080
94282
  form: "single",
@@ -93710,6 +94912,26 @@ Object.freeze({
93710
94912
  form: "single",
93711
94913
  optional: false
93712
94914
  }],
94915
+ "pumpCycle.getStatus": [{
94916
+ name: "deviceId",
94917
+ form: "single",
94918
+ optional: false
94919
+ }],
94920
+ "pumpCycle.setEnabled": [{
94921
+ name: "deviceId",
94922
+ form: "single",
94923
+ optional: false
94924
+ }],
94925
+ "pumpCycle.setOffMinutes": [{
94926
+ name: "deviceId",
94927
+ form: "single",
94928
+ optional: false
94929
+ }],
94930
+ "pumpCycle.setOnMinutes": [{
94931
+ name: "deviceId",
94932
+ form: "single",
94933
+ optional: false
94934
+ }],
93713
94935
  "reboot.reboot": [{
93714
94936
  name: "deviceId",
93715
94937
  form: "single",
@@ -93911,6 +95133,11 @@ Object.freeze({
93911
95133
  form: "single",
93912
95134
  optional: true
93913
95135
  }],
95136
+ "remote.getAssumedStatus": [{
95137
+ name: "deviceId",
95138
+ form: "single",
95139
+ optional: false
95140
+ }],
93914
95141
  "remote.getConfig": [{
93915
95142
  name: "deviceId",
93916
95143
  form: "single",
@@ -94458,99 +95685,6 @@ Object.freeze({
94458
95685
  "network-access": "ingress",
94459
95686
  "smtp-provider": "email"
94460
95687
  });
94461
- /**
94462
- * A saved Customize assignment (D936). The schema is the type: callers use
94463
- * `z.infer`, they do not restate the shape.
94464
- */
94465
- var OverrideSourceSchema = object({
94466
- addonId: string$2().min(1),
94467
- stableId: string$2().min(1),
94468
- cap: string$2().min(1),
94469
- fieldPath: string$2().min(1)
94470
- });
94471
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
94472
- object({ kind: literal("identity") }),
94473
- object({ kind: literal("invert") }),
94474
- object({
94475
- kind: literal("threshold"),
94476
- comparison: _enum(["gte", "lte"]),
94477
- threshold: number(),
94478
- startDwellMs: number().nonnegative(),
94479
- stopDwellMs: number().nonnegative()
94480
- }),
94481
- object({
94482
- kind: literal("linear"),
94483
- scale: number(),
94484
- offset: number(),
94485
- clampMin: number().optional(),
94486
- clampMax: number().optional()
94487
- }),
94488
- object({
94489
- kind: literal("enum-map"),
94490
- mapping: record(string$2(), string$2().nullable())
94491
- }),
94492
- object({ kind: literal("number") })
94493
- ]);
94494
- var ScalarOverrideDraftSchema = object({
94495
- kind: literal("scalar"),
94496
- cap: string$2().min(1),
94497
- targetId: string$2().min(1),
94498
- source: OverrideSourceSchema,
94499
- transform: CustomizeTransformDraftSchema
94500
- });
94501
- var KeyedArrayOverrideDraftSchema = object({
94502
- kind: literal("keyedArray"),
94503
- cap: string$2().min(1),
94504
- targetId: string$2().min(1),
94505
- key: string$2().min(1),
94506
- label: string$2().min(1),
94507
- fieldPath: string$2().min(1),
94508
- source: OverrideSourceSchema,
94509
- transform: CustomizeTransformDraftSchema
94510
- });
94511
- var PulseOverrideDraftSchema = object({
94512
- kind: literal("pulse"),
94513
- cap: string$2().min(1),
94514
- targetId: string$2().min(1),
94515
- enabled: boolean(),
94516
- debounceMs: number().nonnegative(),
94517
- edges: array(_enum([
94518
- "rising",
94519
- "button",
94520
- "native-event"
94521
- ])).min(1).optional(),
94522
- sources: array(OverrideSourceSchema).min(1)
94523
- });
94524
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
94525
- "in-addition",
94526
- "confirm",
94527
- "replace"
94528
- ]).optional() });
94529
- discriminatedUnion("kind", [
94530
- ScalarOverrideDraftSchema,
94531
- KeyedArrayOverrideDraftSchema,
94532
- PulseOverrideDraftSchema,
94533
- object({
94534
- kind: literal("span"),
94535
- cap: string$2().min(1),
94536
- targetId: string$2().min(1),
94537
- timeoutMs: number().nonnegative(),
94538
- cooldownMs: number().nonnegative().optional(),
94539
- sources: array(SpanSourceDraftSchema)
94540
- })
94541
- ]);
94542
- object({
94543
- targetId: string$2().min(1),
94544
- configured: boolean(),
94545
- source: _enum(["native", "assigned"]),
94546
- state: _enum([
94547
- "active",
94548
- "stale",
94549
- "released",
94550
- "unavailable"
94551
- ]),
94552
- reason: string$2().nullable()
94553
- });
94554
95688
  var G711_SCALE_CORRECTION_DB = {
94555
95689
  PCMU: 20 * Math.log10(4),
94556
95690
  PCMA: 20 * Math.log10(8)
@@ -95166,6 +96300,7 @@ var STATE_SUMMARY_VARS = [
95166
96300
  [...RULE_TEXT],
95167
96301
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95168
96302
  ([...RULE_TEXT], [...COMBINED_ALARM]),
96303
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
95169
96304
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95170
96305
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
95171
96306
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -206243,13 +207378,16 @@ var MatterBrokerRegistry = class {
206243
207378
  }
206244
207379
  async restore(entries) {
206245
207380
  for (const entry of entries) this.#seedCounter(entry.id);
206246
- for (const entry of entries) try {
206247
- await this.#startManager(entry);
206248
- } catch (err) {
206249
- this.#logger.warn("MatterBrokerRegistry: failed to restore manager", {
206250
- tags: { brokerId: entry.id },
206251
- meta: { error: errMsg(err) }
206252
- });
207381
+ for (const entry of entries) {
207382
+ if (entry.enabled === false) continue;
207383
+ try {
207384
+ await this.#startManager(entry);
207385
+ } catch (err) {
207386
+ this.#logger.warn("MatterBrokerRegistry: failed to restore manager", {
207387
+ tags: { brokerId: entry.id },
207388
+ meta: { error: errMsg(err) }
207389
+ });
207390
+ }
206253
207391
  }
206254
207392
  }
206255
207393
  async shutdown() {
@@ -206280,7 +207418,8 @@ var MatterBrokerRegistry = class {
206280
207418
  this.#onAttributeReport = cb;
206281
207419
  }
206282
207420
  async createEntry(name, connection, opts = {}) {
206283
- const id = this.#allocateId();
207421
+ const id = opts.id ?? this.#allocateId();
207422
+ if (opts.id !== void 0) this.#seedCounter(opts.id);
206284
207423
  const entry = {
206285
207424
  id,
206286
207425
  name,
@@ -206293,7 +207432,7 @@ var MatterBrokerRegistry = class {
206293
207432
  }
206294
207433
  async removeEntry(id) {
206295
207434
  const mgr = this.#managers.get(id);
206296
- if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${id}"`);
207435
+ if (!mgr) return;
206297
207436
  for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
206298
207437
  this.#managers.delete(id);
206299
207438
  try {
@@ -206375,6 +207514,10 @@ var MatterBrokerRegistry = class {
206375
207514
  if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
206376
207515
  await mgr.start();
206377
207516
  }
207517
+ /** The next free broker id, for a record minted before its manager exists. */
207518
+ allocateId() {
207519
+ return this.#allocateId();
207520
+ }
206378
207521
  #allocateId() {
206379
207522
  const id = `matter_${String(this.#nextId).padStart(3, "0")}`;
206380
207523
  this.#nextId++;
@@ -206413,10 +207556,12 @@ var MatterBrokerEntrySchema = object({
206413
207556
  id: string$2().min(1),
206414
207557
  name: string$2().min(1),
206415
207558
  connection: matterConnectionSchema,
206416
- integrationId: string$2().optional()
207559
+ integrationId: string$2().optional(),
207560
+ /** Absent means on; the instances page switches a fabric off without deleting it. */
207561
+ enabled: boolean().optional()
206417
207562
  });
206418
- /** Top-level addon config — an ordered list of broker entries (default empty). */
206419
- var matterAddonConfigSchema = object({ brokers: array(MatterBrokerEntrySchema).default([]) });
207563
+ /** Top-level addon config — the fabrics are records, so nothing is left here. */
207564
+ var matterAddonConfigSchema = object({});
206420
207565
  /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
206421
207566
  function toRecord(value) {
206422
207567
  if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
@@ -207498,19 +208643,146 @@ var MatterNodeDevice = class extends BaseDevice {
207498
208643
  var matterBrokersEntryKind = defineEntryKind("brokers", {
207499
208644
  title: "Matter brokers",
207500
208645
  schema: entryBody({
207501
- connection: MatterBrokerEntrySchema.shape.connection,
207502
- integrationId: string$2().optional()
208646
+ ...matterConnectionSchema.shape,
208647
+ integrationId: string$2().optional(),
208648
+ enabled: boolean().optional()
207503
208649
  }),
207504
- writeTrigger: "an operator adds, edits or removes a Matter fabric broker"
208650
+ writeTrigger: "an operator adds, edits or removes a Matter fabric broker",
208651
+ retiredSecretPaths: ["connection.threadDataset", "connection.wifiPassword"]
207505
208652
  });
207506
208653
  //#endregion
208654
+ //#region src/matter-instances.ts
208655
+ /**
208656
+ * The Matter fabrics as `integration-instances` (planning#408 P3): one instance per `brokers`
208657
+ * record, over the SAME records and ids (`matter_NNN`) the `broker` cap serves. The body is flat.
208658
+ * A fabric needs no sign-in; commissioning a device stays on the adoption flow, behind the fabric.
208659
+ */
208660
+ var MATTER_INSTANCE_KIND = "matter";
208661
+ function statusOf(info) {
208662
+ if (info === null) return {
208663
+ state: "unknown",
208664
+ label: "Not started"
208665
+ };
208666
+ if (info.status === "connected") return {
208667
+ state: "ok",
208668
+ label: "Running"
208669
+ };
208670
+ if (info.status === "connecting") return {
208671
+ state: "unknown",
208672
+ label: "Starting"
208673
+ };
208674
+ return {
208675
+ state: "error",
208676
+ label: info.status,
208677
+ ...info.error !== null ? { detail: info.error } : {}
208678
+ };
208679
+ }
208680
+ function buildMatterInstances(records, deps) {
208681
+ return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208682
+ description: {
208683
+ role: "both",
208684
+ instanceMode: "multiple",
208685
+ kind: "connection",
208686
+ canTest: true,
208687
+ kinds: [{
208688
+ id: MATTER_INSTANCE_KIND,
208689
+ label: "Matter"
208690
+ }],
208691
+ allowRestoreId: true
208692
+ },
208693
+ formSchema: () => buildConnectionFormSchema(),
208694
+ initialBody: (name) => ({
208695
+ name,
208696
+ threadDataset: "",
208697
+ wifiSsid: "",
208698
+ wifiPassword: "",
208699
+ reconcileIntervalMs: 3e5
208700
+ }),
208701
+ mintId: () => deps.mintId(),
208702
+ kindOf: () => MATTER_INSTANCE_KIND,
208703
+ health: (_body, id) => {
208704
+ const info = deps.infoOf(id);
208705
+ return {
208706
+ status: statusOf(info),
208707
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
208708
+ };
208709
+ },
208710
+ configured: () => true,
208711
+ onChanged: deps.onChanged,
208712
+ test: async ({ id }) => {
208713
+ const info = id === null ? null : deps.infoOf(id);
208714
+ if (info === null) return {
208715
+ success: false,
208716
+ status: "inconclusive",
208717
+ error: "Not started: save the fabric first"
208718
+ };
208719
+ return info.status === "connected" ? {
208720
+ success: true,
208721
+ status: "validated"
208722
+ } : {
208723
+ success: false,
208724
+ status: "rejected",
208725
+ error: info.error ?? `status: ${info.status}`
208726
+ };
208727
+ }
208728
+ });
208729
+ }
208730
+ //#endregion
208731
+ //#region src/migrations/flatten-broker-records.ts
208732
+ /**
208733
+ * The Matter broker records keep their fabric settings flat (planning#408 P3): the nested
208734
+ * `connection` object (`threadDataset`, `wifiSsid`, `wifiPassword`, `reconcileIntervalMs`) becomes
208735
+ * top-level fields under the SAME record id (`matter_NNN`), so the devices that carry the id keep
208736
+ * their fabric. The work is `flattenNestedRecords`: every secret is read before anything is
208737
+ * written, read back from its new ref before the nested object goes, and the old refs are
208738
+ * deleted last.
208739
+ */
208740
+ var FLATTEN_BROKER_RECORDS_ID = "matter-broker/2026-11-04-flatten-broker-records";
208741
+ async function flattenBrokerRecords(ports) {
208742
+ return flattenNestedRecords(ports, {
208743
+ legacyKey: "connection",
208744
+ flatSecretKeys: ["threadDataset", "wifiPassword"],
208745
+ toFlat: (legacy) => Object.fromEntries([
208746
+ "threadDataset",
208747
+ "wifiSsid",
208748
+ "wifiPassword",
208749
+ "reconcileIntervalMs"
208750
+ ].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
208751
+ });
208752
+ }
208753
+ //#endregion
208754
+ //#region src/migrations/index.ts
208755
+ /** The `matter-broker` catalogue (D987): offered to the engine when the addon initialises. */
208756
+ var ADDON_MIGRATIONS = [{
208757
+ meta: {
208758
+ id: FLATTEN_BROKER_RECORDS_ID,
208759
+ owner: "matter-broker",
208760
+ level: "addon-state",
208761
+ seq: 1,
208762
+ dependsOn: [],
208763
+ store: "settings",
208764
+ scope: "cluster",
208765
+ onFailure: "block-owner",
208766
+ bodyKind: "procedure",
208767
+ description: "Move each Matter fabric from a nested connection object to flat top-level fields under the same id, reading every secret back first",
208768
+ fixtureMark: "the integration link and the Wi-Fi SSID of each fabric"
208769
+ },
208770
+ procedure: flattenBrokerRecords
208771
+ }];
208772
+ //#endregion
207507
208773
  //#region src/matter-broker-store.ts
207508
208774
  function rowToEntry(id, data) {
207509
208775
  const parsed = MatterBrokerEntrySchema.safeParse({
207510
208776
  id,
207511
208777
  name: data.name,
207512
- connection: data.connection,
207513
- integrationId: data.integrationId
208778
+ connection: {
208779
+ threadDataset: data.threadDataset,
208780
+ wifiSsid: data.wifiSsid,
208781
+ wifiPassword: data.wifiPassword,
208782
+ reconcileIntervalMs: data.reconcileIntervalMs
208783
+ },
208784
+ integrationId: data.integrationId,
208785
+ enabled: data.enabled
207514
208786
  });
207515
208787
  return parsed.success ? parsed.data : null;
207516
208788
  }
@@ -207538,16 +208810,20 @@ var MatterBrokerStore = class MatterBrokerStore {
207538
208810
  }
207539
208811
  return out;
207540
208812
  }
208813
+ async getById(id) {
208814
+ return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
208815
+ }
207541
208816
  async replaceAll(entries) {
207542
208817
  const page = await this.kind.query({ limit: 1e3 });
207543
208818
  const nextIds = new Set(entries.map((e) => e.id));
207544
208819
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
207545
208820
  for (const entry of entries) {
207546
- const { id, name, connection, integrationId } = entry;
208821
+ const { id, name, connection, integrationId, enabled } = entry;
207547
208822
  const body = {
207548
208823
  name,
207549
- connection,
207550
- ...integrationId === void 0 ? {} : { integrationId }
208824
+ ...connection,
208825
+ ...integrationId === void 0 ? {} : { integrationId },
208826
+ ...enabled === void 0 ? {} : { enabled }
207551
208827
  };
207552
208828
  const parsed = matterBrokersEntryKind.schema.safeParse(body);
207553
208829
  if (!parsed.success) throw new Error(`invalid Matter broker '${id}': ${parsed.error.message}`);
@@ -207561,7 +208837,7 @@ var MatterBrokerStore = class MatterBrokerStore {
207561
208837
  //#endregion
207562
208838
  //#region src/addon.ts
207563
208839
  /** Default config — a fresh install starts with no fabric. */
207564
- var DEFAULTS = { brokers: [] };
208840
+ var DEFAULTS = {};
207565
208841
  /**
207566
208842
  * Matter broker addon (hub-only). Owns a Matter fabric per broker entry via an
207567
208843
  * embedded matter.js controller (behind the {@link matterCommands} /
@@ -207583,6 +208859,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207583
208859
  deviceClasses = { [DeviceType.Container]: MatterNodeDevice };
207584
208860
  registry = null;
207585
208861
  brokerStore = null;
208862
+ instances = null;
207586
208863
  activeBrokers = [];
207587
208864
  constructor() {
207588
208865
  super({ ...DEFAULTS });
@@ -207593,6 +208870,13 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207593
208870
  if (records === void 0) throw new Error("matter-broker: ctx.records is required (hub-only addon)");
207594
208871
  await records.declare({ brokers: matterBrokersEntryKind });
207595
208872
  this.brokerStore = MatterBrokerStore.of(records);
208873
+ this.instances = buildMatterInstances(records, {
208874
+ mintId: () => this.requireRegistry().allocateId(),
208875
+ infoOf: (id) => this.registry?.get(id) ?? null,
208876
+ onChanged: (id) => this.syncBrokerRuntime(id)
208877
+ });
208878
+ if (this.ctx.secrets === void 0) throw new Error("matter-broker: ctx.secrets is required");
208879
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, flattenPortsOver(records.kind(matterBrokersEntryKind))));
207596
208880
  this.activeBrokers = await this.loadPersistedBrokers();
207597
208881
  this.registry = new MatterBrokerRegistry(this.ctx.logger, {
207598
208882
  dataDir: this.ctx.dataDir,
@@ -207621,6 +208905,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207621
208905
  capability: brokerCapability,
207622
208906
  provider: this.buildBrokerProvider()
207623
208907
  },
208908
+ {
208909
+ capability: integrationInstancesCapability,
208910
+ provider: this.requireInstances().toProvider()
208911
+ },
207624
208912
  {
207625
208913
  capability: deviceAdoptionCapability,
207626
208914
  provider: this.buildAdoptionProvider()
@@ -207628,21 +208916,50 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207628
208916
  ];
207629
208917
  }
207630
208918
  async loadPersistedBrokers() {
207631
- const store = this.brokerStore;
207632
- if (store !== null) {
207633
- const fromRecords = await store.listResolved();
207634
- if (fromRecords.length > 0) return fromRecords;
208919
+ return this.requireBrokerStore().listResolved();
208920
+ }
208921
+ requireBrokerStore() {
208922
+ if (this.brokerStore === null) throw new Error("matter-broker: broker store not ready");
208923
+ return this.brokerStore;
208924
+ }
208925
+ requireInstances() {
208926
+ if (this.instances === null) throw new Error("matter-broker: instances not ready");
208927
+ return this.instances;
208928
+ }
208929
+ /**
208930
+ * Bring the runtime in line with one record after the instances page changed it: a removed
208931
+ * record stops its controller and cascades its devices, an off record only stops the
208932
+ * controller, a live one re-applies its settings or starts under the record's own id.
208933
+ */
208934
+ async syncBrokerRuntime(id) {
208935
+ const reg = this.requireRegistry();
208936
+ const entry = await this.requireBrokerStore().getById(id);
208937
+ this.activeBrokers = await this.loadPersistedBrokers();
208938
+ if (entry === null) {
208939
+ await reg.removeEntry(id);
208940
+ await this.cascadeRemoveDevicesForBroker(id);
208941
+ return;
208942
+ }
208943
+ if (entry.enabled === false) {
208944
+ await reg.removeEntry(id);
208945
+ return;
208946
+ }
208947
+ try {
208948
+ if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
208949
+ else await reg.createEntry(entry.name, entry.connection, {
208950
+ id,
208951
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
208952
+ });
208953
+ } catch (err) {
208954
+ this.ctx.logger.warn("matter instance: could not start the fabric", { meta: {
208955
+ brokerId: id,
208956
+ error: errMsg(err)
208957
+ } });
207635
208958
  }
207636
- return this.config.brokers;
207637
208959
  }
207638
208960
  async persistBrokerEntries(entries) {
207639
208961
  this.activeBrokers = [...entries];
207640
- const store = this.brokerStore;
207641
- if (store !== null) {
207642
- await store.replaceAll(entries);
207643
- return;
207644
- }
207645
- await this.updateGlobalSettings({ brokers: [...entries] });
208962
+ await this.requireBrokerStore().replaceAll(entries);
207646
208963
  }
207647
208964
  async onConfigChanged() {
207648
208965
  const reg = this.registry;
@@ -207660,6 +208977,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207660
208977
  } });
207661
208978
  }
207662
208979
  for (const entry of persisted) {
208980
+ if (entry.enabled === false) continue;
207663
208981
  if (entry.id && liveIds.has(entry.id)) {
207664
208982
  if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
207665
208983
  try {