@camstack/addon-import-alexa 0.2.177 → 0.2.179

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -832,7 +832,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
832
832
  return EventCategory;
833
833
  }({});
834
834
  //#endregion
835
- //#region ../types/dist/device-type-BOWLtoOt.mjs
835
+ //#region ../types/dist/device-type-CfHw_quR.mjs
836
836
  /**
837
837
  import { errMsg } from '@camstack/types'
838
838
  * Extract a human-readable message from an unknown error value.
@@ -1021,6 +1021,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1021
1021
  DeviceFeature["CoverPositionable"] = "cover-positionable";
1022
1022
  /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
1023
1023
  DeviceFeature["CoverTilt"] = "cover-tilt";
1024
+ /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
1025
+ * control in exporters; a provider whose `stop` is a no-op never declares it. */
1026
+ DeviceFeature["CoverStoppable"] = "cover-stoppable";
1024
1027
  /** Valve exposes intermediate position control (0..100). Gates the
1025
1028
  * position slider / drag surface UI. */
1026
1029
  DeviceFeature["ValvePositionable"] = "valve-positionable";
@@ -6033,7 +6036,7 @@ function number(params) {
6033
6036
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6034
6037
  }
6035
6038
  //#endregion
6036
- //#region ../types/dist/sleep-_4vKNWxb.mjs
6039
+ //#region ../types/dist/sleep-hmSP-eJt.mjs
6037
6040
  /**
6038
6041
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6039
6042
  * window to float samples (D455).
@@ -6150,7 +6153,7 @@ function isKeychainStoredField(field) {
6150
6153
  var SECRET_CLEAR = { $secretClear: true };
6151
6154
  /** The `'***'` token D634 redaction writes for non-admins. */
6152
6155
  var LEGACY_DEVICE_REDACTION = "***";
6153
- function isRecord$6(value) {
6156
+ function isRecord$7(value) {
6154
6157
  return typeof value === "object" && value !== null && !Array.isArray(value);
6155
6158
  }
6156
6159
  function makeSecretMarker(ref) {
@@ -6161,12 +6164,12 @@ function makeSecretMarker(ref) {
6161
6164
  }
6162
6165
  /** A stored marker: an object whose `$secret` is a non-empty string. */
6163
6166
  function isSecretMarker(value) {
6164
- if (!isRecord$6(value)) return false;
6167
+ if (!isRecord$7(value)) return false;
6165
6168
  const ref = value["$secret"];
6166
6169
  return typeof ref === "string" && ref.length > 0;
6167
6170
  }
6168
6171
  function isSecretClear(value) {
6169
- return isRecord$6(value) && value["$secretClear"] === true;
6172
+ return isRecord$7(value) && value["$secretClear"] === true;
6170
6173
  }
6171
6174
  /**
6172
6175
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6919,7 +6922,7 @@ var KeychainWriteRefusedError = class extends Error {
6919
6922
  }
6920
6923
  };
6921
6924
  var KEEP = { kind: "keep" };
6922
- function isRecord$5(value) {
6925
+ function isRecord$6(value) {
6923
6926
  return typeof value === "object" && value !== null && !Array.isArray(value);
6924
6927
  }
6925
6928
  function defaultMintId() {
@@ -6932,7 +6935,7 @@ function collectSecretMarkerRefs(value, out = []) {
6932
6935
  return out;
6933
6936
  }
6934
6937
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6935
- else if (isRecord$5(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6938
+ else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6936
6939
  return out;
6937
6940
  }
6938
6941
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6987,7 +6990,7 @@ function rowId(row, idKey) {
6987
6990
  return null;
6988
6991
  }
6989
6992
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6990
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord$5) : [];
6993
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
6991
6994
  if (!Array.isArray(incoming)) {
6992
6995
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6993
6996
  return {
@@ -7003,7 +7006,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
7003
7006
  const kept = /* @__PURE__ */ new Set();
7004
7007
  const rows = [];
7005
7008
  for (const row of incoming) {
7006
- if (!isRecord$5(row)) {
7009
+ if (!isRecord$6(row)) {
7007
7010
  rows.push(row);
7008
7011
  continue;
7009
7012
  }
@@ -7028,7 +7031,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
7028
7031
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
7029
7032
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
7030
7033
  case "object": {
7031
- if (!isRecord$5(incoming)) {
7034
+ if (!isRecord$6(incoming)) {
7032
7035
  if (incoming === void 0) return KEEP;
7033
7036
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
7034
7037
  return {
@@ -7036,7 +7039,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
7036
7039
  value: incoming
7037
7040
  };
7038
7041
  }
7039
- const storedObj = isRecord$5(stored) ? stored : {};
7042
+ const storedObj = isRecord$6(stored) ? stored : {};
7040
7043
  return {
7041
7044
  kind: "value",
7042
7045
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -7159,7 +7162,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
7159
7162
  value: items
7160
7163
  };
7161
7164
  }
7162
- if (isRecord$5(v)) {
7165
+ if (isRecord$6(v)) {
7163
7166
  const out = {};
7164
7167
  for (const [k, child] of Object.entries(v)) {
7165
7168
  const s = substitute(child);
@@ -7191,7 +7194,7 @@ var SecretOverwriteRefusedError = class extends Error {
7191
7194
  this.name = "SecretOverwriteRefusedError";
7192
7195
  }
7193
7196
  };
7194
- function isRecord$4$1(value) {
7197
+ function isRecord$5(value) {
7195
7198
  return typeof value === "object" && value !== null && !Array.isArray(value);
7196
7199
  }
7197
7200
  function resolvedValue(pass, marker) {
@@ -7220,7 +7223,7 @@ function inArray(pass, incoming, stored) {
7220
7223
  return incoming.map((item, index) => {
7221
7224
  const twin = stored[index];
7222
7225
  if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
7223
- if (isRecord$4$1(item) && isRecord$4$1(twin)) return inArrayRow(pass, item, twin);
7226
+ if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
7224
7227
  if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
7225
7228
  return item;
7226
7229
  });
@@ -7231,7 +7234,7 @@ function inArrayRow(pass, incoming, stored) {
7231
7234
  const twin = stored[key];
7232
7235
  if (isSecretMarker(twin)) {
7233
7236
  if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
7234
- } else if (isRecord$4$1(value) && isRecord$4$1(twin)) out[key] = inArrayRow(pass, value, twin);
7237
+ } else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
7235
7238
  else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
7236
7239
  }
7237
7240
  return out;
@@ -7239,7 +7242,7 @@ function inArrayRow(pass, incoming, stored) {
7239
7242
  async function walk(pass, incoming, stored) {
7240
7243
  if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
7241
7244
  if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
7242
- if (!isRecord$4$1(incoming) || !isRecord$4$1(stored)) return incoming;
7245
+ if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
7243
7246
  const out = { ...incoming };
7244
7247
  for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
7245
7248
  return out;
@@ -7270,7 +7273,7 @@ async function restoreUnchangedSecrets(input) {
7270
7273
  function isJsonEqual(a, b) {
7271
7274
  if (Object.is(a, b)) return true;
7272
7275
  if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
7273
- if (isRecord$4$1(a) && isRecord$4$1(b)) {
7276
+ if (isRecord$5(a) && isRecord$5(b)) {
7274
7277
  const keys = Object.keys(a);
7275
7278
  if (keys.length !== Object.keys(b).length) return false;
7276
7279
  return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
@@ -7294,7 +7297,7 @@ function markerKeysOf(blob, owner) {
7294
7297
  }
7295
7298
  function nodeFromStored(value, owner) {
7296
7299
  if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
7297
- if (!isRecord$4$1(value)) return null;
7300
+ if (!isRecord$5(value)) return null;
7298
7301
  const children = /* @__PURE__ */ new Map();
7299
7302
  for (const [key, child] of Object.entries(value)) {
7300
7303
  const node = nodeFromStored(child, owner);
@@ -7913,6 +7916,44 @@ var BaseAddon = class {
7913
7916
  return null;
7914
7917
  }
7915
7918
  /**
7919
+ * D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
7920
+ * provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
7921
+ * declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
7922
+ * {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
7923
+ * contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
7924
+ */
7925
+ async clusterSettingsSchema(_cap) {
7926
+ return null;
7927
+ }
7928
+ /**
7929
+ * D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
7930
+ * `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
7931
+ * forgotten node's rows go in one central delete.
7932
+ */
7933
+ async nodeSettingsSchema(_cap) {
7934
+ return null;
7935
+ }
7936
+ /**
7937
+ * D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
7938
+ * or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
7939
+ * as device row, then this default, then the schema default (`resolveDeviceSettings`).
7940
+ */
7941
+ async deviceDefaultsSchema(_cap) {
7942
+ return null;
7943
+ }
7944
+ /**
7945
+ * The host's read of the scoped declarations: the schema one capability declares for `scope`,
7946
+ * or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
7947
+ * host asks this method.
7948
+ */
7949
+ async getScopedSettingsSchema(scope, cap) {
7950
+ switch (scope) {
7951
+ case "cluster": return this.clusterSettingsSchema(cap);
7952
+ case "node": return this.nodeSettingsSchema(cap);
7953
+ case "device-defaults": return this.deviceDefaultsSchema(cap);
7954
+ }
7955
+ }
7956
+ /**
7916
7957
  * What the create wizard and the integration edit page render for one instance: the addon's
7917
7958
  * per-instance schema when it declares one, else its global schema (what they always showed).
7918
7959
  */
@@ -8706,47 +8747,6 @@ function normalizeAddonInitResult(result) {
8706
8747
  if (Array.isArray(result)) return { providers: result };
8707
8748
  return result;
8708
8749
  }
8709
- /**
8710
- * The share-view scope kinds a share-link token can carry. The hub mints
8711
- * and validates them (`ShareViewGrantSchema` builds its
8712
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8713
- */
8714
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8715
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8716
- var PeerBytesTicketSchema = object({
8717
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8718
- url: string().min(1),
8719
- /**
8720
- * The HOST node this URL means something on — the hub or a named agent,
8721
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8722
- * refuses `cross-node` by name when they differ, without dialling.
8723
- */
8724
- hostNodeId: string().min(1),
8725
- expiresAtMs: number$1().int().nonnegative(),
8726
- /**
8727
- * What the producer DECLARED the body to be, when it knows — `null` when it
8728
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8729
- * must be able to tell "the producer did not say" from "the body is empty".
8730
- */
8731
- declaredBytes: number$1().int().nonnegative().nullable()
8732
- });
8733
- /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8734
- /** The files a kind may live in. `history` is device-history's (D850), never records. */
8735
- var RECORD_DATABASES = ["settings", "events"];
8736
- /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8737
- var RECORD_CLASSES = [
8738
- "config",
8739
- "ledger",
8740
- "audit",
8741
- "mirror",
8742
- "ephemeral"
8743
- ];
8744
- var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8745
- var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8746
- var RECORD_BATCH_MAX = 1e3;
8747
- var RECORD_QUERY_MAX = 1e3;
8748
- /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8749
- var RECORD_SETTINGS_MAX_ROWS = 5e4;
8750
8750
  var PREFIX = "records/";
8751
8751
  /** The base of every records failure. `detail` is the human part of the message. */
8752
8752
  var RecordsError = class extends Error {
@@ -8779,6 +8779,83 @@ var DeclarationRefusedError = class extends RecordsError {
8779
8779
  this.name = "DeclarationRefusedError";
8780
8780
  }
8781
8781
  };
8782
+ /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
8783
+ /** The files a kind may live in. `history` is device-history's (D850), never records. */
8784
+ var RECORD_DATABASES = ["settings", "events"];
8785
+ /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
8786
+ var RECORD_CLASSES = [
8787
+ "config",
8788
+ "ledger",
8789
+ "audit",
8790
+ "mirror",
8791
+ "ephemeral"
8792
+ ];
8793
+ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
8794
+ var RECORD_MAX_DATA_BYTES = 1024 * 1024;
8795
+ var RECORD_BATCH_MAX = 1e3;
8796
+ var RECORD_QUERY_MAX = 1e3;
8797
+ /** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
8798
+ var RECORD_SETTINGS_MAX_ROWS = 5e4;
8799
+ /**
8800
+ * Secret fields of a record, addressed the way a Zod schema declares them
8801
+ * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8802
+ * (`auth.accessToken`, `headers[].value`).
8803
+ *
8804
+ * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8805
+ * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8806
+ * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8807
+ * leaf names its element: `headers[X-Api].value`.
8808
+ *
8809
+ * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8810
+ */
8811
+ /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8812
+ function parseSecretPath(path) {
8813
+ if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8814
+ return path.split(".").map((raw) => {
8815
+ const each = raw.endsWith("[]");
8816
+ const key = each ? raw.slice(0, -2) : raw;
8817
+ if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8818
+ return {
8819
+ key,
8820
+ each
8821
+ };
8822
+ });
8823
+ }
8824
+ /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8825
+ function secretArrayPrefixes(path) {
8826
+ const segments = parseSecretPath(path);
8827
+ const prefixes = [];
8828
+ const walked = [];
8829
+ for (const segment of segments) {
8830
+ walked.push(segment.key);
8831
+ if (segment.each) prefixes.push(walked.join("."));
8832
+ }
8833
+ return prefixes;
8834
+ }
8835
+ /**
8836
+ * The share-view scope kinds a share-link token can carry. The hub mints
8837
+ * and validates them (`ShareViewGrantSchema` builds its
8838
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8839
+ */
8840
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8841
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8842
+ var PeerBytesTicketSchema = object({
8843
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8844
+ url: string().min(1),
8845
+ /**
8846
+ * The HOST node this URL means something on — the hub or a named agent,
8847
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8848
+ * refuses `cross-node` by name when they differ, without dialling.
8849
+ */
8850
+ hostNodeId: string().min(1),
8851
+ expiresAtMs: number$1().int().nonnegative(),
8852
+ /**
8853
+ * What the producer DECLARED the body to be, when it knows — `null` when it
8854
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8855
+ * must be able to tell "the producer did not say" from "the body is empty".
8856
+ */
8857
+ declaredBytes: number$1().int().nonnegative().nullable()
8858
+ });
8782
8859
  function defOf(schema) {
8783
8860
  const zodBag = Reflect.get(schema, "_zod");
8784
8861
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -8847,42 +8924,6 @@ function collectZodSecretPaths(schema, prefix = "") {
8847
8924
  return [...new Set(paths)];
8848
8925
  }
8849
8926
  /**
8850
- * Secret fields of a record, addressed the way a Zod schema declares them
8851
- * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8852
- * (`auth.accessToken`, `headers[].value`).
8853
- *
8854
- * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8855
- * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8856
- * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
8857
- * leaf names its element: `headers[X-Api].value`.
8858
- *
8859
- * Pure and browser-safe. Nothing here mutates: every transform returns a new value.
8860
- */
8861
- /** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
8862
- function parseSecretPath(path) {
8863
- if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8864
- return path.split(".").map((raw) => {
8865
- const each = raw.endsWith("[]");
8866
- const key = each ? raw.slice(0, -2) : raw;
8867
- if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8868
- return {
8869
- key,
8870
- each
8871
- };
8872
- });
8873
- }
8874
- /** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
8875
- function secretArrayPrefixes(path) {
8876
- const segments = parseSecretPath(path);
8877
- const prefixes = [];
8878
- const walked = [];
8879
- for (const segment of segments) {
8880
- walked.push(segment.key);
8881
- if (segment.each) prefixes.push(walked.join("."));
8882
- }
8883
- return prefixes;
8884
- }
8885
- /**
8886
8927
  * Record kinds (D881): the declaration an addon makes of one kind of fact, the
8887
8928
  * Zod row schema that describes it, and the helper that turns the two into the
8888
8929
  * JSON the engine stores.
@@ -9722,6 +9763,7 @@ var CAP_NAMES = Object.freeze([
9722
9763
  "backup",
9723
9764
  "battery",
9724
9765
  "binary",
9766
+ "board-key-source",
9725
9767
  "brightness",
9726
9768
  "broker",
9727
9769
  "button",
@@ -9764,6 +9806,7 @@ var CAP_NAMES = Object.freeze([
9764
9806
  "device-status",
9765
9807
  "dishwasher",
9766
9808
  "display-palette",
9809
+ "do-not-disturb",
9767
9810
  "doorbell",
9768
9811
  "embedding-encoder",
9769
9812
  "enum-sensor",
@@ -9784,6 +9827,7 @@ var CAP_NAMES = Object.freeze([
9784
9827
  "humidity-sensor",
9785
9828
  "image",
9786
9829
  "image-settings",
9830
+ "integration-instances",
9787
9831
  "integrations",
9788
9832
  "intercom",
9789
9833
  "ir-transmitter",
@@ -9841,6 +9885,7 @@ var CAP_NAMES = Object.freeze([
9841
9885
  "privacy-mask",
9842
9886
  "ptz",
9843
9887
  "ptz-autotrack",
9888
+ "pump-cycle",
9844
9889
  "reboot",
9845
9890
  "record-entries",
9846
9891
  "recording",
@@ -10523,6 +10568,10 @@ Object.freeze({
10523
10568
  "valueType": "string"
10524
10569
  }
10525
10570
  ],
10571
+ "do-not-disturb": [{
10572
+ "path": "enabled",
10573
+ "valueType": "boolean"
10574
+ }],
10526
10575
  "doorbell": [{
10527
10576
  "path": "lastPressedAt",
10528
10577
  "valueType": "number"
@@ -11129,18 +11178,6 @@ Object.freeze({
11129
11178
  "path": "pumpRunning",
11130
11179
  "valueType": "boolean"
11131
11180
  },
11132
- {
11133
- "path": "pumpIntervalEnabled",
11134
- "valueType": "boolean"
11135
- },
11136
- {
11137
- "path": "pumpOnTime",
11138
- "valueType": "number"
11139
- },
11140
- {
11141
- "path": "pumpOffTime",
11142
- "valueType": "number"
11143
- },
11144
11181
  {
11145
11182
  "path": "lastDrink",
11146
11183
  "valueType": "number"
@@ -11264,6 +11301,20 @@ Object.freeze({
11264
11301
  "valueType": "number"
11265
11302
  }
11266
11303
  ],
11304
+ "pump-cycle": [
11305
+ {
11306
+ "path": "enabled",
11307
+ "valueType": "boolean"
11308
+ },
11309
+ {
11310
+ "path": "onTime",
11311
+ "valueType": "number"
11312
+ },
11313
+ {
11314
+ "path": "offTime",
11315
+ "valueType": "number"
11316
+ }
11317
+ ],
11267
11318
  "recording-archive": [
11268
11319
  {
11269
11320
  "path": "deviceId",
@@ -11713,7 +11764,7 @@ object({
11713
11764
  params: record(string(), union([string(), number$1()])).optional()
11714
11765
  });
11715
11766
  //#endregion
11716
- //#region ../types/dist/composition-kKKOdFIG.mjs
11767
+ //#region ../types/dist/composition-Dh3RfMx5.mjs
11717
11768
  /**
11718
11769
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11719
11770
  *
@@ -12927,6 +12978,73 @@ var NodeColorIdSchema = _enum([
12927
12978
  "node-g",
12928
12979
  "node-h"
12929
12980
  ]);
12981
+ //#endregion
12982
+ //#region ../types/dist/index.mjs
12983
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
12984
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
12985
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
12986
+ /** The text excerpt a message carries on the bus. */
12987
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
12988
+ var PortSchema = number$1().int().min(1).max(65535);
12989
+ /** A recipient local-part: what stands before the `@`. */
12990
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
12991
+ object({
12992
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
12993
+ bindHost: string().min(1).default("0.0.0.0"),
12994
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
12995
+ requireAuth: boolean().default(true),
12996
+ username: string().default(""),
12997
+ password: string().default("").meta({ secret: true }),
12998
+ starttls: boolean().default(false),
12999
+ maxMessageBytes: number$1().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
13000
+ });
13001
+ object({
13002
+ host: string().min(1).nullable(),
13003
+ hostReason: string().nullable(),
13004
+ port: PortSchema,
13005
+ domain: string().min(1),
13006
+ requireAuth: boolean(),
13007
+ username: string(),
13008
+ password: string(),
13009
+ starttls: boolean()
13010
+ });
13011
+ object({
13012
+ recipients: array(SmtpRecipientSchema).min(1),
13013
+ /**
13014
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13015
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13016
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13017
+ * Absent, the subscriber is anonymous and unique.
13018
+ */
13019
+ subscriberId: string().min(1).max(64).optional()
13020
+ });
13021
+ var SmtpInboundAttachmentSchema = object({
13022
+ filename: string(),
13023
+ contentType: string(),
13024
+ bytes: number$1().int().nonnegative()
13025
+ });
13026
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13027
+ var SmtpInboundMessageSchema = object({
13028
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13029
+ seq: number$1().int().positive(),
13030
+ messageId: string(),
13031
+ receivedAt: number$1().int().nonnegative(),
13032
+ from: string(),
13033
+ to: string(),
13034
+ subject: string(),
13035
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13036
+ attachments: array(SmtpInboundAttachmentSchema)
13037
+ });
13038
+ object({
13039
+ /** The newest seq issued so far (0 before any mail). */
13040
+ lastSeq: number$1().int().nonnegative(),
13041
+ /** The oldest seq still retained; messages below it are gone. */
13042
+ oldestSeq: number$1().int().nonnegative(),
13043
+ messages: array(object({
13044
+ key: string(),
13045
+ message: SmtpInboundMessageSchema
13046
+ }))
13047
+ });
12930
13048
  new Set([
12931
13049
  "track",
12932
13050
  "face",
@@ -16086,8 +16204,18 @@ var LogChannelDescriptorSchema = object({
16086
16204
  * the operator narrows to one camera, sees nothing, and concludes the code
16087
16205
  * path was never taken.
16088
16206
  */
16089
- perDevice: boolean()
16090
- });
16207
+ perDevice: boolean(),
16208
+ /**
16209
+ * What the channel is FOR, so the UI can list the channels an addon offers.
16210
+ *
16211
+ * - `diagnostic` (the default): a per-component diagnostic armed from the
16212
+ * Levels page, emitted at its declared level.
16213
+ * - `debug`: a `debug.<channel>` dump watched live for a short window
16214
+ * (`ctx.debug`). Always per-device, and its lines are denied by the log
16215
+ * ring unless somebody is watching that device.
16216
+ */
16217
+ kind: _enum(["diagnostic", "debug"]).default("diagnostic")
16218
+ }).refine((d) => d.kind !== "debug" || d.name.startsWith("debug.") && d.perDevice, { message: "a debug channel is named debug.<channel> and is per-device" });
16091
16219
  /**
16092
16220
  * An armed window over one channel, as the document hands it to a mirror.
16093
16221
  *
@@ -19306,24 +19434,52 @@ var APPLIANCE_CAP_VOCABULARY = {
19306
19434
  args: { on: false }
19307
19435
  },
19308
19436
  {
19309
- id: "petFountain.intervalOn",
19310
- method: "setPumpInterval",
19437
+ id: "petFountain.resetDailyConsumption",
19438
+ method: "resetDailyConsumption",
19439
+ label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19440
+ phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19441
+ icon: "rotate-ccw"
19442
+ }
19443
+ ],
19444
+ state: [{
19445
+ kind: "boolean",
19446
+ field: "empty",
19447
+ label: t("Tank empty", "Serbatoio vuoto"),
19448
+ whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19449
+ whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19450
+ icon: "droplet"
19451
+ }, {
19452
+ kind: "numeric",
19453
+ field: "waterLevel",
19454
+ quantity: t("the water level", "il livello dell’acqua"),
19455
+ label: t("Water level", "Livello dell’acqua"),
19456
+ unit: "%",
19457
+ icon: "droplets"
19458
+ }]
19459
+ },
19460
+ "pump-cycle": {
19461
+ scope: "device",
19462
+ noun: t("Pump cycle", "Ciclo della pompa"),
19463
+ actions: [
19464
+ {
19465
+ id: "pumpCycle.on",
19466
+ method: "setEnabled",
19311
19467
  label: t("Pump cycle on", "Ciclo pompa attivo"),
19312
19468
  phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
19313
19469
  icon: "timer",
19314
19470
  args: { on: true }
19315
19471
  },
19316
19472
  {
19317
- id: "petFountain.intervalOff",
19318
- method: "setPumpInterval",
19473
+ id: "pumpCycle.off",
19474
+ method: "setEnabled",
19319
19475
  label: t("Pump cycle off", "Ciclo pompa spento"),
19320
19476
  phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
19321
19477
  icon: "timer-off",
19322
19478
  args: { on: false }
19323
19479
  },
19324
19480
  {
19325
- id: "petFountain.pumpOnMinutes",
19326
- method: "setPumpOnMinutes",
19481
+ id: "pumpCycle.onMinutes",
19482
+ method: "setOnMinutes",
19327
19483
  label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
19328
19484
  phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
19329
19485
  icon: "timer",
@@ -19339,8 +19495,8 @@ var APPLIANCE_CAP_VOCABULARY = {
19339
19495
  }]
19340
19496
  },
19341
19497
  {
19342
- id: "petFountain.pumpOffMinutes",
19343
- method: "setPumpOffMinutes",
19498
+ id: "pumpCycle.offMinutes",
19499
+ method: "setOffMinutes",
19344
19500
  label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
19345
19501
  phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
19346
19502
  icon: "timer-off",
@@ -19354,29 +19510,15 @@ var APPLIANCE_CAP_VOCABULARY = {
19354
19510
  unit: "min"
19355
19511
  }
19356
19512
  }]
19357
- },
19358
- {
19359
- id: "petFountain.resetDailyConsumption",
19360
- method: "resetDailyConsumption",
19361
- label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
19362
- phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
19363
- icon: "rotate-ccw"
19364
19513
  }
19365
19514
  ],
19366
19515
  state: [{
19367
19516
  kind: "boolean",
19368
- field: "empty",
19369
- label: t("Tank empty", "Serbatoio vuoto"),
19370
- whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
19371
- whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
19372
- icon: "droplet"
19373
- }, {
19374
- kind: "numeric",
19375
- field: "waterLevel",
19376
- quantity: t("the water level", "il livello dell’acqua"),
19377
- label: t("Water level", "Livello dell’acqua"),
19378
- unit: "%",
19379
- icon: "droplets"
19517
+ field: "enabled",
19518
+ label: t("Pump cycle", "Ciclo pompa"),
19519
+ whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
19520
+ whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
19521
+ icon: "timer"
19380
19522
  }]
19381
19523
  },
19382
19524
  "media-player": {
@@ -20062,6 +20204,33 @@ var APPLIANCE_CAP_VOCABULARY = {
20062
20204
  }
20063
20205
  ]
20064
20206
  },
20207
+ "do-not-disturb": {
20208
+ scope: "device",
20209
+ noun: t("Do not disturb", "Non disturbare"),
20210
+ actions: [{
20211
+ id: "dnd.on",
20212
+ method: "setEnabled",
20213
+ label: t("Do not disturb on", "Attiva Non disturbare"),
20214
+ phrase: t("Turn on Do not disturb on {device}", "Attiva Non disturbare su {device}"),
20215
+ icon: "bell-off",
20216
+ args: { enabled: true }
20217
+ }, {
20218
+ id: "dnd.off",
20219
+ method: "setEnabled",
20220
+ label: t("Do not disturb off", "Disattiva Non disturbare"),
20221
+ phrase: t("Turn off Do not disturb on {device}", "Disattiva Non disturbare su {device}"),
20222
+ icon: "bell",
20223
+ args: { enabled: false }
20224
+ }],
20225
+ state: [{
20226
+ kind: "boolean",
20227
+ field: "enabled",
20228
+ label: t("Do not disturb", "Non disturbare"),
20229
+ whenTrue: t("When Do not disturb turns on on {device}", "Quando {device} attiva Non disturbare"),
20230
+ whenFalse: t("When Do not disturb turns off on {device}", "Quando {device} disattiva Non disturbare"),
20231
+ icon: "bell-off"
20232
+ }]
20233
+ },
20065
20234
  update: {
20066
20235
  scope: "device",
20067
20236
  noun: t("Firmware update", "Aggiornamento firmware"),
@@ -21242,6 +21411,7 @@ var SYSTEM_CAP_VOCABULARY = {
21242
21411
  "audio-analyzer": system(PIPELINE),
21243
21412
  "audio-codec": system(INTERNAL),
21244
21413
  "auth-provider": system(IDENTITY),
21414
+ "board-key-source": system(INTERNAL),
21245
21415
  backup: system("system backups — run and restored from the Backups page"),
21246
21416
  broker: system("message-broker connections — configured, not actuated"),
21247
21417
  "calendar-source": system(INTERNAL),
@@ -21276,6 +21446,7 @@ var SYSTEM_CAP_VOCABULARY = {
21276
21446
  "load-contribution": system(INTERNAL),
21277
21447
  "local-network": system(NETWORK),
21278
21448
  "log-channels": system(INTERNAL),
21449
+ "integration-instances": system(INTERNAL),
21279
21450
  "log-destination": system(INTERNAL),
21280
21451
  "login-method": system(IDENTITY),
21281
21452
  "mesh-network": system(NETWORK),
@@ -42178,14 +42349,16 @@ var DeviceIsLeafSchema = object({
42178
42349
  });
42179
42350
  /**
42180
42351
  * True when the camera currently has a PARKED object matching `subjects` in at
42181
- * least one of `zones`. Reads the zone-analytics snapshot through the same
42352
+ * least one of `zones` (`minCount` of them, default one). Reads the zone-analytics snapshot through the same
42182
42353
  * mirror as every other state leaf, so a dropped event never decides it (D49).
42183
42354
  */
42184
42355
  var StationaryPresentLeafSchema = object({
42185
42356
  kind: literal("stationary-present"),
42186
42357
  deviceId: number$1().int().nonnegative(),
42187
42358
  subjects: array(StationarySubjectSchema).min(1).optional(),
42188
- zones: array(string().min(1)).min(1).optional()
42359
+ zones: array(string().min(1)).min(1).optional(),
42360
+ /** How many matching parked objects must be there; absent means one. */
42361
+ minCount: number$1().int().positive().optional()
42189
42362
  });
42190
42363
  /**
42191
42364
  * The escape hatch: the bounded expression engine over named device bindings.
@@ -42697,6 +42870,28 @@ function convertLegacyRecipe(legacy) {
42697
42870
  * it. The output type is always the new one.
42698
42871
  */
42699
42872
  var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
42873
+ var DETECTION_DESCRIPTIONS = {
42874
+ deviceId: "the camera that saw it",
42875
+ className: "what it saw",
42876
+ trackId: "the track",
42877
+ score: "how sure the detector was",
42878
+ label: "the label it matched (a name, a plate), empty when none"
42879
+ };
42880
+ var STATIONARY_DESCRIPTIONS = {
42881
+ deviceId: "the camera that saw it",
42882
+ className: "what it saw",
42883
+ zones: "the zones it is in",
42884
+ phase: "appeared or departed",
42885
+ entryId: "the stopped object"
42886
+ };
42887
+ function describe$1(descriptions) {
42888
+ return Object.entries(descriptions).map(([name, description]) => ({
42889
+ name,
42890
+ description
42891
+ }));
42892
+ }
42893
+ describe$1(DETECTION_DESCRIPTIONS);
42894
+ describe$1(STATIONARY_DESCRIPTIONS);
42700
42895
  new Set(Object.values(COCO_TO_MACRO.mapping));
42701
42896
  /**
42702
42897
  * Schedulers (D983) — an entity that performs actions over the day.
@@ -43458,7 +43653,7 @@ var TargetKindCapsSchema = object({
43458
43653
  * the union is large and not meant for runtime validation here; the exported
43459
43654
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
43460
43655
  */
43461
- var ConfigSchemaPassthrough$3 = unknown();
43656
+ var ConfigSchemaPassthrough$4 = unknown();
43462
43657
  var TargetKindSchema = object({
43463
43658
  kind: string(),
43464
43659
  label: string(),
@@ -43495,7 +43690,7 @@ var TargetKindSchema = object({
43495
43690
  * its raster path, which is the safe default for an unknown image.
43496
43691
  */
43497
43692
  iconMediaType: string().optional(),
43498
- configSchema: ConfigSchemaPassthrough$3,
43693
+ configSchema: ConfigSchemaPassthrough$4,
43499
43694
  /**
43500
43695
  * The config keys that decide WHERE this kind's secrets are sent (server,
43501
43696
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -46167,6 +46362,49 @@ var binaryCapability = {
46167
46362
  volatileStateFields: ["lastChangedAt"]
46168
46363
  };
46169
46364
  /**
46365
+ * Why a board key could not be read. Typed so a caller reports the cause and
46366
+ * never treats "could not read" as "has no key" (D49).
46367
+ */
46368
+ var BoardKeyFailureReasonSchema = _enum([
46369
+ "unknown-source",
46370
+ "not-admin",
46371
+ "no-builder",
46372
+ "no-ingress-url",
46373
+ "unreachable",
46374
+ "unknown-board",
46375
+ "no-encryption-key",
46376
+ "unreadable-config"
46377
+ ]);
46378
+ var BoardKeySourceSchema = object({
46379
+ sourceId: string(),
46380
+ name: string()
46381
+ });
46382
+ var BoardKeyBoardSchema = object({
46383
+ /** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
46384
+ configuration: string(),
46385
+ /** The node name the builder gives the board. */
46386
+ name: string(),
46387
+ /** The address the builder knows the board by, when it has one. */
46388
+ address: string().nullable()
46389
+ });
46390
+ var BoardKeyFailureSchema = object({
46391
+ ok: literal(false),
46392
+ reason: BoardKeyFailureReasonSchema,
46393
+ detail: string().optional()
46394
+ });
46395
+ var ListBoardsResultSchema = discriminatedUnion("ok", [object({
46396
+ ok: literal(true),
46397
+ boards: array(BoardKeyBoardSchema).readonly()
46398
+ }), BoardKeyFailureSchema]);
46399
+ var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
46400
+ ok: literal(true),
46401
+ key: string()
46402
+ }), BoardKeyFailureSchema]);
46403
+ method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
46404
+ sourceId: string().min(1),
46405
+ configuration: string().min(1)
46406
+ }), GetBoardKeyResultSchema, { auth: "admin" });
46407
+ /**
46170
46408
  * Dimmable-light brightness control. Co-exists with `switch` on the
46171
46409
  * same device — the switch toggles on/off, this cap sets the level
46172
46410
  * applied when the light is on. Drivers map their per-vendor dim
@@ -51200,6 +51438,45 @@ var deviceProviderCapability = {
51200
51438
  }
51201
51439
  };
51202
51440
  /**
51441
+ * What the passive listener of `local-network` saw on the wire, before any
51442
+ * provider classified it. Device-manager turns an observation into the hits
51443
+ * of the providers whose declared discovery modes it matches.
51444
+ */
51445
+ var DiscoveryObservationSchema = discriminatedUnion("kind", [object({
51446
+ kind: literal("mdns"),
51447
+ /** Normalised DNS-SD service type, e.g. `_esphomelib._tcp`. */
51448
+ serviceType: string(),
51449
+ instance: string(),
51450
+ host: string(),
51451
+ /** The A record address when the announcement carried one. */
51452
+ address: string().nullable(),
51453
+ port: number$1().int(),
51454
+ txt: record(string(), string())
51455
+ }), object({
51456
+ kind: literal("ssdp"),
51457
+ /** Sender address of the datagram. */
51458
+ host: string(),
51459
+ port: number$1().int(),
51460
+ /** `NT` of a NOTIFY, `ST` of a search response. */
51461
+ target: string(),
51462
+ headers: record(string(), string())
51463
+ })]);
51464
+ object({
51465
+ addonId: string(),
51466
+ candidate: DiscoveryCandidateSchema,
51467
+ firstSeenAt: number$1(),
51468
+ lastSeenAt: number$1(),
51469
+ dismissed: boolean()
51470
+ });
51471
+ var DiscoveryProposalSchema = object({
51472
+ id: string(),
51473
+ addonId: string(),
51474
+ candidate: DiscoveryCandidateSchema,
51475
+ firstSeenAt: number$1(),
51476
+ lastSeenAt: number$1(),
51477
+ dismissed: boolean()
51478
+ });
51479
+ /**
51203
51480
  * Device Manager capability — hub-side singleton that unifies device persistence,
51204
51481
  * live registry access, and all management operations into a single tRPC surface.
51205
51482
  *
@@ -51928,6 +52205,19 @@ method(object({
51928
52205
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
51929
52206
  kind: "mutation",
51930
52207
  auth: "admin"
52208
+ }), method(object({}), object({ proposals: array(DiscoveryProposalSchema).readonly() }), { auth: "admin" }), method(object({ id: string() }), object({ dismissed: boolean() }), {
52209
+ kind: "mutation",
52210
+ auth: "admin"
52211
+ }), method(object({ observations: array(DiscoveryObservationSchema).max(512) }), object({ proposed: number$1().int() }), {
52212
+ kind: "mutation",
52213
+ auth: "admin"
52214
+ }), method(object({}), object({
52215
+ providers: number$1().int(),
52216
+ proposed: number$1().int()
52217
+ }), {
52218
+ kind: "mutation",
52219
+ auth: "admin",
52220
+ timeoutMs: 12e4
51931
52221
  }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
51932
52222
  deviceId: number$1(),
51933
52223
  key: string(),
@@ -52784,7 +53074,7 @@ var DishwasherStatusSchema = object({
52784
53074
  /** Ms epoch of the last value received from the appliance; 0 = never. */
52785
53075
  lastFetchedAt: number$1()
52786
53076
  });
52787
- var DeviceIdSchema$1 = number$1().int().nonnegative();
53077
+ var DeviceIdSchema$2 = number$1().int().nonnegative();
52788
53078
  /** A value a program option or setting can take. */
52789
53079
  var DishwasherValueSchema = union([
52790
53080
  boolean(),
@@ -52859,77 +53149,77 @@ var dishwasherCapability = {
52859
53149
  * option feature keys; each is checked against the program's constraints.
52860
53150
  */
52861
53151
  start: method(object({
52862
- deviceId: DeviceIdSchema$1,
53152
+ deviceId: DeviceIdSchema$2,
52863
53153
  program: string().min(1).optional(),
52864
53154
  options: record(string(), DishwasherValueSchema).optional()
52865
53155
  }), _void(), {
52866
53156
  kind: "mutation",
52867
53157
  auth: "protected"
52868
53158
  }),
52869
- pause: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53159
+ pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52870
53160
  kind: "mutation",
52871
53161
  auth: "protected"
52872
53162
  }),
52873
- resume: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53163
+ resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52874
53164
  kind: "mutation",
52875
53165
  auth: "protected"
52876
53166
  }),
52877
- stop: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
53167
+ stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
52878
53168
  kind: "mutation",
52879
53169
  auth: "protected"
52880
53170
  }),
52881
53171
  /** Select (not start) a program; refused when the appliance does not offer it. */
52882
53172
  setProgram: method(object({
52883
- deviceId: DeviceIdSchema$1,
53173
+ deviceId: DeviceIdSchema$2,
52884
53174
  program: string().min(1)
52885
53175
  }), _void(), {
52886
53176
  kind: "mutation",
52887
53177
  auth: "protected"
52888
53178
  }),
52889
53179
  setPower: method(object({
52890
- deviceId: DeviceIdSchema$1,
53180
+ deviceId: DeviceIdSchema$2,
52891
53181
  on: boolean()
52892
53182
  }), _void(), {
52893
53183
  kind: "mutation",
52894
53184
  auth: "protected"
52895
53185
  }),
52896
53186
  setHalfLoad: method(object({
52897
- deviceId: DeviceIdSchema$1,
53187
+ deviceId: DeviceIdSchema$2,
52898
53188
  on: boolean()
52899
53189
  }), _void(), {
52900
53190
  kind: "mutation",
52901
53191
  auth: "protected"
52902
53192
  }),
52903
53193
  setHygienePlus: method(object({
52904
- deviceId: DeviceIdSchema$1,
53194
+ deviceId: DeviceIdSchema$2,
52905
53195
  on: boolean()
52906
53196
  }), _void(), {
52907
53197
  kind: "mutation",
52908
53198
  auth: "protected"
52909
53199
  }),
52910
53200
  setExtraDry: method(object({
52911
- deviceId: DeviceIdSchema$1,
53201
+ deviceId: DeviceIdSchema$2,
52912
53202
  on: boolean()
52913
53203
  }), _void(), {
52914
53204
  kind: "mutation",
52915
53205
  auth: "protected"
52916
53206
  }),
52917
53207
  setIntensiveZone: method(object({
52918
- deviceId: DeviceIdSchema$1,
53208
+ deviceId: DeviceIdSchema$2,
52919
53209
  on: boolean()
52920
53210
  }), _void(), {
52921
53211
  kind: "mutation",
52922
53212
  auth: "protected"
52923
53213
  }),
52924
53214
  setVarioSpeedPlus: method(object({
52925
- deviceId: DeviceIdSchema$1,
53215
+ deviceId: DeviceIdSchema$2,
52926
53216
  on: boolean()
52927
53217
  }), _void(), {
52928
53218
  kind: "mutation",
52929
53219
  auth: "protected"
52930
53220
  }),
52931
53221
  setChildLock: method(object({
52932
- deviceId: DeviceIdSchema$1,
53222
+ deviceId: DeviceIdSchema$2,
52933
53223
  on: boolean()
52934
53224
  }), _void(), {
52935
53225
  kind: "mutation",
@@ -52941,7 +53231,7 @@ var dishwasherCapability = {
52941
53231
  * its declared range / members.
52942
53232
  */
52943
53233
  setOption: method(object({
52944
- deviceId: DeviceIdSchema$1,
53234
+ deviceId: DeviceIdSchema$2,
52945
53235
  key: string().min(1),
52946
53236
  value: DishwasherValueSchema
52947
53237
  }), _void(), {
@@ -52949,7 +53239,7 @@ var dishwasherCapability = {
52949
53239
  auth: "protected"
52950
53240
  }),
52951
53241
  /** What this appliance can do, from its own description — the UI is derived from it. */
52952
- getOptions: method(object({ deviceId: DeviceIdSchema$1 }), DishwasherOptionsSchema, { auth: "protected" })
53242
+ getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
52953
53243
  },
52954
53244
  status: {
52955
53245
  schema: DishwasherStatusSchema,
@@ -53377,6 +53667,49 @@ method(_void(), paletteResultSchema, {
53377
53667
  kind: "mutation",
53378
53668
  auth: "admin"
53379
53669
  });
53670
+ /**
53671
+ * do-not-disturb - whether a speaker or display holds back its own sounds
53672
+ * (announcements, notifications, alarms) until the operator lets it speak again.
53673
+ *
53674
+ * It is NOT part of `media-player`: a player is about what it plays and how
53675
+ * loud, while Do Not Disturb is a household switch the vendor keeps per device.
53676
+ * `enabled` is null when the vendor has not said (never a guess of `false`).
53677
+ */
53678
+ var DoNotDisturbStatusSchema = object({
53679
+ /** True while Do Not Disturb is on; null when unknown. */
53680
+ enabled: boolean().nullable().meta({ label: "Do not disturb" }),
53681
+ /** Ms epoch of the last read or write that set `enabled`. */
53682
+ lastChangedAt: number$1()
53683
+ });
53684
+ var doNotDisturbCapability = {
53685
+ name: "do-not-disturb",
53686
+ scope: "device",
53687
+ deviceNative: true,
53688
+ mode: "singleton",
53689
+ deviceTypes: [DeviceType.MediaPlayer],
53690
+ methods: { setEnabled: method(object({
53691
+ deviceId: number$1().int(),
53692
+ enabled: boolean()
53693
+ }), _void(), {
53694
+ kind: "mutation",
53695
+ auth: "protected"
53696
+ }) },
53697
+ status: {
53698
+ schema: DoNotDisturbStatusSchema,
53699
+ kind: "push"
53700
+ },
53701
+ runtimeState: DoNotDisturbStatusSchema,
53702
+ /**
53703
+ * Runtime-state durability: **session** - the vendor owns the switch and the
53704
+ * device re-reads it on the next poll, so a restored value would only be stale.
53705
+ *
53706
+ * See `RuntimeStateDurability`. Enforced by
53707
+ * `scripts/check-runtime-state-durability.ts`.
53708
+ */
53709
+ durability: "session",
53710
+ /** History (D850): when Do Not Disturb was switched on and off. */
53711
+ history: { fields: { enabled: { kind: "boolean" } } }
53712
+ };
53380
53713
  /** Pulse override: a rising edge advances `lastPressedAt` and the press count. */
53381
53714
  var doorbellCustomize = {
53382
53715
  version: 1,
@@ -55229,6 +55562,89 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55229
55562
  auth: "admin"
55230
55563
  });
55231
55564
  /**
55565
+ * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
55566
+ * (an account, a relay, a destination) as typed records (D985, D986). The addon's
55567
+ * own function page reads and writes them (Logging for Loki); the Integrations
55568
+ * page lists only the integrations that bring devices (D996).
55569
+ *
55570
+ * A collection of providers: each addon that holds connections registers one and a
55571
+ * caller reaches a provider with the pinned `addonId`.
55572
+ *
55573
+ * Identity: the instance id is immutable and opaque to the caller. A record the
55574
+ * Loki migration made keeps the `integrations` registry id it was created under
55575
+ * (`int_NNNN`); a single instance made by its function page takes the addon id.
55576
+ * `name` is the editable label.
55577
+ *
55578
+ * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55579
+ * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
55580
+ * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
55581
+ * clears it.
55582
+ */
55583
+ var ConfigSchemaPassthrough$3 = custom();
55584
+ var IntegrationInstanceIdSchema = string().min(1);
55585
+ var IntegrationInstanceDescriptionSchema = object({
55586
+ /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
55587
+ instanceMode: _enum(["single", "multiple"]),
55588
+ /** What the instance is, for the UI (`destination`, `relay`, `account`). */
55589
+ kind: string(),
55590
+ /** True when `testInstance` really probes the remote. */
55591
+ canTest: boolean()
55592
+ });
55593
+ var IntegrationInstanceStatusSchema = object({
55594
+ state: _enum([
55595
+ "ok",
55596
+ "off",
55597
+ "error",
55598
+ "unknown"
55599
+ ]),
55600
+ detail: string().optional()
55601
+ });
55602
+ var IntegrationInstanceSchema = object({
55603
+ id: IntegrationInstanceIdSchema,
55604
+ name: string(),
55605
+ /** The addon's own rule (an URL is present, a client id is present...). */
55606
+ configured: boolean(),
55607
+ /**
55608
+ * The instance's own on/off switch (D996): always present, derived from the record body
55609
+ * (`enabled !== false`, so a row written before the field existed is on).
55610
+ */
55611
+ enabled: boolean(),
55612
+ /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55613
+ formValues: record(string(), unknown()),
55614
+ status: IntegrationInstanceStatusSchema.optional()
55615
+ });
55616
+ var IntegrationInstancePatchSchema = object({
55617
+ id: IntegrationInstanceIdSchema,
55618
+ name: string().min(1).optional(),
55619
+ /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55620
+ values: record(string(), unknown())
55621
+ });
55622
+ 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({
55623
+ id: IntegrationInstanceIdSchema,
55624
+ name: string().min(1)
55625
+ }), object({ id: string() }), {
55626
+ kind: "mutation",
55627
+ auth: "admin"
55628
+ }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55629
+ kind: "mutation",
55630
+ auth: "admin"
55631
+ }), method(object({
55632
+ id: IntegrationInstanceIdSchema,
55633
+ enabled: boolean()
55634
+ }), object({ ok: literal(true) }), {
55635
+ kind: "mutation",
55636
+ auth: "admin"
55637
+ }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55638
+ kind: "mutation",
55639
+ auth: "admin"
55640
+ }), optionalMethod(object({
55641
+ id: IntegrationInstanceIdSchema.nullable(),
55642
+ values: record(string(), unknown())
55643
+ }), TestConnectionResultSchema$1, {
55644
+ kind: "mutation",
55645
+ auth: "admin"
55646
+ });
55647
+ /**
55232
55648
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55233
55649
  * session (server-initiated offer pattern, identical to the main
55234
55650
  * video WebRTC flow) and the browser captures the mic; the server
@@ -55968,6 +56384,14 @@ var LlmGenerateBaseInputSchema = object({
55968
56384
  topP: number$1().min(0).max(1).optional(),
55969
56385
  /** Per-call override of the profile default (top-k sampling). */
55970
56386
  topK: number$1().int().positive().optional(),
56387
+ /** Per-call override of `profile.reasoningEffort`. Sent only to a kind whose
56388
+ * descriptor offers the field; a kind that does not never sends it. */
56389
+ reasoningEffort: _enum([
56390
+ "off",
56391
+ "low",
56392
+ "medium",
56393
+ "high"
56394
+ ]).optional(),
55971
56395
  /** Per-call override of `profile.timeoutMs` — the total generation bound. */
55972
56396
  timeoutMs: number$1().int().positive().optional(),
55973
56397
  /** Per-call override; beats both the consumer table and the profile. */
@@ -56259,7 +56683,10 @@ var LlmProfileKindSchema = string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "a kind i
56259
56683
  var LlmModelFeaturesSchema = object({
56260
56684
  vision: boolean(),
56261
56685
  tools: boolean(),
56262
- streaming: boolean()
56686
+ streaming: boolean(),
56687
+ /** Whether the model thinks before it answers. `null` = the endpoint does not
56688
+ * say; an editor then still offers the switch (unknown is not "no"). */
56689
+ reasoning: boolean().nullable().optional()
56263
56690
  });
56264
56691
  /** One model a provider offers. `features` overrides the kind's defaults, per field. */
56265
56692
  var LlmModelInfoSchema = object({
@@ -65985,7 +66412,14 @@ var NcMediaPolicySchema = object({
65985
66412
  */
65986
66413
  profile: CamProfileSchema.optional(),
65987
66414
  /** See {@link NcLinkedCameraModeSchema}. */
65988
- linkedCameras: NcLinkedCameraModeSchema.optional()
66415
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
66416
+ /**
66417
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
66418
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
66419
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
66420
+ * following the hub's. One bounded wake per event; nothing is attached later.
66421
+ */
66422
+ wakeSleepingCamera: boolean().optional()
65989
66423
  });
65990
66424
  /**
65991
66425
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -68128,7 +68562,7 @@ var petFeederCapability = {
68128
68562
  /**
68129
68563
  * Pet water-fountain cap. Models the telemetry and the control surface of a
68130
68564
  * circulating drinking fountain: how much water is in the tank, how much the
68131
- * pet drank, whether the pump runs and on what duty cycle.
68565
+ * pet drank, whether the pump runs.
68132
68566
  *
68133
68567
  * It has no native vendor provider of its own: a fountain reaches CamStack as
68134
68568
  * a container of loose entities (a pump switch, two number boxes, a "filter
@@ -68144,8 +68578,10 @@ var petFeederCapability = {
68144
68578
  * never here. Every reading may be `null`: a source that is offline or does
68145
68579
  * not report is unknown, never zero.
68146
68580
  *
68147
- * The filter is not a field of this cap: it is an item of the `consumables`
68148
- * cap on the same device (kind `wear`), reset by the same forward mechanism.
68581
+ * The pump's on/off duty cycle is model-specific and lives in the optional
68582
+ * `pump-cycle` cap. The filter, the cleaning and the water change are not
68583
+ * fields of this cap: they are items of the `consumables` cap on the same
68584
+ * device (kind `wear`), reset by the same forward mechanism.
68149
68585
  */
68150
68586
  var PetFountainStatusSchema = object({
68151
68587
  /** Water in the tank, 0..100 (%). Null while the source has not reported. */
@@ -68156,27 +68592,20 @@ var PetFountainStatusSchema = object({
68156
68592
  empty: boolean().nullable(),
68157
68593
  /** True while the pump is running. Null while unknown. */
68158
68594
  pumpRunning: boolean().nullable(),
68159
- /** True when the pump works on an on/off duty cycle rather than continuously. */
68160
- pumpIntervalEnabled: boolean().nullable(),
68161
- /** How long the pump runs per cycle, in seconds. */
68162
- pumpOnTime: number$1().min(0).nullable().meta(readingMeta("duration")),
68163
- /** How long the pump rests per cycle, in seconds. */
68164
- pumpOffTime: number$1().min(0).nullable().meta(readingMeta("duration")),
68165
68595
  /** Water drunk in the last drink, in litres. */
68166
68596
  lastDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
68167
68597
  /** Water drunk in the current hour, in litres. */
68168
68598
  currentHourDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
68169
68599
  /** Water drunk over the last 24 hours (rolling), in litres. */
68170
68600
  last24hDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
68171
- /** Water drunk since local midnight, in litres. */
68601
+ /** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
68172
68602
  todayDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
68173
68603
  /** Decoded fault message. Null means healthy; a non-null string is an active fault. */
68174
68604
  error: string().nullable(),
68175
68605
  /** Ms epoch when the slice was last refreshed. */
68176
68606
  lastFetchedAt: number$1()
68177
68607
  });
68178
- var DeviceIdSchema = number$1().int().nonnegative();
68179
- var MinutesSchema = number$1().int().min(0);
68608
+ var DeviceIdSchema$1 = number$1().int().nonnegative();
68180
68609
  var petFountainCapability = {
68181
68610
  name: "pet-fountain",
68182
68611
  scope: "device",
@@ -68186,38 +68615,14 @@ var petFountainCapability = {
68186
68615
  methods: {
68187
68616
  /** Start or stop the pump. */
68188
68617
  setPump: method(object({
68189
- deviceId: DeviceIdSchema,
68190
- on: boolean()
68191
- }), _void(), {
68192
- kind: "mutation",
68193
- auth: "protected"
68194
- }),
68195
- /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
68196
- setPumpInterval: method(object({
68197
- deviceId: DeviceIdSchema,
68618
+ deviceId: DeviceIdSchema$1,
68198
68619
  on: boolean()
68199
68620
  }), _void(), {
68200
68621
  kind: "mutation",
68201
68622
  auth: "protected"
68202
68623
  }),
68203
- /** Set how many minutes the pump runs per cycle. */
68204
- setPumpOnMinutes: method(object({
68205
- deviceId: DeviceIdSchema,
68206
- minutes: MinutesSchema
68207
- }), _void(), {
68208
- kind: "mutation",
68209
- auth: "protected"
68210
- }),
68211
- /** Set how many minutes the pump rests per cycle. */
68212
- setPumpOffMinutes: method(object({
68213
- deviceId: DeviceIdSchema,
68214
- minutes: MinutesSchema
68215
- }), _void(), {
68216
- kind: "mutation",
68217
- auth: "protected"
68218
- }),
68219
68624
  /** Clear the rolling daily consumption counters on the device. */
68220
- resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
68625
+ resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
68221
68626
  kind: "mutation",
68222
68627
  auth: "protected"
68223
68628
  })
@@ -70550,6 +70955,79 @@ var ptzAutotrackCapability = {
70550
70955
  */
70551
70956
  history: "none"
70552
70957
  };
70958
+ /**
70959
+ * The on/off duty cycle of a pet fountain's pump. Some models run the pump
70960
+ * continuously and have nothing to configure; others can alternate a run time
70961
+ * and a rest time. That is model-specific, so it is its own optional cap on the
70962
+ * fountain device: a fountain without cycle entities simply never composes it
70963
+ * (planning#366), and the `pet-fountain` cap stays the model-independent core.
70964
+ *
70965
+ * Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
70966
+ * cycle from loose entities (a cycle switch and two number boxes) and every
70967
+ * method is a forward to one source entity (D659, D967). Durations are stored
70968
+ * in SECONDS (D944); a number box that cannot be read back leaves them `null`
70969
+ * — unknown, never zero (D393).
70970
+ */
70971
+ var PumpCycleStatusSchema = object({
70972
+ /** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
70973
+ enabled: boolean().nullable(),
70974
+ /** How long the pump runs per cycle, in seconds. Null while unknown. */
70975
+ onTime: number$1().min(0).nullable().meta(readingMeta("duration")),
70976
+ /** How long the pump rests per cycle, in seconds. Null while unknown. */
70977
+ offTime: number$1().min(0).nullable().meta(readingMeta("duration")),
70978
+ /** Ms epoch when the slice was last refreshed. */
70979
+ lastFetchedAt: number$1()
70980
+ });
70981
+ var DeviceIdSchema = number$1().int().nonnegative();
70982
+ var MinutesSchema = number$1().int().min(0);
70983
+ var pumpCycleCapability = {
70984
+ name: "pump-cycle",
70985
+ scope: "device",
70986
+ deviceNative: true,
70987
+ mode: "singleton",
70988
+ deviceTypes: [DeviceType.PetFountain],
70989
+ methods: {
70990
+ /** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
70991
+ setEnabled: method(object({
70992
+ deviceId: DeviceIdSchema,
70993
+ on: boolean()
70994
+ }), _void(), {
70995
+ kind: "mutation",
70996
+ auth: "protected"
70997
+ }),
70998
+ /** Set how many minutes the pump runs per cycle. */
70999
+ setOnMinutes: method(object({
71000
+ deviceId: DeviceIdSchema,
71001
+ minutes: MinutesSchema
71002
+ }), _void(), {
71003
+ kind: "mutation",
71004
+ auth: "protected"
71005
+ }),
71006
+ /** Set how many minutes the pump rests per cycle. */
71007
+ setOffMinutes: method(object({
71008
+ deviceId: DeviceIdSchema,
71009
+ minutes: MinutesSchema
71010
+ }), _void(), {
71011
+ kind: "mutation",
71012
+ auth: "protected"
71013
+ })
71014
+ },
71015
+ status: {
71016
+ schema: PumpCycleStatusSchema,
71017
+ kind: "push"
71018
+ },
71019
+ /** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
71020
+ runtimeState: PumpCycleStatusSchema,
71021
+ /**
71022
+ * Runtime-state durability: **session** — live appliance state re-published on connect.
71023
+ *
71024
+ * See `RuntimeStateDurability`. Enforced by
71025
+ * `scripts/check-runtime-state-durability.ts`.
71026
+ */
71027
+ durability: "session",
71028
+ /** History (D850): a duty-cycle setting is not a series worth recording. */
71029
+ history: "none"
71030
+ };
70553
71031
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number$1().int().nonnegative() }), object({ success: literal(true) }), {
70554
71032
  kind: "mutation",
70555
71033
  auth: "admin"
@@ -74445,9 +74923,17 @@ var ServerUpdateActionResultSchema = object({
74445
74923
  */
74446
74924
  requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
74447
74925
  });
74448
- var LaunchOverridesSchema = object({
74449
- /** The persisted heap ceiling in MB, or `null` when none is set. */
74450
- heapMb: number$1().int().nullable() });
74926
+ var LaunchOverridesSchema = object({
74927
+ /** The persisted heap ceiling in MB, or `null` when none is set. */
74928
+ heapMb: number$1().int().nullable(),
74929
+ /** The persisted hub-main RSS budget in MB, or `null` when none is set. */
74930
+ rssBudgetMb: number$1().int().nullable()
74931
+ });
74932
+ var SetLaunchOverridesInputSchema = object({
74933
+ heapMb: number$1().int().min(512).max(65536).nullable(),
74934
+ /** Omitted keeps the stored budget; `null` clears it. */
74935
+ rssBudgetMb: number$1().int().min(512).max(65536).nullable().optional()
74936
+ });
74451
74937
  var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
74452
74938
  /** Always true: the override is read at launch, not by the running process. */
74453
74939
  restartRequired: boolean() });
@@ -74476,7 +74962,7 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
74476
74962
  }), method(_void(), ServerUpdateActionResultSchema, {
74477
74963
  kind: "mutation",
74478
74964
  auth: "admin"
74479
- }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(object({ heapMb: number$1().int().min(512).max(65536).nullable() }), SetLaunchOverridesResultSchema, {
74965
+ }), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(SetLaunchOverridesInputSchema, SetLaunchOverridesResultSchema, {
74480
74966
  kind: "mutation",
74481
74967
  auth: "admin"
74482
74968
  });
@@ -74534,7 +75020,16 @@ var smokeCapability = {
74534
75020
  * future fields without breaking the existing call sites.
74535
75021
  */
74536
75022
  var EmailAddressSchema = email();
75023
+ /**
75024
+ * Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
75025
+ * relay, so a caller written before relays existed keeps working unchanged. A provider with one
75026
+ * fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
75027
+ * collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
75028
+ */
75029
+ var InstanceIdSchema = string().min(1).optional();
75030
+ var RelayPinSchema = object({ instanceId: InstanceIdSchema });
74537
75031
  var SendEmailInputSchema = object({
75032
+ instanceId: InstanceIdSchema,
74538
75033
  to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
74539
75034
  cc: array(EmailAddressSchema).optional(),
74540
75035
  bcc: array(EmailAddressSchema).optional(),
@@ -74570,11 +75065,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
74570
75065
  kind: "mutation",
74571
75066
  auth: "admin",
74572
75067
  access: "create"
74573
- }), method(_void(), SmtpStatusSchema, {
75068
+ }), method(RelayPinSchema, SmtpStatusSchema, {
74574
75069
  kind: "mutation",
74575
75070
  auth: "admin",
74576
75071
  access: "view"
74577
- }), method(_void(), SmtpStatusSchema, { auth: "admin" });
75072
+ }), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
74578
75073
  /**
74579
75074
  * A single device snapshot returned as base64 JPEG/PNG.
74580
75075
  *
@@ -74606,10 +75101,12 @@ DeviceType.Camera, method(object({
74606
75101
  * even when the cache is well within the device's
74607
75102
  * `snapshotMaxAgeS` window.
74608
75103
  *
74609
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
75104
+ * **`force` is a demand with a named principal, not a freshness
75105
+ * preference** — an operator button, the scheduled refresh (D847), or a
75106
+ * notification rule that left the camera's wake allowed (D998). On a
74610
75107
  * battery camera it is the one thing that walks past the wrapper's
74611
- * sleep gate and wakes the camera, so a background caller — a poller,
74612
- * an event handler, a thumbnail — must NEVER set it. Every such caller
75108
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
75109
+ * prestream must NEVER set it. Every such caller
74613
75110
  * gets the cached frame, which on a sleeping battery camera is the
74614
75111
  * correct answer: stale but honest beats woken.
74615
75112
  */
@@ -76179,6 +76676,40 @@ var LoggingSettingsStateSchema = object({
76179
76676
  opsLogRetentionDays: number$1().int().min(0),
76180
76677
  persisted: boolean()
76181
76678
  });
76679
+ /**
76680
+ * Debug WATCH — a short, per-device window over `debug.<channel>` lines.
76681
+ *
76682
+ * `start` opens (or re-arms) a 10 minute window; `rearm` extends a LIVE one
76683
+ * and never revives a lapsed one; `stop` closes it; `list` only reads. No
76684
+ * chain of re-arms outlives 60 minutes from the first start. The hub holds the
76685
+ * watches in memory and pushes the interest to every node, so a watch lapses
76686
+ * by itself and survives nothing — a restart is a stop.
76687
+ */
76688
+ var DebugWatchInputSchema = object({
76689
+ action: _enum([
76690
+ "list",
76691
+ "start",
76692
+ "rearm",
76693
+ "stop"
76694
+ ]),
76695
+ /** Full channel names, `debug.<channel>`, each declared with `kind: 'debug'`. */
76696
+ channels: array(string().min(1)).readonly().default([]),
76697
+ /** Numeric device ids. Required (non-empty) for `start`. */
76698
+ deviceIds: array(number$1().int()).readonly().default([])
76699
+ });
76700
+ var DebugWatchStateSchema = object({
76701
+ channel: string(),
76702
+ deviceIds: array(number$1().int()).readonly(),
76703
+ startedAtMs: number$1(),
76704
+ untilMs: number$1(),
76705
+ capAtMs: number$1()
76706
+ });
76707
+ var DebugWatchResultSchema = object({
76708
+ nowMs: number$1(),
76709
+ watches: array(DebugWatchStateSchema).readonly(),
76710
+ /** Channels a `rearm` found already lapsed: nothing was revived. */
76711
+ lapsed: array(string()).readonly()
76712
+ });
76182
76713
  method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
76183
76714
  kind: "mutation",
76184
76715
  auth: "admin"
@@ -76194,6 +76725,9 @@ method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_
76194
76725
  }), method(_void(), RequestCensusStatusSchema, { auth: "admin" }), method(_void(), array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }), method(_void(), array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }), method(GetLoggingSettingsInputSchema, LoggingSettingsStateSchema, { auth: "admin" }), method(SetLoggingSettingsInputSchema, LoggingSettingsStateSchema, {
76195
76726
  kind: "mutation",
76196
76727
  auth: "admin"
76728
+ }), method(DebugWatchInputSchema, DebugWatchResultSchema, {
76729
+ kind: "mutation",
76730
+ auth: "admin"
76197
76731
  });
76198
76732
  /**
76199
76733
  * Tamper / case-open detection sensor. Drives Home Assistant
@@ -79259,7 +79793,7 @@ discriminatedUnion("kind", [object({
79259
79793
  kind: literal("pronto"),
79260
79794
  code: string()
79261
79795
  })]);
79262
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79796
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
79263
79797
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
79264
79798
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
79265
79799
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -79582,6 +80116,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79582
80116
  deviceDiscovery: deviceDiscoveryCapability,
79583
80117
  deviceStatus: deviceStatusCapability,
79584
80118
  dishwasher: dishwasherCapability,
80119
+ doNotDisturb: doNotDisturbCapability,
79585
80120
  doorbell: doorbellCapability,
79586
80121
  enumSensor: enumSensorCapability,
79587
80122
  eventEmitter: eventEmitterCapability,
@@ -79618,6 +80153,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
79618
80153
  pressureSensor: pressureSensorCapability,
79619
80154
  privacyMask: privacyMaskCapability,
79620
80155
  ptzAutotrack: ptzAutotrackCapability,
80156
+ pumpCycle: pumpCycleCapability,
79621
80157
  recordingOnboard: recordingOnboardCapability,
79622
80158
  recordingSignal: recordingSignalCapability,
79623
80159
  runningState: runningStateCapability,
@@ -81802,6 +82338,27 @@ Object.freeze({
81802
82338
  access: "view",
81803
82339
  role: "viewer"
81804
82340
  },
82341
+ "boardKeySource.getBoardKey": {
82342
+ capName: "board-key-source",
82343
+ capScope: "system",
82344
+ addonId: null,
82345
+ access: "view",
82346
+ role: "admin"
82347
+ },
82348
+ "boardKeySource.listBoards": {
82349
+ capName: "board-key-source",
82350
+ capScope: "system",
82351
+ addonId: null,
82352
+ access: "view",
82353
+ role: "admin"
82354
+ },
82355
+ "boardKeySource.listSources": {
82356
+ capName: "board-key-source",
82357
+ capScope: "system",
82358
+ addonId: null,
82359
+ access: "view",
82360
+ role: "admin"
82361
+ },
81805
82362
  "brightness.getStatus": {
81806
82363
  capName: "brightness",
81807
82364
  capScope: "device",
@@ -83139,6 +83696,13 @@ Object.freeze({
83139
83696
  access: "view",
83140
83697
  role: "admin"
83141
83698
  },
83699
+ "deviceManager.dismissDiscoveryProposal": {
83700
+ capName: "device-manager",
83701
+ capScope: "system",
83702
+ addonId: null,
83703
+ access: "create",
83704
+ role: "admin"
83705
+ },
83142
83706
  "deviceManager.enable": {
83143
83707
  capName: "device-manager",
83144
83708
  capScope: "system",
@@ -83321,6 +83885,13 @@ Object.freeze({
83321
83885
  access: "view",
83322
83886
  role: "viewer"
83323
83887
  },
83888
+ "deviceManager.listDiscoveryProposals": {
83889
+ capName: "device-manager",
83890
+ capScope: "system",
83891
+ addonId: null,
83892
+ access: "view",
83893
+ role: "admin"
83894
+ },
83324
83895
  "deviceManager.listLocationDescriptions": {
83325
83896
  capName: "device-manager",
83326
83897
  capScope: "system",
@@ -83475,6 +84046,13 @@ Object.freeze({
83475
84046
  access: "create",
83476
84047
  role: "admin"
83477
84048
  },
84049
+ "deviceManager.reportDiscoveryObservations": {
84050
+ capName: "device-manager",
84051
+ capScope: "system",
84052
+ addonId: null,
84053
+ access: "create",
84054
+ role: "admin"
84055
+ },
83478
84056
  "deviceManager.runDeviceAction": {
83479
84057
  capName: "device-manager",
83480
84058
  capScope: "system",
@@ -83482,6 +84060,13 @@ Object.freeze({
83482
84060
  access: "create",
83483
84061
  role: "admin"
83484
84062
  },
84063
+ "deviceManager.runDiscoverySweep": {
84064
+ capName: "device-manager",
84065
+ capScope: "system",
84066
+ addonId: null,
84067
+ access: "create",
84068
+ role: "admin"
84069
+ },
83485
84070
  "deviceManager.setChildLayout": {
83486
84071
  capName: "device-manager",
83487
84072
  capScope: "system",
@@ -84035,6 +84620,20 @@ Object.freeze({
84035
84620
  access: "create",
84036
84621
  role: "admin"
84037
84622
  },
84623
+ "doNotDisturb.getStatus": {
84624
+ capName: "do-not-disturb",
84625
+ capScope: "device",
84626
+ addonId: null,
84627
+ access: "view",
84628
+ role: "viewer"
84629
+ },
84630
+ "doNotDisturb.setEnabled": {
84631
+ capName: "do-not-disturb",
84632
+ capScope: "device",
84633
+ addonId: null,
84634
+ access: "create",
84635
+ role: "viewer"
84636
+ },
84038
84637
  "doorbell.getStatus": {
84039
84638
  capName: "doorbell",
84040
84639
  capScope: "device",
@@ -84399,6 +84998,69 @@ Object.freeze({
84399
84998
  access: "create",
84400
84999
  role: "admin"
84401
85000
  },
85001
+ "integrationInstances.createInstance": {
85002
+ capName: "integration-instances",
85003
+ capScope: "system",
85004
+ addonId: null,
85005
+ access: "create",
85006
+ role: "admin"
85007
+ },
85008
+ "integrationInstances.deleteInstance": {
85009
+ capName: "integration-instances",
85010
+ capScope: "system",
85011
+ addonId: null,
85012
+ access: "delete",
85013
+ role: "admin"
85014
+ },
85015
+ "integrationInstances.describe": {
85016
+ capName: "integration-instances",
85017
+ capScope: "system",
85018
+ addonId: null,
85019
+ access: "view",
85020
+ role: "admin"
85021
+ },
85022
+ "integrationInstances.getInstance": {
85023
+ capName: "integration-instances",
85024
+ capScope: "system",
85025
+ addonId: null,
85026
+ access: "view",
85027
+ role: "admin"
85028
+ },
85029
+ "integrationInstances.instanceFormSchema": {
85030
+ capName: "integration-instances",
85031
+ capScope: "system",
85032
+ addonId: null,
85033
+ access: "view",
85034
+ role: "admin"
85035
+ },
85036
+ "integrationInstances.listInstances": {
85037
+ capName: "integration-instances",
85038
+ capScope: "system",
85039
+ addonId: null,
85040
+ access: "view",
85041
+ role: "admin"
85042
+ },
85043
+ "integrationInstances.setInstanceEnabled": {
85044
+ capName: "integration-instances",
85045
+ capScope: "system",
85046
+ addonId: null,
85047
+ access: "create",
85048
+ role: "admin"
85049
+ },
85050
+ "integrationInstances.testInstance": {
85051
+ capName: "integration-instances",
85052
+ capScope: "system",
85053
+ addonId: null,
85054
+ access: "create",
85055
+ role: "admin"
85056
+ },
85057
+ "integrationInstances.updateInstance": {
85058
+ capName: "integration-instances",
85059
+ capScope: "system",
85060
+ addonId: null,
85061
+ access: "create",
85062
+ role: "admin"
85063
+ },
84402
85064
  "integrations.create": {
84403
85065
  capName: "integrations",
84404
85066
  capScope: "system",
@@ -86520,27 +87182,6 @@ Object.freeze({
86520
87182
  access: "create",
86521
87183
  role: "viewer"
86522
87184
  },
86523
- "petFountain.setPumpInterval": {
86524
- capName: "pet-fountain",
86525
- capScope: "device",
86526
- addonId: null,
86527
- access: "create",
86528
- role: "viewer"
86529
- },
86530
- "petFountain.setPumpOffMinutes": {
86531
- capName: "pet-fountain",
86532
- capScope: "device",
86533
- addonId: null,
86534
- access: "create",
86535
- role: "viewer"
86536
- },
86537
- "petFountain.setPumpOnMinutes": {
86538
- capName: "pet-fountain",
86539
- capScope: "device",
86540
- addonId: null,
86541
- access: "create",
86542
- role: "viewer"
86543
- },
86544
87185
  "pipelineAnalytics.addRetrainFrameFromRecording": {
86545
87186
  capName: "pipeline-analytics",
86546
87187
  capScope: "device",
@@ -88319,6 +88960,34 @@ Object.freeze({
88319
88960
  access: "create",
88320
88961
  role: "admin"
88321
88962
  },
88963
+ "pumpCycle.getStatus": {
88964
+ capName: "pump-cycle",
88965
+ capScope: "device",
88966
+ addonId: null,
88967
+ access: "view",
88968
+ role: "viewer"
88969
+ },
88970
+ "pumpCycle.setEnabled": {
88971
+ capName: "pump-cycle",
88972
+ capScope: "device",
88973
+ addonId: null,
88974
+ access: "create",
88975
+ role: "viewer"
88976
+ },
88977
+ "pumpCycle.setOffMinutes": {
88978
+ capName: "pump-cycle",
88979
+ capScope: "device",
88980
+ addonId: null,
88981
+ access: "create",
88982
+ role: "viewer"
88983
+ },
88984
+ "pumpCycle.setOnMinutes": {
88985
+ capName: "pump-cycle",
88986
+ capScope: "device",
88987
+ addonId: null,
88988
+ access: "create",
88989
+ role: "viewer"
88990
+ },
88322
88991
  "reboot.reboot": {
88323
88992
  capName: "reboot",
88324
88993
  capScope: "device",
@@ -90055,6 +90724,13 @@ Object.freeze({
90055
90724
  access: "create",
90056
90725
  role: "viewer"
90057
90726
  },
90727
+ "system.debugWatch": {
90728
+ capName: "system",
90729
+ capScope: "system",
90730
+ addonId: null,
90731
+ access: "create",
90732
+ role: "admin"
90733
+ },
90058
90734
  "system.detectSiteLocation": {
90059
90735
  capName: "system",
90060
90736
  capScope: "system",
@@ -92223,6 +92899,16 @@ Object.freeze({
92223
92899
  form: "single",
92224
92900
  optional: false
92225
92901
  }],
92902
+ "doNotDisturb.getStatus": [{
92903
+ name: "deviceId",
92904
+ form: "single",
92905
+ optional: false
92906
+ }],
92907
+ "doNotDisturb.setEnabled": [{
92908
+ name: "deviceId",
92909
+ form: "single",
92910
+ optional: false
92911
+ }],
92226
92912
  "doorbell.getStatus": [{
92227
92913
  name: "deviceId",
92228
92914
  form: "single",
@@ -92846,21 +93532,6 @@ Object.freeze({
92846
93532
  form: "single",
92847
93533
  optional: false
92848
93534
  }],
92849
- "petFountain.setPumpInterval": [{
92850
- name: "deviceId",
92851
- form: "single",
92852
- optional: false
92853
- }],
92854
- "petFountain.setPumpOffMinutes": [{
92855
- name: "deviceId",
92856
- form: "single",
92857
- optional: false
92858
- }],
92859
- "petFountain.setPumpOnMinutes": [{
92860
- name: "deviceId",
92861
- form: "single",
92862
- optional: false
92863
- }],
92864
93535
  "pipelineAnalytics.addRetrainFrameFromRecording": [{
92865
93536
  name: "deviceId",
92866
93537
  form: "single",
@@ -93496,6 +94167,26 @@ Object.freeze({
93496
94167
  form: "single",
93497
94168
  optional: false
93498
94169
  }],
94170
+ "pumpCycle.getStatus": [{
94171
+ name: "deviceId",
94172
+ form: "single",
94173
+ optional: false
94174
+ }],
94175
+ "pumpCycle.setEnabled": [{
94176
+ name: "deviceId",
94177
+ form: "single",
94178
+ optional: false
94179
+ }],
94180
+ "pumpCycle.setOffMinutes": [{
94181
+ name: "deviceId",
94182
+ form: "single",
94183
+ optional: false
94184
+ }],
94185
+ "pumpCycle.setOnMinutes": [{
94186
+ name: "deviceId",
94187
+ form: "single",
94188
+ optional: false
94189
+ }],
93499
94190
  "reboot.reboot": [{
93500
94191
  name: "deviceId",
93501
94192
  form: "single",
@@ -93963,6 +94654,11 @@ Object.freeze({
93963
94654
  form: "single",
93964
94655
  optional: false
93965
94656
  }],
94657
+ "system.debugWatch": [{
94658
+ name: "deviceIds",
94659
+ form: "array",
94660
+ optional: true
94661
+ }],
93966
94662
  "tamper.getStatus": [{
93967
94663
  name: "deviceId",
93968
94664
  form: "single",
@@ -140378,6 +141074,13 @@ var ALEXA_BROKER_KIND = "alexa";
140378
141074
  var DEVICE_REFRESH_MS = 5 * 6e4;
140379
141075
  /** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
140380
141076
  var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
141077
+ /** How often an adopted Echo's player state is read (a push channel is a later phase). */
141078
+ var ECHO_POLL_MS = 3e4;
141079
+ /** One more read after a command, once Amazon has had time to apply it. */
141080
+ var ECHO_REREAD_MS = 1500;
141081
+ /** After a 429 the account is left alone for this long, doubling up to the cap. */
141082
+ var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
141083
+ var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
140381
141084
  //#endregion
140382
141085
  //#region src/alexa-broker-provider.ts
140383
141086
  /**
@@ -140417,7 +141120,7 @@ function buildAlexaBrokerProvider(deps) {
140417
141120
  label: "Alexa",
140418
141121
  signIn: {
140419
141122
  basePath: "/addon/import-alexa/login",
140420
- methods: ["device-code"]
141123
+ methods: ["device-code", "proxy"]
140421
141124
  }
140422
141125
  }]
140423
141126
  }],
@@ -140607,6 +141310,37 @@ function buildAlexaApplianceCandidate(input) {
140607
141310
  function mappableAppliance(appliance) {
140608
141311
  return mapAlexaCapabilitiesToCamStack(appliance.interfaces).capabilities.length > 0;
140609
141312
  }
141313
+ /** `echo:<serial>`: how an Echo is named among an account's adoption candidates. */
141314
+ var ECHO_NATIVE_ID_PREFIX = "echo:";
141315
+ function echoNativeId(serial) {
141316
+ return `${ECHO_NATIVE_ID_PREFIX}${serial}`;
141317
+ }
141318
+ /** The serial an Echo candidate id names, or null when the id is not an Echo's. */
141319
+ function echoSerialOf(childNativeId) {
141320
+ if (!childNativeId.startsWith("echo:")) return null;
141321
+ const serial = childNativeId.slice(5);
141322
+ return serial === "" ? null : serial;
141323
+ }
141324
+ /** An Echo the adoption list offers: a media player, a flat sibling under the Alexa integration. */
141325
+ function isAdoptableEcho(device) {
141326
+ return device.kind === "device";
141327
+ }
141328
+ function buildAlexaEchoCandidate(input) {
141329
+ const { device, adoptedDeviceId } = input;
141330
+ return {
141331
+ childNativeId: echoNativeId(device.id),
141332
+ name: device.name.length > 0 ? device.name : device.id,
141333
+ type: DeviceType.MediaPlayer,
141334
+ status: device.online ? "online" : "offline",
141335
+ metadata: {
141336
+ manufacturer: "Amazon",
141337
+ ...device.room !== void 0 ? { externalLocation: device.room } : {}
141338
+ },
141339
+ alreadyAdopted: adoptedDeviceId !== null,
141340
+ adoptedDeviceId,
141341
+ capabilities: ["media-player", "do-not-disturb"]
141342
+ };
141343
+ }
140610
141344
  //#endregion
140611
141345
  //#region src/alexa-adoption-provider.ts
140612
141346
  /** Single granularity: each Alexa appliance adopts as one device. */
@@ -140620,6 +141354,11 @@ async function adoptedApplianceId(brokerId, applianceId, listAdoptedAlexa) {
140620
141354
  for (const d of await listAdoptedAlexa()) if (d.config["system"] === "alexa" && d.config["brokerId"] === brokerId && d.config["applianceId"] === applianceId) return d.id;
140621
141355
  return null;
140622
141356
  }
141357
+ /** Resolve the adopted CamStack device id for a (broker, Echo serial), or null. */
141358
+ async function adoptedEchoId(brokerId, serial, listAdoptedAlexa) {
141359
+ for (const d of await listAdoptedAlexa()) if (d.config["system"] === "alexa" && d.config["brokerId"] === brokerId && d.config["serial"] === serial) return d.id;
141360
+ return null;
141361
+ }
140623
141362
  /**
140624
141363
  * Construct the `device-adoption` cap provider for the Alexa import addon. Pure
140625
141364
  * builder. One broker = one Amazon account; each mappable appliance is a flat
@@ -140627,7 +141366,7 @@ async function adoptedApplianceId(brokerId, applianceId, listAdoptedAlexa) {
140627
141366
  * Container fan-out — Alexa appliances are standalone devices).
140628
141367
  */
140629
141368
  function buildAlexaAdoptionProvider(deps) {
140630
- const { registry, getBrokerIdForIntegration, listAdoptedAlexa, adoptAppliance, removeDevice, findDeviceConfig, logger } = deps;
141369
+ const { registry, getBrokerIdForIntegration, listAdoptedAlexa, adoptAppliance, adoptEcho, removeDevice, findDeviceConfig, logger } = deps;
140631
141370
  async function candidatesForBroker(brokerId) {
140632
141371
  const appliances = registry.getAppliancesForBroker(brokerId).filter(mappableAppliance);
140633
141372
  const out = [];
@@ -140638,6 +141377,13 @@ function buildAlexaAdoptionProvider(deps) {
140638
141377
  adoptedDeviceId
140639
141378
  }));
140640
141379
  }
141380
+ for (const device of registry.getEchoesForBroker(brokerId).filter(isAdoptableEcho)) {
141381
+ const adoptedDeviceId = await adoptedEchoId(brokerId, device.id, listAdoptedAlexa);
141382
+ out.push(buildAlexaEchoCandidate({
141383
+ device,
141384
+ adoptedDeviceId
141385
+ }));
141386
+ }
140641
141387
  return out;
140642
141388
  }
140643
141389
  return {
@@ -140692,7 +141438,41 @@ function buildAlexaAdoptionProvider(deps) {
140692
141438
  const appliances = registry.getAppliancesForBroker(brokerId);
140693
141439
  const adopted = [];
140694
141440
  let failures = 0;
141441
+ const echoes = registry.getEchoesForBroker(brokerId);
140695
141442
  for (const nativeId of childNativeIds) {
141443
+ const echoSerial = echoSerialOf(nativeId);
141444
+ if (echoSerial !== null) {
141445
+ const echo = echoes.find((device) => device.id === echoSerial && isAdoptableEcho(device));
141446
+ if (echo === void 0) {
141447
+ logger.warn("alexa adopt: Echo not found in the account — skipping", { meta: {
141448
+ nativeId,
141449
+ brokerId
141450
+ } });
141451
+ failures++;
141452
+ continue;
141453
+ }
141454
+ try {
141455
+ const { deviceId } = await adoptEcho({
141456
+ brokerId,
141457
+ echo,
141458
+ integrationId,
141459
+ name: perCandidate?.[nativeId]?.name ?? echo.name
141460
+ });
141461
+ adopted.push({
141462
+ childNativeId: nativeId,
141463
+ parentDeviceId: deviceId,
141464
+ accessoryDeviceIds: []
141465
+ });
141466
+ } catch (err) {
141467
+ logger.warn("alexa adopt: failed to adopt Echo", { meta: {
141468
+ brokerId,
141469
+ serial: echoSerial,
141470
+ error: errMsg(err)
141471
+ } });
141472
+ failures++;
141473
+ }
141474
+ continue;
141475
+ }
140696
141476
  const appliance = appliances.find((a) => a.applianceId === nativeId);
140697
141477
  if (appliance === void 0) {
140698
141478
  logger.warn("alexa adopt: appliance not found in inventory — skipping", { meta: {
@@ -140785,6 +141565,7 @@ function setBrokerDevicesOnline(devices, brokerId, online) {
140785
141565
  for (const dev of devices) {
140786
141566
  if (dev.config.get("system") !== "alexa") continue;
140787
141567
  if (dev.config.get("brokerId") !== brokerId) continue;
141568
+ if (typeof dev.config.get("serial") === "string") continue;
140788
141569
  if (dev.online === online) continue;
140789
141570
  dev.markOnline(online);
140790
141571
  count += 1;
@@ -141192,6 +141973,421 @@ var AlexaDevice = class extends BaseDevice {
141192
141973
  function isKnownCap(cap) {
141193
141974
  return KNOWN_CAPS.has(cap);
141194
141975
  }
141976
+ var EchoSessionRegistry = class {
141977
+ #resolve = null;
141978
+ #dnd = /* @__PURE__ */ new Map();
141979
+ setResolver(fn) {
141980
+ this.#resolve = fn;
141981
+ }
141982
+ get(brokerId) {
141983
+ return this.#resolve === null ? null : this.#resolve(brokerId);
141984
+ }
141985
+ /** Do Not Disturb of every device of the account. A failed read is not kept for the next device. */
141986
+ dnd(brokerId, now) {
141987
+ const shared = this.#dnd.get(brokerId);
141988
+ if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
141989
+ const session = this.get(brokerId);
141990
+ if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
141991
+ const pending = session.dndState();
141992
+ this.#dnd.set(brokerId, {
141993
+ at: now,
141994
+ pending
141995
+ });
141996
+ pending.catch(() => {
141997
+ if (this.#dnd.get(brokerId)?.pending === pending) this.#dnd.delete(brokerId);
141998
+ });
141999
+ return pending;
142000
+ }
142001
+ clear() {
142002
+ this.#resolve = null;
142003
+ this.#dnd.clear();
142004
+ }
142005
+ };
142006
+ /** The single in-process registry shared across the addon. */
142007
+ var alexaEchoSessions = new EchoSessionRegistry();
142008
+ //#endregion
142009
+ //#region src/alexa-echo-state-to-cap.ts
142010
+ /** The slice of an Echo that cannot be read: every reading null, the state as given. */
142011
+ function emptyEchoStatus(state, now) {
142012
+ return {
142013
+ state,
142014
+ stateSource: "reported",
142015
+ assumedFields: [],
142016
+ volumeLevel: null,
142017
+ isMuted: null,
142018
+ source: "",
142019
+ availableSources: [],
142020
+ currentMedia: null,
142021
+ position: null,
142022
+ duration: null,
142023
+ shuffle: null,
142024
+ repeat: null,
142025
+ lastChangedAt: now
142026
+ };
142027
+ }
142028
+ function mapEchoPlaybackState(value) {
142029
+ switch (value) {
142030
+ case "PLAYING": return "playing";
142031
+ case "PAUSED": return "paused";
142032
+ case "BUFFERING": return "buffering";
142033
+ default: return "idle";
142034
+ }
142035
+ }
142036
+ function percent(value) {
142037
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
142038
+ return Math.round(Math.max(0, Math.min(100, value)));
142039
+ }
142040
+ function secondsToMs(value) {
142041
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
142042
+ return Math.round(value * 1e3);
142043
+ }
142044
+ function nonEmpty(value) {
142045
+ return typeof value === "string" && value !== "" ? value : null;
142046
+ }
142047
+ /** `SELECTED` is on, `UNSELECTED` is off; `HIDDEN` and `DISABLED` mean the surface does not exist. */
142048
+ function shuffleOf(value) {
142049
+ if (value === "SELECTED") return true;
142050
+ if (value === "UNSELECTED") return false;
142051
+ return null;
142052
+ }
142053
+ function repeatOf(value) {
142054
+ if (value === "SELECTED") return "all";
142055
+ if (value === "UNSELECTED") return "off";
142056
+ return null;
142057
+ }
142058
+ function currentMediaOf(info) {
142059
+ const title = nonEmpty(info.infoText?.title);
142060
+ if (title === null) return null;
142061
+ const artist = nonEmpty(info.infoText?.subText1);
142062
+ const album = nonEmpty(info.infoText?.subText2);
142063
+ const imageUrl = nonEmpty(info.mainArt?.url);
142064
+ return {
142065
+ type: "unknown",
142066
+ title,
142067
+ ...artist !== null ? { artist } : {},
142068
+ ...album !== null ? { album } : {},
142069
+ ...imageUrl !== null ? { imageUrl } : {}
142070
+ };
142071
+ }
142072
+ function mapPlayerInfoToStatus(info, input) {
142073
+ if (!input.online) return emptyEchoStatus("off", input.now);
142074
+ const length = secondsToMs(info.progress?.mediaLength);
142075
+ return {
142076
+ ...emptyEchoStatus(mapEchoPlaybackState(info.state), input.now),
142077
+ volumeLevel: percent(info.volume?.volume),
142078
+ isMuted: typeof info.volume?.muted === "boolean" ? info.volume.muted : null,
142079
+ currentMedia: currentMediaOf(info),
142080
+ position: secondsToMs(info.progress?.mediaProgress),
142081
+ duration: length === 0 ? null : length,
142082
+ shuffle: shuffleOf(info.transport?.shuffle),
142083
+ repeat: repeatOf(info.transport?.repeat)
142084
+ };
142085
+ }
142086
+ //#endregion
142087
+ //#region src/speak/worker-protocol.ts
142088
+ /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
142089
+ var EchoDeviceSchema = object({
142090
+ id: string().min(1),
142091
+ name: string(),
142092
+ family: string(),
142093
+ online: boolean(),
142094
+ kind: _enum(["device", "group"]),
142095
+ dnd: boolean(),
142096
+ deviceType: string(),
142097
+ /** The room the account files the device under, when Amazon names one. */
142098
+ room: string().optional()
142099
+ });
142100
+ var LoginStatusSchema = _enum(["ok", "failed"]);
142101
+ var SessionStatusSchema = object({
142102
+ loggedIn: boolean(),
142103
+ ageSec: number$1().int().nonnegative(),
142104
+ lastSuccessAt: number$1().int().nullable()
142105
+ });
142106
+ var TargetDeliverySchema = object({
142107
+ targetId: string(),
142108
+ ok: boolean(),
142109
+ error: string().optional()
142110
+ });
142111
+ var WorkerLineSchema = union([object({
142112
+ id: string(),
142113
+ ok: literal(true),
142114
+ result: unknown().optional()
142115
+ }), object({
142116
+ id: string(),
142117
+ ok: literal(false),
142118
+ error: string(),
142119
+ /** A typed reason the worker names; `rate-limited` is Amazon's 429. */
142120
+ code: string().optional()
142121
+ })]);
142122
+ var RATE_LIMITED_CODE = "rate-limited";
142123
+ /** Amazon answered 429. Never an empty answer: the caller backs off and keeps what it had. */
142124
+ var RateLimitedError = class extends Error {
142125
+ code = RATE_LIMITED_CODE;
142126
+ constructor(message) {
142127
+ super(message);
142128
+ this.name = "RateLimitedError";
142129
+ }
142130
+ };
142131
+ function requestLine(id, request) {
142132
+ return `${JSON.stringify({
142133
+ id,
142134
+ ...request
142135
+ })}\n`;
142136
+ }
142137
+ function parseLoginResult(result) {
142138
+ const parsed = object({
142139
+ status: LoginStatusSchema,
142140
+ error: string().optional()
142141
+ }).safeParse(result);
142142
+ if (!parsed.success) return {
142143
+ status: "failed",
142144
+ error: "worker returned an unexpected login result"
142145
+ };
142146
+ return parsed.data;
142147
+ }
142148
+ function parseDevices(result) {
142149
+ const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
142150
+ if (!parsed.success) throw new Error("worker returned an unexpected device list");
142151
+ return parsed.data.devices;
142152
+ }
142153
+ function parseDeliveries(result) {
142154
+ const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
142155
+ if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
142156
+ return parsed.data.outcomes;
142157
+ }
142158
+ function parseSessionStatus(result) {
142159
+ const parsed = SessionStatusSchema.safeParse(result);
142160
+ if (!parsed.success) throw new Error("worker returned an unexpected session status");
142161
+ return parsed.data;
142162
+ }
142163
+ /**
142164
+ * The raw `playerInfo` of `/api/np/player`. Amazon omits what it does not know,
142165
+ * so every field is nullable; the mapping to the media-player cap turns a missing
142166
+ * value into null, never into 0 (D393).
142167
+ */
142168
+ var PlayerInfoSchema = object({
142169
+ state: string().nullish(),
142170
+ volume: object({
142171
+ volume: number$1().nullish(),
142172
+ muted: boolean().nullish()
142173
+ }).nullish(),
142174
+ infoText: object({
142175
+ title: string().nullish(),
142176
+ subText1: string().nullish(),
142177
+ subText2: string().nullish()
142178
+ }).nullish(),
142179
+ mainArt: object({ url: string().nullish() }).nullish(),
142180
+ progress: object({
142181
+ mediaLength: number$1().nullish(),
142182
+ mediaProgress: number$1().nullish()
142183
+ }).nullish(),
142184
+ transport: object({
142185
+ shuffle: string().nullish(),
142186
+ repeat: string().nullish()
142187
+ }).nullish()
142188
+ });
142189
+ function parsePlayerState(result) {
142190
+ const parsed = object({ playerInfo: PlayerInfoSchema.nullish() }).safeParse(result);
142191
+ if (!parsed.success) throw new Error("worker returned an unexpected player state");
142192
+ return parsed.data.playerInfo ?? {};
142193
+ }
142194
+ var DndEntrySchema = object({
142195
+ serial: string().min(1),
142196
+ enabled: boolean()
142197
+ });
142198
+ function parseDndState(result) {
142199
+ const parsed = object({ devices: array(DndEntrySchema) }).safeParse(result);
142200
+ if (!parsed.success) throw new Error("worker returned an unexpected Do Not Disturb list");
142201
+ return new Map(parsed.data.devices.map((entry) => [entry.serial, entry.enabled]));
142202
+ }
142203
+ //#endregion
142204
+ //#region src/devices/alexa-echo-device.ts
142205
+ /**
142206
+ * An Echo is a media-player device. Identity only: the account (`brokerId`) and
142207
+ * the Echo's serial. The Echo's notification target is a separate record linked
142208
+ * by the same (brokerId, serial); nothing here sets or reads a notification.
142209
+ */
142210
+ var alexaEchoDeviceSchema = object({
142211
+ brokerId: string().min(1).describe("Registry broker id"),
142212
+ serial: string().min(1).describe("The Echo serial number"),
142213
+ deviceType: string().default("").describe("Amazon device type id"),
142214
+ system: literal("alexa").optional(),
142215
+ integrationId: string().optional()
142216
+ });
142217
+ var COLD_DND = {
142218
+ enabled: null,
142219
+ lastChangedAt: 0
142220
+ };
142221
+ function unsupported(what) {
142222
+ throw new Error(`${what} is not available on an Echo`);
142223
+ }
142224
+ var AlexaEchoDevice = class extends BaseDevice {
142225
+ features = [
142226
+ DeviceFeature.MediaPlayerVolume,
142227
+ DeviceFeature.MediaPlayerNext,
142228
+ DeviceFeature.MediaPlayerPrevious
142229
+ ];
142230
+ brokerId;
142231
+ serial;
142232
+ amazonDeviceType;
142233
+ timer = null;
142234
+ rereadTimer = null;
142235
+ polling = false;
142236
+ failureLogged = false;
142237
+ health = "ok";
142238
+ constructor(ctx) {
142239
+ const persisted = alexaEchoDeviceSchema.parse(ctx.persistedConfig ?? {});
142240
+ super(ctx, alexaEchoDeviceSchema, { type: DeviceType.MediaPlayer });
142241
+ this.brokerId = persisted.brokerId;
142242
+ this.serial = persisted.serial;
142243
+ this.amazonDeviceType = persisted.deviceType;
142244
+ }
142245
+ /** The outcome of the last read, for the broker status and the specs. */
142246
+ get pollHealth() {
142247
+ return this.health;
142248
+ }
142249
+ async onActivate() {
142250
+ await super.onActivate();
142251
+ this.registerMediaPlayer();
142252
+ this.registerDoNotDisturb();
142253
+ this.runtimeState.setCapState("media-player", emptyEchoStatus("off", 0));
142254
+ this.runtimeState.setCapState("do-not-disturb", COLD_DND);
142255
+ this.poll();
142256
+ this.timer = setInterval(() => void this.poll(), ECHO_POLL_MS);
142257
+ this.timer.unref();
142258
+ }
142259
+ async removeDevice() {
142260
+ if (this.timer !== null) clearInterval(this.timer);
142261
+ if (this.rereadTimer !== null) clearTimeout(this.rereadTimer);
142262
+ this.timer = null;
142263
+ this.rereadTimer = null;
142264
+ await super.removeDevice();
142265
+ }
142266
+ session() {
142267
+ const session = alexaEchoSessions.get(this.brokerId);
142268
+ if (session === null) throw new Error("the Amazon account of this Echo is not connected");
142269
+ return session;
142270
+ }
142271
+ /** One re-read shortly after a command, once Amazon has applied it. */
142272
+ rereadSoon() {
142273
+ if (this.rereadTimer !== null) clearTimeout(this.rereadTimer);
142274
+ this.rereadTimer = setTimeout(() => {
142275
+ this.rereadTimer = null;
142276
+ this.poll();
142277
+ }, ECHO_REREAD_MS);
142278
+ this.rereadTimer.unref();
142279
+ }
142280
+ /**
142281
+ * Read the player and Do Not Disturb. A failed read leaves both slices as they
142282
+ * were and marks the device degraded; a 429 is a pause, not a fault. Never throws.
142283
+ */
142284
+ async poll() {
142285
+ if (this.polling) return;
142286
+ this.polling = true;
142287
+ try {
142288
+ await this.readOnce();
142289
+ } finally {
142290
+ this.polling = false;
142291
+ }
142292
+ }
142293
+ async readOnce() {
142294
+ const session = alexaEchoSessions.get(this.brokerId);
142295
+ if (session === null || session.state !== "ready") return;
142296
+ if (session.rateLimitedUntil !== null) {
142297
+ this.health = "rate-limited";
142298
+ return;
142299
+ }
142300
+ const listed = session.devices.find((device) => device.id === this.serial);
142301
+ if (listed !== void 0 && !listed.online) {
142302
+ this.runtimeState.setCapState("media-player", emptyEchoStatus("off", Date.now()));
142303
+ this.markOnline(false);
142304
+ this.health = "ok";
142305
+ return;
142306
+ }
142307
+ const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
142308
+ try {
142309
+ const info = await session.playerState(this.serial, deviceType);
142310
+ const now = Date.now();
142311
+ this.runtimeState.setCapState("media-player", mapPlayerInfoToStatus(info, {
142312
+ online: true,
142313
+ now
142314
+ }));
142315
+ this.markOnline(true);
142316
+ this.noteRecovered();
142317
+ } catch (err) {
142318
+ this.noteReadFailure(err);
142319
+ return;
142320
+ }
142321
+ await this.readDoNotDisturb();
142322
+ }
142323
+ async readDoNotDisturb() {
142324
+ try {
142325
+ const enabled = (await alexaEchoSessions.dnd(this.brokerId, Date.now())).get(this.serial);
142326
+ if (enabled === void 0) return;
142327
+ this.runtimeState.setCapState("do-not-disturb", {
142328
+ enabled,
142329
+ lastChangedAt: Date.now()
142330
+ });
142331
+ } catch (err) {
142332
+ this.noteReadFailure(err);
142333
+ }
142334
+ }
142335
+ noteRecovered() {
142336
+ if (this.failureLogged) this.ctx.logger.info("alexa echo: reads are working again", { tags: { deviceId: this.id } });
142337
+ this.failureLogged = false;
142338
+ this.health = "ok";
142339
+ }
142340
+ noteReadFailure(err) {
142341
+ if (err instanceof RateLimitedError) {
142342
+ this.health = "rate-limited";
142343
+ this.ctx.logger.info("alexa echo: amazon asked for a pause, keeping the last state", { tags: { deviceId: this.id } });
142344
+ return;
142345
+ }
142346
+ this.health = "degraded";
142347
+ if (this.failureLogged) return;
142348
+ this.failureLogged = true;
142349
+ this.ctx.logger.warn("alexa echo: could not read the player, keeping the last state", {
142350
+ tags: { deviceId: this.id },
142351
+ meta: { error: err instanceof Error ? err.message : String(err) }
142352
+ });
142353
+ }
142354
+ async command(command) {
142355
+ await this.session().npCommand(this.serial, command);
142356
+ this.rereadSoon();
142357
+ }
142358
+ registerMediaPlayer() {
142359
+ this.ctx.registerNativeCap(mediaPlayerCapability, {
142360
+ getStatus: async () => this.runtimeState.getCapState("media-player") ?? emptyEchoStatus("off", 0),
142361
+ play: () => this.command("play"),
142362
+ pause: () => this.command("pause"),
142363
+ next: () => this.command("next"),
142364
+ previous: () => this.command("previous"),
142365
+ setVolume: async ({ volumeLevel }) => {
142366
+ await this.session().setVolume(this.serial, Math.round(volumeLevel));
142367
+ this.rereadSoon();
142368
+ },
142369
+ stop: async () => unsupported("stop"),
142370
+ seek: async () => unsupported("seek"),
142371
+ setMute: async () => unsupported("mute"),
142372
+ setShuffle: async () => unsupported("shuffle"),
142373
+ setRepeat: async () => unsupported("repeat"),
142374
+ selectSource: async () => unsupported("source selection"),
142375
+ playMedia: async () => unsupported("playing media")
142376
+ });
142377
+ }
142378
+ registerDoNotDisturb() {
142379
+ this.ctx.registerNativeCap(doNotDisturbCapability, {
142380
+ getStatus: async () => this.runtimeState.getCapState("do-not-disturb") ?? COLD_DND,
142381
+ setEnabled: async ({ enabled }) => {
142382
+ await this.session().setDnd(this.serial, enabled);
142383
+ this.runtimeState.setCapState("do-not-disturb", {
142384
+ enabled,
142385
+ lastChangedAt: Date.now()
142386
+ });
142387
+ }
142388
+ });
142389
+ }
142390
+ };
141195
142391
  var alexaBrokersEntryKind = defineEntryKind("brokers", {
141196
142392
  title: "Alexa import brokers",
141197
142393
  schema: entryBody({
@@ -141500,78 +142696,14 @@ function scrubSecrets(text, secrets) {
141500
142696
  return out;
141501
142697
  }
141502
142698
  //#endregion
141503
- //#region src/speak/worker-protocol.ts
141504
- /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
141505
- var EchoDeviceSchema = object({
141506
- id: string().min(1),
141507
- name: string(),
141508
- family: string(),
141509
- online: boolean(),
141510
- kind: _enum(["device", "group"]),
141511
- dnd: boolean(),
141512
- deviceType: string(),
141513
- /** The room the account files the device under, when Amazon names one. */
141514
- room: string().optional()
141515
- });
141516
- var LoginStatusSchema = _enum(["ok", "failed"]);
141517
- var SessionStatusSchema = object({
141518
- loggedIn: boolean(),
141519
- ageSec: number$1().int().nonnegative(),
141520
- lastSuccessAt: number$1().int().nullable()
141521
- });
141522
- var TargetDeliverySchema = object({
141523
- targetId: string(),
141524
- ok: boolean(),
141525
- error: string().optional()
141526
- });
141527
- var WorkerLineSchema = union([object({
141528
- id: string(),
141529
- ok: literal(true),
141530
- result: unknown().optional()
141531
- }), object({
141532
- id: string(),
141533
- ok: literal(false),
141534
- error: string()
141535
- })]);
141536
- function requestLine(id, request) {
141537
- return `${JSON.stringify({
141538
- id,
141539
- ...request
141540
- })}\n`;
141541
- }
141542
- function parseLoginResult(result) {
141543
- const parsed = object({
141544
- status: LoginStatusSchema,
141545
- error: string().optional()
141546
- }).safeParse(result);
141547
- if (!parsed.success) return {
141548
- status: "failed",
141549
- error: "worker returned an unexpected login result"
141550
- };
141551
- return parsed.data;
141552
- }
141553
- function parseDevices(result) {
141554
- const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
141555
- if (!parsed.success) throw new Error("worker returned an unexpected device list");
141556
- return parsed.data.devices;
141557
- }
141558
- function parseDeliveries(result) {
141559
- const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
141560
- if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
141561
- return parsed.data.outcomes;
141562
- }
141563
- function parseSessionStatus(result) {
141564
- const parsed = SessionStatusSchema.safeParse(result);
141565
- if (!parsed.success) throw new Error("worker returned an unexpected session status");
141566
- return parsed.data;
141567
- }
141568
- //#endregion
141569
142699
  //#region src/speak/broker-echo.ts
141570
142700
  var BrokerEcho = class {
141571
142701
  deps;
141572
142702
  currentState = "starting";
141573
142703
  currentError = null;
141574
142704
  currentDevices = [];
142705
+ backoffMs = 0;
142706
+ limitedUntil = 0;
141575
142707
  constructor(deps) {
141576
142708
  this.deps = deps;
141577
142709
  }
@@ -141590,6 +142722,13 @@ var BrokerEcho = class {
141590
142722
  get devices() {
141591
142723
  return this.currentDevices;
141592
142724
  }
142725
+ /** Ms epoch until which Amazon asked us to wait (429), or null when the account is free. */
142726
+ get rateLimitedUntil() {
142727
+ return this.limitedUntil > this.nowMs() ? this.limitedUntil : null;
142728
+ }
142729
+ nowMs() {
142730
+ return (this.deps.now ?? Date.now)();
142731
+ }
141593
142732
  stop() {
141594
142733
  this.deps.worker.stop();
141595
142734
  }
@@ -141680,6 +142819,69 @@ var BrokerEcho = class {
141680
142819
  text
141681
142820
  }));
141682
142821
  }
142822
+ /**
142823
+ * Run one worker call. A 429 starts (or doubles) the account's back-off and is
142824
+ * rethrown as is; any answer ends it. Rate limiting is a status, not a failure.
142825
+ */
142826
+ async limited(call) {
142827
+ try {
142828
+ const result = await call();
142829
+ this.backoffMs = 0;
142830
+ this.limitedUntil = 0;
142831
+ return result;
142832
+ } catch (err) {
142833
+ if (err instanceof RateLimitedError) {
142834
+ this.backoffMs = this.backoffMs === 0 ? ECHO_RATE_BACKOFF_BASE_MS : Math.min(this.backoffMs * 2, ECHO_RATE_BACKOFF_MAX_MS);
142835
+ this.limitedUntil = this.nowMs() + this.backoffMs;
142836
+ this.deps.logger.info("alexa echo: amazon asked for a pause (429)", {
142837
+ tags: { brokerId: this.deps.brokerId },
142838
+ meta: { backoffMs: this.backoffMs }
142839
+ });
142840
+ }
142841
+ throw err;
142842
+ }
142843
+ }
142844
+ /** The raw player state of one Echo. Throws on any failure, a 429 included: never an empty answer. */
142845
+ async playerState(serial, deviceType) {
142846
+ await this.requireReady();
142847
+ return this.limited(async () => parsePlayerState(await this.deps.worker.request({
142848
+ op: "player_state",
142849
+ serial,
142850
+ deviceType
142851
+ })));
142852
+ }
142853
+ async npCommand(serial, command) {
142854
+ await this.requireReady();
142855
+ await this.limited(() => this.deps.worker.request({
142856
+ op: "np_command",
142857
+ serial,
142858
+ command
142859
+ }));
142860
+ }
142861
+ async setVolume(serial, level) {
142862
+ await this.requireReady();
142863
+ await this.limited(() => this.deps.worker.request({
142864
+ op: "set_volume",
142865
+ serial,
142866
+ level
142867
+ }));
142868
+ }
142869
+ /** Do Not Disturb of every device of the account, by serial, in one call. */
142870
+ async dndState() {
142871
+ await this.requireReady();
142872
+ return this.limited(async () => parseDndState(await this.deps.worker.request({ op: "dnd_state" })));
142873
+ }
142874
+ async setDnd(serial, enabled) {
142875
+ await this.requireReady();
142876
+ await this.limited(() => this.deps.worker.request({
142877
+ op: "set_dnd",
142878
+ serial,
142879
+ enabled
142880
+ }));
142881
+ }
142882
+ async requireReady() {
142883
+ if (!await this.ensureReady()) throw new Error(this.notReadyReason());
142884
+ }
141683
142885
  async session() {
141684
142886
  if (this.deps.worker.failed) return {
141685
142887
  loggedIn: false,
@@ -142402,6 +143604,7 @@ function startAlexaWorker(deps) {
142402
143604
  const current = pending;
142403
143605
  pending = null;
142404
143606
  if (message.ok) current.resolve(message.result);
143607
+ else if (message.code === "rate-limited") current.reject(new RateLimitedError(scrub(message.error)));
142405
143608
  else current.reject(new Error(scrub(message.error)));
142406
143609
  pump();
142407
143610
  }
@@ -142555,6 +143758,10 @@ var AlexaSpeakController = class {
142555
143758
  this.queue = this.queue.then(() => this.reconcile()).catch(() => void 0);
142556
143759
  return this.queue;
142557
143760
  }
143761
+ /** The live session of one account, for the Echo devices that poll it. */
143762
+ echoFor(brokerId) {
143763
+ return this.echoes.get(brokerId) ?? null;
143764
+ }
142558
143765
  brokerViews() {
142559
143766
  return this.host.listBrokers().map((ref) => {
142560
143767
  const echo = this.echoes.get(ref.id);
@@ -142562,7 +143769,8 @@ var AlexaSpeakController = class {
142562
143769
  id: ref.id,
142563
143770
  name: ref.name,
142564
143771
  ready: echo?.state === "ready",
142565
- devices: echo?.devices ?? []
143772
+ devices: echo?.devices ?? [],
143773
+ rateLimitedUntil: echo?.rateLimitedUntil ?? null
142566
143774
  };
142567
143775
  });
142568
143776
  }
@@ -142606,7 +143814,8 @@ var AlexaSpeakController = class {
142606
143814
  worker: this.makeWorker(ref.id, pythonPath),
142607
143815
  readSeed: () => this.host.readSeed(ref.id),
142608
143816
  sessions: this.host.sessions,
142609
- logger: this.host.logger
143817
+ logger: this.host.logger,
143818
+ now: this.now
142610
143819
  }));
142611
143820
  await Promise.all(refs.map(async (ref) => {
142612
143821
  const echo = this.echoes.get(ref.id);
@@ -142765,7 +143974,10 @@ var ALEXA_DEVICE_TYPES = [
142765
143974
  var AlexaImportAddon = class extends BaseDeviceProvider {
142766
143975
  addonId = "import-alexa";
142767
143976
  providerName = "Alexa";
142768
- deviceClasses = Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice]));
143977
+ deviceClasses = {
143978
+ ...Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice])),
143979
+ [DeviceType.MediaPlayer]: AlexaEchoDevice
143980
+ };
142769
143981
  registry = null;
142770
143982
  proxyLogin = null;
142771
143983
  deviceCodeLogin = null;
@@ -142852,6 +144064,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
142852
144064
  ensurePython: () => this.ctx.deps.ensurePython()
142853
144065
  });
142854
144066
  this.speak = speak;
144067
+ alexaEchoSessions.setResolver((brokerId) => speak.echoFor(brokerId));
142855
144068
  speak.start().catch((err) => {
142856
144069
  this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
142857
144070
  });
@@ -142956,6 +144169,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
142956
144169
  async onShutdown() {
142957
144170
  this.speak?.stop();
142958
144171
  this.speak = null;
144172
+ alexaEchoSessions.clear();
142959
144173
  alexaCommands.clear();
142960
144174
  try {
142961
144175
  await this.proxyLogin?.dispose();
@@ -142979,7 +144193,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
142979
144193
  }
142980
144194
  /** One broker = one account; the appliance id distinguishes devices within it. */
142981
144195
  generateStableId(_type, config) {
142982
- return `alexa:${String(config?.["brokerId"] ?? "unknown")}:${String(config?.["applianceId"] ?? "unknown")}`;
144196
+ const brokerId = String(config?.["brokerId"] ?? "unknown");
144197
+ const serial = config?.["serial"];
144198
+ if (typeof serial === "string") return `alexa:${brokerId}:echo:${serial}`;
144199
+ return `alexa:${brokerId}:${String(config?.["applianceId"] ?? "unknown")}`;
142983
144200
  }
142984
144201
  /**
142985
144202
  * Re-derive every adopted Alexa device's `capabilities[]` from the live
@@ -143142,7 +144359,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
143142
144359
  id: b.id,
143143
144360
  name: b.name
143144
144361
  })),
143145
- has: (id) => this.requireRegistry().get(id) !== null
144362
+ has: (id) => this.requireRegistry().get(id) !== null,
144363
+ getEchoesForBroker: (id) => this.speak?.echoFor(id)?.devices ?? []
143146
144364
  },
143147
144365
  logger: this.ctx.logger,
143148
144366
  getBrokerIdForIntegration: async (id) => {
@@ -143184,6 +144402,23 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
143184
144402
  integrationId
143185
144403
  })).id };
143186
144404
  },
144405
+ adoptEcho: async ({ brokerId, echo, integrationId, name }) => {
144406
+ const devices = this.ctx.kernel.devices;
144407
+ if (!devices) throw new Error("alexa adopt: kernel.devices unavailable");
144408
+ const config = {
144409
+ brokerId,
144410
+ serial: echo.id,
144411
+ deviceType: echo.deviceType,
144412
+ system: "alexa",
144413
+ integrationId,
144414
+ name
144415
+ };
144416
+ return { deviceId: (await devices.create(this.generateStableId(DeviceType.MediaPlayer, config), AlexaEchoDevice, config, null, {
144417
+ type: DeviceType.MediaPlayer,
144418
+ name,
144419
+ integrationId
144420
+ })).id };
144421
+ },
143187
144422
  removeDevice: async (id) => {
143188
144423
  await this.ctx.kernel.devices?.remove(id);
143189
144424
  },