@camstack/addon-provider-homeassistant 1.2.654 → 1.2.656

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.
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
5980
5980
  var ZodFirstPartyTypeKind;
5981
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
5982
  //#endregion
5983
- //#region ../types/dist/sleep-BKClVk0K.mjs
5983
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
5984
5984
  /**
5985
5985
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
5986
  * window to float samples (D455).
@@ -6075,6 +6075,16 @@ var AddonMigrationsBlockedError = class extends Error {
6075
6075
  this.migrationIds = migrationIds;
6076
6076
  }
6077
6077
  };
6078
+ /**
6079
+ * The value a redacted secret is replaced with on every read surface.
6080
+ *
6081
+ * It is also the WRITE-BACK token: an `upsertLocation` that sends this value
6082
+ * back for a key means "keep what is stored", which is what lets an operator
6083
+ * rename a location without retyping its password. A literal an operator could
6084
+ * plausibly choose as a real password would turn that convenience into a way
6085
+ * to lock yourself out, hence the sentinel shape.
6086
+ */
6087
+ var REDACTED_SECRET = "__camstack_redacted__";
6078
6088
  /** Is this field's stored value a credential? */
6079
6089
  function isSecretConfigField(field) {
6080
6090
  if (field.type === "password") return true;
@@ -6128,9 +6138,16 @@ function isSecretMarker(value) {
6128
6138
  const ref = value["$secret"];
6129
6139
  return typeof ref === "string" && ref.length > 0;
6130
6140
  }
6141
+ /** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
6142
+ function isSecretMarkerLike(value) {
6143
+ return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
6144
+ }
6131
6145
  function isSecretClear(value) {
6132
6146
  return isRecord$7(value) && value["$secretClear"] === true;
6133
6147
  }
6148
+ function isSecretFieldView(value) {
6149
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6150
+ }
6134
6151
  /**
6135
6152
  * The UI view of a stored secret field value: a marker becomes "set", inline
6136
6153
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6780,11 +6797,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6780
6797
  }
6781
6798
  return out;
6782
6799
  }
6800
+ /**
6801
+ * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6802
+ *
6803
+ * A ref is DETERMINISTIC from where the value lives — the owner addon, the
6804
+ * scope (global, one node, one device, one integration, one row of a business
6805
+ * collection) and the field path inside that scope — so saving the same field
6806
+ * twice updates the same ref, and the ref itself says which settings blob
6807
+ * points at it. It is also the AAD of the value's ciphertext: a ciphertext
6808
+ * copied onto another ref does not decrypt.
6809
+ *
6810
+ * Array items are addressed by ITEM ID, never by index
6811
+ * (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
6812
+ *
6813
+ * Pure and browser-safe: no node builtins, no I/O.
6814
+ */
6815
+ /** The URI scheme of every keychain ref. */
6816
+ var SECRET_REF_SCHEME = "secret://";
6783
6817
  var GLOBAL_SCOPE = { kind: "global" };
6784
6818
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6785
6819
  function globalSecretScope() {
6786
6820
  return GLOBAL_SCOPE;
6787
6821
  }
6822
+ /**
6823
+ * An addon id as a ref owner. A hub in-process builtin's context id is
6824
+ * `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
6825
+ * addon running forked.
6826
+ */
6827
+ function normalizeSecretOwner(addonId) {
6828
+ return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
6829
+ }
6788
6830
  function encodeSegment(value) {
6789
6831
  return encodeURIComponent(value);
6790
6832
  }
@@ -6795,6 +6837,16 @@ function decodeSegment(value) {
6795
6837
  return null;
6796
6838
  }
6797
6839
  }
6840
+ /** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
6841
+ function formatSecretScope(scope) {
6842
+ switch (scope.kind) {
6843
+ case "global": return "global";
6844
+ case "node": return `node/${encodeSegment(scope.nodeId)}`;
6845
+ case "device": return `device/${scope.deviceId}`;
6846
+ case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
6847
+ case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
6848
+ }
6849
+ }
6798
6850
  /**
6799
6851
  * The field path of a value inside an array item, addressed by the item's id:
6800
6852
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6803,6 +6855,13 @@ function decodeSegment(value) {
6803
6855
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6804
6856
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6805
6857
  }
6858
+ /** Build a ref. Throws on an empty owner or field path — both are bugs. */
6859
+ function buildSecretRef(parts) {
6860
+ const owner = normalizeSecretOwner(parts.owner);
6861
+ if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
6862
+ if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
6863
+ return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
6864
+ }
6806
6865
  function parseScope(segments) {
6807
6866
  switch (segments[0]) {
6808
6867
  case "global": return {
@@ -8068,6 +8127,10 @@ var BaseAddon = class {
8068
8127
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8069
8128
  const patch = {};
8070
8129
  for (const key of keys) {
8130
+ if (tree.get(key)?.kind === "array") {
8131
+ patch[key] = [];
8132
+ continue;
8133
+ }
8071
8134
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8072
8135
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8073
8136
  }
@@ -8719,6 +8782,13 @@ var RecordsError = class extends Error {
8719
8782
  this.detail = detail;
8720
8783
  }
8721
8784
  };
8785
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8786
+ var RevConflictError = class extends RecordsError {
8787
+ constructor(detail) {
8788
+ super("rev-conflict", detail);
8789
+ this.name = "RevConflictError";
8790
+ }
8791
+ };
8722
8792
  /** The keychain could not store or serve a value: explicit, never an empty string. */
8723
8793
  var RecordSecretUnavailableError = class extends RecordsError {
8724
8794
  constructor(detail) {
@@ -8756,11 +8826,17 @@ var RECORD_BATCH_MAX = 1e3;
8756
8826
  var RECORD_QUERY_MAX = 1e3;
8757
8827
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8758
8828
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8829
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8830
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8759
8831
  /**
8760
8832
  * Secret fields of a record, addressed the way a Zod schema declares them
8761
8833
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8762
8834
  * (`auth.accessToken`, `headers[].value`).
8763
8835
  *
8836
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8837
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8838
+ * key releases exactly that key's secret.
8839
+ *
8764
8840
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8765
8841
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8766
8842
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8773,11 +8849,13 @@ function parseSecretPath(path) {
8773
8849
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8774
8850
  return path.split(".").map((raw) => {
8775
8851
  const each = raw.endsWith("[]");
8776
- const key = each ? raw.slice(0, -2) : raw;
8852
+ const map = raw.endsWith("{}");
8853
+ const key = each || map ? raw.slice(0, -2) : raw;
8777
8854
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8778
8855
  return {
8779
8856
  key,
8780
- each
8857
+ each,
8858
+ map
8781
8859
  };
8782
8860
  });
8783
8861
  }
@@ -8792,6 +8870,128 @@ function secretArrayPrefixes(path) {
8792
8870
  }
8793
8871
  return prefixes;
8794
8872
  }
8873
+ function isRecord$4(value) {
8874
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8875
+ }
8876
+ function elementId(element, idKey, declared, prefix) {
8877
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8878
+ if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
8879
+ return id;
8880
+ }
8881
+ /**
8882
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8883
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8884
+ * result is a new body, `data` is untouched.
8885
+ */
8886
+ var REMOVE_LEAF = Symbol("remove-leaf");
8887
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8888
+ if (!isRecord$4(node)) return node;
8889
+ const segment = segments[index];
8890
+ if (segment === void 0) return node;
8891
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8892
+ const child = node[segment.key];
8893
+ const keys = [...walkedKeys, segment.key];
8894
+ const last = index === segments.length - 1;
8895
+ if (segment.each) {
8896
+ if (!Array.isArray(child)) return node;
8897
+ const prefix = keys.join(".");
8898
+ const idKey = arrayKeys[prefix];
8899
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8900
+ const mapped = child.map((element) => {
8901
+ const id = elementId(element, idKey, declared, prefix);
8902
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8903
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8904
+ });
8905
+ return {
8906
+ ...node,
8907
+ [segment.key]: mapped
8908
+ };
8909
+ }
8910
+ if (segment.map) {
8911
+ if (!isRecord$4(child)) return node;
8912
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8913
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8914
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8915
+ return [mapKey, visit({
8916
+ declared,
8917
+ concrete,
8918
+ value
8919
+ })];
8920
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8921
+ return {
8922
+ ...node,
8923
+ [segment.key]: Object.fromEntries(kept)
8924
+ };
8925
+ }
8926
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8927
+ if (last) {
8928
+ const replacement = visit({
8929
+ declared,
8930
+ concrete,
8931
+ value: child
8932
+ });
8933
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8934
+ return {
8935
+ ...node,
8936
+ [segment.key]: replacement
8937
+ };
8938
+ }
8939
+ return {
8940
+ ...node,
8941
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8942
+ };
8943
+ }
8944
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8945
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8946
+ let current = data;
8947
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8948
+ return current;
8949
+ }
8950
+ /**
8951
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8952
+ * record's refs never collide with the refs a legacy collection of the same name
8953
+ * already holds during an adoption.
8954
+ */
8955
+ function recordSecretCollection(kind) {
8956
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8957
+ }
8958
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8959
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8960
+ return buildSecretRef({
8961
+ owner,
8962
+ scope: {
8963
+ kind: "row",
8964
+ collection: recordSecretCollection(kind),
8965
+ rowId
8966
+ },
8967
+ fieldPath: concretePath
8968
+ });
8969
+ }
8970
+ /**
8971
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8972
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8973
+ * token. The form hands it to the secret field, which reveals the value by ref
8974
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8975
+ * stored secret (the records door reads a view as KEEP).
8976
+ *
8977
+ * ONE implementation for the entries service and for every addon that serves
8978
+ * its own instances (`RecordIntegrationInstances`).
8979
+ */
8980
+ function isRecord$3(value) {
8981
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8982
+ }
8983
+ function secretViewsOf(owner, kind, id, decl, data) {
8984
+ if (decl.secretPaths.length === 0) return data;
8985
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8986
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8987
+ return {
8988
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8989
+ v: 1,
8990
+ set: true
8991
+ };
8992
+ });
8993
+ return isRecord$3(shown) ? shown : data;
8994
+ }
8795
8995
  /**
8796
8996
  * The share-view scope kinds a share-link token can carry. The hub mints
8797
8997
  * and validates them (`ShareViewGrantSchema` builds its
@@ -8864,7 +9064,8 @@ function isZodSecret(schema) {
8864
9064
  }
8865
9065
  /**
8866
9066
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
8867
- * element of this array" (`connection.password`, `rows[].token`). A declared
9067
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9068
+ * map" (`applianceSecrets{}`). A declared
8868
9069
  * object or array is reported as one path and not descended into — everything
8869
9070
  * under it is secret.
8870
9071
  */
@@ -8875,6 +9076,7 @@ function collectZodSecretPaths(schema, prefix = "") {
8875
9076
  const def = defOf(layer);
8876
9077
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
8877
9078
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9079
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
8878
9080
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
8879
9081
  else if (def.type === "intersection") {
8880
9082
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -9014,7 +9216,7 @@ function defineRecordKinds(specs) {
9014
9216
  retention: spec.retention,
9015
9217
  deviceRemoval: spec.deviceRemoval,
9016
9218
  maxDataBytes: spec.maxDataBytes,
9017
- secretPaths: [...secretPaths],
9219
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9018
9220
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9019
9221
  transactFamily: spec.transactFamily,
9020
9222
  entry: spec.entry === void 0 ? void 0 : {
@@ -9059,6 +9261,7 @@ function defineEntryKind(name, spec) {
9059
9261
  ...spec.database === void 0 ? {} : { database: spec.database },
9060
9262
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9061
9263
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9264
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9062
9265
  entry: {
9063
9266
  title: spec.title,
9064
9267
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11786,7 +11989,7 @@ object({
11786
11989
  params: record(string(), union([string(), number()])).optional()
11787
11990
  });
11788
11991
  //#endregion
11789
- //#region ../types/dist/composition-BodkvOV0.mjs
11992
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11790
11993
  /**
11791
11994
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11792
11995
  *
@@ -12549,38 +12752,6 @@ function validateExpressionSource(src) {
12549
12752
  return null;
12550
12753
  }
12551
12754
  /**
12552
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12553
- *
12554
- * Standby of the control board is under 5 W; a motor or a heater is far
12555
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12556
- * a cycle, so running starts only after 30 s above the line.
12557
- *
12558
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12559
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12560
- * report "finished" in the middle of the program, so the stop dwell is
12561
- * 10 minutes on every type. The number is stored per type so one machine
12562
- * can move later without moving the others.
12563
- */
12564
- var APPLIANCE_TEMPLATE_ID = "appliance";
12565
- var APPLIANCE_KINDS = [
12566
- "washer",
12567
- "dryer",
12568
- "dishwasher"
12569
- ];
12570
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12571
- /**
12572
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12573
- *
12574
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12575
- * (D967), so the heater switches exactly as the plug does. Running is the
12576
- * plug being on; when a power reading is also named, running is the draw
12577
- * staying above the line instead (a plug left on with the stove's own
12578
- * thermostat open is not heating). 50 W is far above any standby and below
12579
- * the smallest radiator.
12580
- */
12581
- var HEATER_TEMPLATE_ID = "heater";
12582
- DeviceType.Heater;
12583
- /**
12584
12755
  * Statistics and integrals over a history read (D944).
12585
12756
  *
12586
12757
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12724,34 +12895,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12724
12895
  /** Actuator groups only. Readings leave this unset. */
12725
12896
  active: ActiveCriterionSchema.optional()
12726
12897
  }),
12727
- object({
12728
- kind: literal("appliance"),
12729
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12730
- appliance: _enum(APPLIANCE_KINDS),
12731
- thresholdWatts: number().positive(),
12732
- startDwellMs: number().int().nonnegative(),
12733
- stopDwellMs: number().int().nonnegative(),
12734
- member: HelperMemberSchema,
12735
- /**
12736
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12737
- * composition carries a `power-source` feature, so the hub can cut and restore
12738
- * that plug (power-source link of a NEW-target composition).
12739
- */
12740
- powerSwitch: HelperMemberSchema.optional(),
12741
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12742
- offSeconds: number().optional()
12743
- }),
12744
- object({
12745
- kind: literal("heater"),
12746
- templateId: literal(HEATER_TEMPLATE_ID),
12747
- /** The plug, read and commanded at `switch.on`. */
12748
- plug: HelperMemberSchema,
12749
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12750
- meter: HelperMemberSchema.optional(),
12751
- thresholdWatts: number().positive(),
12752
- startDwellMs: number().int().nonnegative(),
12753
- stopDwellMs: number().int().nonnegative()
12754
- }),
12755
12898
  object({
12756
12899
  kind: literal("statistic"),
12757
12900
  stat: _enum(STATISTIC_STATS),
@@ -12975,7 +13118,7 @@ var CompositionSchema = object({
12975
13118
  kind: literal("existing"),
12976
13119
  device: CompositionSourceRefSchema
12977
13120
  })]),
12978
- features: array(CompositionFeatureSchema).min(1).max(16),
13121
+ features: array(CompositionFeatureSchema).min(0).max(16),
12979
13122
  /**
12980
13123
  * Present when the block is a helper template. Absent on a free
12981
13124
  * composition (the room example, device 4603). The editor reads this,
@@ -13310,6 +13453,205 @@ function buildCapturingReply(envelope) {
13310
13453
  };
13311
13454
  return wrapper;
13312
13455
  }
13456
+ /**
13457
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13458
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13459
+ *
13460
+ * Every instance is one record of the addon's own kind, keyed by the
13461
+ * `integrations` registry id. The records door does the secret work: a field the
13462
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13463
+ * deleting the row deletes the secret, and one row's value is never another's.
13464
+ *
13465
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13466
+ * token or the view the form was given KEEP the stored secret;
13467
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13468
+ */
13469
+ /** The most instances one addon lists (an integrations page, not a table). */
13470
+ var INSTANCES_LIST_MAX = 1e3;
13471
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13472
+ function topLevelSecretKeys(paths) {
13473
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13474
+ }
13475
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13476
+ function secretMapKeys(paths) {
13477
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13478
+ }
13479
+ /**
13480
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13481
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13482
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13483
+ */
13484
+ function secretMapPatch(value) {
13485
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13486
+ const out = {};
13487
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13488
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13489
+ return Object.keys(out).length > 0 ? out : null;
13490
+ }
13491
+ /** Does the kind's body schema carry an `enabled` field? */
13492
+ function declaresEnabled(kind) {
13493
+ const json = toJSONSchema(kind.schema, {
13494
+ unrepresentable: "any",
13495
+ io: "input"
13496
+ });
13497
+ const properties = Reflect.get(json, "properties");
13498
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13499
+ }
13500
+ var RecordIntegrationInstances = class {
13501
+ records;
13502
+ kind;
13503
+ hooks;
13504
+ handle;
13505
+ secretKeys;
13506
+ secretMaps;
13507
+ nameField;
13508
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13509
+ registryOwnsEnabled;
13510
+ constructor(records, kind, hooks) {
13511
+ this.records = records;
13512
+ this.kind = kind;
13513
+ this.hooks = hooks;
13514
+ this.handle = records.kind(kind);
13515
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13516
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13517
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13518
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13519
+ 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"`);
13520
+ }
13521
+ /**
13522
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13523
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13524
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13525
+ */
13526
+ async readResolved(id) {
13527
+ try {
13528
+ const record = await this.handle.getResolved(id);
13529
+ return record === null ? null : {
13530
+ id,
13531
+ body: record.data,
13532
+ enabled: record.data.enabled !== false,
13533
+ secrets: "resolved"
13534
+ };
13535
+ } catch (err) {
13536
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13537
+ const stored = await this.handle.get(id);
13538
+ if (stored === null) return null;
13539
+ return {
13540
+ id,
13541
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13542
+ enabled: stored.data.enabled !== false,
13543
+ secrets: "unavailable"
13544
+ };
13545
+ }
13546
+ }
13547
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13548
+ async getResolved(id) {
13549
+ return (await this.readResolved(id))?.body ?? null;
13550
+ }
13551
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13552
+ async listResolved() {
13553
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13554
+ const out = [];
13555
+ for (const record of page.records) {
13556
+ const read = await this.readResolved(record.id);
13557
+ if (read !== null) out.push(read);
13558
+ }
13559
+ return out;
13560
+ }
13561
+ viewOf(record) {
13562
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13563
+ const rawName = record.data[this.nameField];
13564
+ const kind = this.hooks.kindOf?.(record.data);
13565
+ const health = this.hooks.health?.(record.data, record.id);
13566
+ return {
13567
+ id: record.id,
13568
+ name: typeof rawName === "string" ? rawName : record.id,
13569
+ configured: this.hooks.configured(record.data),
13570
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13571
+ formValues: shown,
13572
+ ...kind !== void 0 ? { kind } : {},
13573
+ ...health?.status !== void 0 ? { status: health.status } : {},
13574
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13575
+ };
13576
+ }
13577
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13578
+ ...this.hooks.description,
13579
+ mintsId: true
13580
+ };
13581
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13582
+ listInstances = async () => {
13583
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13584
+ };
13585
+ getInstance = async ({ id }) => {
13586
+ const record = await this.handle.get(id);
13587
+ return record === null ? null : this.viewOf(record);
13588
+ };
13589
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13590
+ createInstance = async ({ id, name, kind }) => {
13591
+ const minted = id ?? this.hooks.mintId?.({
13592
+ name,
13593
+ ...kind !== void 0 ? { kind } : {}
13594
+ });
13595
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13596
+ await this.handle.put({
13597
+ id: minted,
13598
+ data: this.hooks.initialBody(name, kind)
13599
+ }, { ifRev: 0 });
13600
+ await this.hooks.onChanged?.(minted, "created");
13601
+ return { id: minted };
13602
+ };
13603
+ updateInstance = async ({ id, name, values }) => {
13604
+ const patch = {};
13605
+ for (const [key, value] of Object.entries(values)) {
13606
+ if (value === void 0) continue;
13607
+ if (this.secretMaps.has(key)) {
13608
+ const entries = secretMapPatch(value);
13609
+ if (entries !== null) patch[key] = entries;
13610
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13611
+ else if (isSecretClear(value)) patch[key] = null;
13612
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13613
+ }
13614
+ if (name !== void 0) patch[this.nameField] = name;
13615
+ await this.handle.patch(id, patch);
13616
+ await this.hooks.onChanged?.(id, "updated");
13617
+ return { ok: true };
13618
+ };
13619
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13620
+ setInstanceEnabled = async ({ id, enabled }) => {
13621
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13622
+ await this.handle.patch(id, { enabled });
13623
+ await this.hooks.onChanged?.(id, "enabled");
13624
+ return { ok: true };
13625
+ };
13626
+ /** The row and its keychain refs go together (the records door). */
13627
+ deleteInstance = async ({ id }) => {
13628
+ await this.handle.delete(id);
13629
+ await this.hooks.onChanged?.(id, "deleted");
13630
+ return { ok: true };
13631
+ };
13632
+ testInstance = async (input) => {
13633
+ if (this.hooks.test === void 0) return {
13634
+ success: false,
13635
+ status: "unsupported",
13636
+ error: "This integration declares no connection test"
13637
+ };
13638
+ return this.hooks.test(input);
13639
+ };
13640
+ /** The flat provider object to hand to `registerProvider`. */
13641
+ toProvider() {
13642
+ return {
13643
+ describe: this.describe,
13644
+ instanceFormSchema: this.instanceFormSchema,
13645
+ listInstances: this.listInstances,
13646
+ getInstance: this.getInstance,
13647
+ createInstance: this.createInstance,
13648
+ updateInstance: this.updateInstance,
13649
+ setInstanceEnabled: this.setInstanceEnabled,
13650
+ deleteInstance: this.deleteInstance,
13651
+ testInstance: this.testInstance
13652
+ };
13653
+ }
13654
+ };
13313
13655
  var EncodeProfileSchema = object({
13314
13656
  video: object({
13315
13657
  codec: _enum([
@@ -14415,6 +14757,58 @@ var MigrationOwnerStepSchema = object({
14415
14757
  /** The ledger state the engine found: `pending` has no row yet. */
14416
14758
  state: union([literal("pending"), MigrationStatusSchema])
14417
14759
  });
14760
+ function isPlain(value) {
14761
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14762
+ }
14763
+ async function flattenNestedRecords(ports, spec) {
14764
+ const ids = await ports.listNested(spec.legacyKey);
14765
+ if (ids.length === 0) return {
14766
+ outcome: "already-done",
14767
+ summary: { converted: 0 }
14768
+ };
14769
+ const rows = /* @__PURE__ */ new Map();
14770
+ for (const id of ids) try {
14771
+ rows.set(id, await ports.readResolved(id));
14772
+ } catch (err) {
14773
+ const why = err instanceof Error ? err.message : String(err);
14774
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14775
+ }
14776
+ for (const [id, row] of rows) {
14777
+ const legacy = row[spec.legacyKey];
14778
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14779
+ const flat = spec.toFlat(legacy, row);
14780
+ await ports.patch(id, flat);
14781
+ const back = await ports.readResolved(id);
14782
+ const wrong = spec.flatSecretKeys.filter((key) => {
14783
+ const sent = flat[key];
14784
+ const got = back[key];
14785
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14786
+ });
14787
+ if (wrong.length > 0) {
14788
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14789
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14790
+ }
14791
+ await ports.patch(id, { [spec.legacyKey]: null });
14792
+ }
14793
+ return {
14794
+ outcome: "done",
14795
+ summary: { converted: rows.size }
14796
+ };
14797
+ }
14798
+ /** The ports over one kind's records handle. */
14799
+ function flattenPortsOver(handle) {
14800
+ return {
14801
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14802
+ readResolved: async (id) => {
14803
+ const row = await handle.getResolved(id);
14804
+ if (row === null) throw new Error("the record is gone");
14805
+ return row.data;
14806
+ },
14807
+ patch: async (id, patch) => {
14808
+ await handle.patch(id, patch);
14809
+ }
14810
+ };
14811
+ }
14418
14812
  /**
14419
14813
  * The trainer contract — what the hub hands the cloud trainer and what it
14420
14814
  * gets back (cloud training, D805+).
@@ -50287,117 +50681,6 @@ function actuator(input) {
50287
50681
  };
50288
50682
  }
50289
50683
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
50290
- var PowerSourceRefusalReasonSchema = _enum([
50291
- "no-power-source",
50292
- "switch-missing",
50293
- "switch-offline",
50294
- "switch-unknown",
50295
- "restart-in-progress",
50296
- "switch-call-failed"
50297
- ]);
50298
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
50299
- accepted: literal(false),
50300
- reason: PowerSourceRefusalReasonSchema,
50301
- /** One line for the operator, naming the switch. */
50302
- message: string()
50303
- })]);
50304
- var PowerSourceInfoSchema = object({
50305
- /** False when the device has no power source linked; the rest is then empty. */
50306
- linked: boolean(),
50307
- switchDeviceId: number().nullable(),
50308
- switchName: string().nullable(),
50309
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
50310
- switchOn: boolean().nullable(),
50311
- switchOnline: boolean().nullable(),
50312
- restartInProgress: boolean(),
50313
- offSeconds: number().int()
50314
- });
50315
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
50316
- var PowerSourceStateSchema = object({
50317
- /** Null while the switch has not reported: unknown is never `false` (D393). */
50318
- switchOn: boolean().nullable(),
50319
- offSeconds: number().int(),
50320
- lastFetchedAt: number()
50321
- });
50322
- var POWER_SOURCE_DEVICE_TYPES = [
50323
- DeviceType.Gateway,
50324
- DeviceType.Hub,
50325
- DeviceType.Camera,
50326
- DeviceType.Sensor,
50327
- DeviceType.MediaPlayer,
50328
- DeviceType.Vacuum,
50329
- DeviceType.Generic,
50330
- DeviceType.Washer,
50331
- DeviceType.Dryer,
50332
- DeviceType.Dishwasher,
50333
- DeviceType.PetFountain,
50334
- DeviceType.Fan,
50335
- DeviceType.Humidifier,
50336
- DeviceType.Climate,
50337
- DeviceType.WaterHeater
50338
- ];
50339
- var powerSourceCustomize = {
50340
- version: 1,
50341
- targets: [{
50342
- id: "switch",
50343
- cardinality: "scalar",
50344
- vocabulary: {
50345
- cap: "power-source",
50346
- stateField: "switchOn"
50347
- },
50348
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
50349
- apply: "add",
50350
- path: "switchOn",
50351
- sources: [{
50352
- kind: "switch",
50353
- transforms: [{ kind: "identity" }]
50354
- }],
50355
- companions: [{
50356
- kind: "constant",
50357
- path: "offSeconds",
50358
- value: 10,
50359
- when: "add"
50360
- }, {
50361
- kind: "clock",
50362
- path: "lastFetchedAt",
50363
- when: "always"
50364
- }],
50365
- sourceReport: { status: "assignment-status" }
50366
- }]
50367
- };
50368
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
50369
- var powerSourceCapability = {
50370
- name: "power-source",
50371
- scope: "device",
50372
- mode: "singleton",
50373
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
50374
- methods: {
50375
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
50376
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50377
- kind: "mutation",
50378
- auth: "admin"
50379
- }),
50380
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50381
- kind: "mutation",
50382
- auth: "admin"
50383
- }),
50384
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50385
- kind: "mutation",
50386
- auth: "admin"
50387
- })
50388
- },
50389
- runtimeState: PowerSourceStateSchema,
50390
- customize: powerSourceCustomize,
50391
- /**
50392
- * Runtime-state durability: **session** — mirrored from the linked switch on
50393
- * its next report; a restored value would outlive the reading it names.
50394
- *
50395
- * See `RuntimeStateDurability`. Enforced by
50396
- * `scripts/check-runtime-state-durability.ts`.
50397
- */
50398
- durability: "session",
50399
- history: "none"
50400
- };
50401
50684
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
50402
50685
  DeviceType.Camera;
50403
50686
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -50425,6 +50708,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
50425
50708
  }).optional()
50426
50709
  })) });
50427
50710
  /**
50711
+ * A saved Customize assignment (D936). The schema is the type: callers use
50712
+ * `z.infer`, they do not restate the shape.
50713
+ */
50714
+ var OverrideSourceSchema = object({
50715
+ addonId: string().min(1),
50716
+ stableId: string().min(1),
50717
+ cap: string().min(1),
50718
+ fieldPath: string().min(1)
50719
+ });
50720
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50721
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50722
+ var ThresholdTransformDraftSchema = object({
50723
+ kind: literal("threshold"),
50724
+ comparison: _enum(["gte", "lte"]),
50725
+ threshold: number(),
50726
+ startDwellMs: number().nonnegative(),
50727
+ stopDwellMs: number().nonnegative()
50728
+ });
50729
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50730
+ IdentityTransformDraftSchema,
50731
+ InvertTransformDraftSchema,
50732
+ ThresholdTransformDraftSchema,
50733
+ object({
50734
+ kind: literal("linear"),
50735
+ scale: number(),
50736
+ offset: number(),
50737
+ clampMin: number().optional(),
50738
+ clampMax: number().optional()
50739
+ }),
50740
+ object({
50741
+ kind: literal("enum-map"),
50742
+ mapping: record(string(), string().nullable())
50743
+ }),
50744
+ object({ kind: literal("number") })
50745
+ ]);
50746
+ var ScalarOverrideDraftSchema = object({
50747
+ kind: literal("scalar"),
50748
+ cap: string().min(1),
50749
+ targetId: string().min(1),
50750
+ source: OverrideSourceSchema,
50751
+ transform: CustomizeTransformDraftSchema
50752
+ });
50753
+ var KeyedArrayOverrideDraftSchema = object({
50754
+ kind: literal("keyedArray"),
50755
+ cap: string().min(1),
50756
+ targetId: string().min(1),
50757
+ key: string().min(1),
50758
+ label: string().min(1),
50759
+ fieldPath: string().min(1),
50760
+ source: OverrideSourceSchema,
50761
+ transform: CustomizeTransformDraftSchema
50762
+ });
50763
+ var PulseOverrideDraftSchema = object({
50764
+ kind: literal("pulse"),
50765
+ cap: string().min(1),
50766
+ targetId: string().min(1),
50767
+ enabled: boolean(),
50768
+ debounceMs: number().nonnegative(),
50769
+ edges: array(_enum([
50770
+ "rising",
50771
+ "button",
50772
+ "native-event"
50773
+ ])).min(1).optional(),
50774
+ sources: array(OverrideSourceSchema).min(1)
50775
+ });
50776
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50777
+ "in-addition",
50778
+ "confirm",
50779
+ "replace"
50780
+ ]).optional() });
50781
+ discriminatedUnion("kind", [
50782
+ ScalarOverrideDraftSchema,
50783
+ KeyedArrayOverrideDraftSchema,
50784
+ PulseOverrideDraftSchema,
50785
+ object({
50786
+ kind: literal("span"),
50787
+ cap: string().min(1),
50788
+ targetId: string().min(1),
50789
+ timeoutMs: number().nonnegative(),
50790
+ cooldownMs: number().nonnegative().optional(),
50791
+ sources: array(SpanSourceDraftSchema)
50792
+ })
50793
+ ]);
50794
+ object({
50795
+ targetId: string().min(1),
50796
+ configured: boolean(),
50797
+ source: _enum(["native", "assigned"]),
50798
+ state: _enum([
50799
+ "active",
50800
+ "stale",
50801
+ "released",
50802
+ "unavailable"
50803
+ ]),
50804
+ reason: string().nullable()
50805
+ });
50806
+ /**
50807
+ * What a running-threshold proposal says (planning#405). The schema is the
50808
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50809
+ *
50810
+ * A proposal is read-only advice: it carries the numbers it was derived from
50811
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50812
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50813
+ */
50814
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50815
+ var RunningProposalConfidenceSchema = _enum([
50816
+ "high",
50817
+ "medium",
50818
+ "low"
50819
+ ]);
50820
+ var RunningProposalInsufficientReasonSchema = _enum([
50821
+ "no-history",
50822
+ "span-too-short",
50823
+ "truncated",
50824
+ "no-idle-floor",
50825
+ "no-runs",
50826
+ "no-separation"
50827
+ ]);
50828
+ var RunningProposalReplaySchema = object({
50829
+ /** Intervals the real dwell expression held true over the window. */
50830
+ intervals: number().int().nonnegative(),
50831
+ /** Intervals outside every run, or a second interval inside one. */
50832
+ extra: number().int().nonnegative(),
50833
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50834
+ missed: number().int().nonnegative()
50835
+ });
50836
+ var RunningProposalEvidenceSchema = object({
50837
+ tier: RunningProposalTierSchema,
50838
+ spanMs: number().nonnegative(),
50839
+ from: number(),
50840
+ to: number(),
50841
+ points: number().int().nonnegative(),
50842
+ runsFound: number().int().nonnegative(),
50843
+ pausesFound: number().int().nonnegative(),
50844
+ longestPauseMs: number().nonnegative(),
50845
+ idleFloorW: number(),
50846
+ noiseCeilingW: number(),
50847
+ plateauW: number(),
50848
+ replay: RunningProposalReplaySchema
50849
+ });
50850
+ var RunningProposalInsufficientEvidenceSchema = object({
50851
+ spanMs: number().nonnegative(),
50852
+ points: number().int().nonnegative(),
50853
+ runsFound: number().int().nonnegative()
50854
+ });
50855
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50856
+ state: literal("proposal"),
50857
+ transform: ThresholdTransformDraftSchema,
50858
+ evidence: RunningProposalEvidenceSchema,
50859
+ confidence: RunningProposalConfidenceSchema,
50860
+ /** Why the confidence is not higher; empty when nothing held it back. */
50861
+ notes: array(string())
50862
+ }), object({
50863
+ state: literal("insufficient"),
50864
+ reason: RunningProposalInsufficientReasonSchema,
50865
+ evidence: RunningProposalInsufficientEvidenceSchema,
50866
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50867
+ fallback: ThresholdTransformDraftSchema
50868
+ })]);
50869
+ /**
50428
50870
  * The report of a one-shot "move this owner's plaintext secrets into the
50429
50871
  * keychain" run (D878). COUNTS only - never a value, never a ref.
50430
50872
  *
@@ -50714,6 +51156,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
50714
51156
  kind: "mutation",
50715
51157
  auth: "admin",
50716
51158
  access: "view"
51159
+ }), method(object({
51160
+ deviceId: number().int(),
51161
+ cap: string().min(1),
51162
+ fieldPath: string().min(1),
51163
+ deviceType: _enum(DeviceType).optional()
51164
+ }), RunningThresholdProposalSchema, {
51165
+ auth: "admin",
51166
+ access: "view"
50717
51167
  }), method(object({
50718
51168
  blockId: string().min(1),
50719
51169
  trace: AutomationRunTraceSchema.optional(),
@@ -51690,7 +52140,46 @@ method(object({
51690
52140
  * exposes only the COMMON surface: link state, supported device
51691
52141
  * kinds (so the UI can filter the device picker), and per-device
51692
52142
  * expose/unexpose.
52143
+ *
52144
+ * INSTANCES. One export addon can serve several independent targets (a
52145
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
52146
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
52147
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
52148
+ * default instance's); present = that instance; an id the provider does not
52149
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
52150
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
52151
+ * change always names the target it changes. A single-instance provider
52152
+ * accepts an absent id on a read or its own id, and refuses any other.
52153
+ */
52154
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
52155
+ var InstanceIdSchema$1 = string().min(1);
52156
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
52157
+ /** One independent export target of an addon. */
52158
+ var ExportInstanceSchema = object({
52159
+ instanceId: InstanceIdSchema$1,
52160
+ name: string(),
52161
+ enabled: boolean(),
52162
+ kind: string().optional()
52163
+ });
52164
+ var EXPORT_INSTANCE_UNKNOWN = "export-instance-unknown";
52165
+ /** The named failure of an `instanceId` the provider does not serve. The message starts with the code. */
52166
+ var ExportInstanceUnknownError = class extends Error {
52167
+ code = EXPORT_INSTANCE_UNKNOWN;
52168
+ instanceId;
52169
+ constructor(addon, instanceId) {
52170
+ super(`${EXPORT_INSTANCE_UNKNOWN}: ${addon} serves no instance "${instanceId}"`);
52171
+ this.name = "ExportInstanceUnknownError";
52172
+ this.instanceId = instanceId;
52173
+ }
52174
+ };
52175
+ /**
52176
+ * The instance a `getStatus` call names. The cap's `status` block injects a per-device
52177
+ * `getStatus({ deviceId })` beside the method's own `{ instanceId? }`, so the provider sees
52178
+ * both shapes; only the second names an instance (a per-device status read is addon-wide).
51693
52179
  */
52180
+ function statusInstanceOf(input) {
52181
+ return "instanceId" in input ? input.instanceId : void 0;
52182
+ }
51694
52183
  var LinkStateSchema = _enum([
51695
52184
  "unlinked",
51696
52185
  "linked",
@@ -51741,10 +52230,14 @@ var ExposedDeviceSchema = object({
51741
52230
  capabilities: array(string()).optional()
51742
52231
  });
51743
52232
  var ExposeInputSchema = object({
52233
+ instanceId: InstanceIdSchema$1,
51744
52234
  deviceId: string(),
51745
52235
  capabilities: array(string()).optional()
51746
52236
  });
51747
- var UnexposeInputSchema = object({ deviceId: string() });
52237
+ var UnexposeInputSchema = object({
52238
+ instanceId: InstanceIdSchema$1,
52239
+ deviceId: string()
52240
+ });
51748
52241
  /**
51749
52242
  * What an exporter may expose besides devices (D968). An action and a
51750
52243
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -51773,8 +52266,14 @@ var ExposedEntitySchema = object({
51773
52266
  entity: ExportEntityRefSchema,
51774
52267
  exposedAs: string().optional()
51775
52268
  });
51776
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51777
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
52269
+ var ExposeEntityInputSchema = object({
52270
+ instanceId: InstanceIdSchema$1,
52271
+ entity: ExportEntityRefSchema
52272
+ });
52273
+ var UnexposeEntityInputSchema = object({
52274
+ instanceId: InstanceIdSchema$1,
52275
+ entity: ExportEntityRefSchema
52276
+ });
51778
52277
  var deviceExportCapability = {
51779
52278
  name: "device-export",
51780
52279
  scope: "system",
@@ -51794,9 +52293,10 @@ var deviceExportCapability = {
51794
52293
  */
51795
52294
  exposesDeviceSettings: true,
51796
52295
  methods: {
51797
- getStatus: method(_void(), DeviceExportStatusSchema),
51798
- listSupportedDeviceKinds: method(_void(), array(DeviceKindSchema)),
51799
- listExposedDevices: method(_void(), array(ExposedDeviceSchema)),
52296
+ getStatus: method(InstancePinSchema, DeviceExportStatusSchema),
52297
+ listInstances: method(_void(), array(ExportInstanceSchema)),
52298
+ listSupportedDeviceKinds: method(InstancePinSchema, array(DeviceKindSchema)),
52299
+ listExposedDevices: method(InstancePinSchema, array(ExposedDeviceSchema)),
51800
52300
  exposeDevice: method(ExposeInputSchema, _void(), {
51801
52301
  kind: "mutation",
51802
52302
  auth: "admin"
@@ -51806,7 +52306,7 @@ var deviceExportCapability = {
51806
52306
  auth: "admin"
51807
52307
  }),
51808
52308
  listSupportedEntityKinds: optionalMethod(_void(), array(ExportEntityKindSchema)),
51809
- listExposedEntities: optionalMethod(_void(), array(ExposedEntitySchema)),
52309
+ listExposedEntities: optionalMethod(InstancePinSchema, array(ExposedEntitySchema)),
51810
52310
  exposeEntity: optionalMethod(ExposeEntityInputSchema, _void(), {
51811
52311
  kind: "mutation",
51812
52312
  auth: "admin"
@@ -55411,15 +55911,15 @@ var FanPresetCaptureResultSchema = object({
55411
55911
  items: array(FanCaptureItemSchema),
55412
55912
  omitted: array(FanCaptureOmissionSchema)
55413
55913
  });
55414
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55914
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
55415
55915
  DeviceType.Fan;
55416
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55916
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55417
55917
  kind: "mutation",
55418
55918
  auth: "admin"
55419
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55919
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55420
55920
  kind: "mutation",
55421
55921
  auth: "protected"
55422
- }), method(deviceInput$1.extend({
55922
+ }), method(deviceInput$2.extend({
55423
55923
  fields: array(FanCaptureFieldSchema).optional(),
55424
55924
  steps: array(ActionStepSchema).max(20).default([])
55425
55925
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -56330,13 +56830,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56330
56830
  */
56331
56831
  var ConfigSchemaPassthrough$3 = custom();
56332
56832
  var IntegrationInstanceIdSchema = string().min(1);
56833
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56834
+ /**
56835
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56836
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56837
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56838
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56839
+ * instance id.
56840
+ */
56841
+ var IntegrationInstanceKindSchema = object({
56842
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56843
+ id: string().min(1),
56844
+ label: string().min(1),
56845
+ /** The kind signs in through a server-held interactive session. */
56846
+ signIn: object({
56847
+ basePath: string().min(1),
56848
+ methods: array(_enum(["device-code", "proxy"])).optional()
56849
+ }).optional(),
56850
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56851
+ connectPath: string().min(1).optional(),
56852
+ /** Relative hub path to disconnect that account. */
56853
+ disconnectPath: string().min(1).optional()
56854
+ });
56333
56855
  var IntegrationInstanceDescriptionSchema = object({
56856
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56857
+ role: _enum([
56858
+ "devices",
56859
+ "services",
56860
+ "both"
56861
+ ]),
56862
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56863
+ discovery: object({
56864
+ mode: _enum([
56865
+ "none",
56866
+ "lan",
56867
+ "account"
56868
+ ]),
56869
+ scopedToInstance: boolean()
56870
+ }).optional(),
56334
56871
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56335
- instanceMode: _enum(["single", "multiple"]),
56872
+ instanceMode: IntegrationInstanceModeSchema,
56336
56873
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
56337
56874
  kind: string(),
56338
56875
  /** True when `testInstance` really probes the remote. */
56339
- canTest: boolean()
56876
+ canTest: boolean(),
56877
+ /**
56878
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56879
+ * Add flow asks which first and sends it as `createInstance.kind`.
56880
+ */
56881
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56882
+ /**
56883
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56884
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56885
+ */
56886
+ allowRestoreId: boolean().optional(),
56887
+ /**
56888
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56889
+ * send an id (the UI then mints an opaque one).
56890
+ */
56891
+ mintsId: boolean().optional()
56340
56892
  });
56341
56893
  var IntegrationInstanceStatusSchema = object({
56342
56894
  state: _enum([
@@ -56345,7 +56897,9 @@ var IntegrationInstanceStatusSchema = object({
56345
56897
  "error",
56346
56898
  "unknown"
56347
56899
  ]),
56348
- detail: string().optional()
56900
+ detail: string().optional(),
56901
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56902
+ label: string().optional()
56349
56903
  });
56350
56904
  var IntegrationInstanceSchema = object({
56351
56905
  id: IntegrationInstanceIdSchema,
@@ -56353,13 +56907,18 @@ var IntegrationInstanceSchema = object({
56353
56907
  /** The addon's own rule (an URL is present, a client id is present...). */
56354
56908
  configured: boolean(),
56355
56909
  /**
56356
- * The instance's own on/off switch (D996): always present, derived from the record body
56357
- * (`enabled !== false`, so a row written before the field existed is on).
56910
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56911
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56912
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56358
56913
  */
56359
- enabled: boolean(),
56914
+ enabled: boolean().optional(),
56915
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56916
+ kind: string().optional(),
56360
56917
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
56361
56918
  formValues: record(string(), unknown()),
56362
- status: IntegrationInstanceStatusSchema.optional()
56919
+ status: IntegrationInstanceStatusSchema.optional(),
56920
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56921
+ lastCheckedAt: number().optional()
56363
56922
  });
56364
56923
  var IntegrationInstancePatchSchema = object({
56365
56924
  id: IntegrationInstanceIdSchema,
@@ -56367,31 +56926,57 @@ var IntegrationInstancePatchSchema = object({
56367
56926
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
56368
56927
  values: record(string(), unknown())
56369
56928
  });
56370
- 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({
56371
- id: IntegrationInstanceIdSchema,
56372
- name: string().min(1)
56373
- }), object({ id: string() }), {
56374
- kind: "mutation",
56375
- auth: "admin"
56376
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56377
- kind: "mutation",
56378
- auth: "admin"
56379
- }), method(object({
56380
- id: IntegrationInstanceIdSchema,
56381
- enabled: boolean()
56382
- }), object({ ok: literal(true) }), {
56383
- kind: "mutation",
56384
- auth: "admin"
56385
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56386
- kind: "mutation",
56387
- auth: "admin"
56388
- }), optionalMethod(object({
56389
- id: IntegrationInstanceIdSchema.nullable(),
56390
- values: record(string(), unknown())
56391
- }), TestConnectionResultSchema$1, {
56392
- kind: "mutation",
56393
- auth: "admin"
56394
- });
56929
+ var integrationInstancesCapability = {
56930
+ name: "integration-instances",
56931
+ scope: "system",
56932
+ mode: "collection",
56933
+ methods: {
56934
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56935
+ /** The form; `id: null` is the create form. */
56936
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56937
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56938
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56939
+ /**
56940
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56941
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56942
+ */
56943
+ createInstance: method(object({
56944
+ id: IntegrationInstanceIdSchema.optional(),
56945
+ name: string().min(1),
56946
+ kind: string().min(1).optional()
56947
+ }), object({ id: string() }), {
56948
+ kind: "mutation",
56949
+ auth: "admin"
56950
+ }),
56951
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56952
+ kind: "mutation",
56953
+ auth: "admin"
56954
+ }),
56955
+ /**
56956
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56957
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56958
+ */
56959
+ setInstanceEnabled: method(object({
56960
+ id: IntegrationInstanceIdSchema,
56961
+ enabled: boolean()
56962
+ }), object({ ok: literal(true) }), {
56963
+ kind: "mutation",
56964
+ auth: "admin"
56965
+ }),
56966
+ /** Delete the record and its keychain refs together. */
56967
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56968
+ kind: "mutation",
56969
+ auth: "admin"
56970
+ }),
56971
+ testInstance: optionalMethod(object({
56972
+ id: IntegrationInstanceIdSchema.nullable(),
56973
+ values: record(string(), unknown())
56974
+ }), TestConnectionResultSchema$1, {
56975
+ kind: "mutation",
56976
+ auth: "admin"
56977
+ })
56978
+ }
56979
+ };
56395
56980
  /**
56396
56981
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
56397
56982
  * session (server-initiated offer pattern, identical to the main
@@ -58409,6 +58994,11 @@ var LogEntrySchema = object({
58409
58994
  * flight, and not asking for the next slice ends the scan.
58410
58995
  */
58411
58996
  var LogReadQuerySchema = object({
58997
+ /**
58998
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58999
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
59000
+ */
59001
+ instanceId: string().min(1).max(200).optional(),
58412
59002
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
58413
59003
  requestId: string().min(1).max(64).optional(),
58414
59004
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -58471,6 +59061,8 @@ var LogReadResultSchema = object({
58471
59061
  /** True when `cancelRead` ended this slice early. */
58472
59062
  cancelled: boolean().optional()
58473
59063
  });
59064
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
59065
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
58474
59066
  var LogReadCapabilitiesSchema = object({
58475
59067
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
58476
59068
  readable: boolean(),
@@ -58489,7 +59081,7 @@ var LogReadCapabilitiesSchema = object({
58489
59081
  /** How far back the store reaches, when the provider knows (days). */
58490
59082
  retentionDays: number().nullable()
58491
59083
  });
58492
- 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() }), {
59084
+ 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() }), {
58493
59085
  kind: "mutation",
58494
59086
  auth: "admin"
58495
59087
  });
@@ -70832,6 +71424,14 @@ var powerMeterCapability = {
70832
71424
  deviceNative: true,
70833
71425
  mode: "singleton",
70834
71426
  deviceTypes: POWER_METER_DEVICE_TYPES,
71427
+ deviceConfig: { ui: {
71428
+ kind: "widget",
71429
+ widgetId: "host/power-meter-line",
71430
+ tab: "power",
71431
+ placement: "hero",
71432
+ label: "Power",
71433
+ order: 4
71434
+ } },
70835
71435
  methods: {},
70836
71436
  status: {
70837
71437
  schema: PowerMeterStatusSchema,
@@ -70891,6 +71491,117 @@ var powerMeterCapability = {
70891
71491
  volatileStateFields: ["lastFetchedAt"],
70892
71492
  customize: powerMeterCustomize
70893
71493
  };
71494
+ var PowerSourceRefusalReasonSchema = _enum([
71495
+ "no-power-source",
71496
+ "switch-missing",
71497
+ "switch-offline",
71498
+ "switch-unknown",
71499
+ "restart-in-progress",
71500
+ "switch-call-failed"
71501
+ ]);
71502
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
71503
+ accepted: literal(false),
71504
+ reason: PowerSourceRefusalReasonSchema,
71505
+ /** One line for the operator, naming the switch. */
71506
+ message: string()
71507
+ })]);
71508
+ var PowerSourceInfoSchema = object({
71509
+ /** False when the device has no power source linked; the rest is then empty. */
71510
+ linked: boolean(),
71511
+ switchDeviceId: number().nullable(),
71512
+ switchName: string().nullable(),
71513
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
71514
+ switchOn: boolean().nullable(),
71515
+ switchOnline: boolean().nullable(),
71516
+ restartInProgress: boolean(),
71517
+ offSeconds: number().int()
71518
+ });
71519
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
71520
+ var PowerSourceStateSchema = object({
71521
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
71522
+ switchOn: boolean().nullable(),
71523
+ offSeconds: number().int(),
71524
+ lastFetchedAt: number()
71525
+ });
71526
+ var POWER_SOURCE_DEVICE_TYPES = [
71527
+ DeviceType.Gateway,
71528
+ DeviceType.Hub,
71529
+ DeviceType.Camera,
71530
+ DeviceType.Sensor,
71531
+ DeviceType.MediaPlayer,
71532
+ DeviceType.Vacuum,
71533
+ DeviceType.Generic,
71534
+ DeviceType.Washer,
71535
+ DeviceType.Dryer,
71536
+ DeviceType.Dishwasher,
71537
+ DeviceType.PetFountain,
71538
+ DeviceType.Fan,
71539
+ DeviceType.Humidifier,
71540
+ DeviceType.Climate,
71541
+ DeviceType.WaterHeater
71542
+ ];
71543
+ var powerSourceCustomize = {
71544
+ version: 1,
71545
+ targets: [{
71546
+ id: "switch",
71547
+ cardinality: "scalar",
71548
+ vocabulary: {
71549
+ cap: "power-source",
71550
+ stateField: "switchOn"
71551
+ },
71552
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71553
+ apply: "add",
71554
+ path: "switchOn",
71555
+ sources: [{
71556
+ kind: "switch",
71557
+ transforms: [{ kind: "identity" }]
71558
+ }],
71559
+ companions: [{
71560
+ kind: "constant",
71561
+ path: "offSeconds",
71562
+ value: 10,
71563
+ when: "add"
71564
+ }, {
71565
+ kind: "clock",
71566
+ path: "lastFetchedAt",
71567
+ when: "always"
71568
+ }],
71569
+ sourceReport: { status: "assignment-status" }
71570
+ }]
71571
+ };
71572
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
71573
+ var powerSourceCapability = {
71574
+ name: "power-source",
71575
+ scope: "device",
71576
+ mode: "singleton",
71577
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71578
+ methods: {
71579
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71580
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71581
+ kind: "mutation",
71582
+ auth: "admin"
71583
+ }),
71584
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71585
+ kind: "mutation",
71586
+ auth: "admin"
71587
+ }),
71588
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71589
+ kind: "mutation",
71590
+ auth: "admin"
71591
+ })
71592
+ },
71593
+ runtimeState: PowerSourceStateSchema,
71594
+ customize: powerSourceCustomize,
71595
+ /**
71596
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71597
+ * its next report; a restored value would outlive the reading it names.
71598
+ *
71599
+ * See `RuntimeStateDurability`. Enforced by
71600
+ * `scripts/check-runtime-state-durability.ts`.
71601
+ */
71602
+ durability: "session",
71603
+ history: "none"
71604
+ };
70894
71605
  /**
70895
71606
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
70896
71607
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -80386,6 +81097,25 @@ function buildDescriptor(capName, kind) {
80386
81097
  * contributes one entry per bound cap present in this map.
80387
81098
  */
80388
81099
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
81100
+ /**
81101
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
81102
+ * shared by the admin UI and the viewer. A role is data on the wire; each
81103
+ * client resolves its name from `label` (English and Italian side by side) and
81104
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
81105
+ * server sends an id, a client maps it to its own icon set.
81106
+ *
81107
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
81108
+ * pictogram): a client that can draw text shows it, one that cannot falls back
81109
+ * to `icon`.
81110
+ */
81111
+ function digit(n) {
81112
+ return {
81113
+ label: t(n, n),
81114
+ icon: "hash",
81115
+ legend: n
81116
+ };
81117
+ }
81118
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
80389
81119
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
80390
81120
  var btn = (idSuffix, row, col, init) => ({
80391
81121
  idSuffix,
@@ -80550,6 +81280,34 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80550
81280
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80551
81281
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80552
81282
  }
81283
+ /**
81284
+ * Propose a running threshold from a device's own power history (planning#405).
81285
+ *
81286
+ * Pure and deterministic: the same series gives the same answer. The series is
81287
+ * a HELD-VALUE step series — a point's value stands until the next point — which
81288
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
81289
+ * count of samples.
81290
+ *
81291
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
81292
+ * near F. A device that is not near one level most of the time has no floor.
81293
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
81294
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
81295
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
81296
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
81297
+ * grid a person would type; stop dwell covers the longest pause; start dwell
81298
+ * covers the longest standby blip above T.
81299
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
81300
+ * run must hold exactly one true interval and nothing may flap outside.
81301
+ */
81302
+ var SECOND_MS = 1e3;
81303
+ var MINUTE_MS$1 = 60 * SECOND_MS;
81304
+ 24 * (60 * MINUTE_MS$1);
81305
+ 10 * MINUTE_MS$1;
81306
+ 30 * SECOND_MS;
81307
+ 60 * SECOND_MS;
81308
+ 20 * MINUTE_MS$1;
81309
+ 10 * SECOND_MS;
81310
+ 10 * SECOND_MS;
80553
81311
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80554
81312
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80555
81313
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83655,6 +84413,13 @@ Object.freeze({
83655
84413
  access: "create",
83656
84414
  role: "admin"
83657
84415
  },
84416
+ "coreBlocks.proposeRunningThreshold": {
84417
+ capName: "core-blocks",
84418
+ capScope: "system",
84419
+ addonId: null,
84420
+ access: "view",
84421
+ role: "admin"
84422
+ },
83658
84423
  "coreBlocks.reportAutomationRun": {
83659
84424
  capName: "core-blocks",
83660
84425
  capScope: "system",
@@ -84166,6 +84931,13 @@ Object.freeze({
84166
84931
  access: "view",
84167
84932
  role: "admin"
84168
84933
  },
84934
+ "deviceExport.listInstances": {
84935
+ capName: "device-export",
84936
+ capScope: "system",
84937
+ addonId: null,
84938
+ access: "view",
84939
+ role: "admin"
84940
+ },
84169
84941
  "deviceExport.listSupportedDeviceKinds": {
84170
84942
  capName: "device-export",
84171
84943
  capScope: "system",
@@ -93124,6 +93896,11 @@ Object.freeze({
93124
93896
  form: "single",
93125
93897
  optional: false
93126
93898
  }],
93899
+ "coreBlocks.proposeRunningThreshold": [{
93900
+ name: "deviceId",
93901
+ form: "single",
93902
+ optional: false
93903
+ }],
93127
93904
  "cover.close": [{
93128
93905
  name: "deviceId",
93129
93906
  form: "single",
@@ -95787,99 +96564,6 @@ Object.freeze({
95787
96564
  "network-access": "ingress",
95788
96565
  "smtp-provider": "email"
95789
96566
  });
95790
- /**
95791
- * A saved Customize assignment (D936). The schema is the type: callers use
95792
- * `z.infer`, they do not restate the shape.
95793
- */
95794
- var OverrideSourceSchema = object({
95795
- addonId: string().min(1),
95796
- stableId: string().min(1),
95797
- cap: string().min(1),
95798
- fieldPath: string().min(1)
95799
- });
95800
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
95801
- object({ kind: literal("identity") }),
95802
- object({ kind: literal("invert") }),
95803
- object({
95804
- kind: literal("threshold"),
95805
- comparison: _enum(["gte", "lte"]),
95806
- threshold: number(),
95807
- startDwellMs: number().nonnegative(),
95808
- stopDwellMs: number().nonnegative()
95809
- }),
95810
- object({
95811
- kind: literal("linear"),
95812
- scale: number(),
95813
- offset: number(),
95814
- clampMin: number().optional(),
95815
- clampMax: number().optional()
95816
- }),
95817
- object({
95818
- kind: literal("enum-map"),
95819
- mapping: record(string(), string().nullable())
95820
- }),
95821
- object({ kind: literal("number") })
95822
- ]);
95823
- var ScalarOverrideDraftSchema = object({
95824
- kind: literal("scalar"),
95825
- cap: string().min(1),
95826
- targetId: string().min(1),
95827
- source: OverrideSourceSchema,
95828
- transform: CustomizeTransformDraftSchema
95829
- });
95830
- var KeyedArrayOverrideDraftSchema = object({
95831
- kind: literal("keyedArray"),
95832
- cap: string().min(1),
95833
- targetId: string().min(1),
95834
- key: string().min(1),
95835
- label: string().min(1),
95836
- fieldPath: string().min(1),
95837
- source: OverrideSourceSchema,
95838
- transform: CustomizeTransformDraftSchema
95839
- });
95840
- var PulseOverrideDraftSchema = object({
95841
- kind: literal("pulse"),
95842
- cap: string().min(1),
95843
- targetId: string().min(1),
95844
- enabled: boolean(),
95845
- debounceMs: number().nonnegative(),
95846
- edges: array(_enum([
95847
- "rising",
95848
- "button",
95849
- "native-event"
95850
- ])).min(1).optional(),
95851
- sources: array(OverrideSourceSchema).min(1)
95852
- });
95853
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
95854
- "in-addition",
95855
- "confirm",
95856
- "replace"
95857
- ]).optional() });
95858
- discriminatedUnion("kind", [
95859
- ScalarOverrideDraftSchema,
95860
- KeyedArrayOverrideDraftSchema,
95861
- PulseOverrideDraftSchema,
95862
- object({
95863
- kind: literal("span"),
95864
- cap: string().min(1),
95865
- targetId: string().min(1),
95866
- timeoutMs: number().nonnegative(),
95867
- cooldownMs: number().nonnegative().optional(),
95868
- sources: array(SpanSourceDraftSchema)
95869
- })
95870
- ]);
95871
- object({
95872
- targetId: string().min(1),
95873
- configured: boolean(),
95874
- source: _enum(["native", "assigned"]),
95875
- state: _enum([
95876
- "active",
95877
- "stale",
95878
- "released",
95879
- "unavailable"
95880
- ]),
95881
- reason: string().nullable()
95882
- });
95883
96567
  var G711_SCALE_CORRECTION_DB = {
95884
96568
  PCMU: 20 * Math.log10(4),
95885
96569
  PCMA: 20 * Math.log10(8)
@@ -97710,20 +98394,5 @@ function haInstanceSettingsPortsOver(api) {
97710
98394
  }
97711
98395
  };
97712
98396
  }
97713
- /** The brokerId → settings view of the instances: what a reader keeps per broker. */
97714
- function settingsByBroker(rows) {
97715
- const out = /* @__PURE__ */ new Map();
97716
- for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
97717
- return out;
97718
- }
97719
- /** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
97720
- async function listInstancesOrNothing(ports, onError) {
97721
- try {
97722
- return await ports.listInstances();
97723
- } catch (err) {
97724
- onError(errMsg(err));
97725
- return [];
97726
- }
97727
- }
97728
98397
  //#endregion
97729
- export { fanControlCapability as $, nodePin as $t, bestLocationMatch as A, scriptRunnerCapability as At, climateControlCapability as B, valveCapability as Bt, accessoriesCapability as C, quantityFromUnit as Ct, ambientLightSensorCapability as D, runningFromMediaPlayer as Dt, alarmPanelCapability as E, runningFromLawnMower as Et, buildAddonRouteProvider as F, temperatureSensorCapability as Ft, contactCapability as G, RecordSecretUnavailableError as Gt, connectivityCapability as H, waterHeaterCapability as Ht, buildDeviceAdminUrl as I, tryConvertDelta as It, deviceAdminLinkCapability as J, defineAddonMigrations as Jt, controlCapability as K, baseUnitOf as Kt, buttonCapability as L, tryConvertUnit as Lt, boardKeySourceCapability as M, smokeCapability as Mt, brightnessCapability as N, switchCapability as Nt, automationControlCapability as O, runningFromVacuumState as Ot, brokerCapability as P, tamperCapability as Pt, eventEmitterCapability as Q, isReadingQuantity as Qt, calendarSourceCapability as R, updateCapability as Rt, TargetSchema as S, publicEndpointReaderFrom as St, airQualitySensorCapability as T, runExportedAction as Tt, consumableFormOf as U, weatherCapability as Ut, colorCapability as V, vibrationCapability as Vt, consumableKindOf as W, BaseAddon as Wt, deviceExportCapability as X, defineRecordKinds as Xt, deviceAdoptionCapability as Y, defineEntryKind as Yt, enumSensorCapability as Z, entryBody as Zt, FanDirectionSchema as _, preconditionOf as _t, HA_PROVIDER_ADDON_ID$1 as a, number as an, ingestReading as at, PLUG_APPLIANCE_TYPES as b, pressureSensorCapability as bt, BaseDevice as c, string as cn, mediaPlayerCapability as ct, COCO_TO_MACRO as d, url as dn, notificationOutputCapability as dt, _enum as en, floodCapability as et, CameraSwitchIdSchema as f, DeviceFeature as fn, notifierCapability as ft, EnumSensorDateTimeFormatSchema as g, EventCategory as gn, powerMeterCapability as gt, ConsumableStatusSchema as h, errMsg as hn, parseAdminBase as ht, HA_BROKER_KIND as i, literal as in, imageCapability as it, binaryCapability as j, sendToExportedTarget as jt, batteryCapability as k, runningStateCapability as kt, BaseDeviceProvider as l, union as ln, motionCapability as lt, ConsumableItemSchema as m, DeviceType as mn, oauthIntegrationCapability as mt, listInstancesOrNothing as n, boolean as nn, humidifierCapability as nt, listHomeAssistantBrokers as o, object as on, lawnMowerControlCapability as ot, ConsumableFormSchema as p, DeviceRole as pn, numericSensorCapability as pt, coverCapability as q, createEvent as qt, settingsByBroker as r, discriminatedUnion as rn, humiditySensorCapability as rt, AlarmArmModeSchema as s, record as sn, lockControlCapability as st, haInstanceSettingsPortsOver as t, array as tn, gasCapability as tt, CAMERA_SWITCH_ORDER as u, unknown as un, normalizeUnit as ut, HvacModeSchema as v, prepareNotification as vt, addonRoutesCapability as w, resolveAttachmentUrls as wt, RUNNING_STATE_COLD as x, pruneDeletedEntities as xt, MediaPlayerRepeatSchema as y, presenceCapability as yt, carbonMonoxideCapability as z, vacuumControlCapability as zt };
98398
+ export { fanControlCapability as $, createEvent as $t, bestLocationMatch as A, runningFromMediaPlayer as At, climateControlCapability as B, tryConvertDelta as Bt, accessoriesCapability as C, pressureSensorCapability as Ct, ambientLightSensorCapability as D, resolveAttachmentUrls as Dt, alarmPanelCapability as E, quantityFromUnit as Et, buildAddonRouteProvider as F, smokeCapability as Ft, contactCapability as G, vibrationCapability as Gt, connectivityCapability as H, updateCapability as Ht, buildDeviceAdminUrl as I, statusInstanceOf as It, deviceAdminLinkCapability as J, BaseAddon as Jt, controlCapability as K, waterHeaterCapability as Kt, buttonCapability as L, switchCapability as Lt, boardKeySourceCapability as M, runningStateCapability as Mt, brightnessCapability as N, scriptRunnerCapability as Nt, automationControlCapability as O, runExportedAction as Ot, brokerCapability as P, sendToExportedTarget as Pt, eventEmitterCapability as Q, baseUnitOf as Qt, calendarSourceCapability as R, tamperCapability as Rt, TargetSchema as S, EventCategory as Sn, presenceCapability as St, airQualitySensorCapability as T, publicEndpointReaderFrom as Tt, consumableFormOf as U, vacuumControlCapability as Ut, colorCapability as V, tryConvertUnit as Vt, consumableKindOf as W, valveCapability as Wt, deviceExportCapability as X, RecordSecretUnavailableError as Xt, deviceAdoptionCapability as Y, REDACTED_SECRET as Yt, enumSensorCapability as Z, RevConflictError as Zt, HvacModeSchema as _, url as _n, oauthIntegrationCapability as _t, AlarmArmModeSchema as a, nodePin as an, humiditySensorCapability as at, RUNNING_STATE_COLD as b, DeviceType as bn, preconditionOf as bt, CAMERA_SWITCH_ORDER as c, boolean as cn, integrationInstancesCapability as ct, ConsumableFormSchema as d, number as dn, mediaPlayerCapability as dt, defineAddonMigrations as en, flattenNestedRecords as et, ConsumableItemSchema as f, object as fn, motionCapability as ft, FanDirectionSchema as g, unknown as gn, numericSensorCapability as gt, ExportInstanceUnknownError as h, union as hn, notifierCapability as ht, listHomeAssistantBrokers as i, isReadingQuantity as in, humidifierCapability as it, binaryCapability as j, runningFromVacuumState as jt, batteryCapability as k, runningFromLawnMower as kt, COCO_TO_MACRO as l, discriminatedUnion as ln, lawnMowerControlCapability as lt, EnumSensorDateTimeFormatSchema as m, string as mn, notificationOutputCapability as mt, HA_BROKER_KIND as n, defineRecordKinds as nn, floodCapability as nt, BaseDevice as o, _enum as on, imageCapability as ot, ConsumableStatusSchema as p, record as pn, normalizeUnit as pt, coverCapability as q, weatherCapability as qt, HA_PROVIDER_ADDON_ID$1 as r, entryBody as rn, gasCapability as rt, BaseDeviceProvider as s, array as sn, ingestReading as st, haInstanceSettingsPortsOver as t, defineEntryKind as tn, flattenPortsOver as tt, CameraSwitchIdSchema as u, literal as un, lockControlCapability as ut, MediaPlayerRepeatSchema as v, DeviceFeature as vn, parseAdminBase as vt, addonRoutesCapability as w, pruneDeletedEntities as wt, RecordIntegrationInstances as x, errMsg as xn, prepareNotification as xt, PLUG_APPLIANCE_TYPES as y, DeviceRole as yn, powerMeterCapability as yt, carbonMonoxideCapability as z, temperatureSensorCapability as zt };