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