@camstack/addon-provider-velux 0.2.156 → 0.2.158

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
@@ -820,7 +820,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
820
820
  return EventCategory;
821
821
  }({});
822
822
  //#endregion
823
- //#region ../types/dist/device-type-CfHw_quR.mjs
823
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
824
824
  /**
825
825
  import { errMsg } from '@camstack/types'
826
826
  * Extract a human-readable message from an unknown error value.
@@ -1051,6 +1051,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1051
1051
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1052
1052
  /** Media player exposes stop distinct from pause. */
1053
1053
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1054
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1055
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1056
+ /** Media player exposes channel up / down in steps. */
1057
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1058
+ /** Media player exposes power on / off. */
1059
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1054
1060
  /** Alarm panel requires a PIN code on arm/disarm. */
1055
1061
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1056
1062
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -4587,6 +4593,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
4587
4593
  var booleanProcessor = (_schema, _ctx, json, _params) => {
4588
4594
  json.type = "boolean";
4589
4595
  };
4596
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
4597
+ if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
4598
+ };
4599
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
4600
+ if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
4601
+ };
4590
4602
  var nullProcessor = (_schema, ctx, json, _params) => {
4591
4603
  if (ctx.target === "openapi-3.0") {
4592
4604
  json.type = "string";
@@ -4594,12 +4606,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
4594
4606
  json.enum = [null];
4595
4607
  } else json.type = "null";
4596
4608
  };
4609
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
4610
+ if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
4611
+ };
4597
4612
  var voidProcessor = (_schema, ctx, _json, _params) => {
4598
4613
  if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
4599
4614
  };
4600
4615
  var neverProcessor = (_schema, _ctx, json, _params) => {
4601
4616
  json.not = {};
4602
4617
  };
4618
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
4619
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {};
4603
4620
  var dateProcessor = (_schema, ctx, _json, _params) => {
4604
4621
  if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
4605
4622
  };
@@ -4631,12 +4648,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
4631
4648
  json.enum = vals;
4632
4649
  }
4633
4650
  };
4651
+ var nanProcessor = (_schema, ctx, _json, _params) => {
4652
+ if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
4653
+ };
4654
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
4655
+ const _json = json;
4656
+ const pattern = schema._zod.pattern;
4657
+ if (!pattern) throw new Error("Pattern not found in template literal");
4658
+ _json.type = "string";
4659
+ _json.pattern = pattern.source;
4660
+ };
4661
+ var fileProcessor = (schema, _ctx, json, _params) => {
4662
+ const _json = json;
4663
+ const file = {
4664
+ type: "string",
4665
+ format: "binary",
4666
+ contentEncoding: "binary"
4667
+ };
4668
+ const { minimum, maximum, mime } = schema._zod.bag;
4669
+ if (minimum !== void 0) file.minLength = minimum;
4670
+ if (maximum !== void 0) file.maxLength = maximum;
4671
+ if (mime) if (mime.length === 1) {
4672
+ file.contentMediaType = mime[0];
4673
+ Object.assign(_json, file);
4674
+ } else {
4675
+ Object.assign(_json, file);
4676
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
4677
+ }
4678
+ else Object.assign(_json, file);
4679
+ };
4680
+ var successProcessor = (_schema, _ctx, json, _params) => {
4681
+ json.type = "boolean";
4682
+ };
4634
4683
  var customProcessor = (_schema, ctx, _json, _params) => {
4635
4684
  if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
4636
4685
  };
4686
+ var functionProcessor = (_schema, ctx, _json, _params) => {
4687
+ if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
4688
+ };
4637
4689
  var transformProcessor = (_schema, ctx, _json, _params) => {
4638
4690
  if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
4639
4691
  };
4692
+ var mapProcessor = (_schema, ctx, _json, _params) => {
4693
+ if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
4694
+ };
4695
+ var setProcessor = (_schema, ctx, _json, _params) => {
4696
+ if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
4697
+ };
4640
4698
  var arrayProcessor = (schema, ctx, _json, params) => {
4641
4699
  const json = _json;
4642
4700
  const def = schema._zod.def;
@@ -4841,6 +4899,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
4841
4899
  seen.ref = def.innerType;
4842
4900
  json.readOnly = true;
4843
4901
  };
4902
+ var promiseProcessor = (schema, ctx, _json, params) => {
4903
+ const def = schema._zod.def;
4904
+ process$1(def.innerType, ctx, params);
4905
+ const seen = ctx.seen.get(schema);
4906
+ seen.ref = def.innerType;
4907
+ };
4844
4908
  var optionalProcessor = (schema, ctx, _json, params) => {
4845
4909
  const def = schema._zod.def;
4846
4910
  process$1(def.innerType, ctx, params);
@@ -4853,6 +4917,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
4853
4917
  const seen = ctx.seen.get(schema);
4854
4918
  seen.ref = innerType;
4855
4919
  };
4920
+ var allProcessors = {
4921
+ string: stringProcessor,
4922
+ number: numberProcessor,
4923
+ boolean: booleanProcessor,
4924
+ bigint: bigintProcessor,
4925
+ symbol: symbolProcessor,
4926
+ null: nullProcessor,
4927
+ undefined: undefinedProcessor,
4928
+ void: voidProcessor,
4929
+ never: neverProcessor,
4930
+ any: anyProcessor,
4931
+ unknown: unknownProcessor,
4932
+ date: dateProcessor,
4933
+ enum: enumProcessor,
4934
+ literal: literalProcessor,
4935
+ nan: nanProcessor,
4936
+ template_literal: templateLiteralProcessor,
4937
+ file: fileProcessor,
4938
+ success: successProcessor,
4939
+ custom: customProcessor,
4940
+ function: functionProcessor,
4941
+ transform: transformProcessor,
4942
+ map: mapProcessor,
4943
+ set: setProcessor,
4944
+ array: arrayProcessor,
4945
+ object: objectProcessor,
4946
+ union: unionProcessor,
4947
+ intersection: intersectionProcessor,
4948
+ tuple: tupleProcessor,
4949
+ record: recordProcessor,
4950
+ nullable: nullableProcessor,
4951
+ nonoptional: nonoptionalProcessor,
4952
+ default: defaultProcessor,
4953
+ prefault: prefaultProcessor,
4954
+ catch: catchProcessor,
4955
+ pipe: pipeProcessor,
4956
+ readonly: readonlyProcessor,
4957
+ promise: promiseProcessor,
4958
+ optional: optionalProcessor,
4959
+ lazy: lazyProcessor
4960
+ };
4961
+ function toJSONSchema(input, params) {
4962
+ if ("_idmap" in input) {
4963
+ const registry = input;
4964
+ const ctx = initializeContext({
4965
+ ...params,
4966
+ processors: allProcessors
4967
+ });
4968
+ const defs = {};
4969
+ for (const entry of registry._idmap.entries()) {
4970
+ const [_, schema] = entry;
4971
+ process$1(schema, ctx);
4972
+ }
4973
+ const schemas = {};
4974
+ ctx.external = {
4975
+ registry,
4976
+ uri: params?.uri,
4977
+ defs
4978
+ };
4979
+ for (const entry of registry._idmap.entries()) {
4980
+ const [key, schema] = entry;
4981
+ extractDefs(ctx, schema);
4982
+ schemas[key] = finalize(ctx, schema);
4983
+ }
4984
+ if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
4985
+ return { schemas };
4986
+ }
4987
+ const ctx = initializeContext({
4988
+ ...params,
4989
+ processors: allProcessors
4990
+ });
4991
+ process$1(input, ctx);
4992
+ extractDefs(ctx, input);
4993
+ return finalize(ctx, input);
4994
+ }
4856
4995
  //#endregion
4857
4996
  //#region ../../node_modules/zod/v4/classic/iso.js
4858
4997
  var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
@@ -5877,7 +6016,7 @@ var ZodIssueCode = {
5877
6016
  var ZodFirstPartyTypeKind;
5878
6017
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5879
6018
  //#endregion
5880
- //#region ../types/dist/sleep-QsoHs66J.mjs
6019
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
5881
6020
  /**
5882
6021
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5883
6022
  * window to float samples (D455).
@@ -5956,6 +6095,22 @@ function buildAlawTable() {
5956
6095
  }
5957
6096
  buildUlawTable();
5958
6097
  buildAlawTable();
6098
+ /**
6099
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
6100
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
6101
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
6102
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
6103
+ */
6104
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
6105
+ var AddonMigrationsBlockedError = class extends Error {
6106
+ /** The ledger ids that did not apply and block the owner. */
6107
+ migrationIds;
6108
+ constructor(migrationIds) {
6109
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
6110
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
6111
+ this.migrationIds = migrationIds;
6112
+ }
6113
+ };
5959
6114
  /** Is this field's stored value a credential? */
5960
6115
  function isSecretConfigField(field) {
5961
6116
  if (field.type === "password") return true;
@@ -6009,9 +6164,16 @@ function isSecretMarker(value) {
6009
6164
  const ref = value["$secret"];
6010
6165
  return typeof ref === "string" && ref.length > 0;
6011
6166
  }
6167
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6168
+ function isSecretMarkerLike(value) {
6169
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6170
+ }
6012
6171
  function isSecretClear(value) {
6013
6172
  return isRecord$7(value) && value["$secretClear"] === true;
6014
6173
  }
6174
+ function isSecretFieldView(value) {
6175
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6176
+ }
6015
6177
  /**
6016
6178
  * The UI view of a stored secret field value: a marker becomes "set", inline
6017
6179
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6393,6 +6555,53 @@ function emitReadiness(bus, params) {
6393
6555
  ts
6394
6556
  }));
6395
6557
  }
6558
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6559
+ var AddonMigrationDefinitionError = class extends Error {
6560
+ id;
6561
+ constructor(id, message) {
6562
+ super(`${id}: ${message}`);
6563
+ this.id = id;
6564
+ this.name = "AddonMigrationDefinitionError";
6565
+ }
6566
+ };
6567
+ /**
6568
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6569
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6570
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6571
+ */
6572
+ function defineAddonMigrations(catalogue, ports) {
6573
+ return catalogue.map((entry) => {
6574
+ const { meta } = entry;
6575
+ if ("plan" in entry) {
6576
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6577
+ return {
6578
+ meta,
6579
+ execute: () => guarded(async () => ({
6580
+ kind: "plan",
6581
+ plan: await entry.plan(ports)
6582
+ }))
6583
+ };
6584
+ }
6585
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6586
+ return {
6587
+ meta,
6588
+ execute: () => guarded(async () => ({
6589
+ kind: "procedure",
6590
+ result: await entry.procedure(ports)
6591
+ }))
6592
+ };
6593
+ });
6594
+ }
6595
+ async function guarded(work) {
6596
+ try {
6597
+ return await work();
6598
+ } catch (err) {
6599
+ return {
6600
+ kind: "failed",
6601
+ error: errMsg(err)
6602
+ };
6603
+ }
6604
+ }
6396
6605
  var NOT_RUN = {
6397
6606
  declared: false,
6398
6607
  snapshot: null,
@@ -6407,7 +6616,7 @@ var NOT_RUN = {
6407
6616
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6408
6617
  * because the hub (and this addon's boot) must not hang on a migration.
6409
6618
  */
6410
- async function runAddonMigrations(host, migrations, logger) {
6619
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6411
6620
  const first = migrations[0];
6412
6621
  if (first === void 0) return NOT_RUN;
6413
6622
  const owner = first.meta.owner;
@@ -6436,7 +6645,9 @@ async function runAddonMigrations(host, migrations, logger) {
6436
6645
  else if (status === "blocked") blocked.push(id);
6437
6646
  else skipped.push(id);
6438
6647
  };
6648
+ const selected = options.only === void 0 ? null : new Set(options.only);
6439
6649
  for (const step of declaration.steps) {
6650
+ if (selected !== null && !selected.has(step.id)) continue;
6440
6651
  const bound = byId.get(step.id);
6441
6652
  if (bound === void 0 || step.action === "skip") {
6442
6653
  skipped.push(step.id);
@@ -6614,11 +6825,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6614
6825
  }
6615
6826
  return out;
6616
6827
  }
6828
+ /**
6829
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6830
+ *
6831
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6832
+ * scope (global, one node, one device, one integration, one row of a business
6833
+ * collection) and the field path inside that scope — so saving the same field
6834
+ * twice updates the same ref, and the ref itself says which settings blob
6835
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6836
+ * copied onto another ref does not decrypt.
6837
+ *
6838
+ * Array items are addressed by ITEM ID, never by index
6839
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6840
+ *
6841
+ * Pure and browser-safe: no node builtins, no I/O.
6842
+ */
6843
+ /** The URI scheme of every keychain ref. */
6844
+ var SECRET_REF_SCHEME = "secret://";
6617
6845
  var GLOBAL_SCOPE = { kind: "global" };
6618
6846
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6619
6847
  function globalSecretScope() {
6620
6848
  return GLOBAL_SCOPE;
6621
6849
  }
6850
+ /**
6851
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6852
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6853
+ * addon running forked.
6854
+ */
6855
+ function normalizeSecretOwner(addonId) {
6856
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6857
+ }
6622
6858
  function encodeSegment(value) {
6623
6859
  return encodeURIComponent(value);
6624
6860
  }
@@ -6629,6 +6865,16 @@ function decodeSegment(value) {
6629
6865
  return null;
6630
6866
  }
6631
6867
  }
6868
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6869
+ function formatSecretScope(scope) {
6870
+ switch (scope.kind) {
6871
+ case "global": return "global";
6872
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6873
+ case "device": return `device/${scope.deviceId}`;
6874
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6875
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6876
+ }
6877
+ }
6632
6878
  /**
6633
6879
  * The field path of a value inside an array item, addressed by the item's id:
6634
6880
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6637,6 +6883,13 @@ function decodeSegment(value) {
6637
6883
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6638
6884
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6639
6885
  }
6886
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6887
+ function buildSecretRef(parts) {
6888
+ const owner = normalizeSecretOwner(parts.owner);
6889
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6890
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6891
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6892
+ }
6640
6893
  function parseScope(segments) {
6641
6894
  switch (segments[0]) {
6642
6895
  case "global": return {
@@ -7554,15 +7807,19 @@ var BaseAddon = class {
7554
7807
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7555
7808
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7556
7809
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7810
+ *
7811
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7812
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7813
+ * omitted applied ids "newer").
7557
7814
  */
7558
- async runMigrations(migrations) {
7815
+ async runMigrations(migrations, options) {
7559
7816
  const api = this.ctx.api.migrations;
7560
7817
  const report = await runAddonMigrations({
7561
7818
  declareOwner: (input) => api.declareOwner.mutate(input),
7562
7819
  startMigration: (input) => api.startMigration.mutate(input),
7563
7820
  finishMigration: (input) => api.finishMigration.mutate(input)
7564
- }, migrations, this.ctx.logger);
7565
- 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`);
7821
+ }, migrations, this.ctx.logger, options);
7822
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7566
7823
  return report;
7567
7824
  }
7568
7825
  /**
@@ -7902,6 +8159,10 @@ var BaseAddon = class {
7902
8159
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
7903
8160
  const patch = {};
7904
8161
  for (const key of keys) {
8162
+ if (tree.get(key)?.kind === "array") {
8163
+ patch[key] = [];
8164
+ continue;
8165
+ }
7905
8166
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
7906
8167
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
7907
8168
  }
@@ -8541,6 +8802,45 @@ function normalizeAddonInitResult(result) {
8541
8802
  if (Array.isArray(result)) return { providers: result };
8542
8803
  return result;
8543
8804
  }
8805
+ var PREFIX = "records/";
8806
+ /** The base of every records failure. `detail` is the human part of the message. */
8807
+ var RecordsError = class extends Error {
8808
+ code;
8809
+ detail;
8810
+ constructor(code, detail) {
8811
+ super(`${PREFIX}${code}: ${detail}`);
8812
+ this.name = "RecordsError";
8813
+ this.code = code;
8814
+ this.detail = detail;
8815
+ }
8816
+ };
8817
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8818
+ var RevConflictError = class extends RecordsError {
8819
+ constructor(detail) {
8820
+ super("rev-conflict", detail);
8821
+ this.name = "RevConflictError";
8822
+ }
8823
+ };
8824
+ /** The keychain could not store or serve a value: explicit, never an empty string. */
8825
+ var RecordSecretUnavailableError = class extends RecordsError {
8826
+ constructor(detail) {
8827
+ super("secret-unavailable", detail);
8828
+ this.name = "RecordSecretUnavailableError";
8829
+ }
8830
+ };
8831
+ var InvalidRecordError = class extends RecordsError {
8832
+ constructor(detail) {
8833
+ super("invalid-record", detail);
8834
+ this.name = "InvalidRecordError";
8835
+ }
8836
+ };
8837
+ /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
8838
+ var DeclarationRefusedError = class extends RecordsError {
8839
+ constructor(detail) {
8840
+ super("declaration-refused", detail);
8841
+ this.name = "DeclarationRefusedError";
8842
+ }
8843
+ };
8544
8844
  /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8545
8845
  /** The files a kind may live in. `history` is device-history's (D850), never records. */
8546
8846
  var RECORD_DATABASES = ["settings", "events"];
@@ -8556,6 +8856,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8556
8856
  var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8557
8857
  var RECORD_BATCH_MAX = 1e3;
8558
8858
  var RECORD_QUERY_MAX = 1e3;
8859
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8860
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8861
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8862
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8863
+ /**
8864
+ * Secret fields of a record, addressed the way a Zod schema declares them
8865
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8866
+ * (`auth.accessToken`, `headers[].value`).
8867
+ *
8868
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8869
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8870
+ * key releases exactly that key's secret.
8871
+ *
8872
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8873
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8874
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8875
+ * leaf names its element: `headers[X-Api].value`.
8876
+ *
8877
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8878
+ */
8879
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8880
+ function parseSecretPath(path) {
8881
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8882
+ return path.split(".").map((raw) => {
8883
+ const each = raw.endsWith("[]");
8884
+ const map = raw.endsWith("{}");
8885
+ const key = each || map ? raw.slice(0, -2) : raw;
8886
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8887
+ return {
8888
+ key,
8889
+ each,
8890
+ map
8891
+ };
8892
+ });
8893
+ }
8894
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8895
+ function secretArrayPrefixes(path) {
8896
+ const segments = parseSecretPath(path);
8897
+ const prefixes = [];
8898
+ const walked = [];
8899
+ for (const segment of segments) {
8900
+ walked.push(segment.key);
8901
+ if (segment.each) prefixes.push(walked.join("."));
8902
+ }
8903
+ return prefixes;
8904
+ }
8905
+ function isRecord$4(value) {
8906
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8907
+ }
8908
+ function elementId(element, idKey, declared, prefix) {
8909
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8910
+ 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`);
8911
+ return id;
8912
+ }
8913
+ /**
8914
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8915
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8916
+ * result is a new body, `data` is untouched.
8917
+ */
8918
+ var REMOVE_LEAF = Symbol("remove-leaf");
8919
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8920
+ if (!isRecord$4(node)) return node;
8921
+ const segment = segments[index];
8922
+ if (segment === void 0) return node;
8923
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8924
+ const child = node[segment.key];
8925
+ const keys = [...walkedKeys, segment.key];
8926
+ const last = index === segments.length - 1;
8927
+ if (segment.each) {
8928
+ if (!Array.isArray(child)) return node;
8929
+ const prefix = keys.join(".");
8930
+ const idKey = arrayKeys[prefix];
8931
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8932
+ const mapped = child.map((element) => {
8933
+ const id = elementId(element, idKey, declared, prefix);
8934
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8935
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8936
+ });
8937
+ return {
8938
+ ...node,
8939
+ [segment.key]: mapped
8940
+ };
8941
+ }
8942
+ if (segment.map) {
8943
+ if (!isRecord$4(child)) return node;
8944
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8945
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8946
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8947
+ return [mapKey, visit({
8948
+ declared,
8949
+ concrete,
8950
+ value
8951
+ })];
8952
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8953
+ return {
8954
+ ...node,
8955
+ [segment.key]: Object.fromEntries(kept)
8956
+ };
8957
+ }
8958
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8959
+ if (last) {
8960
+ const replacement = visit({
8961
+ declared,
8962
+ concrete,
8963
+ value: child
8964
+ });
8965
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8966
+ return {
8967
+ ...node,
8968
+ [segment.key]: replacement
8969
+ };
8970
+ }
8971
+ return {
8972
+ ...node,
8973
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8974
+ };
8975
+ }
8976
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8977
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8978
+ let current = data;
8979
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8980
+ return current;
8981
+ }
8982
+ /** Every present secret leaf, read-only. */
8983
+ function collectSecretLeaves(data, paths, arrayKeys) {
8984
+ const found = [];
8985
+ mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
8986
+ found.push(leaf);
8987
+ return leaf.value;
8988
+ });
8989
+ return found;
8990
+ }
8991
+ /**
8992
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8993
+ * record's refs never collide with the refs a legacy collection of the same name
8994
+ * already holds during an adoption.
8995
+ */
8996
+ function recordSecretCollection(kind) {
8997
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8998
+ }
8999
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
9000
+ function recordSecretRef(owner, kind, rowId, concretePath) {
9001
+ return buildSecretRef({
9002
+ owner,
9003
+ scope: {
9004
+ kind: "row",
9005
+ collection: recordSecretCollection(kind),
9006
+ rowId
9007
+ },
9008
+ fieldPath: concretePath
9009
+ });
9010
+ }
9011
+ /**
9012
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
9013
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
9014
+ * token. The form hands it to the secret field, which reveals the value by ref
9015
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
9016
+ * stored secret (the records door reads a view as KEEP).
9017
+ *
9018
+ * ONE implementation for the entries service and for every addon that serves
9019
+ * its own instances (`RecordIntegrationInstances`).
9020
+ */
9021
+ function isRecord$3(value) {
9022
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9023
+ }
9024
+ function secretViewsOf(owner, kind, id, decl, data) {
9025
+ if (decl.secretPaths.length === 0) return data;
9026
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
9027
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
9028
+ return {
9029
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
9030
+ v: 1,
9031
+ set: true
9032
+ };
9033
+ });
9034
+ return isRecord$3(shown) ? shown : data;
9035
+ }
8559
9036
  /**
8560
9037
  * The share-view scope kinds a share-link token can carry. The hub mints
8561
9038
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8628,7 +9105,8 @@ function isZodSecret(schema) {
8628
9105
  }
8629
9106
  /**
8630
9107
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8631
- * element of this array" (`connection.password`, `rows[].token`). A declared
9108
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9109
+ * map" (`applianceSecrets{}`). A declared
8632
9110
  * object or array is reported as one path and not descended into — everything
8633
9111
  * under it is secret.
8634
9112
  */
@@ -8639,6 +9117,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8639
9117
  const def = defOf(layer);
8640
9118
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8641
9119
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9120
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8642
9121
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8643
9122
  else if (def.type === "intersection") {
8644
9123
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -8713,6 +9192,126 @@ var RecordKindDeclarationSchema = object({
8713
9192
  }).strict();
8714
9193
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8715
9194
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9195
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9196
+ function assertValidDeclaration(name, decl) {
9197
+ const refuse = (why) => {
9198
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9199
+ };
9200
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9201
+ const seen = /* @__PURE__ */ new Set();
9202
+ for (const index of decl.indexes) {
9203
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9204
+ seen.add(index.name);
9205
+ }
9206
+ const { retention } = decl;
9207
+ const hasSecrets = decl.secretPaths.length > 0;
9208
+ if ("ttlMs" in retention) {
9209
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9210
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9211
+ }
9212
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9213
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9214
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9215
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9216
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9217
+ if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
9218
+ }
9219
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9220
+ for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
9221
+ }
9222
+ function bodyFieldsOf(schema) {
9223
+ return { ...toJSONSchema(schema, {
9224
+ unrepresentable: "any",
9225
+ io: "input"
9226
+ }) };
9227
+ }
9228
+ function isRecordLike(value) {
9229
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9230
+ }
9231
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9232
+ function hasNameField(json, field) {
9233
+ const branches = json["oneOf"] ?? json["anyOf"];
9234
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9235
+ const properties = json["properties"];
9236
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9237
+ }
9238
+ /**
9239
+ * Declare the kinds of a package. The keys are the kind names. The result is
9240
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9241
+ * types its handle from.
9242
+ */
9243
+ function defineRecordKinds(specs) {
9244
+ const out = {};
9245
+ for (const [name, spec] of Object.entries(specs)) {
9246
+ const secretPaths = collectZodSecretPaths(spec.schema);
9247
+ const nameField = spec.entry?.nameField ?? "name";
9248
+ const fields = bodyFieldsOf(spec.schema);
9249
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9250
+ const parsed = RecordKindDeclarationSchema.parse({
9251
+ database: spec.database,
9252
+ class: spec.class,
9253
+ writeTrigger: spec.writeTrigger,
9254
+ device: spec.device,
9255
+ at: spec.at,
9256
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9257
+ retention: spec.retention,
9258
+ deviceRemoval: spec.deviceRemoval,
9259
+ maxDataBytes: spec.maxDataBytes,
9260
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9261
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9262
+ transactFamily: spec.transactFamily,
9263
+ entry: spec.entry === void 0 ? void 0 : {
9264
+ title: spec.entry.title,
9265
+ nameField,
9266
+ fields
9267
+ }
9268
+ });
9269
+ assertValidDeclaration(name, parsed);
9270
+ out[name] = {
9271
+ name,
9272
+ schema: spec.schema,
9273
+ declaration: parsed
9274
+ };
9275
+ }
9276
+ return out;
9277
+ }
9278
+ /**
9279
+ * Entry kinds (D881): every list of user-created entries (notification
9280
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9281
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9282
+ * through the `record-entries` capability with password fields derived from
9283
+ * the schema (`.meta({ secret: true })`).
9284
+ */
9285
+ /** The body of a plain entry: a name and the fields the kind adds. */
9286
+ function entryBody(shape) {
9287
+ return object({
9288
+ name: string().min(1),
9289
+ ...shape
9290
+ });
9291
+ }
9292
+ /**
9293
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9294
+ * engine, never aged, belonging to no device.
9295
+ */
9296
+ function defineEntryKind(name, spec) {
9297
+ const defined = defineRecordKinds({ [name]: {
9298
+ schema: spec.schema,
9299
+ class: "config",
9300
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9301
+ retention: { none: "an entry goes only when the operator deletes it" },
9302
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9303
+ ...spec.database === void 0 ? {} : { database: spec.database },
9304
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9305
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9306
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9307
+ entry: {
9308
+ title: spec.title,
9309
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9310
+ }
9311
+ } })[name];
9312
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9313
+ return defined;
9314
+ }
8716
9315
  object({
8717
9316
  field: string().min(1),
8718
9317
  on: literal("advance")
@@ -9391,6 +9990,7 @@ var CAP_NAMES = Object.freeze([
9391
9990
  "backup",
9392
9991
  "battery",
9393
9992
  "binary",
9993
+ "board-key-source",
9394
9994
  "brightness",
9395
9995
  "broker",
9396
9996
  "button",
@@ -9512,6 +10112,7 @@ var CAP_NAMES = Object.freeze([
9512
10112
  "privacy-mask",
9513
10113
  "ptz",
9514
10114
  "ptz-autotrack",
10115
+ "pump-cycle",
9515
10116
  "reboot",
9516
10117
  "record-entries",
9517
10118
  "recording",
@@ -10804,18 +11405,6 @@ Object.freeze({
10804
11405
  "path": "pumpRunning",
10805
11406
  "valueType": "boolean"
10806
11407
  },
10807
- {
10808
- "path": "pumpIntervalEnabled",
10809
- "valueType": "boolean"
10810
- },
10811
- {
10812
- "path": "pumpOnTime",
10813
- "valueType": "number"
10814
- },
10815
- {
10816
- "path": "pumpOffTime",
10817
- "valueType": "number"
10818
- },
10819
11408
  {
10820
11409
  "path": "lastDrink",
10821
11410
  "valueType": "number"
@@ -10939,6 +11528,20 @@ Object.freeze({
10939
11528
  "valueType": "number"
10940
11529
  }
10941
11530
  ],
11531
+ "pump-cycle": [
11532
+ {
11533
+ "path": "enabled",
11534
+ "valueType": "boolean"
11535
+ },
11536
+ {
11537
+ "path": "onTime",
11538
+ "valueType": "number"
11539
+ },
11540
+ {
11541
+ "path": "offTime",
11542
+ "valueType": "number"
11543
+ }
11544
+ ],
10942
11545
  "recording-archive": [
10943
11546
  {
10944
11547
  "path": "deviceId",
@@ -11388,7 +11991,7 @@ object({
11388
11991
  params: record(string(), union([string(), number()])).optional()
11389
11992
  });
11390
11993
  //#endregion
11391
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11994
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11392
11995
  /**
11393
11996
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11394
11997
  *
@@ -12151,38 +12754,6 @@ function validateExpressionSource(src) {
12151
12754
  return null;
12152
12755
  }
12153
12756
  /**
12154
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12155
- *
12156
- * Standby of the control board is under 5 W; a motor or a heater is far
12157
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12158
- * a cycle, so running starts only after 30 s above the line.
12159
- *
12160
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12161
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12162
- * report "finished" in the middle of the program, so the stop dwell is
12163
- * 10 minutes on every type. The number is stored per type so one machine
12164
- * can move later without moving the others.
12165
- */
12166
- var APPLIANCE_TEMPLATE_ID = "appliance";
12167
- var APPLIANCE_KINDS = [
12168
- "washer",
12169
- "dryer",
12170
- "dishwasher"
12171
- ];
12172
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12173
- /**
12174
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12175
- *
12176
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12177
- * (D967), so the heater switches exactly as the plug does. Running is the
12178
- * plug being on; when a power reading is also named, running is the draw
12179
- * staying above the line instead (a plug left on with the stove's own
12180
- * thermostat open is not heating). 50 W is far above any standby and below
12181
- * the smallest radiator.
12182
- */
12183
- var HEATER_TEMPLATE_ID = "heater";
12184
- DeviceType.Heater;
12185
- /**
12186
12757
  * Statistics and integrals over a history read (D944).
12187
12758
  *
12188
12759
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12326,34 +12897,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12326
12897
  /** Actuator groups only. Readings leave this unset. */
12327
12898
  active: ActiveCriterionSchema.optional()
12328
12899
  }),
12329
- object({
12330
- kind: literal("appliance"),
12331
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12332
- appliance: _enum(APPLIANCE_KINDS),
12333
- thresholdWatts: number().positive(),
12334
- startDwellMs: number().int().nonnegative(),
12335
- stopDwellMs: number().int().nonnegative(),
12336
- member: HelperMemberSchema,
12337
- /**
12338
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12339
- * composition carries a `power-source` feature, so the hub can cut and restore
12340
- * that plug (power-source link of a NEW-target composition).
12341
- */
12342
- powerSwitch: HelperMemberSchema.optional(),
12343
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12344
- offSeconds: number().optional()
12345
- }),
12346
- object({
12347
- kind: literal("heater"),
12348
- templateId: literal(HEATER_TEMPLATE_ID),
12349
- /** The plug, read and commanded at `switch.on`. */
12350
- plug: HelperMemberSchema,
12351
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12352
- meter: HelperMemberSchema.optional(),
12353
- thresholdWatts: number().positive(),
12354
- startDwellMs: number().int().nonnegative(),
12355
- stopDwellMs: number().int().nonnegative()
12356
- }),
12357
12900
  object({
12358
12901
  kind: literal("statistic"),
12359
12902
  stat: _enum(STATISTIC_STATS),
@@ -12525,7 +13068,14 @@ var CompositionForwardCommandSchema = object({
12525
13068
  method: string().min(1),
12526
13069
  args: record(string().min(1), CompositionForwardArgSchema).optional()
12527
13070
  });
12528
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
13071
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
13072
+ CompositionForwardCommandSchema,
13073
+ CompositionCodeSourceSchema,
13074
+ object({
13075
+ kind: literal("extension"),
13076
+ target: literal("remote")
13077
+ })
13078
+ ]);
12529
13079
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12530
13080
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12531
13081
  /**
@@ -12570,7 +13120,7 @@ var CompositionSchema = object({
12570
13120
  kind: literal("existing"),
12571
13121
  device: CompositionSourceRefSchema
12572
13122
  })]),
12573
- features: array(CompositionFeatureSchema).min(1).max(16),
13123
+ features: array(CompositionFeatureSchema).min(0).max(16),
12574
13124
  /**
12575
13125
  * Present when the block is a helper template. Absent on a free
12576
13126
  * composition (the room example, device 4603). The editor reads this,
@@ -12602,6 +13152,73 @@ var NodeColorIdSchema = _enum([
12602
13152
  "node-g",
12603
13153
  "node-h"
12604
13154
  ]);
13155
+ //#endregion
13156
+ //#region ../types/dist/index.mjs
13157
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
13158
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13159
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13160
+ /** The text excerpt a message carries on the bus. */
13161
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13162
+ var PortSchema = number().int().min(1).max(65535);
13163
+ /** A recipient local-part: what stands before the `@`. */
13164
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13165
+ object({
13166
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13167
+ bindHost: string().min(1).default("0.0.0.0"),
13168
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13169
+ requireAuth: boolean().default(true),
13170
+ username: string().default(""),
13171
+ password: string().default("").meta({ secret: true }),
13172
+ starttls: boolean().default(false),
13173
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13174
+ });
13175
+ object({
13176
+ host: string().min(1).nullable(),
13177
+ hostReason: string().nullable(),
13178
+ port: PortSchema,
13179
+ domain: string().min(1),
13180
+ requireAuth: boolean(),
13181
+ username: string(),
13182
+ password: string(),
13183
+ starttls: boolean()
13184
+ });
13185
+ object({
13186
+ recipients: array(SmtpRecipientSchema).min(1),
13187
+ /**
13188
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13189
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13190
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13191
+ * Absent, the subscriber is anonymous and unique.
13192
+ */
13193
+ subscriberId: string().min(1).max(64).optional()
13194
+ });
13195
+ var SmtpInboundAttachmentSchema = object({
13196
+ filename: string(),
13197
+ contentType: string(),
13198
+ bytes: number().int().nonnegative()
13199
+ });
13200
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13201
+ var SmtpInboundMessageSchema = object({
13202
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13203
+ seq: number().int().positive(),
13204
+ messageId: string(),
13205
+ receivedAt: number().int().nonnegative(),
13206
+ from: string(),
13207
+ to: string(),
13208
+ subject: string(),
13209
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13210
+ attachments: array(SmtpInboundAttachmentSchema)
13211
+ });
13212
+ object({
13213
+ /** The newest seq issued so far (0 before any mail). */
13214
+ lastSeq: number().int().nonnegative(),
13215
+ /** The oldest seq still retained; messages below it are gone. */
13216
+ oldestSeq: number().int().nonnegative(),
13217
+ messages: array(object({
13218
+ key: string(),
13219
+ message: SmtpInboundMessageSchema
13220
+ }))
13221
+ });
12605
13222
  new Set([
12606
13223
  "track",
12607
13224
  "face",
@@ -12721,6 +13338,205 @@ var IntegrationInstanceRefSchema = object({
12721
13338
  ]),
12722
13339
  candidateStableId: string().optional()
12723
13340
  });
13341
+ /**
13342
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13343
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13344
+ *
13345
+ * Every instance is one record of the addon's own kind, keyed by the
13346
+ * `integrations` registry id. The records door does the secret work: a field the
13347
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13348
+ * deleting the row deletes the secret, and one row's value is never another's.
13349
+ *
13350
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13351
+ * token or the view the form was given KEEP the stored secret;
13352
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13353
+ */
13354
+ /** The most instances one addon lists (an integrations page, not a table). */
13355
+ var INSTANCES_LIST_MAX = 1e3;
13356
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13357
+ function topLevelSecretKeys(paths) {
13358
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13359
+ }
13360
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13361
+ function secretMapKeys(paths) {
13362
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13363
+ }
13364
+ /**
13365
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13366
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13367
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13368
+ */
13369
+ function secretMapPatch(value) {
13370
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13371
+ const out = {};
13372
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13373
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13374
+ return Object.keys(out).length > 0 ? out : null;
13375
+ }
13376
+ /** Does the kind's body schema carry an `enabled` field? */
13377
+ function declaresEnabled(kind) {
13378
+ const json = toJSONSchema(kind.schema, {
13379
+ unrepresentable: "any",
13380
+ io: "input"
13381
+ });
13382
+ const properties = Reflect.get(json, "properties");
13383
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13384
+ }
13385
+ var RecordIntegrationInstances = class {
13386
+ records;
13387
+ kind;
13388
+ hooks;
13389
+ handle;
13390
+ secretKeys;
13391
+ secretMaps;
13392
+ nameField;
13393
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13394
+ registryOwnsEnabled;
13395
+ constructor(records, kind, hooks) {
13396
+ this.records = records;
13397
+ this.kind = kind;
13398
+ this.hooks = hooks;
13399
+ this.handle = records.kind(kind);
13400
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13401
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13402
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13403
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13404
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13405
+ }
13406
+ /**
13407
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13408
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13409
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13410
+ */
13411
+ async readResolved(id) {
13412
+ try {
13413
+ const record = await this.handle.getResolved(id);
13414
+ return record === null ? null : {
13415
+ id,
13416
+ body: record.data,
13417
+ enabled: record.data.enabled !== false,
13418
+ secrets: "resolved"
13419
+ };
13420
+ } catch (err) {
13421
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13422
+ const stored = await this.handle.get(id);
13423
+ if (stored === null) return null;
13424
+ return {
13425
+ id,
13426
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13427
+ enabled: stored.data.enabled !== false,
13428
+ secrets: "unavailable"
13429
+ };
13430
+ }
13431
+ }
13432
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13433
+ async getResolved(id) {
13434
+ return (await this.readResolved(id))?.body ?? null;
13435
+ }
13436
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13437
+ async listResolved() {
13438
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13439
+ const out = [];
13440
+ for (const record of page.records) {
13441
+ const read = await this.readResolved(record.id);
13442
+ if (read !== null) out.push(read);
13443
+ }
13444
+ return out;
13445
+ }
13446
+ viewOf(record) {
13447
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13448
+ const rawName = record.data[this.nameField];
13449
+ const kind = this.hooks.kindOf?.(record.data);
13450
+ const health = this.hooks.health?.(record.data, record.id);
13451
+ return {
13452
+ id: record.id,
13453
+ name: typeof rawName === "string" ? rawName : record.id,
13454
+ configured: this.hooks.configured(record.data),
13455
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13456
+ formValues: shown,
13457
+ ...kind !== void 0 ? { kind } : {},
13458
+ ...health?.status !== void 0 ? { status: health.status } : {},
13459
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13460
+ };
13461
+ }
13462
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13463
+ ...this.hooks.description,
13464
+ mintsId: true
13465
+ };
13466
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13467
+ listInstances = async () => {
13468
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13469
+ };
13470
+ getInstance = async ({ id }) => {
13471
+ const record = await this.handle.get(id);
13472
+ return record === null ? null : this.viewOf(record);
13473
+ };
13474
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13475
+ createInstance = async ({ id, name, kind }) => {
13476
+ const minted = id ?? this.hooks.mintId?.({
13477
+ name,
13478
+ ...kind !== void 0 ? { kind } : {}
13479
+ });
13480
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13481
+ await this.handle.put({
13482
+ id: minted,
13483
+ data: this.hooks.initialBody(name, kind)
13484
+ }, { ifRev: 0 });
13485
+ await this.hooks.onChanged?.(minted, "created");
13486
+ return { id: minted };
13487
+ };
13488
+ updateInstance = async ({ id, name, values }) => {
13489
+ const patch = {};
13490
+ for (const [key, value] of Object.entries(values)) {
13491
+ if (value === void 0) continue;
13492
+ if (this.secretMaps.has(key)) {
13493
+ const entries = secretMapPatch(value);
13494
+ if (entries !== null) patch[key] = entries;
13495
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13496
+ else if (isSecretClear(value)) patch[key] = null;
13497
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13498
+ }
13499
+ if (name !== void 0) patch[this.nameField] = name;
13500
+ await this.handle.patch(id, patch);
13501
+ await this.hooks.onChanged?.(id, "updated");
13502
+ return { ok: true };
13503
+ };
13504
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13505
+ setInstanceEnabled = async ({ id, enabled }) => {
13506
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13507
+ await this.handle.patch(id, { enabled });
13508
+ await this.hooks.onChanged?.(id, "enabled");
13509
+ return { ok: true };
13510
+ };
13511
+ /** The row and its keychain refs go together (the records door). */
13512
+ deleteInstance = async ({ id }) => {
13513
+ await this.handle.delete(id);
13514
+ await this.hooks.onChanged?.(id, "deleted");
13515
+ return { ok: true };
13516
+ };
13517
+ testInstance = async (input) => {
13518
+ if (this.hooks.test === void 0) return {
13519
+ success: false,
13520
+ status: "unsupported",
13521
+ error: "This integration declares no connection test"
13522
+ };
13523
+ return this.hooks.test(input);
13524
+ };
13525
+ /** The flat provider object to hand to `registerProvider`. */
13526
+ toProvider() {
13527
+ return {
13528
+ describe: this.describe,
13529
+ instanceFormSchema: this.instanceFormSchema,
13530
+ listInstances: this.listInstances,
13531
+ getInstance: this.getInstance,
13532
+ createInstance: this.createInstance,
13533
+ updateInstance: this.updateInstance,
13534
+ setInstanceEnabled: this.setInstanceEnabled,
13535
+ deleteInstance: this.deleteInstance,
13536
+ testInstance: this.testInstance
13537
+ };
13538
+ }
13539
+ };
12724
13540
  var EncodeProfileSchema = object({
12725
13541
  video: object({
12726
13542
  codec: _enum([
@@ -13756,7 +14572,13 @@ var MigrationLedgerEntrySchema = object({
13756
14572
  * joined by the engine from the catalogues it knows this boot. Absent when no
13757
14573
  * catalogue lists the id (a newer database).
13758
14574
  */
13759
- meta: MigrationMetaSchema.optional()
14575
+ meta: MigrationMetaSchema.optional(),
14576
+ /**
14577
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14578
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14579
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14580
+ */
14581
+ ownerNotLoaded: boolean().optional()
13760
14582
  });
13761
14583
  /** A catalogued migration that is not applied, with where it stands. */
13762
14584
  var MigrationPendingEntrySchema = object({
@@ -13790,6 +14612,221 @@ var MigrationOwnerStepSchema = object({
13790
14612
  state: union([literal("pending"), MigrationStatusSchema])
13791
14613
  });
13792
14614
  /**
14615
+ * Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
14616
+ * kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
14617
+ * the integration id, the devices stamped with it and the automations that name it do not change.
14618
+ *
14619
+ * Order, so a crash or a locked keychain is safe at every point:
14620
+ *
14621
+ * 1. Phase A, read only: every row of every integration of the addon is read with its secrets
14622
+ * resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
14623
+ * anything is written; `block-owner` then holds the addon.
14624
+ * 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
14625
+ * `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
14626
+ * and every secret leaf must equal what the rows held, else the record is deleted and the pass
14627
+ * throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
14628
+ * a warning names the path. Only then are the rows (and their keychain values) cleared.
14629
+ *
14630
+ * Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
14631
+ * a no-op. A default integration with no rows still gets its record (an empty body, D985).
14632
+ * Never logs or throws a secret value; messages name integration ids and secret PATHS only.
14633
+ */
14634
+ function secretLeavesOf(body, spec) {
14635
+ const out = /* @__PURE__ */ new Map();
14636
+ for (const leaf of collectSecretLeaves(body, spec.secretPaths, spec.secretArrayKeys ?? {})) if (typeof leaf.value === "string" && leaf.value.length > 0) out.set(leaf.concrete, leaf.value);
14637
+ return out;
14638
+ }
14639
+ function why(err) {
14640
+ return err instanceof Error ? err.message : String(err);
14641
+ }
14642
+ async function moveIntegrationRowsToRecords(ports, spec) {
14643
+ const integrations = await ports.listIntegrations(spec.addonId);
14644
+ if (integrations.length === 0) return {
14645
+ outcome: "already-done",
14646
+ summary: { integrations: 0 }
14647
+ };
14648
+ const rowsOf = /* @__PURE__ */ new Map();
14649
+ for (const integration of integrations) try {
14650
+ rowsOf.set(integration.id, await ports.readRows(integration.id));
14651
+ } catch (err) {
14652
+ throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
14653
+ }
14654
+ let created = 0;
14655
+ let cleared = 0;
14656
+ let kept = 0;
14657
+ for (const integration of integrations) {
14658
+ const values = rowsOf.get(integration.id) ?? {};
14659
+ const hasRows = Object.keys(values).length > 0;
14660
+ const body = spec.bodyFrom(values, ports, integration);
14661
+ const put = await ports.putRecord(integration.id, body);
14662
+ if (put === "created") created += 1;
14663
+ if (!hasRows) continue;
14664
+ const wanted = secretLeavesOf(body, spec);
14665
+ const back = await ports.readRecord(integration.id);
14666
+ if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
14667
+ const held = secretLeavesOf(back, spec);
14668
+ if (put === "created") {
14669
+ const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
14670
+ if (wrong.length > 0) {
14671
+ await ports.deleteRecord(integration.id);
14672
+ throw new Error(`integration '${integration.id}': ${wrong.join(", ")} did not read back from the record - the record was removed and the legacy settings are kept`);
14673
+ }
14674
+ } else {
14675
+ const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
14676
+ if (missing.length > 0) {
14677
+ ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
14678
+ kept += 1;
14679
+ continue;
14680
+ }
14681
+ }
14682
+ await ports.clearRows(integration.id);
14683
+ cleared += 1;
14684
+ }
14685
+ return created === 0 && cleared === 0 ? {
14686
+ outcome: "already-done",
14687
+ summary: {
14688
+ integrations: integrations.length,
14689
+ kept
14690
+ }
14691
+ } : {
14692
+ outcome: "done",
14693
+ summary: {
14694
+ integrations: integrations.length,
14695
+ created,
14696
+ cleared,
14697
+ kept
14698
+ }
14699
+ };
14700
+ }
14701
+ /**
14702
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14703
+ * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14704
+ * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14705
+ * the new record through the addon's own records handle. The hub deletes the rows and their
14706
+ * keychain values; the addon never touches the registry.
14707
+ */
14708
+ function integrationRowsPortsOverApi(deps) {
14709
+ return {
14710
+ warn: (message) => deps.log.warn(message),
14711
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14712
+ id: i.id,
14713
+ name: i.name
14714
+ })),
14715
+ readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14716
+ putRecord: async (id, body) => {
14717
+ if (await deps.records.get(id) !== null) return "exists";
14718
+ try {
14719
+ await deps.records.put({
14720
+ id,
14721
+ data: body
14722
+ }, { ifRev: 0 });
14723
+ } catch (err) {
14724
+ if (err instanceof RevConflictError) return "exists";
14725
+ throw err;
14726
+ }
14727
+ return "created";
14728
+ },
14729
+ readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
14730
+ deleteRecord: async (id) => {
14731
+ await deps.records.delete(id);
14732
+ },
14733
+ clearRows: async (integrationId) => {
14734
+ await deps.api.clearSettings({ id: integrationId });
14735
+ }
14736
+ };
14737
+ }
14738
+ /**
14739
+ * The rows-to-record migration of a device addon whose integrations hold REAL settings
14740
+ * (planning#416, S-B): every integration of the addon becomes a record of the addon's own kind
14741
+ * under its registry id, the record body built from the legacy `integration_settings` rows, and
14742
+ * the rows and their keychain values are cleared once the record is read back
14743
+ * (`moveIntegrationRowsToRecords`). The addon declares ONE entry kind; this file turns that kind
14744
+ * into the body, the migration and its ports, so an addon states nothing but the kind.
14745
+ *
14746
+ * It runs in the addon process through the hub API, BEFORE the addon registers its
14747
+ * `integration-instances` provider, and nothing reads the old rows afterwards.
14748
+ */
14749
+ /**
14750
+ * The record body of one integration from its resolved legacy rows. A key the kind has no field
14751
+ * for, or whose value the field refuses, is dropped and named in a warning (keys only, never a
14752
+ * value); a blank value is no value. A secret the keychain could not serve (a marker, not a
14753
+ * string) throws: moving it would lose it, and the rows are still there.
14754
+ */
14755
+ function integrationBodyFromRows(kind, values, log, integration) {
14756
+ const fields = new Map(Object.entries(kind.schema.shape));
14757
+ const body = { name: integration.name };
14758
+ const dropped = [];
14759
+ for (const [key, value] of Object.entries(values)) {
14760
+ if (isSecretMarkerLike(value)) throw new Error(`integration '${integration.id}': ${key} could not be read from the keychain - nothing moved`);
14761
+ if (value === void 0 || value === null || value === "") continue;
14762
+ const field = key === "name" ? void 0 : fields.get(key);
14763
+ if (field === void 0 || !field.safeParse(value).success) {
14764
+ dropped.push(key);
14765
+ continue;
14766
+ }
14767
+ body[key] = value;
14768
+ }
14769
+ if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings the ${kind.name} record has no valid field for are dropped: ${dropped.join(", ")}`);
14770
+ return kind.schema.parse(body);
14771
+ }
14772
+ function integrationRowsToRecordId(addonId, date) {
14773
+ return `${addonId}/${date}-integration-rows-to-record`;
14774
+ }
14775
+ function integrationRowsMigration(spec) {
14776
+ return {
14777
+ meta: {
14778
+ id: integrationRowsToRecordId(spec.addonId, spec.date),
14779
+ owner: spec.addonId,
14780
+ level: "addon-state",
14781
+ seq: 1,
14782
+ dependsOn: [],
14783
+ store: "settings",
14784
+ scope: "cluster",
14785
+ onFailure: "block-owner",
14786
+ bodyKind: "procedure",
14787
+ description: "Move every integration of the addon into a typed instance record keyed by its registry id, with its connection settings and secrets, then clear the legacy integration settings rows",
14788
+ fixtureMark: spec.fixtureMark
14789
+ },
14790
+ procedure: (ports) => moveIntegrationRowsToRecords(ports, {
14791
+ addonId: spec.addonId,
14792
+ secretPaths: spec.kind.declaration.secretPaths,
14793
+ secretArrayKeys: spec.kind.declaration.secretArrayKeys,
14794
+ bodyFrom: (values, log, integration) => integrationBodyFromRows(spec.kind, values, log, integration)
14795
+ })
14796
+ };
14797
+ }
14798
+ /** The hub and records sides of {@link integrationRowsMigration}. */
14799
+ function integrationRowsPortsOver(deps) {
14800
+ return integrationRowsPortsOverApi({
14801
+ api: deps.api,
14802
+ records: deps.records.kind({
14803
+ name: deps.kind.name,
14804
+ declaration: deps.kind.declaration,
14805
+ schema: record(string(), unknown())
14806
+ }),
14807
+ log: { warn: deps.warn }
14808
+ });
14809
+ }
14810
+ function defaultIntegrationApiOver(api) {
14811
+ return {
14812
+ list: () => api.integrations.list.query(),
14813
+ getSettings: (input) => api.integrations.getSettings.query(input),
14814
+ clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14815
+ listDevices: (addonId) => api.deviceManager.listAll.query({
14816
+ addonId,
14817
+ projection: "slim"
14818
+ }),
14819
+ setDeviceIntegrationId: async (deviceId, integrationId) => {
14820
+ await api.deviceManager.setIntegrationId.mutate({
14821
+ deviceId,
14822
+ integrationId
14823
+ });
14824
+ },
14825
+ readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
14826
+ readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
14827
+ };
14828
+ }
14829
+ /**
13793
14830
  * The trainer contract — what the hub hands the cloud trainer and what it
13794
14831
  * gets back (cloud training, D805+).
13795
14832
  *
@@ -18874,24 +19911,52 @@ var APPLIANCE_CAP_VOCABULARY = {
18874
19911
  args: { on: false }
18875
19912
  },
18876
19913
  {
18877
- id: "petFountain.intervalOn",
18878
- method: "setPumpInterval",
19914
+ id: "petFountain.resetDailyConsumption",
19915
+ method: "resetDailyConsumption",
19916
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19917
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19918
+ icon: "rotate-ccw"
19919
+ }
19920
+ ],
19921
+ state: [{
19922
+ kind: "boolean",
19923
+ field: "empty",
19924
+ label: t("Tank empty", "Serbatoio vuoto"),
19925
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19926
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19927
+ icon: "droplet"
19928
+ }, {
19929
+ kind: "numeric",
19930
+ field: "waterLevel",
19931
+ quantity: t("the water level", "il livello dell’acqua"),
19932
+ label: t("Water level", "Livello dell’acqua"),
19933
+ unit: "%",
19934
+ icon: "droplets"
19935
+ }]
19936
+ },
19937
+ "pump-cycle": {
19938
+ scope: "device",
19939
+ noun: t("Pump cycle", "Ciclo della pompa"),
19940
+ actions: [
19941
+ {
19942
+ id: "pumpCycle.on",
19943
+ method: "setEnabled",
18879
19944
  label: t("Pump cycle on", "Ciclo pompa attivo"),
18880
19945
  phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
18881
19946
  icon: "timer",
18882
19947
  args: { on: true }
18883
19948
  },
18884
19949
  {
18885
- id: "petFountain.intervalOff",
18886
- method: "setPumpInterval",
19950
+ id: "pumpCycle.off",
19951
+ method: "setEnabled",
18887
19952
  label: t("Pump cycle off", "Ciclo pompa spento"),
18888
19953
  phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
18889
19954
  icon: "timer-off",
18890
19955
  args: { on: false }
18891
19956
  },
18892
19957
  {
18893
- id: "petFountain.pumpOnMinutes",
18894
- method: "setPumpOnMinutes",
19958
+ id: "pumpCycle.onMinutes",
19959
+ method: "setOnMinutes",
18895
19960
  label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
18896
19961
  phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
18897
19962
  icon: "timer",
@@ -18907,8 +19972,8 @@ var APPLIANCE_CAP_VOCABULARY = {
18907
19972
  }]
18908
19973
  },
18909
19974
  {
18910
- id: "petFountain.pumpOffMinutes",
18911
- method: "setPumpOffMinutes",
19975
+ id: "pumpCycle.offMinutes",
19976
+ method: "setOffMinutes",
18912
19977
  label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
18913
19978
  phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
18914
19979
  icon: "timer-off",
@@ -18922,35 +19987,63 @@ var APPLIANCE_CAP_VOCABULARY = {
18922
19987
  unit: "min"
18923
19988
  }
18924
19989
  }]
18925
- },
18926
- {
18927
- id: "petFountain.resetDailyConsumption",
18928
- method: "resetDailyConsumption",
18929
- label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
18930
- phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
18931
- icon: "rotate-ccw"
18932
19990
  }
18933
19991
  ],
18934
19992
  state: [{
18935
19993
  kind: "boolean",
18936
- field: "empty",
18937
- label: t("Tank empty", "Serbatoio vuoto"),
18938
- whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
18939
- whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
18940
- icon: "droplet"
18941
- }, {
18942
- kind: "numeric",
18943
- field: "waterLevel",
18944
- quantity: t("the water level", "il livello dell’acqua"),
18945
- label: t("Water level", "Livello dell’acqua"),
18946
- unit: "%",
18947
- icon: "droplets"
19994
+ field: "enabled",
19995
+ label: t("Pump cycle", "Ciclo pompa"),
19996
+ whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
19997
+ whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
19998
+ icon: "timer"
18948
19999
  }]
18949
20000
  },
18950
20001
  "media-player": {
18951
20002
  scope: "device",
18952
20003
  noun: t("Media player", "Lettore multimediale"),
18953
20004
  actions: [
20005
+ {
20006
+ id: "media.turnOn",
20007
+ method: "turnOn",
20008
+ label: t("Turn on", "Accendi"),
20009
+ phrase: t("Turn on {device}", "Accendi {device}"),
20010
+ icon: "power"
20011
+ },
20012
+ {
20013
+ id: "media.turnOff",
20014
+ method: "turnOff",
20015
+ label: t("Turn off", "Spegni"),
20016
+ phrase: t("Turn off {device}", "Spegni {device}"),
20017
+ icon: "power"
20018
+ },
20019
+ {
20020
+ id: "media.volumeUp",
20021
+ method: "volumeUp",
20022
+ label: t("Volume up", "Alza il volume"),
20023
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
20024
+ icon: "volume-2"
20025
+ },
20026
+ {
20027
+ id: "media.volumeDown",
20028
+ method: "volumeDown",
20029
+ label: t("Volume down", "Abbassa il volume"),
20030
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
20031
+ icon: "volume-2"
20032
+ },
20033
+ {
20034
+ id: "media.channelUp",
20035
+ method: "channelUp",
20036
+ label: t("Channel up", "Canale successivo"),
20037
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
20038
+ icon: "skip-forward"
20039
+ },
20040
+ {
20041
+ id: "media.channelDown",
20042
+ method: "channelDown",
20043
+ label: t("Channel down", "Canale precedente"),
20044
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
20045
+ icon: "skip-back"
20046
+ },
18954
20047
  {
18955
20048
  id: "media.play",
18956
20049
  method: "play",
@@ -20837,6 +21930,7 @@ var SYSTEM_CAP_VOCABULARY = {
20837
21930
  "audio-analyzer": system(PIPELINE),
20838
21931
  "audio-codec": system(INTERNAL),
20839
21932
  "auth-provider": system(IDENTITY),
21933
+ "board-key-source": system(INTERNAL),
20840
21934
  backup: system("system backups — run and restored from the Backups page"),
20841
21935
  broker: system("message-broker connections — configured, not actuated"),
20842
21936
  "calendar-source": system(INTERNAL),
@@ -45746,6 +46840,49 @@ var binaryCapability = {
45746
46840
  volatileStateFields: ["lastChangedAt"]
45747
46841
  };
45748
46842
  /**
46843
+ * Why a board key could not be read. Typed so a caller reports the cause and
46844
+ * never treats "could not read" as "has no key" (D49).
46845
+ */
46846
+ var BoardKeyFailureReasonSchema = _enum([
46847
+ "unknown-source",
46848
+ "not-admin",
46849
+ "no-builder",
46850
+ "no-ingress-url",
46851
+ "unreachable",
46852
+ "unknown-board",
46853
+ "no-encryption-key",
46854
+ "unreadable-config"
46855
+ ]);
46856
+ var BoardKeySourceSchema = object({
46857
+ sourceId: string(),
46858
+ name: string()
46859
+ });
46860
+ var BoardKeyBoardSchema = object({
46861
+ /** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
46862
+ configuration: string(),
46863
+ /** The node name the builder gives the board. */
46864
+ name: string(),
46865
+ /** The address the builder knows the board by, when it has one. */
46866
+ address: string().nullable()
46867
+ });
46868
+ var BoardKeyFailureSchema = object({
46869
+ ok: literal(false),
46870
+ reason: BoardKeyFailureReasonSchema,
46871
+ detail: string().optional()
46872
+ });
46873
+ var ListBoardsResultSchema = discriminatedUnion("ok", [object({
46874
+ ok: literal(true),
46875
+ boards: array(BoardKeyBoardSchema).readonly()
46876
+ }), BoardKeyFailureSchema]);
46877
+ var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
46878
+ ok: literal(true),
46879
+ key: string()
46880
+ }), BoardKeyFailureSchema]);
46881
+ method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
46882
+ sourceId: string().min(1),
46883
+ configuration: string().min(1)
46884
+ }), GetBoardKeyResultSchema, { auth: "admin" });
46885
+ /**
45749
46886
  * Dimmable-light brightness control. Co-exists with `switch` on the
45750
46887
  * same device — the switch toggles on/off, this cap sets the level
45751
46888
  * applied when the light is on. Drivers map their per-vendor dim
@@ -48489,6 +49626,244 @@ var controlCapability = {
48489
49626
  */
48490
49627
  history: "none"
48491
49628
  };
49629
+ /**
49630
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49631
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49632
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49633
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49634
+ *
49635
+ * The cap declares a wide method surface; not every backend supports
49636
+ * every method. Drivers signal sub-capability via the
49637
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49638
+ * corresponding method) so the UI gates buttons without a slice
49639
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49640
+ */
49641
+ var MediaPlayerStateSchema = _enum([
49642
+ "off",
49643
+ "on",
49644
+ "idle",
49645
+ "playing",
49646
+ "paused",
49647
+ "buffering",
49648
+ "standby"
49649
+ ]);
49650
+ var MediaPlayerRepeatSchema = _enum([
49651
+ "off",
49652
+ "all",
49653
+ "one"
49654
+ ]);
49655
+ var MediaInfoSchema = object({
49656
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49657
+ * `podcast`, …). */
49658
+ type: string(),
49659
+ /** Human-readable title. */
49660
+ title: string(),
49661
+ /** Optional artist / channel / station label. */
49662
+ artist: string().optional(),
49663
+ /** Optional album / season / show name. */
49664
+ album: string().optional(),
49665
+ /** Optional cover-art / thumbnail URL. */
49666
+ imageUrl: string().optional()
49667
+ });
49668
+ /**
49669
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49670
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49671
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49672
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49673
+ * guess, never as a reading.
49674
+ */
49675
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49676
+ /** The fields of a media-player status a driver can only guess. */
49677
+ var MediaPlayerAssumedFieldSchema = _enum([
49678
+ "state",
49679
+ "volumeLevel",
49680
+ "isMuted",
49681
+ "source"
49682
+ ]);
49683
+ var MediaPlayerStatusSchema = object({
49684
+ /** Playback lifecycle state. */
49685
+ state: MediaPlayerStateSchema,
49686
+ /**
49687
+ * Whether the state fields are readings or the driver's own bookkeeping.
49688
+ * A slice written before this field existed parses as `reported`.
49689
+ */
49690
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49691
+ /**
49692
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49693
+ * this is not empty; a field not listed here is a reading even then (power
49694
+ * from a running-state source, mute guessed from the last send).
49695
+ */
49696
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49697
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49698
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49699
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
49700
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49701
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49702
+ isMuted: boolean().nullable(),
49703
+ /** Active source / input. Empty when no source surface. */
49704
+ source: string(),
49705
+ /** Selectable sources. Empty when no source surface.
49706
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49707
+ availableSources: array(string()),
49708
+ /** Currently-playing media info. Null when nothing is playing. */
49709
+ currentMedia: MediaInfoSchema.nullable(),
49710
+ /** Current playback position in ms. Null when not seekable or
49711
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
49712
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49713
+ /** Total duration of the current media in ms. Null when unknown
49714
+ * (live stream) or nothing is playing. */
49715
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49716
+ /** Shuffle toggle. Null when no shuffle surface.
49717
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
49718
+ shuffle: boolean().nullable(),
49719
+ /** Repeat mode. Null when no repeat surface.
49720
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
49721
+ repeat: MediaPlayerRepeatSchema.nullable(),
49722
+ /** Ms epoch when the slice was last updated. */
49723
+ lastChangedAt: number()
49724
+ });
49725
+ var stepInput = object({
49726
+ deviceId: number().int().nonnegative(),
49727
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
49728
+ steps: number().int().min(1).max(50).default(1)
49729
+ });
49730
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
49731
+ var mediaPlayerCapability = {
49732
+ name: "media-player",
49733
+ scope: "device",
49734
+ deviceNative: true,
49735
+ mode: "singleton",
49736
+ deviceTypes: [DeviceType.MediaPlayer],
49737
+ methods: {
49738
+ turnOn: method(deviceOnly, _void(), {
49739
+ kind: "mutation",
49740
+ auth: "protected"
49741
+ }),
49742
+ turnOff: method(deviceOnly, _void(), {
49743
+ kind: "mutation",
49744
+ auth: "protected"
49745
+ }),
49746
+ volumeUp: method(stepInput, _void(), {
49747
+ kind: "mutation",
49748
+ auth: "protected"
49749
+ }),
49750
+ volumeDown: method(stepInput, _void(), {
49751
+ kind: "mutation",
49752
+ auth: "protected"
49753
+ }),
49754
+ channelUp: method(stepInput, _void(), {
49755
+ kind: "mutation",
49756
+ auth: "protected"
49757
+ }),
49758
+ channelDown: method(stepInput, _void(), {
49759
+ kind: "mutation",
49760
+ auth: "protected"
49761
+ }),
49762
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49763
+ kind: "mutation",
49764
+ auth: "protected"
49765
+ }),
49766
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49767
+ kind: "mutation",
49768
+ auth: "protected"
49769
+ }),
49770
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49771
+ kind: "mutation",
49772
+ auth: "protected"
49773
+ }),
49774
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49775
+ kind: "mutation",
49776
+ auth: "protected"
49777
+ }),
49778
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49779
+ kind: "mutation",
49780
+ auth: "protected"
49781
+ }),
49782
+ seek: method(object({
49783
+ deviceId: number().int().nonnegative(),
49784
+ position: number().int().nonnegative()
49785
+ }), _void(), {
49786
+ kind: "mutation",
49787
+ auth: "protected"
49788
+ }),
49789
+ setVolume: method(object({
49790
+ deviceId: number().int().nonnegative(),
49791
+ volumeLevel: number().min(0).max(100)
49792
+ }), _void(), {
49793
+ kind: "mutation",
49794
+ auth: "protected"
49795
+ }),
49796
+ setMute: method(object({
49797
+ deviceId: number().int().nonnegative(),
49798
+ muted: boolean()
49799
+ }), _void(), {
49800
+ kind: "mutation",
49801
+ auth: "protected"
49802
+ }),
49803
+ setShuffle: method(object({
49804
+ deviceId: number().int().nonnegative(),
49805
+ shuffle: boolean()
49806
+ }), _void(), {
49807
+ kind: "mutation",
49808
+ auth: "protected"
49809
+ }),
49810
+ setRepeat: method(object({
49811
+ deviceId: number().int().nonnegative(),
49812
+ repeat: MediaPlayerRepeatSchema
49813
+ }), _void(), {
49814
+ kind: "mutation",
49815
+ auth: "protected"
49816
+ }),
49817
+ selectSource: method(object({
49818
+ deviceId: number().int().nonnegative(),
49819
+ source: string().min(1)
49820
+ }), _void(), {
49821
+ kind: "mutation",
49822
+ auth: "protected"
49823
+ }),
49824
+ playMedia: method(object({
49825
+ deviceId: number().int().nonnegative(),
49826
+ /** Media identifier / URL. */
49827
+ mediaId: string().min(1),
49828
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
49829
+ * passes it through to the upstream service. */
49830
+ mediaType: string().min(1)
49831
+ }), _void(), {
49832
+ kind: "mutation",
49833
+ auth: "protected"
49834
+ })
49835
+ },
49836
+ status: {
49837
+ schema: MediaPlayerStatusSchema,
49838
+ kind: "push"
49839
+ },
49840
+ /**
49841
+ * Runtime-state slice — mirrored by the kernel. UI players read the
49842
+ * full slice for live now-playing, volume, and progress updates
49843
+ * without polling.
49844
+ */
49845
+ runtimeState: MediaPlayerStatusSchema,
49846
+ /**
49847
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
49848
+ *
49849
+ * See `RuntimeStateDurability`. Enforced by
49850
+ * `scripts/check-runtime-state-durability.ts`.
49851
+ */
49852
+ durability: "session",
49853
+ /**
49854
+ * History (D850): volume (a level an operator or an automation sets; the
49855
+ * default 10 s floor folds a slider drag into its end value) and mute.
49856
+ * `position` is not recorded: it moves continuously while playing.
49857
+ */
49858
+ history: { fields: {
49859
+ volumeLevel: {
49860
+ kind: "gauge",
49861
+ unit: "%",
49862
+ deadband: 1
49863
+ },
49864
+ isMuted: { kind: "boolean" }
49865
+ } }
49866
+ };
48492
49867
  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;
48493
49868
  var NUMBER = {
48494
49869
  kind: "fixed",
@@ -48936,117 +50311,6 @@ function actuator(input) {
48936
50311
  };
48937
50312
  }
48938
50313
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48939
- var PowerSourceRefusalReasonSchema = _enum([
48940
- "no-power-source",
48941
- "switch-missing",
48942
- "switch-offline",
48943
- "switch-unknown",
48944
- "restart-in-progress",
48945
- "switch-call-failed"
48946
- ]);
48947
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48948
- accepted: literal(false),
48949
- reason: PowerSourceRefusalReasonSchema,
48950
- /** One line for the operator, naming the switch. */
48951
- message: string()
48952
- })]);
48953
- var PowerSourceInfoSchema = object({
48954
- /** False when the device has no power source linked; the rest is then empty. */
48955
- linked: boolean(),
48956
- switchDeviceId: number().nullable(),
48957
- switchName: string().nullable(),
48958
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48959
- switchOn: boolean().nullable(),
48960
- switchOnline: boolean().nullable(),
48961
- restartInProgress: boolean(),
48962
- offSeconds: number().int()
48963
- });
48964
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
48965
- var PowerSourceStateSchema = object({
48966
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48967
- switchOn: boolean().nullable(),
48968
- offSeconds: number().int(),
48969
- lastFetchedAt: number()
48970
- });
48971
- var POWER_SOURCE_DEVICE_TYPES = [
48972
- DeviceType.Gateway,
48973
- DeviceType.Hub,
48974
- DeviceType.Camera,
48975
- DeviceType.Sensor,
48976
- DeviceType.MediaPlayer,
48977
- DeviceType.Vacuum,
48978
- DeviceType.Generic,
48979
- DeviceType.Washer,
48980
- DeviceType.Dryer,
48981
- DeviceType.Dishwasher,
48982
- DeviceType.PetFountain,
48983
- DeviceType.Fan,
48984
- DeviceType.Humidifier,
48985
- DeviceType.Climate,
48986
- DeviceType.WaterHeater
48987
- ];
48988
- var powerSourceCustomize = {
48989
- version: 1,
48990
- targets: [{
48991
- id: "switch",
48992
- cardinality: "scalar",
48993
- vocabulary: {
48994
- cap: "power-source",
48995
- stateField: "switchOn"
48996
- },
48997
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
48998
- apply: "add",
48999
- path: "switchOn",
49000
- sources: [{
49001
- kind: "switch",
49002
- transforms: [{ kind: "identity" }]
49003
- }],
49004
- companions: [{
49005
- kind: "constant",
49006
- path: "offSeconds",
49007
- value: 10,
49008
- when: "add"
49009
- }, {
49010
- kind: "clock",
49011
- path: "lastFetchedAt",
49012
- when: "always"
49013
- }],
49014
- sourceReport: { status: "assignment-status" }
49015
- }]
49016
- };
49017
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
49018
- var powerSourceCapability = {
49019
- name: "power-source",
49020
- scope: "device",
49021
- mode: "singleton",
49022
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
49023
- methods: {
49024
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
49025
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49026
- kind: "mutation",
49027
- auth: "admin"
49028
- }),
49029
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49030
- kind: "mutation",
49031
- auth: "admin"
49032
- }),
49033
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
49034
- kind: "mutation",
49035
- auth: "admin"
49036
- })
49037
- },
49038
- runtimeState: PowerSourceStateSchema,
49039
- customize: powerSourceCustomize,
49040
- /**
49041
- * Runtime-state durability: **session** — mirrored from the linked switch on
49042
- * its next report; a restored value would outlive the reading it names.
49043
- *
49044
- * See `RuntimeStateDurability`. Enforced by
49045
- * `scripts/check-runtime-state-durability.ts`.
49046
- */
49047
- durability: "session",
49048
- history: "none"
49049
- };
49050
50314
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
49051
50315
  DeviceType.Camera;
49052
50316
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -49074,6 +50338,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
49074
50338
  }).optional()
49075
50339
  })) });
49076
50340
  /**
50341
+ * A saved Customize assignment (D936). The schema is the type: callers use
50342
+ * `z.infer`, they do not restate the shape.
50343
+ */
50344
+ var OverrideSourceSchema = object({
50345
+ addonId: string().min(1),
50346
+ stableId: string().min(1),
50347
+ cap: string().min(1),
50348
+ fieldPath: string().min(1)
50349
+ });
50350
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50351
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50352
+ var ThresholdTransformDraftSchema = object({
50353
+ kind: literal("threshold"),
50354
+ comparison: _enum(["gte", "lte"]),
50355
+ threshold: number(),
50356
+ startDwellMs: number().nonnegative(),
50357
+ stopDwellMs: number().nonnegative()
50358
+ });
50359
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50360
+ IdentityTransformDraftSchema,
50361
+ InvertTransformDraftSchema,
50362
+ ThresholdTransformDraftSchema,
50363
+ object({
50364
+ kind: literal("linear"),
50365
+ scale: number(),
50366
+ offset: number(),
50367
+ clampMin: number().optional(),
50368
+ clampMax: number().optional()
50369
+ }),
50370
+ object({
50371
+ kind: literal("enum-map"),
50372
+ mapping: record(string(), string().nullable())
50373
+ }),
50374
+ object({ kind: literal("number") })
50375
+ ]);
50376
+ var ScalarOverrideDraftSchema = object({
50377
+ kind: literal("scalar"),
50378
+ cap: string().min(1),
50379
+ targetId: string().min(1),
50380
+ source: OverrideSourceSchema,
50381
+ transform: CustomizeTransformDraftSchema
50382
+ });
50383
+ var KeyedArrayOverrideDraftSchema = object({
50384
+ kind: literal("keyedArray"),
50385
+ cap: string().min(1),
50386
+ targetId: string().min(1),
50387
+ key: string().min(1),
50388
+ label: string().min(1),
50389
+ fieldPath: string().min(1),
50390
+ source: OverrideSourceSchema,
50391
+ transform: CustomizeTransformDraftSchema
50392
+ });
50393
+ var PulseOverrideDraftSchema = object({
50394
+ kind: literal("pulse"),
50395
+ cap: string().min(1),
50396
+ targetId: string().min(1),
50397
+ enabled: boolean(),
50398
+ debounceMs: number().nonnegative(),
50399
+ edges: array(_enum([
50400
+ "rising",
50401
+ "button",
50402
+ "native-event"
50403
+ ])).min(1).optional(),
50404
+ sources: array(OverrideSourceSchema).min(1)
50405
+ });
50406
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50407
+ "in-addition",
50408
+ "confirm",
50409
+ "replace"
50410
+ ]).optional() });
50411
+ discriminatedUnion("kind", [
50412
+ ScalarOverrideDraftSchema,
50413
+ KeyedArrayOverrideDraftSchema,
50414
+ PulseOverrideDraftSchema,
50415
+ object({
50416
+ kind: literal("span"),
50417
+ cap: string().min(1),
50418
+ targetId: string().min(1),
50419
+ timeoutMs: number().nonnegative(),
50420
+ cooldownMs: number().nonnegative().optional(),
50421
+ sources: array(SpanSourceDraftSchema)
50422
+ })
50423
+ ]);
50424
+ object({
50425
+ targetId: string().min(1),
50426
+ configured: boolean(),
50427
+ source: _enum(["native", "assigned"]),
50428
+ state: _enum([
50429
+ "active",
50430
+ "stale",
50431
+ "released",
50432
+ "unavailable"
50433
+ ]),
50434
+ reason: string().nullable()
50435
+ });
50436
+ /**
50437
+ * What a running-threshold proposal says (planning#405). The schema is the
50438
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50439
+ *
50440
+ * A proposal is read-only advice: it carries the numbers it was derived from
50441
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50442
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50443
+ */
50444
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50445
+ var RunningProposalConfidenceSchema = _enum([
50446
+ "high",
50447
+ "medium",
50448
+ "low"
50449
+ ]);
50450
+ var RunningProposalInsufficientReasonSchema = _enum([
50451
+ "no-history",
50452
+ "span-too-short",
50453
+ "truncated",
50454
+ "no-idle-floor",
50455
+ "no-runs",
50456
+ "no-separation"
50457
+ ]);
50458
+ var RunningProposalReplaySchema = object({
50459
+ /** Intervals the real dwell expression held true over the window. */
50460
+ intervals: number().int().nonnegative(),
50461
+ /** Intervals outside every run, or a second interval inside one. */
50462
+ extra: number().int().nonnegative(),
50463
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50464
+ missed: number().int().nonnegative()
50465
+ });
50466
+ var RunningProposalEvidenceSchema = object({
50467
+ tier: RunningProposalTierSchema,
50468
+ spanMs: number().nonnegative(),
50469
+ from: number(),
50470
+ to: number(),
50471
+ points: number().int().nonnegative(),
50472
+ runsFound: number().int().nonnegative(),
50473
+ pausesFound: number().int().nonnegative(),
50474
+ longestPauseMs: number().nonnegative(),
50475
+ idleFloorW: number(),
50476
+ noiseCeilingW: number(),
50477
+ plateauW: number(),
50478
+ replay: RunningProposalReplaySchema
50479
+ });
50480
+ var RunningProposalInsufficientEvidenceSchema = object({
50481
+ spanMs: number().nonnegative(),
50482
+ points: number().int().nonnegative(),
50483
+ runsFound: number().int().nonnegative()
50484
+ });
50485
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50486
+ state: literal("proposal"),
50487
+ transform: ThresholdTransformDraftSchema,
50488
+ evidence: RunningProposalEvidenceSchema,
50489
+ confidence: RunningProposalConfidenceSchema,
50490
+ /** Why the confidence is not higher; empty when nothing held it back. */
50491
+ notes: array(string())
50492
+ }), object({
50493
+ state: literal("insufficient"),
50494
+ reason: RunningProposalInsufficientReasonSchema,
50495
+ evidence: RunningProposalInsufficientEvidenceSchema,
50496
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50497
+ fallback: ThresholdTransformDraftSchema
50498
+ })]);
50499
+ /**
49077
50500
  * The report of a one-shot "move this owner's plaintext secrets into the
49078
50501
  * keychain" run (D878). COUNTS only - never a value, never a ref.
49079
50502
  *
@@ -49363,6 +50786,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49363
50786
  kind: "mutation",
49364
50787
  auth: "admin",
49365
50788
  access: "view"
50789
+ }), method(object({
50790
+ deviceId: number().int(),
50791
+ cap: string().min(1),
50792
+ fieldPath: string().min(1),
50793
+ deviceType: _enum(DeviceType).optional()
50794
+ }), RunningThresholdProposalSchema, {
50795
+ auth: "admin",
50796
+ access: "view"
49366
50797
  }), method(object({
49367
50798
  blockId: string().min(1),
49368
50799
  trace: AutomationRunTraceSchema.optional(),
@@ -50339,7 +51770,27 @@ method(object({
50339
51770
  * exposes only the COMMON surface: link state, supported device
50340
51771
  * kinds (so the UI can filter the device picker), and per-device
50341
51772
  * expose/unexpose.
50342
- */
51773
+ *
51774
+ * INSTANCES. One export addon can serve several independent targets (a
51775
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51776
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51777
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51778
+ * default instance's); present = that instance; an id the provider does not
51779
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51780
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51781
+ * change always names the target it changes. A single-instance provider
51782
+ * accepts an absent id on a read or its own id, and refuses any other.
51783
+ */
51784
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51785
+ var InstanceIdSchema$1 = string().min(1);
51786
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51787
+ /** One independent export target of an addon. */
51788
+ var ExportInstanceSchema = object({
51789
+ instanceId: InstanceIdSchema$1,
51790
+ name: string(),
51791
+ enabled: boolean(),
51792
+ kind: string().optional()
51793
+ });
50343
51794
  var LinkStateSchema = _enum([
50344
51795
  "unlinked",
50345
51796
  "linked",
@@ -50390,10 +51841,14 @@ var ExposedDeviceSchema = object({
50390
51841
  capabilities: array(string()).optional()
50391
51842
  });
50392
51843
  var ExposeInputSchema = object({
51844
+ instanceId: InstanceIdSchema$1,
50393
51845
  deviceId: string(),
50394
51846
  capabilities: array(string()).optional()
50395
51847
  });
50396
- var UnexposeInputSchema = object({ deviceId: string() });
51848
+ var UnexposeInputSchema = object({
51849
+ instanceId: InstanceIdSchema$1,
51850
+ deviceId: string()
51851
+ });
50397
51852
  /**
50398
51853
  * What an exporter may expose besides devices (D968). An action and a
50399
51854
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50422,15 +51877,21 @@ var ExposedEntitySchema = object({
50422
51877
  entity: ExportEntityRefSchema,
50423
51878
  exposedAs: string().optional()
50424
51879
  });
50425
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50426
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50427
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51880
+ var ExposeEntityInputSchema = object({
51881
+ instanceId: InstanceIdSchema$1,
51882
+ entity: ExportEntityRefSchema
51883
+ });
51884
+ var UnexposeEntityInputSchema = object({
51885
+ instanceId: InstanceIdSchema$1,
51886
+ entity: ExportEntityRefSchema
51887
+ });
51888
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50428
51889
  kind: "mutation",
50429
51890
  auth: "admin"
50430
51891
  }), method(UnexposeInputSchema, _void(), {
50431
51892
  kind: "mutation",
50432
51893
  auth: "admin"
50433
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51894
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50434
51895
  kind: "mutation",
50435
51896
  auth: "admin"
50436
51897
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -52432,7 +53893,7 @@ var DishwasherStatusSchema = object({
52432
53893
  /** Ms epoch of the last value received from the appliance; 0 = never. */
52433
53894
  lastFetchedAt: number()
52434
53895
  });
52435
- var DeviceIdSchema$1 = number().int().nonnegative();
53896
+ var DeviceIdSchema$2 = number().int().nonnegative();
52436
53897
  /** A value a program option or setting can take. */
52437
53898
  var DishwasherValueSchema = union([
52438
53899
  boolean(),
@@ -52507,77 +53968,77 @@ var dishwasherCapability = {
52507
53968
  * option feature keys; each is checked against the program's constraints.
52508
53969
  */
52509
53970
  start: method(object({
52510
- deviceId: DeviceIdSchema$1,
53971
+ deviceId: DeviceIdSchema$2,
52511
53972
  program: string().min(1).optional(),
52512
53973
  options: record(string(), DishwasherValueSchema).optional()
52513
53974
  }), _void(), {
52514
53975
  kind: "mutation",
52515
53976
  auth: "protected"
52516
53977
  }),
52517
- pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53978
+ pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52518
53979
  kind: "mutation",
52519
53980
  auth: "protected"
52520
53981
  }),
52521
- resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53982
+ resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52522
53983
  kind: "mutation",
52523
53984
  auth: "protected"
52524
53985
  }),
52525
- stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53986
+ stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52526
53987
  kind: "mutation",
52527
53988
  auth: "protected"
52528
53989
  }),
52529
53990
  /** Select (not start) a program; refused when the appliance does not offer it. */
52530
53991
  setProgram: method(object({
52531
- deviceId: DeviceIdSchema$1,
53992
+ deviceId: DeviceIdSchema$2,
52532
53993
  program: string().min(1)
52533
53994
  }), _void(), {
52534
53995
  kind: "mutation",
52535
53996
  auth: "protected"
52536
53997
  }),
52537
53998
  setPower: method(object({
52538
- deviceId: DeviceIdSchema$1,
53999
+ deviceId: DeviceIdSchema$2,
52539
54000
  on: boolean()
52540
54001
  }), _void(), {
52541
54002
  kind: "mutation",
52542
54003
  auth: "protected"
52543
54004
  }),
52544
54005
  setHalfLoad: method(object({
52545
- deviceId: DeviceIdSchema$1,
54006
+ deviceId: DeviceIdSchema$2,
52546
54007
  on: boolean()
52547
54008
  }), _void(), {
52548
54009
  kind: "mutation",
52549
54010
  auth: "protected"
52550
54011
  }),
52551
54012
  setHygienePlus: method(object({
52552
- deviceId: DeviceIdSchema$1,
54013
+ deviceId: DeviceIdSchema$2,
52553
54014
  on: boolean()
52554
54015
  }), _void(), {
52555
54016
  kind: "mutation",
52556
54017
  auth: "protected"
52557
54018
  }),
52558
54019
  setExtraDry: method(object({
52559
- deviceId: DeviceIdSchema$1,
54020
+ deviceId: DeviceIdSchema$2,
52560
54021
  on: boolean()
52561
54022
  }), _void(), {
52562
54023
  kind: "mutation",
52563
54024
  auth: "protected"
52564
54025
  }),
52565
54026
  setIntensiveZone: method(object({
52566
- deviceId: DeviceIdSchema$1,
54027
+ deviceId: DeviceIdSchema$2,
52567
54028
  on: boolean()
52568
54029
  }), _void(), {
52569
54030
  kind: "mutation",
52570
54031
  auth: "protected"
52571
54032
  }),
52572
54033
  setVarioSpeedPlus: method(object({
52573
- deviceId: DeviceIdSchema$1,
54034
+ deviceId: DeviceIdSchema$2,
52574
54035
  on: boolean()
52575
54036
  }), _void(), {
52576
54037
  kind: "mutation",
52577
54038
  auth: "protected"
52578
54039
  }),
52579
54040
  setChildLock: method(object({
52580
- deviceId: DeviceIdSchema$1,
54041
+ deviceId: DeviceIdSchema$2,
52581
54042
  on: boolean()
52582
54043
  }), _void(), {
52583
54044
  kind: "mutation",
@@ -52589,7 +54050,7 @@ var dishwasherCapability = {
52589
54050
  * its declared range / members.
52590
54051
  */
52591
54052
  setOption: method(object({
52592
- deviceId: DeviceIdSchema$1,
54053
+ deviceId: DeviceIdSchema$2,
52593
54054
  key: string().min(1),
52594
54055
  value: DishwasherValueSchema
52595
54056
  }), _void(), {
@@ -52597,7 +54058,7 @@ var dishwasherCapability = {
52597
54058
  auth: "protected"
52598
54059
  }),
52599
54060
  /** What this appliance can do, from its own description — the UI is derived from it. */
52600
- getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
54061
+ getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
52601
54062
  },
52602
54063
  status: {
52603
54064
  schema: DishwasherStatusSchema,
@@ -54021,15 +55482,15 @@ var FanPresetCaptureResultSchema = object({
54021
55482
  items: array(FanCaptureItemSchema),
54022
55483
  omitted: array(FanCaptureOmissionSchema)
54023
55484
  });
54024
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55485
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
54025
55486
  DeviceType.Fan;
54026
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55487
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
54027
55488
  kind: "mutation",
54028
55489
  auth: "admin"
54029
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55490
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
54030
55491
  kind: "mutation",
54031
55492
  auth: "protected"
54032
- }), method(deviceInput$1.extend({
55493
+ }), method(deviceInput$2.extend({
54033
55494
  fields: array(FanCaptureFieldSchema).optional(),
54034
55495
  steps: array(ActionStepSchema).max(20).default([])
54035
55496
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -54912,6 +56373,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54912
56373
  }), object({ success: boolean() }), {
54913
56374
  kind: "mutation",
54914
56375
  auth: "admin"
56376
+ }), method(object({ id: string() }), object({
56377
+ rows: number().int().nonnegative(),
56378
+ secrets: number().int().nonnegative()
56379
+ }), {
56380
+ kind: "mutation",
56381
+ auth: "admin"
54915
56382
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54916
56383
  addonId: string(),
54917
56384
  settings: record(string(), unknown())
@@ -54940,13 +56407,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54940
56407
  */
54941
56408
  var ConfigSchemaPassthrough$3 = custom();
54942
56409
  var IntegrationInstanceIdSchema = string().min(1);
56410
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56411
+ /**
56412
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56413
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56414
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56415
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56416
+ * instance id.
56417
+ */
56418
+ var IntegrationInstanceKindSchema = object({
56419
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56420
+ id: string().min(1),
56421
+ label: string().min(1),
56422
+ /** The kind signs in through a server-held interactive session. */
56423
+ signIn: object({
56424
+ basePath: string().min(1),
56425
+ methods: array(_enum(["device-code", "proxy"])).optional()
56426
+ }).optional(),
56427
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56428
+ connectPath: string().min(1).optional(),
56429
+ /** Relative hub path to disconnect that account. */
56430
+ disconnectPath: string().min(1).optional()
56431
+ });
54943
56432
  var IntegrationInstanceDescriptionSchema = object({
56433
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56434
+ role: _enum([
56435
+ "devices",
56436
+ "services",
56437
+ "both"
56438
+ ]),
56439
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56440
+ discovery: object({
56441
+ mode: _enum([
56442
+ "none",
56443
+ "lan",
56444
+ "account"
56445
+ ]),
56446
+ scopedToInstance: boolean()
56447
+ }).optional(),
54944
56448
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
54945
- instanceMode: _enum(["single", "multiple"]),
56449
+ instanceMode: IntegrationInstanceModeSchema,
54946
56450
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
54947
56451
  kind: string(),
54948
56452
  /** True when `testInstance` really probes the remote. */
54949
- canTest: boolean()
56453
+ canTest: boolean(),
56454
+ /**
56455
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56456
+ * Add flow asks which first and sends it as `createInstance.kind`.
56457
+ */
56458
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56459
+ /**
56460
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56461
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56462
+ */
56463
+ allowRestoreId: boolean().optional(),
56464
+ /**
56465
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56466
+ * send an id (the UI then mints an opaque one).
56467
+ */
56468
+ mintsId: boolean().optional()
54950
56469
  });
54951
56470
  var IntegrationInstanceStatusSchema = object({
54952
56471
  state: _enum([
@@ -54955,16 +56474,28 @@ var IntegrationInstanceStatusSchema = object({
54955
56474
  "error",
54956
56475
  "unknown"
54957
56476
  ]),
54958
- detail: string().optional()
56477
+ detail: string().optional(),
56478
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56479
+ label: string().optional()
54959
56480
  });
54960
56481
  var IntegrationInstanceSchema = object({
54961
56482
  id: IntegrationInstanceIdSchema,
54962
56483
  name: string(),
54963
56484
  /** The addon's own rule (an URL is present, a client id is present...). */
54964
56485
  configured: boolean(),
56486
+ /**
56487
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56488
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56489
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56490
+ */
56491
+ enabled: boolean().optional(),
56492
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56493
+ kind: string().optional(),
54965
56494
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
54966
56495
  formValues: record(string(), unknown()),
54967
- status: IntegrationInstanceStatusSchema.optional()
56496
+ status: IntegrationInstanceStatusSchema.optional(),
56497
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56498
+ lastCheckedAt: number().optional()
54968
56499
  });
54969
56500
  var IntegrationInstancePatchSchema = object({
54970
56501
  id: IntegrationInstanceIdSchema,
@@ -54972,25 +56503,57 @@ var IntegrationInstancePatchSchema = object({
54972
56503
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
54973
56504
  values: record(string(), unknown())
54974
56505
  });
54975
- 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({
54976
- id: IntegrationInstanceIdSchema,
54977
- name: string().min(1)
54978
- }), object({ id: string() }), {
54979
- kind: "mutation",
54980
- auth: "admin"
54981
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
54982
- kind: "mutation",
54983
- auth: "admin"
54984
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
54985
- kind: "mutation",
54986
- auth: "admin"
54987
- }), optionalMethod(object({
54988
- id: IntegrationInstanceIdSchema.nullable(),
54989
- values: record(string(), unknown())
54990
- }), TestConnectionResultSchema$1, {
54991
- kind: "mutation",
54992
- auth: "admin"
54993
- });
56506
+ var integrationInstancesCapability = {
56507
+ name: "integration-instances",
56508
+ scope: "system",
56509
+ mode: "collection",
56510
+ methods: {
56511
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56512
+ /** The form; `id: null` is the create form. */
56513
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56514
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56515
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56516
+ /**
56517
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56518
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56519
+ */
56520
+ createInstance: method(object({
56521
+ id: IntegrationInstanceIdSchema.optional(),
56522
+ name: string().min(1),
56523
+ kind: string().min(1).optional()
56524
+ }), object({ id: string() }), {
56525
+ kind: "mutation",
56526
+ auth: "admin"
56527
+ }),
56528
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56529
+ kind: "mutation",
56530
+ auth: "admin"
56531
+ }),
56532
+ /**
56533
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56534
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56535
+ */
56536
+ setInstanceEnabled: method(object({
56537
+ id: IntegrationInstanceIdSchema,
56538
+ enabled: boolean()
56539
+ }), object({ ok: literal(true) }), {
56540
+ kind: "mutation",
56541
+ auth: "admin"
56542
+ }),
56543
+ /** Delete the record and its keychain refs together. */
56544
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56545
+ kind: "mutation",
56546
+ auth: "admin"
56547
+ }),
56548
+ testInstance: optionalMethod(object({
56549
+ id: IntegrationInstanceIdSchema.nullable(),
56550
+ values: record(string(), unknown())
56551
+ }), TestConnectionResultSchema$1, {
56552
+ kind: "mutation",
56553
+ auth: "admin"
56554
+ })
56555
+ }
56556
+ };
54994
56557
  /**
54995
56558
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
54996
56559
  * session (server-initiated offer pattern, identical to the main
@@ -57008,6 +58571,11 @@ var LogEntrySchema = object({
57008
58571
  * flight, and not asking for the next slice ends the scan.
57009
58572
  */
57010
58573
  var LogReadQuerySchema = object({
58574
+ /**
58575
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58576
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58577
+ */
58578
+ instanceId: string().min(1).max(200).optional(),
57011
58579
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
57012
58580
  requestId: string().min(1).max(64).optional(),
57013
58581
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -57070,6 +58638,8 @@ var LogReadResultSchema = object({
57070
58638
  /** True when `cancelRead` ended this slice early. */
57071
58639
  cancelled: boolean().optional()
57072
58640
  });
58641
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58642
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
57073
58643
  var LogReadCapabilitiesSchema = object({
57074
58644
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
57075
58645
  readable: boolean(),
@@ -57088,7 +58658,7 @@ var LogReadCapabilitiesSchema = object({
57088
58658
  /** How far back the store reaches, when the provider knows (days). */
57089
58659
  retentionDays: number().nullable()
57090
58660
  });
57091
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
58661
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
57092
58662
  kind: "mutation",
57093
58663
  auth: "admin"
57094
58664
  });
@@ -57192,214 +58762,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
57192
58762
  ]);
57193
58763
  method(_void(), array(LoginMethodContributionSchema).readonly());
57194
58764
  /**
57195
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
57196
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
57197
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
57198
- * surface — those use `DeviceType.Camera` + the broker pipeline.
57199
- *
57200
- * The cap declares a wide method surface; not every backend supports
57201
- * every method. Drivers signal sub-capability via the
57202
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
57203
- * corresponding method) so the UI gates buttons without a slice
57204
- * fetch — same pattern as `PanTiltZoom` for cameras.
57205
- */
57206
- var MediaPlayerStateSchema = _enum([
57207
- "off",
57208
- "on",
57209
- "idle",
57210
- "playing",
57211
- "paused",
57212
- "buffering",
57213
- "standby"
57214
- ]);
57215
- var MediaPlayerRepeatSchema = _enum([
57216
- "off",
57217
- "all",
57218
- "one"
57219
- ]);
57220
- var MediaInfoSchema = object({
57221
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
57222
- * `podcast`, …). */
57223
- type: string(),
57224
- /** Human-readable title. */
57225
- title: string(),
57226
- /** Optional artist / channel / station label. */
57227
- artist: string().optional(),
57228
- /** Optional album / season / show name. */
57229
- album: string().optional(),
57230
- /** Optional cover-art / thumbnail URL. */
57231
- imageUrl: string().optional()
57232
- });
57233
- /**
57234
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
57235
- * the device said so. `assumed`: the driver has no feedback (infrared) and
57236
- * tracks what it last SENT; it moves only after a send was accepted, and a
57237
- * human at the real remote makes it wrong. A UI shows an assumed value as a
57238
- * guess, never as a reading.
57239
- */
57240
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
57241
- /** The fields of a media-player status a driver can only guess. */
57242
- var MediaPlayerAssumedFieldSchema = _enum([
57243
- "state",
57244
- "volumeLevel",
57245
- "isMuted",
57246
- "source"
57247
- ]);
57248
- var MediaPlayerStatusSchema = object({
57249
- /** Playback lifecycle state. */
57250
- state: MediaPlayerStateSchema,
57251
- /**
57252
- * Whether the state fields are readings or the driver's own bookkeeping.
57253
- * A slice written before this field existed parses as `reported`.
57254
- */
57255
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
57256
- /**
57257
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
57258
- * this is not empty; a field not listed here is a reading even then (power
57259
- * from a running-state source, mute guessed from the last send).
57260
- */
57261
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
57262
- /** Volume as 0..100 inclusive. Null when the device has no volume
57263
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
57264
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57265
- /** Mute toggle distinct from volume=0. Null when no mute surface.
57266
- * Pair with `DeviceFeature.MediaPlayerMute`. */
57267
- isMuted: boolean().nullable(),
57268
- /** Active source / input. Empty when no source surface. */
57269
- source: string(),
57270
- /** Selectable sources. Empty when no source surface.
57271
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
57272
- availableSources: array(string()),
57273
- /** Currently-playing media info. Null when nothing is playing. */
57274
- currentMedia: MediaInfoSchema.nullable(),
57275
- /** Current playback position in ms. Null when not seekable or
57276
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
57277
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57278
- /** Total duration of the current media in ms. Null when unknown
57279
- * (live stream) or nothing is playing. */
57280
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57281
- /** Shuffle toggle. Null when no shuffle surface.
57282
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
57283
- shuffle: boolean().nullable(),
57284
- /** Repeat mode. Null when no repeat surface.
57285
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
57286
- repeat: MediaPlayerRepeatSchema.nullable(),
57287
- /** Ms epoch when the slice was last updated. */
57288
- lastChangedAt: number()
57289
- });
57290
- var mediaPlayerCapability = {
57291
- name: "media-player",
57292
- scope: "device",
57293
- deviceNative: true,
57294
- mode: "singleton",
57295
- deviceTypes: [DeviceType.MediaPlayer],
57296
- methods: {
57297
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57298
- kind: "mutation",
57299
- auth: "protected"
57300
- }),
57301
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57302
- kind: "mutation",
57303
- auth: "protected"
57304
- }),
57305
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57306
- kind: "mutation",
57307
- auth: "protected"
57308
- }),
57309
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57310
- kind: "mutation",
57311
- auth: "protected"
57312
- }),
57313
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57314
- kind: "mutation",
57315
- auth: "protected"
57316
- }),
57317
- seek: method(object({
57318
- deviceId: number().int().nonnegative(),
57319
- position: number().int().nonnegative()
57320
- }), _void(), {
57321
- kind: "mutation",
57322
- auth: "protected"
57323
- }),
57324
- setVolume: method(object({
57325
- deviceId: number().int().nonnegative(),
57326
- volumeLevel: number().min(0).max(100)
57327
- }), _void(), {
57328
- kind: "mutation",
57329
- auth: "protected"
57330
- }),
57331
- setMute: method(object({
57332
- deviceId: number().int().nonnegative(),
57333
- muted: boolean()
57334
- }), _void(), {
57335
- kind: "mutation",
57336
- auth: "protected"
57337
- }),
57338
- setShuffle: method(object({
57339
- deviceId: number().int().nonnegative(),
57340
- shuffle: boolean()
57341
- }), _void(), {
57342
- kind: "mutation",
57343
- auth: "protected"
57344
- }),
57345
- setRepeat: method(object({
57346
- deviceId: number().int().nonnegative(),
57347
- repeat: MediaPlayerRepeatSchema
57348
- }), _void(), {
57349
- kind: "mutation",
57350
- auth: "protected"
57351
- }),
57352
- selectSource: method(object({
57353
- deviceId: number().int().nonnegative(),
57354
- source: string().min(1)
57355
- }), _void(), {
57356
- kind: "mutation",
57357
- auth: "protected"
57358
- }),
57359
- playMedia: method(object({
57360
- deviceId: number().int().nonnegative(),
57361
- /** Media identifier / URL. */
57362
- mediaId: string().min(1),
57363
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
57364
- * passes it through to the upstream service. */
57365
- mediaType: string().min(1)
57366
- }), _void(), {
57367
- kind: "mutation",
57368
- auth: "protected"
57369
- })
57370
- },
57371
- status: {
57372
- schema: MediaPlayerStatusSchema,
57373
- kind: "push"
57374
- },
57375
- /**
57376
- * Runtime-state slice — mirrored by the kernel. UI players read the
57377
- * full slice for live now-playing, volume, and progress updates
57378
- * without polling.
57379
- */
57380
- runtimeState: MediaPlayerStatusSchema,
57381
- /**
57382
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
57383
- *
57384
- * See `RuntimeStateDurability`. Enforced by
57385
- * `scripts/check-runtime-state-durability.ts`.
57386
- */
57387
- durability: "session",
57388
- /**
57389
- * History (D850): volume (a level an operator or an automation sets; the
57390
- * default 10 s floor folds a slider drag into its end value) and mute.
57391
- * `position` is not recorded: it moves continuously while playing.
57392
- */
57393
- history: { fields: {
57394
- volumeLevel: {
57395
- kind: "gauge",
57396
- unit: "%",
57397
- deadband: 1
57398
- },
57399
- isMuted: { kind: "boolean" }
57400
- } }
57401
- };
57402
- /**
57403
58765
  * mesh-network — collection cap for mesh-VPN providers.
57404
58766
  *
57405
58767
  * Different shape from `network-access` (public tunnels):
@@ -66158,6 +67520,21 @@ var NcTemplateVarKindSchema = _enum([
66158
67520
  "sound",
66159
67521
  "system"
66160
67522
  ]);
67523
+ /**
67524
+ * The default title/body a rule of one SITUATION is delivered with when the
67525
+ * operator wrote no template — served so an editor can pre-fill it (and the
67526
+ * operator can rebuild it from placeholders) instead of starting from a blank
67527
+ * box that, once saved, silently replaces it. `id` names the situation inside
67528
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
67529
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
67530
+ * the hub's current language and with its overrides applied.
67531
+ */
67532
+ var NcTemplateDefaultSchema = object({
67533
+ id: _enum(["appliance-cycle"]),
67534
+ kind: NcRuleKindSchema,
67535
+ title: string(),
67536
+ body: string()
67537
+ });
66161
67538
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
66162
67539
  var NcTemplateFamilySchema = _enum([
66163
67540
  "rule",
@@ -67013,7 +68390,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
67013
68390
  }), method(object({}), object({
67014
68391
  catalog: array(NcConditionDescriptorSchema),
67015
68392
  taxonomy: NcTaxonomySchema.optional()
67016
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
68393
+ })), method(object({}), object({
68394
+ vars: array(NcTemplateVarDescriptorSchema),
68395
+ /** Per-situation default texts; absent on a hub that predates them. */
68396
+ defaults: array(NcTemplateDefaultSchema)
68397
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
67017
68398
  kind: "mutation",
67018
68399
  access: "view"
67019
68400
  }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
@@ -67909,7 +69290,7 @@ var petFeederCapability = {
67909
69290
  /**
67910
69291
  * Pet water-fountain cap. Models the telemetry and the control surface of a
67911
69292
  * circulating drinking fountain: how much water is in the tank, how much the
67912
- * pet drank, whether the pump runs and on what duty cycle.
69293
+ * pet drank, whether the pump runs.
67913
69294
  *
67914
69295
  * It has no native vendor provider of its own: a fountain reaches CamStack as
67915
69296
  * a container of loose entities (a pump switch, two number boxes, a "filter
@@ -67925,8 +69306,10 @@ var petFeederCapability = {
67925
69306
  * never here. Every reading may be `null`: a source that is offline or does
67926
69307
  * not report is unknown, never zero.
67927
69308
  *
67928
- * The filter is not a field of this cap: it is an item of the `consumables`
67929
- * cap on the same device (kind `wear`), reset by the same forward mechanism.
69309
+ * The pump's on/off duty cycle is model-specific and lives in the optional
69310
+ * `pump-cycle` cap. The filter, the cleaning and the water change are not
69311
+ * fields of this cap: they are items of the `consumables` cap on the same
69312
+ * device (kind `wear`), reset by the same forward mechanism.
67930
69313
  */
67931
69314
  var PetFountainStatusSchema = object({
67932
69315
  /** Water in the tank, 0..100 (%). Null while the source has not reported. */
@@ -67937,27 +69320,20 @@ var PetFountainStatusSchema = object({
67937
69320
  empty: boolean().nullable(),
67938
69321
  /** True while the pump is running. Null while unknown. */
67939
69322
  pumpRunning: boolean().nullable(),
67940
- /** True when the pump works on an on/off duty cycle rather than continuously. */
67941
- pumpIntervalEnabled: boolean().nullable(),
67942
- /** How long the pump runs per cycle, in seconds. */
67943
- pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
67944
- /** How long the pump rests per cycle, in seconds. */
67945
- pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
67946
69323
  /** Water drunk in the last drink, in litres. */
67947
69324
  lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
67948
69325
  /** Water drunk in the current hour, in litres. */
67949
69326
  currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
67950
69327
  /** Water drunk over the last 24 hours (rolling), in litres. */
67951
69328
  last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
67952
- /** Water drunk since local midnight, in litres. */
69329
+ /** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
67953
69330
  todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
67954
69331
  /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
67955
69332
  error: string().nullable(),
67956
69333
  /** Ms epoch when the slice was last refreshed. */
67957
69334
  lastFetchedAt: number()
67958
69335
  });
67959
- var DeviceIdSchema = number().int().nonnegative();
67960
- var MinutesSchema = number().int().min(0);
69336
+ var DeviceIdSchema$1 = number().int().nonnegative();
67961
69337
  var petFountainCapability = {
67962
69338
  name: "pet-fountain",
67963
69339
  scope: "device",
@@ -67967,38 +69343,14 @@ var petFountainCapability = {
67967
69343
  methods: {
67968
69344
  /** Start or stop the pump. */
67969
69345
  setPump: method(object({
67970
- deviceId: DeviceIdSchema,
67971
- on: boolean()
67972
- }), _void(), {
67973
- kind: "mutation",
67974
- auth: "protected"
67975
- }),
67976
- /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
67977
- setPumpInterval: method(object({
67978
- deviceId: DeviceIdSchema,
69346
+ deviceId: DeviceIdSchema$1,
67979
69347
  on: boolean()
67980
69348
  }), _void(), {
67981
69349
  kind: "mutation",
67982
69350
  auth: "protected"
67983
69351
  }),
67984
- /** Set how many minutes the pump runs per cycle. */
67985
- setPumpOnMinutes: method(object({
67986
- deviceId: DeviceIdSchema,
67987
- minutes: MinutesSchema
67988
- }), _void(), {
67989
- kind: "mutation",
67990
- auth: "protected"
67991
- }),
67992
- /** Set how many minutes the pump rests per cycle. */
67993
- setPumpOffMinutes: method(object({
67994
- deviceId: DeviceIdSchema,
67995
- minutes: MinutesSchema
67996
- }), _void(), {
67997
- kind: "mutation",
67998
- auth: "protected"
67999
- }),
68000
69352
  /** Clear the rolling daily consumption counters on the device. */
68001
- resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69353
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
68002
69354
  kind: "mutation",
68003
69355
  auth: "protected"
68004
69356
  })
@@ -69633,6 +70985,14 @@ var powerMeterCapability = {
69633
70985
  deviceNative: true,
69634
70986
  mode: "singleton",
69635
70987
  deviceTypes: POWER_METER_DEVICE_TYPES,
70988
+ deviceConfig: { ui: {
70989
+ kind: "widget",
70990
+ widgetId: "host/power-meter-line",
70991
+ tab: "power",
70992
+ placement: "hero",
70993
+ label: "Power",
70994
+ order: 4
70995
+ } },
69636
70996
  methods: {},
69637
70997
  status: {
69638
70998
  schema: PowerMeterStatusSchema,
@@ -69692,6 +71052,117 @@ var powerMeterCapability = {
69692
71052
  volatileStateFields: ["lastFetchedAt"],
69693
71053
  customize: powerMeterCustomize
69694
71054
  };
71055
+ var PowerSourceRefusalReasonSchema = _enum([
71056
+ "no-power-source",
71057
+ "switch-missing",
71058
+ "switch-offline",
71059
+ "switch-unknown",
71060
+ "restart-in-progress",
71061
+ "switch-call-failed"
71062
+ ]);
71063
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
71064
+ accepted: literal(false),
71065
+ reason: PowerSourceRefusalReasonSchema,
71066
+ /** One line for the operator, naming the switch. */
71067
+ message: string()
71068
+ })]);
71069
+ var PowerSourceInfoSchema = object({
71070
+ /** False when the device has no power source linked; the rest is then empty. */
71071
+ linked: boolean(),
71072
+ switchDeviceId: number().nullable(),
71073
+ switchName: string().nullable(),
71074
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
71075
+ switchOn: boolean().nullable(),
71076
+ switchOnline: boolean().nullable(),
71077
+ restartInProgress: boolean(),
71078
+ offSeconds: number().int()
71079
+ });
71080
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
71081
+ var PowerSourceStateSchema = object({
71082
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
71083
+ switchOn: boolean().nullable(),
71084
+ offSeconds: number().int(),
71085
+ lastFetchedAt: number()
71086
+ });
71087
+ var POWER_SOURCE_DEVICE_TYPES = [
71088
+ DeviceType.Gateway,
71089
+ DeviceType.Hub,
71090
+ DeviceType.Camera,
71091
+ DeviceType.Sensor,
71092
+ DeviceType.MediaPlayer,
71093
+ DeviceType.Vacuum,
71094
+ DeviceType.Generic,
71095
+ DeviceType.Washer,
71096
+ DeviceType.Dryer,
71097
+ DeviceType.Dishwasher,
71098
+ DeviceType.PetFountain,
71099
+ DeviceType.Fan,
71100
+ DeviceType.Humidifier,
71101
+ DeviceType.Climate,
71102
+ DeviceType.WaterHeater
71103
+ ];
71104
+ var powerSourceCustomize = {
71105
+ version: 1,
71106
+ targets: [{
71107
+ id: "switch",
71108
+ cardinality: "scalar",
71109
+ vocabulary: {
71110
+ cap: "power-source",
71111
+ stateField: "switchOn"
71112
+ },
71113
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71114
+ apply: "add",
71115
+ path: "switchOn",
71116
+ sources: [{
71117
+ kind: "switch",
71118
+ transforms: [{ kind: "identity" }]
71119
+ }],
71120
+ companions: [{
71121
+ kind: "constant",
71122
+ path: "offSeconds",
71123
+ value: 10,
71124
+ when: "add"
71125
+ }, {
71126
+ kind: "clock",
71127
+ path: "lastFetchedAt",
71128
+ when: "always"
71129
+ }],
71130
+ sourceReport: { status: "assignment-status" }
71131
+ }]
71132
+ };
71133
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
71134
+ var powerSourceCapability = {
71135
+ name: "power-source",
71136
+ scope: "device",
71137
+ mode: "singleton",
71138
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71139
+ methods: {
71140
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71141
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71142
+ kind: "mutation",
71143
+ auth: "admin"
71144
+ }),
71145
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71146
+ kind: "mutation",
71147
+ auth: "admin"
71148
+ }),
71149
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71150
+ kind: "mutation",
71151
+ auth: "admin"
71152
+ })
71153
+ },
71154
+ runtimeState: PowerSourceStateSchema,
71155
+ customize: powerSourceCustomize,
71156
+ /**
71157
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71158
+ * its next report; a restored value would outlive the reading it names.
71159
+ *
71160
+ * See `RuntimeStateDurability`. Enforced by
71161
+ * `scripts/check-runtime-state-durability.ts`.
71162
+ */
71163
+ durability: "session",
71164
+ history: "none"
71165
+ };
69695
71166
  /**
69696
71167
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
69697
71168
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -70332,6 +71803,79 @@ var ptzAutotrackCapability = {
70332
71803
  history: "none"
70333
71804
  };
70334
71805
  /**
71806
+ * The on/off duty cycle of a pet fountain's pump. Some models run the pump
71807
+ * continuously and have nothing to configure; others can alternate a run time
71808
+ * and a rest time. That is model-specific, so it is its own optional cap on the
71809
+ * fountain device: a fountain without cycle entities simply never composes it
71810
+ * (planning#366), and the `pet-fountain` cap stays the model-independent core.
71811
+ *
71812
+ * Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
71813
+ * cycle from loose entities (a cycle switch and two number boxes) and every
71814
+ * method is a forward to one source entity (D659, D967). Durations are stored
71815
+ * in SECONDS (D944); a number box that cannot be read back leaves them `null`
71816
+ * — unknown, never zero (D393).
71817
+ */
71818
+ var PumpCycleStatusSchema = object({
71819
+ /** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
71820
+ enabled: boolean().nullable(),
71821
+ /** How long the pump runs per cycle, in seconds. Null while unknown. */
71822
+ onTime: number().min(0).nullable().meta(readingMeta("duration")),
71823
+ /** How long the pump rests per cycle, in seconds. Null while unknown. */
71824
+ offTime: number().min(0).nullable().meta(readingMeta("duration")),
71825
+ /** Ms epoch when the slice was last refreshed. */
71826
+ lastFetchedAt: number()
71827
+ });
71828
+ var DeviceIdSchema = number().int().nonnegative();
71829
+ var MinutesSchema = number().int().min(0);
71830
+ var pumpCycleCapability = {
71831
+ name: "pump-cycle",
71832
+ scope: "device",
71833
+ deviceNative: true,
71834
+ mode: "singleton",
71835
+ deviceTypes: [DeviceType.PetFountain],
71836
+ methods: {
71837
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
71838
+ setEnabled: method(object({
71839
+ deviceId: DeviceIdSchema,
71840
+ on: boolean()
71841
+ }), _void(), {
71842
+ kind: "mutation",
71843
+ auth: "protected"
71844
+ }),
71845
+ /** Set how many minutes the pump runs per cycle. */
71846
+ setOnMinutes: method(object({
71847
+ deviceId: DeviceIdSchema,
71848
+ minutes: MinutesSchema
71849
+ }), _void(), {
71850
+ kind: "mutation",
71851
+ auth: "protected"
71852
+ }),
71853
+ /** Set how many minutes the pump rests per cycle. */
71854
+ setOffMinutes: method(object({
71855
+ deviceId: DeviceIdSchema,
71856
+ minutes: MinutesSchema
71857
+ }), _void(), {
71858
+ kind: "mutation",
71859
+ auth: "protected"
71860
+ })
71861
+ },
71862
+ status: {
71863
+ schema: PumpCycleStatusSchema,
71864
+ kind: "push"
71865
+ },
71866
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
71867
+ runtimeState: PumpCycleStatusSchema,
71868
+ /**
71869
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
71870
+ *
71871
+ * See `RuntimeStateDurability`. Enforced by
71872
+ * `scripts/check-runtime-state-durability.ts`.
71873
+ */
71874
+ durability: "session",
71875
+ /** History (D850): a duty-cycle setting is not a series worth recording. */
71876
+ history: "none"
71877
+ };
71878
+ /**
70335
71879
  * reboot — device-scoped capability for "soft" device reboots (firmware
70336
71880
  * reboot via vendor protocol; cameras, NVRs, doorbells). Surfaces a
70337
71881
  * single mutation so the UI can offer a confirm-and-reboot button for
@@ -73147,7 +74691,13 @@ var RemoteMediaCommandSchema = _enum([
73147
74691
  "next",
73148
74692
  "previous",
73149
74693
  "setMute",
73150
- "selectSource"
74694
+ "selectSource",
74695
+ "turnOn",
74696
+ "turnOff",
74697
+ "volumeUp",
74698
+ "volumeDown",
74699
+ "channelUp",
74700
+ "channelDown"
73151
74701
  ]);
73152
74702
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
73153
74703
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -73168,7 +74718,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
73168
74718
  }), RemoteSendResultSchema, {
73169
74719
  kind: "mutation",
73170
74720
  auth: "protected"
73171
- }), method(deviceInput.extend({
74721
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
73172
74722
  command: RemoteMediaCommandSchema,
73173
74723
  muted: boolean().optional(),
73174
74724
  source: string().optional(),
@@ -75414,7 +76964,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
75414
76964
  * `getSettingsUISchema()` surface.
75415
76965
  */
75416
76966
  var SwitchStatusSchema = object({
75417
- on: boolean().meta({ label: "On" }),
76967
+ /**
76968
+ * `null` = the device has never reported its state (a measurement that
76969
+ * has not happened is unknown, never `false`).
76970
+ */
76971
+ on: boolean().nullable().meta({ label: "On" }),
75418
76972
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
75419
76973
  lastChangedAt: number()
75420
76974
  });
@@ -75439,7 +76993,7 @@ var switchCapability = {
75439
76993
  }) },
75440
76994
  events: { onStateChanged: { data: object({
75441
76995
  deviceId: number(),
75442
- on: boolean(),
76996
+ on: boolean().nullable(),
75443
76997
  lastChangedAt: number()
75444
76998
  }) } },
75445
76999
  status: {
@@ -79088,6 +80642,26 @@ function buildDescriptor(capName, kind) {
79088
80642
  * contributes one entry per bound cap present in this map.
79089
80643
  */
79090
80644
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80645
+ /**
80646
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80647
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80648
+ * client resolves its name from `label` (English and Italian side by side) and
80649
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80650
+ * server sends an id, a client maps it to its own icon set.
80651
+ *
80652
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80653
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80654
+ * to `icon`.
80655
+ */
80656
+ function digit(n) {
80657
+ return {
80658
+ label: t(n, n),
80659
+ icon: "hash",
80660
+ legend: n
80661
+ };
80662
+ }
80663
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
80664
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79091
80665
  var btn = (idSuffix, row, col, init) => ({
79092
80666
  idSuffix,
79093
80667
  row,
@@ -79214,6 +80788,34 @@ discriminatedUnion("kind", [object({
79214
80788
  kind: literal("pronto"),
79215
80789
  code: string()
79216
80790
  })]);
80791
+ /**
80792
+ * Propose a running threshold from a device's own power history (planning#405).
80793
+ *
80794
+ * Pure and deterministic: the same series gives the same answer. The series is
80795
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80796
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80797
+ * count of samples.
80798
+ *
80799
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80800
+ * near F. A device that is not near one level most of the time has no floor.
80801
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80802
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80803
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80804
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80805
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80806
+ * covers the longest standby blip above T.
80807
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80808
+ * run must hold exactly one true interval and nothing may flap outside.
80809
+ */
80810
+ var SECOND_MS = 1e3;
80811
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80812
+ 24 * (60 * MINUTE_MS$1);
80813
+ 10 * MINUTE_MS$1;
80814
+ 30 * SECOND_MS;
80815
+ 60 * SECOND_MS;
80816
+ 20 * MINUTE_MS$1;
80817
+ 10 * SECOND_MS;
80818
+ 10 * SECOND_MS;
79217
80819
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79218
80820
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79219
80821
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -79574,6 +81176,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79574
81176
  pressureSensor: pressureSensorCapability,
79575
81177
  privacyMask: privacyMaskCapability,
79576
81178
  ptzAutotrack: ptzAutotrackCapability,
81179
+ pumpCycle: pumpCycleCapability,
79577
81180
  recordingOnboard: recordingOnboardCapability,
79578
81181
  recordingSignal: recordingSignalCapability,
79579
81182
  runningState: runningStateCapability,
@@ -81758,6 +83361,27 @@ Object.freeze({
81758
83361
  access: "view",
81759
83362
  role: "viewer"
81760
83363
  },
83364
+ "boardKeySource.getBoardKey": {
83365
+ capName: "board-key-source",
83366
+ capScope: "system",
83367
+ addonId: null,
83368
+ access: "view",
83369
+ role: "admin"
83370
+ },
83371
+ "boardKeySource.listBoards": {
83372
+ capName: "board-key-source",
83373
+ capScope: "system",
83374
+ addonId: null,
83375
+ access: "view",
83376
+ role: "admin"
83377
+ },
83378
+ "boardKeySource.listSources": {
83379
+ capName: "board-key-source",
83380
+ capScope: "system",
83381
+ addonId: null,
83382
+ access: "view",
83383
+ role: "admin"
83384
+ },
81761
83385
  "brightness.getStatus": {
81762
83386
  capName: "brightness",
81763
83387
  capScope: "device",
@@ -82297,6 +83921,13 @@ Object.freeze({
82297
83921
  access: "create",
82298
83922
  role: "admin"
82299
83923
  },
83924
+ "coreBlocks.proposeRunningThreshold": {
83925
+ capName: "core-blocks",
83926
+ capScope: "system",
83927
+ addonId: null,
83928
+ access: "view",
83929
+ role: "admin"
83930
+ },
82300
83931
  "coreBlocks.reportAutomationRun": {
82301
83932
  capName: "core-blocks",
82302
83933
  capScope: "system",
@@ -82808,6 +84439,13 @@ Object.freeze({
82808
84439
  access: "view",
82809
84440
  role: "admin"
82810
84441
  },
84442
+ "deviceExport.listInstances": {
84443
+ capName: "device-export",
84444
+ capScope: "system",
84445
+ addonId: null,
84446
+ access: "view",
84447
+ role: "admin"
84448
+ },
82811
84449
  "deviceExport.listSupportedDeviceKinds": {
82812
84450
  capName: "device-export",
82813
84451
  capScope: "system",
@@ -84439,6 +86077,13 @@ Object.freeze({
84439
86077
  access: "view",
84440
86078
  role: "admin"
84441
86079
  },
86080
+ "integrationInstances.setInstanceEnabled": {
86081
+ capName: "integration-instances",
86082
+ capScope: "system",
86083
+ addonId: null,
86084
+ access: "create",
86085
+ role: "admin"
86086
+ },
84442
86087
  "integrationInstances.testInstance": {
84443
86088
  capName: "integration-instances",
84444
86089
  capScope: "system",
@@ -84453,6 +86098,13 @@ Object.freeze({
84453
86098
  access: "create",
84454
86099
  role: "admin"
84455
86100
  },
86101
+ "integrations.clearSettings": {
86102
+ capName: "integrations",
86103
+ capScope: "system",
86104
+ addonId: null,
86105
+ access: "delete",
86106
+ role: "admin"
86107
+ },
84456
86108
  "integrations.create": {
84457
86109
  capName: "integrations",
84458
86110
  capScope: "system",
@@ -85237,6 +86889,20 @@ Object.freeze({
85237
86889
  access: "view",
85238
86890
  role: "admin"
85239
86891
  },
86892
+ "mediaPlayer.channelDown": {
86893
+ capName: "media-player",
86894
+ capScope: "device",
86895
+ addonId: null,
86896
+ access: "create",
86897
+ role: "viewer"
86898
+ },
86899
+ "mediaPlayer.channelUp": {
86900
+ capName: "media-player",
86901
+ capScope: "device",
86902
+ addonId: null,
86903
+ access: "create",
86904
+ role: "viewer"
86905
+ },
85240
86906
  "mediaPlayer.getStatus": {
85241
86907
  capName: "media-player",
85242
86908
  capScope: "device",
@@ -85328,6 +86994,34 @@ Object.freeze({
85328
86994
  access: "create",
85329
86995
  role: "viewer"
85330
86996
  },
86997
+ "mediaPlayer.turnOff": {
86998
+ capName: "media-player",
86999
+ capScope: "device",
87000
+ addonId: null,
87001
+ access: "create",
87002
+ role: "viewer"
87003
+ },
87004
+ "mediaPlayer.turnOn": {
87005
+ capName: "media-player",
87006
+ capScope: "device",
87007
+ addonId: null,
87008
+ access: "create",
87009
+ role: "viewer"
87010
+ },
87011
+ "mediaPlayer.volumeDown": {
87012
+ capName: "media-player",
87013
+ capScope: "device",
87014
+ addonId: null,
87015
+ access: "create",
87016
+ role: "viewer"
87017
+ },
87018
+ "mediaPlayer.volumeUp": {
87019
+ capName: "media-player",
87020
+ capScope: "device",
87021
+ addonId: null,
87022
+ access: "create",
87023
+ role: "viewer"
87024
+ },
85331
87025
  "meshNetwork.getStatus": {
85332
87026
  capName: "mesh-network",
85333
87027
  capScope: "system",
@@ -86574,27 +88268,6 @@ Object.freeze({
86574
88268
  access: "create",
86575
88269
  role: "viewer"
86576
88270
  },
86577
- "petFountain.setPumpInterval": {
86578
- capName: "pet-fountain",
86579
- capScope: "device",
86580
- addonId: null,
86581
- access: "create",
86582
- role: "viewer"
86583
- },
86584
- "petFountain.setPumpOffMinutes": {
86585
- capName: "pet-fountain",
86586
- capScope: "device",
86587
- addonId: null,
86588
- access: "create",
86589
- role: "viewer"
86590
- },
86591
- "petFountain.setPumpOnMinutes": {
86592
- capName: "pet-fountain",
86593
- capScope: "device",
86594
- addonId: null,
86595
- access: "create",
86596
- role: "viewer"
86597
- },
86598
88271
  "pipelineAnalytics.addRetrainFrameFromRecording": {
86599
88272
  capName: "pipeline-analytics",
86600
88273
  capScope: "device",
@@ -88373,6 +90046,34 @@ Object.freeze({
88373
90046
  access: "create",
88374
90047
  role: "admin"
88375
90048
  },
90049
+ "pumpCycle.getStatus": {
90050
+ capName: "pump-cycle",
90051
+ capScope: "device",
90052
+ addonId: null,
90053
+ access: "view",
90054
+ role: "viewer"
90055
+ },
90056
+ "pumpCycle.setEnabled": {
90057
+ capName: "pump-cycle",
90058
+ capScope: "device",
90059
+ addonId: null,
90060
+ access: "create",
90061
+ role: "viewer"
90062
+ },
90063
+ "pumpCycle.setOffMinutes": {
90064
+ capName: "pump-cycle",
90065
+ capScope: "device",
90066
+ addonId: null,
90067
+ access: "create",
90068
+ role: "viewer"
90069
+ },
90070
+ "pumpCycle.setOnMinutes": {
90071
+ capName: "pump-cycle",
90072
+ capScope: "device",
90073
+ addonId: null,
90074
+ access: "create",
90075
+ role: "viewer"
90076
+ },
88376
90077
  "reboot.reboot": {
88377
90078
  capName: "reboot",
88378
90079
  capScope: "device",
@@ -88877,6 +90578,13 @@ Object.freeze({
88877
90578
  access: "create",
88878
90579
  role: "admin"
88879
90580
  },
90581
+ "remote.getAssumedStatus": {
90582
+ capName: "remote",
90583
+ capScope: "system",
90584
+ addonId: null,
90585
+ access: "view",
90586
+ role: "admin"
90587
+ },
88880
90588
  "remote.getConfig": {
88881
90589
  capName: "remote",
88882
90590
  capScope: "system",
@@ -91692,6 +93400,11 @@ Object.freeze({
91692
93400
  form: "single",
91693
93401
  optional: false
91694
93402
  }],
93403
+ "coreBlocks.proposeRunningThreshold": [{
93404
+ name: "deviceId",
93405
+ form: "single",
93406
+ optional: false
93407
+ }],
91695
93408
  "cover.close": [{
91696
93409
  name: "deviceId",
91697
93410
  form: "single",
@@ -92543,6 +94256,16 @@ Object.freeze({
92543
94256
  form: "single",
92544
94257
  optional: true
92545
94258
  }],
94259
+ "mediaPlayer.channelDown": [{
94260
+ name: "deviceId",
94261
+ form: "single",
94262
+ optional: false
94263
+ }],
94264
+ "mediaPlayer.channelUp": [{
94265
+ name: "deviceId",
94266
+ form: "single",
94267
+ optional: false
94268
+ }],
92546
94269
  "mediaPlayer.getStatus": [{
92547
94270
  name: "deviceId",
92548
94271
  form: "single",
@@ -92608,6 +94331,26 @@ Object.freeze({
92608
94331
  form: "single",
92609
94332
  optional: false
92610
94333
  }],
94334
+ "mediaPlayer.turnOff": [{
94335
+ name: "deviceId",
94336
+ form: "single",
94337
+ optional: false
94338
+ }],
94339
+ "mediaPlayer.turnOn": [{
94340
+ name: "deviceId",
94341
+ form: "single",
94342
+ optional: false
94343
+ }],
94344
+ "mediaPlayer.volumeDown": [{
94345
+ name: "deviceId",
94346
+ form: "single",
94347
+ optional: false
94348
+ }],
94349
+ "mediaPlayer.volumeUp": [{
94350
+ name: "deviceId",
94351
+ form: "single",
94352
+ optional: false
94353
+ }],
92611
94354
  "meter.getStatus": [{
92612
94355
  name: "deviceId",
92613
94356
  form: "single",
@@ -92917,21 +94660,6 @@ Object.freeze({
92917
94660
  form: "single",
92918
94661
  optional: false
92919
94662
  }],
92920
- "petFountain.setPumpInterval": [{
92921
- name: "deviceId",
92922
- form: "single",
92923
- optional: false
92924
- }],
92925
- "petFountain.setPumpOffMinutes": [{
92926
- name: "deviceId",
92927
- form: "single",
92928
- optional: false
92929
- }],
92930
- "petFountain.setPumpOnMinutes": [{
92931
- name: "deviceId",
92932
- form: "single",
92933
- optional: false
92934
- }],
92935
94663
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
92936
94664
  name: "deviceId",
92937
94665
  form: "single",
@@ -93567,6 +95295,26 @@ Object.freeze({
93567
95295
  form: "single",
93568
95296
  optional: false
93569
95297
  }],
95298
+ "pumpCycle.getStatus": [{
95299
+ name: "deviceId",
95300
+ form: "single",
95301
+ optional: false
95302
+ }],
95303
+ "pumpCycle.setEnabled": [{
95304
+ name: "deviceId",
95305
+ form: "single",
95306
+ optional: false
95307
+ }],
95308
+ "pumpCycle.setOffMinutes": [{
95309
+ name: "deviceId",
95310
+ form: "single",
95311
+ optional: false
95312
+ }],
95313
+ "pumpCycle.setOnMinutes": [{
95314
+ name: "deviceId",
95315
+ form: "single",
95316
+ optional: false
95317
+ }],
93570
95318
  "reboot.reboot": [{
93571
95319
  name: "deviceId",
93572
95320
  form: "single",
@@ -93768,6 +95516,11 @@ Object.freeze({
93768
95516
  form: "single",
93769
95517
  optional: true
93770
95518
  }],
95519
+ "remote.getAssumedStatus": [{
95520
+ name: "deviceId",
95521
+ form: "single",
95522
+ optional: false
95523
+ }],
93771
95524
  "remote.getConfig": [{
93772
95525
  name: "deviceId",
93773
95526
  form: "single",
@@ -94315,99 +96068,6 @@ Object.freeze({
94315
96068
  "network-access": "ingress",
94316
96069
  "smtp-provider": "email"
94317
96070
  });
94318
- /**
94319
- * A saved Customize assignment (D936). The schema is the type: callers use
94320
- * `z.infer`, they do not restate the shape.
94321
- */
94322
- var OverrideSourceSchema = object({
94323
- addonId: string().min(1),
94324
- stableId: string().min(1),
94325
- cap: string().min(1),
94326
- fieldPath: string().min(1)
94327
- });
94328
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
94329
- object({ kind: literal("identity") }),
94330
- object({ kind: literal("invert") }),
94331
- object({
94332
- kind: literal("threshold"),
94333
- comparison: _enum(["gte", "lte"]),
94334
- threshold: number(),
94335
- startDwellMs: number().nonnegative(),
94336
- stopDwellMs: number().nonnegative()
94337
- }),
94338
- object({
94339
- kind: literal("linear"),
94340
- scale: number(),
94341
- offset: number(),
94342
- clampMin: number().optional(),
94343
- clampMax: number().optional()
94344
- }),
94345
- object({
94346
- kind: literal("enum-map"),
94347
- mapping: record(string(), string().nullable())
94348
- }),
94349
- object({ kind: literal("number") })
94350
- ]);
94351
- var ScalarOverrideDraftSchema = object({
94352
- kind: literal("scalar"),
94353
- cap: string().min(1),
94354
- targetId: string().min(1),
94355
- source: OverrideSourceSchema,
94356
- transform: CustomizeTransformDraftSchema
94357
- });
94358
- var KeyedArrayOverrideDraftSchema = object({
94359
- kind: literal("keyedArray"),
94360
- cap: string().min(1),
94361
- targetId: string().min(1),
94362
- key: string().min(1),
94363
- label: string().min(1),
94364
- fieldPath: string().min(1),
94365
- source: OverrideSourceSchema,
94366
- transform: CustomizeTransformDraftSchema
94367
- });
94368
- var PulseOverrideDraftSchema = object({
94369
- kind: literal("pulse"),
94370
- cap: string().min(1),
94371
- targetId: string().min(1),
94372
- enabled: boolean(),
94373
- debounceMs: number().nonnegative(),
94374
- edges: array(_enum([
94375
- "rising",
94376
- "button",
94377
- "native-event"
94378
- ])).min(1).optional(),
94379
- sources: array(OverrideSourceSchema).min(1)
94380
- });
94381
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
94382
- "in-addition",
94383
- "confirm",
94384
- "replace"
94385
- ]).optional() });
94386
- discriminatedUnion("kind", [
94387
- ScalarOverrideDraftSchema,
94388
- KeyedArrayOverrideDraftSchema,
94389
- PulseOverrideDraftSchema,
94390
- object({
94391
- kind: literal("span"),
94392
- cap: string().min(1),
94393
- targetId: string().min(1),
94394
- timeoutMs: number().nonnegative(),
94395
- cooldownMs: number().nonnegative().optional(),
94396
- sources: array(SpanSourceDraftSchema)
94397
- })
94398
- ]);
94399
- object({
94400
- targetId: string().min(1),
94401
- configured: boolean(),
94402
- source: _enum(["native", "assigned"]),
94403
- state: _enum([
94404
- "active",
94405
- "stale",
94406
- "released",
94407
- "unavailable"
94408
- ]),
94409
- reason: string().nullable()
94410
- });
94411
96071
  var G711_SCALE_CORRECTION_DB = {
94412
96072
  PCMU: 20 * Math.log10(4),
94413
96073
  PCMA: 20 * Math.log10(8)
@@ -95023,6 +96683,7 @@ var STATE_SUMMARY_VARS = [
95023
96683
  [...RULE_TEXT],
95024
96684
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95025
96685
  ([...RULE_TEXT], [...COMBINED_ALARM]),
96686
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
95026
96687
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95027
96688
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
95028
96689
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -95911,6 +97572,20 @@ var VeluxReconciler = class {
95911
97572
  }
95912
97573
  };
95913
97574
  //#endregion
97575
+ //#region src/custom-actions.ts
97576
+ /** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
97577
+ var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
97578
+ hosts: array(object({
97579
+ host: string(),
97580
+ name: string(),
97581
+ id: string()
97582
+ })),
97583
+ summary: string()
97584
+ }), {
97585
+ kind: "mutation",
97586
+ auth: "admin"
97587
+ }) });
97588
+ //#endregion
95914
97589
  //#region src/config.ts
95915
97590
  /**
95916
97591
  * Operator-supplied connection to ONE Velux KLF-200 gateway on the local
@@ -96034,20 +97709,6 @@ function buildConnectionFormSchema() {
96034
97709
  }] };
96035
97710
  }
96036
97711
  //#endregion
96037
- //#region src/custom-actions.ts
96038
- /** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
96039
- var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
96040
- hosts: array(object({
96041
- host: string(),
96042
- name: string(),
96043
- id: string()
96044
- })),
96045
- summary: string()
96046
- }), {
96047
- kind: "mutation",
96048
- auth: "admin"
96049
- }) });
96050
- //#endregion
96051
97712
  //#region src/velux-node-resolver.ts
96052
97713
  /**
96053
97714
  * Per-integration registry that device classes use to reach the live
@@ -96643,6 +98304,7 @@ var VeluxWindowDevice = class extends BaseDevice {
96643
98304
  stableIdSuffix: "rain",
96644
98305
  meta: {
96645
98306
  type: DeviceType.Sensor,
98307
+ role: DeviceRole.LeakSensor,
96646
98308
  name: `${this.name} rain`,
96647
98309
  ...link
96648
98310
  },
@@ -105368,7 +107030,54 @@ function veluxDiscoverySummary(foundCount, proposedCount) {
105368
107030
  return `Found ${proposedCount} ${proposedCount === 1 ? "gateway" : "gateways"}.`;
105369
107031
  }
105370
107032
  //#endregion
107033
+ //#region src/velux-instances.ts
107034
+ /**
107035
+ * Velux integrations as `integration-instances` (planning#416, D1007): one `connections` record
107036
+ * per integration under its registry id, holding the connection to ONE KLF-200 gateway. The
107037
+ * gateway password is declared a secret by type; the records door keeps it in the keychain.
107038
+ */
107039
+ var veluxConnectionsEntryKind = defineEntryKind("connections", {
107040
+ title: "Velux gateways",
107041
+ schema: entryBody({
107042
+ host: string().optional(),
107043
+ password: string().optional().meta({ secret: true })
107044
+ }),
107045
+ writeTrigger: "an operator adds, edits or removes a Velux KLF-200 gateway integration"
107046
+ });
107047
+ /** Every kind this addon declares, keyed by its name. */
107048
+ var veluxRecordKinds = { connections: veluxConnectionsEntryKind };
107049
+ function buildVeluxInstances(records, deps) {
107050
+ return new RecordIntegrationInstances(records, veluxConnectionsEntryKind, {
107051
+ description: {
107052
+ role: "devices",
107053
+ discovery: {
107054
+ mode: "account",
107055
+ scopedToInstance: true
107056
+ },
107057
+ instanceMode: "multiple",
107058
+ kind: "server",
107059
+ canTest: false
107060
+ },
107061
+ formSchema: () => buildConnectionFormSchema(),
107062
+ initialBody: (name) => ({ name }),
107063
+ configured: (body) => (body.host ?? "").length > 0 && body.password !== void 0,
107064
+ onChanged: () => deps.onChanged()
107065
+ });
107066
+ }
107067
+ /**
107068
+ * The gateway connection of one integration as the addon's runtime reads it: from the record,
107069
+ * never from the legacy rows. Null while the record is absent, incomplete, or its password is
107070
+ * unreadable (the runtime keeps what it had, D49).
107071
+ */
107072
+ async function veluxConnectionOf(instances, id) {
107073
+ const read = await instances.readResolved(id);
107074
+ if (read === null || read.secrets === "unavailable") return null;
107075
+ return connectionFromSettings(read.body);
107076
+ }
107077
+ //#endregion
105371
107078
  //#region src/addon.ts
107079
+ /** The date of the integration-rows-to-record migration (D1007). */
107080
+ var VELUX_MIGRATIONS_DATE = "2026-11-05";
105372
107081
  /**
105373
107082
  * Velux KLF-200 — `mode: account`, one integration per gateway.
105374
107083
  *
@@ -105391,11 +107100,27 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105391
107100
  };
105392
107101
  integrationReconciler = null;
105393
107102
  clients = null;
107103
+ instances = null;
105394
107104
  constructor() {
105395
107105
  super({});
105396
107106
  }
105397
107107
  async onInitialize() {
105398
107108
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
107109
+ const records = this.ctx.records;
107110
+ if (records === void 0) throw new Error("provider-velux: ctx.records is required (hub-only addon)");
107111
+ await records.declare(veluxRecordKinds);
107112
+ await this.runMigrations(defineAddonMigrations([integrationRowsMigration({
107113
+ addonId: this.addonId,
107114
+ date: VELUX_MIGRATIONS_DATE,
107115
+ kind: veluxConnectionsEntryKind,
107116
+ fixtureMark: "host"
107117
+ })], integrationRowsPortsOver({
107118
+ api: defaultIntegrationApiOver(this.ctx.api),
107119
+ records,
107120
+ kind: veluxConnectionsEntryKind,
107121
+ warn: (message) => this.ctx.logger.warn(message)
107122
+ })));
107123
+ this.instances = buildVeluxInstances(records, { onChanged: () => this.reconcileIntegrations() });
105399
107124
  setKlfLibraryLogSink((level, message) => {
105400
107125
  this.ctx.logger.debug(`klf-200-api ${level}: ${message}`);
105401
107126
  });
@@ -105421,6 +107146,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105421
107146
  {
105422
107147
  capability: connectionTestCapability,
105423
107148
  provider: this.buildConnectionTestProvider()
107149
+ },
107150
+ {
107151
+ capability: integrationInstancesCapability,
107152
+ provider: this.requireInstances().toProvider()
105424
107153
  }
105425
107154
  ],
105426
107155
  customActions: veluxActions,
@@ -105455,9 +107184,6 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105455
107184
  setKlfLibraryLogSink(null);
105456
107185
  await super.onShutdown();
105457
107186
  }
105458
- globalSettingsSchema() {
105459
- return buildConnectionFormSchema();
105460
- }
105461
107187
  async supportsManualCreation() {
105462
107188
  return false;
105463
107189
  }
@@ -105499,7 +107225,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105499
107225
  for (const integration of all) {
105500
107226
  if (integration.addonId !== this.ctx.id) continue;
105501
107227
  try {
105502
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
107228
+ const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
105503
107229
  if (connection) hosts.push(connection.host);
105504
107230
  } catch (err) {
105505
107231
  this.ctx.logger.warn("Velux discovery: settings read failed", {
@@ -105514,6 +107240,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105514
107240
  return [];
105515
107241
  }
105516
107242
  }
107243
+ requireInstances() {
107244
+ if (!this.instances) throw new Error("Velux provider not initialised");
107245
+ return this.instances;
107246
+ }
105517
107247
  requireClients() {
105518
107248
  if (!this.clients) throw new Error("Velux provider not initialised");
105519
107249
  return this.clients;
@@ -105530,7 +107260,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
105530
107260
  },
105531
107261
  listIntegrations: () => this.ctx.api.integrations.list.query(),
105532
107262
  applySettings: async (integration) => {
105533
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
107263
+ const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
105534
107264
  if (!connection) return false;
105535
107265
  await this.requireClients().upsert(integration.id, integration.name, connection);
105536
107266
  return true;