@camstack/addon-provider-homeassistant 1.2.653 → 1.2.655

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.
@@ -784,7 +784,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
784
784
  return EventCategory;
785
785
  }({});
786
786
  //#endregion
787
- //#region ../types/dist/device-type-CfHw_quR.mjs
787
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
788
788
  /**
789
789
  import { errMsg } from '@camstack/types'
790
790
  * Extract a human-readable message from an unknown error value.
@@ -1015,6 +1015,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1015
1015
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1016
1016
  /** Media player exposes stop distinct from pause. */
1017
1017
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1018
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1019
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1020
+ /** Media player exposes channel up / down in steps. */
1021
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1022
+ /** Media player exposes power on / off. */
1023
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1018
1024
  /** Alarm panel requires a PIN code on arm/disarm. */
1019
1025
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1020
1026
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -5974,7 +5980,7 @@ var ZodIssueCode = {
5974
5980
  var ZodFirstPartyTypeKind;
5975
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5976
5982
  //#endregion
5977
- //#region ../types/dist/sleep-C6LqREpb.mjs
5983
+ //#region ../types/dist/sleep-BkzQdpzF.mjs
5978
5984
  /**
5979
5985
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5980
5986
  * window to float samples (D455).
@@ -6069,6 +6075,16 @@ var AddonMigrationsBlockedError = class extends Error {
6069
6075
  this.migrationIds = migrationIds;
6070
6076
  }
6071
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__";
6072
6088
  /** Is this field's stored value a credential? */
6073
6089
  function isSecretConfigField(field) {
6074
6090
  if (field.type === "password") return true;
@@ -6122,9 +6138,16 @@ function isSecretMarker(value) {
6122
6138
  const ref = value["$secret"];
6123
6139
  return typeof ref === "string" && ref.length > 0;
6124
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
+ }
6125
6145
  function isSecretClear(value) {
6126
6146
  return isRecord$7(value) && value["$secretClear"] === true;
6127
6147
  }
6148
+ function isSecretFieldView(value) {
6149
+ return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
6150
+ }
6128
6151
  /**
6129
6152
  * The UI view of a stored secret field value: a marker becomes "set", inline
6130
6153
  * plaintext becomes "set (inline)", nothing becomes `null`. A value never
@@ -6774,11 +6797,36 @@ function projectStore(store, perNodeKeys, nodeId) {
6774
6797
  }
6775
6798
  return out;
6776
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://";
6777
6817
  var GLOBAL_SCOPE = { kind: "global" };
6778
6818
  /** The scope every addon-wide (not per-node, not per-device) setting lives in. */
6779
6819
  function globalSecretScope() {
6780
6820
  return GLOBAL_SCOPE;
6781
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
+ }
6782
6830
  function encodeSegment(value) {
6783
6831
  return encodeURIComponent(value);
6784
6832
  }
@@ -6789,6 +6837,16 @@ function decodeSegment(value) {
6789
6837
  return null;
6790
6838
  }
6791
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
+ }
6792
6850
  /**
6793
6851
  * The field path of a value inside an array item, addressed by the item's id:
6794
6852
  * `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
@@ -6797,6 +6855,13 @@ function decodeSegment(value) {
6797
6855
  function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
6798
6856
  return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
6799
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
+ }
6800
6865
  function parseScope(segments) {
6801
6866
  switch (segments[0]) {
6802
6867
  case "global": return {
@@ -8062,6 +8127,10 @@ var BaseAddon = class {
8062
8127
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8063
8128
  const patch = {};
8064
8129
  for (const key of keys) {
8130
+ if (tree.get(key)?.kind === "array") {
8131
+ patch[key] = [];
8132
+ continue;
8133
+ }
8065
8134
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8066
8135
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8067
8136
  }
@@ -8750,6 +8819,8 @@ var RECORD_BATCH_MAX = 1e3;
8750
8819
  var RECORD_QUERY_MAX = 1e3;
8751
8820
  /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8752
8821
  var RECORD_SETTINGS_MAX_ROWS = 5e4;
8822
+ /** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
8823
+ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8753
8824
  /**
8754
8825
  * Secret fields of a record, addressed the way a Zod schema declares them
8755
8826
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
@@ -8786,6 +8857,112 @@ function secretArrayPrefixes(path) {
8786
8857
  }
8787
8858
  return prefixes;
8788
8859
  }
8860
+ function isRecord$4(value) {
8861
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8862
+ }
8863
+ function elementId(element, idKey, declared, prefix) {
8864
+ const id = isRecord$4(element) ? element[idKey] : void 0;
8865
+ 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`);
8866
+ return id;
8867
+ }
8868
+ /**
8869
+ * Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
8870
+ * visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
8871
+ * result is a new body, `data` is untouched.
8872
+ */
8873
+ var REMOVE_LEAF = Symbol("remove-leaf");
8874
+ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
8875
+ if (!isRecord$4(node)) return node;
8876
+ const segment = segments[index];
8877
+ if (segment === void 0) return node;
8878
+ if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
8879
+ const child = node[segment.key];
8880
+ const keys = [...walkedKeys, segment.key];
8881
+ const last = index === segments.length - 1;
8882
+ if (segment.each) {
8883
+ if (!Array.isArray(child)) return node;
8884
+ const prefix = keys.join(".");
8885
+ const idKey = arrayKeys[prefix];
8886
+ if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
8887
+ const mapped = child.map((element) => {
8888
+ const id = elementId(element, idKey, declared, prefix);
8889
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
8890
+ return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
8891
+ });
8892
+ return {
8893
+ ...node,
8894
+ [segment.key]: mapped
8895
+ };
8896
+ }
8897
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8898
+ if (last) {
8899
+ const replacement = visit({
8900
+ declared,
8901
+ concrete,
8902
+ value: child
8903
+ });
8904
+ if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
8905
+ return {
8906
+ ...node,
8907
+ [segment.key]: replacement
8908
+ };
8909
+ }
8910
+ return {
8911
+ ...node,
8912
+ [segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
8913
+ };
8914
+ }
8915
+ /** Replace/remove every present secret leaf; returns a NEW body. */
8916
+ function mapSecretLeaves(data, paths, arrayKeys, visit) {
8917
+ let current = data;
8918
+ for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
8919
+ return current;
8920
+ }
8921
+ /**
8922
+ * The keychain collection name a kind's secrets live under. Prefixed so a
8923
+ * record's refs never collide with the refs a legacy collection of the same name
8924
+ * already holds during an adoption.
8925
+ */
8926
+ function recordSecretCollection(kind) {
8927
+ return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
8928
+ }
8929
+ /** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
8930
+ function recordSecretRef(owner, kind, rowId, concretePath) {
8931
+ return buildSecretRef({
8932
+ owner,
8933
+ scope: {
8934
+ kind: "row",
8935
+ collection: recordSecretCollection(kind),
8936
+ rowId
8937
+ },
8938
+ fieldPath: concretePath
8939
+ });
8940
+ }
8941
+ /**
8942
+ * A stored secret, as an admin read shows it: a VIEW naming the row's own
8943
+ * keychain ref (`set: true`), never the value and no longer the bare redaction
8944
+ * token. The form hands it to the secret field, which reveals the value by ref
8945
+ * through the audited `keychain-admin.reveal`; sent back untouched it keeps the
8946
+ * stored secret (the records door reads a view as KEEP).
8947
+ *
8948
+ * ONE implementation for the entries service and for every addon that serves
8949
+ * its own instances (`RecordIntegrationInstances`).
8950
+ */
8951
+ function isRecord$3(value) {
8952
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8953
+ }
8954
+ function secretViewsOf(owner, kind, id, decl, data) {
8955
+ if (decl.secretPaths.length === 0) return data;
8956
+ const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
8957
+ if (leaf.value !== "__camstack_redacted__") return leaf.value;
8958
+ return {
8959
+ $secret: recordSecretRef(owner, kind, id, leaf.concrete),
8960
+ v: 1,
8961
+ set: true
8962
+ };
8963
+ });
8964
+ return isRecord$3(shown) ? shown : data;
8965
+ }
8789
8966
  /**
8790
8967
  * The share-view scope kinds a share-link token can carry. The hub mints
8791
8968
  * and validates them (`ShareViewGrantSchema` builds its
@@ -9008,7 +9185,7 @@ function defineRecordKinds(specs) {
9008
9185
  retention: spec.retention,
9009
9186
  deviceRemoval: spec.deviceRemoval,
9010
9187
  maxDataBytes: spec.maxDataBytes,
9011
- secretPaths: [...secretPaths],
9188
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9012
9189
  secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9013
9190
  transactFamily: spec.transactFamily,
9014
9191
  entry: spec.entry === void 0 ? void 0 : {
@@ -9053,6 +9230,7 @@ function defineEntryKind(name, spec) {
9053
9230
  ...spec.database === void 0 ? {} : { database: spec.database },
9054
9231
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9055
9232
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9233
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9056
9234
  entry: {
9057
9235
  title: spec.title,
9058
9236
  ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
@@ -11780,7 +11958,7 @@ object({
11780
11958
  params: record(string(), union([string(), number()])).optional()
11781
11959
  });
11782
11960
  //#endregion
11783
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11961
+ //#region ../types/dist/composition-CRNwcFYY.mjs
11784
11962
  /**
11785
11963
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11786
11964
  *
@@ -12543,38 +12721,6 @@ function validateExpressionSource(src) {
12543
12721
  return null;
12544
12722
  }
12545
12723
  /**
12546
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12547
- *
12548
- * Standby of the control board is under 5 W; a motor or a heater is far
12549
- * above that, so 5 W is the line. A door lock or a short pump spike is not
12550
- * a cycle, so running starts only after 30 s above the line.
12551
- *
12552
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12553
- * bursts) lasts several minutes. Ending the cycle on the first dip would
12554
- * report "finished" in the middle of the program, so the stop dwell is
12555
- * 10 minutes on every type. The number is stored per type so one machine
12556
- * can move later without moving the others.
12557
- */
12558
- var APPLIANCE_TEMPLATE_ID = "appliance";
12559
- var APPLIANCE_KINDS = [
12560
- "washer",
12561
- "dryer",
12562
- "dishwasher"
12563
- ];
12564
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12565
- /**
12566
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
12567
- *
12568
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
12569
- * (D967), so the heater switches exactly as the plug does. Running is the
12570
- * plug being on; when a power reading is also named, running is the draw
12571
- * staying above the line instead (a plug left on with the stove's own
12572
- * thermostat open is not heating). 50 W is far above any standby and below
12573
- * the smallest radiator.
12574
- */
12575
- var HEATER_TEMPLATE_ID = "heater";
12576
- DeviceType.Heater;
12577
- /**
12578
12724
  * Statistics and integrals over a history read (D944).
12579
12725
  *
12580
12726
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -12718,34 +12864,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
12718
12864
  /** Actuator groups only. Readings leave this unset. */
12719
12865
  active: ActiveCriterionSchema.optional()
12720
12866
  }),
12721
- object({
12722
- kind: literal("appliance"),
12723
- templateId: literal(APPLIANCE_TEMPLATE_ID),
12724
- appliance: _enum(APPLIANCE_KINDS),
12725
- thresholdWatts: number().positive(),
12726
- startDwellMs: number().int().nonnegative(),
12727
- stopDwellMs: number().int().nonnegative(),
12728
- member: HelperMemberSchema,
12729
- /**
12730
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
12731
- * composition carries a `power-source` feature, so the hub can cut and restore
12732
- * that plug (power-source link of a NEW-target composition).
12733
- */
12734
- powerSwitch: HelperMemberSchema.optional(),
12735
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
12736
- offSeconds: number().optional()
12737
- }),
12738
- object({
12739
- kind: literal("heater"),
12740
- templateId: literal(HEATER_TEMPLATE_ID),
12741
- /** The plug, read and commanded at `switch.on`. */
12742
- plug: HelperMemberSchema,
12743
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
12744
- meter: HelperMemberSchema.optional(),
12745
- thresholdWatts: number().positive(),
12746
- startDwellMs: number().int().nonnegative(),
12747
- stopDwellMs: number().int().nonnegative()
12748
- }),
12749
12867
  object({
12750
12868
  kind: literal("statistic"),
12751
12869
  stat: _enum(STATISTIC_STATS),
@@ -12917,7 +13035,14 @@ var CompositionForwardCommandSchema = object({
12917
13035
  method: string().min(1),
12918
13036
  args: record(string().min(1), CompositionForwardArgSchema).optional()
12919
13037
  });
12920
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
13038
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
13039
+ CompositionForwardCommandSchema,
13040
+ CompositionCodeSourceSchema,
13041
+ object({
13042
+ kind: literal("extension"),
13043
+ target: literal("remote")
13044
+ })
13045
+ ]);
12921
13046
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12922
13047
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12923
13048
  /**
@@ -12962,7 +13087,7 @@ var CompositionSchema = object({
12962
13087
  kind: literal("existing"),
12963
13088
  device: CompositionSourceRefSchema
12964
13089
  })]),
12965
- features: array(CompositionFeatureSchema).min(1).max(16),
13090
+ features: array(CompositionFeatureSchema).min(0).max(16),
12966
13091
  /**
12967
13092
  * Present when the block is a helper template. Absent on a free
12968
13093
  * composition (the room example, device 4603). The editor reads this,
@@ -13297,6 +13422,157 @@ function buildCapturingReply(envelope) {
13297
13422
  };
13298
13423
  return wrapper;
13299
13424
  }
13425
+ /** The most instances one addon lists (an integrations page, not a table). */
13426
+ var INSTANCES_LIST_MAX = 1e3;
13427
+ /** The keys of the kind's secret fields that sit at the top level of the body. */
13428
+ function topLevelSecretKeys(paths) {
13429
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13430
+ }
13431
+ var RecordIntegrationInstances = class {
13432
+ records;
13433
+ kind;
13434
+ hooks;
13435
+ handle;
13436
+ secretKeys;
13437
+ nameField;
13438
+ constructor(records, kind, hooks) {
13439
+ this.records = records;
13440
+ this.kind = kind;
13441
+ this.hooks = hooks;
13442
+ this.handle = records.kind(kind);
13443
+ this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13444
+ this.nameField = kind.declaration.entry?.nameField ?? "name";
13445
+ }
13446
+ /**
13447
+ * One instance read for the addon's own runtime; null when absent. When the keychain is
13448
+ * locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
13449
+ * caller can keep what it had instead of reconfiguring with an empty secret (D49).
13450
+ */
13451
+ async readResolved(id) {
13452
+ try {
13453
+ const record = await this.handle.getResolved(id);
13454
+ return record === null ? null : {
13455
+ id,
13456
+ body: record.data,
13457
+ enabled: record.data.enabled !== false,
13458
+ secrets: "resolved"
13459
+ };
13460
+ } catch (err) {
13461
+ if (!(err instanceof RecordSecretUnavailableError)) throw err;
13462
+ const stored = await this.handle.get(id);
13463
+ if (stored === null) return null;
13464
+ return {
13465
+ id,
13466
+ body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
13467
+ enabled: stored.data.enabled !== false,
13468
+ secrets: "unavailable"
13469
+ };
13470
+ }
13471
+ }
13472
+ /** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
13473
+ async getResolved(id) {
13474
+ return (await this.readResolved(id))?.body ?? null;
13475
+ }
13476
+ /** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
13477
+ async listResolved() {
13478
+ const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
13479
+ const out = [];
13480
+ for (const record of page.records) {
13481
+ const read = await this.readResolved(record.id);
13482
+ if (read !== null) out.push(read);
13483
+ }
13484
+ return out;
13485
+ }
13486
+ viewOf(record) {
13487
+ const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
13488
+ const rawName = record.data[this.nameField];
13489
+ const kind = this.hooks.kindOf?.(record.data);
13490
+ const health = this.hooks.health?.(record.data, record.id);
13491
+ return {
13492
+ id: record.id,
13493
+ name: typeof rawName === "string" ? rawName : record.id,
13494
+ configured: this.hooks.configured(record.data),
13495
+ enabled: record.data.enabled !== false,
13496
+ formValues: shown,
13497
+ ...kind !== void 0 ? { kind } : {},
13498
+ ...health?.status !== void 0 ? { status: health.status } : {},
13499
+ ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13500
+ };
13501
+ }
13502
+ describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13503
+ ...this.hooks.description,
13504
+ mintsId: true
13505
+ };
13506
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13507
+ listInstances = async () => {
13508
+ return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13509
+ };
13510
+ getInstance = async ({ id }) => {
13511
+ const record = await this.handle.get(id);
13512
+ return record === null ? null : this.viewOf(record);
13513
+ };
13514
+ /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
13515
+ createInstance = async ({ id, name, kind }) => {
13516
+ const minted = id ?? this.hooks.mintId?.({
13517
+ name,
13518
+ ...kind !== void 0 ? { kind } : {}
13519
+ });
13520
+ if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
13521
+ await this.handle.put({
13522
+ id: minted,
13523
+ data: this.hooks.initialBody(name, kind)
13524
+ }, { ifRev: 0 });
13525
+ await this.hooks.onChanged?.(minted);
13526
+ return { id: minted };
13527
+ };
13528
+ updateInstance = async ({ id, name, values }) => {
13529
+ const patch = {};
13530
+ for (const [key, value] of Object.entries(values)) {
13531
+ if (value === void 0) continue;
13532
+ if (!this.secretKeys.has(key)) patch[key] = value;
13533
+ else if (isSecretClear(value)) patch[key] = null;
13534
+ else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13535
+ }
13536
+ if (name !== void 0) patch[this.nameField] = name;
13537
+ await this.handle.patch(id, patch);
13538
+ await this.hooks.onChanged?.(id);
13539
+ return { ok: true };
13540
+ };
13541
+ /** The switch lives in the record body; the addon is told so it applies it (D996). */
13542
+ setInstanceEnabled = async ({ id, enabled }) => {
13543
+ await this.handle.patch(id, { enabled });
13544
+ await this.hooks.onChanged?.(id);
13545
+ return { ok: true };
13546
+ };
13547
+ /** The row and its keychain refs go together (the records door). */
13548
+ deleteInstance = async ({ id }) => {
13549
+ await this.handle.delete(id);
13550
+ await this.hooks.onChanged?.(id);
13551
+ return { ok: true };
13552
+ };
13553
+ testInstance = async (input) => {
13554
+ if (this.hooks.test === void 0) return {
13555
+ success: false,
13556
+ status: "unsupported",
13557
+ error: "This integration declares no connection test"
13558
+ };
13559
+ return this.hooks.test(input);
13560
+ };
13561
+ /** The flat provider object to hand to `registerProvider`. */
13562
+ toProvider() {
13563
+ return {
13564
+ describe: this.describe,
13565
+ instanceFormSchema: this.instanceFormSchema,
13566
+ listInstances: this.listInstances,
13567
+ getInstance: this.getInstance,
13568
+ createInstance: this.createInstance,
13569
+ updateInstance: this.updateInstance,
13570
+ setInstanceEnabled: this.setInstanceEnabled,
13571
+ deleteInstance: this.deleteInstance,
13572
+ testInstance: this.testInstance
13573
+ };
13574
+ }
13575
+ };
13300
13576
  var EncodeProfileSchema = object({
13301
13577
  video: object({
13302
13578
  codec: _enum([
@@ -14402,6 +14678,58 @@ var MigrationOwnerStepSchema = object({
14402
14678
  /** The ledger state the engine found: `pending` has no row yet. */
14403
14679
  state: union([literal("pending"), MigrationStatusSchema])
14404
14680
  });
14681
+ function isPlain(value) {
14682
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14683
+ }
14684
+ async function flattenNestedRecords(ports, spec) {
14685
+ const ids = await ports.listNested(spec.legacyKey);
14686
+ if (ids.length === 0) return {
14687
+ outcome: "already-done",
14688
+ summary: { converted: 0 }
14689
+ };
14690
+ const rows = /* @__PURE__ */ new Map();
14691
+ for (const id of ids) try {
14692
+ rows.set(id, await ports.readResolved(id));
14693
+ } catch (err) {
14694
+ const why = err instanceof Error ? err.message : String(err);
14695
+ throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
14696
+ }
14697
+ for (const [id, row] of rows) {
14698
+ const legacy = row[spec.legacyKey];
14699
+ if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
14700
+ const flat = spec.toFlat(legacy, row);
14701
+ await ports.patch(id, flat);
14702
+ const back = await ports.readResolved(id);
14703
+ const wrong = spec.flatSecretKeys.filter((key) => {
14704
+ const sent = flat[key];
14705
+ const got = back[key];
14706
+ return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
14707
+ });
14708
+ if (wrong.length > 0) {
14709
+ await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
14710
+ throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
14711
+ }
14712
+ await ports.patch(id, { [spec.legacyKey]: null });
14713
+ }
14714
+ return {
14715
+ outcome: "done",
14716
+ summary: { converted: rows.size }
14717
+ };
14718
+ }
14719
+ /** The ports over one kind's records handle. */
14720
+ function flattenPortsOver(handle) {
14721
+ return {
14722
+ listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
14723
+ readResolved: async (id) => {
14724
+ const row = await handle.getResolved(id);
14725
+ if (row === null) throw new Error("the record is gone");
14726
+ return row.data;
14727
+ },
14728
+ patch: async (id, patch) => {
14729
+ await handle.patch(id, patch);
14730
+ }
14731
+ };
14732
+ }
14405
14733
  /**
14406
14734
  * The trainer contract — what the hub hands the cloud trainer and what it
14407
14735
  * gets back (cloud training, D805+).
@@ -19578,6 +19906,48 @@ var APPLIANCE_CAP_VOCABULARY = {
19578
19906
  scope: "device",
19579
19907
  noun: t("Media player", "Lettore multimediale"),
19580
19908
  actions: [
19909
+ {
19910
+ id: "media.turnOn",
19911
+ method: "turnOn",
19912
+ label: t("Turn on", "Accendi"),
19913
+ phrase: t("Turn on {device}", "Accendi {device}"),
19914
+ icon: "power"
19915
+ },
19916
+ {
19917
+ id: "media.turnOff",
19918
+ method: "turnOff",
19919
+ label: t("Turn off", "Spegni"),
19920
+ phrase: t("Turn off {device}", "Spegni {device}"),
19921
+ icon: "power"
19922
+ },
19923
+ {
19924
+ id: "media.volumeUp",
19925
+ method: "volumeUp",
19926
+ label: t("Volume up", "Alza il volume"),
19927
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
19928
+ icon: "volume-2"
19929
+ },
19930
+ {
19931
+ id: "media.volumeDown",
19932
+ method: "volumeDown",
19933
+ label: t("Volume down", "Abbassa il volume"),
19934
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
19935
+ icon: "volume-2"
19936
+ },
19937
+ {
19938
+ id: "media.channelUp",
19939
+ method: "channelUp",
19940
+ label: t("Channel up", "Canale successivo"),
19941
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
19942
+ icon: "skip-forward"
19943
+ },
19944
+ {
19945
+ id: "media.channelDown",
19946
+ method: "channelDown",
19947
+ label: t("Channel down", "Canale precedente"),
19948
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
19949
+ icon: "skip-back"
19950
+ },
19581
19951
  {
19582
19952
  id: "media.play",
19583
19953
  method: "play",
@@ -49547,6 +49917,244 @@ var controlCapability = {
49547
49917
  */
49548
49918
  history: "none"
49549
49919
  };
49920
+ /**
49921
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49922
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49923
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49924
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49925
+ *
49926
+ * The cap declares a wide method surface; not every backend supports
49927
+ * every method. Drivers signal sub-capability via the
49928
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49929
+ * corresponding method) so the UI gates buttons without a slice
49930
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49931
+ */
49932
+ var MediaPlayerStateSchema = _enum([
49933
+ "off",
49934
+ "on",
49935
+ "idle",
49936
+ "playing",
49937
+ "paused",
49938
+ "buffering",
49939
+ "standby"
49940
+ ]);
49941
+ var MediaPlayerRepeatSchema = _enum([
49942
+ "off",
49943
+ "all",
49944
+ "one"
49945
+ ]);
49946
+ var MediaInfoSchema = object({
49947
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49948
+ * `podcast`, …). */
49949
+ type: string(),
49950
+ /** Human-readable title. */
49951
+ title: string(),
49952
+ /** Optional artist / channel / station label. */
49953
+ artist: string().optional(),
49954
+ /** Optional album / season / show name. */
49955
+ album: string().optional(),
49956
+ /** Optional cover-art / thumbnail URL. */
49957
+ imageUrl: string().optional()
49958
+ });
49959
+ /**
49960
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49961
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49962
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49963
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49964
+ * guess, never as a reading.
49965
+ */
49966
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49967
+ /** The fields of a media-player status a driver can only guess. */
49968
+ var MediaPlayerAssumedFieldSchema = _enum([
49969
+ "state",
49970
+ "volumeLevel",
49971
+ "isMuted",
49972
+ "source"
49973
+ ]);
49974
+ var MediaPlayerStatusSchema = object({
49975
+ /** Playback lifecycle state. */
49976
+ state: MediaPlayerStateSchema,
49977
+ /**
49978
+ * Whether the state fields are readings or the driver's own bookkeeping.
49979
+ * A slice written before this field existed parses as `reported`.
49980
+ */
49981
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49982
+ /**
49983
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49984
+ * this is not empty; a field not listed here is a reading even then (power
49985
+ * from a running-state source, mute guessed from the last send).
49986
+ */
49987
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49988
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49989
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49990
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
49991
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49992
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49993
+ isMuted: boolean().nullable(),
49994
+ /** Active source / input. Empty when no source surface. */
49995
+ source: string(),
49996
+ /** Selectable sources. Empty when no source surface.
49997
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49998
+ availableSources: array(string()),
49999
+ /** Currently-playing media info. Null when nothing is playing. */
50000
+ currentMedia: MediaInfoSchema.nullable(),
50001
+ /** Current playback position in ms. Null when not seekable or
50002
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
50003
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
50004
+ /** Total duration of the current media in ms. Null when unknown
50005
+ * (live stream) or nothing is playing. */
50006
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
50007
+ /** Shuffle toggle. Null when no shuffle surface.
50008
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
50009
+ shuffle: boolean().nullable(),
50010
+ /** Repeat mode. Null when no repeat surface.
50011
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
50012
+ repeat: MediaPlayerRepeatSchema.nullable(),
50013
+ /** Ms epoch when the slice was last updated. */
50014
+ lastChangedAt: number()
50015
+ });
50016
+ var stepInput = object({
50017
+ deviceId: number().int().nonnegative(),
50018
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
50019
+ steps: number().int().min(1).max(50).default(1)
50020
+ });
50021
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
50022
+ var mediaPlayerCapability = {
50023
+ name: "media-player",
50024
+ scope: "device",
50025
+ deviceNative: true,
50026
+ mode: "singleton",
50027
+ deviceTypes: [DeviceType.MediaPlayer],
50028
+ methods: {
50029
+ turnOn: method(deviceOnly, _void(), {
50030
+ kind: "mutation",
50031
+ auth: "protected"
50032
+ }),
50033
+ turnOff: method(deviceOnly, _void(), {
50034
+ kind: "mutation",
50035
+ auth: "protected"
50036
+ }),
50037
+ volumeUp: method(stepInput, _void(), {
50038
+ kind: "mutation",
50039
+ auth: "protected"
50040
+ }),
50041
+ volumeDown: method(stepInput, _void(), {
50042
+ kind: "mutation",
50043
+ auth: "protected"
50044
+ }),
50045
+ channelUp: method(stepInput, _void(), {
50046
+ kind: "mutation",
50047
+ auth: "protected"
50048
+ }),
50049
+ channelDown: method(stepInput, _void(), {
50050
+ kind: "mutation",
50051
+ auth: "protected"
50052
+ }),
50053
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
50054
+ kind: "mutation",
50055
+ auth: "protected"
50056
+ }),
50057
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
50058
+ kind: "mutation",
50059
+ auth: "protected"
50060
+ }),
50061
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
50062
+ kind: "mutation",
50063
+ auth: "protected"
50064
+ }),
50065
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
50066
+ kind: "mutation",
50067
+ auth: "protected"
50068
+ }),
50069
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
50070
+ kind: "mutation",
50071
+ auth: "protected"
50072
+ }),
50073
+ seek: method(object({
50074
+ deviceId: number().int().nonnegative(),
50075
+ position: number().int().nonnegative()
50076
+ }), _void(), {
50077
+ kind: "mutation",
50078
+ auth: "protected"
50079
+ }),
50080
+ setVolume: method(object({
50081
+ deviceId: number().int().nonnegative(),
50082
+ volumeLevel: number().min(0).max(100)
50083
+ }), _void(), {
50084
+ kind: "mutation",
50085
+ auth: "protected"
50086
+ }),
50087
+ setMute: method(object({
50088
+ deviceId: number().int().nonnegative(),
50089
+ muted: boolean()
50090
+ }), _void(), {
50091
+ kind: "mutation",
50092
+ auth: "protected"
50093
+ }),
50094
+ setShuffle: method(object({
50095
+ deviceId: number().int().nonnegative(),
50096
+ shuffle: boolean()
50097
+ }), _void(), {
50098
+ kind: "mutation",
50099
+ auth: "protected"
50100
+ }),
50101
+ setRepeat: method(object({
50102
+ deviceId: number().int().nonnegative(),
50103
+ repeat: MediaPlayerRepeatSchema
50104
+ }), _void(), {
50105
+ kind: "mutation",
50106
+ auth: "protected"
50107
+ }),
50108
+ selectSource: method(object({
50109
+ deviceId: number().int().nonnegative(),
50110
+ source: string().min(1)
50111
+ }), _void(), {
50112
+ kind: "mutation",
50113
+ auth: "protected"
50114
+ }),
50115
+ playMedia: method(object({
50116
+ deviceId: number().int().nonnegative(),
50117
+ /** Media identifier / URL. */
50118
+ mediaId: string().min(1),
50119
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
50120
+ * passes it through to the upstream service. */
50121
+ mediaType: string().min(1)
50122
+ }), _void(), {
50123
+ kind: "mutation",
50124
+ auth: "protected"
50125
+ })
50126
+ },
50127
+ status: {
50128
+ schema: MediaPlayerStatusSchema,
50129
+ kind: "push"
50130
+ },
50131
+ /**
50132
+ * Runtime-state slice — mirrored by the kernel. UI players read the
50133
+ * full slice for live now-playing, volume, and progress updates
50134
+ * without polling.
50135
+ */
50136
+ runtimeState: MediaPlayerStatusSchema,
50137
+ /**
50138
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
50139
+ *
50140
+ * See `RuntimeStateDurability`. Enforced by
50141
+ * `scripts/check-runtime-state-durability.ts`.
50142
+ */
50143
+ durability: "session",
50144
+ /**
50145
+ * History (D850): volume (a level an operator or an automation sets; the
50146
+ * default 10 s floor folds a slider drag into its end value) and mute.
50147
+ * `position` is not recorded: it moves continuously while playing.
50148
+ */
50149
+ history: { fields: {
50150
+ volumeLevel: {
50151
+ kind: "gauge",
50152
+ unit: "%",
50153
+ deadband: 1
50154
+ },
50155
+ isMuted: { kind: "boolean" }
50156
+ } }
50157
+ };
49550
50158
  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;
49551
50159
  var NUMBER = {
49552
50160
  kind: "fixed",
@@ -49994,117 +50602,6 @@ function actuator(input) {
49994
50602
  };
49995
50603
  }
49996
50604
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
49997
- var PowerSourceRefusalReasonSchema = _enum([
49998
- "no-power-source",
49999
- "switch-missing",
50000
- "switch-offline",
50001
- "switch-unknown",
50002
- "restart-in-progress",
50003
- "switch-call-failed"
50004
- ]);
50005
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
50006
- accepted: literal(false),
50007
- reason: PowerSourceRefusalReasonSchema,
50008
- /** One line for the operator, naming the switch. */
50009
- message: string()
50010
- })]);
50011
- var PowerSourceInfoSchema = object({
50012
- /** False when the device has no power source linked; the rest is then empty. */
50013
- linked: boolean(),
50014
- switchDeviceId: number().nullable(),
50015
- switchName: string().nullable(),
50016
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
50017
- switchOn: boolean().nullable(),
50018
- switchOnline: boolean().nullable(),
50019
- restartInProgress: boolean(),
50020
- offSeconds: number().int()
50021
- });
50022
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
50023
- var PowerSourceStateSchema = object({
50024
- /** Null while the switch has not reported: unknown is never `false` (D393). */
50025
- switchOn: boolean().nullable(),
50026
- offSeconds: number().int(),
50027
- lastFetchedAt: number()
50028
- });
50029
- var POWER_SOURCE_DEVICE_TYPES = [
50030
- DeviceType.Gateway,
50031
- DeviceType.Hub,
50032
- DeviceType.Camera,
50033
- DeviceType.Sensor,
50034
- DeviceType.MediaPlayer,
50035
- DeviceType.Vacuum,
50036
- DeviceType.Generic,
50037
- DeviceType.Washer,
50038
- DeviceType.Dryer,
50039
- DeviceType.Dishwasher,
50040
- DeviceType.PetFountain,
50041
- DeviceType.Fan,
50042
- DeviceType.Humidifier,
50043
- DeviceType.Climate,
50044
- DeviceType.WaterHeater
50045
- ];
50046
- var powerSourceCustomize = {
50047
- version: 1,
50048
- targets: [{
50049
- id: "switch",
50050
- cardinality: "scalar",
50051
- vocabulary: {
50052
- cap: "power-source",
50053
- stateField: "switchOn"
50054
- },
50055
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
50056
- apply: "add",
50057
- path: "switchOn",
50058
- sources: [{
50059
- kind: "switch",
50060
- transforms: [{ kind: "identity" }]
50061
- }],
50062
- companions: [{
50063
- kind: "constant",
50064
- path: "offSeconds",
50065
- value: 10,
50066
- when: "add"
50067
- }, {
50068
- kind: "clock",
50069
- path: "lastFetchedAt",
50070
- when: "always"
50071
- }],
50072
- sourceReport: { status: "assignment-status" }
50073
- }]
50074
- };
50075
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
50076
- var powerSourceCapability = {
50077
- name: "power-source",
50078
- scope: "device",
50079
- mode: "singleton",
50080
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
50081
- methods: {
50082
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
50083
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50084
- kind: "mutation",
50085
- auth: "admin"
50086
- }),
50087
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50088
- kind: "mutation",
50089
- auth: "admin"
50090
- }),
50091
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
50092
- kind: "mutation",
50093
- auth: "admin"
50094
- })
50095
- },
50096
- runtimeState: PowerSourceStateSchema,
50097
- customize: powerSourceCustomize,
50098
- /**
50099
- * Runtime-state durability: **session** — mirrored from the linked switch on
50100
- * its next report; a restored value would outlive the reading it names.
50101
- *
50102
- * See `RuntimeStateDurability`. Enforced by
50103
- * `scripts/check-runtime-state-durability.ts`.
50104
- */
50105
- durability: "session",
50106
- history: "none"
50107
- };
50108
50605
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
50109
50606
  DeviceType.Camera;
50110
50607
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -50132,6 +50629,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
50132
50629
  }).optional()
50133
50630
  })) });
50134
50631
  /**
50632
+ * A saved Customize assignment (D936). The schema is the type: callers use
50633
+ * `z.infer`, they do not restate the shape.
50634
+ */
50635
+ var OverrideSourceSchema = object({
50636
+ addonId: string().min(1),
50637
+ stableId: string().min(1),
50638
+ cap: string().min(1),
50639
+ fieldPath: string().min(1)
50640
+ });
50641
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
50642
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
50643
+ var ThresholdTransformDraftSchema = object({
50644
+ kind: literal("threshold"),
50645
+ comparison: _enum(["gte", "lte"]),
50646
+ threshold: number(),
50647
+ startDwellMs: number().nonnegative(),
50648
+ stopDwellMs: number().nonnegative()
50649
+ });
50650
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
50651
+ IdentityTransformDraftSchema,
50652
+ InvertTransformDraftSchema,
50653
+ ThresholdTransformDraftSchema,
50654
+ object({
50655
+ kind: literal("linear"),
50656
+ scale: number(),
50657
+ offset: number(),
50658
+ clampMin: number().optional(),
50659
+ clampMax: number().optional()
50660
+ }),
50661
+ object({
50662
+ kind: literal("enum-map"),
50663
+ mapping: record(string(), string().nullable())
50664
+ }),
50665
+ object({ kind: literal("number") })
50666
+ ]);
50667
+ var ScalarOverrideDraftSchema = object({
50668
+ kind: literal("scalar"),
50669
+ cap: string().min(1),
50670
+ targetId: string().min(1),
50671
+ source: OverrideSourceSchema,
50672
+ transform: CustomizeTransformDraftSchema
50673
+ });
50674
+ var KeyedArrayOverrideDraftSchema = object({
50675
+ kind: literal("keyedArray"),
50676
+ cap: string().min(1),
50677
+ targetId: string().min(1),
50678
+ key: string().min(1),
50679
+ label: string().min(1),
50680
+ fieldPath: string().min(1),
50681
+ source: OverrideSourceSchema,
50682
+ transform: CustomizeTransformDraftSchema
50683
+ });
50684
+ var PulseOverrideDraftSchema = object({
50685
+ kind: literal("pulse"),
50686
+ cap: string().min(1),
50687
+ targetId: string().min(1),
50688
+ enabled: boolean(),
50689
+ debounceMs: number().nonnegative(),
50690
+ edges: array(_enum([
50691
+ "rising",
50692
+ "button",
50693
+ "native-event"
50694
+ ])).min(1).optional(),
50695
+ sources: array(OverrideSourceSchema).min(1)
50696
+ });
50697
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
50698
+ "in-addition",
50699
+ "confirm",
50700
+ "replace"
50701
+ ]).optional() });
50702
+ discriminatedUnion("kind", [
50703
+ ScalarOverrideDraftSchema,
50704
+ KeyedArrayOverrideDraftSchema,
50705
+ PulseOverrideDraftSchema,
50706
+ object({
50707
+ kind: literal("span"),
50708
+ cap: string().min(1),
50709
+ targetId: string().min(1),
50710
+ timeoutMs: number().nonnegative(),
50711
+ cooldownMs: number().nonnegative().optional(),
50712
+ sources: array(SpanSourceDraftSchema)
50713
+ })
50714
+ ]);
50715
+ object({
50716
+ targetId: string().min(1),
50717
+ configured: boolean(),
50718
+ source: _enum(["native", "assigned"]),
50719
+ state: _enum([
50720
+ "active",
50721
+ "stale",
50722
+ "released",
50723
+ "unavailable"
50724
+ ]),
50725
+ reason: string().nullable()
50726
+ });
50727
+ /**
50728
+ * What a running-threshold proposal says (planning#405). The schema is the
50729
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
50730
+ *
50731
+ * A proposal is read-only advice: it carries the numbers it was derived from
50732
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
50733
+ * WHY with a typed reason and the catalog defaults, never an invented number.
50734
+ */
50735
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
50736
+ var RunningProposalConfidenceSchema = _enum([
50737
+ "high",
50738
+ "medium",
50739
+ "low"
50740
+ ]);
50741
+ var RunningProposalInsufficientReasonSchema = _enum([
50742
+ "no-history",
50743
+ "span-too-short",
50744
+ "truncated",
50745
+ "no-idle-floor",
50746
+ "no-runs",
50747
+ "no-separation"
50748
+ ]);
50749
+ var RunningProposalReplaySchema = object({
50750
+ /** Intervals the real dwell expression held true over the window. */
50751
+ intervals: number().int().nonnegative(),
50752
+ /** Intervals outside every run, or a second interval inside one. */
50753
+ extra: number().int().nonnegative(),
50754
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
50755
+ missed: number().int().nonnegative()
50756
+ });
50757
+ var RunningProposalEvidenceSchema = object({
50758
+ tier: RunningProposalTierSchema,
50759
+ spanMs: number().nonnegative(),
50760
+ from: number(),
50761
+ to: number(),
50762
+ points: number().int().nonnegative(),
50763
+ runsFound: number().int().nonnegative(),
50764
+ pausesFound: number().int().nonnegative(),
50765
+ longestPauseMs: number().nonnegative(),
50766
+ idleFloorW: number(),
50767
+ noiseCeilingW: number(),
50768
+ plateauW: number(),
50769
+ replay: RunningProposalReplaySchema
50770
+ });
50771
+ var RunningProposalInsufficientEvidenceSchema = object({
50772
+ spanMs: number().nonnegative(),
50773
+ points: number().int().nonnegative(),
50774
+ runsFound: number().int().nonnegative()
50775
+ });
50776
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
50777
+ state: literal("proposal"),
50778
+ transform: ThresholdTransformDraftSchema,
50779
+ evidence: RunningProposalEvidenceSchema,
50780
+ confidence: RunningProposalConfidenceSchema,
50781
+ /** Why the confidence is not higher; empty when nothing held it back. */
50782
+ notes: array(string())
50783
+ }), object({
50784
+ state: literal("insufficient"),
50785
+ reason: RunningProposalInsufficientReasonSchema,
50786
+ evidence: RunningProposalInsufficientEvidenceSchema,
50787
+ /** The catalog defaults, offered as what stands when nothing better is known. */
50788
+ fallback: ThresholdTransformDraftSchema
50789
+ })]);
50790
+ /**
50135
50791
  * The report of a one-shot "move this owner's plaintext secrets into the
50136
50792
  * keychain" run (D878). COUNTS only - never a value, never a ref.
50137
50793
  *
@@ -50421,6 +51077,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
50421
51077
  kind: "mutation",
50422
51078
  auth: "admin",
50423
51079
  access: "view"
51080
+ }), method(object({
51081
+ deviceId: number().int(),
51082
+ cap: string().min(1),
51083
+ fieldPath: string().min(1),
51084
+ deviceType: _enum(DeviceType).optional()
51085
+ }), RunningThresholdProposalSchema, {
51086
+ auth: "admin",
51087
+ access: "view"
50424
51088
  }), method(object({
50425
51089
  blockId: string().min(1),
50426
51090
  trace: AutomationRunTraceSchema.optional(),
@@ -51397,7 +52061,46 @@ method(object({
51397
52061
  * exposes only the COMMON surface: link state, supported device
51398
52062
  * kinds (so the UI can filter the device picker), and per-device
51399
52063
  * expose/unexpose.
52064
+ *
52065
+ * INSTANCES. One export addon can serve several independent targets (a
52066
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
52067
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
52068
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
52069
+ * default instance's); present = that instance; an id the provider does not
52070
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
52071
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
52072
+ * change always names the target it changes. A single-instance provider
52073
+ * accepts an absent id on a read or its own id, and refuses any other.
52074
+ */
52075
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
52076
+ var InstanceIdSchema$1 = string().min(1);
52077
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
52078
+ /** One independent export target of an addon. */
52079
+ var ExportInstanceSchema = object({
52080
+ instanceId: InstanceIdSchema$1,
52081
+ name: string(),
52082
+ enabled: boolean(),
52083
+ kind: string().optional()
52084
+ });
52085
+ var EXPORT_INSTANCE_UNKNOWN = "export-instance-unknown";
52086
+ /** The named failure of an `instanceId` the provider does not serve. The message starts with the code. */
52087
+ var ExportInstanceUnknownError = class extends Error {
52088
+ code = EXPORT_INSTANCE_UNKNOWN;
52089
+ instanceId;
52090
+ constructor(addon, instanceId) {
52091
+ super(`${EXPORT_INSTANCE_UNKNOWN}: ${addon} serves no instance "${instanceId}"`);
52092
+ this.name = "ExportInstanceUnknownError";
52093
+ this.instanceId = instanceId;
52094
+ }
52095
+ };
52096
+ /**
52097
+ * The instance a `getStatus` call names. The cap's `status` block injects a per-device
52098
+ * `getStatus({ deviceId })` beside the method's own `{ instanceId? }`, so the provider sees
52099
+ * both shapes; only the second names an instance (a per-device status read is addon-wide).
51400
52100
  */
52101
+ function statusInstanceOf(input) {
52102
+ return "instanceId" in input ? input.instanceId : void 0;
52103
+ }
51401
52104
  var LinkStateSchema = _enum([
51402
52105
  "unlinked",
51403
52106
  "linked",
@@ -51448,10 +52151,14 @@ var ExposedDeviceSchema = object({
51448
52151
  capabilities: array(string()).optional()
51449
52152
  });
51450
52153
  var ExposeInputSchema = object({
52154
+ instanceId: InstanceIdSchema$1,
51451
52155
  deviceId: string(),
51452
52156
  capabilities: array(string()).optional()
51453
52157
  });
51454
- var UnexposeInputSchema = object({ deviceId: string() });
52158
+ var UnexposeInputSchema = object({
52159
+ instanceId: InstanceIdSchema$1,
52160
+ deviceId: string()
52161
+ });
51455
52162
  /**
51456
52163
  * What an exporter may expose besides devices (D968). An action and a
51457
52164
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -51480,8 +52187,14 @@ var ExposedEntitySchema = object({
51480
52187
  entity: ExportEntityRefSchema,
51481
52188
  exposedAs: string().optional()
51482
52189
  });
51483
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
51484
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
52190
+ var ExposeEntityInputSchema = object({
52191
+ instanceId: InstanceIdSchema$1,
52192
+ entity: ExportEntityRefSchema
52193
+ });
52194
+ var UnexposeEntityInputSchema = object({
52195
+ instanceId: InstanceIdSchema$1,
52196
+ entity: ExportEntityRefSchema
52197
+ });
51485
52198
  var deviceExportCapability = {
51486
52199
  name: "device-export",
51487
52200
  scope: "system",
@@ -51501,9 +52214,10 @@ var deviceExportCapability = {
51501
52214
  */
51502
52215
  exposesDeviceSettings: true,
51503
52216
  methods: {
51504
- getStatus: method(_void(), DeviceExportStatusSchema),
51505
- listSupportedDeviceKinds: method(_void(), array(DeviceKindSchema)),
51506
- listExposedDevices: method(_void(), array(ExposedDeviceSchema)),
52217
+ getStatus: method(InstancePinSchema, DeviceExportStatusSchema),
52218
+ listInstances: method(_void(), array(ExportInstanceSchema)),
52219
+ listSupportedDeviceKinds: method(InstancePinSchema, array(DeviceKindSchema)),
52220
+ listExposedDevices: method(InstancePinSchema, array(ExposedDeviceSchema)),
51507
52221
  exposeDevice: method(ExposeInputSchema, _void(), {
51508
52222
  kind: "mutation",
51509
52223
  auth: "admin"
@@ -51513,7 +52227,7 @@ var deviceExportCapability = {
51513
52227
  auth: "admin"
51514
52228
  }),
51515
52229
  listSupportedEntityKinds: optionalMethod(_void(), array(ExportEntityKindSchema)),
51516
- listExposedEntities: optionalMethod(_void(), array(ExposedEntitySchema)),
52230
+ listExposedEntities: optionalMethod(InstancePinSchema, array(ExposedEntitySchema)),
51517
52231
  exposeEntity: optionalMethod(ExposeEntityInputSchema, _void(), {
51518
52232
  kind: "mutation",
51519
52233
  auth: "admin"
@@ -55118,15 +55832,15 @@ var FanPresetCaptureResultSchema = object({
55118
55832
  items: array(FanCaptureItemSchema),
55119
55833
  omitted: array(FanCaptureOmissionSchema)
55120
55834
  });
55121
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
55835
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
55122
55836
  DeviceType.Fan;
55123
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55837
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
55124
55838
  kind: "mutation",
55125
55839
  auth: "admin"
55126
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55840
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
55127
55841
  kind: "mutation",
55128
55842
  auth: "protected"
55129
- }), method(deviceInput$1.extend({
55843
+ }), method(deviceInput$2.extend({
55130
55844
  fields: array(FanCaptureFieldSchema).optional(),
55131
55845
  steps: array(ActionStepSchema).max(20).default([])
55132
55846
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -56037,13 +56751,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56037
56751
  */
56038
56752
  var ConfigSchemaPassthrough$3 = custom();
56039
56753
  var IntegrationInstanceIdSchema = string().min(1);
56754
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
56755
+ /**
56756
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
56757
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
56758
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
56759
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
56760
+ * instance id.
56761
+ */
56762
+ var IntegrationInstanceKindSchema = object({
56763
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
56764
+ id: string().min(1),
56765
+ label: string().min(1),
56766
+ /** The kind signs in through a server-held interactive session. */
56767
+ signIn: object({
56768
+ basePath: string().min(1),
56769
+ methods: array(_enum(["device-code", "proxy"])).optional()
56770
+ }).optional(),
56771
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
56772
+ connectPath: string().min(1).optional(),
56773
+ /** Relative hub path to disconnect that account. */
56774
+ disconnectPath: string().min(1).optional()
56775
+ });
56040
56776
  var IntegrationInstanceDescriptionSchema = object({
56041
56777
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56042
- instanceMode: _enum(["single", "multiple"]),
56778
+ instanceMode: IntegrationInstanceModeSchema,
56043
56779
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
56044
56780
  kind: string(),
56045
56781
  /** True when `testInstance` really probes the remote. */
56046
- canTest: boolean()
56782
+ canTest: boolean(),
56783
+ /**
56784
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
56785
+ * Add flow asks which first and sends it as `createInstance.kind`.
56786
+ */
56787
+ kinds: array(IntegrationInstanceKindSchema).optional(),
56788
+ /**
56789
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
56790
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
56791
+ */
56792
+ allowRestoreId: boolean().optional(),
56793
+ /**
56794
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
56795
+ * send an id (the UI then mints an opaque one).
56796
+ */
56797
+ mintsId: boolean().optional()
56047
56798
  });
56048
56799
  var IntegrationInstanceStatusSchema = object({
56049
56800
  state: _enum([
@@ -56052,7 +56803,9 @@ var IntegrationInstanceStatusSchema = object({
56052
56803
  "error",
56053
56804
  "unknown"
56054
56805
  ]),
56055
- detail: string().optional()
56806
+ detail: string().optional(),
56807
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
56808
+ label: string().optional()
56056
56809
  });
56057
56810
  var IntegrationInstanceSchema = object({
56058
56811
  id: IntegrationInstanceIdSchema,
@@ -56064,9 +56817,13 @@ var IntegrationInstanceSchema = object({
56064
56817
  * (`enabled !== false`, so a row written before the field existed is on).
56065
56818
  */
56066
56819
  enabled: boolean(),
56820
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56821
+ kind: string().optional(),
56067
56822
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
56068
56823
  formValues: record(string(), unknown()),
56069
- status: IntegrationInstanceStatusSchema.optional()
56824
+ status: IntegrationInstanceStatusSchema.optional(),
56825
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
56826
+ lastCheckedAt: number().optional()
56070
56827
  });
56071
56828
  var IntegrationInstancePatchSchema = object({
56072
56829
  id: IntegrationInstanceIdSchema,
@@ -56074,31 +56831,57 @@ var IntegrationInstancePatchSchema = object({
56074
56831
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
56075
56832
  values: record(string(), unknown())
56076
56833
  });
56077
- 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({
56078
- id: IntegrationInstanceIdSchema,
56079
- name: string().min(1)
56080
- }), object({ id: string() }), {
56081
- kind: "mutation",
56082
- auth: "admin"
56083
- }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56084
- kind: "mutation",
56085
- auth: "admin"
56086
- }), method(object({
56087
- id: IntegrationInstanceIdSchema,
56088
- enabled: boolean()
56089
- }), object({ ok: literal(true) }), {
56090
- kind: "mutation",
56091
- auth: "admin"
56092
- }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56093
- kind: "mutation",
56094
- auth: "admin"
56095
- }), optionalMethod(object({
56096
- id: IntegrationInstanceIdSchema.nullable(),
56097
- values: record(string(), unknown())
56098
- }), TestConnectionResultSchema$1, {
56099
- kind: "mutation",
56100
- auth: "admin"
56101
- });
56834
+ var integrationInstancesCapability = {
56835
+ name: "integration-instances",
56836
+ scope: "system",
56837
+ mode: "collection",
56838
+ methods: {
56839
+ describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
56840
+ /** The form; `id: null` is the create form. */
56841
+ instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
56842
+ listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
56843
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
56844
+ /**
56845
+ * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
56846
+ * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
56847
+ */
56848
+ createInstance: method(object({
56849
+ id: IntegrationInstanceIdSchema.optional(),
56850
+ name: string().min(1),
56851
+ kind: string().min(1).optional()
56852
+ }), object({ id: string() }), {
56853
+ kind: "mutation",
56854
+ auth: "admin"
56855
+ }),
56856
+ updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56857
+ kind: "mutation",
56858
+ auth: "admin"
56859
+ }),
56860
+ /**
56861
+ * Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
56862
+ * form patch (D62, D996). An off instance keeps its record and secrets and does no work.
56863
+ */
56864
+ setInstanceEnabled: method(object({
56865
+ id: IntegrationInstanceIdSchema,
56866
+ enabled: boolean()
56867
+ }), object({ ok: literal(true) }), {
56868
+ kind: "mutation",
56869
+ auth: "admin"
56870
+ }),
56871
+ /** Delete the record and its keychain refs together. */
56872
+ deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56873
+ kind: "mutation",
56874
+ auth: "admin"
56875
+ }),
56876
+ testInstance: optionalMethod(object({
56877
+ id: IntegrationInstanceIdSchema.nullable(),
56878
+ values: record(string(), unknown())
56879
+ }), TestConnectionResultSchema$1, {
56880
+ kind: "mutation",
56881
+ auth: "admin"
56882
+ })
56883
+ }
56884
+ };
56102
56885
  /**
56103
56886
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
56104
56887
  * session (server-initiated offer pattern, identical to the main
@@ -58116,6 +58899,11 @@ var LogEntrySchema = object({
58116
58899
  * flight, and not asking for the next slice ends the scan.
58117
58900
  */
58118
58901
  var LogReadQuerySchema = object({
58902
+ /**
58903
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
58904
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
58905
+ */
58906
+ instanceId: string().min(1).max(200).optional(),
58119
58907
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
58120
58908
  requestId: string().min(1).max(64).optional(),
58121
58909
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -58178,6 +58966,8 @@ var LogReadResultSchema = object({
58178
58966
  /** True when `cancelRead` ended this slice early. */
58179
58967
  cancelled: boolean().optional()
58180
58968
  });
58969
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
58970
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
58181
58971
  var LogReadCapabilitiesSchema = object({
58182
58972
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
58183
58973
  readable: boolean(),
@@ -58196,7 +58986,7 @@ var LogReadCapabilitiesSchema = object({
58196
58986
  /** How far back the store reaches, when the provider knows (days). */
58197
58987
  retentionDays: number().nullable()
58198
58988
  });
58199
- 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() }), {
58989
+ 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() }), {
58200
58990
  kind: "mutation",
58201
58991
  auth: "admin"
58202
58992
  });
@@ -58300,214 +59090,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
58300
59090
  ]);
58301
59091
  method(_void(), array(LoginMethodContributionSchema).readonly());
58302
59092
  /**
58303
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
58304
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
58305
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
58306
- * surface — those use `DeviceType.Camera` + the broker pipeline.
58307
- *
58308
- * The cap declares a wide method surface; not every backend supports
58309
- * every method. Drivers signal sub-capability via the
58310
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
58311
- * corresponding method) so the UI gates buttons without a slice
58312
- * fetch — same pattern as `PanTiltZoom` for cameras.
58313
- */
58314
- var MediaPlayerStateSchema = _enum([
58315
- "off",
58316
- "on",
58317
- "idle",
58318
- "playing",
58319
- "paused",
58320
- "buffering",
58321
- "standby"
58322
- ]);
58323
- var MediaPlayerRepeatSchema = _enum([
58324
- "off",
58325
- "all",
58326
- "one"
58327
- ]);
58328
- var MediaInfoSchema = object({
58329
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
58330
- * `podcast`, …). */
58331
- type: string(),
58332
- /** Human-readable title. */
58333
- title: string(),
58334
- /** Optional artist / channel / station label. */
58335
- artist: string().optional(),
58336
- /** Optional album / season / show name. */
58337
- album: string().optional(),
58338
- /** Optional cover-art / thumbnail URL. */
58339
- imageUrl: string().optional()
58340
- });
58341
- /**
58342
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
58343
- * the device said so. `assumed`: the driver has no feedback (infrared) and
58344
- * tracks what it last SENT; it moves only after a send was accepted, and a
58345
- * human at the real remote makes it wrong. A UI shows an assumed value as a
58346
- * guess, never as a reading.
58347
- */
58348
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
58349
- /** The fields of a media-player status a driver can only guess. */
58350
- var MediaPlayerAssumedFieldSchema = _enum([
58351
- "state",
58352
- "volumeLevel",
58353
- "isMuted",
58354
- "source"
58355
- ]);
58356
- var MediaPlayerStatusSchema = object({
58357
- /** Playback lifecycle state. */
58358
- state: MediaPlayerStateSchema,
58359
- /**
58360
- * Whether the state fields are readings or the driver's own bookkeeping.
58361
- * A slice written before this field existed parses as `reported`.
58362
- */
58363
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
58364
- /**
58365
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
58366
- * this is not empty; a field not listed here is a reading even then (power
58367
- * from a running-state source, mute guessed from the last send).
58368
- */
58369
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
58370
- /** Volume as 0..100 inclusive. Null when the device has no volume
58371
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
58372
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58373
- /** Mute toggle distinct from volume=0. Null when no mute surface.
58374
- * Pair with `DeviceFeature.MediaPlayerMute`. */
58375
- isMuted: boolean().nullable(),
58376
- /** Active source / input. Empty when no source surface. */
58377
- source: string(),
58378
- /** Selectable sources. Empty when no source surface.
58379
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
58380
- availableSources: array(string()),
58381
- /** Currently-playing media info. Null when nothing is playing. */
58382
- currentMedia: MediaInfoSchema.nullable(),
58383
- /** Current playback position in ms. Null when not seekable or
58384
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
58385
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58386
- /** Total duration of the current media in ms. Null when unknown
58387
- * (live stream) or nothing is playing. */
58388
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58389
- /** Shuffle toggle. Null when no shuffle surface.
58390
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
58391
- shuffle: boolean().nullable(),
58392
- /** Repeat mode. Null when no repeat surface.
58393
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
58394
- repeat: MediaPlayerRepeatSchema.nullable(),
58395
- /** Ms epoch when the slice was last updated. */
58396
- lastChangedAt: number()
58397
- });
58398
- var mediaPlayerCapability = {
58399
- name: "media-player",
58400
- scope: "device",
58401
- deviceNative: true,
58402
- mode: "singleton",
58403
- deviceTypes: [DeviceType.MediaPlayer],
58404
- methods: {
58405
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
58406
- kind: "mutation",
58407
- auth: "protected"
58408
- }),
58409
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
58410
- kind: "mutation",
58411
- auth: "protected"
58412
- }),
58413
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
58414
- kind: "mutation",
58415
- auth: "protected"
58416
- }),
58417
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
58418
- kind: "mutation",
58419
- auth: "protected"
58420
- }),
58421
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
58422
- kind: "mutation",
58423
- auth: "protected"
58424
- }),
58425
- seek: method(object({
58426
- deviceId: number().int().nonnegative(),
58427
- position: number().int().nonnegative()
58428
- }), _void(), {
58429
- kind: "mutation",
58430
- auth: "protected"
58431
- }),
58432
- setVolume: method(object({
58433
- deviceId: number().int().nonnegative(),
58434
- volumeLevel: number().min(0).max(100)
58435
- }), _void(), {
58436
- kind: "mutation",
58437
- auth: "protected"
58438
- }),
58439
- setMute: method(object({
58440
- deviceId: number().int().nonnegative(),
58441
- muted: boolean()
58442
- }), _void(), {
58443
- kind: "mutation",
58444
- auth: "protected"
58445
- }),
58446
- setShuffle: method(object({
58447
- deviceId: number().int().nonnegative(),
58448
- shuffle: boolean()
58449
- }), _void(), {
58450
- kind: "mutation",
58451
- auth: "protected"
58452
- }),
58453
- setRepeat: method(object({
58454
- deviceId: number().int().nonnegative(),
58455
- repeat: MediaPlayerRepeatSchema
58456
- }), _void(), {
58457
- kind: "mutation",
58458
- auth: "protected"
58459
- }),
58460
- selectSource: method(object({
58461
- deviceId: number().int().nonnegative(),
58462
- source: string().min(1)
58463
- }), _void(), {
58464
- kind: "mutation",
58465
- auth: "protected"
58466
- }),
58467
- playMedia: method(object({
58468
- deviceId: number().int().nonnegative(),
58469
- /** Media identifier / URL. */
58470
- mediaId: string().min(1),
58471
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
58472
- * passes it through to the upstream service. */
58473
- mediaType: string().min(1)
58474
- }), _void(), {
58475
- kind: "mutation",
58476
- auth: "protected"
58477
- })
58478
- },
58479
- status: {
58480
- schema: MediaPlayerStatusSchema,
58481
- kind: "push"
58482
- },
58483
- /**
58484
- * Runtime-state slice — mirrored by the kernel. UI players read the
58485
- * full slice for live now-playing, volume, and progress updates
58486
- * without polling.
58487
- */
58488
- runtimeState: MediaPlayerStatusSchema,
58489
- /**
58490
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
58491
- *
58492
- * See `RuntimeStateDurability`. Enforced by
58493
- * `scripts/check-runtime-state-durability.ts`.
58494
- */
58495
- durability: "session",
58496
- /**
58497
- * History (D850): volume (a level an operator or an automation sets; the
58498
- * default 10 s floor folds a slider drag into its end value) and mute.
58499
- * `position` is not recorded: it moves continuously while playing.
58500
- */
58501
- history: { fields: {
58502
- volumeLevel: {
58503
- kind: "gauge",
58504
- unit: "%",
58505
- deadband: 1
58506
- },
58507
- isMuted: { kind: "boolean" }
58508
- } }
58509
- };
58510
- /**
58511
59093
  * mesh-network — collection cap for mesh-VPN providers.
58512
59094
  *
58513
59095
  * Different shape from `network-access` (public tunnels):
@@ -70747,6 +71329,14 @@ var powerMeterCapability = {
70747
71329
  deviceNative: true,
70748
71330
  mode: "singleton",
70749
71331
  deviceTypes: POWER_METER_DEVICE_TYPES,
71332
+ deviceConfig: { ui: {
71333
+ kind: "widget",
71334
+ widgetId: "host/power-meter-line",
71335
+ tab: "power",
71336
+ placement: "hero",
71337
+ label: "Power",
71338
+ order: 4
71339
+ } },
70750
71340
  methods: {},
70751
71341
  status: {
70752
71342
  schema: PowerMeterStatusSchema,
@@ -70806,6 +71396,117 @@ var powerMeterCapability = {
70806
71396
  volatileStateFields: ["lastFetchedAt"],
70807
71397
  customize: powerMeterCustomize
70808
71398
  };
71399
+ var PowerSourceRefusalReasonSchema = _enum([
71400
+ "no-power-source",
71401
+ "switch-missing",
71402
+ "switch-offline",
71403
+ "switch-unknown",
71404
+ "restart-in-progress",
71405
+ "switch-call-failed"
71406
+ ]);
71407
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
71408
+ accepted: literal(false),
71409
+ reason: PowerSourceRefusalReasonSchema,
71410
+ /** One line for the operator, naming the switch. */
71411
+ message: string()
71412
+ })]);
71413
+ var PowerSourceInfoSchema = object({
71414
+ /** False when the device has no power source linked; the rest is then empty. */
71415
+ linked: boolean(),
71416
+ switchDeviceId: number().nullable(),
71417
+ switchName: string().nullable(),
71418
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
71419
+ switchOn: boolean().nullable(),
71420
+ switchOnline: boolean().nullable(),
71421
+ restartInProgress: boolean(),
71422
+ offSeconds: number().int()
71423
+ });
71424
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
71425
+ var PowerSourceStateSchema = object({
71426
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
71427
+ switchOn: boolean().nullable(),
71428
+ offSeconds: number().int(),
71429
+ lastFetchedAt: number()
71430
+ });
71431
+ var POWER_SOURCE_DEVICE_TYPES = [
71432
+ DeviceType.Gateway,
71433
+ DeviceType.Hub,
71434
+ DeviceType.Camera,
71435
+ DeviceType.Sensor,
71436
+ DeviceType.MediaPlayer,
71437
+ DeviceType.Vacuum,
71438
+ DeviceType.Generic,
71439
+ DeviceType.Washer,
71440
+ DeviceType.Dryer,
71441
+ DeviceType.Dishwasher,
71442
+ DeviceType.PetFountain,
71443
+ DeviceType.Fan,
71444
+ DeviceType.Humidifier,
71445
+ DeviceType.Climate,
71446
+ DeviceType.WaterHeater
71447
+ ];
71448
+ var powerSourceCustomize = {
71449
+ version: 1,
71450
+ targets: [{
71451
+ id: "switch",
71452
+ cardinality: "scalar",
71453
+ vocabulary: {
71454
+ cap: "power-source",
71455
+ stateField: "switchOn"
71456
+ },
71457
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71458
+ apply: "add",
71459
+ path: "switchOn",
71460
+ sources: [{
71461
+ kind: "switch",
71462
+ transforms: [{ kind: "identity" }]
71463
+ }],
71464
+ companions: [{
71465
+ kind: "constant",
71466
+ path: "offSeconds",
71467
+ value: 10,
71468
+ when: "add"
71469
+ }, {
71470
+ kind: "clock",
71471
+ path: "lastFetchedAt",
71472
+ when: "always"
71473
+ }],
71474
+ sourceReport: { status: "assignment-status" }
71475
+ }]
71476
+ };
71477
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
71478
+ var powerSourceCapability = {
71479
+ name: "power-source",
71480
+ scope: "device",
71481
+ mode: "singleton",
71482
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
71483
+ methods: {
71484
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
71485
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71486
+ kind: "mutation",
71487
+ auth: "admin"
71488
+ }),
71489
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71490
+ kind: "mutation",
71491
+ auth: "admin"
71492
+ }),
71493
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
71494
+ kind: "mutation",
71495
+ auth: "admin"
71496
+ })
71497
+ },
71498
+ runtimeState: PowerSourceStateSchema,
71499
+ customize: powerSourceCustomize,
71500
+ /**
71501
+ * Runtime-state durability: **session** — mirrored from the linked switch on
71502
+ * its next report; a restored value would outlive the reading it names.
71503
+ *
71504
+ * See `RuntimeStateDurability`. Enforced by
71505
+ * `scripts/check-runtime-state-durability.ts`.
71506
+ */
71507
+ durability: "session",
71508
+ history: "none"
71509
+ };
70809
71510
  /**
70810
71511
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
70811
71512
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -74308,7 +75009,13 @@ var RemoteMediaCommandSchema = _enum([
74308
75009
  "next",
74309
75010
  "previous",
74310
75011
  "setMute",
74311
- "selectSource"
75012
+ "selectSource",
75013
+ "turnOn",
75014
+ "turnOff",
75015
+ "volumeUp",
75016
+ "volumeDown",
75017
+ "channelUp",
75018
+ "channelDown"
74312
75019
  ]);
74313
75020
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
74314
75021
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -74329,7 +75036,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
74329
75036
  }), RemoteSendResultSchema, {
74330
75037
  kind: "mutation",
74331
75038
  auth: "protected"
74332
- }), method(deviceInput.extend({
75039
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
74333
75040
  command: RemoteMediaCommandSchema,
74334
75041
  muted: boolean().optional(),
74335
75042
  source: string().optional(),
@@ -76583,7 +77290,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
76583
77290
  * `getSettingsUISchema()` surface.
76584
77291
  */
76585
77292
  var SwitchStatusSchema = object({
76586
- on: boolean().meta({ label: "On" }),
77293
+ /**
77294
+ * `null` = the device has never reported its state (a measurement that
77295
+ * has not happened is unknown, never `false`).
77296
+ */
77297
+ on: boolean().nullable().meta({ label: "On" }),
76587
77298
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
76588
77299
  lastChangedAt: number()
76589
77300
  });
@@ -76608,7 +77319,7 @@ var switchCapability = {
76608
77319
  }) },
76609
77320
  events: { onStateChanged: { data: object({
76610
77321
  deviceId: number(),
76611
- on: boolean(),
77322
+ on: boolean().nullable(),
76612
77323
  lastChangedAt: number()
76613
77324
  }) } },
76614
77325
  status: {
@@ -80291,6 +81002,26 @@ function buildDescriptor(capName, kind) {
80291
81002
  * contributes one entry per bound cap present in this map.
80292
81003
  */
80293
81004
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
81005
+ /**
81006
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
81007
+ * shared by the admin UI and the viewer. A role is data on the wire; each
81008
+ * client resolves its name from `label` (English and Italian side by side) and
81009
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
81010
+ * server sends an id, a client maps it to its own icon set.
81011
+ *
81012
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
81013
+ * pictogram): a client that can draw text shows it, one that cannot falls back
81014
+ * to `icon`.
81015
+ */
81016
+ function digit(n) {
81017
+ return {
81018
+ label: t(n, n),
81019
+ icon: "hash",
81020
+ legend: n
81021
+ };
81022
+ }
81023
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
81024
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
80294
81025
  var btn = (idSuffix, row, col, init) => ({
80295
81026
  idSuffix,
80296
81027
  row,
@@ -80454,6 +81185,34 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80454
81185
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80455
81186
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80456
81187
  }
81188
+ /**
81189
+ * Propose a running threshold from a device's own power history (planning#405).
81190
+ *
81191
+ * Pure and deterministic: the same series gives the same answer. The series is
81192
+ * a HELD-VALUE step series — a point's value stands until the next point — which
81193
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
81194
+ * count of samples.
81195
+ *
81196
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
81197
+ * near F. A device that is not near one level most of the time has no floor.
81198
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
81199
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
81200
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
81201
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
81202
+ * grid a person would type; stop dwell covers the longest pause; start dwell
81203
+ * covers the longest standby blip above T.
81204
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
81205
+ * run must hold exactly one true interval and nothing may flap outside.
81206
+ */
81207
+ var SECOND_MS = 1e3;
81208
+ var MINUTE_MS$1 = 60 * SECOND_MS;
81209
+ 24 * (60 * MINUTE_MS$1);
81210
+ 10 * MINUTE_MS$1;
81211
+ 30 * SECOND_MS;
81212
+ 60 * SECOND_MS;
81213
+ 20 * MINUTE_MS$1;
81214
+ 10 * SECOND_MS;
81215
+ 10 * SECOND_MS;
80457
81216
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80458
81217
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80459
81218
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83559,6 +84318,13 @@ Object.freeze({
83559
84318
  access: "create",
83560
84319
  role: "admin"
83561
84320
  },
84321
+ "coreBlocks.proposeRunningThreshold": {
84322
+ capName: "core-blocks",
84323
+ capScope: "system",
84324
+ addonId: null,
84325
+ access: "view",
84326
+ role: "admin"
84327
+ },
83562
84328
  "coreBlocks.reportAutomationRun": {
83563
84329
  capName: "core-blocks",
83564
84330
  capScope: "system",
@@ -84070,6 +84836,13 @@ Object.freeze({
84070
84836
  access: "view",
84071
84837
  role: "admin"
84072
84838
  },
84839
+ "deviceExport.listInstances": {
84840
+ capName: "device-export",
84841
+ capScope: "system",
84842
+ addonId: null,
84843
+ access: "view",
84844
+ role: "admin"
84845
+ },
84073
84846
  "deviceExport.listSupportedDeviceKinds": {
84074
84847
  capName: "device-export",
84075
84848
  capScope: "system",
@@ -86506,6 +87279,20 @@ Object.freeze({
86506
87279
  access: "view",
86507
87280
  role: "admin"
86508
87281
  },
87282
+ "mediaPlayer.channelDown": {
87283
+ capName: "media-player",
87284
+ capScope: "device",
87285
+ addonId: null,
87286
+ access: "create",
87287
+ role: "viewer"
87288
+ },
87289
+ "mediaPlayer.channelUp": {
87290
+ capName: "media-player",
87291
+ capScope: "device",
87292
+ addonId: null,
87293
+ access: "create",
87294
+ role: "viewer"
87295
+ },
86509
87296
  "mediaPlayer.getStatus": {
86510
87297
  capName: "media-player",
86511
87298
  capScope: "device",
@@ -86597,6 +87384,34 @@ Object.freeze({
86597
87384
  access: "create",
86598
87385
  role: "viewer"
86599
87386
  },
87387
+ "mediaPlayer.turnOff": {
87388
+ capName: "media-player",
87389
+ capScope: "device",
87390
+ addonId: null,
87391
+ access: "create",
87392
+ role: "viewer"
87393
+ },
87394
+ "mediaPlayer.turnOn": {
87395
+ capName: "media-player",
87396
+ capScope: "device",
87397
+ addonId: null,
87398
+ access: "create",
87399
+ role: "viewer"
87400
+ },
87401
+ "mediaPlayer.volumeDown": {
87402
+ capName: "media-player",
87403
+ capScope: "device",
87404
+ addonId: null,
87405
+ access: "create",
87406
+ role: "viewer"
87407
+ },
87408
+ "mediaPlayer.volumeUp": {
87409
+ capName: "media-player",
87410
+ capScope: "device",
87411
+ addonId: null,
87412
+ access: "create",
87413
+ role: "viewer"
87414
+ },
86600
87415
  "meshNetwork.getStatus": {
86601
87416
  capName: "mesh-network",
86602
87417
  capScope: "system",
@@ -90153,6 +90968,13 @@ Object.freeze({
90153
90968
  access: "create",
90154
90969
  role: "admin"
90155
90970
  },
90971
+ "remote.getAssumedStatus": {
90972
+ capName: "remote",
90973
+ capScope: "system",
90974
+ addonId: null,
90975
+ access: "view",
90976
+ role: "admin"
90977
+ },
90156
90978
  "remote.getConfig": {
90157
90979
  capName: "remote",
90158
90980
  capScope: "system",
@@ -92979,6 +93801,11 @@ Object.freeze({
92979
93801
  form: "single",
92980
93802
  optional: false
92981
93803
  }],
93804
+ "coreBlocks.proposeRunningThreshold": [{
93805
+ name: "deviceId",
93806
+ form: "single",
93807
+ optional: false
93808
+ }],
92982
93809
  "cover.close": [{
92983
93810
  name: "deviceId",
92984
93811
  form: "single",
@@ -93830,6 +94657,16 @@ Object.freeze({
93830
94657
  form: "single",
93831
94658
  optional: true
93832
94659
  }],
94660
+ "mediaPlayer.channelDown": [{
94661
+ name: "deviceId",
94662
+ form: "single",
94663
+ optional: false
94664
+ }],
94665
+ "mediaPlayer.channelUp": [{
94666
+ name: "deviceId",
94667
+ form: "single",
94668
+ optional: false
94669
+ }],
93833
94670
  "mediaPlayer.getStatus": [{
93834
94671
  name: "deviceId",
93835
94672
  form: "single",
@@ -93895,6 +94732,26 @@ Object.freeze({
93895
94732
  form: "single",
93896
94733
  optional: false
93897
94734
  }],
94735
+ "mediaPlayer.turnOff": [{
94736
+ name: "deviceId",
94737
+ form: "single",
94738
+ optional: false
94739
+ }],
94740
+ "mediaPlayer.turnOn": [{
94741
+ name: "deviceId",
94742
+ form: "single",
94743
+ optional: false
94744
+ }],
94745
+ "mediaPlayer.volumeDown": [{
94746
+ name: "deviceId",
94747
+ form: "single",
94748
+ optional: false
94749
+ }],
94750
+ "mediaPlayer.volumeUp": [{
94751
+ name: "deviceId",
94752
+ form: "single",
94753
+ optional: false
94754
+ }],
93898
94755
  "meter.getStatus": [{
93899
94756
  name: "deviceId",
93900
94757
  form: "single",
@@ -95060,6 +95917,11 @@ Object.freeze({
95060
95917
  form: "single",
95061
95918
  optional: true
95062
95919
  }],
95920
+ "remote.getAssumedStatus": [{
95921
+ name: "deviceId",
95922
+ form: "single",
95923
+ optional: false
95924
+ }],
95063
95925
  "remote.getConfig": [{
95064
95926
  name: "deviceId",
95065
95927
  form: "single",
@@ -95607,99 +96469,6 @@ Object.freeze({
95607
96469
  "network-access": "ingress",
95608
96470
  "smtp-provider": "email"
95609
96471
  });
95610
- /**
95611
- * A saved Customize assignment (D936). The schema is the type: callers use
95612
- * `z.infer`, they do not restate the shape.
95613
- */
95614
- var OverrideSourceSchema = object({
95615
- addonId: string().min(1),
95616
- stableId: string().min(1),
95617
- cap: string().min(1),
95618
- fieldPath: string().min(1)
95619
- });
95620
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
95621
- object({ kind: literal("identity") }),
95622
- object({ kind: literal("invert") }),
95623
- object({
95624
- kind: literal("threshold"),
95625
- comparison: _enum(["gte", "lte"]),
95626
- threshold: number(),
95627
- startDwellMs: number().nonnegative(),
95628
- stopDwellMs: number().nonnegative()
95629
- }),
95630
- object({
95631
- kind: literal("linear"),
95632
- scale: number(),
95633
- offset: number(),
95634
- clampMin: number().optional(),
95635
- clampMax: number().optional()
95636
- }),
95637
- object({
95638
- kind: literal("enum-map"),
95639
- mapping: record(string(), string().nullable())
95640
- }),
95641
- object({ kind: literal("number") })
95642
- ]);
95643
- var ScalarOverrideDraftSchema = object({
95644
- kind: literal("scalar"),
95645
- cap: string().min(1),
95646
- targetId: string().min(1),
95647
- source: OverrideSourceSchema,
95648
- transform: CustomizeTransformDraftSchema
95649
- });
95650
- var KeyedArrayOverrideDraftSchema = object({
95651
- kind: literal("keyedArray"),
95652
- cap: string().min(1),
95653
- targetId: string().min(1),
95654
- key: string().min(1),
95655
- label: string().min(1),
95656
- fieldPath: string().min(1),
95657
- source: OverrideSourceSchema,
95658
- transform: CustomizeTransformDraftSchema
95659
- });
95660
- var PulseOverrideDraftSchema = object({
95661
- kind: literal("pulse"),
95662
- cap: string().min(1),
95663
- targetId: string().min(1),
95664
- enabled: boolean(),
95665
- debounceMs: number().nonnegative(),
95666
- edges: array(_enum([
95667
- "rising",
95668
- "button",
95669
- "native-event"
95670
- ])).min(1).optional(),
95671
- sources: array(OverrideSourceSchema).min(1)
95672
- });
95673
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
95674
- "in-addition",
95675
- "confirm",
95676
- "replace"
95677
- ]).optional() });
95678
- discriminatedUnion("kind", [
95679
- ScalarOverrideDraftSchema,
95680
- KeyedArrayOverrideDraftSchema,
95681
- PulseOverrideDraftSchema,
95682
- object({
95683
- kind: literal("span"),
95684
- cap: string().min(1),
95685
- targetId: string().min(1),
95686
- timeoutMs: number().nonnegative(),
95687
- cooldownMs: number().nonnegative().optional(),
95688
- sources: array(SpanSourceDraftSchema)
95689
- })
95690
- ]);
95691
- object({
95692
- targetId: string().min(1),
95693
- configured: boolean(),
95694
- source: _enum(["native", "assigned"]),
95695
- state: _enum([
95696
- "active",
95697
- "stale",
95698
- "released",
95699
- "unavailable"
95700
- ]),
95701
- reason: string().nullable()
95702
- });
95703
96472
  var G711_SCALE_CORRECTION_DB = {
95704
96473
  PCMU: 20 * Math.log10(4),
95705
96474
  PCMA: 20 * Math.log10(8)
@@ -97483,7 +98252,7 @@ object({
97483
98252
  * short-circuits any cap the calling process already provides.
97484
98253
  */
97485
98254
  /** The addon that owns the import direction, and every `ha_*` broker. */
97486
- var HA_PROVIDER_ADDON_ID = "provider-homeassistant";
98255
+ var HA_PROVIDER_ADDON_ID$1 = "provider-homeassistant";
97487
98256
  /** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
97488
98257
  var HA_BROKER_KIND = "home-assistant";
97489
98258
  /**
@@ -97497,5 +98266,53 @@ var HA_BROKER_KIND = "home-assistant";
97497
98266
  async function listHomeAssistantBrokers(query) {
97498
98267
  return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
97499
98268
  }
98269
+ function haInstanceSettingsPortsOver(api) {
98270
+ const listInstances = async () => {
98271
+ const all = await api.list.query();
98272
+ const rows = [];
98273
+ for (const integration of all) {
98274
+ if (integration.addonId !== "provider-homeassistant") continue;
98275
+ const settings = await api.getSettings.query({ id: integration.id });
98276
+ const brokerId = settings["brokerId"];
98277
+ rows.push({
98278
+ integrationId: integration.id,
98279
+ brokerId: typeof brokerId === "string" && brokerId.length > 0 ? brokerId : null,
98280
+ settings
98281
+ });
98282
+ }
98283
+ return rows;
98284
+ };
98285
+ return {
98286
+ listInstances,
98287
+ patchInstanceSettings: async (integrationId, patch) => {
98288
+ if (!(await api.list.query()).some((i) => i.id === integrationId && i.addonId === "provider-homeassistant")) return false;
98289
+ const stored = await api.getSettings.query({ id: integrationId });
98290
+ await api.setSettings.mutate({
98291
+ id: integrationId,
98292
+ settings: {
98293
+ ...stored,
98294
+ ...patch
98295
+ },
98296
+ skipRestart: true
98297
+ });
98298
+ return true;
98299
+ }
98300
+ };
98301
+ }
98302
+ /** The brokerId → settings view of the instances: what a reader keeps per broker. */
98303
+ function settingsByBroker(rows) {
98304
+ const out = /* @__PURE__ */ new Map();
98305
+ for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
98306
+ return out;
98307
+ }
98308
+ /** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
98309
+ async function listInstancesOrNothing(ports, onError) {
98310
+ try {
98311
+ return await ports.listInstances();
98312
+ } catch (err) {
98313
+ onError(errMsg(err));
98314
+ return [];
98315
+ }
98316
+ }
97500
98317
  //#endregion
97501
- export { humidifierCapability as $, boolean as $t, brightnessCapability as A, switchCapability as At, consumableFormOf as B, weatherCapability as Bt, alarmPanelCapability as C, runningFromLawnMower as Ct, bestLocationMatch as D, scriptRunnerCapability as Dt, batteryCapability as E, runningStateCapability as Et, calendarSourceCapability as F, updateCapability as Ft, deviceAdminLinkCapability as G, defineAddonMigrations as Gt, contactCapability as H, RecordSecretUnavailableError as Ht, carbonMonoxideCapability as I, vacuumControlCapability as It, enumSensorCapability as J, entryBody as Jt, deviceAdoptionCapability as K, defineEntryKind as Kt, climateControlCapability as L, valveCapability as Lt, buildAddonRouteProvider as M, temperatureSensorCapability as Mt, buildDeviceAdminUrl as N, tryConvertDelta as Nt, binaryCapability as O, sendToExportedTarget as Ot, buttonCapability as P, tryConvertUnit as Pt, gasCapability as Q, array as Qt, colorCapability as R, vibrationCapability as Rt, airQualitySensorCapability as S, runExportedAction as St, automationControlCapability as T, runningFromVacuumState as Tt, controlCapability as U, baseUnitOf as Ut, consumableKindOf as V, BaseAddon as Vt, coverCapability as W, createEvent as Wt, fanControlCapability as X, nodePin as Xt, eventEmitterCapability as Y, isReadingQuantity as Yt, floodCapability as Z, _enum as Zt, PLUG_APPLIANCE_TYPES as _, pressureSensorCapability as _t, BaseDevice as a, string as an, mediaPlayerCapability as at, accessoriesCapability as b, quantityFromUnit as bt, COCO_TO_MACRO as c, url as cn, notificationOutputCapability as ct, ConsumableItemSchema as d, DeviceType as dn, oauthIntegrationCapability as dt, discriminatedUnion as en, humiditySensorCapability as et, ConsumableStatusSchema as f, errMsg as fn, parseAdminBase as ft, MediaPlayerRepeatSchema as g, presenceCapability as gt, HvacModeSchema as h, prepareNotification as ht, AlarmArmModeSchema as i, record as in, lockControlCapability as it, brokerCapability as j, tamperCapability as jt, boardKeySourceCapability as k, smokeCapability as kt, CameraSwitchIdSchema as l, DeviceFeature as ln, notifierCapability as lt, FanDirectionSchema as m, preconditionOf as mt, HA_PROVIDER_ADDON_ID as n, number as nn, ingestReading as nt, BaseDeviceProvider as o, union as on, motionCapability as ot, EnumSensorDateTimeFormatSchema as p, EventCategory as pn, powerMeterCapability as pt, deviceExportCapability as q, defineRecordKinds as qt, listHomeAssistantBrokers as r, object as rn, lawnMowerControlCapability as rt, CAMERA_SWITCH_ORDER as s, unknown as sn, normalizeUnit as st, HA_BROKER_KIND as t, literal as tn, imageCapability as tt, ConsumableFormSchema as u, DeviceRole as un, numericSensorCapability as ut, RUNNING_STATE_COLD as v, pruneDeletedEntities as vt, ambientLightSensorCapability as w, runningFromMediaPlayer as wt, addonRoutesCapability as x, resolveAttachmentUrls as xt, TargetSchema as y, publicEndpointReaderFrom as yt, connectivityCapability as z, waterHeaterCapability as zt };
98318
+ export { enumSensorCapability as $, baseUnitOf as $t, automationControlCapability as A, runExportedAction as At, calendarSourceCapability as B, tamperCapability as Bt, RecordIntegrationInstances as C, EventCategory as Cn, prepareNotification as Ct, airQualitySensorCapability as D, publicEndpointReaderFrom as Dt, addonRoutesCapability as E, pruneDeletedEntities as Et, brightnessCapability as F, scriptRunnerCapability as Ft, consumableFormOf as G, vacuumControlCapability as Gt, climateControlCapability as H, tryConvertDelta as Ht, brokerCapability as I, sendToExportedTarget as It, controlCapability as J, waterHeaterCapability as Jt, consumableKindOf as K, valveCapability as Kt, buildAddonRouteProvider as L, smokeCapability as Lt, bestLocationMatch as M, runningFromMediaPlayer as Mt, binaryCapability as N, runningFromVacuumState as Nt, alarmPanelCapability as O, quantityFromUnit as Ot, boardKeySourceCapability as P, runningStateCapability as Pt, deviceExportCapability as Q, RecordSecretUnavailableError as Qt, buildDeviceAdminUrl as R, statusInstanceOf as Rt, RUNNING_STATE_COLD as S, errMsg as Sn, preconditionOf as St, accessoriesCapability as T, pressureSensorCapability as Tt, colorCapability as U, tryConvertUnit as Ut, carbonMonoxideCapability as V, temperatureSensorCapability as Vt, connectivityCapability as W, updateCapability as Wt, deviceAdminLinkCapability as X, BaseAddon as Xt, coverCapability as Y, weatherCapability as Yt, deviceAdoptionCapability as Z, REDACTED_SECRET as Zt, ExportInstanceUnknownError as _, unknown as _n, notifierCapability as _t, HA_PROVIDER_ADDON_ID$1 as a, isReadingQuantity as an, gasCapability as at, MediaPlayerRepeatSchema as b, DeviceRole as bn, parseAdminBase as bt, BaseDevice as c, array as cn, imageCapability as ct, COCO_TO_MACRO as d, literal as dn, lawnMowerControlCapability as dt, createEvent as en, eventEmitterCapability as et, CameraSwitchIdSchema as f, number as fn, lockControlCapability as ft, EnumSensorDateTimeFormatSchema as g, union as gn, notificationOutputCapability as gt, ConsumableStatusSchema as h, string as hn, normalizeUnit as ht, HA_BROKER_KIND as i, entryBody as in, floodCapability as it, batteryCapability as j, runningFromLawnMower as jt, ambientLightSensorCapability as k, resolveAttachmentUrls as kt, BaseDeviceProvider as l, boolean as ln, ingestReading as lt, ConsumableItemSchema as m, record as mn, motionCapability as mt, listInstancesOrNothing as n, defineEntryKind as nn, flattenNestedRecords as nt, listHomeAssistantBrokers as o, nodePin as on, humidifierCapability as ot, ConsumableFormSchema as p, object as pn, mediaPlayerCapability as pt, contactCapability as q, vibrationCapability as qt, settingsByBroker as r, defineRecordKinds as rn, flattenPortsOver as rt, AlarmArmModeSchema as s, _enum as sn, humiditySensorCapability as st, haInstanceSettingsPortsOver as t, defineAddonMigrations as tn, fanControlCapability as tt, CAMERA_SWITCH_ORDER as u, discriminatedUnion as un, integrationInstancesCapability as ut, FanDirectionSchema as v, url as vn, numericSensorCapability as vt, TargetSchema as w, presenceCapability as wt, PLUG_APPLIANCE_TYPES as x, DeviceType as xn, powerMeterCapability as xt, HvacModeSchema as y, DeviceFeature as yn, oauthIntegrationCapability as yt, buttonCapability as z, switchCapability as zt };