@camstack/addon-provider-rademacher 0.2.157 → 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
@@ -788,7 +788,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
788
788
  return EventCategory;
789
789
  }({});
790
790
  //#endregion
791
- //#region ../types/dist/device-type-CfHw_quR.mjs
791
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
792
792
  /**
793
793
  import { errMsg } from '@camstack/types'
794
794
  * Extract a human-readable message from an unknown error value.
@@ -1019,6 +1019,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1019
1019
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1020
1020
  /** Media player exposes stop distinct from pause. */
1021
1021
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1022
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1023
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1024
+ /** Media player exposes channel up / down in steps. */
1025
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1026
+ /** Media player exposes power on / off. */
1027
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1022
1028
  /** Alarm panel requires a PIN code on arm/disarm. */
1023
1029
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1024
1030
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -4555,6 +4561,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
4555
4561
  var booleanProcessor = (_schema, _ctx, json, _params) => {
4556
4562
  json.type = "boolean";
4557
4563
  };
4564
+ var bigintProcessor = (_schema, ctx, _json, _params) => {
4565
+ if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
4566
+ };
4567
+ var symbolProcessor = (_schema, ctx, _json, _params) => {
4568
+ if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
4569
+ };
4558
4570
  var nullProcessor = (_schema, ctx, json, _params) => {
4559
4571
  if (ctx.target === "openapi-3.0") {
4560
4572
  json.type = "string";
@@ -4562,12 +4574,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
4562
4574
  json.enum = [null];
4563
4575
  } else json.type = "null";
4564
4576
  };
4577
+ var undefinedProcessor = (_schema, ctx, _json, _params) => {
4578
+ if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
4579
+ };
4565
4580
  var voidProcessor = (_schema, ctx, _json, _params) => {
4566
4581
  if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
4567
4582
  };
4568
4583
  var neverProcessor = (_schema, _ctx, json, _params) => {
4569
4584
  json.not = {};
4570
4585
  };
4586
+ var anyProcessor = (_schema, _ctx, _json, _params) => {};
4587
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {};
4571
4588
  var dateProcessor = (_schema, ctx, _json, _params) => {
4572
4589
  if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
4573
4590
  };
@@ -4599,12 +4616,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
4599
4616
  json.enum = vals;
4600
4617
  }
4601
4618
  };
4619
+ var nanProcessor = (_schema, ctx, _json, _params) => {
4620
+ if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
4621
+ };
4622
+ var templateLiteralProcessor = (schema, _ctx, json, _params) => {
4623
+ const _json = json;
4624
+ const pattern = schema._zod.pattern;
4625
+ if (!pattern) throw new Error("Pattern not found in template literal");
4626
+ _json.type = "string";
4627
+ _json.pattern = pattern.source;
4628
+ };
4629
+ var fileProcessor = (schema, _ctx, json, _params) => {
4630
+ const _json = json;
4631
+ const file = {
4632
+ type: "string",
4633
+ format: "binary",
4634
+ contentEncoding: "binary"
4635
+ };
4636
+ const { minimum, maximum, mime } = schema._zod.bag;
4637
+ if (minimum !== void 0) file.minLength = minimum;
4638
+ if (maximum !== void 0) file.maxLength = maximum;
4639
+ if (mime) if (mime.length === 1) {
4640
+ file.contentMediaType = mime[0];
4641
+ Object.assign(_json, file);
4642
+ } else {
4643
+ Object.assign(_json, file);
4644
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
4645
+ }
4646
+ else Object.assign(_json, file);
4647
+ };
4648
+ var successProcessor = (_schema, _ctx, json, _params) => {
4649
+ json.type = "boolean";
4650
+ };
4602
4651
  var customProcessor = (_schema, ctx, _json, _params) => {
4603
4652
  if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
4604
4653
  };
4654
+ var functionProcessor = (_schema, ctx, _json, _params) => {
4655
+ if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
4656
+ };
4605
4657
  var transformProcessor = (_schema, ctx, _json, _params) => {
4606
4658
  if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
4607
4659
  };
4660
+ var mapProcessor = (_schema, ctx, _json, _params) => {
4661
+ if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
4662
+ };
4663
+ var setProcessor = (_schema, ctx, _json, _params) => {
4664
+ if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
4665
+ };
4608
4666
  var arrayProcessor = (schema, ctx, _json, params) => {
4609
4667
  const json = _json;
4610
4668
  const def = schema._zod.def;
@@ -4809,6 +4867,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
4809
4867
  seen.ref = def.innerType;
4810
4868
  json.readOnly = true;
4811
4869
  };
4870
+ var promiseProcessor = (schema, ctx, _json, params) => {
4871
+ const def = schema._zod.def;
4872
+ process(def.innerType, ctx, params);
4873
+ const seen = ctx.seen.get(schema);
4874
+ seen.ref = def.innerType;
4875
+ };
4812
4876
  var optionalProcessor = (schema, ctx, _json, params) => {
4813
4877
  const def = schema._zod.def;
4814
4878
  process(def.innerType, ctx, params);
@@ -4821,6 +4885,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
4821
4885
  const seen = ctx.seen.get(schema);
4822
4886
  seen.ref = innerType;
4823
4887
  };
4888
+ var allProcessors = {
4889
+ string: stringProcessor,
4890
+ number: numberProcessor,
4891
+ boolean: booleanProcessor,
4892
+ bigint: bigintProcessor,
4893
+ symbol: symbolProcessor,
4894
+ null: nullProcessor,
4895
+ undefined: undefinedProcessor,
4896
+ void: voidProcessor,
4897
+ never: neverProcessor,
4898
+ any: anyProcessor,
4899
+ unknown: unknownProcessor,
4900
+ date: dateProcessor,
4901
+ enum: enumProcessor,
4902
+ literal: literalProcessor,
4903
+ nan: nanProcessor,
4904
+ template_literal: templateLiteralProcessor,
4905
+ file: fileProcessor,
4906
+ success: successProcessor,
4907
+ custom: customProcessor,
4908
+ function: functionProcessor,
4909
+ transform: transformProcessor,
4910
+ map: mapProcessor,
4911
+ set: setProcessor,
4912
+ array: arrayProcessor,
4913
+ object: objectProcessor,
4914
+ union: unionProcessor,
4915
+ intersection: intersectionProcessor,
4916
+ tuple: tupleProcessor,
4917
+ record: recordProcessor,
4918
+ nullable: nullableProcessor,
4919
+ nonoptional: nonoptionalProcessor,
4920
+ default: defaultProcessor,
4921
+ prefault: prefaultProcessor,
4922
+ catch: catchProcessor,
4923
+ pipe: pipeProcessor,
4924
+ readonly: readonlyProcessor,
4925
+ promise: promiseProcessor,
4926
+ optional: optionalProcessor,
4927
+ lazy: lazyProcessor
4928
+ };
4929
+ function toJSONSchema(input, params) {
4930
+ if ("_idmap" in input) {
4931
+ const registry = input;
4932
+ const ctx = initializeContext({
4933
+ ...params,
4934
+ processors: allProcessors
4935
+ });
4936
+ const defs = {};
4937
+ for (const entry of registry._idmap.entries()) {
4938
+ const [_, schema] = entry;
4939
+ process(schema, ctx);
4940
+ }
4941
+ const schemas = {};
4942
+ ctx.external = {
4943
+ registry,
4944
+ uri: params?.uri,
4945
+ defs
4946
+ };
4947
+ for (const entry of registry._idmap.entries()) {
4948
+ const [key, schema] = entry;
4949
+ extractDefs(ctx, schema);
4950
+ schemas[key] = finalize(ctx, schema);
4951
+ }
4952
+ if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
4953
+ return { schemas };
4954
+ }
4955
+ const ctx = initializeContext({
4956
+ ...params,
4957
+ processors: allProcessors
4958
+ });
4959
+ process(input, ctx);
4960
+ extractDefs(ctx, input);
4961
+ return finalize(ctx, input);
4962
+ }
4824
4963
  //#endregion
4825
4964
  //#region ../../node_modules/zod/v4/classic/iso.js
4826
4965
  var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
@@ -5845,7 +5984,7 @@ var ZodIssueCode = {
5845
5984
  var ZodFirstPartyTypeKind;
5846
5985
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5847
5986
  //#endregion
5848
- //#region ../types/dist/sleep-QsoHs66J.mjs
5987
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
5849
5988
  /**
5850
5989
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5851
5990
  * window to float samples (D455).
@@ -5924,6 +6063,22 @@ function buildAlawTable() {
5924
6063
  }
5925
6064
  buildUlawTable();
5926
6065
  buildAlawTable();
6066
+ /**
6067
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
6068
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
6069
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
6070
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
6071
+ */
6072
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
6073
+ var AddonMigrationsBlockedError = class extends Error {
6074
+ /** The ledger ids that did not apply and block the owner. */
6075
+ migrationIds;
6076
+ constructor(migrationIds) {
6077
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
6078
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
6079
+ this.migrationIds = migrationIds;
6080
+ }
6081
+ };
5927
6082
  /** Is this field's stored value a credential? */
5928
6083
  function isSecretConfigField(field) {
5929
6084
  if (field.type === "password") return true;
@@ -5977,9 +6132,16 @@ function isSecretMarker(value) {
5977
6132
  const ref = value["$secret"];
5978
6133
  return typeof ref === "string" && ref.length > 0;
5979
6134
  }
6135
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6136
+ function isSecretMarkerLike(value) {
6137
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6138
+ }
5980
6139
  function isSecretClear(value) {
5981
6140
  return isRecord$7(value) && value["$secretClear"] === true;
5982
6141
  }
6142
+ function isSecretFieldView(value) {
6143
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6144
+ }
5983
6145
  /**
5984
6146
  * The UI view of a stored secret field value: a marker becomes "set", inline
5985
6147
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6361,6 +6523,53 @@ function emitReadiness(bus, params) {
6361
6523
  ts
6362
6524
  }));
6363
6525
  }
6526
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6527
+ var AddonMigrationDefinitionError = class extends Error {
6528
+ id;
6529
+ constructor(id, message) {
6530
+ super(`${id}: ${message}`);
6531
+ this.id = id;
6532
+ this.name = "AddonMigrationDefinitionError";
6533
+ }
6534
+ };
6535
+ /**
6536
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6537
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6538
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6539
+ */
6540
+ function defineAddonMigrations(catalogue, ports) {
6541
+ return catalogue.map((entry) => {
6542
+ const { meta } = entry;
6543
+ if ("plan" in entry) {
6544
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6545
+ return {
6546
+ meta,
6547
+ execute: () => guarded(async () => ({
6548
+ kind: "plan",
6549
+ plan: await entry.plan(ports)
6550
+ }))
6551
+ };
6552
+ }
6553
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6554
+ return {
6555
+ meta,
6556
+ execute: () => guarded(async () => ({
6557
+ kind: "procedure",
6558
+ result: await entry.procedure(ports)
6559
+ }))
6560
+ };
6561
+ });
6562
+ }
6563
+ async function guarded(work) {
6564
+ try {
6565
+ return await work();
6566
+ } catch (err) {
6567
+ return {
6568
+ kind: "failed",
6569
+ error: errMsg(err)
6570
+ };
6571
+ }
6572
+ }
6364
6573
  var NOT_RUN = {
6365
6574
  declared: false,
6366
6575
  snapshot: null,
@@ -6375,7 +6584,7 @@ var NOT_RUN = {
6375
6584
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6376
6585
  * because the hub (and this addon's boot) must not hang on a migration.
6377
6586
  */
6378
- async function runAddonMigrations(host, migrations, logger) {
6587
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6379
6588
  const first = migrations[0];
6380
6589
  if (first === void 0) return NOT_RUN;
6381
6590
  const owner = first.meta.owner;
@@ -6404,7 +6613,9 @@ async function runAddonMigrations(host, migrations, logger) {
6404
6613
  else if (status === "blocked") blocked.push(id);
6405
6614
  else skipped.push(id);
6406
6615
  };
6616
+ const selected = options.only === void 0 ? null : new Set(options.only);
6407
6617
  for (const step of declaration.steps) {
6618
+ if (selected !== null && !selected.has(step.id)) continue;
6408
6619
  const bound = byId.get(step.id);
6409
6620
  if (bound === void 0 || step.action === "skip") {
6410
6621
  skipped.push(step.id);
@@ -6582,11 +6793,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6582
6793
  }
6583
6794
  return out;
6584
6795
  }
6796
+ /**
6797
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6798
+ *
6799
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6800
+ * scope (global, one node, one device, one integration, one row of a business
6801
+ * collection) and the field path inside that scope — so saving the same field
6802
+ * twice updates the same ref, and the ref itself says which settings blob
6803
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6804
+ * copied onto another ref does not decrypt.
6805
+ *
6806
+ * Array items are addressed by ITEM ID, never by index
6807
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6808
+ *
6809
+ * Pure and browser-safe: no node builtins, no I/O.
6810
+ */
6811
+ /** The URI scheme of every keychain ref. */
6812
+ var SECRET_REF_SCHEME = "secret://";
6585
6813
  var GLOBAL_SCOPE = { kind: "global" };
6586
6814
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6587
6815
  function globalSecretScope() {
6588
6816
  return GLOBAL_SCOPE;
6589
6817
  }
6818
+ /**
6819
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6820
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6821
+ * addon running forked.
6822
+ */
6823
+ function normalizeSecretOwner(addonId) {
6824
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6825
+ }
6590
6826
  function encodeSegment(value) {
6591
6827
  return encodeURIComponent(value);
6592
6828
  }
@@ -6597,6 +6833,16 @@ function decodeSegment(value) {
6597
6833
  return null;
6598
6834
  }
6599
6835
  }
6836
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6837
+ function formatSecretScope(scope) {
6838
+ switch (scope.kind) {
6839
+ case "global": return "global";
6840
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6841
+ case "device": return `device/${scope.deviceId}`;
6842
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6843
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6844
+ }
6845
+ }
6600
6846
  /**
6601
6847
  * The field path of a value inside an array item, addressed by the item's id:
6602
6848
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6605,6 +6851,13 @@ function decodeSegment(value) {
6605
6851
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6606
6852
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6607
6853
  }
6854
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6855
+ function buildSecretRef(parts) {
6856
+ const owner = normalizeSecretOwner(parts.owner);
6857
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6858
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6859
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6860
+ }
6608
6861
  function parseScope(segments) {
6609
6862
  switch (segments[0]) {
6610
6863
  case "global": return {
@@ -7522,15 +7775,19 @@ var BaseAddon = class {
7522
7775
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7523
7776
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7524
7777
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7778
+ *
7779
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7780
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7781
+ * omitted applied ids "newer").
7525
7782
  */
7526
- async runMigrations(migrations) {
7783
+ async runMigrations(migrations, options) {
7527
7784
  const api = this.ctx.api.migrations;
7528
7785
  const report = await runAddonMigrations({
7529
7786
  declareOwner: (input) => api.declareOwner.mutate(input),
7530
7787
  startMigration: (input) => api.startMigration.mutate(input),
7531
7788
  finishMigration: (input) => api.finishMigration.mutate(input)
7532
- }, migrations, this.ctx.logger);
7533
- 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`);
7789
+ }, migrations, this.ctx.logger, options);
7790
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7534
7791
  return report;
7535
7792
  }
7536
7793
  /**
@@ -7870,6 +8127,10 @@ var BaseAddon = class {
7870
8127
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
7871
8128
  const patch = {};
7872
8129
  for (const key of keys) {
8130
+ if (tree.get(key)?.kind === "array") {
8131
+ patch[key] = [];
8132
+ continue;
8133
+ }
7873
8134
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
7874
8135
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
7875
8136
  }
@@ -8509,6 +8770,45 @@ function normalizeAddonInitResult(result) {
8509
8770
  if (Array.isArray(result)) return { providers: result };
8510
8771
  return result;
8511
8772
  }
8773
+ var PREFIX = "records/";
8774
+ /** The base of every records failure. `detail` is the human part of the message. */
8775
+ var RecordsError = class extends Error {
8776
+ code;
8777
+ detail;
8778
+ constructor(code, detail) {
8779
+ super(`${PREFIX}${code}: ${detail}`);
8780
+ this.name = "RecordsError";
8781
+ this.code = code;
8782
+ this.detail = detail;
8783
+ }
8784
+ };
8785
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8786
+ var RevConflictError = class extends RecordsError {
8787
+ constructor(detail) {
8788
+ super("rev-conflict", detail);
8789
+ this.name = "RevConflictError";
8790
+ }
8791
+ };
8792
+ /** The keychain could not store or serve a value: explicit, never an empty string. */
8793
+ var RecordSecretUnavailableError = class extends RecordsError {
8794
+ constructor(detail) {
8795
+ super("secret-unavailable", detail);
8796
+ this.name = "RecordSecretUnavailableError";
8797
+ }
8798
+ };
8799
+ var InvalidRecordError = class extends RecordsError {
8800
+ constructor(detail) {
8801
+ super("invalid-record", detail);
8802
+ this.name = "InvalidRecordError";
8803
+ }
8804
+ };
8805
+ /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
8806
+ var DeclarationRefusedError = class extends RecordsError {
8807
+ constructor(detail) {
8808
+ super("declaration-refused", detail);
8809
+ this.name = "DeclarationRefusedError";
8810
+ }
8811
+ };
8512
8812
  /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8513
8813
  /** The files a kind may live in. `history` is device-history's (D850), never records. */
8514
8814
  var RECORD_DATABASES = ["settings", "events"];
@@ -8524,6 +8824,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8524
8824
  var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8525
8825
  var RECORD_BATCH_MAX = 1e3;
8526
8826
  var RECORD_QUERY_MAX = 1e3;
8827
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8828
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8829
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8830
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8831
+ /**
8832
+ * Secret fields of a record, addressed the way a Zod schema declares them
8833
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8834
+ * (`auth.accessToken`, `headers[].value`).
8835
+ *
8836
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8837
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8838
+ * key releases exactly that key's secret.
8839
+ *
8840
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8841
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8842
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8843
+ * leaf names its element: `headers[X-Api].value`.
8844
+ *
8845
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8846
+ */
8847
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8848
+ function parseSecretPath(path) {
8849
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8850
+ return path.split(".").map((raw) => {
8851
+ const each = raw.endsWith("[]");
8852
+ const map = raw.endsWith("{}");
8853
+ const key = each || map ? raw.slice(0, -2) : raw;
8854
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8855
+ return {
8856
+ key,
8857
+ each,
8858
+ map
8859
+ };
8860
+ });
8861
+ }
8862
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8863
+ function secretArrayPrefixes(path) {
8864
+ const segments = parseSecretPath(path);
8865
+ const prefixes = [];
8866
+ const walked = [];
8867
+ for (const segment of segments) {
8868
+ walked.push(segment.key);
8869
+ if (segment.each) prefixes.push(walked.join("."));
8870
+ }
8871
+ return prefixes;
8872
+ }
8873
+ function isRecord$4(value) {
8874
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8875
+ }
8876
+ function elementId(element, idKey, declared, prefix) {
8877
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8878
+ 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`);
8879
+ return id;
8880
+ }
8881
+ /**
8882
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8883
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8884
+ * result is a new body, `data` is untouched.
8885
+ */
8886
+ var REMOVE_LEAF = Symbol("remove-leaf");
8887
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8888
+ if (!isRecord$4(node)) return node;
8889
+ const segment = segments[index];
8890
+ if (segment === void 0) return node;
8891
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8892
+ const child = node[segment.key];
8893
+ const keys = [...walkedKeys, segment.key];
8894
+ const last = index === segments.length - 1;
8895
+ if (segment.each) {
8896
+ if (!Array.isArray(child)) return node;
8897
+ const prefix = keys.join(".");
8898
+ const idKey = arrayKeys[prefix];
8899
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8900
+ const mapped = child.map((element) => {
8901
+ const id = elementId(element, idKey, declared, prefix);
8902
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8903
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8904
+ });
8905
+ return {
8906
+ ...node,
8907
+ [segment.key]: mapped
8908
+ };
8909
+ }
8910
+ if (segment.map) {
8911
+ if (!isRecord$4(child)) return node;
8912
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8913
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8914
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8915
+ return [mapKey, visit({
8916
+ declared,
8917
+ concrete,
8918
+ value
8919
+ })];
8920
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8921
+ return {
8922
+ ...node,
8923
+ [segment.key]: Object.fromEntries(kept)
8924
+ };
8925
+ }
8926
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8927
+ if (last) {
8928
+ const replacement = visit({
8929
+ declared,
8930
+ concrete,
8931
+ value: child
8932
+ });
8933
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8934
+ return {
8935
+ ...node,
8936
+ [segment.key]: replacement
8937
+ };
8938
+ }
8939
+ return {
8940
+ ...node,
8941
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8942
+ };
8943
+ }
8944
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8945
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8946
+ let current = data;
8947
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8948
+ return current;
8949
+ }
8950
+ /** Every present secret leaf, read-only. */
8951
+ function collectSecretLeaves(data, paths, arrayKeys) {
8952
+ const found = [];
8953
+ mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
8954
+ found.push(leaf);
8955
+ return leaf.value;
8956
+ });
8957
+ return found;
8958
+ }
8959
+ /**
8960
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8961
+ * record's refs never collide with the refs a legacy collection of the same name
8962
+ * already holds during an adoption.
8963
+ */
8964
+ function recordSecretCollection(kind) {
8965
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8966
+ }
8967
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8968
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8969
+ return buildSecretRef({
8970
+ owner,
8971
+ scope: {
8972
+ kind: "row",
8973
+ collection: recordSecretCollection(kind),
8974
+ rowId
8975
+ },
8976
+ fieldPath: concretePath
8977
+ });
8978
+ }
8979
+ /**
8980
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8981
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8982
+ * token. The form hands it to the secret field, which reveals the value by ref
8983
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8984
+ * stored secret (the records door reads a view as KEEP).
8985
+ *
8986
+ * ONE implementation for the entries service and for every addon that serves
8987
+ * its own instances (`RecordIntegrationInstances`).
8988
+ */
8989
+ function isRecord$3(value) {
8990
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8991
+ }
8992
+ function secretViewsOf(owner, kind, id, decl, data) {
8993
+ if (decl.secretPaths.length === 0) return data;
8994
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8995
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8996
+ return {
8997
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8998
+ v: 1,
8999
+ set: true
9000
+ };
9001
+ });
9002
+ return isRecord$3(shown) ? shown : data;
9003
+ }
8527
9004
  /**
8528
9005
  * The share-view scope kinds a share-link token can carry. The hub mints
8529
9006
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8596,7 +9073,8 @@ function isZodSecret(schema) {
8596
9073
  }
8597
9074
  /**
8598
9075
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8599
- * element of this array" (`connection.password`, `rows[].token`). A declared
9076
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9077
+ * map" (`applianceSecrets{}`). A declared
8600
9078
  * object or array is reported as one path and not descended into — everything
8601
9079
  * under it is secret.
8602
9080
  */
@@ -8607,6 +9085,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8607
9085
  const def = defOf(layer);
8608
9086
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8609
9087
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9088
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8610
9089
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8611
9090
  else if (def.type === "intersection") {
8612
9091
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -8681,6 +9160,126 @@ var RecordKindDeclarationSchema = object({
8681
9160
  }).strict();
8682
9161
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8683
9162
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9163
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9164
+ function assertValidDeclaration(name, decl) {
9165
+ const refuse = (why) => {
9166
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9167
+ };
9168
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9169
+ const seen = /* @__PURE__ */ new Set();
9170
+ for (const index of decl.indexes) {
9171
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9172
+ seen.add(index.name);
9173
+ }
9174
+ const { retention } = decl;
9175
+ const hasSecrets = decl.secretPaths.length > 0;
9176
+ if ("ttlMs" in retention) {
9177
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9178
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9179
+ }
9180
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9181
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9182
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9183
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9184
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9185
+ 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)`);
9186
+ }
9187
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9188
+ 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`);
9189
+ }
9190
+ function bodyFieldsOf(schema) {
9191
+ return { ...toJSONSchema(schema, {
9192
+ unrepresentable: "any",
9193
+ io: "input"
9194
+ }) };
9195
+ }
9196
+ function isRecordLike(value) {
9197
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9198
+ }
9199
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9200
+ function hasNameField(json, field) {
9201
+ const branches = json["oneOf"] ?? json["anyOf"];
9202
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9203
+ const properties = json["properties"];
9204
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9205
+ }
9206
+ /**
9207
+ * Declare the kinds of a package. The keys are the kind names. The result is
9208
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9209
+ * types its handle from.
9210
+ */
9211
+ function defineRecordKinds(specs) {
9212
+ const out = {};
9213
+ for (const [name, spec] of Object.entries(specs)) {
9214
+ const secretPaths = collectZodSecretPaths(spec.schema);
9215
+ const nameField = spec.entry?.nameField ?? "name";
9216
+ const fields = bodyFieldsOf(spec.schema);
9217
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9218
+ const parsed = RecordKindDeclarationSchema.parse({
9219
+ database: spec.database,
9220
+ class: spec.class,
9221
+ writeTrigger: spec.writeTrigger,
9222
+ device: spec.device,
9223
+ at: spec.at,
9224
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9225
+ retention: spec.retention,
9226
+ deviceRemoval: spec.deviceRemoval,
9227
+ maxDataBytes: spec.maxDataBytes,
9228
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9229
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9230
+ transactFamily: spec.transactFamily,
9231
+ entry: spec.entry === void 0 ? void 0 : {
9232
+ title: spec.entry.title,
9233
+ nameField,
9234
+ fields
9235
+ }
9236
+ });
9237
+ assertValidDeclaration(name, parsed);
9238
+ out[name] = {
9239
+ name,
9240
+ schema: spec.schema,
9241
+ declaration: parsed
9242
+ };
9243
+ }
9244
+ return out;
9245
+ }
9246
+ /**
9247
+ * Entry kinds (D881): every list of user-created entries (notification
9248
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9249
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9250
+ * through the `record-entries` capability with password fields derived from
9251
+ * the schema (`.meta({ secret: true })`).
9252
+ */
9253
+ /** The body of a plain entry: a name and the fields the kind adds. */
9254
+ function entryBody(shape) {
9255
+ return object({
9256
+ name: string().min(1),
9257
+ ...shape
9258
+ });
9259
+ }
9260
+ /**
9261
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9262
+ * engine, never aged, belonging to no device.
9263
+ */
9264
+ function defineEntryKind(name, spec) {
9265
+ const defined = defineRecordKinds({ [name]: {
9266
+ schema: spec.schema,
9267
+ class: "config",
9268
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9269
+ retention: { none: "an entry goes only when the operator deletes it" },
9270
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9271
+ ...spec.database === void 0 ? {} : { database: spec.database },
9272
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9273
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9274
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9275
+ entry: {
9276
+ title: spec.title,
9277
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9278
+ }
9279
+ } })[name];
9280
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9281
+ return defined;
9282
+ }
8684
9283
  object({
8685
9284
  field: string().min(1),
8686
9285
  on: literal("advance")
@@ -9337,6 +9936,7 @@ var CAP_NAMES = Object.freeze([
9337
9936
  "backup",
9338
9937
  "battery",
9339
9938
  "binary",
9939
+ "board-key-source",
9340
9940
  "brightness",
9341
9941
  "broker",
9342
9942
  "button",
@@ -9458,6 +10058,7 @@ var CAP_NAMES = Object.freeze([
9458
10058
  "privacy-mask",
9459
10059
  "ptz",
9460
10060
  "ptz-autotrack",
10061
+ "pump-cycle",
9461
10062
  "reboot",
9462
10063
  "record-entries",
9463
10064
  "recording",
@@ -10750,18 +11351,6 @@ Object.freeze({
10750
11351
  "path": "pumpRunning",
10751
11352
  "valueType": "boolean"
10752
11353
  },
10753
- {
10754
- "path": "pumpIntervalEnabled",
10755
- "valueType": "boolean"
10756
- },
10757
- {
10758
- "path": "pumpOnTime",
10759
- "valueType": "number"
10760
- },
10761
- {
10762
- "path": "pumpOffTime",
10763
- "valueType": "number"
10764
- },
10765
11354
  {
10766
11355
  "path": "lastDrink",
10767
11356
  "valueType": "number"
@@ -10885,6 +11474,20 @@ Object.freeze({
10885
11474
  "valueType": "number"
10886
11475
  }
10887
11476
  ],
11477
+ "pump-cycle": [
11478
+ {
11479
+ "path": "enabled",
11480
+ "valueType": "boolean"
11481
+ },
11482
+ {
11483
+ "path": "onTime",
11484
+ "valueType": "number"
11485
+ },
11486
+ {
11487
+ "path": "offTime",
11488
+ "valueType": "number"
11489
+ }
11490
+ ],
10888
11491
  "recording-archive": [
10889
11492
  {
10890
11493
  "path": "deviceId",
@@ -11334,7 +11937,7 @@ object({
11334
11937
  params: record(string(), union([string(), number()])).optional()
11335
11938
  });
11336
11939
  //#endregion
11337
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11940
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11338
11941
  /**
11339
11942
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11340
11943
  *
@@ -12097,38 +12700,6 @@ function validateExpressionSource(src) {
12097
12700
  return null;
12098
12701
  }
12099
12702
  /**
12100
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12101
- *
12102
- * Standby of the control board is under 5 W; a motor or a heater is far
12103
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12104
- * a cycle, so running starts only after 30 s above the line.
12105
- *
12106
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12107
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12108
- * report "finished" in the middle of the program, so the stop dwell is
12109
- * 10 minutes on every type. The number is stored per type so one machine
12110
- * can move later without moving the others.
12111
- */
12112
- var APPLIANCE_TEMPLATE_ID = "appliance";
12113
- var APPLIANCE_KINDS = [
12114
- "washer",
12115
- "dryer",
12116
- "dishwasher"
12117
- ];
12118
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12119
- /**
12120
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12121
- *
12122
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12123
- * (D967), so the heater switches exactly as the plug does. Running is the
12124
- * plug being on; when a power reading is also named, running is the draw
12125
- * staying above the line instead (a plug left on with the stove's own
12126
- * thermostat open is not heating). 50 W is far above any standby and below
12127
- * the smallest radiator.
12128
- */
12129
- var HEATER_TEMPLATE_ID = "heater";
12130
- DeviceType.Heater;
12131
- /**
12132
12703
  * Statistics and integrals over a history read (D944).
12133
12704
  *
12134
12705
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12272,34 +12843,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12272
12843
  /** Actuator groups only. Readings leave this unset. */
12273
12844
  active: ActiveCriterionSchema.optional()
12274
12845
  }),
12275
- object({
12276
- kind: literal("appliance"),
12277
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12278
- appliance: _enum(APPLIANCE_KINDS),
12279
- thresholdWatts: number().positive(),
12280
- startDwellMs: number().int().nonnegative(),
12281
- stopDwellMs: number().int().nonnegative(),
12282
- member: HelperMemberSchema,
12283
- /**
12284
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12285
- * composition carries a `power-source` feature, so the hub can cut and restore
12286
- * that plug (power-source link of a NEW-target composition).
12287
- */
12288
- powerSwitch: HelperMemberSchema.optional(),
12289
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12290
- offSeconds: number().optional()
12291
- }),
12292
- object({
12293
- kind: literal("heater"),
12294
- templateId: literal(HEATER_TEMPLATE_ID),
12295
- /** The plug, read and commanded at `switch.on`. */
12296
- plug: HelperMemberSchema,
12297
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12298
- meter: HelperMemberSchema.optional(),
12299
- thresholdWatts: number().positive(),
12300
- startDwellMs: number().int().nonnegative(),
12301
- stopDwellMs: number().int().nonnegative()
12302
- }),
12303
12846
  object({
12304
12847
  kind: literal("statistic"),
12305
12848
  stat: _enum(STATISTIC_STATS),
@@ -12471,7 +13014,14 @@ var CompositionForwardCommandSchema = object({
12471
13014
  method: string().min(1),
12472
13015
  args: record(string().min(1), CompositionForwardArgSchema).optional()
12473
13016
  });
12474
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
13017
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
13018
+ CompositionForwardCommandSchema,
13019
+ CompositionCodeSourceSchema,
13020
+ object({
13021
+ kind: literal("extension"),
13022
+ target: literal("remote")
13023
+ })
13024
+ ]);
12475
13025
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12476
13026
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12477
13027
  /**
@@ -12516,7 +13066,7 @@ var CompositionSchema = object({
12516
13066
  kind: literal("existing"),
12517
13067
  device: CompositionSourceRefSchema
12518
13068
  })]),
12519
- features: array(CompositionFeatureSchema).min(1).max(16),
13069
+ features: array(CompositionFeatureSchema).min(0).max(16),
12520
13070
  /**
12521
13071
  * Present when the block is a helper template. Absent on a free
12522
13072
  * composition (the room example, device 4603). The editor reads this,
@@ -12548,6 +13098,73 @@ var NodeColorIdSchema = _enum([
12548
13098
  "node-g",
12549
13099
  "node-h"
12550
13100
  ]);
13101
+ //#endregion
13102
+ //#region ../types/dist/index.mjs
13103
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
13104
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
13105
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
13106
+ /** The text excerpt a message carries on the bus. */
13107
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
13108
+ var PortSchema = number().int().min(1).max(65535);
13109
+ /** A recipient local-part: what stands before the `@`. */
13110
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
13111
+ object({
13112
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
13113
+ bindHost: string().min(1).default("0.0.0.0"),
13114
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
13115
+ requireAuth: boolean().default(true),
13116
+ username: string().default(""),
13117
+ password: string().default("").meta({ secret: true }),
13118
+ starttls: boolean().default(false),
13119
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13120
+ });
13121
+ object({
13122
+ host: string().min(1).nullable(),
13123
+ hostReason: string().nullable(),
13124
+ port: PortSchema,
13125
+ domain: string().min(1),
13126
+ requireAuth: boolean(),
13127
+ username: string(),
13128
+ password: string(),
13129
+ starttls: boolean()
13130
+ });
13131
+ object({
13132
+ recipients: array(SmtpRecipientSchema).min(1),
13133
+ /**
13134
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13135
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13136
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13137
+ * Absent, the subscriber is anonymous and unique.
13138
+ */
13139
+ subscriberId: string().min(1).max(64).optional()
13140
+ });
13141
+ var SmtpInboundAttachmentSchema = object({
13142
+ filename: string(),
13143
+ contentType: string(),
13144
+ bytes: number().int().nonnegative()
13145
+ });
13146
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13147
+ var SmtpInboundMessageSchema = object({
13148
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13149
+ seq: number().int().positive(),
13150
+ messageId: string(),
13151
+ receivedAt: number().int().nonnegative(),
13152
+ from: string(),
13153
+ to: string(),
13154
+ subject: string(),
13155
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13156
+ attachments: array(SmtpInboundAttachmentSchema)
13157
+ });
13158
+ object({
13159
+ /** The newest seq issued so far (0 before any mail). */
13160
+ lastSeq: number().int().nonnegative(),
13161
+ /** The oldest seq still retained; messages below it are gone. */
13162
+ oldestSeq: number().int().nonnegative(),
13163
+ messages: array(object({
13164
+ key: string(),
13165
+ message: SmtpInboundMessageSchema
13166
+ }))
13167
+ });
12551
13168
  new Set([
12552
13169
  "track",
12553
13170
  "face",
@@ -12667,6 +13284,205 @@ var IntegrationInstanceRefSchema = object({
12667
13284
  ]),
12668
13285
  candidateStableId: string().optional()
12669
13286
  });
13287
+ /**
13288
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13289
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13290
+ *
13291
+ * Every instance is one record of the addon's own kind, keyed by the
13292
+ * `integrations` registry id. The records door does the secret work: a field the
13293
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13294
+ * deleting the row deletes the secret, and one row's value is never another's.
13295
+ *
13296
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13297
+ * token or the view the form was given KEEP the stored secret;
13298
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13299
+ */
13300
+ /** The most instances one addon lists (an integrations page, not a table). */
13301
+ var INSTANCES_LIST_MAX = 1e3;
13302
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13303
+ function topLevelSecretKeys(paths) {
13304
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13305
+ }
13306
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13307
+ function secretMapKeys(paths) {
13308
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13309
+ }
13310
+ /**
13311
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13312
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13313
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13314
+ */
13315
+ function secretMapPatch(value) {
13316
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13317
+ const out = {};
13318
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13319
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13320
+ return Object.keys(out).length > 0 ? out : null;
13321
+ }
13322
+ /** Does the kind's body schema carry an `enabled` field? */
13323
+ function declaresEnabled(kind) {
13324
+ const json = toJSONSchema(kind.schema, {
13325
+ unrepresentable: "any",
13326
+ io: "input"
13327
+ });
13328
+ const properties = Reflect.get(json, "properties");
13329
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13330
+ }
13331
+ var RecordIntegrationInstances = class {
13332
+ records;
13333
+ kind;
13334
+ hooks;
13335
+ handle;
13336
+ secretKeys;
13337
+ secretMaps;
13338
+ nameField;
13339
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13340
+ registryOwnsEnabled;
13341
+ constructor(records, kind, hooks) {
13342
+ this.records = records;
13343
+ this.kind = kind;
13344
+ this.hooks = hooks;
13345
+ this.handle = records.kind(kind);
13346
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13347
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13348
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13349
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13350
+ 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"`);
13351
+ }
13352
+ /**
13353
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13354
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13355
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13356
+ */
13357
+ async readResolved(id) {
13358
+ try {
13359
+ const record = await this.handle.getResolved(id);
13360
+ return record === null ? null : {
13361
+ id,
13362
+ body: record.data,
13363
+ enabled: record.data.enabled !== false,
13364
+ secrets: "resolved"
13365
+ };
13366
+ } catch (err) {
13367
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13368
+ const stored = await this.handle.get(id);
13369
+ if (stored === null) return null;
13370
+ return {
13371
+ id,
13372
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13373
+ enabled: stored.data.enabled !== false,
13374
+ secrets: "unavailable"
13375
+ };
13376
+ }
13377
+ }
13378
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13379
+ async getResolved(id) {
13380
+ return (await this.readResolved(id))?.body ?? null;
13381
+ }
13382
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13383
+ async listResolved() {
13384
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13385
+ const out = [];
13386
+ for (const record of page.records) {
13387
+ const read = await this.readResolved(record.id);
13388
+ if (read !== null) out.push(read);
13389
+ }
13390
+ return out;
13391
+ }
13392
+ viewOf(record) {
13393
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13394
+ const rawName = record.data[this.nameField];
13395
+ const kind = this.hooks.kindOf?.(record.data);
13396
+ const health = this.hooks.health?.(record.data, record.id);
13397
+ return {
13398
+ id: record.id,
13399
+ name: typeof rawName === "string" ? rawName : record.id,
13400
+ configured: this.hooks.configured(record.data),
13401
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13402
+ formValues: shown,
13403
+ ...kind !== void 0 ? { kind } : {},
13404
+ ...health?.status !== void 0 ? { status: health.status } : {},
13405
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13406
+ };
13407
+ }
13408
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13409
+ ...this.hooks.description,
13410
+ mintsId: true
13411
+ };
13412
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13413
+ listInstances = async () => {
13414
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13415
+ };
13416
+ getInstance = async ({ id }) => {
13417
+ const record = await this.handle.get(id);
13418
+ return record === null ? null : this.viewOf(record);
13419
+ };
13420
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13421
+ createInstance = async ({ id, name, kind }) => {
13422
+ const minted = id ?? this.hooks.mintId?.({
13423
+ name,
13424
+ ...kind !== void 0 ? { kind } : {}
13425
+ });
13426
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13427
+ await this.handle.put({
13428
+ id: minted,
13429
+ data: this.hooks.initialBody(name, kind)
13430
+ }, { ifRev: 0 });
13431
+ await this.hooks.onChanged?.(minted, "created");
13432
+ return { id: minted };
13433
+ };
13434
+ updateInstance = async ({ id, name, values }) => {
13435
+ const patch = {};
13436
+ for (const [key, value] of Object.entries(values)) {
13437
+ if (value === void 0) continue;
13438
+ if (this.secretMaps.has(key)) {
13439
+ const entries = secretMapPatch(value);
13440
+ if (entries !== null) patch[key] = entries;
13441
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13442
+ else if (isSecretClear(value)) patch[key] = null;
13443
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13444
+ }
13445
+ if (name !== void 0) patch[this.nameField] = name;
13446
+ await this.handle.patch(id, patch);
13447
+ await this.hooks.onChanged?.(id, "updated");
13448
+ return { ok: true };
13449
+ };
13450
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13451
+ setInstanceEnabled = async ({ id, enabled }) => {
13452
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13453
+ await this.handle.patch(id, { enabled });
13454
+ await this.hooks.onChanged?.(id, "enabled");
13455
+ return { ok: true };
13456
+ };
13457
+ /** The row and its keychain refs go together (the records door). */
13458
+ deleteInstance = async ({ id }) => {
13459
+ await this.handle.delete(id);
13460
+ await this.hooks.onChanged?.(id, "deleted");
13461
+ return { ok: true };
13462
+ };
13463
+ testInstance = async (input) => {
13464
+ if (this.hooks.test === void 0) return {
13465
+ success: false,
13466
+ status: "unsupported",
13467
+ error: "This integration declares no connection test"
13468
+ };
13469
+ return this.hooks.test(input);
13470
+ };
13471
+ /** The flat provider object to hand to `registerProvider`. */
13472
+ toProvider() {
13473
+ return {
13474
+ describe: this.describe,
13475
+ instanceFormSchema: this.instanceFormSchema,
13476
+ listInstances: this.listInstances,
13477
+ getInstance: this.getInstance,
13478
+ createInstance: this.createInstance,
13479
+ updateInstance: this.updateInstance,
13480
+ setInstanceEnabled: this.setInstanceEnabled,
13481
+ deleteInstance: this.deleteInstance,
13482
+ testInstance: this.testInstance
13483
+ };
13484
+ }
13485
+ };
12670
13486
  var EncodeProfileSchema = object({
12671
13487
  video: object({
12672
13488
  codec: _enum([
@@ -13702,7 +14518,13 @@ var MigrationLedgerEntrySchema = object({
13702
14518
  * joined by the engine from the catalogues it knows this boot. Absent when no
13703
14519
  * catalogue lists the id (a newer database).
13704
14520
  */
13705
- meta: MigrationMetaSchema.optional()
14521
+ meta: MigrationMetaSchema.optional(),
14522
+ /**
14523
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14524
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14525
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14526
+ */
14527
+ ownerNotLoaded: boolean().optional()
13706
14528
  });
13707
14529
  /** A catalogued migration that is not applied, with where it stands. */
13708
14530
  var MigrationPendingEntrySchema = object({
@@ -13736,6 +14558,149 @@ var MigrationOwnerStepSchema = object({
13736
14558
  state: union([literal("pending"), MigrationStatusSchema])
13737
14559
  });
13738
14560
  /**
14561
+ * Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
14562
+ * kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
14563
+ * the integration id, the devices stamped with it and the automations that name it do not change.
14564
+ *
14565
+ * Order, so a crash or a locked keychain is safe at every point:
14566
+ *
14567
+ * 1. Phase A, read only: every row of every integration of the addon is read with its secrets
14568
+ * resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
14569
+ * anything is written; `block-owner` then holds the addon.
14570
+ * 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
14571
+ * `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
14572
+ * and every secret leaf must equal what the rows held, else the record is deleted and the pass
14573
+ * throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
14574
+ * a warning names the path. Only then are the rows (and their keychain values) cleared.
14575
+ *
14576
+ * Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
14577
+ * a no-op. A default integration with no rows still gets its record (an empty body, D985).
14578
+ * Never logs or throws a secret value; messages name integration ids and secret PATHS only.
14579
+ */
14580
+ function secretLeavesOf(body, spec) {
14581
+ const out = /* @__PURE__ */ new Map();
14582
+ 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);
14583
+ return out;
14584
+ }
14585
+ function why(err) {
14586
+ return err instanceof Error ? err.message : String(err);
14587
+ }
14588
+ async function moveIntegrationRowsToRecords(ports, spec) {
14589
+ const integrations = await ports.listIntegrations(spec.addonId);
14590
+ if (integrations.length === 0) return {
14591
+ outcome: "already-done",
14592
+ summary: { integrations: 0 }
14593
+ };
14594
+ const rowsOf = /* @__PURE__ */ new Map();
14595
+ for (const integration of integrations) try {
14596
+ rowsOf.set(integration.id, await ports.readRows(integration.id));
14597
+ } catch (err) {
14598
+ throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
14599
+ }
14600
+ let created = 0;
14601
+ let cleared = 0;
14602
+ let kept = 0;
14603
+ for (const integration of integrations) {
14604
+ const values = rowsOf.get(integration.id) ?? {};
14605
+ const hasRows = Object.keys(values).length > 0;
14606
+ const body = spec.bodyFrom(values, ports, integration);
14607
+ const put = await ports.putRecord(integration.id, body);
14608
+ if (put === "created") created += 1;
14609
+ if (!hasRows) continue;
14610
+ const wanted = secretLeavesOf(body, spec);
14611
+ const back = await ports.readRecord(integration.id);
14612
+ if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
14613
+ const held = secretLeavesOf(back, spec);
14614
+ if (put === "created") {
14615
+ const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
14616
+ if (wrong.length > 0) {
14617
+ await ports.deleteRecord(integration.id);
14618
+ 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`);
14619
+ }
14620
+ } else {
14621
+ const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
14622
+ if (missing.length > 0) {
14623
+ ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
14624
+ kept += 1;
14625
+ continue;
14626
+ }
14627
+ }
14628
+ await ports.clearRows(integration.id);
14629
+ cleared += 1;
14630
+ }
14631
+ return created === 0 && cleared === 0 ? {
14632
+ outcome: "already-done",
14633
+ summary: {
14634
+ integrations: integrations.length,
14635
+ kept
14636
+ }
14637
+ } : {
14638
+ outcome: "done",
14639
+ summary: {
14640
+ integrations: integrations.length,
14641
+ created,
14642
+ cleared,
14643
+ kept
14644
+ }
14645
+ };
14646
+ }
14647
+ /**
14648
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14649
+ * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14650
+ * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14651
+ * the new record through the addon's own records handle. The hub deletes the rows and their
14652
+ * keychain values; the addon never touches the registry.
14653
+ */
14654
+ function integrationRowsPortsOverApi(deps) {
14655
+ return {
14656
+ warn: (message) => deps.log.warn(message),
14657
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14658
+ id: i.id,
14659
+ name: i.name
14660
+ })),
14661
+ readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14662
+ putRecord: async (id, body) => {
14663
+ if (await deps.records.get(id) !== null) return "exists";
14664
+ try {
14665
+ await deps.records.put({
14666
+ id,
14667
+ data: body
14668
+ }, { ifRev: 0 });
14669
+ } catch (err) {
14670
+ if (err instanceof RevConflictError) return "exists";
14671
+ throw err;
14672
+ }
14673
+ return "created";
14674
+ },
14675
+ readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
14676
+ deleteRecord: async (id) => {
14677
+ await deps.records.delete(id);
14678
+ },
14679
+ clearRows: async (integrationId) => {
14680
+ await deps.api.clearSettings({ id: integrationId });
14681
+ }
14682
+ };
14683
+ }
14684
+ function defaultIntegrationApiOver(api) {
14685
+ return {
14686
+ list: () => api.integrations.list.query(),
14687
+ getSettings: (input) => api.integrations.getSettings.query(input),
14688
+ clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14689
+ listDevices: (addonId) => api.deviceManager.listAll.query({
14690
+ addonId,
14691
+ projection: "slim"
14692
+ }),
14693
+ setDeviceIntegrationId: async (deviceId, integrationId) => {
14694
+ await api.deviceManager.setIntegrationId.mutate({
14695
+ deviceId,
14696
+ integrationId
14697
+ });
14698
+ },
14699
+ readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
14700
+ readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
14701
+ };
14702
+ }
14703
+ /**
13739
14704
  * The trainer contract — what the hub hands the cloud trainer and what it
13740
14705
  * gets back (cloud training, D805+).
13741
14706
  *
@@ -18820,24 +19785,52 @@ var APPLIANCE_CAP_VOCABULARY = {
18820
19785
  args: { on: false }
18821
19786
  },
18822
19787
  {
18823
- id: "petFountain.intervalOn",
18824
- method: "setPumpInterval",
19788
+ id: "petFountain.resetDailyConsumption",
19789
+ method: "resetDailyConsumption",
19790
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19791
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19792
+ icon: "rotate-ccw"
19793
+ }
19794
+ ],
19795
+ state: [{
19796
+ kind: "boolean",
19797
+ field: "empty",
19798
+ label: t("Tank empty", "Serbatoio vuoto"),
19799
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19800
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19801
+ icon: "droplet"
19802
+ }, {
19803
+ kind: "numeric",
19804
+ field: "waterLevel",
19805
+ quantity: t("the water level", "il livello dell’acqua"),
19806
+ label: t("Water level", "Livello dell’acqua"),
19807
+ unit: "%",
19808
+ icon: "droplets"
19809
+ }]
19810
+ },
19811
+ "pump-cycle": {
19812
+ scope: "device",
19813
+ noun: t("Pump cycle", "Ciclo della pompa"),
19814
+ actions: [
19815
+ {
19816
+ id: "pumpCycle.on",
19817
+ method: "setEnabled",
18825
19818
  label: t("Pump cycle on", "Ciclo pompa attivo"),
18826
19819
  phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
18827
19820
  icon: "timer",
18828
19821
  args: { on: true }
18829
19822
  },
18830
19823
  {
18831
- id: "petFountain.intervalOff",
18832
- method: "setPumpInterval",
19824
+ id: "pumpCycle.off",
19825
+ method: "setEnabled",
18833
19826
  label: t("Pump cycle off", "Ciclo pompa spento"),
18834
19827
  phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
18835
19828
  icon: "timer-off",
18836
19829
  args: { on: false }
18837
19830
  },
18838
19831
  {
18839
- id: "petFountain.pumpOnMinutes",
18840
- method: "setPumpOnMinutes",
19832
+ id: "pumpCycle.onMinutes",
19833
+ method: "setOnMinutes",
18841
19834
  label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
18842
19835
  phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
18843
19836
  icon: "timer",
@@ -18853,8 +19846,8 @@ var APPLIANCE_CAP_VOCABULARY = {
18853
19846
  }]
18854
19847
  },
18855
19848
  {
18856
- id: "petFountain.pumpOffMinutes",
18857
- method: "setPumpOffMinutes",
19849
+ id: "pumpCycle.offMinutes",
19850
+ method: "setOffMinutes",
18858
19851
  label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
18859
19852
  phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
18860
19853
  icon: "timer-off",
@@ -18868,35 +19861,63 @@ var APPLIANCE_CAP_VOCABULARY = {
18868
19861
  unit: "min"
18869
19862
  }
18870
19863
  }]
18871
- },
18872
- {
18873
- id: "petFountain.resetDailyConsumption",
18874
- method: "resetDailyConsumption",
18875
- label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
18876
- phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
18877
- icon: "rotate-ccw"
18878
19864
  }
18879
19865
  ],
18880
19866
  state: [{
18881
19867
  kind: "boolean",
18882
- field: "empty",
18883
- label: t("Tank empty", "Serbatoio vuoto"),
18884
- whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
18885
- whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
18886
- icon: "droplet"
18887
- }, {
18888
- kind: "numeric",
18889
- field: "waterLevel",
18890
- quantity: t("the water level", "il livello dell’acqua"),
18891
- label: t("Water level", "Livello dell’acqua"),
18892
- unit: "%",
18893
- icon: "droplets"
19868
+ field: "enabled",
19869
+ label: t("Pump cycle", "Ciclo pompa"),
19870
+ whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
19871
+ whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
19872
+ icon: "timer"
18894
19873
  }]
18895
19874
  },
18896
19875
  "media-player": {
18897
19876
  scope: "device",
18898
19877
  noun: t("Media player", "Lettore multimediale"),
18899
19878
  actions: [
19879
+ {
19880
+ id: "media.turnOn",
19881
+ method: "turnOn",
19882
+ label: t("Turn on", "Accendi"),
19883
+ phrase: t("Turn on {device}", "Accendi {device}"),
19884
+ icon: "power"
19885
+ },
19886
+ {
19887
+ id: "media.turnOff",
19888
+ method: "turnOff",
19889
+ label: t("Turn off", "Spegni"),
19890
+ phrase: t("Turn off {device}", "Spegni {device}"),
19891
+ icon: "power"
19892
+ },
19893
+ {
19894
+ id: "media.volumeUp",
19895
+ method: "volumeUp",
19896
+ label: t("Volume up", "Alza il volume"),
19897
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
19898
+ icon: "volume-2"
19899
+ },
19900
+ {
19901
+ id: "media.volumeDown",
19902
+ method: "volumeDown",
19903
+ label: t("Volume down", "Abbassa il volume"),
19904
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
19905
+ icon: "volume-2"
19906
+ },
19907
+ {
19908
+ id: "media.channelUp",
19909
+ method: "channelUp",
19910
+ label: t("Channel up", "Canale successivo"),
19911
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
19912
+ icon: "skip-forward"
19913
+ },
19914
+ {
19915
+ id: "media.channelDown",
19916
+ method: "channelDown",
19917
+ label: t("Channel down", "Canale precedente"),
19918
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
19919
+ icon: "skip-back"
19920
+ },
18900
19921
  {
18901
19922
  id: "media.play",
18902
19923
  method: "play",
@@ -20783,6 +21804,7 @@ var SYSTEM_CAP_VOCABULARY = {
20783
21804
  "audio-analyzer": system(PIPELINE),
20784
21805
  "audio-codec": system(INTERNAL),
20785
21806
  "auth-provider": system(IDENTITY),
21807
+ "board-key-source": system(INTERNAL),
20786
21808
  backup: system("system backups — run and restored from the Backups page"),
20787
21809
  broker: system("message-broker connections — configured, not actuated"),
20788
21810
  "calendar-source": system(INTERNAL),
@@ -45692,6 +46714,49 @@ var binaryCapability = {
45692
46714
  volatileStateFields: ["lastChangedAt"]
45693
46715
  };
45694
46716
  /**
46717
+ * Why a board key could not be read. Typed so a caller reports the cause and
46718
+ * never treats "could not read" as "has no key" (D49).
46719
+ */
46720
+ var BoardKeyFailureReasonSchema = _enum([
46721
+ "unknown-source",
46722
+ "not-admin",
46723
+ "no-builder",
46724
+ "no-ingress-url",
46725
+ "unreachable",
46726
+ "unknown-board",
46727
+ "no-encryption-key",
46728
+ "unreadable-config"
46729
+ ]);
46730
+ var BoardKeySourceSchema = object({
46731
+ sourceId: string(),
46732
+ name: string()
46733
+ });
46734
+ var BoardKeyBoardSchema = object({
46735
+ /** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
46736
+ configuration: string(),
46737
+ /** The node name the builder gives the board. */
46738
+ name: string(),
46739
+ /** The address the builder knows the board by, when it has one. */
46740
+ address: string().nullable()
46741
+ });
46742
+ var BoardKeyFailureSchema = object({
46743
+ ok: literal(false),
46744
+ reason: BoardKeyFailureReasonSchema,
46745
+ detail: string().optional()
46746
+ });
46747
+ var ListBoardsResultSchema = discriminatedUnion("ok", [object({
46748
+ ok: literal(true),
46749
+ boards: array(BoardKeyBoardSchema).readonly()
46750
+ }), BoardKeyFailureSchema]);
46751
+ var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
46752
+ ok: literal(true),
46753
+ key: string()
46754
+ }), BoardKeyFailureSchema]);
46755
+ method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
46756
+ sourceId: string().min(1),
46757
+ configuration: string().min(1)
46758
+ }), GetBoardKeyResultSchema, { auth: "admin" });
46759
+ /**
45695
46760
  * Dimmable-light brightness control. Co-exists with `switch` on the
45696
46761
  * same device — the switch toggles on/off, this cap sets the level
45697
46762
  * applied when the light is on. Drivers map their per-vendor dim
@@ -48381,6 +49446,244 @@ var controlCapability = {
48381
49446
  */
48382
49447
  history: "none"
48383
49448
  };
49449
+ /**
49450
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49451
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49452
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49453
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49454
+ *
49455
+ * The cap declares a wide method surface; not every backend supports
49456
+ * every method. Drivers signal sub-capability via the
49457
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49458
+ * corresponding method) so the UI gates buttons without a slice
49459
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49460
+ */
49461
+ var MediaPlayerStateSchema = _enum([
49462
+ "off",
49463
+ "on",
49464
+ "idle",
49465
+ "playing",
49466
+ "paused",
49467
+ "buffering",
49468
+ "standby"
49469
+ ]);
49470
+ var MediaPlayerRepeatSchema = _enum([
49471
+ "off",
49472
+ "all",
49473
+ "one"
49474
+ ]);
49475
+ var MediaInfoSchema = object({
49476
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49477
+ * `podcast`, …). */
49478
+ type: string(),
49479
+ /** Human-readable title. */
49480
+ title: string(),
49481
+ /** Optional artist / channel / station label. */
49482
+ artist: string().optional(),
49483
+ /** Optional album / season / show name. */
49484
+ album: string().optional(),
49485
+ /** Optional cover-art / thumbnail URL. */
49486
+ imageUrl: string().optional()
49487
+ });
49488
+ /**
49489
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49490
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49491
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49492
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49493
+ * guess, never as a reading.
49494
+ */
49495
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49496
+ /** The fields of a media-player status a driver can only guess. */
49497
+ var MediaPlayerAssumedFieldSchema = _enum([
49498
+ "state",
49499
+ "volumeLevel",
49500
+ "isMuted",
49501
+ "source"
49502
+ ]);
49503
+ var MediaPlayerStatusSchema = object({
49504
+ /** Playback lifecycle state. */
49505
+ state: MediaPlayerStateSchema,
49506
+ /**
49507
+ * Whether the state fields are readings or the driver's own bookkeeping.
49508
+ * A slice written before this field existed parses as `reported`.
49509
+ */
49510
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49511
+ /**
49512
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49513
+ * this is not empty; a field not listed here is a reading even then (power
49514
+ * from a running-state source, mute guessed from the last send).
49515
+ */
49516
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49517
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49518
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49519
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
49520
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49521
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49522
+ isMuted: boolean().nullable(),
49523
+ /** Active source / input. Empty when no source surface. */
49524
+ source: string(),
49525
+ /** Selectable sources. Empty when no source surface.
49526
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49527
+ availableSources: array(string()),
49528
+ /** Currently-playing media info. Null when nothing is playing. */
49529
+ currentMedia: MediaInfoSchema.nullable(),
49530
+ /** Current playback position in ms. Null when not seekable or
49531
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
49532
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49533
+ /** Total duration of the current media in ms. Null when unknown
49534
+ * (live stream) or nothing is playing. */
49535
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49536
+ /** Shuffle toggle. Null when no shuffle surface.
49537
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
49538
+ shuffle: boolean().nullable(),
49539
+ /** Repeat mode. Null when no repeat surface.
49540
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
49541
+ repeat: MediaPlayerRepeatSchema.nullable(),
49542
+ /** Ms epoch when the slice was last updated. */
49543
+ lastChangedAt: number()
49544
+ });
49545
+ var stepInput = object({
49546
+ deviceId: number().int().nonnegative(),
49547
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
49548
+ steps: number().int().min(1).max(50).default(1)
49549
+ });
49550
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
49551
+ var mediaPlayerCapability = {
49552
+ name: "media-player",
49553
+ scope: "device",
49554
+ deviceNative: true,
49555
+ mode: "singleton",
49556
+ deviceTypes: [DeviceType.MediaPlayer],
49557
+ methods: {
49558
+ turnOn: method(deviceOnly, _void(), {
49559
+ kind: "mutation",
49560
+ auth: "protected"
49561
+ }),
49562
+ turnOff: method(deviceOnly, _void(), {
49563
+ kind: "mutation",
49564
+ auth: "protected"
49565
+ }),
49566
+ volumeUp: method(stepInput, _void(), {
49567
+ kind: "mutation",
49568
+ auth: "protected"
49569
+ }),
49570
+ volumeDown: method(stepInput, _void(), {
49571
+ kind: "mutation",
49572
+ auth: "protected"
49573
+ }),
49574
+ channelUp: method(stepInput, _void(), {
49575
+ kind: "mutation",
49576
+ auth: "protected"
49577
+ }),
49578
+ channelDown: method(stepInput, _void(), {
49579
+ kind: "mutation",
49580
+ auth: "protected"
49581
+ }),
49582
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49583
+ kind: "mutation",
49584
+ auth: "protected"
49585
+ }),
49586
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49587
+ kind: "mutation",
49588
+ auth: "protected"
49589
+ }),
49590
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49591
+ kind: "mutation",
49592
+ auth: "protected"
49593
+ }),
49594
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49595
+ kind: "mutation",
49596
+ auth: "protected"
49597
+ }),
49598
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
49599
+ kind: "mutation",
49600
+ auth: "protected"
49601
+ }),
49602
+ seek: method(object({
49603
+ deviceId: number().int().nonnegative(),
49604
+ position: number().int().nonnegative()
49605
+ }), _void(), {
49606
+ kind: "mutation",
49607
+ auth: "protected"
49608
+ }),
49609
+ setVolume: method(object({
49610
+ deviceId: number().int().nonnegative(),
49611
+ volumeLevel: number().min(0).max(100)
49612
+ }), _void(), {
49613
+ kind: "mutation",
49614
+ auth: "protected"
49615
+ }),
49616
+ setMute: method(object({
49617
+ deviceId: number().int().nonnegative(),
49618
+ muted: boolean()
49619
+ }), _void(), {
49620
+ kind: "mutation",
49621
+ auth: "protected"
49622
+ }),
49623
+ setShuffle: method(object({
49624
+ deviceId: number().int().nonnegative(),
49625
+ shuffle: boolean()
49626
+ }), _void(), {
49627
+ kind: "mutation",
49628
+ auth: "protected"
49629
+ }),
49630
+ setRepeat: method(object({
49631
+ deviceId: number().int().nonnegative(),
49632
+ repeat: MediaPlayerRepeatSchema
49633
+ }), _void(), {
49634
+ kind: "mutation",
49635
+ auth: "protected"
49636
+ }),
49637
+ selectSource: method(object({
49638
+ deviceId: number().int().nonnegative(),
49639
+ source: string().min(1)
49640
+ }), _void(), {
49641
+ kind: "mutation",
49642
+ auth: "protected"
49643
+ }),
49644
+ playMedia: method(object({
49645
+ deviceId: number().int().nonnegative(),
49646
+ /** Media identifier / URL. */
49647
+ mediaId: string().min(1),
49648
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
49649
+ * passes it through to the upstream service. */
49650
+ mediaType: string().min(1)
49651
+ }), _void(), {
49652
+ kind: "mutation",
49653
+ auth: "protected"
49654
+ })
49655
+ },
49656
+ status: {
49657
+ schema: MediaPlayerStatusSchema,
49658
+ kind: "push"
49659
+ },
49660
+ /**
49661
+ * Runtime-state slice — mirrored by the kernel. UI players read the
49662
+ * full slice for live now-playing, volume, and progress updates
49663
+ * without polling.
49664
+ */
49665
+ runtimeState: MediaPlayerStatusSchema,
49666
+ /**
49667
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
49668
+ *
49669
+ * See `RuntimeStateDurability`. Enforced by
49670
+ * `scripts/check-runtime-state-durability.ts`.
49671
+ */
49672
+ durability: "session",
49673
+ /**
49674
+ * History (D850): volume (a level an operator or an automation sets; the
49675
+ * default 10 s floor folds a slider drag into its end value) and mute.
49676
+ * `position` is not recorded: it moves continuously while playing.
49677
+ */
49678
+ history: { fields: {
49679
+ volumeLevel: {
49680
+ kind: "gauge",
49681
+ unit: "%",
49682
+ deadband: 1
49683
+ },
49684
+ isMuted: { kind: "boolean" }
49685
+ } }
49686
+ };
48384
49687
  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;
48385
49688
  var NUMBER = {
48386
49689
  kind: "fixed",
@@ -48828,117 +50131,6 @@ function actuator(input) {
48828
50131
  };
48829
50132
  }
48830
50133
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
48831
- var PowerSourceRefusalReasonSchema = _enum([
48832
- "no-power-source",
48833
- "switch-missing",
48834
- "switch-offline",
48835
- "switch-unknown",
48836
- "restart-in-progress",
48837
- "switch-call-failed"
48838
- ]);
48839
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
48840
- accepted: literal(false),
48841
- reason: PowerSourceRefusalReasonSchema,
48842
- /** One line for the operator, naming the switch. */
48843
- message: string()
48844
- })]);
48845
- var PowerSourceInfoSchema = object({
48846
- /** False when the device has no power source linked; the rest is then empty. */
48847
- linked: boolean(),
48848
- switchDeviceId: number().nullable(),
48849
- switchName: string().nullable(),
48850
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
48851
- switchOn: boolean().nullable(),
48852
- switchOnline: boolean().nullable(),
48853
- restartInProgress: boolean(),
48854
- offSeconds: number().int()
48855
- });
48856
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
48857
- var PowerSourceStateSchema = object({
48858
- /** Null while the switch has not reported: unknown is never `false` (D393). */
48859
- switchOn: boolean().nullable(),
48860
- offSeconds: number().int(),
48861
- lastFetchedAt: number()
48862
- });
48863
- var POWER_SOURCE_DEVICE_TYPES = [
48864
- DeviceType.Gateway,
48865
- DeviceType.Hub,
48866
- DeviceType.Camera,
48867
- DeviceType.Sensor,
48868
- DeviceType.MediaPlayer,
48869
- DeviceType.Vacuum,
48870
- DeviceType.Generic,
48871
- DeviceType.Washer,
48872
- DeviceType.Dryer,
48873
- DeviceType.Dishwasher,
48874
- DeviceType.PetFountain,
48875
- DeviceType.Fan,
48876
- DeviceType.Humidifier,
48877
- DeviceType.Climate,
48878
- DeviceType.WaterHeater
48879
- ];
48880
- var powerSourceCustomize = {
48881
- version: 1,
48882
- targets: [{
48883
- id: "switch",
48884
- cardinality: "scalar",
48885
- vocabulary: {
48886
- cap: "power-source",
48887
- stateField: "switchOn"
48888
- },
48889
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
48890
- apply: "add",
48891
- path: "switchOn",
48892
- sources: [{
48893
- kind: "switch",
48894
- transforms: [{ kind: "identity" }]
48895
- }],
48896
- companions: [{
48897
- kind: "constant",
48898
- path: "offSeconds",
48899
- value: 10,
48900
- when: "add"
48901
- }, {
48902
- kind: "clock",
48903
- path: "lastFetchedAt",
48904
- when: "always"
48905
- }],
48906
- sourceReport: { status: "assignment-status" }
48907
- }]
48908
- };
48909
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
48910
- var powerSourceCapability = {
48911
- name: "power-source",
48912
- scope: "device",
48913
- mode: "singleton",
48914
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
48915
- methods: {
48916
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
48917
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48918
- kind: "mutation",
48919
- auth: "admin"
48920
- }),
48921
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48922
- kind: "mutation",
48923
- auth: "admin"
48924
- }),
48925
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
48926
- kind: "mutation",
48927
- auth: "admin"
48928
- })
48929
- },
48930
- runtimeState: PowerSourceStateSchema,
48931
- customize: powerSourceCustomize,
48932
- /**
48933
- * Runtime-state durability: **session** — mirrored from the linked switch on
48934
- * its next report; a restored value would outlive the reading it names.
48935
- *
48936
- * See `RuntimeStateDurability`. Enforced by
48937
- * `scripts/check-runtime-state-durability.ts`.
48938
- */
48939
- durability: "session",
48940
- history: "none"
48941
- };
48942
50134
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
48943
50135
  DeviceType.Camera;
48944
50136
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -48966,6 +50158,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
48966
50158
  }).optional()
48967
50159
  })) });
48968
50160
  /**
50161
+ * A saved Customize assignment (D936). The schema is the type: callers use
50162
+ * `z.infer`, they do not restate the shape.
50163
+ */
50164
+ var OverrideSourceSchema = object({
50165
+ addonId: string().min(1),
50166
+ stableId: string().min(1),
50167
+ cap: string().min(1),
50168
+ fieldPath: string().min(1)
50169
+ });
50170
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50171
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50172
+ var ThresholdTransformDraftSchema = object({
50173
+ kind: literal("threshold"),
50174
+ comparison: _enum(["gte", "lte"]),
50175
+ threshold: number(),
50176
+ startDwellMs: number().nonnegative(),
50177
+ stopDwellMs: number().nonnegative()
50178
+ });
50179
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50180
+ IdentityTransformDraftSchema,
50181
+ InvertTransformDraftSchema,
50182
+ ThresholdTransformDraftSchema,
50183
+ object({
50184
+ kind: literal("linear"),
50185
+ scale: number(),
50186
+ offset: number(),
50187
+ clampMin: number().optional(),
50188
+ clampMax: number().optional()
50189
+ }),
50190
+ object({
50191
+ kind: literal("enum-map"),
50192
+ mapping: record(string(), string().nullable())
50193
+ }),
50194
+ object({ kind: literal("number") })
50195
+ ]);
50196
+ var ScalarOverrideDraftSchema = object({
50197
+ kind: literal("scalar"),
50198
+ cap: string().min(1),
50199
+ targetId: string().min(1),
50200
+ source: OverrideSourceSchema,
50201
+ transform: CustomizeTransformDraftSchema
50202
+ });
50203
+ var KeyedArrayOverrideDraftSchema = object({
50204
+ kind: literal("keyedArray"),
50205
+ cap: string().min(1),
50206
+ targetId: string().min(1),
50207
+ key: string().min(1),
50208
+ label: string().min(1),
50209
+ fieldPath: string().min(1),
50210
+ source: OverrideSourceSchema,
50211
+ transform: CustomizeTransformDraftSchema
50212
+ });
50213
+ var PulseOverrideDraftSchema = object({
50214
+ kind: literal("pulse"),
50215
+ cap: string().min(1),
50216
+ targetId: string().min(1),
50217
+ enabled: boolean(),
50218
+ debounceMs: number().nonnegative(),
50219
+ edges: array(_enum([
50220
+ "rising",
50221
+ "button",
50222
+ "native-event"
50223
+ ])).min(1).optional(),
50224
+ sources: array(OverrideSourceSchema).min(1)
50225
+ });
50226
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50227
+ "in-addition",
50228
+ "confirm",
50229
+ "replace"
50230
+ ]).optional() });
50231
+ discriminatedUnion("kind", [
50232
+ ScalarOverrideDraftSchema,
50233
+ KeyedArrayOverrideDraftSchema,
50234
+ PulseOverrideDraftSchema,
50235
+ object({
50236
+ kind: literal("span"),
50237
+ cap: string().min(1),
50238
+ targetId: string().min(1),
50239
+ timeoutMs: number().nonnegative(),
50240
+ cooldownMs: number().nonnegative().optional(),
50241
+ sources: array(SpanSourceDraftSchema)
50242
+ })
50243
+ ]);
50244
+ object({
50245
+ targetId: string().min(1),
50246
+ configured: boolean(),
50247
+ source: _enum(["native", "assigned"]),
50248
+ state: _enum([
50249
+ "active",
50250
+ "stale",
50251
+ "released",
50252
+ "unavailable"
50253
+ ]),
50254
+ reason: string().nullable()
50255
+ });
50256
+ /**
50257
+ * What a running-threshold proposal says (planning#405). The schema is the
50258
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50259
+ *
50260
+ * A proposal is read-only advice: it carries the numbers it was derived from
50261
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50262
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50263
+ */
50264
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50265
+ var RunningProposalConfidenceSchema = _enum([
50266
+ "high",
50267
+ "medium",
50268
+ "low"
50269
+ ]);
50270
+ var RunningProposalInsufficientReasonSchema = _enum([
50271
+ "no-history",
50272
+ "span-too-short",
50273
+ "truncated",
50274
+ "no-idle-floor",
50275
+ "no-runs",
50276
+ "no-separation"
50277
+ ]);
50278
+ var RunningProposalReplaySchema = object({
50279
+ /** Intervals the real dwell expression held true over the window. */
50280
+ intervals: number().int().nonnegative(),
50281
+ /** Intervals outside every run, or a second interval inside one. */
50282
+ extra: number().int().nonnegative(),
50283
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50284
+ missed: number().int().nonnegative()
50285
+ });
50286
+ var RunningProposalEvidenceSchema = object({
50287
+ tier: RunningProposalTierSchema,
50288
+ spanMs: number().nonnegative(),
50289
+ from: number(),
50290
+ to: number(),
50291
+ points: number().int().nonnegative(),
50292
+ runsFound: number().int().nonnegative(),
50293
+ pausesFound: number().int().nonnegative(),
50294
+ longestPauseMs: number().nonnegative(),
50295
+ idleFloorW: number(),
50296
+ noiseCeilingW: number(),
50297
+ plateauW: number(),
50298
+ replay: RunningProposalReplaySchema
50299
+ });
50300
+ var RunningProposalInsufficientEvidenceSchema = object({
50301
+ spanMs: number().nonnegative(),
50302
+ points: number().int().nonnegative(),
50303
+ runsFound: number().int().nonnegative()
50304
+ });
50305
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50306
+ state: literal("proposal"),
50307
+ transform: ThresholdTransformDraftSchema,
50308
+ evidence: RunningProposalEvidenceSchema,
50309
+ confidence: RunningProposalConfidenceSchema,
50310
+ /** Why the confidence is not higher; empty when nothing held it back. */
50311
+ notes: array(string())
50312
+ }), object({
50313
+ state: literal("insufficient"),
50314
+ reason: RunningProposalInsufficientReasonSchema,
50315
+ evidence: RunningProposalInsufficientEvidenceSchema,
50316
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50317
+ fallback: ThresholdTransformDraftSchema
50318
+ })]);
50319
+ /**
48969
50320
  * The report of a one-shot "move this owner's plaintext secrets into the
48970
50321
  * keychain" run (D878). COUNTS only - never a value, never a ref.
48971
50322
  *
@@ -49255,6 +50606,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
49255
50606
  kind: "mutation",
49256
50607
  auth: "admin",
49257
50608
  access: "view"
50609
+ }), method(object({
50610
+ deviceId: number().int(),
50611
+ cap: string().min(1),
50612
+ fieldPath: string().min(1),
50613
+ deviceType: _enum(DeviceType).optional()
50614
+ }), RunningThresholdProposalSchema, {
50615
+ auth: "admin",
50616
+ access: "view"
49258
50617
  }), method(object({
49259
50618
  blockId: string().min(1),
49260
50619
  trace: AutomationRunTraceSchema.optional(),
@@ -50231,7 +51590,27 @@ method(object({
50231
51590
  * exposes only the COMMON surface: link state, supported device
50232
51591
  * kinds (so the UI can filter the device picker), and per-device
50233
51592
  * expose/unexpose.
50234
- */
51593
+ *
51594
+ * INSTANCES. One export addon can serve several independent targets (a
51595
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
51596
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
51597
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
51598
+ * default instance's); present = that instance; an id the provider does not
51599
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
51600
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
51601
+ * change always names the target it changes. A single-instance provider
51602
+ * accepts an absent id on a read or its own id, and refuses any other.
51603
+ */
51604
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
51605
+ var InstanceIdSchema$1 = string().min(1);
51606
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
51607
+ /** One independent export target of an addon. */
51608
+ var ExportInstanceSchema = object({
51609
+ instanceId: InstanceIdSchema$1,
51610
+ name: string(),
51611
+ enabled: boolean(),
51612
+ kind: string().optional()
51613
+ });
50235
51614
  var LinkStateSchema = _enum([
50236
51615
  "unlinked",
50237
51616
  "linked",
@@ -50282,10 +51661,14 @@ var ExposedDeviceSchema = object({
50282
51661
  capabilities: array(string()).optional()
50283
51662
  });
50284
51663
  var ExposeInputSchema = object({
51664
+ instanceId: InstanceIdSchema$1,
50285
51665
  deviceId: string(),
50286
51666
  capabilities: array(string()).optional()
50287
51667
  });
50288
- var UnexposeInputSchema = object({ deviceId: string() });
51668
+ var UnexposeInputSchema = object({
51669
+ instanceId: InstanceIdSchema$1,
51670
+ deviceId: string()
51671
+ });
50289
51672
  /**
50290
51673
  * What an exporter may expose besides devices (D968). An action and a
50291
51674
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -50314,15 +51697,21 @@ var ExposedEntitySchema = object({
50314
51697
  entity: ExportEntityRefSchema,
50315
51698
  exposedAs: string().optional()
50316
51699
  });
50317
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50318
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
50319
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
51700
+ var ExposeEntityInputSchema = object({
51701
+ instanceId: InstanceIdSchema$1,
51702
+ entity: ExportEntityRefSchema
51703
+ });
51704
+ var UnexposeEntityInputSchema = object({
51705
+ instanceId: InstanceIdSchema$1,
51706
+ entity: ExportEntityRefSchema
51707
+ });
51708
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
50320
51709
  kind: "mutation",
50321
51710
  auth: "admin"
50322
51711
  }), method(UnexposeInputSchema, _void(), {
50323
51712
  kind: "mutation",
50324
51713
  auth: "admin"
50325
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
51714
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
50326
51715
  kind: "mutation",
50327
51716
  auth: "admin"
50328
51717
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -52324,7 +53713,7 @@ var DishwasherStatusSchema = object({
52324
53713
  /** Ms epoch of the last value received from the appliance; 0 = never. */
52325
53714
  lastFetchedAt: number()
52326
53715
  });
52327
- var DeviceIdSchema$1 = number().int().nonnegative();
53716
+ var DeviceIdSchema$2 = number().int().nonnegative();
52328
53717
  /** A value a program option or setting can take. */
52329
53718
  var DishwasherValueSchema = union([
52330
53719
  boolean(),
@@ -52399,77 +53788,77 @@ var dishwasherCapability = {
52399
53788
  * option feature keys; each is checked against the program's constraints.
52400
53789
  */
52401
53790
  start: method(object({
52402
- deviceId: DeviceIdSchema$1,
53791
+ deviceId: DeviceIdSchema$2,
52403
53792
  program: string().min(1).optional(),
52404
53793
  options: record(string(), DishwasherValueSchema).optional()
52405
53794
  }), _void(), {
52406
53795
  kind: "mutation",
52407
53796
  auth: "protected"
52408
53797
  }),
52409
- pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53798
+ pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52410
53799
  kind: "mutation",
52411
53800
  auth: "protected"
52412
53801
  }),
52413
- resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53802
+ resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52414
53803
  kind: "mutation",
52415
53804
  auth: "protected"
52416
53805
  }),
52417
- stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53806
+ stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52418
53807
  kind: "mutation",
52419
53808
  auth: "protected"
52420
53809
  }),
52421
53810
  /** Select (not start) a program; refused when the appliance does not offer it. */
52422
53811
  setProgram: method(object({
52423
- deviceId: DeviceIdSchema$1,
53812
+ deviceId: DeviceIdSchema$2,
52424
53813
  program: string().min(1)
52425
53814
  }), _void(), {
52426
53815
  kind: "mutation",
52427
53816
  auth: "protected"
52428
53817
  }),
52429
53818
  setPower: method(object({
52430
- deviceId: DeviceIdSchema$1,
53819
+ deviceId: DeviceIdSchema$2,
52431
53820
  on: boolean()
52432
53821
  }), _void(), {
52433
53822
  kind: "mutation",
52434
53823
  auth: "protected"
52435
53824
  }),
52436
53825
  setHalfLoad: method(object({
52437
- deviceId: DeviceIdSchema$1,
53826
+ deviceId: DeviceIdSchema$2,
52438
53827
  on: boolean()
52439
53828
  }), _void(), {
52440
53829
  kind: "mutation",
52441
53830
  auth: "protected"
52442
53831
  }),
52443
53832
  setHygienePlus: method(object({
52444
- deviceId: DeviceIdSchema$1,
53833
+ deviceId: DeviceIdSchema$2,
52445
53834
  on: boolean()
52446
53835
  }), _void(), {
52447
53836
  kind: "mutation",
52448
53837
  auth: "protected"
52449
53838
  }),
52450
53839
  setExtraDry: method(object({
52451
- deviceId: DeviceIdSchema$1,
53840
+ deviceId: DeviceIdSchema$2,
52452
53841
  on: boolean()
52453
53842
  }), _void(), {
52454
53843
  kind: "mutation",
52455
53844
  auth: "protected"
52456
53845
  }),
52457
53846
  setIntensiveZone: method(object({
52458
- deviceId: DeviceIdSchema$1,
53847
+ deviceId: DeviceIdSchema$2,
52459
53848
  on: boolean()
52460
53849
  }), _void(), {
52461
53850
  kind: "mutation",
52462
53851
  auth: "protected"
52463
53852
  }),
52464
53853
  setVarioSpeedPlus: method(object({
52465
- deviceId: DeviceIdSchema$1,
53854
+ deviceId: DeviceIdSchema$2,
52466
53855
  on: boolean()
52467
53856
  }), _void(), {
52468
53857
  kind: "mutation",
52469
53858
  auth: "protected"
52470
53859
  }),
52471
53860
  setChildLock: method(object({
52472
- deviceId: DeviceIdSchema$1,
53861
+ deviceId: DeviceIdSchema$2,
52473
53862
  on: boolean()
52474
53863
  }), _void(), {
52475
53864
  kind: "mutation",
@@ -52481,7 +53870,7 @@ var dishwasherCapability = {
52481
53870
  * its declared range / members.
52482
53871
  */
52483
53872
  setOption: method(object({
52484
- deviceId: DeviceIdSchema$1,
53873
+ deviceId: DeviceIdSchema$2,
52485
53874
  key: string().min(1),
52486
53875
  value: DishwasherValueSchema
52487
53876
  }), _void(), {
@@ -52489,7 +53878,7 @@ var dishwasherCapability = {
52489
53878
  auth: "protected"
52490
53879
  }),
52491
53880
  /** What this appliance can do, from its own description — the UI is derived from it. */
52492
- getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
53881
+ getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
52493
53882
  },
52494
53883
  status: {
52495
53884
  schema: DishwasherStatusSchema,
@@ -53913,15 +55302,15 @@ var FanPresetCaptureResultSchema = object({
53913
55302
  items: array(FanCaptureItemSchema),
53914
55303
  omitted: array(FanCaptureOmissionSchema)
53915
55304
  });
53916
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55305
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
53917
55306
  DeviceType.Fan;
53918
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55307
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
53919
55308
  kind: "mutation",
53920
55309
  auth: "admin"
53921
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55310
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
53922
55311
  kind: "mutation",
53923
55312
  auth: "protected"
53924
- }), method(deviceInput$1.extend({
55313
+ }), method(deviceInput$2.extend({
53925
55314
  fields: array(FanCaptureFieldSchema).optional(),
53926
55315
  steps: array(ActionStepSchema).max(20).default([])
53927
55316
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -54804,6 +56193,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54804
56193
  }), object({ success: boolean() }), {
54805
56194
  kind: "mutation",
54806
56195
  auth: "admin"
56196
+ }), method(object({ id: string() }), object({
56197
+ rows: number().int().nonnegative(),
56198
+ secrets: number().int().nonnegative()
56199
+ }), {
56200
+ kind: "mutation",
56201
+ auth: "admin"
54807
56202
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54808
56203
  addonId: string(),
54809
56204
  settings: record(string(), unknown())
@@ -54832,13 +56227,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54832
56227
  */
54833
56228
  var ConfigSchemaPassthrough$3 = custom();
54834
56229
  var IntegrationInstanceIdSchema = string().min(1);
56230
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56231
+ /**
56232
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56233
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56234
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56235
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56236
+ * instance id.
56237
+ */
56238
+ var IntegrationInstanceKindSchema = object({
56239
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56240
+ id: string().min(1),
56241
+ label: string().min(1),
56242
+ /** The kind signs in through a server-held interactive session. */
56243
+ signIn: object({
56244
+ basePath: string().min(1),
56245
+ methods: array(_enum(["device-code", "proxy"])).optional()
56246
+ }).optional(),
56247
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56248
+ connectPath: string().min(1).optional(),
56249
+ /** Relative hub path to disconnect that account. */
56250
+ disconnectPath: string().min(1).optional()
56251
+ });
54835
56252
  var IntegrationInstanceDescriptionSchema = object({
56253
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56254
+ role: _enum([
56255
+ "devices",
56256
+ "services",
56257
+ "both"
56258
+ ]),
56259
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56260
+ discovery: object({
56261
+ mode: _enum([
56262
+ "none",
56263
+ "lan",
56264
+ "account"
56265
+ ]),
56266
+ scopedToInstance: boolean()
56267
+ }).optional(),
54836
56268
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
54837
- instanceMode: _enum(["single", "multiple"]),
56269
+ instanceMode: IntegrationInstanceModeSchema,
54838
56270
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
54839
56271
  kind: string(),
54840
56272
  /** True when `testInstance` really probes the remote. */
54841
- canTest: boolean()
56273
+ canTest: boolean(),
56274
+ /**
56275
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56276
+ * Add flow asks which first and sends it as `createInstance.kind`.
56277
+ */
56278
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56279
+ /**
56280
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56281
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56282
+ */
56283
+ allowRestoreId: boolean().optional(),
56284
+ /**
56285
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56286
+ * send an id (the UI then mints an opaque one).
56287
+ */
56288
+ mintsId: boolean().optional()
54842
56289
  });
54843
56290
  var IntegrationInstanceStatusSchema = object({
54844
56291
  state: _enum([
@@ -54847,16 +56294,28 @@ var IntegrationInstanceStatusSchema = object({
54847
56294
  "error",
54848
56295
  "unknown"
54849
56296
  ]),
54850
- detail: string().optional()
56297
+ detail: string().optional(),
56298
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56299
+ label: string().optional()
54851
56300
  });
54852
56301
  var IntegrationInstanceSchema = object({
54853
56302
  id: IntegrationInstanceIdSchema,
54854
56303
  name: string(),
54855
56304
  /** The addon's own rule (an URL is present, a client id is present...). */
54856
56305
  configured: boolean(),
56306
+ /**
56307
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56308
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56309
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56310
+ */
56311
+ enabled: boolean().optional(),
56312
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56313
+ kind: string().optional(),
54857
56314
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
54858
56315
  formValues: record(string(), unknown()),
54859
- status: IntegrationInstanceStatusSchema.optional()
56316
+ status: IntegrationInstanceStatusSchema.optional(),
56317
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56318
+ lastCheckedAt: number().optional()
54860
56319
  });
54861
56320
  var IntegrationInstancePatchSchema = object({
54862
56321
  id: IntegrationInstanceIdSchema,
@@ -54864,25 +56323,57 @@ var IntegrationInstancePatchSchema = object({
54864
56323
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
54865
56324
  values: record(string(), unknown())
54866
56325
  });
54867
- 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({
54868
- id: IntegrationInstanceIdSchema,
54869
- name: string().min(1)
54870
- }), object({ id: string() }), {
54871
- kind: "mutation",
54872
- auth: "admin"
54873
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
54874
- kind: "mutation",
54875
- auth: "admin"
54876
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
54877
- kind: "mutation",
54878
- auth: "admin"
54879
- }), optionalMethod(object({
54880
- id: IntegrationInstanceIdSchema.nullable(),
54881
- values: record(string(), unknown())
54882
- }), TestConnectionResultSchema$1, {
54883
- kind: "mutation",
54884
- auth: "admin"
54885
- });
56326
+ var integrationInstancesCapability = {
56327
+ name: "integration-instances",
56328
+ scope: "system",
56329
+ mode: "collection",
56330
+ methods: {
56331
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56332
+ /** The form; `id: null` is the create form. */
56333
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56334
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56335
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56336
+ /**
56337
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56338
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56339
+ */
56340
+ createInstance: method(object({
56341
+ id: IntegrationInstanceIdSchema.optional(),
56342
+ name: string().min(1),
56343
+ kind: string().min(1).optional()
56344
+ }), object({ id: string() }), {
56345
+ kind: "mutation",
56346
+ auth: "admin"
56347
+ }),
56348
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56349
+ kind: "mutation",
56350
+ auth: "admin"
56351
+ }),
56352
+ /**
56353
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56354
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56355
+ */
56356
+ setInstanceEnabled: method(object({
56357
+ id: IntegrationInstanceIdSchema,
56358
+ enabled: boolean()
56359
+ }), object({ ok: literal(true) }), {
56360
+ kind: "mutation",
56361
+ auth: "admin"
56362
+ }),
56363
+ /** Delete the record and its keychain refs together. */
56364
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56365
+ kind: "mutation",
56366
+ auth: "admin"
56367
+ }),
56368
+ testInstance: optionalMethod(object({
56369
+ id: IntegrationInstanceIdSchema.nullable(),
56370
+ values: record(string(), unknown())
56371
+ }), TestConnectionResultSchema$1, {
56372
+ kind: "mutation",
56373
+ auth: "admin"
56374
+ })
56375
+ }
56376
+ };
54886
56377
  /**
54887
56378
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
54888
56379
  * session (server-initiated offer pattern, identical to the main
@@ -56900,6 +58391,11 @@ var LogEntrySchema = object({
56900
58391
  * flight, and not asking for the next slice ends the scan.
56901
58392
  */
56902
58393
  var LogReadQuerySchema = object({
58394
+ /**
58395
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58396
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58397
+ */
58398
+ instanceId: string().min(1).max(200).optional(),
56903
58399
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
56904
58400
  requestId: string().min(1).max(64).optional(),
56905
58401
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -56962,6 +58458,8 @@ var LogReadResultSchema = object({
56962
58458
  /** True when `cancelRead` ended this slice early. */
56963
58459
  cancelled: boolean().optional()
56964
58460
  });
58461
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58462
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
56965
58463
  var LogReadCapabilitiesSchema = object({
56966
58464
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
56967
58465
  readable: boolean(),
@@ -56980,7 +58478,7 @@ var LogReadCapabilitiesSchema = object({
56980
58478
  /** How far back the store reaches, when the provider knows (days). */
56981
58479
  retentionDays: number().nullable()
56982
58480
  });
56983
- 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() }), {
58481
+ 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() }), {
56984
58482
  kind: "mutation",
56985
58483
  auth: "admin"
56986
58484
  });
@@ -57084,214 +58582,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
57084
58582
  ]);
57085
58583
  method(_void(), array(LoginMethodContributionSchema).readonly());
57086
58584
  /**
57087
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
57088
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
57089
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
57090
- * surface — those use `DeviceType.Camera` + the broker pipeline.
57091
- *
57092
- * The cap declares a wide method surface; not every backend supports
57093
- * every method. Drivers signal sub-capability via the
57094
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
57095
- * corresponding method) so the UI gates buttons without a slice
57096
- * fetch — same pattern as `PanTiltZoom` for cameras.
57097
- */
57098
- var MediaPlayerStateSchema = _enum([
57099
- "off",
57100
- "on",
57101
- "idle",
57102
- "playing",
57103
- "paused",
57104
- "buffering",
57105
- "standby"
57106
- ]);
57107
- var MediaPlayerRepeatSchema = _enum([
57108
- "off",
57109
- "all",
57110
- "one"
57111
- ]);
57112
- var MediaInfoSchema = object({
57113
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
57114
- * `podcast`, …). */
57115
- type: string(),
57116
- /** Human-readable title. */
57117
- title: string(),
57118
- /** Optional artist / channel / station label. */
57119
- artist: string().optional(),
57120
- /** Optional album / season / show name. */
57121
- album: string().optional(),
57122
- /** Optional cover-art / thumbnail URL. */
57123
- imageUrl: string().optional()
57124
- });
57125
- /**
57126
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
57127
- * the device said so. `assumed`: the driver has no feedback (infrared) and
57128
- * tracks what it last SENT; it moves only after a send was accepted, and a
57129
- * human at the real remote makes it wrong. A UI shows an assumed value as a
57130
- * guess, never as a reading.
57131
- */
57132
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
57133
- /** The fields of a media-player status a driver can only guess. */
57134
- var MediaPlayerAssumedFieldSchema = _enum([
57135
- "state",
57136
- "volumeLevel",
57137
- "isMuted",
57138
- "source"
57139
- ]);
57140
- var MediaPlayerStatusSchema = object({
57141
- /** Playback lifecycle state. */
57142
- state: MediaPlayerStateSchema,
57143
- /**
57144
- * Whether the state fields are readings or the driver's own bookkeeping.
57145
- * A slice written before this field existed parses as `reported`.
57146
- */
57147
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
57148
- /**
57149
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
57150
- * this is not empty; a field not listed here is a reading even then (power
57151
- * from a running-state source, mute guessed from the last send).
57152
- */
57153
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
57154
- /** Volume as 0..100 inclusive. Null when the device has no volume
57155
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
57156
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57157
- /** Mute toggle distinct from volume=0. Null when no mute surface.
57158
- * Pair with `DeviceFeature.MediaPlayerMute`. */
57159
- isMuted: boolean().nullable(),
57160
- /** Active source / input. Empty when no source surface. */
57161
- source: string(),
57162
- /** Selectable sources. Empty when no source surface.
57163
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
57164
- availableSources: array(string()),
57165
- /** Currently-playing media info. Null when nothing is playing. */
57166
- currentMedia: MediaInfoSchema.nullable(),
57167
- /** Current playback position in ms. Null when not seekable or
57168
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
57169
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57170
- /** Total duration of the current media in ms. Null when unknown
57171
- * (live stream) or nothing is playing. */
57172
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57173
- /** Shuffle toggle. Null when no shuffle surface.
57174
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
57175
- shuffle: boolean().nullable(),
57176
- /** Repeat mode. Null when no repeat surface.
57177
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
57178
- repeat: MediaPlayerRepeatSchema.nullable(),
57179
- /** Ms epoch when the slice was last updated. */
57180
- lastChangedAt: number()
57181
- });
57182
- var mediaPlayerCapability = {
57183
- name: "media-player",
57184
- scope: "device",
57185
- deviceNative: true,
57186
- mode: "singleton",
57187
- deviceTypes: [DeviceType.MediaPlayer],
57188
- methods: {
57189
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57190
- kind: "mutation",
57191
- auth: "protected"
57192
- }),
57193
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57194
- kind: "mutation",
57195
- auth: "protected"
57196
- }),
57197
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57198
- kind: "mutation",
57199
- auth: "protected"
57200
- }),
57201
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57202
- kind: "mutation",
57203
- auth: "protected"
57204
- }),
57205
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
57206
- kind: "mutation",
57207
- auth: "protected"
57208
- }),
57209
- seek: method(object({
57210
- deviceId: number().int().nonnegative(),
57211
- position: number().int().nonnegative()
57212
- }), _void(), {
57213
- kind: "mutation",
57214
- auth: "protected"
57215
- }),
57216
- setVolume: method(object({
57217
- deviceId: number().int().nonnegative(),
57218
- volumeLevel: number().min(0).max(100)
57219
- }), _void(), {
57220
- kind: "mutation",
57221
- auth: "protected"
57222
- }),
57223
- setMute: method(object({
57224
- deviceId: number().int().nonnegative(),
57225
- muted: boolean()
57226
- }), _void(), {
57227
- kind: "mutation",
57228
- auth: "protected"
57229
- }),
57230
- setShuffle: method(object({
57231
- deviceId: number().int().nonnegative(),
57232
- shuffle: boolean()
57233
- }), _void(), {
57234
- kind: "mutation",
57235
- auth: "protected"
57236
- }),
57237
- setRepeat: method(object({
57238
- deviceId: number().int().nonnegative(),
57239
- repeat: MediaPlayerRepeatSchema
57240
- }), _void(), {
57241
- kind: "mutation",
57242
- auth: "protected"
57243
- }),
57244
- selectSource: method(object({
57245
- deviceId: number().int().nonnegative(),
57246
- source: string().min(1)
57247
- }), _void(), {
57248
- kind: "mutation",
57249
- auth: "protected"
57250
- }),
57251
- playMedia: method(object({
57252
- deviceId: number().int().nonnegative(),
57253
- /** Media identifier / URL. */
57254
- mediaId: string().min(1),
57255
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
57256
- * passes it through to the upstream service. */
57257
- mediaType: string().min(1)
57258
- }), _void(), {
57259
- kind: "mutation",
57260
- auth: "protected"
57261
- })
57262
- },
57263
- status: {
57264
- schema: MediaPlayerStatusSchema,
57265
- kind: "push"
57266
- },
57267
- /**
57268
- * Runtime-state slice — mirrored by the kernel. UI players read the
57269
- * full slice for live now-playing, volume, and progress updates
57270
- * without polling.
57271
- */
57272
- runtimeState: MediaPlayerStatusSchema,
57273
- /**
57274
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
57275
- *
57276
- * See `RuntimeStateDurability`. Enforced by
57277
- * `scripts/check-runtime-state-durability.ts`.
57278
- */
57279
- durability: "session",
57280
- /**
57281
- * History (D850): volume (a level an operator or an automation sets; the
57282
- * default 10 s floor folds a slider drag into its end value) and mute.
57283
- * `position` is not recorded: it moves continuously while playing.
57284
- */
57285
- history: { fields: {
57286
- volumeLevel: {
57287
- kind: "gauge",
57288
- unit: "%",
57289
- deadband: 1
57290
- },
57291
- isMuted: { kind: "boolean" }
57292
- } }
57293
- };
57294
- /**
57295
58585
  * mesh-network — collection cap for mesh-VPN providers.
57296
58586
  *
57297
58587
  * Different shape from `network-access` (public tunnels):
@@ -66050,6 +67340,21 @@ var NcTemplateVarKindSchema = _enum([
66050
67340
  "sound",
66051
67341
  "system"
66052
67342
  ]);
67343
+ /**
67344
+ * The default title/body a rule of one SITUATION is delivered with when the
67345
+ * operator wrote no template — served so an editor can pre-fill it (and the
67346
+ * operator can rebuild it from placeholders) instead of starting from a blank
67347
+ * box that, once saved, silently replaces it. `id` names the situation inside
67348
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
67349
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
67350
+ * the hub's current language and with its overrides applied.
67351
+ */
67352
+ var NcTemplateDefaultSchema = object({
67353
+ id: _enum(["appliance-cycle"]),
67354
+ kind: NcRuleKindSchema,
67355
+ title: string(),
67356
+ body: string()
67357
+ });
66053
67358
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
66054
67359
  var NcTemplateFamilySchema = _enum([
66055
67360
  "rule",
@@ -66905,7 +68210,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
66905
68210
  }), method(object({}), object({
66906
68211
  catalog: array(NcConditionDescriptorSchema),
66907
68212
  taxonomy: NcTaxonomySchema.optional()
66908
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
68213
+ })), method(object({}), object({
68214
+ vars: array(NcTemplateVarDescriptorSchema),
68215
+ /** Per-situation default texts; absent on a hub that predates them. */
68216
+ defaults: array(NcTemplateDefaultSchema)
68217
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
66909
68218
  kind: "mutation",
66910
68219
  access: "view"
66911
68220
  }), 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 }), {
@@ -67801,7 +69110,7 @@ var petFeederCapability = {
67801
69110
  /**
67802
69111
  * Pet water-fountain cap. Models the telemetry and the control surface of a
67803
69112
  * circulating drinking fountain: how much water is in the tank, how much the
67804
- * pet drank, whether the pump runs and on what duty cycle.
69113
+ * pet drank, whether the pump runs.
67805
69114
  *
67806
69115
  * It has no native vendor provider of its own: a fountain reaches CamStack as
67807
69116
  * a container of loose entities (a pump switch, two number boxes, a "filter
@@ -67817,8 +69126,10 @@ var petFeederCapability = {
67817
69126
  * never here. Every reading may be `null`: a source that is offline or does
67818
69127
  * not report is unknown, never zero.
67819
69128
  *
67820
- * The filter is not a field of this cap: it is an item of the `consumables`
67821
- * cap on the same device (kind `wear`), reset by the same forward mechanism.
69129
+ * The pump's on/off duty cycle is model-specific and lives in the optional
69130
+ * `pump-cycle` cap. The filter, the cleaning and the water change are not
69131
+ * fields of this cap: they are items of the `consumables` cap on the same
69132
+ * device (kind `wear`), reset by the same forward mechanism.
67822
69133
  */
67823
69134
  var PetFountainStatusSchema = object({
67824
69135
  /** Water in the tank, 0..100 (%). Null while the source has not reported. */
@@ -67829,27 +69140,20 @@ var PetFountainStatusSchema = object({
67829
69140
  empty: boolean().nullable(),
67830
69141
  /** True while the pump is running. Null while unknown. */
67831
69142
  pumpRunning: boolean().nullable(),
67832
- /** True when the pump works on an on/off duty cycle rather than continuously. */
67833
- pumpIntervalEnabled: boolean().nullable(),
67834
- /** How long the pump runs per cycle, in seconds. */
67835
- pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
67836
- /** How long the pump rests per cycle, in seconds. */
67837
- pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
67838
69143
  /** Water drunk in the last drink, in litres. */
67839
69144
  lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
67840
69145
  /** Water drunk in the current hour, in litres. */
67841
69146
  currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
67842
69147
  /** Water drunk over the last 24 hours (rolling), in litres. */
67843
69148
  last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
67844
- /** Water drunk since local midnight, in litres. */
69149
+ /** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
67845
69150
  todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
67846
69151
  /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
67847
69152
  error: string().nullable(),
67848
69153
  /** Ms epoch when the slice was last refreshed. */
67849
69154
  lastFetchedAt: number()
67850
69155
  });
67851
- var DeviceIdSchema = number().int().nonnegative();
67852
- var MinutesSchema = number().int().min(0);
69156
+ var DeviceIdSchema$1 = number().int().nonnegative();
67853
69157
  var petFountainCapability = {
67854
69158
  name: "pet-fountain",
67855
69159
  scope: "device",
@@ -67859,38 +69163,14 @@ var petFountainCapability = {
67859
69163
  methods: {
67860
69164
  /** Start or stop the pump. */
67861
69165
  setPump: method(object({
67862
- deviceId: DeviceIdSchema,
67863
- on: boolean()
67864
- }), _void(), {
67865
- kind: "mutation",
67866
- auth: "protected"
67867
- }),
67868
- /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
67869
- setPumpInterval: method(object({
67870
- deviceId: DeviceIdSchema,
69166
+ deviceId: DeviceIdSchema$1,
67871
69167
  on: boolean()
67872
69168
  }), _void(), {
67873
69169
  kind: "mutation",
67874
69170
  auth: "protected"
67875
69171
  }),
67876
- /** Set how many minutes the pump runs per cycle. */
67877
- setPumpOnMinutes: method(object({
67878
- deviceId: DeviceIdSchema,
67879
- minutes: MinutesSchema
67880
- }), _void(), {
67881
- kind: "mutation",
67882
- auth: "protected"
67883
- }),
67884
- /** Set how many minutes the pump rests per cycle. */
67885
- setPumpOffMinutes: method(object({
67886
- deviceId: DeviceIdSchema,
67887
- minutes: MinutesSchema
67888
- }), _void(), {
67889
- kind: "mutation",
67890
- auth: "protected"
67891
- }),
67892
69172
  /** Clear the rolling daily consumption counters on the device. */
67893
- resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
69173
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
67894
69174
  kind: "mutation",
67895
69175
  auth: "protected"
67896
69176
  })
@@ -69525,6 +70805,14 @@ var powerMeterCapability = {
69525
70805
  deviceNative: true,
69526
70806
  mode: "singleton",
69527
70807
  deviceTypes: POWER_METER_DEVICE_TYPES,
70808
+ deviceConfig: { ui: {
70809
+ kind: "widget",
70810
+ widgetId: "host/power-meter-line",
70811
+ tab: "power",
70812
+ placement: "hero",
70813
+ label: "Power",
70814
+ order: 4
70815
+ } },
69528
70816
  methods: {},
69529
70817
  status: {
69530
70818
  schema: PowerMeterStatusSchema,
@@ -69584,6 +70872,117 @@ var powerMeterCapability = {
69584
70872
  volatileStateFields: ["lastFetchedAt"],
69585
70873
  customize: powerMeterCustomize
69586
70874
  };
70875
+ var PowerSourceRefusalReasonSchema = _enum([
70876
+ "no-power-source",
70877
+ "switch-missing",
70878
+ "switch-offline",
70879
+ "switch-unknown",
70880
+ "restart-in-progress",
70881
+ "switch-call-failed"
70882
+ ]);
70883
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
70884
+ accepted: literal(false),
70885
+ reason: PowerSourceRefusalReasonSchema,
70886
+ /** One line for the operator, naming the switch. */
70887
+ message: string()
70888
+ })]);
70889
+ var PowerSourceInfoSchema = object({
70890
+ /** False when the device has no power source linked; the rest is then empty. */
70891
+ linked: boolean(),
70892
+ switchDeviceId: number().nullable(),
70893
+ switchName: string().nullable(),
70894
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
70895
+ switchOn: boolean().nullable(),
70896
+ switchOnline: boolean().nullable(),
70897
+ restartInProgress: boolean(),
70898
+ offSeconds: number().int()
70899
+ });
70900
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
70901
+ var PowerSourceStateSchema = object({
70902
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
70903
+ switchOn: boolean().nullable(),
70904
+ offSeconds: number().int(),
70905
+ lastFetchedAt: number()
70906
+ });
70907
+ var POWER_SOURCE_DEVICE_TYPES = [
70908
+ DeviceType.Gateway,
70909
+ DeviceType.Hub,
70910
+ DeviceType.Camera,
70911
+ DeviceType.Sensor,
70912
+ DeviceType.MediaPlayer,
70913
+ DeviceType.Vacuum,
70914
+ DeviceType.Generic,
70915
+ DeviceType.Washer,
70916
+ DeviceType.Dryer,
70917
+ DeviceType.Dishwasher,
70918
+ DeviceType.PetFountain,
70919
+ DeviceType.Fan,
70920
+ DeviceType.Humidifier,
70921
+ DeviceType.Climate,
70922
+ DeviceType.WaterHeater
70923
+ ];
70924
+ var powerSourceCustomize = {
70925
+ version: 1,
70926
+ targets: [{
70927
+ id: "switch",
70928
+ cardinality: "scalar",
70929
+ vocabulary: {
70930
+ cap: "power-source",
70931
+ stateField: "switchOn"
70932
+ },
70933
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70934
+ apply: "add",
70935
+ path: "switchOn",
70936
+ sources: [{
70937
+ kind: "switch",
70938
+ transforms: [{ kind: "identity" }]
70939
+ }],
70940
+ companions: [{
70941
+ kind: "constant",
70942
+ path: "offSeconds",
70943
+ value: 10,
70944
+ when: "add"
70945
+ }, {
70946
+ kind: "clock",
70947
+ path: "lastFetchedAt",
70948
+ when: "always"
70949
+ }],
70950
+ sourceReport: { status: "assignment-status" }
70951
+ }]
70952
+ };
70953
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
70954
+ var powerSourceCapability = {
70955
+ name: "power-source",
70956
+ scope: "device",
70957
+ mode: "singleton",
70958
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
70959
+ methods: {
70960
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
70961
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70962
+ kind: "mutation",
70963
+ auth: "admin"
70964
+ }),
70965
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70966
+ kind: "mutation",
70967
+ auth: "admin"
70968
+ }),
70969
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
70970
+ kind: "mutation",
70971
+ auth: "admin"
70972
+ })
70973
+ },
70974
+ runtimeState: PowerSourceStateSchema,
70975
+ customize: powerSourceCustomize,
70976
+ /**
70977
+ * Runtime-state durability: **session** — mirrored from the linked switch on
70978
+ * its next report; a restored value would outlive the reading it names.
70979
+ *
70980
+ * See `RuntimeStateDurability`. Enforced by
70981
+ * `scripts/check-runtime-state-durability.ts`.
70982
+ */
70983
+ durability: "session",
70984
+ history: "none"
70985
+ };
69587
70986
  /**
69588
70987
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
69589
70988
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -70223,6 +71622,79 @@ var ptzAutotrackCapability = {
70223
71622
  */
70224
71623
  history: "none"
70225
71624
  };
71625
+ /**
71626
+ * The on/off duty cycle of a pet fountain's pump. Some models run the pump
71627
+ * continuously and have nothing to configure; others can alternate a run time
71628
+ * and a rest time. That is model-specific, so it is its own optional cap on the
71629
+ * fountain device: a fountain without cycle entities simply never composes it
71630
+ * (planning#366), and the `pet-fountain` cap stays the model-independent core.
71631
+ *
71632
+ * Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
71633
+ * cycle from loose entities (a cycle switch and two number boxes) and every
71634
+ * method is a forward to one source entity (D659, D967). Durations are stored
71635
+ * in SECONDS (D944); a number box that cannot be read back leaves them `null`
71636
+ * — unknown, never zero (D393).
71637
+ */
71638
+ var PumpCycleStatusSchema = object({
71639
+ /** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
71640
+ enabled: boolean().nullable(),
71641
+ /** How long the pump runs per cycle, in seconds. Null while unknown. */
71642
+ onTime: number().min(0).nullable().meta(readingMeta("duration")),
71643
+ /** How long the pump rests per cycle, in seconds. Null while unknown. */
71644
+ offTime: number().min(0).nullable().meta(readingMeta("duration")),
71645
+ /** Ms epoch when the slice was last refreshed. */
71646
+ lastFetchedAt: number()
71647
+ });
71648
+ var DeviceIdSchema = number().int().nonnegative();
71649
+ var MinutesSchema = number().int().min(0);
71650
+ var pumpCycleCapability = {
71651
+ name: "pump-cycle",
71652
+ scope: "device",
71653
+ deviceNative: true,
71654
+ mode: "singleton",
71655
+ deviceTypes: [DeviceType.PetFountain],
71656
+ methods: {
71657
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
71658
+ setEnabled: method(object({
71659
+ deviceId: DeviceIdSchema,
71660
+ on: boolean()
71661
+ }), _void(), {
71662
+ kind: "mutation",
71663
+ auth: "protected"
71664
+ }),
71665
+ /** Set how many minutes the pump runs per cycle. */
71666
+ setOnMinutes: method(object({
71667
+ deviceId: DeviceIdSchema,
71668
+ minutes: MinutesSchema
71669
+ }), _void(), {
71670
+ kind: "mutation",
71671
+ auth: "protected"
71672
+ }),
71673
+ /** Set how many minutes the pump rests per cycle. */
71674
+ setOffMinutes: method(object({
71675
+ deviceId: DeviceIdSchema,
71676
+ minutes: MinutesSchema
71677
+ }), _void(), {
71678
+ kind: "mutation",
71679
+ auth: "protected"
71680
+ })
71681
+ },
71682
+ status: {
71683
+ schema: PumpCycleStatusSchema,
71684
+ kind: "push"
71685
+ },
71686
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
71687
+ runtimeState: PumpCycleStatusSchema,
71688
+ /**
71689
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
71690
+ *
71691
+ * See `RuntimeStateDurability`. Enforced by
71692
+ * `scripts/check-runtime-state-durability.ts`.
71693
+ */
71694
+ durability: "session",
71695
+ /** History (D850): a duty-cycle setting is not a series worth recording. */
71696
+ history: "none"
71697
+ };
70226
71698
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
70227
71699
  kind: "mutation",
70228
71700
  auth: "admin"
@@ -73013,7 +74485,13 @@ var RemoteMediaCommandSchema = _enum([
73013
74485
  "next",
73014
74486
  "previous",
73015
74487
  "setMute",
73016
- "selectSource"
74488
+ "selectSource",
74489
+ "turnOn",
74490
+ "turnOff",
74491
+ "volumeUp",
74492
+ "volumeDown",
74493
+ "channelUp",
74494
+ "channelDown"
73017
74495
  ]);
73018
74496
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
73019
74497
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -73034,7 +74512,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
73034
74512
  }), RemoteSendResultSchema, {
73035
74513
  kind: "mutation",
73036
74514
  auth: "protected"
73037
- }), method(deviceInput.extend({
74515
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
73038
74516
  command: RemoteMediaCommandSchema,
73039
74517
  muted: boolean().optional(),
73040
74518
  source: string().optional(),
@@ -75280,7 +76758,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
75280
76758
  * `getSettingsUISchema()` surface.
75281
76759
  */
75282
76760
  var SwitchStatusSchema = object({
75283
- on: boolean().meta({ label: "On" }),
76761
+ /**
76762
+ * `null` = the device has never reported its state (a measurement that
76763
+ * has not happened is unknown, never `false`).
76764
+ */
76765
+ on: boolean().nullable().meta({ label: "On" }),
75284
76766
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
75285
76767
  lastChangedAt: number()
75286
76768
  });
@@ -75305,7 +76787,7 @@ var switchCapability = {
75305
76787
  }) },
75306
76788
  events: { onStateChanged: { data: object({
75307
76789
  deviceId: number(),
75308
- on: boolean(),
76790
+ on: boolean().nullable(),
75309
76791
  lastChangedAt: number()
75310
76792
  }) } },
75311
76793
  status: {
@@ -78954,6 +80436,26 @@ function buildDescriptor(capName, kind) {
78954
80436
  * contributes one entry per bound cap present in this map.
78955
80437
  */
78956
80438
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
80439
+ /**
80440
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
80441
+ * shared by the admin UI and the viewer. A role is data on the wire; each
80442
+ * client resolves its name from `label` (English and Italian side by side) and
80443
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
80444
+ * server sends an id, a client maps it to its own icon set.
80445
+ *
80446
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
80447
+ * pictogram): a client that can draw text shows it, one that cannot falls back
80448
+ * to `icon`.
80449
+ */
80450
+ function digit(n) {
80451
+ return {
80452
+ label: t(n, n),
80453
+ icon: "hash",
80454
+ legend: n
80455
+ };
80456
+ }
80457
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
80458
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
78957
80459
  var btn = (idSuffix, row, col, init) => ({
78958
80460
  idSuffix,
78959
80461
  row,
@@ -79080,6 +80582,34 @@ discriminatedUnion("kind", [object({
79080
80582
  kind: literal("pronto"),
79081
80583
  code: string()
79082
80584
  })]);
80585
+ /**
80586
+ * Propose a running threshold from a device's own power history (planning#405).
80587
+ *
80588
+ * Pure and deterministic: the same series gives the same answer. The series is
80589
+ * a HELD-VALUE step series — a point's value stands until the next point — which
80590
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
80591
+ * count of samples.
80592
+ *
80593
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80594
+ * near F. A device that is not near one level most of the time has no floor.
80595
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80596
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80597
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80598
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80599
+ * grid a person would type; stop dwell covers the longest pause; start dwell
80600
+ * covers the longest standby blip above T.
80601
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80602
+ * run must hold exactly one true interval and nothing may flap outside.
80603
+ */
80604
+ var SECOND_MS = 1e3;
80605
+ var MINUTE_MS$1 = 60 * SECOND_MS;
80606
+ 24 * (60 * MINUTE_MS$1);
80607
+ 10 * MINUTE_MS$1;
80608
+ 30 * SECOND_MS;
80609
+ 60 * SECOND_MS;
80610
+ 20 * MINUTE_MS$1;
80611
+ 10 * SECOND_MS;
80612
+ 10 * SECOND_MS;
79083
80613
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79084
80614
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79085
80615
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -79440,6 +80970,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79440
80970
  pressureSensor: pressureSensorCapability,
79441
80971
  privacyMask: privacyMaskCapability,
79442
80972
  ptzAutotrack: ptzAutotrackCapability,
80973
+ pumpCycle: pumpCycleCapability,
79443
80974
  recordingOnboard: recordingOnboardCapability,
79444
80975
  recordingSignal: recordingSignalCapability,
79445
80976
  runningState: runningStateCapability,
@@ -81624,6 +83155,27 @@ Object.freeze({
81624
83155
  access: "view",
81625
83156
  role: "viewer"
81626
83157
  },
83158
+ "boardKeySource.getBoardKey": {
83159
+ capName: "board-key-source",
83160
+ capScope: "system",
83161
+ addonId: null,
83162
+ access: "view",
83163
+ role: "admin"
83164
+ },
83165
+ "boardKeySource.listBoards": {
83166
+ capName: "board-key-source",
83167
+ capScope: "system",
83168
+ addonId: null,
83169
+ access: "view",
83170
+ role: "admin"
83171
+ },
83172
+ "boardKeySource.listSources": {
83173
+ capName: "board-key-source",
83174
+ capScope: "system",
83175
+ addonId: null,
83176
+ access: "view",
83177
+ role: "admin"
83178
+ },
81627
83179
  "brightness.getStatus": {
81628
83180
  capName: "brightness",
81629
83181
  capScope: "device",
@@ -82163,6 +83715,13 @@ Object.freeze({
82163
83715
  access: "create",
82164
83716
  role: "admin"
82165
83717
  },
83718
+ "coreBlocks.proposeRunningThreshold": {
83719
+ capName: "core-blocks",
83720
+ capScope: "system",
83721
+ addonId: null,
83722
+ access: "view",
83723
+ role: "admin"
83724
+ },
82166
83725
  "coreBlocks.reportAutomationRun": {
82167
83726
  capName: "core-blocks",
82168
83727
  capScope: "system",
@@ -82674,6 +84233,13 @@ Object.freeze({
82674
84233
  access: "view",
82675
84234
  role: "admin"
82676
84235
  },
84236
+ "deviceExport.listInstances": {
84237
+ capName: "device-export",
84238
+ capScope: "system",
84239
+ addonId: null,
84240
+ access: "view",
84241
+ role: "admin"
84242
+ },
82677
84243
  "deviceExport.listSupportedDeviceKinds": {
82678
84244
  capName: "device-export",
82679
84245
  capScope: "system",
@@ -84305,6 +85871,13 @@ Object.freeze({
84305
85871
  access: "view",
84306
85872
  role: "admin"
84307
85873
  },
85874
+ "integrationInstances.setInstanceEnabled": {
85875
+ capName: "integration-instances",
85876
+ capScope: "system",
85877
+ addonId: null,
85878
+ access: "create",
85879
+ role: "admin"
85880
+ },
84308
85881
  "integrationInstances.testInstance": {
84309
85882
  capName: "integration-instances",
84310
85883
  capScope: "system",
@@ -84319,6 +85892,13 @@ Object.freeze({
84319
85892
  access: "create",
84320
85893
  role: "admin"
84321
85894
  },
85895
+ "integrations.clearSettings": {
85896
+ capName: "integrations",
85897
+ capScope: "system",
85898
+ addonId: null,
85899
+ access: "delete",
85900
+ role: "admin"
85901
+ },
84322
85902
  "integrations.create": {
84323
85903
  capName: "integrations",
84324
85904
  capScope: "system",
@@ -85103,6 +86683,20 @@ Object.freeze({
85103
86683
  access: "view",
85104
86684
  role: "admin"
85105
86685
  },
86686
+ "mediaPlayer.channelDown": {
86687
+ capName: "media-player",
86688
+ capScope: "device",
86689
+ addonId: null,
86690
+ access: "create",
86691
+ role: "viewer"
86692
+ },
86693
+ "mediaPlayer.channelUp": {
86694
+ capName: "media-player",
86695
+ capScope: "device",
86696
+ addonId: null,
86697
+ access: "create",
86698
+ role: "viewer"
86699
+ },
85106
86700
  "mediaPlayer.getStatus": {
85107
86701
  capName: "media-player",
85108
86702
  capScope: "device",
@@ -85194,6 +86788,34 @@ Object.freeze({
85194
86788
  access: "create",
85195
86789
  role: "viewer"
85196
86790
  },
86791
+ "mediaPlayer.turnOff": {
86792
+ capName: "media-player",
86793
+ capScope: "device",
86794
+ addonId: null,
86795
+ access: "create",
86796
+ role: "viewer"
86797
+ },
86798
+ "mediaPlayer.turnOn": {
86799
+ capName: "media-player",
86800
+ capScope: "device",
86801
+ addonId: null,
86802
+ access: "create",
86803
+ role: "viewer"
86804
+ },
86805
+ "mediaPlayer.volumeDown": {
86806
+ capName: "media-player",
86807
+ capScope: "device",
86808
+ addonId: null,
86809
+ access: "create",
86810
+ role: "viewer"
86811
+ },
86812
+ "mediaPlayer.volumeUp": {
86813
+ capName: "media-player",
86814
+ capScope: "device",
86815
+ addonId: null,
86816
+ access: "create",
86817
+ role: "viewer"
86818
+ },
85197
86819
  "meshNetwork.getStatus": {
85198
86820
  capName: "mesh-network",
85199
86821
  capScope: "system",
@@ -86440,27 +88062,6 @@ Object.freeze({
86440
88062
  access: "create",
86441
88063
  role: "viewer"
86442
88064
  },
86443
- "petFountain.setPumpInterval": {
86444
- capName: "pet-fountain",
86445
- capScope: "device",
86446
- addonId: null,
86447
- access: "create",
86448
- role: "viewer"
86449
- },
86450
- "petFountain.setPumpOffMinutes": {
86451
- capName: "pet-fountain",
86452
- capScope: "device",
86453
- addonId: null,
86454
- access: "create",
86455
- role: "viewer"
86456
- },
86457
- "petFountain.setPumpOnMinutes": {
86458
- capName: "pet-fountain",
86459
- capScope: "device",
86460
- addonId: null,
86461
- access: "create",
86462
- role: "viewer"
86463
- },
86464
88065
  "pipelineAnalytics.addRetrainFrameFromRecording": {
86465
88066
  capName: "pipeline-analytics",
86466
88067
  capScope: "device",
@@ -88239,6 +89840,34 @@ Object.freeze({
88239
89840
  access: "create",
88240
89841
  role: "admin"
88241
89842
  },
89843
+ "pumpCycle.getStatus": {
89844
+ capName: "pump-cycle",
89845
+ capScope: "device",
89846
+ addonId: null,
89847
+ access: "view",
89848
+ role: "viewer"
89849
+ },
89850
+ "pumpCycle.setEnabled": {
89851
+ capName: "pump-cycle",
89852
+ capScope: "device",
89853
+ addonId: null,
89854
+ access: "create",
89855
+ role: "viewer"
89856
+ },
89857
+ "pumpCycle.setOffMinutes": {
89858
+ capName: "pump-cycle",
89859
+ capScope: "device",
89860
+ addonId: null,
89861
+ access: "create",
89862
+ role: "viewer"
89863
+ },
89864
+ "pumpCycle.setOnMinutes": {
89865
+ capName: "pump-cycle",
89866
+ capScope: "device",
89867
+ addonId: null,
89868
+ access: "create",
89869
+ role: "viewer"
89870
+ },
88242
89871
  "reboot.reboot": {
88243
89872
  capName: "reboot",
88244
89873
  capScope: "device",
@@ -88743,6 +90372,13 @@ Object.freeze({
88743
90372
  access: "create",
88744
90373
  role: "admin"
88745
90374
  },
90375
+ "remote.getAssumedStatus": {
90376
+ capName: "remote",
90377
+ capScope: "system",
90378
+ addonId: null,
90379
+ access: "view",
90380
+ role: "admin"
90381
+ },
88746
90382
  "remote.getConfig": {
88747
90383
  capName: "remote",
88748
90384
  capScope: "system",
@@ -91558,6 +93194,11 @@ Object.freeze({
91558
93194
  form: "single",
91559
93195
  optional: false
91560
93196
  }],
93197
+ "coreBlocks.proposeRunningThreshold": [{
93198
+ name: "deviceId",
93199
+ form: "single",
93200
+ optional: false
93201
+ }],
91561
93202
  "cover.close": [{
91562
93203
  name: "deviceId",
91563
93204
  form: "single",
@@ -92409,6 +94050,16 @@ Object.freeze({
92409
94050
  form: "single",
92410
94051
  optional: true
92411
94052
  }],
94053
+ "mediaPlayer.channelDown": [{
94054
+ name: "deviceId",
94055
+ form: "single",
94056
+ optional: false
94057
+ }],
94058
+ "mediaPlayer.channelUp": [{
94059
+ name: "deviceId",
94060
+ form: "single",
94061
+ optional: false
94062
+ }],
92412
94063
  "mediaPlayer.getStatus": [{
92413
94064
  name: "deviceId",
92414
94065
  form: "single",
@@ -92474,6 +94125,26 @@ Object.freeze({
92474
94125
  form: "single",
92475
94126
  optional: false
92476
94127
  }],
94128
+ "mediaPlayer.turnOff": [{
94129
+ name: "deviceId",
94130
+ form: "single",
94131
+ optional: false
94132
+ }],
94133
+ "mediaPlayer.turnOn": [{
94134
+ name: "deviceId",
94135
+ form: "single",
94136
+ optional: false
94137
+ }],
94138
+ "mediaPlayer.volumeDown": [{
94139
+ name: "deviceId",
94140
+ form: "single",
94141
+ optional: false
94142
+ }],
94143
+ "mediaPlayer.volumeUp": [{
94144
+ name: "deviceId",
94145
+ form: "single",
94146
+ optional: false
94147
+ }],
92477
94148
  "meter.getStatus": [{
92478
94149
  name: "deviceId",
92479
94150
  form: "single",
@@ -92783,21 +94454,6 @@ Object.freeze({
92783
94454
  form: "single",
92784
94455
  optional: false
92785
94456
  }],
92786
- "petFountain.setPumpInterval": [{
92787
- name: "deviceId",
92788
- form: "single",
92789
- optional: false
92790
- }],
92791
- "petFountain.setPumpOffMinutes": [{
92792
- name: "deviceId",
92793
- form: "single",
92794
- optional: false
92795
- }],
92796
- "petFountain.setPumpOnMinutes": [{
92797
- name: "deviceId",
92798
- form: "single",
92799
- optional: false
92800
- }],
92801
94457
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
92802
94458
  name: "deviceId",
92803
94459
  form: "single",
@@ -93433,6 +95089,26 @@ Object.freeze({
93433
95089
  form: "single",
93434
95090
  optional: false
93435
95091
  }],
95092
+ "pumpCycle.getStatus": [{
95093
+ name: "deviceId",
95094
+ form: "single",
95095
+ optional: false
95096
+ }],
95097
+ "pumpCycle.setEnabled": [{
95098
+ name: "deviceId",
95099
+ form: "single",
95100
+ optional: false
95101
+ }],
95102
+ "pumpCycle.setOffMinutes": [{
95103
+ name: "deviceId",
95104
+ form: "single",
95105
+ optional: false
95106
+ }],
95107
+ "pumpCycle.setOnMinutes": [{
95108
+ name: "deviceId",
95109
+ form: "single",
95110
+ optional: false
95111
+ }],
93436
95112
  "reboot.reboot": [{
93437
95113
  name: "deviceId",
93438
95114
  form: "single",
@@ -93634,6 +95310,11 @@ Object.freeze({
93634
95310
  form: "single",
93635
95311
  optional: true
93636
95312
  }],
95313
+ "remote.getAssumedStatus": [{
95314
+ name: "deviceId",
95315
+ form: "single",
95316
+ optional: false
95317
+ }],
93637
95318
  "remote.getConfig": [{
93638
95319
  name: "deviceId",
93639
95320
  form: "single",
@@ -94181,99 +95862,6 @@ Object.freeze({
94181
95862
  "network-access": "ingress",
94182
95863
  "smtp-provider": "email"
94183
95864
  });
94184
- /**
94185
- * A saved Customize assignment (D936). The schema is the type: callers use
94186
- * `z.infer`, they do not restate the shape.
94187
- */
94188
- var OverrideSourceSchema = object({
94189
- addonId: string().min(1),
94190
- stableId: string().min(1),
94191
- cap: string().min(1),
94192
- fieldPath: string().min(1)
94193
- });
94194
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
94195
- object({ kind: literal("identity") }),
94196
- object({ kind: literal("invert") }),
94197
- object({
94198
- kind: literal("threshold"),
94199
- comparison: _enum(["gte", "lte"]),
94200
- threshold: number(),
94201
- startDwellMs: number().nonnegative(),
94202
- stopDwellMs: number().nonnegative()
94203
- }),
94204
- object({
94205
- kind: literal("linear"),
94206
- scale: number(),
94207
- offset: number(),
94208
- clampMin: number().optional(),
94209
- clampMax: number().optional()
94210
- }),
94211
- object({
94212
- kind: literal("enum-map"),
94213
- mapping: record(string(), string().nullable())
94214
- }),
94215
- object({ kind: literal("number") })
94216
- ]);
94217
- var ScalarOverrideDraftSchema = object({
94218
- kind: literal("scalar"),
94219
- cap: string().min(1),
94220
- targetId: string().min(1),
94221
- source: OverrideSourceSchema,
94222
- transform: CustomizeTransformDraftSchema
94223
- });
94224
- var KeyedArrayOverrideDraftSchema = object({
94225
- kind: literal("keyedArray"),
94226
- cap: string().min(1),
94227
- targetId: string().min(1),
94228
- key: string().min(1),
94229
- label: string().min(1),
94230
- fieldPath: string().min(1),
94231
- source: OverrideSourceSchema,
94232
- transform: CustomizeTransformDraftSchema
94233
- });
94234
- var PulseOverrideDraftSchema = object({
94235
- kind: literal("pulse"),
94236
- cap: string().min(1),
94237
- targetId: string().min(1),
94238
- enabled: boolean(),
94239
- debounceMs: number().nonnegative(),
94240
- edges: array(_enum([
94241
- "rising",
94242
- "button",
94243
- "native-event"
94244
- ])).min(1).optional(),
94245
- sources: array(OverrideSourceSchema).min(1)
94246
- });
94247
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
94248
- "in-addition",
94249
- "confirm",
94250
- "replace"
94251
- ]).optional() });
94252
- discriminatedUnion("kind", [
94253
- ScalarOverrideDraftSchema,
94254
- KeyedArrayOverrideDraftSchema,
94255
- PulseOverrideDraftSchema,
94256
- object({
94257
- kind: literal("span"),
94258
- cap: string().min(1),
94259
- targetId: string().min(1),
94260
- timeoutMs: number().nonnegative(),
94261
- cooldownMs: number().nonnegative().optional(),
94262
- sources: array(SpanSourceDraftSchema)
94263
- })
94264
- ]);
94265
- object({
94266
- targetId: string().min(1),
94267
- configured: boolean(),
94268
- source: _enum(["native", "assigned"]),
94269
- state: _enum([
94270
- "active",
94271
- "stale",
94272
- "released",
94273
- "unavailable"
94274
- ]),
94275
- reason: string().nullable()
94276
- });
94277
95865
  var G711_SCALE_CORRECTION_DB = {
94278
95866
  PCMU: 20 * Math.log10(4),
94279
95867
  PCMA: 20 * Math.log10(8)
@@ -94889,6 +96477,7 @@ var STATE_SUMMARY_VARS = [
94889
96477
  [...RULE_TEXT],
94890
96478
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
94891
96479
  ([...RULE_TEXT], [...COMBINED_ALARM]),
96480
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
94892
96481
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
94893
96482
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
94894
96483
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -95876,6 +97465,140 @@ function buildConnectionFormSchema() {
95876
97465
  }] };
95877
97466
  }
95878
97467
  //#endregion
97468
+ //#region src/rademacher-instances.ts
97469
+ /**
97470
+ * Rademacher integrations as `integration-instances` (planning#416, D985, D1007): one
97471
+ * `connections` record per HomePilot / Start2Smart hub under the `integrations` registry id. The
97472
+ * body IS the connection (host, password in the keychain, base path, poll interval), so the form
97473
+ * the instances page edits is the stored shape and the addon reads nothing else.
97474
+ */
97475
+ var rademacherConnectionsEntryKind = defineEntryKind("connections", {
97476
+ title: "Rademacher hubs",
97477
+ schema: entryBody({
97478
+ host: string().optional(),
97479
+ password: string().optional().meta({ secret: true }),
97480
+ basePath: string().optional(),
97481
+ pollIntervalMs: number().optional()
97482
+ }),
97483
+ writeTrigger: "an operator adds, edits or removes a Rademacher hub connection"
97484
+ });
97485
+ /** Every kind this addon declares, keyed by its name. */
97486
+ var rademacherRecordKinds = { connections: rademacherConnectionsEntryKind };
97487
+ /** The legacy row keys the record carries over; any other key is dropped and named. */
97488
+ var LEGACY_KEYS = [
97489
+ "host",
97490
+ "password",
97491
+ "basePath",
97492
+ "pollIntervalMs"
97493
+ ];
97494
+ function text(value) {
97495
+ return typeof value === "string" && value.length > 0 ? value : void 0;
97496
+ }
97497
+ function whole(value) {
97498
+ if (typeof value === "number" && Number.isFinite(value)) return value;
97499
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
97500
+ return Number.isFinite(parsed) ? parsed : void 0;
97501
+ }
97502
+ /** The record body of one integration, from its legacy rows (secrets resolved). */
97503
+ function rademacherBodyFromRows(values, log, integration) {
97504
+ const dropped = Object.keys(values).filter((key) => !LEGACY_KEYS.includes(key));
97505
+ if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${dropped.join(", ")}`);
97506
+ const host = text(values["host"]);
97507
+ const password = text(values["password"]);
97508
+ const basePath = text(values["basePath"]);
97509
+ const pollIntervalMs = whole(values["pollIntervalMs"]);
97510
+ return {
97511
+ name: integration.name,
97512
+ ...host !== void 0 ? { host } : {},
97513
+ ...password !== void 0 ? { password } : {},
97514
+ ...basePath !== void 0 ? { basePath } : {},
97515
+ ...pollIntervalMs !== void 0 ? { pollIntervalMs } : {}
97516
+ };
97517
+ }
97518
+ /** The whole `provider-rademacher` catalogue (D987): offered to the engine at `onInitialize`. */
97519
+ var RADEMACHER_MIGRATIONS = [{
97520
+ meta: {
97521
+ id: "provider-rademacher/2026-11-05-integration-rows-to-record",
97522
+ owner: "provider-rademacher",
97523
+ level: "addon-state",
97524
+ seq: 1,
97525
+ dependsOn: [],
97526
+ store: "settings",
97527
+ scope: "cluster",
97528
+ onFailure: "block-owner",
97529
+ bodyKind: "procedure",
97530
+ description: "Move every Rademacher hub connection into a typed instance record keyed by its registry id, then clear the legacy integration settings rows and their keychain values",
97531
+ fixtureMark: "a legacy setting key the migration does not know"
97532
+ },
97533
+ procedure: (ports) => moveIntegrationRowsToRecords(ports, {
97534
+ addonId: "provider-rademacher",
97535
+ secretPaths: rademacherConnectionsEntryKind.declaration.secretPaths,
97536
+ bodyFrom: rademacherBodyFromRows
97537
+ })
97538
+ }];
97539
+ /** The hub side and the records side of the move, over `ctx.api` and `ctx.records`. */
97540
+ function rademacherRowsPorts(deps) {
97541
+ return integrationRowsPortsOverApi({
97542
+ api: deps.api,
97543
+ records: deps.records.kind({
97544
+ name: rademacherConnectionsEntryKind.name,
97545
+ declaration: rademacherConnectionsEntryKind.declaration,
97546
+ schema: record(string(), unknown())
97547
+ }),
97548
+ log: { warn: deps.warn }
97549
+ });
97550
+ }
97551
+ /**
97552
+ * The live connection of one instance, read from its record. null while the host is missing; a
97553
+ * locked keychain throws so the caller keeps the client it has instead of reconfiguring it with an
97554
+ * empty password (D49).
97555
+ */
97556
+ async function rademacherConnectionOf(instances, id) {
97557
+ const read = await instances.readResolved(id);
97558
+ if (read === null) return null;
97559
+ if (read.secrets === "unavailable") throw new Error("the keychain is locked, the saved password cannot be read");
97560
+ return connectionFromSettings(read.body);
97561
+ }
97562
+ function typedPassword(value) {
97563
+ return typeof value === "string" && value !== "" && value !== "__camstack_redacted__" ? value : void 0;
97564
+ }
97565
+ function buildRademacherInstances(records, deps) {
97566
+ const handle = records.kind(rademacherConnectionsEntryKind);
97567
+ return new RecordIntegrationInstances(records, rademacherConnectionsEntryKind, {
97568
+ description: {
97569
+ role: "devices",
97570
+ instanceMode: "multiple",
97571
+ kind: "connection",
97572
+ canTest: true
97573
+ },
97574
+ formSchema: () => buildConnectionFormSchema(),
97575
+ initialBody: (name) => ({ name }),
97576
+ configured: (body) => connectionFromSettings(body) !== null,
97577
+ onChanged: async (_id, change) => {
97578
+ if (change !== "deleted") await deps.onChanged();
97579
+ },
97580
+ test: async ({ id, values }) => {
97581
+ const stored = id === null ? null : (await handle.getResolved(id))?.data.password ?? null;
97582
+ const password = typedPassword(values["password"]) ?? stored ?? "";
97583
+ const outcome = await deps.tester.testSettings({ settings: {
97584
+ ...values,
97585
+ password
97586
+ } });
97587
+ if (outcome.outcome === "validated") return {
97588
+ success: true,
97589
+ status: "validated",
97590
+ ...outcome.latencyMs !== void 0 ? { latencyMs: outcome.latencyMs } : {},
97591
+ ...outcome.detail !== void 0 ? { detail: outcome.detail } : {}
97592
+ };
97593
+ return {
97594
+ success: false,
97595
+ status: outcome.outcome,
97596
+ error: outcome.error
97597
+ };
97598
+ }
97599
+ });
97600
+ }
97601
+ //#endregion
95879
97602
  //#region src/cover-mapping.ts
95880
97603
  /** Clamp a raw percentage to the cap's `0..100` integer domain. */
95881
97604
  function clampPercent(value) {
@@ -97930,11 +99653,25 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
97930
99653
  };
97931
99654
  integrationReconciler = null;
97932
99655
  clients = null;
99656
+ instances = null;
97933
99657
  constructor() {
97934
99658
  super({});
97935
99659
  }
97936
99660
  async onInitialize() {
97937
99661
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
99662
+ const records = this.ctx.records;
99663
+ if (records === void 0) throw new Error("provider-rademacher: ctx.records is required (hub-only addon)");
99664
+ await records.declare(rademacherRecordKinds);
99665
+ await this.runMigrations(defineAddonMigrations(RADEMACHER_MIGRATIONS, rademacherRowsPorts({
99666
+ api: defaultIntegrationApiOver(this.ctx.api),
99667
+ records,
99668
+ warn: (message) => this.ctx.logger.warn(message)
99669
+ })));
99670
+ const instances = buildRademacherInstances(records, {
99671
+ onChanged: () => this.reconcileIntegrations(),
99672
+ tester: this.buildConnectionTestProvider()
99673
+ });
99674
+ this.instances = instances;
97938
99675
  this.clients = new RademacherClientRegistry(this.ctx.logger);
97939
99676
  this.clients.setOnConnected((integrationId) => {
97940
99677
  this.setIntegrationDevicesOnline(integrationId, true);
@@ -97953,6 +99690,10 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
97953
99690
  {
97954
99691
  capability: connectionTestCapability,
97955
99692
  provider: this.buildConnectionTestProvider()
99693
+ },
99694
+ {
99695
+ capability: integrationInstancesCapability,
99696
+ provider: instances.toProvider()
97956
99697
  }
97957
99698
  ] };
97958
99699
  }
@@ -97965,11 +99706,9 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
97965
99706
  this.clients = null;
97966
99707
  this.integrationReconciler?.dispose();
97967
99708
  this.integrationReconciler = null;
99709
+ this.instances = null;
97968
99710
  await super.onShutdown();
97969
99711
  }
97970
- globalSettingsSchema() {
97971
- return buildConnectionFormSchema();
97972
- }
97973
99712
  async supportsManualCreation() {
97974
99713
  return false;
97975
99714
  }
@@ -97985,6 +99724,10 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
97985
99724
  if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("provider-rademacher generateStableId: nativeId required");
97986
99725
  return `rademacher:${nativeId}`;
97987
99726
  }
99727
+ requireInstances() {
99728
+ if (!this.instances) throw new Error("Rademacher provider not initialised");
99729
+ return this.instances;
99730
+ }
97988
99731
  requireClients() {
97989
99732
  if (!this.clients) throw new Error("Rademacher provider not initialised");
97990
99733
  return this.clients;
@@ -98001,7 +99744,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
98001
99744
  },
98002
99745
  listIntegrations: () => this.ctx.api.integrations.list.query(),
98003
99746
  applySettings: async (integration) => {
98004
- const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
99747
+ const connection = await rademacherConnectionOf(this.requireInstances(), integration.id);
98005
99748
  if (!connection) return false;
98006
99749
  await this.requireClients().upsert(integration.id, integration.name, connection);
98007
99750
  return true;