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