@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/CHANGELOG.md +8 -0
- package/dist/addon.js +339 -119
- package/dist/addon.mjs +339 -119
- package/i18n/en.json +0 -3
- package/i18n/it.json +0 -3
- package/package.json +2 -2
- package/python/alexa_gateway.py +17 -2
- package/python/alexa_logging.py +33 -0
- package/python/alexa_worker.py +3 -0
- package/python/test_alexa_command_limits.py +80 -0
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
8302
|
+
const cached = this._nodeKeysByCap.get(cacheKey);
|
|
8224
8303
|
if (cached) return cached;
|
|
8225
|
-
const
|
|
8226
|
-
const keys = new Set(
|
|
8227
|
-
this.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
98422
|
-
|
|
98423
|
-
|
|
98424
|
-
|
|
98425
|
-
|
|
98426
|
-
|
|
98427
|
-
|
|
98428
|
-
|
|
98429
|
-
|
|
98430
|
-
|
|
98431
|
-
|
|
98432
|
-
|
|
98433
|
-
|
|
98434
|
-
|
|
98435
|
-
|
|
98436
|
-
|
|
98437
|
-
|
|
98438
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
145524
|
-
|
|
145525
|
-
|
|
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
|
|
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
|
|
145563
|
-
|
|
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
|
|
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
|
|
145569
|
-
|
|
145771
|
+
const key = coalesceKey(body);
|
|
145772
|
+
const isRead = READ_OPS.has(body.op);
|
|
145570
145773
|
return new Promise((resolve, reject) => {
|
|
145571
|
-
|
|
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
|
-
|
|
145574
|
-
|
|
145575
|
-
|
|
145576
|
-
|
|
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
|
|
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
|
-
|
|
146575
|
-
|
|
146576
|
-
|
|
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;
|