@camstack/addon-import-alexa 0.2.188 → 0.2.189

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
@@ -6044,7 +6044,7 @@ function number(params) {
6044
6044
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6045
6045
  }
6046
6046
  //#endregion
6047
- //#region ../types/dist/sleep-9TIHFj1w.mjs
6047
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
6048
6048
  /**
6049
6049
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6050
6050
  * window to float samples (D455).
@@ -6853,6 +6853,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6853
6853
  }
6854
6854
  return out;
6855
6855
  }
6856
+ /** Every scope, in the order the guard and the docs list them. */
6857
+ var SCOPED_SETTINGS_SCOPES = [
6858
+ "cluster",
6859
+ "device-defaults",
6860
+ "node"
6861
+ ];
6856
6862
  /**
6857
6863
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6858
6864
  *
@@ -7744,6 +7750,13 @@ var BaseAddon = class {
7744
7750
  */
7745
7751
  secretStatePaths = [];
7746
7752
  _addonSecretTree = null;
7753
+ /**
7754
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7755
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7756
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7757
+ */
7758
+ _scopedByCap = /* @__PURE__ */ new Map();
7759
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7747
7760
  _deviceSecretTree = null;
7748
7761
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7749
7762
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7813,6 +7826,7 @@ var BaseAddon = class {
7813
7826
  this._ctx = context;
7814
7827
  this._secretSettings = null;
7815
7828
  }
7829
+ await this.loadScopedSchemas();
7816
7830
  await this.resolveConfig();
7817
7831
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7818
7832
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7822,6 +7836,7 @@ var BaseAddon = class {
7822
7836
  const normalized = normalizeAddonInitResult(result);
7823
7837
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7824
7838
  this._registeredCapNames = providers.map((p) => p.capability.name);
7839
+ await this.loadScopedSchemas();
7825
7840
  return normalized;
7826
7841
  }
7827
7842
  /**
@@ -7910,6 +7925,16 @@ var BaseAddon = class {
7910
7925
  schema(schema) {
7911
7926
  return schema;
7912
7927
  }
7928
+ /**
7929
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7930
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7931
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7932
+ * must already know which keys are per node. Names the providers register at init are added
7933
+ * once known.
7934
+ */
7935
+ scopedSettingsCapabilities() {
7936
+ return [];
7937
+ }
7913
7938
  /** Override to provide global-level settings UI schema. */
7914
7939
  globalSettingsSchema(_cap) {
7915
7940
  return null;
@@ -7953,7 +7978,8 @@ var BaseAddon = class {
7953
7978
  return [];
7954
7979
  }
7955
7980
  async getGlobalSettings(overlay, cap, nodeId) {
7956
- const schema = this.globalSettingsSchema(cap);
7981
+ await this.loadScopedSchemas(cap);
7982
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7957
7983
  if (!schema) return { sections: [] };
7958
7984
  const projected = await this.readRawProjectedStore(nodeId, cap);
7959
7985
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8084,6 +8110,7 @@ var BaseAddon = class {
8084
8110
  * resolved blob back is a no-op for every unchanged secret.
8085
8111
  */
8086
8112
  async resolveGlobalStore(nodeId, cap) {
8113
+ await this.loadScopedSchemas(cap);
8087
8114
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8088
8115
  return this.projectForNode(raw, nodeId, cap);
8089
8116
  }
@@ -8094,6 +8121,7 @@ var BaseAddon = class {
8094
8121
  }
8095
8122
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8096
8123
  async readRawProjectedStore(nodeId, cap) {
8124
+ await this.loadScopedSchemas(cap);
8097
8125
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8098
8126
  return this.projectForNode(raw, nodeId, cap);
8099
8127
  }
@@ -8196,6 +8224,7 @@ var BaseAddon = class {
8196
8224
  await this.applyGlobalPatch(patch, nodeId);
8197
8225
  }
8198
8226
  async applyGlobalPatch(barePatch, nodeId) {
8227
+ await this.loadScopedSchemas();
8199
8228
  const keys = this.perNodeKeys();
8200
8229
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8201
8230
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8209,7 +8238,8 @@ var BaseAddon = class {
8209
8238
  await this.resolveConfig();
8210
8239
  await this.onConfigChanged();
8211
8240
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8212
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8241
+ const legacy = this.globalSettingsSchema();
8242
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8213
8243
  }
8214
8244
  /**
8215
8245
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8217,14 +8247,67 @@ var BaseAddon = class {
8217
8247
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8218
8248
  * settings API behaves exactly like the legacy node-agnostic one.
8219
8249
  */
8220
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8250
+ /**
8251
+ * Load the scoped schemas of every capability this addon declares (the override and the
8252
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8253
+ * derived from the previous set. Idempotent.
8254
+ */
8255
+ async loadScopedSchemas(extraCap) {
8256
+ const caps = new Set([
8257
+ ...this.scopedSettingsCapabilities(),
8258
+ ...this._registeredCapNames,
8259
+ ...extraCap === void 0 ? [] : [extraCap]
8260
+ ]);
8261
+ let added = false;
8262
+ for (const cap of caps) {
8263
+ if (this._scopedByCap.has(cap)) continue;
8264
+ const loaded = [];
8265
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8266
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8267
+ if (schema !== null) loaded.push({
8268
+ scope,
8269
+ schema
8270
+ });
8271
+ }
8272
+ this._scopedByCap.set(cap, loaded);
8273
+ added = true;
8274
+ }
8275
+ if (!added) return;
8276
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8277
+ this._addonSecretTree = null;
8278
+ }
8279
+ /**
8280
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8281
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8282
+ */
8283
+ scopedFormSchema(cap) {
8284
+ const byId = /* @__PURE__ */ new Map();
8285
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8286
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8287
+ }
8288
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8289
+ loadedScopedSchemas(cap) {
8290
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8291
+ return [...this._scopedByCap.values()].flat();
8292
+ }
8293
+ /**
8294
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8295
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8296
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8297
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8298
+ * Call {@link loadScopedSchemas} first (the async seams do).
8299
+ */
8221
8300
  perNodeKeys(cap) {
8222
8301
  const cacheKey = cap ?? "";
8223
- const cached = this._perNodeKeysCache.get(cacheKey);
8302
+ const cached = this._nodeKeysByCap.get(cacheKey);
8224
8303
  if (cached) return cached;
8225
- const schema = this.globalSettingsSchema(cap);
8226
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8227
- this._perNodeKeysCache.set(cacheKey, keys);
8304
+ const legacy = this.globalSettingsSchema(cap);
8305
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8306
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8307
+ if (scope !== "node") continue;
8308
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8309
+ }
8310
+ this._nodeKeysByCap.set(cacheKey, keys);
8228
8311
  return keys;
8229
8312
  }
8230
8313
  /**
@@ -8234,10 +8317,9 @@ var BaseAddon = class {
8234
8317
  * time to return its response before the addon is torn down and
8235
8318
  * re-initialised by `AddonRegistryService.restartAddon`.
8236
8319
  */
8237
- maybeAutoRestart(patchKeys, schema) {
8238
- if (!schema) return;
8320
+ maybeAutoRestart(patchKeys, schemas) {
8239
8321
  const restartKeys = /* @__PURE__ */ new Set();
8240
- for (const section of schema.sections) for (const field of section.fields) {
8322
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8241
8323
  if (field.type === "separator" || field.type === "info") continue;
8242
8324
  if (field.requiresRestart) restartKeys.add(field.key);
8243
8325
  }
@@ -8433,12 +8515,14 @@ var BaseAddon = class {
8433
8515
  * here, in the addon's own process, so the hub keeps no copy of it.
8434
8516
  */
8435
8517
  async getDeclaredSettingsKeys() {
8518
+ await this.loadScopedSchemas();
8436
8519
  return { keys: [...this.declaredKeySet()].toSorted() };
8437
8520
  }
8438
8521
  declaredKeySet() {
8439
8522
  const keys = new Set(Object.keys(this.defaults));
8440
8523
  const schema = this.globalSettingsSchema();
8441
8524
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8525
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8442
8526
  for (const key of this._declaredStateKeys) keys.add(key);
8443
8527
  for (const path of this.secretStatePaths) {
8444
8528
  const root = path.split(/[.[]/, 1)[0];
@@ -8534,7 +8618,11 @@ var BaseAddon = class {
8534
8618
  }
8535
8619
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8536
8620
  addonSecretTree() {
8537
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8621
+ if (this._addonSecretTree === null) {
8622
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8623
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8624
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8625
+ }
8538
8626
  return this._addonSecretTree;
8539
8627
  }
8540
8628
  deviceSecretTree() {
@@ -8784,6 +8872,23 @@ function dropPerNodeFields(fields) {
8784
8872
  }
8785
8873
  return kept;
8786
8874
  }
8875
+ /** The keys of the value-holding fields, recursing into layout containers. */
8876
+ function collectValueFieldKeys(fields) {
8877
+ const collected = [];
8878
+ for (const field of fields) {
8879
+ if (field.type === "group") {
8880
+ collected.push(...collectValueFieldKeys(field.fields));
8881
+ continue;
8882
+ }
8883
+ if (field.type === "sub-tabs") {
8884
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8885
+ continue;
8886
+ }
8887
+ if (field.type === "separator" || field.type === "info") continue;
8888
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8889
+ }
8890
+ return collected;
8891
+ }
8787
8892
  function collectFieldKeys(fields) {
8788
8893
  const collected = [];
8789
8894
  for (const field of fields) {
@@ -13576,12 +13681,22 @@ var RecordIntegrationInstances = class {
13576
13681
  ...this.hooks.description,
13577
13682
  mintsId: true
13578
13683
  };
13579
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13684
+ /**
13685
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13686
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13687
+ * record id answers as it is. An id that names nothing comes back unchanged.
13688
+ */
13689
+ async resolveRecordId(id) {
13690
+ if (await this.handle.get(id) !== null) return id;
13691
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13692
+ return linked === void 0 ? id : linked.id;
13693
+ }
13694
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13580
13695
  listInstances = async () => {
13581
13696
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13582
13697
  };
13583
13698
  getInstance = async ({ id }) => {
13584
- const record = await this.handle.get(id);
13699
+ const record = await this.handle.get(await this.resolveRecordId(id));
13585
13700
  return record === null ? null : this.viewOf(record);
13586
13701
  };
13587
13702
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13598,7 +13713,8 @@ var RecordIntegrationInstances = class {
13598
13713
  await this.hooks.onChanged?.(minted, "created");
13599
13714
  return { id: minted };
13600
13715
  };
13601
- updateInstance = async ({ id, name, values }) => {
13716
+ updateInstance = async ({ id: askedId, name, values }) => {
13717
+ const id = await this.resolveRecordId(askedId);
13602
13718
  const patch = {};
13603
13719
  for (const [key, value] of Object.entries(values)) {
13604
13720
  if (value === void 0) continue;
@@ -13633,7 +13749,10 @@ var RecordIntegrationInstances = class {
13633
13749
  status: "unsupported",
13634
13750
  error: "This integration declares no connection test"
13635
13751
  };
13636
- return this.hooks.test(input);
13752
+ return this.hooks.test(input.id === null ? input : {
13753
+ ...input,
13754
+ id: await this.resolveRecordId(input.id)
13755
+ });
13637
13756
  };
13638
13757
  /** The flat provider object to hand to `registerProvider`. */
13639
13758
  toProvider() {
@@ -98407,7 +98526,12 @@ var AMAZON_DOMAIN_BY_COUNTRY = {
98407
98526
  BR: "amazon.com.br",
98408
98527
  MX: "amazon.com.mx"
98409
98528
  };
98410
- /** Hand-written connection form for the broker/integration creation UI. */
98529
+ /**
98530
+ * The connection form: what the operator chooses (region, poll cadence). The cookie blob and the
98531
+ * refresh token are login STATE minted by the in-house sign-in; they live on the record as
98532
+ * keychain secrets, are written by that flow and are never part of this form, so a form save
98533
+ * cannot overwrite or clear them.
98534
+ */
98411
98535
  function buildConnectionFormSchema() {
98412
98536
  return { sections: [{
98413
98537
  id: "alexa-account",
@@ -98416,45 +98540,26 @@ function buildConnectionFormSchema() {
98416
98540
  description: "Sign in with your Amazon account to link it. This is an unofficial integration: Amazon can change or break it without notice, and it may violate Amazon ToS. State is polled, not pushed.",
98417
98541
  descriptionKey: "import-alexa:connection.s_alexa-account.description",
98418
98542
  columns: 1,
98419
- fields: [
98420
- {
98421
- type: "select",
98422
- key: "amazonDomain",
98423
- label: "Amazon domain (region)",
98424
- labelKey: "import-alexa:connection.s_alexa-account.amazonDomain.label",
98425
- options: AMAZON_DOMAINS.map((value) => ({
98426
- value,
98427
- label: value
98428
- })),
98429
- default: "amazon.com",
98430
- defaultByCountry: AMAZON_DOMAIN_BY_COUNTRY
98431
- },
98432
- {
98433
- type: "password",
98434
- key: "cookie",
98435
- label: "Cookie blob (filled by the sign-in)",
98436
- labelKey: "import-alexa:connection.s_alexa-account.cookie.label",
98437
- showToggle: true,
98438
- filledBySignIn: true
98439
- },
98440
- {
98441
- type: "password",
98442
- key: "refreshToken",
98443
- label: "Refresh token (alternative to the sign-in)",
98444
- labelKey: "import-alexa:connection.s_alexa-account.refreshToken.label",
98445
- showToggle: true,
98446
- filledBySignIn: true
98447
- },
98448
- {
98449
- type: "number",
98450
- key: "pollIntervalMs",
98451
- label: "State poll interval (ms)",
98452
- labelKey: "import-alexa:connection.s_alexa-account.pollIntervalMs.label",
98453
- min: 15e3,
98454
- max: 36e5,
98455
- default: 6e4
98456
- }
98457
- ]
98543
+ fields: [{
98544
+ type: "select",
98545
+ key: "amazonDomain",
98546
+ label: "Amazon domain (region)",
98547
+ labelKey: "import-alexa:connection.s_alexa-account.amazonDomain.label",
98548
+ options: AMAZON_DOMAINS.map((value) => ({
98549
+ value,
98550
+ label: value
98551
+ })),
98552
+ default: "amazon.com",
98553
+ defaultByCountry: AMAZON_DOMAIN_BY_COUNTRY
98554
+ }, {
98555
+ type: "number",
98556
+ key: "pollIntervalMs",
98557
+ label: "State poll interval (ms)",
98558
+ labelKey: "import-alexa:connection.s_alexa-account.pollIntervalMs.label",
98559
+ min: 15e3,
98560
+ max: 36e5,
98561
+ default: 6e4
98562
+ }]
98458
98563
  }] };
98459
98564
  }
98460
98565
  //#endregion
@@ -142819,6 +142924,13 @@ var ECHO_REREAD_MS = 1500;
142819
142924
  /** After a 429 the account is left alone for this long, doubling up to the cap. */
142820
142925
  var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
142821
142926
  var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
142927
+ /**
142928
+ * A command (volume, play, Do Not Disturb) that has not started within this, or not
142929
+ * finished, is answered with an error instead of running minutes late.
142930
+ */
142931
+ var ECHO_COMMAND_DEADLINE_MS = 12e3;
142932
+ /** A poll read is stale after this; it is dropped from the queue rather than sent late. */
142933
+ var ECHO_READ_DEADLINE_MS = 25e3;
142822
142934
  //#endregion
142823
142935
  //#region src/alexa-broker-provider.ts
142824
142936
  /**
@@ -144019,6 +144131,10 @@ var AlexaEchoDevice = class extends BaseDevice {
144019
144131
  failureLogged = false;
144020
144132
  volumeFailureLogged = false;
144021
144133
  health = "ok";
144134
+ /** The last level this device asked Amazon for, and when; a step starts from it until a read is newer. */
144135
+ lastSet = null;
144136
+ /** When the read that produced the current media-player slice began. */
144137
+ polledFrom = 0;
144022
144138
  constructor(ctx) {
144023
144139
  const persisted = alexaEchoDeviceSchema.parse(ctx.persistedConfig ?? {});
144024
144140
  super(ctx, alexaEchoDeviceSchema, { type: DeviceType.MediaPlayer });
@@ -144090,7 +144206,10 @@ var AlexaEchoDevice = class extends BaseDevice {
144090
144206
  }
144091
144207
  const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
144092
144208
  try {
144093
- const fromPlayer = mapPlayerInfoToStatus(await session.playerState(this.serial, deviceType), {
144209
+ const readStartedAt = Date.now();
144210
+ const info = await session.playerState(this.serial, deviceType, this.id);
144211
+ this.polledFrom = readStartedAt;
144212
+ const fromPlayer = mapPlayerInfoToStatus(info, {
144094
144213
  online: true,
144095
144214
  now: Date.now()
144096
144215
  });
@@ -144158,16 +144277,28 @@ var AlexaEchoDevice = class extends BaseDevice {
144158
144277
  });
144159
144278
  }
144160
144279
  async command(command) {
144161
- await this.session().npCommand(this.serial, command);
144280
+ await this.session().npCommand(this.serial, command, this.id);
144281
+ this.rereadSoon();
144282
+ }
144283
+ async sendVolume(level) {
144284
+ this.lastSet = {
144285
+ level,
144286
+ at: Date.now()
144287
+ };
144288
+ await this.session().setVolume(this.serial, level, this.id);
144162
144289
  this.rereadSoon();
144163
144290
  }
144164
- /** Amazon has no step command: move from the last known level, refuse when there is none. */
144291
+ /**
144292
+ * Amazon has no step command: move from the level we last SET when no read since
144293
+ * is newer (a polled level lags the command), else the polled one; refuse when
144294
+ * there is none.
144295
+ */
144165
144296
  async stepVolume(delta) {
144166
- const level = this.runtimeState.getCapState("media-player")?.volumeLevel;
144297
+ const polled = this.runtimeState.getCapState("media-player")?.volumeLevel;
144298
+ const set = this.lastSet;
144299
+ const level = set !== null && set.at >= this.polledFrom ? set.level : polled;
144167
144300
  if (level === null || level === void 0) throw new Error("the Echo volume is not known yet");
144168
- const next = Math.max(0, Math.min(100, level + delta));
144169
- await this.session().setVolume(this.serial, next);
144170
- this.rereadSoon();
144301
+ await this.sendVolume(Math.max(0, Math.min(100, level + delta)));
144171
144302
  }
144172
144303
  registerMediaPlayer() {
144173
144304
  this.ctx.registerNativeCap(mediaPlayerCapability, {
@@ -144176,10 +144307,7 @@ var AlexaEchoDevice = class extends BaseDevice {
144176
144307
  pause: () => this.command("pause"),
144177
144308
  next: () => this.command("next"),
144178
144309
  previous: () => this.command("previous"),
144179
- setVolume: async ({ volumeLevel }) => {
144180
- await this.session().setVolume(this.serial, Math.round(volumeLevel));
144181
- this.rereadSoon();
144182
- },
144310
+ setVolume: async ({ volumeLevel }) => this.sendVolume(Math.round(volumeLevel)),
144183
144311
  stop: async () => unsupported("stop"),
144184
144312
  turnOn: async () => unsupported("power"),
144185
144313
  turnOff: async () => unsupported("power"),
@@ -144199,7 +144327,7 @@ var AlexaEchoDevice = class extends BaseDevice {
144199
144327
  this.ctx.registerNativeCap(doNotDisturbCapability, {
144200
144328
  getStatus: async () => this.runtimeState.getCapState("do-not-disturb") ?? COLD_DND,
144201
144329
  setEnabled: async ({ enabled }) => {
144202
- await this.session().setDnd(this.serial, enabled);
144330
+ await this.session().setDnd(this.serial, enabled, this.id);
144203
144331
  this.runtimeState.setCapState("do-not-disturb", {
144204
144332
  enabled,
144205
144333
  lastChangedAt: Date.now()
@@ -144301,15 +144429,7 @@ function buildAlexaInstances(records, deps) {
144301
144429
  },
144302
144430
  configured: isAlexaBodyConfigured,
144303
144431
  onChanged: deps.onChanged,
144304
- test: async ({ id, values }) => {
144305
- if (["cookie", "refreshToken"].some((key) => {
144306
- const v = values[key];
144307
- return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
144308
- })) return {
144309
- success: true,
144310
- status: "inconclusive",
144311
- error: "Save the account to check the sign-in"
144312
- };
144432
+ test: async ({ id }) => {
144313
144433
  const info = id === null ? null : deps.infoOf(id);
144314
144434
  if (info === null) return {
144315
144435
  success: false,
@@ -144609,6 +144729,9 @@ function scrubSecrets(text, secrets) {
144609
144729
  }
144610
144730
  //#endregion
144611
144731
  //#region src/speak/broker-echo.ts
144732
+ function withDevice(deviceId) {
144733
+ return deviceId === void 0 ? {} : { deviceId };
144734
+ }
144612
144735
  var BrokerEcho = class {
144613
144736
  deps;
144614
144737
  currentState = "starting";
@@ -144754,29 +144877,29 @@ var BrokerEcho = class {
144754
144877
  }
144755
144878
  }
144756
144879
  /** The raw player state of one Echo. Throws on any failure, a 429 included: never an empty answer. */
144757
- async playerState(serial, deviceType) {
144880
+ async playerState(serial, deviceType, deviceId) {
144758
144881
  await this.requireReady();
144759
144882
  return this.limited(async () => parsePlayerState(await this.deps.worker.request({
144760
144883
  op: "player_state",
144761
144884
  serial,
144762
144885
  deviceType
144763
- })));
144886
+ }, withDevice(deviceId))));
144764
144887
  }
144765
- async npCommand(serial, command) {
144888
+ async npCommand(serial, command, deviceId) {
144766
144889
  await this.requireReady();
144767
144890
  await this.limited(() => this.deps.worker.request({
144768
144891
  op: "np_command",
144769
144892
  serial,
144770
144893
  command
144771
- }));
144894
+ }, withDevice(deviceId)));
144772
144895
  }
144773
- async setVolume(serial, level) {
144896
+ async setVolume(serial, level, deviceId) {
144774
144897
  await this.requireReady();
144775
144898
  await this.limited(() => this.deps.worker.request({
144776
144899
  op: "set_volume",
144777
144900
  serial,
144778
144901
  level
144779
- }));
144902
+ }, withDevice(deviceId)));
144780
144903
  }
144781
144904
  /** Do Not Disturb of every device of the account, by serial, in one call. */
144782
144905
  async dndState() {
@@ -144788,13 +144911,13 @@ var BrokerEcho = class {
144788
144911
  await this.requireReady();
144789
144912
  return this.limited(async () => parseDeviceVolumes(await this.deps.worker.request({ op: "device_volumes" })));
144790
144913
  }
144791
- async setDnd(serial, enabled) {
144914
+ async setDnd(serial, enabled, deviceId) {
144792
144915
  await this.requireReady();
144793
144916
  await this.limited(() => this.deps.worker.request({
144794
144917
  op: "set_dnd",
144795
144918
  serial,
144796
144919
  enabled
144797
- }));
144920
+ }, withDevice(deviceId)));
144798
144921
  }
144799
144922
  async requireReady() {
144800
144923
  if (!await this.ensureReady()) throw new Error(this.notReadyReason());
@@ -145380,6 +145503,28 @@ function resolveAlexaWorkerScript(fromModuleUrl = require("url").pathToFileURL(_
145380
145503
  * A crash restarts the process until the same bound CrashSupervisor uses,
145381
145504
  * then requests fail instead of looping.
145382
145505
  */
145506
+ var READ_OPS = new Set([
145507
+ "player_state",
145508
+ "device_volumes",
145509
+ "dnd_state",
145510
+ "devices",
145511
+ "session_status"
145512
+ ]);
145513
+ function coalesceKey(request) {
145514
+ switch (request.op) {
145515
+ case "set_volume":
145516
+ case "player_state": return `${request.op}:${request.serial}`;
145517
+ case "device_volumes":
145518
+ case "dnd_state":
145519
+ case "devices":
145520
+ case "session_status": return request.op;
145521
+ default: return null;
145522
+ }
145523
+ }
145524
+ function defaultDeadlineMs(request) {
145525
+ if (request.op === "set_volume" || request.op === "np_command" || request.op === "set_dnd") return ECHO_COMMAND_DEADLINE_MS;
145526
+ if (READ_OPS.has(request.op) && request.op !== "devices" && request.op !== "session_status") return ECHO_READ_DEADLINE_MS;
145527
+ }
145383
145528
  /** Pure: a new timestamp list, never a mutated one. */
145384
145529
  function recordWorkerCrash(times, now, windowMs, maxCrashes) {
145385
145530
  const cutoff = now - windowMs;
@@ -145464,10 +145609,40 @@ function startAlexaWorker(deps) {
145464
145609
  function scrub(text) {
145465
145610
  return scrubSecrets(text, secrets);
145466
145611
  }
145612
+ function clearTimer(entry) {
145613
+ if (entry.timer !== null) clearTimeout(entry.timer);
145614
+ entry.timer = null;
145615
+ }
145616
+ function timing(entry, outcome) {
145617
+ const end = now();
145618
+ const queuedMs = (entry.sentAt ?? end) - entry.enqueuedAt;
145619
+ const workerMs = entry.sentAt === null ? 0 : end - entry.sentAt;
145620
+ const totalMs = end - entry.enqueuedAt;
145621
+ const extras = {
145622
+ ...entry.deviceId === void 0 ? {} : { tags: { deviceId: entry.deviceId } },
145623
+ meta: {
145624
+ op: entry.request.op,
145625
+ outcome,
145626
+ queuedMs,
145627
+ workerMs,
145628
+ totalMs
145629
+ }
145630
+ };
145631
+ if (totalMs > 5e3) logger.warn("alexa worker request was slow", extras);
145632
+ else logger.debug("alexa worker request", extras);
145633
+ }
145634
+ function settle(entry, outcome, error, result) {
145635
+ clearTimer(entry);
145636
+ timing(entry, outcome);
145637
+ const waiters = entry.waiters;
145638
+ entry.waiters = [];
145639
+ for (const waiter of waiters) if (error === null) waiter.resolve(result);
145640
+ else waiter.reject(error);
145641
+ }
145467
145642
  function failPending(err) {
145468
145643
  const current = pending;
145469
145644
  pending = null;
145470
- current?.reject(err);
145645
+ if (current !== null && !current.abandoned) settle(current, "worker-gone", err);
145471
145646
  }
145472
145647
  function attach(spawned) {
145473
145648
  child = spawned;
@@ -145520,9 +145695,16 @@ function startAlexaWorker(deps) {
145520
145695
  }
145521
145696
  const current = pending;
145522
145697
  pending = null;
145523
- if (message.ok) current.resolve(message.result);
145524
- else if (message.code === "rate-limited") current.reject(new RateLimitedError(scrub(message.error)));
145525
- else current.reject(new Error(scrub(message.error)));
145698
+ if (current.abandoned) logger.debug("alexa worker late reply dropped", {
145699
+ ...current.deviceId === void 0 ? {} : { tags: { deviceId: current.deviceId } },
145700
+ meta: {
145701
+ op: current.request.op,
145702
+ lateMs: now() - current.enqueuedAt
145703
+ }
145704
+ });
145705
+ else if (message.ok) settle(current, "ok", null, message.result);
145706
+ else if (message.code === "rate-limited") settle(current, "rate-limited", new RateLimitedError(scrub(message.error)));
145707
+ else settle(current, "error", new Error(scrub(message.error)));
145526
145708
  pump();
145527
145709
  }
145528
145710
  function onExit(code) {
@@ -145538,7 +145720,7 @@ function startAlexaWorker(deps) {
145538
145720
  failed = true;
145539
145721
  logger.error("alexa worker restart bound reached", { meta: { crashes: crashTimes.length } });
145540
145722
  const waiting = queue.splice(0, queue.length);
145541
- for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
145723
+ for (const item of waiting) settle(item, "worker-gone", /* @__PURE__ */ new Error("alexa worker restart bound reached"));
145542
145724
  return;
145543
145725
  }
145544
145726
  logger.warn("alexa worker exited; restarting", { meta: {
@@ -145559,23 +145741,74 @@ function startAlexaWorker(deps) {
145559
145741
  if (pending !== null || child === null || failed || stopped) return;
145560
145742
  const next = queue.shift();
145561
145743
  if (next === void 0) return;
145562
- pending = next.pending;
145563
- child.stdin.write(requestLine(next.pending.id, next.request));
145744
+ pending = next;
145745
+ next.sentAt = now();
145746
+ child.stdin.write(requestLine(next.id, next.request));
145747
+ }
145748
+ function expire(entry, deadlineMs) {
145749
+ if (pending === entry) {
145750
+ entry.abandoned = true;
145751
+ settle(entry, "timeout-in-flight", /* @__PURE__ */ new Error(`alexa worker call timed out after ${deadlineMs} ms`));
145752
+ return;
145753
+ }
145754
+ const at = queue.indexOf(entry);
145755
+ if (at < 0) return;
145756
+ queue.splice(at, 1);
145757
+ settle(entry, "expired-queued", /* @__PURE__ */ new Error(`alexa worker call not started within ${deadlineMs} ms`));
145564
145758
  }
145565
- function request(body) {
145759
+ function enqueue(entry) {
145760
+ if (entry.isRead) {
145761
+ queue.push(entry);
145762
+ return;
145763
+ }
145764
+ const firstRead = queue.findIndex((queued) => queued.isRead);
145765
+ if (firstRead < 0) queue.push(entry);
145766
+ else queue.splice(firstRead, 0, entry);
145767
+ }
145768
+ function request(body, options) {
145566
145769
  if (failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
145567
145770
  if (stopped) return Promise.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
145568
- const id = String(nextId);
145569
- nextId += 1;
145771
+ const key = coalesceKey(body);
145772
+ const isRead = READ_OPS.has(body.op);
145570
145773
  return new Promise((resolve, reject) => {
145571
- queue.push({
145774
+ const waiter = {
145775
+ resolve,
145776
+ reject
145777
+ };
145778
+ const twin = key === null ? void 0 : queue.find((queued) => queued.key === key);
145779
+ if (twin !== void 0 && isRead) {
145780
+ twin.waiters.push(waiter);
145781
+ return;
145782
+ }
145783
+ const id = String(nextId);
145784
+ nextId += 1;
145785
+ const entry = {
145786
+ id,
145572
145787
  request: body,
145573
- pending: {
145574
- id,
145575
- resolve,
145576
- reject
145577
- }
145578
- });
145788
+ key,
145789
+ isRead,
145790
+ deviceId: options?.deviceId,
145791
+ enqueuedAt: now(),
145792
+ sentAt: null,
145793
+ waiters: [waiter],
145794
+ abandoned: false,
145795
+ timer: null
145796
+ };
145797
+ if (twin !== void 0) {
145798
+ queue.splice(queue.indexOf(twin), 1);
145799
+ clearTimer(twin);
145800
+ entry.waiters = [...twin.waiters, waiter];
145801
+ logger.debug("alexa worker queued call replaced by a newer one", {
145802
+ ...twin.deviceId === void 0 ? {} : { tags: { deviceId: twin.deviceId } },
145803
+ meta: { op: body.op }
145804
+ });
145805
+ }
145806
+ const deadlineMs = options?.deadlineMs ?? defaultDeadlineMs(body);
145807
+ if (deadlineMs !== void 0) {
145808
+ entry.timer = setTimeout(() => expire(entry, deadlineMs), deadlineMs);
145809
+ entry.timer.unref();
145810
+ }
145811
+ enqueue(entry);
145579
145812
  if (child === null) launch();
145580
145813
  pump();
145581
145814
  });
@@ -145592,7 +145825,7 @@ function startAlexaWorker(deps) {
145592
145825
  current?.kill("SIGTERM");
145593
145826
  failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
145594
145827
  const waiting = queue.splice(0, queue.length);
145595
- for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
145828
+ for (const item of waiting) settle(item, "worker-gone", /* @__PURE__ */ new Error("alexa worker stopped"));
145596
145829
  },
145597
145830
  get failed() {
145598
145831
  return failed;
@@ -146571,22 +146804,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
146571
146804
  }
146572
146805
  });
146573
146806
  }
146574
- globalSettingsSchema() {
146575
- return this.schema({ sections: [{
146576
- id: "alexa-account",
146577
- title: "Amazon Alexa account",
146578
- titleKey: "import-alexa:globalSettings.s_alexa-account.title",
146579
- description: "Alexa accounts are managed in the External systems → Brokers tab.",
146580
- descriptionKey: "import-alexa:globalSettings.s_alexa-account.description",
146581
- columns: 1,
146582
- fields: [{
146583
- type: "info",
146584
- key: "accountHelp",
146585
- label: "Accounts are managed separately",
146586
- labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
146587
- content: "This integration links to an Amazon Alexa account broker. Add, edit, or remove accounts from External systems → Brokers, then adopt the imported devices. The integration only stores a reference to its broker. NOTE: this is an unofficial integration that Amazon can change or break without notice."
146588
- }]
146589
- }] });
146807
+ /** The connection form a record carries; the values live on the record, never in `addon-settings`. */
146808
+ async integrationFormSchema(_instance) {
146809
+ return buildConnectionFormSchema();
146590
146810
  }
146591
146811
  async supportsManualCreation() {
146592
146812
  return false;