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