@camstack/addon-matter-broker 0.2.154 → 0.2.155

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.mjs CHANGED
@@ -797,7 +797,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
797
797
  return EventCategory;
798
798
  }({});
799
799
  //#endregion
800
- //#region ../types/dist/device-type-CfHw_quR.mjs
800
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
801
801
  /**
802
802
  import { errMsg } from '@camstack/types'
803
803
  * Extract a human-readable message from an unknown error value.
@@ -1028,6 +1028,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1028
1028
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1029
1029
  /** Media player exposes stop distinct from pause. */
1030
1030
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1031
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1032
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1033
+ /** Media player exposes channel up / down in steps. */
1034
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1035
+ /** Media player exposes power on / off. */
1036
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1031
1037
  /** Alarm panel requires a PIN code on arm/disarm. */
1032
1038
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1033
1039
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -5987,7 +5993,7 @@ var ZodIssueCode = {
5987
5993
  var ZodFirstPartyTypeKind;
5988
5994
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5989
5995
  //#endregion
5990
- //#region ../types/dist/sleep-QsoHs66J.mjs
5996
+ //#region ../types/dist/sleep-BkzQdpzF.mjs
5991
5997
  /**
5992
5998
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5993
5999
  * window to float samples (D455).
@@ -6066,6 +6072,22 @@ function buildAlawTable() {
6066
6072
  }
6067
6073
  buildUlawTable();
6068
6074
  buildAlawTable();
6075
+ /**
6076
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
6077
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
6078
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
6079
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
6080
+ */
6081
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
6082
+ var AddonMigrationsBlockedError = class extends Error {
6083
+ /** The ledger ids that did not apply and block the owner. */
6084
+ migrationIds;
6085
+ constructor(migrationIds) {
6086
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
6087
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
6088
+ this.migrationIds = migrationIds;
6089
+ }
6090
+ };
6069
6091
  /** Is this field's stored value a credential? */
6070
6092
  function isSecretConfigField(field) {
6071
6093
  if (field.type === "password") return true;
@@ -6119,9 +6141,16 @@ function isSecretMarker(value) {
6119
6141
  const ref = value["$secret"];
6120
6142
  return typeof ref === "string" && ref.length > 0;
6121
6143
  }
6144
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6145
+ function isSecretMarkerLike(value) {
6146
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6147
+ }
6122
6148
  function isSecretClear(value) {
6123
6149
  return isRecord$7(value) && value["$secretClear"] === true;
6124
6150
  }
6151
+ function isSecretFieldView(value) {
6152
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6153
+ }
6125
6154
  /**
6126
6155
  * The UI view of a stored secret field value: a marker becomes "set", inline
6127
6156
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6503,6 +6532,53 @@ function emitReadiness(bus, params) {
6503
6532
  ts
6504
6533
  }));
6505
6534
  }
6535
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6536
+ var AddonMigrationDefinitionError = class extends Error {
6537
+ id;
6538
+ constructor(id, message) {
6539
+ super(`${id}: ${message}`);
6540
+ this.id = id;
6541
+ this.name = "AddonMigrationDefinitionError";
6542
+ }
6543
+ };
6544
+ /**
6545
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6546
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6547
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6548
+ */
6549
+ function defineAddonMigrations(catalogue, ports) {
6550
+ return catalogue.map((entry) => {
6551
+ const { meta } = entry;
6552
+ if ("plan" in entry) {
6553
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6554
+ return {
6555
+ meta,
6556
+ execute: () => guarded(async () => ({
6557
+ kind: "plan",
6558
+ plan: await entry.plan(ports)
6559
+ }))
6560
+ };
6561
+ }
6562
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6563
+ return {
6564
+ meta,
6565
+ execute: () => guarded(async () => ({
6566
+ kind: "procedure",
6567
+ result: await entry.procedure(ports)
6568
+ }))
6569
+ };
6570
+ });
6571
+ }
6572
+ async function guarded(work) {
6573
+ try {
6574
+ return await work();
6575
+ } catch (err) {
6576
+ return {
6577
+ kind: "failed",
6578
+ error: errMsg(err)
6579
+ };
6580
+ }
6581
+ }
6506
6582
  var NOT_RUN = {
6507
6583
  declared: false,
6508
6584
  snapshot: null,
@@ -6724,11 +6800,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6724
6800
  }
6725
6801
  return out;
6726
6802
  }
6803
+ /**
6804
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6805
+ *
6806
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6807
+ * scope (global, one node, one device, one integration, one row of a business
6808
+ * collection) and the field path inside that scope — so saving the same field
6809
+ * twice updates the same ref, and the ref itself says which settings blob
6810
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6811
+ * copied onto another ref does not decrypt.
6812
+ *
6813
+ * Array items are addressed by ITEM ID, never by index
6814
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6815
+ *
6816
+ * Pure and browser-safe: no node builtins, no I/O.
6817
+ */
6818
+ /** The URI scheme of every keychain ref. */
6819
+ var SECRET_REF_SCHEME = "secret://";
6727
6820
  var GLOBAL_SCOPE = { kind: "global" };
6728
6821
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6729
6822
  function globalSecretScope() {
6730
6823
  return GLOBAL_SCOPE;
6731
6824
  }
6825
+ /**
6826
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6827
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6828
+ * addon running forked.
6829
+ */
6830
+ function normalizeSecretOwner(addonId) {
6831
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6832
+ }
6732
6833
  function encodeSegment(value) {
6733
6834
  return encodeURIComponent(value);
6734
6835
  }
@@ -6739,6 +6840,16 @@ function decodeSegment(value) {
6739
6840
  return null;
6740
6841
  }
6741
6842
  }
6843
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6844
+ function formatSecretScope(scope) {
6845
+ switch (scope.kind) {
6846
+ case "global": return "global";
6847
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6848
+ case "device": return `device/${scope.deviceId}`;
6849
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6850
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6851
+ }
6852
+ }
6742
6853
  /**
6743
6854
  * The field path of a value inside an array item, addressed by the item's id:
6744
6855
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6747,6 +6858,13 @@ function decodeSegment(value) {
6747
6858
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6748
6859
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6749
6860
  }
6861
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6862
+ function buildSecretRef(parts) {
6863
+ const owner = normalizeSecretOwner(parts.owner);
6864
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6865
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6866
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6867
+ }
6750
6868
  function parseScope(segments) {
6751
6869
  switch (segments[0]) {
6752
6870
  case "global": return {
@@ -7672,7 +7790,7 @@ var BaseAddon = class {
7672
7790
  startMigration: (input) => api.startMigration.mutate(input),
7673
7791
  finishMigration: (input) => api.finishMigration.mutate(input)
7674
7792
  }, migrations, this.ctx.logger);
7675
- 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`);
7793
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7676
7794
  return report;
7677
7795
  }
7678
7796
  /**
@@ -8012,6 +8130,10 @@ var BaseAddon = class {
8012
8130
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8013
8131
  const patch = {};
8014
8132
  for (const key of keys) {
8133
+ if (tree.get(key)?.kind === "array") {
8134
+ patch[key] = [];
8135
+ continue;
8136
+ }
8015
8137
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8016
8138
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8017
8139
  }
@@ -8700,6 +8822,8 @@ var RECORD_BATCH_MAX = 1e3;
8700
8822
  var RECORD_QUERY_MAX = 1e3;
8701
8823
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8702
8824
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8825
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8826
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8703
8827
  /**
8704
8828
  * Secret fields of a record, addressed the way a Zod schema declares them
8705
8829
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
@@ -8736,6 +8860,112 @@ function secretArrayPrefixes(path) {
8736
8860
  }
8737
8861
  return prefixes;
8738
8862
  }
8863
+ function isRecord$4$1(value) {
8864
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8865
+ }
8866
+ function elementId(element, idKey, declared, prefix) {
8867
+ const id = isRecord$4$1(element) ? element[idKey] : void 0;
8868
+ 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`);
8869
+ return id;
8870
+ }
8871
+ /**
8872
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8873
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8874
+ * result is a new body, `data` is untouched.
8875
+ */
8876
+ var REMOVE_LEAF = Symbol("remove-leaf");
8877
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8878
+ if (!isRecord$4$1(node)) return node;
8879
+ const segment = segments[index];
8880
+ if (segment === void 0) return node;
8881
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8882
+ const child = node[segment.key];
8883
+ const keys = [...walkedKeys, segment.key];
8884
+ const last = index === segments.length - 1;
8885
+ if (segment.each) {
8886
+ if (!Array.isArray(child)) return node;
8887
+ const prefix = keys.join(".");
8888
+ const idKey = arrayKeys[prefix];
8889
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8890
+ const mapped = child.map((element) => {
8891
+ const id = elementId(element, idKey, declared, prefix);
8892
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8893
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8894
+ });
8895
+ return {
8896
+ ...node,
8897
+ [segment.key]: mapped
8898
+ };
8899
+ }
8900
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8901
+ if (last) {
8902
+ const replacement = visit({
8903
+ declared,
8904
+ concrete,
8905
+ value: child
8906
+ });
8907
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8908
+ return {
8909
+ ...node,
8910
+ [segment.key]: replacement
8911
+ };
8912
+ }
8913
+ return {
8914
+ ...node,
8915
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8916
+ };
8917
+ }
8918
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8919
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8920
+ let current = data;
8921
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8922
+ return current;
8923
+ }
8924
+ /**
8925
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8926
+ * record's refs never collide with the refs a legacy collection of the same name
8927
+ * already holds during an adoption.
8928
+ */
8929
+ function recordSecretCollection(kind) {
8930
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8931
+ }
8932
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8933
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8934
+ return buildSecretRef({
8935
+ owner,
8936
+ scope: {
8937
+ kind: "row",
8938
+ collection: recordSecretCollection(kind),
8939
+ rowId
8940
+ },
8941
+ fieldPath: concretePath
8942
+ });
8943
+ }
8944
+ /**
8945
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8946
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8947
+ * token. The form hands it to the secret field, which reveals the value by ref
8948
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8949
+ * stored secret (the records door reads a view as KEEP).
8950
+ *
8951
+ * ONE implementation for the entries service and for every addon that serves
8952
+ * its own instances (`RecordIntegrationInstances`).
8953
+ */
8954
+ function isRecord$3$1(value) {
8955
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8956
+ }
8957
+ function secretViewsOf(owner, kind, id, decl, data) {
8958
+ if (decl.secretPaths.length === 0) return data;
8959
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8960
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8961
+ return {
8962
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8963
+ v: 1,
8964
+ set: true
8965
+ };
8966
+ });
8967
+ return isRecord$3$1(shown) ? shown : data;
8968
+ }
8739
8969
  /**
8740
8970
  * The share-view scope kinds a share-link token can carry. The hub mints
8741
8971
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8958,7 +9188,7 @@ function defineRecordKinds(specs) {
8958
9188
  retention: spec.retention,
8959
9189
  deviceRemoval: spec.deviceRemoval,
8960
9190
  maxDataBytes: spec.maxDataBytes,
8961
- secretPaths: [...secretPaths],
9191
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
8962
9192
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
8963
9193
  transactFamily: spec.transactFamily,
8964
9194
  entry: spec.entry === void 0 ? void 0 : {
@@ -9003,6 +9233,7 @@ function defineEntryKind(name, spec) {
9003
9233
  ...spec.database === void 0 ? {} : { database: spec.database },
9004
9234
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9005
9235
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9236
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9006
9237
  entry: {
9007
9238
  title: spec.title,
9008
9239
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -9667,6 +9898,7 @@ var CAP_NAMES = Object.freeze([
9667
9898
  "backup",
9668
9899
  "battery",
9669
9900
  "binary",
9901
+ "board-key-source",
9670
9902
  "brightness",
9671
9903
  "broker",
9672
9904
  "button",
@@ -9788,6 +10020,7 @@ var CAP_NAMES = Object.freeze([
9788
10020
  "privacy-mask",
9789
10021
  "ptz",
9790
10022
  "ptz-autotrack",
10023
+ "pump-cycle",
9791
10024
  "reboot",
9792
10025
  "record-entries",
9793
10026
  "recording",
@@ -11080,18 +11313,6 @@ Object.freeze({
11080
11313
  "path": "pumpRunning",
11081
11314
  "valueType": "boolean"
11082
11315
  },
11083
- {
11084
- "path": "pumpIntervalEnabled",
11085
- "valueType": "boolean"
11086
- },
11087
- {
11088
- "path": "pumpOnTime",
11089
- "valueType": "number"
11090
- },
11091
- {
11092
- "path": "pumpOffTime",
11093
- "valueType": "number"
11094
- },
11095
11316
  {
11096
11317
  "path": "lastDrink",
11097
11318
  "valueType": "number"
@@ -11215,6 +11436,20 @@ Object.freeze({
11215
11436
  "valueType": "number"
11216
11437
  }
11217
11438
  ],
11439
+ "pump-cycle": [
11440
+ {
11441
+ "path": "enabled",
11442
+ "valueType": "boolean"
11443
+ },
11444
+ {
11445
+ "path": "onTime",
11446
+ "valueType": "number"
11447
+ },
11448
+ {
11449
+ "path": "offTime",
11450
+ "valueType": "number"
11451
+ }
11452
+ ],
11218
11453
  "recording-archive": [
11219
11454
  {
11220
11455
  "path": "deviceId",
@@ -11664,7 +11899,7 @@ object({
11664
11899
  params: record(string$2(), union([string$2(), number()])).optional()
11665
11900
  });
11666
11901
  //#endregion
11667
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11902
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11668
11903
  /**
11669
11904
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11670
11905
  *
@@ -12427,38 +12662,6 @@ function validateExpressionSource(src) {
12427
12662
  return null;
12428
12663
  }
12429
12664
  /**
12430
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12431
- *
12432
- * Standby of the control board is under 5 W; a motor or a heater is far
12433
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12434
- * a cycle, so running starts only after 30 s above the line.
12435
- *
12436
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12437
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12438
- * report "finished" in the middle of the program, so the stop dwell is
12439
- * 10 minutes on every type. The number is stored per type so one machine
12440
- * can move later without moving the others.
12441
- */
12442
- var APPLIANCE_TEMPLATE_ID = "appliance";
12443
- var APPLIANCE_KINDS = [
12444
- "washer",
12445
- "dryer",
12446
- "dishwasher"
12447
- ];
12448
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12449
- /**
12450
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12451
- *
12452
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12453
- * (D967), so the heater switches exactly as the plug does. Running is the
12454
- * plug being on; when a power reading is also named, running is the draw
12455
- * staying above the line instead (a plug left on with the stove's own
12456
- * thermostat open is not heating). 50 W is far above any standby and below
12457
- * the smallest radiator.
12458
- */
12459
- var HEATER_TEMPLATE_ID = "heater";
12460
- DeviceType.Heater;
12461
- /**
12462
12665
  * Statistics and integrals over a history read (D944).
12463
12666
  *
12464
12667
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12602,34 +12805,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12602
12805
  /** Actuator groups only. Readings leave this unset. */
12603
12806
  active: ActiveCriterionSchema.optional()
12604
12807
  }),
12605
- object({
12606
- kind: literal("appliance"),
12607
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12608
- appliance: _enum(APPLIANCE_KINDS),
12609
- thresholdWatts: number().positive(),
12610
- startDwellMs: number().int().nonnegative(),
12611
- stopDwellMs: number().int().nonnegative(),
12612
- member: HelperMemberSchema,
12613
- /**
12614
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12615
- * composition carries a `power-source` feature, so the hub can cut and restore
12616
- * that plug (power-source link of a NEW-target composition).
12617
- */
12618
- powerSwitch: HelperMemberSchema.optional(),
12619
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12620
- offSeconds: number().optional()
12621
- }),
12622
- object({
12623
- kind: literal("heater"),
12624
- templateId: literal(HEATER_TEMPLATE_ID),
12625
- /** The plug, read and commanded at `switch.on`. */
12626
- plug: HelperMemberSchema,
12627
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12628
- meter: HelperMemberSchema.optional(),
12629
- thresholdWatts: number().positive(),
12630
- startDwellMs: number().int().nonnegative(),
12631
- stopDwellMs: number().int().nonnegative()
12632
- }),
12633
12808
  object({
12634
12809
  kind: literal("statistic"),
12635
12810
  stat: _enum(STATISTIC_STATS),
@@ -12801,7 +12976,14 @@ var CompositionForwardCommandSchema = object({
12801
12976
  method: string$2().min(1),
12802
12977
  args: record(string$2().min(1), CompositionForwardArgSchema).optional()
12803
12978
  });
12804
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12979
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
12980
+ CompositionForwardCommandSchema,
12981
+ CompositionCodeSourceSchema,
12982
+ object({
12983
+ kind: literal("extension"),
12984
+ target: literal("remote")
12985
+ })
12986
+ ]);
12805
12987
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12806
12988
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12807
12989
  /**
@@ -12846,7 +13028,7 @@ var CompositionSchema = object({
12846
13028
  kind: literal("existing"),
12847
13029
  device: CompositionSourceRefSchema
12848
13030
  })]),
12849
- features: array(CompositionFeatureSchema).min(1).max(16),
13031
+ features: array(CompositionFeatureSchema).min(0).max(16),
12850
13032
  /**
12851
13033
  * Present when the block is a helper template. Absent on a free
12852
13034
  * composition (the room example, device 4603). The editor reads this,
@@ -12878,6 +13060,73 @@ var NodeColorIdSchema = _enum([
12878
13060
  "node-g",
12879
13061
  "node-h"
12880
13062
  ]);
13063
+ //#endregion
13064
+ //#region ../types/dist/index.mjs
13065
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
13066
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13067
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13068
+ /** The text excerpt a message carries on the bus. */
13069
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13070
+ var PortSchema = number().int().min(1).max(65535);
13071
+ /** A recipient local-part: what stands before the `@`. */
13072
+ var SmtpRecipientSchema = string$2().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13073
+ object({
13074
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13075
+ bindHost: string$2().min(1).default("0.0.0.0"),
13076
+ domain: string$2().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13077
+ requireAuth: boolean().default(true),
13078
+ username: string$2().default(""),
13079
+ password: string$2().default("").meta({ secret: true }),
13080
+ starttls: boolean().default(false),
13081
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13082
+ });
13083
+ object({
13084
+ host: string$2().min(1).nullable(),
13085
+ hostReason: string$2().nullable(),
13086
+ port: PortSchema,
13087
+ domain: string$2().min(1),
13088
+ requireAuth: boolean(),
13089
+ username: string$2(),
13090
+ password: string$2(),
13091
+ starttls: boolean()
13092
+ });
13093
+ object({
13094
+ recipients: array(SmtpRecipientSchema).min(1),
13095
+ /**
13096
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13097
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13098
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13099
+ * Absent, the subscriber is anonymous and unique.
13100
+ */
13101
+ subscriberId: string$2().min(1).max(64).optional()
13102
+ });
13103
+ var SmtpInboundAttachmentSchema = object({
13104
+ filename: string$2(),
13105
+ contentType: string$2(),
13106
+ bytes: number().int().nonnegative()
13107
+ });
13108
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13109
+ var SmtpInboundMessageSchema = object({
13110
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13111
+ seq: number().int().positive(),
13112
+ messageId: string$2(),
13113
+ receivedAt: number().int().nonnegative(),
13114
+ from: string$2(),
13115
+ to: string$2(),
13116
+ subject: string$2(),
13117
+ text: string$2().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13118
+ attachments: array(SmtpInboundAttachmentSchema)
13119
+ });
13120
+ object({
13121
+ /** The newest seq issued so far (0 before any mail). */
13122
+ lastSeq: number().int().nonnegative(),
13123
+ /** The oldest seq still retained; messages below it are gone. */
13124
+ oldestSeq: number().int().nonnegative(),
13125
+ messages: array(object({
13126
+ key: string$2(),
13127
+ message: SmtpInboundMessageSchema
13128
+ }))
13129
+ });
12881
13130
  new Set([
12882
13131
  "track",
12883
13132
  "face",
@@ -12997,6 +13246,157 @@ var IntegrationInstanceRefSchema = object({
12997
13246
  ]),
12998
13247
  candidateStableId: string$2().optional()
12999
13248
  });
13249
+ /** The most instances one addon lists (an integrations page, not a table). */
13250
+ var INSTANCES_LIST_MAX = 1e3;
13251
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13252
+ function topLevelSecretKeys(paths) {
13253
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13254
+ }
13255
+ var RecordIntegrationInstances = class {
13256
+ records;
13257
+ kind;
13258
+ hooks;
13259
+ handle;
13260
+ secretKeys;
13261
+ nameField;
13262
+ constructor(records, kind, hooks) {
13263
+ this.records = records;
13264
+ this.kind = kind;
13265
+ this.hooks = hooks;
13266
+ this.handle = records.kind(kind);
13267
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13268
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13269
+ }
13270
+ /**
13271
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13272
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13273
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13274
+ */
13275
+ async readResolved(id) {
13276
+ try {
13277
+ const record = await this.handle.getResolved(id);
13278
+ return record === null ? null : {
13279
+ id,
13280
+ body: record.data,
13281
+ enabled: record.data.enabled !== false,
13282
+ secrets: "resolved"
13283
+ };
13284
+ } catch (err) {
13285
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13286
+ const stored = await this.handle.get(id);
13287
+ if (stored === null) return null;
13288
+ return {
13289
+ id,
13290
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13291
+ enabled: stored.data.enabled !== false,
13292
+ secrets: "unavailable"
13293
+ };
13294
+ }
13295
+ }
13296
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13297
+ async getResolved(id) {
13298
+ return (await this.readResolved(id))?.body ?? null;
13299
+ }
13300
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13301
+ async listResolved() {
13302
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13303
+ const out = [];
13304
+ for (const record of page.records) {
13305
+ const read = await this.readResolved(record.id);
13306
+ if (read !== null) out.push(read);
13307
+ }
13308
+ return out;
13309
+ }
13310
+ viewOf(record) {
13311
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13312
+ const rawName = record.data[this.nameField];
13313
+ const kind = this.hooks.kindOf?.(record.data);
13314
+ const health = this.hooks.health?.(record.data, record.id);
13315
+ return {
13316
+ id: record.id,
13317
+ name: typeof rawName === "string" ? rawName : record.id,
13318
+ configured: this.hooks.configured(record.data),
13319
+ enabled: record.data.enabled !== false,
13320
+ formValues: shown,
13321
+ ...kind !== void 0 ? { kind } : {},
13322
+ ...health?.status !== void 0 ? { status: health.status } : {},
13323
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13324
+ };
13325
+ }
13326
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13327
+ ...this.hooks.description,
13328
+ mintsId: true
13329
+ };
13330
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13331
+ listInstances = async () => {
13332
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13333
+ };
13334
+ getInstance = async ({ id }) => {
13335
+ const record = await this.handle.get(id);
13336
+ return record === null ? null : this.viewOf(record);
13337
+ };
13338
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13339
+ createInstance = async ({ id, name, kind }) => {
13340
+ const minted = id ?? this.hooks.mintId?.({
13341
+ name,
13342
+ ...kind !== void 0 ? { kind } : {}
13343
+ });
13344
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13345
+ await this.handle.put({
13346
+ id: minted,
13347
+ data: this.hooks.initialBody(name, kind)
13348
+ }, { ifRev: 0 });
13349
+ await this.hooks.onChanged?.(minted);
13350
+ return { id: minted };
13351
+ };
13352
+ updateInstance = async ({ id, name, values }) => {
13353
+ const patch = {};
13354
+ for (const [key, value] of Object.entries(values)) {
13355
+ if (value === void 0) continue;
13356
+ if (!this.secretKeys.has(key)) patch[key] = value;
13357
+ else if (isSecretClear(value)) patch[key] = null;
13358
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13359
+ }
13360
+ if (name !== void 0) patch[this.nameField] = name;
13361
+ await this.handle.patch(id, patch);
13362
+ await this.hooks.onChanged?.(id);
13363
+ return { ok: true };
13364
+ };
13365
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13366
+ setInstanceEnabled = async ({ id, enabled }) => {
13367
+ await this.handle.patch(id, { enabled });
13368
+ await this.hooks.onChanged?.(id);
13369
+ return { ok: true };
13370
+ };
13371
+ /** The row and its keychain refs go together (the records door). */
13372
+ deleteInstance = async ({ id }) => {
13373
+ await this.handle.delete(id);
13374
+ await this.hooks.onChanged?.(id);
13375
+ return { ok: true };
13376
+ };
13377
+ testInstance = async (input) => {
13378
+ if (this.hooks.test === void 0) return {
13379
+ success: false,
13380
+ status: "unsupported",
13381
+ error: "This integration declares no connection test"
13382
+ };
13383
+ return this.hooks.test(input);
13384
+ };
13385
+ /** The flat provider object to hand to `registerProvider`. */
13386
+ toProvider() {
13387
+ return {
13388
+ describe: this.describe,
13389
+ instanceFormSchema: this.instanceFormSchema,
13390
+ listInstances: this.listInstances,
13391
+ getInstance: this.getInstance,
13392
+ createInstance: this.createInstance,
13393
+ updateInstance: this.updateInstance,
13394
+ setInstanceEnabled: this.setInstanceEnabled,
13395
+ deleteInstance: this.deleteInstance,
13396
+ testInstance: this.testInstance
13397
+ };
13398
+ }
13399
+ };
13000
13400
  var EncodeProfileSchema = object({
13001
13401
  video: object({
13002
13402
  codec: _enum([
@@ -14065,6 +14465,58 @@ var MigrationOwnerStepSchema = object({
14065
14465
  /** The ledger state the engine found: `pending` has no row yet. */
14066
14466
  state: union([literal("pending"), MigrationStatusSchema])
14067
14467
  });
14468
+ function isPlain(value) {
14469
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14470
+ }
14471
+ async function flattenNestedRecords(ports, spec) {
14472
+ const ids = await ports.listNested(spec.legacyKey);
14473
+ if (ids.length === 0) return {
14474
+ outcome: "already-done",
14475
+ summary: { converted: 0 }
14476
+ };
14477
+ const rows = /* @__PURE__ */ new Map();
14478
+ for (const id of ids) try {
14479
+ rows.set(id, await ports.readResolved(id));
14480
+ } catch (err) {
14481
+ const why = err instanceof Error ? err.message : String(err);
14482
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14483
+ }
14484
+ for (const [id, row] of rows) {
14485
+ const legacy = row[spec.legacyKey];
14486
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14487
+ const flat = spec.toFlat(legacy, row);
14488
+ await ports.patch(id, flat);
14489
+ const back = await ports.readResolved(id);
14490
+ const wrong = spec.flatSecretKeys.filter((key) => {
14491
+ const sent = flat[key];
14492
+ const got = back[key];
14493
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14494
+ });
14495
+ if (wrong.length > 0) {
14496
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14497
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14498
+ }
14499
+ await ports.patch(id, { [spec.legacyKey]: null });
14500
+ }
14501
+ return {
14502
+ outcome: "done",
14503
+ summary: { converted: rows.size }
14504
+ };
14505
+ }
14506
+ /** The ports over one kind's records handle. */
14507
+ function flattenPortsOver(handle) {
14508
+ return {
14509
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14510
+ readResolved: async (id) => {
14511
+ const row = await handle.getResolved(id);
14512
+ if (row === null) throw new Error("the record is gone");
14513
+ return row.data;
14514
+ },
14515
+ patch: async (id, patch) => {
14516
+ await handle.patch(id, patch);
14517
+ }
14518
+ };
14519
+ }
14068
14520
  /**
14069
14521
  * The trainer contract — what the hub hands the cloud trainer and what it
14070
14522
  * gets back (cloud training, D805+).
@@ -19150,24 +19602,52 @@ var APPLIANCE_CAP_VOCABULARY = {
19150
19602
  args: { on: false }
19151
19603
  },
19152
19604
  {
19153
- id: "petFountain.intervalOn",
19154
- method: "setPumpInterval",
19605
+ id: "petFountain.resetDailyConsumption",
19606
+ method: "resetDailyConsumption",
19607
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19608
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19609
+ icon: "rotate-ccw"
19610
+ }
19611
+ ],
19612
+ state: [{
19613
+ kind: "boolean",
19614
+ field: "empty",
19615
+ label: t("Tank empty", "Serbatoio vuoto"),
19616
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19617
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19618
+ icon: "droplet"
19619
+ }, {
19620
+ kind: "numeric",
19621
+ field: "waterLevel",
19622
+ quantity: t("the water level", "il livello dell’acqua"),
19623
+ label: t("Water level", "Livello dell’acqua"),
19624
+ unit: "%",
19625
+ icon: "droplets"
19626
+ }]
19627
+ },
19628
+ "pump-cycle": {
19629
+ scope: "device",
19630
+ noun: t("Pump cycle", "Ciclo della pompa"),
19631
+ actions: [
19632
+ {
19633
+ id: "pumpCycle.on",
19634
+ method: "setEnabled",
19155
19635
  label: t("Pump cycle on", "Ciclo pompa attivo"),
19156
19636
  phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
19157
19637
  icon: "timer",
19158
19638
  args: { on: true }
19159
19639
  },
19160
19640
  {
19161
- id: "petFountain.intervalOff",
19162
- method: "setPumpInterval",
19641
+ id: "pumpCycle.off",
19642
+ method: "setEnabled",
19163
19643
  label: t("Pump cycle off", "Ciclo pompa spento"),
19164
19644
  phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
19165
19645
  icon: "timer-off",
19166
19646
  args: { on: false }
19167
19647
  },
19168
19648
  {
19169
- id: "petFountain.pumpOnMinutes",
19170
- method: "setPumpOnMinutes",
19649
+ id: "pumpCycle.onMinutes",
19650
+ method: "setOnMinutes",
19171
19651
  label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
19172
19652
  phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
19173
19653
  icon: "timer",
@@ -19183,8 +19663,8 @@ var APPLIANCE_CAP_VOCABULARY = {
19183
19663
  }]
19184
19664
  },
19185
19665
  {
19186
- id: "petFountain.pumpOffMinutes",
19187
- method: "setPumpOffMinutes",
19666
+ id: "pumpCycle.offMinutes",
19667
+ method: "setOffMinutes",
19188
19668
  label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
19189
19669
  phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
19190
19670
  icon: "timer-off",
@@ -19198,35 +19678,63 @@ var APPLIANCE_CAP_VOCABULARY = {
19198
19678
  unit: "min"
19199
19679
  }
19200
19680
  }]
19201
- },
19202
- {
19203
- id: "petFountain.resetDailyConsumption",
19204
- method: "resetDailyConsumption",
19205
- label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19206
- phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19207
- icon: "rotate-ccw"
19208
19681
  }
19209
19682
  ],
19210
19683
  state: [{
19211
19684
  kind: "boolean",
19212
- field: "empty",
19213
- label: t("Tank empty", "Serbatoio vuoto"),
19214
- whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19215
- whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19216
- icon: "droplet"
19217
- }, {
19218
- kind: "numeric",
19219
- field: "waterLevel",
19220
- quantity: t("the water level", "il livello dell’acqua"),
19221
- label: t("Water level", "Livello dell’acqua"),
19222
- unit: "%",
19223
- icon: "droplets"
19685
+ field: "enabled",
19686
+ label: t("Pump cycle", "Ciclo pompa"),
19687
+ whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
19688
+ whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
19689
+ icon: "timer"
19224
19690
  }]
19225
19691
  },
19226
19692
  "media-player": {
19227
19693
  scope: "device",
19228
19694
  noun: t("Media player", "Lettore multimediale"),
19229
19695
  actions: [
19696
+ {
19697
+ id: "media.turnOn",
19698
+ method: "turnOn",
19699
+ label: t("Turn on", "Accendi"),
19700
+ phrase: t("Turn on {device}", "Accendi {device}"),
19701
+ icon: "power"
19702
+ },
19703
+ {
19704
+ id: "media.turnOff",
19705
+ method: "turnOff",
19706
+ label: t("Turn off", "Spegni"),
19707
+ phrase: t("Turn off {device}", "Spegni {device}"),
19708
+ icon: "power"
19709
+ },
19710
+ {
19711
+ id: "media.volumeUp",
19712
+ method: "volumeUp",
19713
+ label: t("Volume up", "Alza il volume"),
19714
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
19715
+ icon: "volume-2"
19716
+ },
19717
+ {
19718
+ id: "media.volumeDown",
19719
+ method: "volumeDown",
19720
+ label: t("Volume down", "Abbassa il volume"),
19721
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
19722
+ icon: "volume-2"
19723
+ },
19724
+ {
19725
+ id: "media.channelUp",
19726
+ method: "channelUp",
19727
+ label: t("Channel up", "Canale successivo"),
19728
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
19729
+ icon: "skip-forward"
19730
+ },
19731
+ {
19732
+ id: "media.channelDown",
19733
+ method: "channelDown",
19734
+ label: t("Channel down", "Canale precedente"),
19735
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
19736
+ icon: "skip-back"
19737
+ },
19230
19738
  {
19231
19739
  id: "media.play",
19232
19740
  method: "play",
@@ -21113,6 +21621,7 @@ var SYSTEM_CAP_VOCABULARY = {
21113
21621
  "audio-analyzer": system(PIPELINE),
21114
21622
  "audio-codec": system(INTERNAL$1),
21115
21623
  "auth-provider": system(IDENTITY),
21624
+ "board-key-source": system(INTERNAL$1),
21116
21625
  backup: system("system backups — run and restored from the Backups page"),
21117
21626
  broker: system("message-broker connections — configured, not actuated"),
21118
21627
  "calendar-source": system(INTERNAL$1),
@@ -46022,6 +46531,49 @@ var binaryCapability = {
46022
46531
  volatileStateFields: ["lastChangedAt"]
46023
46532
  };
46024
46533
  /**
46534
+ * Why a board key could not be read. Typed so a caller reports the cause and
46535
+ * never treats "could not read" as "has no key" (D49).
46536
+ */
46537
+ var BoardKeyFailureReasonSchema = _enum([
46538
+ "unknown-source",
46539
+ "not-admin",
46540
+ "no-builder",
46541
+ "no-ingress-url",
46542
+ "unreachable",
46543
+ "unknown-board",
46544
+ "no-encryption-key",
46545
+ "unreadable-config"
46546
+ ]);
46547
+ var BoardKeySourceSchema = object({
46548
+ sourceId: string$2(),
46549
+ name: string$2()
46550
+ });
46551
+ var BoardKeyBoardSchema = object({
46552
+ /** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
46553
+ configuration: string$2(),
46554
+ /** The node name the builder gives the board. */
46555
+ name: string$2(),
46556
+ /** The address the builder knows the board by, when it has one. */
46557
+ address: string$2().nullable()
46558
+ });
46559
+ var BoardKeyFailureSchema = object({
46560
+ ok: literal(false),
46561
+ reason: BoardKeyFailureReasonSchema,
46562
+ detail: string$2().optional()
46563
+ });
46564
+ var ListBoardsResultSchema = discriminatedUnion("ok", [object({
46565
+ ok: literal(true),
46566
+ boards: array(BoardKeyBoardSchema).readonly()
46567
+ }), BoardKeyFailureSchema]);
46568
+ var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
46569
+ ok: literal(true),
46570
+ key: string$2()
46571
+ }), BoardKeyFailureSchema]);
46572
+ method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string$2().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
46573
+ sourceId: string$2().min(1),
46574
+ configuration: string$2().min(1)
46575
+ }), GetBoardKeyResultSchema, { auth: "admin" });
46576
+ /**
46025
46577
  * Dimmable-light brightness control. Co-exists with `switch` on the
46026
46578
  * same device — the switch toggles on/off, this cap sets the level
46027
46579
  * applied when the light is on. Drivers map their per-vendor dim
@@ -48769,6 +49321,244 @@ var controlCapability = {
48769
49321
  */
48770
49322
  history: "none"
48771
49323
  };
49324
+ /**
49325
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49326
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49327
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49328
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49329
+ *
49330
+ * The cap declares a wide method surface; not every backend supports
49331
+ * every method. Drivers signal sub-capability via the
49332
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49333
+ * corresponding method) so the UI gates buttons without a slice
49334
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49335
+ */
49336
+ var MediaPlayerStateSchema = _enum([
49337
+ "off",
49338
+ "on",
49339
+ "idle",
49340
+ "playing",
49341
+ "paused",
49342
+ "buffering",
49343
+ "standby"
49344
+ ]);
49345
+ var MediaPlayerRepeatSchema = _enum([
49346
+ "off",
49347
+ "all",
49348
+ "one"
49349
+ ]);
49350
+ var MediaInfoSchema = object({
49351
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49352
+ * `podcast`, …). */
49353
+ type: string$2(),
49354
+ /** Human-readable title. */
49355
+ title: string$2(),
49356
+ /** Optional artist / channel / station label. */
49357
+ artist: string$2().optional(),
49358
+ /** Optional album / season / show name. */
49359
+ album: string$2().optional(),
49360
+ /** Optional cover-art / thumbnail URL. */
49361
+ imageUrl: string$2().optional()
49362
+ });
49363
+ /**
49364
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49365
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49366
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49367
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49368
+ * guess, never as a reading.
49369
+ */
49370
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49371
+ /** The fields of a media-player status a driver can only guess. */
49372
+ var MediaPlayerAssumedFieldSchema = _enum([
49373
+ "state",
49374
+ "volumeLevel",
49375
+ "isMuted",
49376
+ "source"
49377
+ ]);
49378
+ var MediaPlayerStatusSchema = object({
49379
+ /** Playback lifecycle state. */
49380
+ state: MediaPlayerStateSchema,
49381
+ /**
49382
+ * Whether the state fields are readings or the driver's own bookkeeping.
49383
+ * A slice written before this field existed parses as `reported`.
49384
+ */
49385
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49386
+ /**
49387
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49388
+ * this is not empty; a field not listed here is a reading even then (power
49389
+ * from a running-state source, mute guessed from the last send).
49390
+ */
49391
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49392
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49393
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49394
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
49395
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49396
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49397
+ isMuted: boolean().nullable(),
49398
+ /** Active source / input. Empty when no source surface. */
49399
+ source: string$2(),
49400
+ /** Selectable sources. Empty when no source surface.
49401
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49402
+ availableSources: array(string$2()),
49403
+ /** Currently-playing media info. Null when nothing is playing. */
49404
+ currentMedia: MediaInfoSchema.nullable(),
49405
+ /** Current playback position in ms. Null when not seekable or
49406
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
49407
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49408
+ /** Total duration of the current media in ms. Null when unknown
49409
+ * (live stream) or nothing is playing. */
49410
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49411
+ /** Shuffle toggle. Null when no shuffle surface.
49412
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
49413
+ shuffle: boolean().nullable(),
49414
+ /** Repeat mode. Null when no repeat surface.
49415
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
49416
+ repeat: MediaPlayerRepeatSchema.nullable(),
49417
+ /** Ms epoch when the slice was last updated. */
49418
+ lastChangedAt: number()
49419
+ });
49420
+ var stepInput = object({
49421
+ deviceId: number().int().nonnegative(),
49422
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
49423
+ steps: number().int().min(1).max(50).default(1)
49424
+ });
49425
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
49426
+ var mediaPlayerCapability = {
49427
+ name: "media-player",
49428
+ scope: "device",
49429
+ deviceNative: true,
49430
+ mode: "singleton",
49431
+ deviceTypes: [DeviceType.MediaPlayer],
49432
+ methods: {
49433
+ turnOn: method(deviceOnly, _void(), {
49434
+ kind: "mutation",
49435
+ auth: "protected"
49436
+ }),
49437
+ turnOff: method(deviceOnly, _void(), {
49438
+ kind: "mutation",
49439
+ auth: "protected"
49440
+ }),
49441
+ volumeUp: method(stepInput, _void(), {
49442
+ kind: "mutation",
49443
+ auth: "protected"
49444
+ }),
49445
+ volumeDown: method(stepInput, _void(), {
49446
+ kind: "mutation",
49447
+ auth: "protected"
49448
+ }),
49449
+ channelUp: method(stepInput, _void(), {
49450
+ kind: "mutation",
49451
+ auth: "protected"
49452
+ }),
49453
+ channelDown: method(stepInput, _void(), {
49454
+ kind: "mutation",
49455
+ auth: "protected"
49456
+ }),
49457
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49458
+ kind: "mutation",
49459
+ auth: "protected"
49460
+ }),
49461
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49462
+ kind: "mutation",
49463
+ auth: "protected"
49464
+ }),
49465
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49466
+ kind: "mutation",
49467
+ auth: "protected"
49468
+ }),
49469
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49470
+ kind: "mutation",
49471
+ auth: "protected"
49472
+ }),
49473
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49474
+ kind: "mutation",
49475
+ auth: "protected"
49476
+ }),
49477
+ seek: method(object({
49478
+ deviceId: number().int().nonnegative(),
49479
+ position: number().int().nonnegative()
49480
+ }), _void(), {
49481
+ kind: "mutation",
49482
+ auth: "protected"
49483
+ }),
49484
+ setVolume: method(object({
49485
+ deviceId: number().int().nonnegative(),
49486
+ volumeLevel: number().min(0).max(100)
49487
+ }), _void(), {
49488
+ kind: "mutation",
49489
+ auth: "protected"
49490
+ }),
49491
+ setMute: method(object({
49492
+ deviceId: number().int().nonnegative(),
49493
+ muted: boolean()
49494
+ }), _void(), {
49495
+ kind: "mutation",
49496
+ auth: "protected"
49497
+ }),
49498
+ setShuffle: method(object({
49499
+ deviceId: number().int().nonnegative(),
49500
+ shuffle: boolean()
49501
+ }), _void(), {
49502
+ kind: "mutation",
49503
+ auth: "protected"
49504
+ }),
49505
+ setRepeat: method(object({
49506
+ deviceId: number().int().nonnegative(),
49507
+ repeat: MediaPlayerRepeatSchema
49508
+ }), _void(), {
49509
+ kind: "mutation",
49510
+ auth: "protected"
49511
+ }),
49512
+ selectSource: method(object({
49513
+ deviceId: number().int().nonnegative(),
49514
+ source: string$2().min(1)
49515
+ }), _void(), {
49516
+ kind: "mutation",
49517
+ auth: "protected"
49518
+ }),
49519
+ playMedia: method(object({
49520
+ deviceId: number().int().nonnegative(),
49521
+ /** Media identifier / URL. */
49522
+ mediaId: string$2().min(1),
49523
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
49524
+ * passes it through to the upstream service. */
49525
+ mediaType: string$2().min(1)
49526
+ }), _void(), {
49527
+ kind: "mutation",
49528
+ auth: "protected"
49529
+ })
49530
+ },
49531
+ status: {
49532
+ schema: MediaPlayerStatusSchema,
49533
+ kind: "push"
49534
+ },
49535
+ /**
49536
+ * Runtime-state slice — mirrored by the kernel. UI players read the
49537
+ * full slice for live now-playing, volume, and progress updates
49538
+ * without polling.
49539
+ */
49540
+ runtimeState: MediaPlayerStatusSchema,
49541
+ /**
49542
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
49543
+ *
49544
+ * See `RuntimeStateDurability`. Enforced by
49545
+ * `scripts/check-runtime-state-durability.ts`.
49546
+ */
49547
+ durability: "session",
49548
+ /**
49549
+ * History (D850): volume (a level an operator or an automation sets; the
49550
+ * default 10 s floor folds a slider drag into its end value) and mute.
49551
+ * `position` is not recorded: it moves continuously while playing.
49552
+ */
49553
+ history: { fields: {
49554
+ volumeLevel: {
49555
+ kind: "gauge",
49556
+ unit: "%",
49557
+ deadband: 1
49558
+ },
49559
+ isMuted: { kind: "boolean" }
49560
+ } }
49561
+ };
48772
49562
  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;
48773
49563
  var NUMBER = {
48774
49564
  kind: "fixed",
@@ -49216,117 +50006,6 @@ function actuator(input) {
49216
50006
  };
49217
50007
  }
49218
50008
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49219
- var PowerSourceRefusalReasonSchema = _enum([
49220
- "no-power-source",
49221
- "switch-missing",
49222
- "switch-offline",
49223
- "switch-unknown",
49224
- "restart-in-progress",
49225
- "switch-call-failed"
49226
- ]);
49227
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
49228
- accepted: literal(false),
49229
- reason: PowerSourceRefusalReasonSchema,
49230
- /** One line for the operator, naming the switch. */
49231
- message: string$2()
49232
- })]);
49233
- var PowerSourceInfoSchema = object({
49234
- /** False when the device has no power source linked; the rest is then empty. */
49235
- linked: boolean(),
49236
- switchDeviceId: number().nullable(),
49237
- switchName: string$2().nullable(),
49238
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
49239
- switchOn: boolean().nullable(),
49240
- switchOnline: boolean().nullable(),
49241
- restartInProgress: boolean(),
49242
- offSeconds: number().int()
49243
- });
49244
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
49245
- var PowerSourceStateSchema = object({
49246
- /** Null while the switch has not reported: unknown is never `false` (D393). */
49247
- switchOn: boolean().nullable(),
49248
- offSeconds: number().int(),
49249
- lastFetchedAt: number()
49250
- });
49251
- var POWER_SOURCE_DEVICE_TYPES = [
49252
- DeviceType.Gateway,
49253
- DeviceType.Hub,
49254
- DeviceType.Camera,
49255
- DeviceType.Sensor,
49256
- DeviceType.MediaPlayer,
49257
- DeviceType.Vacuum,
49258
- DeviceType.Generic,
49259
- DeviceType.Washer,
49260
- DeviceType.Dryer,
49261
- DeviceType.Dishwasher,
49262
- DeviceType.PetFountain,
49263
- DeviceType.Fan,
49264
- DeviceType.Humidifier,
49265
- DeviceType.Climate,
49266
- DeviceType.WaterHeater
49267
- ];
49268
- var powerSourceCustomize = {
49269
- version: 1,
49270
- targets: [{
49271
- id: "switch",
49272
- cardinality: "scalar",
49273
- vocabulary: {
49274
- cap: "power-source",
49275
- stateField: "switchOn"
49276
- },
49277
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49278
- apply: "add",
49279
- path: "switchOn",
49280
- sources: [{
49281
- kind: "switch",
49282
- transforms: [{ kind: "identity" }]
49283
- }],
49284
- companions: [{
49285
- kind: "constant",
49286
- path: "offSeconds",
49287
- value: 10,
49288
- when: "add"
49289
- }, {
49290
- kind: "clock",
49291
- path: "lastFetchedAt",
49292
- when: "always"
49293
- }],
49294
- sourceReport: { status: "assignment-status" }
49295
- }]
49296
- };
49297
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
49298
- var powerSourceCapability = {
49299
- name: "power-source",
49300
- scope: "device",
49301
- mode: "singleton",
49302
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49303
- methods: {
49304
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
49305
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49306
- kind: "mutation",
49307
- auth: "admin"
49308
- }),
49309
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49310
- kind: "mutation",
49311
- auth: "admin"
49312
- }),
49313
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49314
- kind: "mutation",
49315
- auth: "admin"
49316
- })
49317
- },
49318
- runtimeState: PowerSourceStateSchema,
49319
- customize: powerSourceCustomize,
49320
- /**
49321
- * Runtime-state durability: **session** — mirrored from the linked switch on
49322
- * its next report; a restored value would outlive the reading it names.
49323
- *
49324
- * See `RuntimeStateDurability`. Enforced by
49325
- * `scripts/check-runtime-state-durability.ts`.
49326
- */
49327
- durability: "session",
49328
- history: "none"
49329
- };
49330
50009
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
49331
50010
  DeviceType.Camera;
49332
50011
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -49354,6 +50033,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
49354
50033
  }).optional()
49355
50034
  })) });
49356
50035
  /**
50036
+ * A saved Customize assignment (D936). The schema is the type: callers use
50037
+ * `z.infer`, they do not restate the shape.
50038
+ */
50039
+ var OverrideSourceSchema = object({
50040
+ addonId: string$2().min(1),
50041
+ stableId: string$2().min(1),
50042
+ cap: string$2().min(1),
50043
+ fieldPath: string$2().min(1)
50044
+ });
50045
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50046
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50047
+ var ThresholdTransformDraftSchema = object({
50048
+ kind: literal("threshold"),
50049
+ comparison: _enum(["gte", "lte"]),
50050
+ threshold: number(),
50051
+ startDwellMs: number().nonnegative(),
50052
+ stopDwellMs: number().nonnegative()
50053
+ });
50054
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50055
+ IdentityTransformDraftSchema,
50056
+ InvertTransformDraftSchema,
50057
+ ThresholdTransformDraftSchema,
50058
+ object({
50059
+ kind: literal("linear"),
50060
+ scale: number(),
50061
+ offset: number(),
50062
+ clampMin: number().optional(),
50063
+ clampMax: number().optional()
50064
+ }),
50065
+ object({
50066
+ kind: literal("enum-map"),
50067
+ mapping: record(string$2(), string$2().nullable())
50068
+ }),
50069
+ object({ kind: literal("number") })
50070
+ ]);
50071
+ var ScalarOverrideDraftSchema = object({
50072
+ kind: literal("scalar"),
50073
+ cap: string$2().min(1),
50074
+ targetId: string$2().min(1),
50075
+ source: OverrideSourceSchema,
50076
+ transform: CustomizeTransformDraftSchema
50077
+ });
50078
+ var KeyedArrayOverrideDraftSchema = object({
50079
+ kind: literal("keyedArray"),
50080
+ cap: string$2().min(1),
50081
+ targetId: string$2().min(1),
50082
+ key: string$2().min(1),
50083
+ label: string$2().min(1),
50084
+ fieldPath: string$2().min(1),
50085
+ source: OverrideSourceSchema,
50086
+ transform: CustomizeTransformDraftSchema
50087
+ });
50088
+ var PulseOverrideDraftSchema = object({
50089
+ kind: literal("pulse"),
50090
+ cap: string$2().min(1),
50091
+ targetId: string$2().min(1),
50092
+ enabled: boolean(),
50093
+ debounceMs: number().nonnegative(),
50094
+ edges: array(_enum([
50095
+ "rising",
50096
+ "button",
50097
+ "native-event"
50098
+ ])).min(1).optional(),
50099
+ sources: array(OverrideSourceSchema).min(1)
50100
+ });
50101
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50102
+ "in-addition",
50103
+ "confirm",
50104
+ "replace"
50105
+ ]).optional() });
50106
+ discriminatedUnion("kind", [
50107
+ ScalarOverrideDraftSchema,
50108
+ KeyedArrayOverrideDraftSchema,
50109
+ PulseOverrideDraftSchema,
50110
+ object({
50111
+ kind: literal("span"),
50112
+ cap: string$2().min(1),
50113
+ targetId: string$2().min(1),
50114
+ timeoutMs: number().nonnegative(),
50115
+ cooldownMs: number().nonnegative().optional(),
50116
+ sources: array(SpanSourceDraftSchema)
50117
+ })
50118
+ ]);
50119
+ object({
50120
+ targetId: string$2().min(1),
50121
+ configured: boolean(),
50122
+ source: _enum(["native", "assigned"]),
50123
+ state: _enum([
50124
+ "active",
50125
+ "stale",
50126
+ "released",
50127
+ "unavailable"
50128
+ ]),
50129
+ reason: string$2().nullable()
50130
+ });
50131
+ /**
50132
+ * What a running-threshold proposal says (planning#405). The schema is the
50133
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50134
+ *
50135
+ * A proposal is read-only advice: it carries the numbers it was derived from
50136
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50137
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50138
+ */
50139
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50140
+ var RunningProposalConfidenceSchema = _enum([
50141
+ "high",
50142
+ "medium",
50143
+ "low"
50144
+ ]);
50145
+ var RunningProposalInsufficientReasonSchema = _enum([
50146
+ "no-history",
50147
+ "span-too-short",
50148
+ "truncated",
50149
+ "no-idle-floor",
50150
+ "no-runs",
50151
+ "no-separation"
50152
+ ]);
50153
+ var RunningProposalReplaySchema = object({
50154
+ /** Intervals the real dwell expression held true over the window. */
50155
+ intervals: number().int().nonnegative(),
50156
+ /** Intervals outside every run, or a second interval inside one. */
50157
+ extra: number().int().nonnegative(),
50158
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50159
+ missed: number().int().nonnegative()
50160
+ });
50161
+ var RunningProposalEvidenceSchema = object({
50162
+ tier: RunningProposalTierSchema,
50163
+ spanMs: number().nonnegative(),
50164
+ from: number(),
50165
+ to: number(),
50166
+ points: number().int().nonnegative(),
50167
+ runsFound: number().int().nonnegative(),
50168
+ pausesFound: number().int().nonnegative(),
50169
+ longestPauseMs: number().nonnegative(),
50170
+ idleFloorW: number(),
50171
+ noiseCeilingW: number(),
50172
+ plateauW: number(),
50173
+ replay: RunningProposalReplaySchema
50174
+ });
50175
+ var RunningProposalInsufficientEvidenceSchema = object({
50176
+ spanMs: number().nonnegative(),
50177
+ points: number().int().nonnegative(),
50178
+ runsFound: number().int().nonnegative()
50179
+ });
50180
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50181
+ state: literal("proposal"),
50182
+ transform: ThresholdTransformDraftSchema,
50183
+ evidence: RunningProposalEvidenceSchema,
50184
+ confidence: RunningProposalConfidenceSchema,
50185
+ /** Why the confidence is not higher; empty when nothing held it back. */
50186
+ notes: array(string$2())
50187
+ }), object({
50188
+ state: literal("insufficient"),
50189
+ reason: RunningProposalInsufficientReasonSchema,
50190
+ evidence: RunningProposalInsufficientEvidenceSchema,
50191
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50192
+ fallback: ThresholdTransformDraftSchema
50193
+ })]);
50194
+ /**
49357
50195
  * The report of a one-shot "move this owner's plaintext secrets into the
49358
50196
  * keychain" run (D878). COUNTS only - never a value, never a ref.
49359
50197
  *
@@ -49643,6 +50481,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49643
50481
  kind: "mutation",
49644
50482
  auth: "admin",
49645
50483
  access: "view"
50484
+ }), method(object({
50485
+ deviceId: number().int(),
50486
+ cap: string$2().min(1),
50487
+ fieldPath: string$2().min(1),
50488
+ deviceType: _enum(DeviceType).optional()
50489
+ }), RunningThresholdProposalSchema, {
50490
+ auth: "admin",
50491
+ access: "view"
49646
50492
  }), method(object({
49647
50493
  blockId: string$2().min(1),
49648
50494
  trace: AutomationRunTraceSchema.optional(),
@@ -50598,7 +51444,27 @@ method(object({
50598
51444
  * exposes only the COMMON surface: link state, supported device
50599
51445
  * kinds (so the UI can filter the device picker), and per-device
50600
51446
  * expose/unexpose.
50601
- */
51447
+ *
51448
+ * INSTANCES. One export addon can serve several independent targets (a
51449
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51450
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51451
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51452
+ * default instance's); present = that instance; an id the provider does not
51453
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51454
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51455
+ * change always names the target it changes. A single-instance provider
51456
+ * accepts an absent id on a read or its own id, and refuses any other.
51457
+ */
51458
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51459
+ var InstanceIdSchema$1 = string$2().min(1);
51460
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51461
+ /** One independent export target of an addon. */
51462
+ var ExportInstanceSchema = object({
51463
+ instanceId: InstanceIdSchema$1,
51464
+ name: string$2(),
51465
+ enabled: boolean(),
51466
+ kind: string$2().optional()
51467
+ });
50602
51468
  var LinkStateSchema = _enum([
50603
51469
  "unlinked",
50604
51470
  "linked",
@@ -50649,10 +51515,14 @@ var ExposedDeviceSchema = object({
50649
51515
  capabilities: array(string$2()).optional()
50650
51516
  });
50651
51517
  var ExposeInputSchema = object({
51518
+ instanceId: InstanceIdSchema$1,
50652
51519
  deviceId: string$2(),
50653
51520
  capabilities: array(string$2()).optional()
50654
51521
  });
50655
- var UnexposeInputSchema = object({ deviceId: string$2() });
51522
+ var UnexposeInputSchema = object({
51523
+ instanceId: InstanceIdSchema$1,
51524
+ deviceId: string$2()
51525
+ });
50656
51526
  /**
50657
51527
  * What an exporter may expose besides devices (D968). An action and a
50658
51528
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50681,15 +51551,21 @@ var ExposedEntitySchema = object({
50681
51551
  entity: ExportEntityRefSchema,
50682
51552
  exposedAs: string$2().optional()
50683
51553
  });
50684
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50685
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50686
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51554
+ var ExposeEntityInputSchema = object({
51555
+ instanceId: InstanceIdSchema$1,
51556
+ entity: ExportEntityRefSchema
51557
+ });
51558
+ var UnexposeEntityInputSchema = object({
51559
+ instanceId: InstanceIdSchema$1,
51560
+ entity: ExportEntityRefSchema
51561
+ });
51562
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50687
51563
  kind: "mutation",
50688
51564
  auth: "admin"
50689
51565
  }), method(UnexposeInputSchema, _void(), {
50690
51566
  kind: "mutation",
50691
51567
  auth: "admin"
50692
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51568
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50693
51569
  kind: "mutation",
50694
51570
  auth: "admin"
50695
51571
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -52691,7 +53567,7 @@ var DishwasherStatusSchema = object({
52691
53567
  /** Ms epoch of the last value received from the appliance; 0 = never. */
52692
53568
  lastFetchedAt: number()
52693
53569
  });
52694
- var DeviceIdSchema$1 = number().int().nonnegative();
53570
+ var DeviceIdSchema$2 = number().int().nonnegative();
52695
53571
  /** A value a program option or setting can take. */
52696
53572
  var DishwasherValueSchema = union([
52697
53573
  boolean(),
@@ -52766,77 +53642,77 @@ var dishwasherCapability = {
52766
53642
  * option feature keys; each is checked against the program's constraints.
52767
53643
  */
52768
53644
  start: method(object({
52769
- deviceId: DeviceIdSchema$1,
53645
+ deviceId: DeviceIdSchema$2,
52770
53646
  program: string$2().min(1).optional(),
52771
53647
  options: record(string$2(), DishwasherValueSchema).optional()
52772
53648
  }), _void(), {
52773
53649
  kind: "mutation",
52774
53650
  auth: "protected"
52775
53651
  }),
52776
- pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53652
+ pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52777
53653
  kind: "mutation",
52778
53654
  auth: "protected"
52779
53655
  }),
52780
- resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53656
+ resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52781
53657
  kind: "mutation",
52782
53658
  auth: "protected"
52783
53659
  }),
52784
- stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53660
+ stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52785
53661
  kind: "mutation",
52786
53662
  auth: "protected"
52787
53663
  }),
52788
53664
  /** Select (not start) a program; refused when the appliance does not offer it. */
52789
53665
  setProgram: method(object({
52790
- deviceId: DeviceIdSchema$1,
53666
+ deviceId: DeviceIdSchema$2,
52791
53667
  program: string$2().min(1)
52792
53668
  }), _void(), {
52793
53669
  kind: "mutation",
52794
53670
  auth: "protected"
52795
53671
  }),
52796
53672
  setPower: method(object({
52797
- deviceId: DeviceIdSchema$1,
53673
+ deviceId: DeviceIdSchema$2,
52798
53674
  on: boolean()
52799
53675
  }), _void(), {
52800
53676
  kind: "mutation",
52801
53677
  auth: "protected"
52802
53678
  }),
52803
53679
  setHalfLoad: method(object({
52804
- deviceId: DeviceIdSchema$1,
53680
+ deviceId: DeviceIdSchema$2,
52805
53681
  on: boolean()
52806
53682
  }), _void(), {
52807
53683
  kind: "mutation",
52808
53684
  auth: "protected"
52809
53685
  }),
52810
53686
  setHygienePlus: method(object({
52811
- deviceId: DeviceIdSchema$1,
53687
+ deviceId: DeviceIdSchema$2,
52812
53688
  on: boolean()
52813
53689
  }), _void(), {
52814
53690
  kind: "mutation",
52815
53691
  auth: "protected"
52816
53692
  }),
52817
53693
  setExtraDry: method(object({
52818
- deviceId: DeviceIdSchema$1,
53694
+ deviceId: DeviceIdSchema$2,
52819
53695
  on: boolean()
52820
53696
  }), _void(), {
52821
53697
  kind: "mutation",
52822
53698
  auth: "protected"
52823
53699
  }),
52824
53700
  setIntensiveZone: method(object({
52825
- deviceId: DeviceIdSchema$1,
53701
+ deviceId: DeviceIdSchema$2,
52826
53702
  on: boolean()
52827
53703
  }), _void(), {
52828
53704
  kind: "mutation",
52829
53705
  auth: "protected"
52830
53706
  }),
52831
53707
  setVarioSpeedPlus: method(object({
52832
- deviceId: DeviceIdSchema$1,
53708
+ deviceId: DeviceIdSchema$2,
52833
53709
  on: boolean()
52834
53710
  }), _void(), {
52835
53711
  kind: "mutation",
52836
53712
  auth: "protected"
52837
53713
  }),
52838
53714
  setChildLock: method(object({
52839
- deviceId: DeviceIdSchema$1,
53715
+ deviceId: DeviceIdSchema$2,
52840
53716
  on: boolean()
52841
53717
  }), _void(), {
52842
53718
  kind: "mutation",
@@ -52848,7 +53724,7 @@ var dishwasherCapability = {
52848
53724
  * its declared range / members.
52849
53725
  */
52850
53726
  setOption: method(object({
52851
- deviceId: DeviceIdSchema$1,
53727
+ deviceId: DeviceIdSchema$2,
52852
53728
  key: string$2().min(1),
52853
53729
  value: DishwasherValueSchema
52854
53730
  }), _void(), {
@@ -52856,7 +53732,7 @@ var dishwasherCapability = {
52856
53732
  auth: "protected"
52857
53733
  }),
52858
53734
  /** What this appliance can do, from its own description — the UI is derived from it. */
52859
- getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
53735
+ getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
52860
53736
  },
52861
53737
  status: {
52862
53738
  schema: DishwasherStatusSchema,
@@ -54280,15 +55156,15 @@ var FanPresetCaptureResultSchema = object({
54280
55156
  items: array(FanCaptureItemSchema),
54281
55157
  omitted: array(FanCaptureOmissionSchema)
54282
55158
  });
54283
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55159
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
54284
55160
  DeviceType.Fan;
54285
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55161
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54286
55162
  kind: "mutation",
54287
55163
  auth: "admin"
54288
- }), method(deviceInput$1.extend({ presetId: string$2().min(1) }), FanPresetApplyResultSchema, {
55164
+ }), method(deviceInput$2.extend({ presetId: string$2().min(1) }), FanPresetApplyResultSchema, {
54289
55165
  kind: "mutation",
54290
55166
  auth: "protected"
54291
- }), method(deviceInput$1.extend({
55167
+ }), method(deviceInput$2.extend({
54292
55168
  fields: array(FanCaptureFieldSchema).optional(),
54293
55169
  steps: array(ActionStepSchema).max(20).default([])
54294
55170
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -55199,13 +56075,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string$2
55199
56075
  */
55200
56076
  var ConfigSchemaPassthrough$3 = custom();
55201
56077
  var IntegrationInstanceIdSchema = string$2().min(1);
56078
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56079
+ /**
56080
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56081
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56082
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56083
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56084
+ * instance id.
56085
+ */
56086
+ var IntegrationInstanceKindSchema = object({
56087
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56088
+ id: string$2().min(1),
56089
+ label: string$2().min(1),
56090
+ /** The kind signs in through a server-held interactive session. */
56091
+ signIn: object({
56092
+ basePath: string$2().min(1),
56093
+ methods: array(_enum(["device-code", "proxy"])).optional()
56094
+ }).optional(),
56095
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56096
+ connectPath: string$2().min(1).optional(),
56097
+ /** Relative hub path to disconnect that account. */
56098
+ disconnectPath: string$2().min(1).optional()
56099
+ });
55202
56100
  var IntegrationInstanceDescriptionSchema = object({
55203
56101
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55204
- instanceMode: _enum(["single", "multiple"]),
56102
+ instanceMode: IntegrationInstanceModeSchema,
55205
56103
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55206
56104
  kind: string$2(),
55207
56105
  /** True when `testInstance` really probes the remote. */
55208
- canTest: boolean()
56106
+ canTest: boolean(),
56107
+ /**
56108
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56109
+ * Add flow asks which first and sends it as `createInstance.kind`.
56110
+ */
56111
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56112
+ /**
56113
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56114
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56115
+ */
56116
+ allowRestoreId: boolean().optional(),
56117
+ /**
56118
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56119
+ * send an id (the UI then mints an opaque one).
56120
+ */
56121
+ mintsId: boolean().optional()
55209
56122
  });
55210
56123
  var IntegrationInstanceStatusSchema = object({
55211
56124
  state: _enum([
@@ -55214,16 +56127,27 @@ var IntegrationInstanceStatusSchema = object({
55214
56127
  "error",
55215
56128
  "unknown"
55216
56129
  ]),
55217
- detail: string$2().optional()
56130
+ detail: string$2().optional(),
56131
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56132
+ label: string$2().optional()
55218
56133
  });
55219
56134
  var IntegrationInstanceSchema = object({
55220
56135
  id: IntegrationInstanceIdSchema,
55221
56136
  name: string$2(),
55222
56137
  /** The addon's own rule (an URL is present, a client id is present...). */
55223
56138
  configured: boolean(),
56139
+ /**
56140
+ * The instance's own on/off switch (D996): always present, derived from the record body
56141
+ * (`enabled !== false`, so a row written before the field existed is on).
56142
+ */
56143
+ enabled: boolean(),
56144
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56145
+ kind: string$2().optional(),
55224
56146
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55225
56147
  formValues: record(string$2(), unknown()),
55226
- status: IntegrationInstanceStatusSchema.optional()
56148
+ status: IntegrationInstanceStatusSchema.optional(),
56149
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56150
+ lastCheckedAt: number().optional()
55227
56151
  });
55228
56152
  var IntegrationInstancePatchSchema = object({
55229
56153
  id: IntegrationInstanceIdSchema,
@@ -55231,25 +56155,57 @@ var IntegrationInstancePatchSchema = object({
55231
56155
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55232
56156
  values: record(string$2(), unknown())
55233
56157
  });
55234
- 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({
55235
- id: IntegrationInstanceIdSchema,
55236
- name: string$2().min(1)
55237
- }), object({ id: string$2() }), {
55238
- kind: "mutation",
55239
- auth: "admin"
55240
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55241
- kind: "mutation",
55242
- auth: "admin"
55243
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55244
- kind: "mutation",
55245
- auth: "admin"
55246
- }), optionalMethod(object({
55247
- id: IntegrationInstanceIdSchema.nullable(),
55248
- values: record(string$2(), unknown())
55249
- }), TestConnectionResultSchema$1, {
55250
- kind: "mutation",
55251
- auth: "admin"
55252
- });
56158
+ var integrationInstancesCapability = {
56159
+ name: "integration-instances",
56160
+ scope: "system",
56161
+ mode: "collection",
56162
+ methods: {
56163
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56164
+ /** The form; `id: null` is the create form. */
56165
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56166
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56167
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56168
+ /**
56169
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56170
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56171
+ */
56172
+ createInstance: method(object({
56173
+ id: IntegrationInstanceIdSchema.optional(),
56174
+ name: string$2().min(1),
56175
+ kind: string$2().min(1).optional()
56176
+ }), object({ id: string$2() }), {
56177
+ kind: "mutation",
56178
+ auth: "admin"
56179
+ }),
56180
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56181
+ kind: "mutation",
56182
+ auth: "admin"
56183
+ }),
56184
+ /**
56185
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56186
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56187
+ */
56188
+ setInstanceEnabled: method(object({
56189
+ id: IntegrationInstanceIdSchema,
56190
+ enabled: boolean()
56191
+ }), object({ ok: literal(true) }), {
56192
+ kind: "mutation",
56193
+ auth: "admin"
56194
+ }),
56195
+ /** Delete the record and its keychain refs together. */
56196
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56197
+ kind: "mutation",
56198
+ auth: "admin"
56199
+ }),
56200
+ testInstance: optionalMethod(object({
56201
+ id: IntegrationInstanceIdSchema.nullable(),
56202
+ values: record(string$2(), unknown())
56203
+ }), TestConnectionResultSchema$1, {
56204
+ kind: "mutation",
56205
+ auth: "admin"
56206
+ })
56207
+ }
56208
+ };
55253
56209
  /**
55254
56210
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55255
56211
  * session (server-initiated offer pattern, identical to the main
@@ -57267,6 +58223,11 @@ var LogEntrySchema = object({
57267
58223
  * flight, and not asking for the next slice ends the scan.
57268
58224
  */
57269
58225
  var LogReadQuerySchema = object({
58226
+ /**
58227
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58228
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58229
+ */
58230
+ instanceId: string$2().min(1).max(200).optional(),
57270
58231
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57271
58232
  requestId: string$2().min(1).max(64).optional(),
57272
58233
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -57329,6 +58290,8 @@ var LogReadResultSchema = object({
57329
58290
  /** True when `cancelRead` ended this slice early. */
57330
58291
  cancelled: boolean().optional()
57331
58292
  });
58293
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58294
+ var LogInstanceSelectorSchema = object({ instanceId: string$2().min(1).max(200).optional() });
57332
58295
  var LogReadCapabilitiesSchema = object({
57333
58296
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
57334
58297
  readable: boolean(),
@@ -57347,7 +58310,7 @@ var LogReadCapabilitiesSchema = object({
57347
58310
  /** How far back the store reaches, when the provider knows (days). */
57348
58311
  retentionDays: number().nullable()
57349
58312
  });
57350
- 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() }), {
58313
+ 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() }), {
57351
58314
  kind: "mutation",
57352
58315
  auth: "admin"
57353
58316
  });
@@ -57451,214 +58414,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
57451
58414
  ]);
57452
58415
  method(_void(), array(LoginMethodContributionSchema).readonly());
57453
58416
  /**
57454
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
57455
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
57456
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
57457
- * surface — those use `DeviceType.Camera` + the broker pipeline.
57458
- *
57459
- * The cap declares a wide method surface; not every backend supports
57460
- * every method. Drivers signal sub-capability via the
57461
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
57462
- * corresponding method) so the UI gates buttons without a slice
57463
- * fetch — same pattern as `PanTiltZoom` for cameras.
57464
- */
57465
- var MediaPlayerStateSchema = _enum([
57466
- "off",
57467
- "on",
57468
- "idle",
57469
- "playing",
57470
- "paused",
57471
- "buffering",
57472
- "standby"
57473
- ]);
57474
- var MediaPlayerRepeatSchema = _enum([
57475
- "off",
57476
- "all",
57477
- "one"
57478
- ]);
57479
- var MediaInfoSchema = object({
57480
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
57481
- * `podcast`, …). */
57482
- type: string$2(),
57483
- /** Human-readable title. */
57484
- title: string$2(),
57485
- /** Optional artist / channel / station label. */
57486
- artist: string$2().optional(),
57487
- /** Optional album / season / show name. */
57488
- album: string$2().optional(),
57489
- /** Optional cover-art / thumbnail URL. */
57490
- imageUrl: string$2().optional()
57491
- });
57492
- /**
57493
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
57494
- * the device said so. `assumed`: the driver has no feedback (infrared) and
57495
- * tracks what it last SENT; it moves only after a send was accepted, and a
57496
- * human at the real remote makes it wrong. A UI shows an assumed value as a
57497
- * guess, never as a reading.
57498
- */
57499
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
57500
- /** The fields of a media-player status a driver can only guess. */
57501
- var MediaPlayerAssumedFieldSchema = _enum([
57502
- "state",
57503
- "volumeLevel",
57504
- "isMuted",
57505
- "source"
57506
- ]);
57507
- var MediaPlayerStatusSchema = object({
57508
- /** Playback lifecycle state. */
57509
- state: MediaPlayerStateSchema,
57510
- /**
57511
- * Whether the state fields are readings or the driver's own bookkeeping.
57512
- * A slice written before this field existed parses as `reported`.
57513
- */
57514
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
57515
- /**
57516
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
57517
- * this is not empty; a field not listed here is a reading even then (power
57518
- * from a running-state source, mute guessed from the last send).
57519
- */
57520
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
57521
- /** Volume as 0..100 inclusive. Null when the device has no volume
57522
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
57523
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57524
- /** Mute toggle distinct from volume=0. Null when no mute surface.
57525
- * Pair with `DeviceFeature.MediaPlayerMute`. */
57526
- isMuted: boolean().nullable(),
57527
- /** Active source / input. Empty when no source surface. */
57528
- source: string$2(),
57529
- /** Selectable sources. Empty when no source surface.
57530
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
57531
- availableSources: array(string$2()),
57532
- /** Currently-playing media info. Null when nothing is playing. */
57533
- currentMedia: MediaInfoSchema.nullable(),
57534
- /** Current playback position in ms. Null when not seekable or
57535
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
57536
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57537
- /** Total duration of the current media in ms. Null when unknown
57538
- * (live stream) or nothing is playing. */
57539
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57540
- /** Shuffle toggle. Null when no shuffle surface.
57541
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
57542
- shuffle: boolean().nullable(),
57543
- /** Repeat mode. Null when no repeat surface.
57544
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
57545
- repeat: MediaPlayerRepeatSchema.nullable(),
57546
- /** Ms epoch when the slice was last updated. */
57547
- lastChangedAt: number()
57548
- });
57549
- var mediaPlayerCapability = {
57550
- name: "media-player",
57551
- scope: "device",
57552
- deviceNative: true,
57553
- mode: "singleton",
57554
- deviceTypes: [DeviceType.MediaPlayer],
57555
- methods: {
57556
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57557
- kind: "mutation",
57558
- auth: "protected"
57559
- }),
57560
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57561
- kind: "mutation",
57562
- auth: "protected"
57563
- }),
57564
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57565
- kind: "mutation",
57566
- auth: "protected"
57567
- }),
57568
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57569
- kind: "mutation",
57570
- auth: "protected"
57571
- }),
57572
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57573
- kind: "mutation",
57574
- auth: "protected"
57575
- }),
57576
- seek: method(object({
57577
- deviceId: number().int().nonnegative(),
57578
- position: number().int().nonnegative()
57579
- }), _void(), {
57580
- kind: "mutation",
57581
- auth: "protected"
57582
- }),
57583
- setVolume: method(object({
57584
- deviceId: number().int().nonnegative(),
57585
- volumeLevel: number().min(0).max(100)
57586
- }), _void(), {
57587
- kind: "mutation",
57588
- auth: "protected"
57589
- }),
57590
- setMute: method(object({
57591
- deviceId: number().int().nonnegative(),
57592
- muted: boolean()
57593
- }), _void(), {
57594
- kind: "mutation",
57595
- auth: "protected"
57596
- }),
57597
- setShuffle: method(object({
57598
- deviceId: number().int().nonnegative(),
57599
- shuffle: boolean()
57600
- }), _void(), {
57601
- kind: "mutation",
57602
- auth: "protected"
57603
- }),
57604
- setRepeat: method(object({
57605
- deviceId: number().int().nonnegative(),
57606
- repeat: MediaPlayerRepeatSchema
57607
- }), _void(), {
57608
- kind: "mutation",
57609
- auth: "protected"
57610
- }),
57611
- selectSource: method(object({
57612
- deviceId: number().int().nonnegative(),
57613
- source: string$2().min(1)
57614
- }), _void(), {
57615
- kind: "mutation",
57616
- auth: "protected"
57617
- }),
57618
- playMedia: method(object({
57619
- deviceId: number().int().nonnegative(),
57620
- /** Media identifier / URL. */
57621
- mediaId: string$2().min(1),
57622
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
57623
- * passes it through to the upstream service. */
57624
- mediaType: string$2().min(1)
57625
- }), _void(), {
57626
- kind: "mutation",
57627
- auth: "protected"
57628
- })
57629
- },
57630
- status: {
57631
- schema: MediaPlayerStatusSchema,
57632
- kind: "push"
57633
- },
57634
- /**
57635
- * Runtime-state slice — mirrored by the kernel. UI players read the
57636
- * full slice for live now-playing, volume, and progress updates
57637
- * without polling.
57638
- */
57639
- runtimeState: MediaPlayerStatusSchema,
57640
- /**
57641
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
57642
- *
57643
- * See `RuntimeStateDurability`. Enforced by
57644
- * `scripts/check-runtime-state-durability.ts`.
57645
- */
57646
- durability: "session",
57647
- /**
57648
- * History (D850): volume (a level an operator or an automation sets; the
57649
- * default 10 s floor folds a slider drag into its end value) and mute.
57650
- * `position` is not recorded: it moves continuously while playing.
57651
- */
57652
- history: { fields: {
57653
- volumeLevel: {
57654
- kind: "gauge",
57655
- unit: "%",
57656
- deadband: 1
57657
- },
57658
- isMuted: { kind: "boolean" }
57659
- } }
57660
- };
57661
- /**
57662
58417
  * mesh-network — collection cap for mesh-VPN providers.
57663
58418
  *
57664
58419
  * Different shape from `network-access` (public tunnels):
@@ -66417,6 +67172,21 @@ var NcTemplateVarKindSchema = _enum([
66417
67172
  "sound",
66418
67173
  "system"
66419
67174
  ]);
67175
+ /**
67176
+ * The default title/body a rule of one SITUATION is delivered with when the
67177
+ * operator wrote no template — served so an editor can pre-fill it (and the
67178
+ * operator can rebuild it from placeholders) instead of starting from a blank
67179
+ * box that, once saved, silently replaces it. `id` names the situation inside
67180
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
67181
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
67182
+ * the hub's current language and with its overrides applied.
67183
+ */
67184
+ var NcTemplateDefaultSchema = object({
67185
+ id: _enum(["appliance-cycle"]),
67186
+ kind: NcRuleKindSchema,
67187
+ title: string$2(),
67188
+ body: string$2()
67189
+ });
66420
67190
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
66421
67191
  var NcTemplateFamilySchema = _enum([
66422
67192
  "rule",
@@ -67272,7 +68042,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
67272
68042
  }), method(object({}), object({
67273
68043
  catalog: array(NcConditionDescriptorSchema),
67274
68044
  taxonomy: NcTaxonomySchema.optional()
67275
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
68045
+ })), method(object({}), object({
68046
+ vars: array(NcTemplateVarDescriptorSchema),
68047
+ /** Per-situation default texts; absent on a hub that predates them. */
68048
+ defaults: array(NcTemplateDefaultSchema)
68049
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
67276
68050
  kind: "mutation",
67277
68051
  access: "view"
67278
68052
  }), 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 }), {
@@ -68168,7 +68942,7 @@ var petFeederCapability = {
68168
68942
  /**
68169
68943
  * Pet water-fountain cap. Models the telemetry and the control surface of a
68170
68944
  * circulating drinking fountain: how much water is in the tank, how much the
68171
- * pet drank, whether the pump runs and on what duty cycle.
68945
+ * pet drank, whether the pump runs.
68172
68946
  *
68173
68947
  * It has no native vendor provider of its own: a fountain reaches CamStack as
68174
68948
  * a container of loose entities (a pump switch, two number boxes, a "filter
@@ -68184,8 +68958,10 @@ var petFeederCapability = {
68184
68958
  * never here. Every reading may be `null`: a source that is offline or does
68185
68959
  * not report is unknown, never zero.
68186
68960
  *
68187
- * The filter is not a field of this cap: it is an item of the `consumables`
68188
- * cap on the same device (kind `wear`), reset by the same forward mechanism.
68961
+ * The pump's on/off duty cycle is model-specific and lives in the optional
68962
+ * `pump-cycle` cap. The filter, the cleaning and the water change are not
68963
+ * fields of this cap: they are items of the `consumables` cap on the same
68964
+ * device (kind `wear`), reset by the same forward mechanism.
68189
68965
  */
68190
68966
  var PetFountainStatusSchema = object({
68191
68967
  /** Water in the tank, 0..100 (%). Null while the source has not reported. */
@@ -68196,27 +68972,20 @@ var PetFountainStatusSchema = object({
68196
68972
  empty: boolean().nullable(),
68197
68973
  /** True while the pump is running. Null while unknown. */
68198
68974
  pumpRunning: boolean().nullable(),
68199
- /** True when the pump works on an on/off duty cycle rather than continuously. */
68200
- pumpIntervalEnabled: boolean().nullable(),
68201
- /** How long the pump runs per cycle, in seconds. */
68202
- pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
68203
- /** How long the pump rests per cycle, in seconds. */
68204
- pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
68205
68975
  /** Water drunk in the last drink, in litres. */
68206
68976
  lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
68207
68977
  /** Water drunk in the current hour, in litres. */
68208
68978
  currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
68209
68979
  /** Water drunk over the last 24 hours (rolling), in litres. */
68210
68980
  last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
68211
- /** Water drunk since local midnight, in litres. */
68981
+ /** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
68212
68982
  todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
68213
68983
  /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
68214
68984
  error: string$2().nullable(),
68215
68985
  /** Ms epoch when the slice was last refreshed. */
68216
68986
  lastFetchedAt: number()
68217
68987
  });
68218
- var DeviceIdSchema = number().int().nonnegative();
68219
- var MinutesSchema = number().int().min(0);
68988
+ var DeviceIdSchema$1 = number().int().nonnegative();
68220
68989
  var petFountainCapability = {
68221
68990
  name: "pet-fountain",
68222
68991
  scope: "device",
@@ -68226,38 +68995,14 @@ var petFountainCapability = {
68226
68995
  methods: {
68227
68996
  /** Start or stop the pump. */
68228
68997
  setPump: method(object({
68229
- deviceId: DeviceIdSchema,
68230
- on: boolean()
68231
- }), _void(), {
68232
- kind: "mutation",
68233
- auth: "protected"
68234
- }),
68235
- /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
68236
- setPumpInterval: method(object({
68237
- deviceId: DeviceIdSchema,
68998
+ deviceId: DeviceIdSchema$1,
68238
68999
  on: boolean()
68239
69000
  }), _void(), {
68240
69001
  kind: "mutation",
68241
69002
  auth: "protected"
68242
69003
  }),
68243
- /** Set how many minutes the pump runs per cycle. */
68244
- setPumpOnMinutes: method(object({
68245
- deviceId: DeviceIdSchema,
68246
- minutes: MinutesSchema
68247
- }), _void(), {
68248
- kind: "mutation",
68249
- auth: "protected"
68250
- }),
68251
- /** Set how many minutes the pump rests per cycle. */
68252
- setPumpOffMinutes: method(object({
68253
- deviceId: DeviceIdSchema,
68254
- minutes: MinutesSchema
68255
- }), _void(), {
68256
- kind: "mutation",
68257
- auth: "protected"
68258
- }),
68259
69004
  /** Clear the rolling daily consumption counters on the device. */
68260
- resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69005
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
68261
69006
  kind: "mutation",
68262
69007
  auth: "protected"
68263
69008
  })
@@ -69892,6 +70637,14 @@ var powerMeterCapability = {
69892
70637
  deviceNative: true,
69893
70638
  mode: "singleton",
69894
70639
  deviceTypes: POWER_METER_DEVICE_TYPES,
70640
+ deviceConfig: { ui: {
70641
+ kind: "widget",
70642
+ widgetId: "host/power-meter-line",
70643
+ tab: "power",
70644
+ placement: "hero",
70645
+ label: "Power",
70646
+ order: 4
70647
+ } },
69895
70648
  methods: {},
69896
70649
  status: {
69897
70650
  schema: PowerMeterStatusSchema,
@@ -69951,6 +70704,117 @@ var powerMeterCapability = {
69951
70704
  volatileStateFields: ["lastFetchedAt"],
69952
70705
  customize: powerMeterCustomize
69953
70706
  };
70707
+ var PowerSourceRefusalReasonSchema = _enum([
70708
+ "no-power-source",
70709
+ "switch-missing",
70710
+ "switch-offline",
70711
+ "switch-unknown",
70712
+ "restart-in-progress",
70713
+ "switch-call-failed"
70714
+ ]);
70715
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
70716
+ accepted: literal(false),
70717
+ reason: PowerSourceRefusalReasonSchema,
70718
+ /** One line for the operator, naming the switch. */
70719
+ message: string$2()
70720
+ })]);
70721
+ var PowerSourceInfoSchema = object({
70722
+ /** False when the device has no power source linked; the rest is then empty. */
70723
+ linked: boolean(),
70724
+ switchDeviceId: number().nullable(),
70725
+ switchName: string$2().nullable(),
70726
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
70727
+ switchOn: boolean().nullable(),
70728
+ switchOnline: boolean().nullable(),
70729
+ restartInProgress: boolean(),
70730
+ offSeconds: number().int()
70731
+ });
70732
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
70733
+ var PowerSourceStateSchema = object({
70734
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
70735
+ switchOn: boolean().nullable(),
70736
+ offSeconds: number().int(),
70737
+ lastFetchedAt: number()
70738
+ });
70739
+ var POWER_SOURCE_DEVICE_TYPES = [
70740
+ DeviceType.Gateway,
70741
+ DeviceType.Hub,
70742
+ DeviceType.Camera,
70743
+ DeviceType.Sensor,
70744
+ DeviceType.MediaPlayer,
70745
+ DeviceType.Vacuum,
70746
+ DeviceType.Generic,
70747
+ DeviceType.Washer,
70748
+ DeviceType.Dryer,
70749
+ DeviceType.Dishwasher,
70750
+ DeviceType.PetFountain,
70751
+ DeviceType.Fan,
70752
+ DeviceType.Humidifier,
70753
+ DeviceType.Climate,
70754
+ DeviceType.WaterHeater
70755
+ ];
70756
+ var powerSourceCustomize = {
70757
+ version: 1,
70758
+ targets: [{
70759
+ id: "switch",
70760
+ cardinality: "scalar",
70761
+ vocabulary: {
70762
+ cap: "power-source",
70763
+ stateField: "switchOn"
70764
+ },
70765
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70766
+ apply: "add",
70767
+ path: "switchOn",
70768
+ sources: [{
70769
+ kind: "switch",
70770
+ transforms: [{ kind: "identity" }]
70771
+ }],
70772
+ companions: [{
70773
+ kind: "constant",
70774
+ path: "offSeconds",
70775
+ value: 10,
70776
+ when: "add"
70777
+ }, {
70778
+ kind: "clock",
70779
+ path: "lastFetchedAt",
70780
+ when: "always"
70781
+ }],
70782
+ sourceReport: { status: "assignment-status" }
70783
+ }]
70784
+ };
70785
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
70786
+ var powerSourceCapability = {
70787
+ name: "power-source",
70788
+ scope: "device",
70789
+ mode: "singleton",
70790
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70791
+ methods: {
70792
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
70793
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70794
+ kind: "mutation",
70795
+ auth: "admin"
70796
+ }),
70797
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70798
+ kind: "mutation",
70799
+ auth: "admin"
70800
+ }),
70801
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70802
+ kind: "mutation",
70803
+ auth: "admin"
70804
+ })
70805
+ },
70806
+ runtimeState: PowerSourceStateSchema,
70807
+ customize: powerSourceCustomize,
70808
+ /**
70809
+ * Runtime-state durability: **session** — mirrored from the linked switch on
70810
+ * its next report; a restored value would outlive the reading it names.
70811
+ *
70812
+ * See `RuntimeStateDurability`. Enforced by
70813
+ * `scripts/check-runtime-state-durability.ts`.
70814
+ */
70815
+ durability: "session",
70816
+ history: "none"
70817
+ };
69954
70818
  /**
69955
70819
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
69956
70820
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -70590,6 +71454,79 @@ var ptzAutotrackCapability = {
70590
71454
  */
70591
71455
  history: "none"
70592
71456
  };
71457
+ /**
71458
+ * The on/off duty cycle of a pet fountain's pump. Some models run the pump
71459
+ * continuously and have nothing to configure; others can alternate a run time
71460
+ * and a rest time. That is model-specific, so it is its own optional cap on the
71461
+ * fountain device: a fountain without cycle entities simply never composes it
71462
+ * (planning#366), and the `pet-fountain` cap stays the model-independent core.
71463
+ *
71464
+ * Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
71465
+ * cycle from loose entities (a cycle switch and two number boxes) and every
71466
+ * method is a forward to one source entity (D659, D967). Durations are stored
71467
+ * in SECONDS (D944); a number box that cannot be read back leaves them `null`
71468
+ * — unknown, never zero (D393).
71469
+ */
71470
+ var PumpCycleStatusSchema = object({
71471
+ /** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
71472
+ enabled: boolean().nullable(),
71473
+ /** How long the pump runs per cycle, in seconds. Null while unknown. */
71474
+ onTime: number().min(0).nullable().meta(readingMeta("duration")),
71475
+ /** How long the pump rests per cycle, in seconds. Null while unknown. */
71476
+ offTime: number().min(0).nullable().meta(readingMeta("duration")),
71477
+ /** Ms epoch when the slice was last refreshed. */
71478
+ lastFetchedAt: number()
71479
+ });
71480
+ var DeviceIdSchema = number().int().nonnegative();
71481
+ var MinutesSchema = number().int().min(0);
71482
+ var pumpCycleCapability = {
71483
+ name: "pump-cycle",
71484
+ scope: "device",
71485
+ deviceNative: true,
71486
+ mode: "singleton",
71487
+ deviceTypes: [DeviceType.PetFountain],
71488
+ methods: {
71489
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
71490
+ setEnabled: method(object({
71491
+ deviceId: DeviceIdSchema,
71492
+ on: boolean()
71493
+ }), _void(), {
71494
+ kind: "mutation",
71495
+ auth: "protected"
71496
+ }),
71497
+ /** Set how many minutes the pump runs per cycle. */
71498
+ setOnMinutes: method(object({
71499
+ deviceId: DeviceIdSchema,
71500
+ minutes: MinutesSchema
71501
+ }), _void(), {
71502
+ kind: "mutation",
71503
+ auth: "protected"
71504
+ }),
71505
+ /** Set how many minutes the pump rests per cycle. */
71506
+ setOffMinutes: method(object({
71507
+ deviceId: DeviceIdSchema,
71508
+ minutes: MinutesSchema
71509
+ }), _void(), {
71510
+ kind: "mutation",
71511
+ auth: "protected"
71512
+ })
71513
+ },
71514
+ status: {
71515
+ schema: PumpCycleStatusSchema,
71516
+ kind: "push"
71517
+ },
71518
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
71519
+ runtimeState: PumpCycleStatusSchema,
71520
+ /**
71521
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
71522
+ *
71523
+ * See `RuntimeStateDurability`. Enforced by
71524
+ * `scripts/check-runtime-state-durability.ts`.
71525
+ */
71526
+ durability: "session",
71527
+ /** History (D850): a duty-cycle setting is not a series worth recording. */
71528
+ history: "none"
71529
+ };
70593
71530
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
70594
71531
  kind: "mutation",
70595
71532
  auth: "admin"
@@ -73380,7 +74317,13 @@ var RemoteMediaCommandSchema = _enum([
73380
74317
  "next",
73381
74318
  "previous",
73382
74319
  "setMute",
73383
- "selectSource"
74320
+ "selectSource",
74321
+ "turnOn",
74322
+ "turnOff",
74323
+ "volumeUp",
74324
+ "volumeDown",
74325
+ "channelUp",
74326
+ "channelDown"
73384
74327
  ]);
73385
74328
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
73386
74329
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -73401,7 +74344,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
73401
74344
  }), RemoteSendResultSchema, {
73402
74345
  kind: "mutation",
73403
74346
  auth: "protected"
73404
- }), method(deviceInput.extend({
74347
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
73405
74348
  command: RemoteMediaCommandSchema,
73406
74349
  muted: boolean().optional(),
73407
74350
  source: string$2().optional(),
@@ -75647,7 +76590,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
75647
76590
  * `getSettingsUISchema()` surface.
75648
76591
  */
75649
76592
  var SwitchStatusSchema = object({
75650
- on: boolean().meta({ label: "On" }),
76593
+ /**
76594
+ * `null` = the device has never reported its state (a measurement that
76595
+ * has not happened is unknown, never `false`).
76596
+ */
76597
+ on: boolean().nullable().meta({ label: "On" }),
75651
76598
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
75652
76599
  lastChangedAt: number()
75653
76600
  });
@@ -75672,7 +76619,7 @@ var switchCapability = {
75672
76619
  }) },
75673
76620
  events: { onStateChanged: { data: object({
75674
76621
  deviceId: number(),
75675
- on: boolean(),
76622
+ on: boolean().nullable(),
75676
76623
  lastChangedAt: number()
75677
76624
  }) } },
75678
76625
  status: {
@@ -79229,6 +80176,26 @@ function buildDescriptor(capName, kind) {
79229
80176
  * contributes one entry per bound cap present in this map.
79230
80177
  */
79231
80178
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80179
+ /**
80180
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80181
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80182
+ * client resolves its name from `label` (English and Italian side by side) and
80183
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80184
+ * server sends an id, a client maps it to its own icon set.
80185
+ *
80186
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80187
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80188
+ * to `icon`.
80189
+ */
80190
+ function digit(n) {
80191
+ return {
80192
+ label: t(n, n),
80193
+ icon: "hash",
80194
+ legend: n
80195
+ };
80196
+ }
80197
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
80198
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79232
80199
  var btn = (idSuffix, row, col, init) => ({
79233
80200
  idSuffix,
79234
80201
  row,
@@ -79355,6 +80322,34 @@ discriminatedUnion("kind", [object({
79355
80322
  kind: literal("pronto"),
79356
80323
  code: string$2()
79357
80324
  })]);
80325
+ /**
80326
+ * Propose a running threshold from a device's own power history (planning#405).
80327
+ *
80328
+ * Pure and deterministic: the same series gives the same answer. The series is
80329
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80330
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80331
+ * count of samples.
80332
+ *
80333
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80334
+ * near F. A device that is not near one level most of the time has no floor.
80335
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80336
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80337
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80338
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80339
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80340
+ * covers the longest standby blip above T.
80341
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80342
+ * run must hold exactly one true interval and nothing may flap outside.
80343
+ */
80344
+ var SECOND_MS = 1e3;
80345
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80346
+ 24 * (60 * MINUTE_MS$1);
80347
+ 10 * MINUTE_MS$1;
80348
+ 30 * SECOND_MS;
80349
+ 60 * SECOND_MS;
80350
+ 20 * MINUTE_MS$1;
80351
+ 10 * SECOND_MS;
80352
+ 10 * SECOND_MS;
79358
80353
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79359
80354
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79360
80355
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -79715,6 +80710,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79715
80710
  pressureSensor: pressureSensorCapability,
79716
80711
  privacyMask: privacyMaskCapability,
79717
80712
  ptzAutotrack: ptzAutotrackCapability,
80713
+ pumpCycle: pumpCycleCapability,
79718
80714
  recordingOnboard: recordingOnboardCapability,
79719
80715
  recordingSignal: recordingSignalCapability,
79720
80716
  runningState: runningStateCapability,
@@ -81899,6 +82895,27 @@ Object.freeze({
81899
82895
  access: "view",
81900
82896
  role: "viewer"
81901
82897
  },
82898
+ "boardKeySource.getBoardKey": {
82899
+ capName: "board-key-source",
82900
+ capScope: "system",
82901
+ addonId: null,
82902
+ access: "view",
82903
+ role: "admin"
82904
+ },
82905
+ "boardKeySource.listBoards": {
82906
+ capName: "board-key-source",
82907
+ capScope: "system",
82908
+ addonId: null,
82909
+ access: "view",
82910
+ role: "admin"
82911
+ },
82912
+ "boardKeySource.listSources": {
82913
+ capName: "board-key-source",
82914
+ capScope: "system",
82915
+ addonId: null,
82916
+ access: "view",
82917
+ role: "admin"
82918
+ },
81902
82919
  "brightness.getStatus": {
81903
82920
  capName: "brightness",
81904
82921
  capScope: "device",
@@ -82438,6 +83455,13 @@ Object.freeze({
82438
83455
  access: "create",
82439
83456
  role: "admin"
82440
83457
  },
83458
+ "coreBlocks.proposeRunningThreshold": {
83459
+ capName: "core-blocks",
83460
+ capScope: "system",
83461
+ addonId: null,
83462
+ access: "view",
83463
+ role: "admin"
83464
+ },
82441
83465
  "coreBlocks.reportAutomationRun": {
82442
83466
  capName: "core-blocks",
82443
83467
  capScope: "system",
@@ -82949,6 +83973,13 @@ Object.freeze({
82949
83973
  access: "view",
82950
83974
  role: "admin"
82951
83975
  },
83976
+ "deviceExport.listInstances": {
83977
+ capName: "device-export",
83978
+ capScope: "system",
83979
+ addonId: null,
83980
+ access: "view",
83981
+ role: "admin"
83982
+ },
82952
83983
  "deviceExport.listSupportedDeviceKinds": {
82953
83984
  capName: "device-export",
82954
83985
  capScope: "system",
@@ -84580,6 +85611,13 @@ Object.freeze({
84580
85611
  access: "view",
84581
85612
  role: "admin"
84582
85613
  },
85614
+ "integrationInstances.setInstanceEnabled": {
85615
+ capName: "integration-instances",
85616
+ capScope: "system",
85617
+ addonId: null,
85618
+ access: "create",
85619
+ role: "admin"
85620
+ },
84583
85621
  "integrationInstances.testInstance": {
84584
85622
  capName: "integration-instances",
84585
85623
  capScope: "system",
@@ -85378,6 +86416,20 @@ Object.freeze({
85378
86416
  access: "view",
85379
86417
  role: "admin"
85380
86418
  },
86419
+ "mediaPlayer.channelDown": {
86420
+ capName: "media-player",
86421
+ capScope: "device",
86422
+ addonId: null,
86423
+ access: "create",
86424
+ role: "viewer"
86425
+ },
86426
+ "mediaPlayer.channelUp": {
86427
+ capName: "media-player",
86428
+ capScope: "device",
86429
+ addonId: null,
86430
+ access: "create",
86431
+ role: "viewer"
86432
+ },
85381
86433
  "mediaPlayer.getStatus": {
85382
86434
  capName: "media-player",
85383
86435
  capScope: "device",
@@ -85469,6 +86521,34 @@ Object.freeze({
85469
86521
  access: "create",
85470
86522
  role: "viewer"
85471
86523
  },
86524
+ "mediaPlayer.turnOff": {
86525
+ capName: "media-player",
86526
+ capScope: "device",
86527
+ addonId: null,
86528
+ access: "create",
86529
+ role: "viewer"
86530
+ },
86531
+ "mediaPlayer.turnOn": {
86532
+ capName: "media-player",
86533
+ capScope: "device",
86534
+ addonId: null,
86535
+ access: "create",
86536
+ role: "viewer"
86537
+ },
86538
+ "mediaPlayer.volumeDown": {
86539
+ capName: "media-player",
86540
+ capScope: "device",
86541
+ addonId: null,
86542
+ access: "create",
86543
+ role: "viewer"
86544
+ },
86545
+ "mediaPlayer.volumeUp": {
86546
+ capName: "media-player",
86547
+ capScope: "device",
86548
+ addonId: null,
86549
+ access: "create",
86550
+ role: "viewer"
86551
+ },
85472
86552
  "meshNetwork.getStatus": {
85473
86553
  capName: "mesh-network",
85474
86554
  capScope: "system",
@@ -86715,27 +87795,6 @@ Object.freeze({
86715
87795
  access: "create",
86716
87796
  role: "viewer"
86717
87797
  },
86718
- "petFountain.setPumpInterval": {
86719
- capName: "pet-fountain",
86720
- capScope: "device",
86721
- addonId: null,
86722
- access: "create",
86723
- role: "viewer"
86724
- },
86725
- "petFountain.setPumpOffMinutes": {
86726
- capName: "pet-fountain",
86727
- capScope: "device",
86728
- addonId: null,
86729
- access: "create",
86730
- role: "viewer"
86731
- },
86732
- "petFountain.setPumpOnMinutes": {
86733
- capName: "pet-fountain",
86734
- capScope: "device",
86735
- addonId: null,
86736
- access: "create",
86737
- role: "viewer"
86738
- },
86739
87798
  "pipelineAnalytics.addRetrainFrameFromRecording": {
86740
87799
  capName: "pipeline-analytics",
86741
87800
  capScope: "device",
@@ -88514,6 +89573,34 @@ Object.freeze({
88514
89573
  access: "create",
88515
89574
  role: "admin"
88516
89575
  },
89576
+ "pumpCycle.getStatus": {
89577
+ capName: "pump-cycle",
89578
+ capScope: "device",
89579
+ addonId: null,
89580
+ access: "view",
89581
+ role: "viewer"
89582
+ },
89583
+ "pumpCycle.setEnabled": {
89584
+ capName: "pump-cycle",
89585
+ capScope: "device",
89586
+ addonId: null,
89587
+ access: "create",
89588
+ role: "viewer"
89589
+ },
89590
+ "pumpCycle.setOffMinutes": {
89591
+ capName: "pump-cycle",
89592
+ capScope: "device",
89593
+ addonId: null,
89594
+ access: "create",
89595
+ role: "viewer"
89596
+ },
89597
+ "pumpCycle.setOnMinutes": {
89598
+ capName: "pump-cycle",
89599
+ capScope: "device",
89600
+ addonId: null,
89601
+ access: "create",
89602
+ role: "viewer"
89603
+ },
88517
89604
  "reboot.reboot": {
88518
89605
  capName: "reboot",
88519
89606
  capScope: "device",
@@ -89018,6 +90105,13 @@ Object.freeze({
89018
90105
  access: "create",
89019
90106
  role: "admin"
89020
90107
  },
90108
+ "remote.getAssumedStatus": {
90109
+ capName: "remote",
90110
+ capScope: "system",
90111
+ addonId: null,
90112
+ access: "view",
90113
+ role: "admin"
90114
+ },
89021
90115
  "remote.getConfig": {
89022
90116
  capName: "remote",
89023
90117
  capScope: "system",
@@ -91833,6 +92927,11 @@ Object.freeze({
91833
92927
  form: "single",
91834
92928
  optional: false
91835
92929
  }],
92930
+ "coreBlocks.proposeRunningThreshold": [{
92931
+ name: "deviceId",
92932
+ form: "single",
92933
+ optional: false
92934
+ }],
91836
92935
  "cover.close": [{
91837
92936
  name: "deviceId",
91838
92937
  form: "single",
@@ -92684,6 +93783,16 @@ Object.freeze({
92684
93783
  form: "single",
92685
93784
  optional: true
92686
93785
  }],
93786
+ "mediaPlayer.channelDown": [{
93787
+ name: "deviceId",
93788
+ form: "single",
93789
+ optional: false
93790
+ }],
93791
+ "mediaPlayer.channelUp": [{
93792
+ name: "deviceId",
93793
+ form: "single",
93794
+ optional: false
93795
+ }],
92687
93796
  "mediaPlayer.getStatus": [{
92688
93797
  name: "deviceId",
92689
93798
  form: "single",
@@ -92749,6 +93858,26 @@ Object.freeze({
92749
93858
  form: "single",
92750
93859
  optional: false
92751
93860
  }],
93861
+ "mediaPlayer.turnOff": [{
93862
+ name: "deviceId",
93863
+ form: "single",
93864
+ optional: false
93865
+ }],
93866
+ "mediaPlayer.turnOn": [{
93867
+ name: "deviceId",
93868
+ form: "single",
93869
+ optional: false
93870
+ }],
93871
+ "mediaPlayer.volumeDown": [{
93872
+ name: "deviceId",
93873
+ form: "single",
93874
+ optional: false
93875
+ }],
93876
+ "mediaPlayer.volumeUp": [{
93877
+ name: "deviceId",
93878
+ form: "single",
93879
+ optional: false
93880
+ }],
92752
93881
  "meter.getStatus": [{
92753
93882
  name: "deviceId",
92754
93883
  form: "single",
@@ -93058,21 +94187,6 @@ Object.freeze({
93058
94187
  form: "single",
93059
94188
  optional: false
93060
94189
  }],
93061
- "petFountain.setPumpInterval": [{
93062
- name: "deviceId",
93063
- form: "single",
93064
- optional: false
93065
- }],
93066
- "petFountain.setPumpOffMinutes": [{
93067
- name: "deviceId",
93068
- form: "single",
93069
- optional: false
93070
- }],
93071
- "petFountain.setPumpOnMinutes": [{
93072
- name: "deviceId",
93073
- form: "single",
93074
- optional: false
93075
- }],
93076
94190
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
93077
94191
  name: "deviceId",
93078
94192
  form: "single",
@@ -93708,6 +94822,26 @@ Object.freeze({
93708
94822
  form: "single",
93709
94823
  optional: false
93710
94824
  }],
94825
+ "pumpCycle.getStatus": [{
94826
+ name: "deviceId",
94827
+ form: "single",
94828
+ optional: false
94829
+ }],
94830
+ "pumpCycle.setEnabled": [{
94831
+ name: "deviceId",
94832
+ form: "single",
94833
+ optional: false
94834
+ }],
94835
+ "pumpCycle.setOffMinutes": [{
94836
+ name: "deviceId",
94837
+ form: "single",
94838
+ optional: false
94839
+ }],
94840
+ "pumpCycle.setOnMinutes": [{
94841
+ name: "deviceId",
94842
+ form: "single",
94843
+ optional: false
94844
+ }],
93711
94845
  "reboot.reboot": [{
93712
94846
  name: "deviceId",
93713
94847
  form: "single",
@@ -93909,6 +95043,11 @@ Object.freeze({
93909
95043
  form: "single",
93910
95044
  optional: true
93911
95045
  }],
95046
+ "remote.getAssumedStatus": [{
95047
+ name: "deviceId",
95048
+ form: "single",
95049
+ optional: false
95050
+ }],
93912
95051
  "remote.getConfig": [{
93913
95052
  name: "deviceId",
93914
95053
  form: "single",
@@ -94456,99 +95595,6 @@ Object.freeze({
94456
95595
  "network-access": "ingress",
94457
95596
  "smtp-provider": "email"
94458
95597
  });
94459
- /**
94460
- * A saved Customize assignment (D936). The schema is the type: callers use
94461
- * `z.infer`, they do not restate the shape.
94462
- */
94463
- var OverrideSourceSchema = object({
94464
- addonId: string$2().min(1),
94465
- stableId: string$2().min(1),
94466
- cap: string$2().min(1),
94467
- fieldPath: string$2().min(1)
94468
- });
94469
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
94470
- object({ kind: literal("identity") }),
94471
- object({ kind: literal("invert") }),
94472
- object({
94473
- kind: literal("threshold"),
94474
- comparison: _enum(["gte", "lte"]),
94475
- threshold: number(),
94476
- startDwellMs: number().nonnegative(),
94477
- stopDwellMs: number().nonnegative()
94478
- }),
94479
- object({
94480
- kind: literal("linear"),
94481
- scale: number(),
94482
- offset: number(),
94483
- clampMin: number().optional(),
94484
- clampMax: number().optional()
94485
- }),
94486
- object({
94487
- kind: literal("enum-map"),
94488
- mapping: record(string$2(), string$2().nullable())
94489
- }),
94490
- object({ kind: literal("number") })
94491
- ]);
94492
- var ScalarOverrideDraftSchema = object({
94493
- kind: literal("scalar"),
94494
- cap: string$2().min(1),
94495
- targetId: string$2().min(1),
94496
- source: OverrideSourceSchema,
94497
- transform: CustomizeTransformDraftSchema
94498
- });
94499
- var KeyedArrayOverrideDraftSchema = object({
94500
- kind: literal("keyedArray"),
94501
- cap: string$2().min(1),
94502
- targetId: string$2().min(1),
94503
- key: string$2().min(1),
94504
- label: string$2().min(1),
94505
- fieldPath: string$2().min(1),
94506
- source: OverrideSourceSchema,
94507
- transform: CustomizeTransformDraftSchema
94508
- });
94509
- var PulseOverrideDraftSchema = object({
94510
- kind: literal("pulse"),
94511
- cap: string$2().min(1),
94512
- targetId: string$2().min(1),
94513
- enabled: boolean(),
94514
- debounceMs: number().nonnegative(),
94515
- edges: array(_enum([
94516
- "rising",
94517
- "button",
94518
- "native-event"
94519
- ])).min(1).optional(),
94520
- sources: array(OverrideSourceSchema).min(1)
94521
- });
94522
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
94523
- "in-addition",
94524
- "confirm",
94525
- "replace"
94526
- ]).optional() });
94527
- discriminatedUnion("kind", [
94528
- ScalarOverrideDraftSchema,
94529
- KeyedArrayOverrideDraftSchema,
94530
- PulseOverrideDraftSchema,
94531
- object({
94532
- kind: literal("span"),
94533
- cap: string$2().min(1),
94534
- targetId: string$2().min(1),
94535
- timeoutMs: number().nonnegative(),
94536
- cooldownMs: number().nonnegative().optional(),
94537
- sources: array(SpanSourceDraftSchema)
94538
- })
94539
- ]);
94540
- object({
94541
- targetId: string$2().min(1),
94542
- configured: boolean(),
94543
- source: _enum(["native", "assigned"]),
94544
- state: _enum([
94545
- "active",
94546
- "stale",
94547
- "released",
94548
- "unavailable"
94549
- ]),
94550
- reason: string$2().nullable()
94551
- });
94552
95598
  var G711_SCALE_CORRECTION_DB = {
94553
95599
  PCMU: 20 * Math.log10(4),
94554
95600
  PCMA: 20 * Math.log10(8)
@@ -95164,6 +96210,7 @@ var STATE_SUMMARY_VARS = [
95164
96210
  [...RULE_TEXT],
95165
96211
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95166
96212
  ([...RULE_TEXT], [...COMBINED_ALARM]),
96213
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
95167
96214
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95168
96215
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
95169
96216
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -206241,13 +207288,16 @@ var MatterBrokerRegistry = class {
206241
207288
  }
206242
207289
  async restore(entries) {
206243
207290
  for (const entry of entries) this.#seedCounter(entry.id);
206244
- for (const entry of entries) try {
206245
- await this.#startManager(entry);
206246
- } catch (err) {
206247
- this.#logger.warn("MatterBrokerRegistry: failed to restore manager", {
206248
- tags: { brokerId: entry.id },
206249
- meta: { error: errMsg(err) }
206250
- });
207291
+ for (const entry of entries) {
207292
+ if (entry.enabled === false) continue;
207293
+ try {
207294
+ await this.#startManager(entry);
207295
+ } catch (err) {
207296
+ this.#logger.warn("MatterBrokerRegistry: failed to restore manager", {
207297
+ tags: { brokerId: entry.id },
207298
+ meta: { error: errMsg(err) }
207299
+ });
207300
+ }
206251
207301
  }
206252
207302
  }
206253
207303
  async shutdown() {
@@ -206278,7 +207328,8 @@ var MatterBrokerRegistry = class {
206278
207328
  this.#onAttributeReport = cb;
206279
207329
  }
206280
207330
  async createEntry(name, connection, opts = {}) {
206281
- const id = this.#allocateId();
207331
+ const id = opts.id ?? this.#allocateId();
207332
+ if (opts.id !== void 0) this.#seedCounter(opts.id);
206282
207333
  const entry = {
206283
207334
  id,
206284
207335
  name,
@@ -206291,7 +207342,7 @@ var MatterBrokerRegistry = class {
206291
207342
  }
206292
207343
  async removeEntry(id) {
206293
207344
  const mgr = this.#managers.get(id);
206294
- if (!mgr) throw new Error(`MatterBrokerRegistry: unknown broker id "${id}"`);
207345
+ if (!mgr) return;
206295
207346
  for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
206296
207347
  this.#managers.delete(id);
206297
207348
  try {
@@ -206373,6 +207424,10 @@ var MatterBrokerRegistry = class {
206373
207424
  if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
206374
207425
  await mgr.start();
206375
207426
  }
207427
+ /** The next free broker id, for a record minted before its manager exists. */
207428
+ allocateId() {
207429
+ return this.#allocateId();
207430
+ }
206376
207431
  #allocateId() {
206377
207432
  const id = `matter_${String(this.#nextId).padStart(3, "0")}`;
206378
207433
  this.#nextId++;
@@ -206411,10 +207466,12 @@ var MatterBrokerEntrySchema = object({
206411
207466
  id: string$2().min(1),
206412
207467
  name: string$2().min(1),
206413
207468
  connection: matterConnectionSchema,
206414
- integrationId: string$2().optional()
207469
+ integrationId: string$2().optional(),
207470
+ /** Absent means on; the instances page switches a fabric off without deleting it. */
207471
+ enabled: boolean().optional()
206415
207472
  });
206416
- /** Top-level addon config — an ordered list of broker entries (default empty). */
206417
- var matterAddonConfigSchema = object({ brokers: array(MatterBrokerEntrySchema).default([]) });
207473
+ /** Top-level addon config — the fabrics are records, so nothing is left here. */
207474
+ var matterAddonConfigSchema = object({});
206418
207475
  /** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
206419
207476
  function toRecord(value) {
206420
207477
  if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
@@ -207496,19 +208553,145 @@ var MatterNodeDevice = class extends BaseDevice {
207496
208553
  var matterBrokersEntryKind = defineEntryKind("brokers", {
207497
208554
  title: "Matter brokers",
207498
208555
  schema: entryBody({
207499
- connection: MatterBrokerEntrySchema.shape.connection,
207500
- integrationId: string$2().optional()
208556
+ ...matterConnectionSchema.shape,
208557
+ integrationId: string$2().optional(),
208558
+ enabled: boolean().optional()
207501
208559
  }),
207502
- writeTrigger: "an operator adds, edits or removes a Matter fabric broker"
208560
+ writeTrigger: "an operator adds, edits or removes a Matter fabric broker",
208561
+ retiredSecretPaths: ["connection.threadDataset", "connection.wifiPassword"]
207503
208562
  });
207504
208563
  //#endregion
208564
+ //#region src/matter-instances.ts
208565
+ /**
208566
+ * The Matter fabrics as `integration-instances` (planning#408 P3): one instance per `brokers`
208567
+ * record, over the SAME records and ids (`matter_NNN`) the `broker` cap serves. The body is flat.
208568
+ * A fabric needs no sign-in; commissioning a device stays on the adoption flow, behind the fabric.
208569
+ */
208570
+ var MATTER_INSTANCE_KIND = "matter";
208571
+ function statusOf(info) {
208572
+ if (info === null) return {
208573
+ state: "unknown",
208574
+ label: "Not started"
208575
+ };
208576
+ if (info.status === "connected") return {
208577
+ state: "ok",
208578
+ label: "Running"
208579
+ };
208580
+ if (info.status === "connecting") return {
208581
+ state: "unknown",
208582
+ label: "Starting"
208583
+ };
208584
+ return {
208585
+ state: "error",
208586
+ label: info.status,
208587
+ ...info.error !== null ? { detail: info.error } : {}
208588
+ };
208589
+ }
208590
+ function buildMatterInstances(records, deps) {
208591
+ return new RecordIntegrationInstances(records, matterBrokersEntryKind, {
208592
+ description: {
208593
+ instanceMode: "multiple",
208594
+ kind: "connection",
208595
+ canTest: true,
208596
+ kinds: [{
208597
+ id: MATTER_INSTANCE_KIND,
208598
+ label: "Matter"
208599
+ }],
208600
+ allowRestoreId: true
208601
+ },
208602
+ formSchema: () => buildConnectionFormSchema(),
208603
+ initialBody: (name) => ({
208604
+ name,
208605
+ threadDataset: "",
208606
+ wifiSsid: "",
208607
+ wifiPassword: "",
208608
+ reconcileIntervalMs: 3e5
208609
+ }),
208610
+ mintId: () => deps.mintId(),
208611
+ kindOf: () => MATTER_INSTANCE_KIND,
208612
+ health: (_body, id) => {
208613
+ const info = deps.infoOf(id);
208614
+ return {
208615
+ status: statusOf(info),
208616
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
208617
+ };
208618
+ },
208619
+ configured: () => true,
208620
+ onChanged: deps.onChanged,
208621
+ test: async ({ id }) => {
208622
+ const info = id === null ? null : deps.infoOf(id);
208623
+ if (info === null) return {
208624
+ success: false,
208625
+ status: "inconclusive",
208626
+ error: "Not started: save the fabric first"
208627
+ };
208628
+ return info.status === "connected" ? {
208629
+ success: true,
208630
+ status: "validated"
208631
+ } : {
208632
+ success: false,
208633
+ status: "rejected",
208634
+ error: info.error ?? `status: ${info.status}`
208635
+ };
208636
+ }
208637
+ });
208638
+ }
208639
+ //#endregion
208640
+ //#region src/migrations/flatten-broker-records.ts
208641
+ /**
208642
+ * The Matter broker records keep their fabric settings flat (planning#408 P3): the nested
208643
+ * `connection` object (`threadDataset`, `wifiSsid`, `wifiPassword`, `reconcileIntervalMs`) becomes
208644
+ * top-level fields under the SAME record id (`matter_NNN`), so the devices that carry the id keep
208645
+ * their fabric. The work is `flattenNestedRecords`: every secret is read before anything is
208646
+ * written, read back from its new ref before the nested object goes, and the old refs are
208647
+ * deleted last.
208648
+ */
208649
+ var FLATTEN_BROKER_RECORDS_ID = "matter-broker/2026-11-04-flatten-broker-records";
208650
+ async function flattenBrokerRecords(ports) {
208651
+ return flattenNestedRecords(ports, {
208652
+ legacyKey: "connection",
208653
+ flatSecretKeys: ["threadDataset", "wifiPassword"],
208654
+ toFlat: (legacy) => Object.fromEntries([
208655
+ "threadDataset",
208656
+ "wifiSsid",
208657
+ "wifiPassword",
208658
+ "reconcileIntervalMs"
208659
+ ].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
208660
+ });
208661
+ }
208662
+ //#endregion
208663
+ //#region src/migrations/index.ts
208664
+ /** The `matter-broker` catalogue (D987): offered to the engine when the addon initialises. */
208665
+ var ADDON_MIGRATIONS = [{
208666
+ meta: {
208667
+ id: FLATTEN_BROKER_RECORDS_ID,
208668
+ owner: "matter-broker",
208669
+ level: "addon-state",
208670
+ seq: 1,
208671
+ dependsOn: [],
208672
+ store: "settings",
208673
+ scope: "cluster",
208674
+ onFailure: "block-owner",
208675
+ bodyKind: "procedure",
208676
+ description: "Move each Matter fabric from a nested connection object to flat top-level fields under the same id, reading every secret back first",
208677
+ fixtureMark: "the integration link and the Wi-Fi SSID of each fabric"
208678
+ },
208679
+ procedure: flattenBrokerRecords
208680
+ }];
208681
+ //#endregion
207505
208682
  //#region src/matter-broker-store.ts
207506
208683
  function rowToEntry(id, data) {
207507
208684
  const parsed = MatterBrokerEntrySchema.safeParse({
207508
208685
  id,
207509
208686
  name: data.name,
207510
- connection: data.connection,
207511
- integrationId: data.integrationId
208687
+ connection: {
208688
+ threadDataset: data.threadDataset,
208689
+ wifiSsid: data.wifiSsid,
208690
+ wifiPassword: data.wifiPassword,
208691
+ reconcileIntervalMs: data.reconcileIntervalMs
208692
+ },
208693
+ integrationId: data.integrationId,
208694
+ enabled: data.enabled
207512
208695
  });
207513
208696
  return parsed.success ? parsed.data : null;
207514
208697
  }
@@ -207536,16 +208719,20 @@ var MatterBrokerStore = class MatterBrokerStore {
207536
208719
  }
207537
208720
  return out;
207538
208721
  }
208722
+ async getById(id) {
208723
+ return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
208724
+ }
207539
208725
  async replaceAll(entries) {
207540
208726
  const page = await this.kind.query({ limit: 1e3 });
207541
208727
  const nextIds = new Set(entries.map((e) => e.id));
207542
208728
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
207543
208729
  for (const entry of entries) {
207544
- const { id, name, connection, integrationId } = entry;
208730
+ const { id, name, connection, integrationId, enabled } = entry;
207545
208731
  const body = {
207546
208732
  name,
207547
- connection,
207548
- ...integrationId === void 0 ? {} : { integrationId }
208733
+ ...connection,
208734
+ ...integrationId === void 0 ? {} : { integrationId },
208735
+ ...enabled === void 0 ? {} : { enabled }
207549
208736
  };
207550
208737
  const parsed = matterBrokersEntryKind.schema.safeParse(body);
207551
208738
  if (!parsed.success) throw new Error(`invalid Matter broker '${id}': ${parsed.error.message}`);
@@ -207559,7 +208746,7 @@ var MatterBrokerStore = class MatterBrokerStore {
207559
208746
  //#endregion
207560
208747
  //#region src/addon.ts
207561
208748
  /** Default config — a fresh install starts with no fabric. */
207562
- var DEFAULTS = { brokers: [] };
208749
+ var DEFAULTS = {};
207563
208750
  /**
207564
208751
  * Matter broker addon (hub-only). Owns a Matter fabric per broker entry via an
207565
208752
  * embedded matter.js controller (behind the {@link matterCommands} /
@@ -207581,6 +208768,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207581
208768
  deviceClasses = { [DeviceType.Container]: MatterNodeDevice };
207582
208769
  registry = null;
207583
208770
  brokerStore = null;
208771
+ instances = null;
207584
208772
  activeBrokers = [];
207585
208773
  constructor() {
207586
208774
  super({ ...DEFAULTS });
@@ -207591,6 +208779,13 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207591
208779
  if (records === void 0) throw new Error("matter-broker: ctx.records is required (hub-only addon)");
207592
208780
  await records.declare({ brokers: matterBrokersEntryKind });
207593
208781
  this.brokerStore = MatterBrokerStore.of(records);
208782
+ this.instances = buildMatterInstances(records, {
208783
+ mintId: () => this.requireRegistry().allocateId(),
208784
+ infoOf: (id) => this.registry?.get(id) ?? null,
208785
+ onChanged: (id) => this.syncBrokerRuntime(id)
208786
+ });
208787
+ if (this.ctx.secrets === void 0) throw new Error("matter-broker: ctx.secrets is required");
208788
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, flattenPortsOver(records.kind(matterBrokersEntryKind))));
207594
208789
  this.activeBrokers = await this.loadPersistedBrokers();
207595
208790
  this.registry = new MatterBrokerRegistry(this.ctx.logger, {
207596
208791
  dataDir: this.ctx.dataDir,
@@ -207619,6 +208814,10 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207619
208814
  capability: brokerCapability,
207620
208815
  provider: this.buildBrokerProvider()
207621
208816
  },
208817
+ {
208818
+ capability: integrationInstancesCapability,
208819
+ provider: this.requireInstances().toProvider()
208820
+ },
207622
208821
  {
207623
208822
  capability: deviceAdoptionCapability,
207624
208823
  provider: this.buildAdoptionProvider()
@@ -207626,21 +208825,50 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207626
208825
  ];
207627
208826
  }
207628
208827
  async loadPersistedBrokers() {
207629
- const store = this.brokerStore;
207630
- if (store !== null) {
207631
- const fromRecords = await store.listResolved();
207632
- if (fromRecords.length > 0) return fromRecords;
208828
+ return this.requireBrokerStore().listResolved();
208829
+ }
208830
+ requireBrokerStore() {
208831
+ if (this.brokerStore === null) throw new Error("matter-broker: broker store not ready");
208832
+ return this.brokerStore;
208833
+ }
208834
+ requireInstances() {
208835
+ if (this.instances === null) throw new Error("matter-broker: instances not ready");
208836
+ return this.instances;
208837
+ }
208838
+ /**
208839
+ * Bring the runtime in line with one record after the instances page changed it: a removed
208840
+ * record stops its controller and cascades its devices, an off record only stops the
208841
+ * controller, a live one re-applies its settings or starts under the record's own id.
208842
+ */
208843
+ async syncBrokerRuntime(id) {
208844
+ const reg = this.requireRegistry();
208845
+ const entry = await this.requireBrokerStore().getById(id);
208846
+ this.activeBrokers = await this.loadPersistedBrokers();
208847
+ if (entry === null) {
208848
+ await reg.removeEntry(id);
208849
+ await this.cascadeRemoveDevicesForBroker(id);
208850
+ return;
208851
+ }
208852
+ if (entry.enabled === false) {
208853
+ await reg.removeEntry(id);
208854
+ return;
208855
+ }
208856
+ try {
208857
+ if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
208858
+ else await reg.createEntry(entry.name, entry.connection, {
208859
+ id,
208860
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
208861
+ });
208862
+ } catch (err) {
208863
+ this.ctx.logger.warn("matter instance: could not start the fabric", { meta: {
208864
+ brokerId: id,
208865
+ error: errMsg(err)
208866
+ } });
207633
208867
  }
207634
- return this.config.brokers;
207635
208868
  }
207636
208869
  async persistBrokerEntries(entries) {
207637
208870
  this.activeBrokers = [...entries];
207638
- const store = this.brokerStore;
207639
- if (store !== null) {
207640
- await store.replaceAll(entries);
207641
- return;
207642
- }
207643
- await this.updateGlobalSettings({ brokers: [...entries] });
208871
+ await this.requireBrokerStore().replaceAll(entries);
207644
208872
  }
207645
208873
  async onConfigChanged() {
207646
208874
  const reg = this.registry;
@@ -207658,6 +208886,7 @@ var MatterBrokerAddon = class extends BaseDeviceProvider {
207658
208886
  } });
207659
208887
  }
207660
208888
  for (const entry of persisted) {
208889
+ if (entry.enabled === false) continue;
207661
208890
  if (entry.id && liveIds.has(entry.id)) {
207662
208891
  if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
207663
208892
  try {