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