@camstack/addon-import-alexa 0.2.170 → 0.2.172
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 +13 -0
- package/dist/addon.js +140 -88
- package/dist/addon.mjs +140 -88
- package/i18n/en.json +0 -2
- package/i18n/it.json +0 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.172] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Alexa shows an error instead of 'connected' when polling Amazon keeps failing, with the HTTP status in the log.
|
|
15
|
+
- Alexa: quiet vs announce is now decided per notification (a silent notification is spoken quietly, any other announces); the per-target Mode setting is gone and a stored one is ignored.
|
|
16
|
+
|
|
17
|
+
## [0.2.171] - 2026-10-30
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Notification targets pick a broker and list their options inside the add/edit form; the page-wide Find devices dialog is gone
|
|
22
|
+
|
|
10
23
|
## [0.2.170] - 2026-10-30
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -57379,6 +57379,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
57379
57379
|
instanceId: string(),
|
|
57380
57380
|
cameraStableId: string(),
|
|
57381
57381
|
nodeId: string(),
|
|
57382
|
+
/**
|
|
57383
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57384
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57385
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57386
|
+
*/
|
|
57387
|
+
nodeInCluster: boolean(),
|
|
57382
57388
|
profileId: string(),
|
|
57383
57389
|
profileLabel: string(),
|
|
57384
57390
|
name: string(),
|
|
@@ -57428,6 +57434,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
57428
57434
|
auth: "admin"
|
|
57429
57435
|
}), method(object({
|
|
57430
57436
|
instanceId: string().min(1),
|
|
57437
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57438
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
57431
57439
|
name: string().trim().min(1).max(160).optional(),
|
|
57432
57440
|
executable: string().max(1024).optional(),
|
|
57433
57441
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -137140,7 +137148,21 @@ function isRateLimitError(message) {
|
|
|
137140
137148
|
const m = message.toLowerCase();
|
|
137141
137149
|
return m.includes("rate exceeded") || m.includes("throttl") || m.includes("too many requests") || m.includes("429");
|
|
137142
137150
|
}
|
|
137151
|
+
/** HTTP statuses that mean Amazon rejected the session (a re-link is needed). */
|
|
137152
|
+
var SESSION_DEAD_STATUSES = new Set([401, 403]);
|
|
137153
|
+
/** The library logs `No body (code=NNN)` on a body-less non-2xx; it is the only place the status shows. */
|
|
137154
|
+
var NO_BODY_CODE = /No body \(code=(\d{3})\)/;
|
|
137155
|
+
/** A failed Amazon call whose HTTP status is known (null when the library did not say). */
|
|
137156
|
+
var AlexaHttpError = class extends Error {
|
|
137157
|
+
statusCode;
|
|
137158
|
+
constructor(message, statusCode) {
|
|
137159
|
+
super(message);
|
|
137160
|
+
this.name = "AlexaHttpError";
|
|
137161
|
+
this.statusCode = statusCode;
|
|
137162
|
+
}
|
|
137163
|
+
};
|
|
137143
137164
|
var AlexaRemoteClient = class {
|
|
137165
|
+
#lastHttpStatus = null;
|
|
137144
137166
|
#options;
|
|
137145
137167
|
#logger;
|
|
137146
137168
|
#dataDir;
|
|
@@ -137160,7 +137182,10 @@ var AlexaRemoteClient = class {
|
|
|
137160
137182
|
async connect() {
|
|
137161
137183
|
const remote = new import_alexa_remote.default();
|
|
137162
137184
|
this.#remote = remote;
|
|
137163
|
-
const initOptions =
|
|
137185
|
+
const initOptions = {
|
|
137186
|
+
...await this.#buildInitOptions(),
|
|
137187
|
+
logger: (...args) => this.#captureLibraryLine(args)
|
|
137188
|
+
};
|
|
137164
137189
|
remote.on("cookie", () => {
|
|
137165
137190
|
this.#persistSession(remote);
|
|
137166
137191
|
});
|
|
@@ -137254,11 +137279,17 @@ var AlexaRemoteClient = class {
|
|
|
137254
137279
|
async #withBackoff(op, run) {
|
|
137255
137280
|
let lastError = null;
|
|
137256
137281
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) try {
|
|
137282
|
+
this.#lastHttpStatus = null;
|
|
137257
137283
|
return await run();
|
|
137258
137284
|
} catch (err) {
|
|
137259
137285
|
lastError = err;
|
|
137260
137286
|
const message = errMsg(err);
|
|
137261
137287
|
if (isAuthError(message)) throw new AlexaAuthError(message);
|
|
137288
|
+
if (message.toLowerCase().includes("no body")) {
|
|
137289
|
+
const status = this.#lastHttpStatus;
|
|
137290
|
+
if (status !== null && SESSION_DEAD_STATUSES.has(status)) throw new AlexaAuthError(`${message} (HTTP ${status})`);
|
|
137291
|
+
throw new AlexaHttpError(message, status);
|
|
137292
|
+
}
|
|
137262
137293
|
if (!isRateLimitError(message) || attempt === MAX_ATTEMPTS - 1) throw err;
|
|
137263
137294
|
const delay = this.#backoffDelay(attempt);
|
|
137264
137295
|
this.#logger?.warn("AlexaRemoteClient: rate-limited — backing off", { meta: {
|
|
@@ -137271,6 +137302,10 @@ var AlexaRemoteClient = class {
|
|
|
137271
137302
|
}
|
|
137272
137303
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`AlexaRemoteClient: ${op} failed`);
|
|
137273
137304
|
}
|
|
137305
|
+
#captureLibraryLine(args) {
|
|
137306
|
+
const match = NO_BODY_CODE.exec(args.map(String).join(" "));
|
|
137307
|
+
if (match?.[1] !== void 0) this.#lastHttpStatus = Number(match[1]);
|
|
137308
|
+
}
|
|
137274
137309
|
#backoffDelay(attempt) {
|
|
137275
137310
|
return Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_MAX_MS) + Math.floor(Math.random() * BACKOFF_BASE_MS);
|
|
137276
137311
|
}
|
|
@@ -137509,8 +137544,6 @@ var AlexaCommandSink = class {
|
|
|
137509
137544
|
};
|
|
137510
137545
|
/** The single in-process command sink shared across the addon. */
|
|
137511
137546
|
var alexaCommands = new AlexaCommandSink();
|
|
137512
|
-
//#endregion
|
|
137513
|
-
//#region src/alexa-integration-manager.ts
|
|
137514
137547
|
/**
|
|
137515
137548
|
* Wraps exactly one {@link AlexaClient} (= one Amazon account). Authenticates on
|
|
137516
137549
|
* `start()`, lists the appliance inventory, then runs a POLL loop (Alexa has no
|
|
@@ -137538,6 +137571,7 @@ var AlexaIntegrationManager = class {
|
|
|
137538
137571
|
#client = null;
|
|
137539
137572
|
#pollTimer = null;
|
|
137540
137573
|
#status = "disconnected";
|
|
137574
|
+
#pollFailures = 0;
|
|
137541
137575
|
#applianceCount = 0;
|
|
137542
137576
|
#lastCheckedAt = null;
|
|
137543
137577
|
#error = null;
|
|
@@ -137642,14 +137676,37 @@ var AlexaIntegrationManager = class {
|
|
|
137642
137676
|
alexaAccounts.recordStates(this.#id, values);
|
|
137643
137677
|
this.#lastCheckedAt = Date.now();
|
|
137644
137678
|
this.#onStatesPolled(this.#id, values);
|
|
137679
|
+
this.#pollFailures = 0;
|
|
137680
|
+
this.#error = null;
|
|
137681
|
+
if (this.#status === "error" || this.#status === "auth-failed") this.#transition("connected");
|
|
137645
137682
|
} catch (err) {
|
|
137646
137683
|
this.#error = errMsg(err);
|
|
137684
|
+
this.#pollFailures += 1;
|
|
137685
|
+
const httpStatus = err instanceof AlexaHttpError ? err.statusCode : null;
|
|
137647
137686
|
this.#logger.warn("AlexaIntegrationManager: poll failed", {
|
|
137648
137687
|
tags: { brokerId: this.#id },
|
|
137649
|
-
meta: {
|
|
137688
|
+
meta: {
|
|
137689
|
+
error: errMsg(err),
|
|
137690
|
+
consecutiveFailures: this.#pollFailures,
|
|
137691
|
+
httpStatus
|
|
137692
|
+
}
|
|
137650
137693
|
});
|
|
137694
|
+
if (this.#pollFailures >= 3 && this.#status !== "disconnected") this.#transition(err instanceof AlexaAuthError ? "auth-failed" : "error");
|
|
137651
137695
|
}
|
|
137652
137696
|
}
|
|
137697
|
+
#transition(next) {
|
|
137698
|
+
if (this.#status === next) return;
|
|
137699
|
+
const previous = this.#status;
|
|
137700
|
+
this.#status = next;
|
|
137701
|
+
this.#logger.info("AlexaIntegrationManager: status changed", {
|
|
137702
|
+
tags: { brokerId: this.#id },
|
|
137703
|
+
meta: {
|
|
137704
|
+
from: previous,
|
|
137705
|
+
to: next,
|
|
137706
|
+
error: this.#error
|
|
137707
|
+
}
|
|
137708
|
+
});
|
|
137709
|
+
}
|
|
137653
137710
|
/** Stop the poll loop, disconnect the client (FIRST), null it. */
|
|
137654
137711
|
async stop() {
|
|
137655
137712
|
if (this.#pollTimer !== null) {
|
|
@@ -137868,8 +137925,17 @@ var AlexaBrokerRegistry = class {
|
|
|
137868
137925
|
}
|
|
137869
137926
|
};
|
|
137870
137927
|
//#endregion
|
|
137928
|
+
//#region src/speak/constants.ts
|
|
137929
|
+
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
137930
|
+
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
137931
|
+
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
137932
|
+
/** The kind of the Amazon-account broker the Echoes are read from. */
|
|
137933
|
+
var ALEXA_BROKER_KIND = "alexa";
|
|
137934
|
+
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
137935
|
+
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
137936
|
+
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
137937
|
+
//#endregion
|
|
137871
137938
|
//#region src/alexa-broker-provider.ts
|
|
137872
|
-
var ALEXA_KIND = "alexa";
|
|
137873
137939
|
/**
|
|
137874
137940
|
* Construct the `broker` cap provider for the Alexa import addon. Pure builder.
|
|
137875
137941
|
* Mirrors `buildEcowittBrokerProvider`. The cookie + refresh token are secrets:
|
|
@@ -137887,7 +137953,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137887
137953
|
});
|
|
137888
137954
|
return {
|
|
137889
137955
|
list: async ({ kind }) => {
|
|
137890
|
-
if (kind && kind !==
|
|
137956
|
+
if (kind && kind !== "alexa") return [];
|
|
137891
137957
|
return registry.list().map((info) => ({
|
|
137892
137958
|
...info,
|
|
137893
137959
|
addonId: ownerAddonId
|
|
@@ -137903,7 +137969,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137903
137969
|
listProviders: async () => [{
|
|
137904
137970
|
addonId: ownerAddonId,
|
|
137905
137971
|
kinds: [{
|
|
137906
|
-
kind:
|
|
137972
|
+
kind: ALEXA_BROKER_KIND,
|
|
137907
137973
|
label: "Alexa",
|
|
137908
137974
|
signIn: {
|
|
137909
137975
|
basePath: "/addon/import-alexa/login",
|
|
@@ -137912,7 +137978,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137912
137978
|
}]
|
|
137913
137979
|
}],
|
|
137914
137980
|
add: async ({ kind, name, settings }) => {
|
|
137915
|
-
if (kind !==
|
|
137981
|
+
if (kind !== "alexa") throw new Error(`import-alexa: only kind '${ALEXA_BROKER_KIND}' is handled here (got '${kind}')`);
|
|
137916
137982
|
const entry = await registry.createEntry(name, settingsToAlexaConnection(settings));
|
|
137917
137983
|
await persistBrokers([...getBrokers(), entry]);
|
|
137918
137984
|
return { id: entry.id };
|
|
@@ -137970,11 +138036,11 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137970
138036
|
return entry ? redact(entry) : null;
|
|
137971
138037
|
},
|
|
137972
138038
|
getSettingsSchema: async ({ kind }) => {
|
|
137973
|
-
if (kind !==
|
|
138039
|
+
if (kind !== "alexa") return null;
|
|
137974
138040
|
return buildConnectionFormSchema();
|
|
137975
138041
|
},
|
|
137976
138042
|
testSettings: async ({ kind, settings }) => {
|
|
137977
|
-
if (kind !==
|
|
138043
|
+
if (kind !== "alexa") return {
|
|
137978
138044
|
ok: false,
|
|
137979
138045
|
error: `unsupported kind: ${kind}`
|
|
137980
138046
|
};
|
|
@@ -139193,14 +139259,6 @@ var BrokerEcho = class {
|
|
|
139193
139259
|
}
|
|
139194
139260
|
};
|
|
139195
139261
|
//#endregion
|
|
139196
|
-
//#region src/speak/constants.ts
|
|
139197
|
-
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
139198
|
-
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
139199
|
-
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
139200
|
-
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
139201
|
-
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
139202
|
-
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
139203
|
-
//#endregion
|
|
139204
139262
|
//#region src/speak/rate-limit.ts
|
|
139205
139263
|
/**
|
|
139206
139264
|
* Per-Echo admission. A burst is dropped here, not queued: a queue would be
|
|
@@ -139304,9 +139362,16 @@ function readString(config, key) {
|
|
|
139304
139362
|
const value = config[key];
|
|
139305
139363
|
return typeof value === "string" ? value : "";
|
|
139306
139364
|
}
|
|
139307
|
-
/** The
|
|
139308
|
-
|
|
139309
|
-
|
|
139365
|
+
/** The level id an Echo notification uses for "quiet: no chime, no announcement". */
|
|
139366
|
+
var SILENT_LEVEL_ID = "silent";
|
|
139367
|
+
var SILENT_PRIORITY = 1;
|
|
139368
|
+
/**
|
|
139369
|
+
* Quiet or announce is decided PER NOTIFICATION, never per target: a silent
|
|
139370
|
+
* notification (the kind's silent level, or the lowest priority when no level
|
|
139371
|
+
* is named) is spoken quietly, hours-aware; every other one announces.
|
|
139372
|
+
*/
|
|
139373
|
+
function modeFor(notification) {
|
|
139374
|
+
return (notification.level !== void 0 ? notification.level === SILENT_LEVEL_ID : notification.priority <= SILENT_PRIORITY) ? "speak" : "announce";
|
|
139310
139375
|
}
|
|
139311
139376
|
function readVolume(config, delivery) {
|
|
139312
139377
|
const ruled = delivery?.["volume"];
|
|
@@ -139317,21 +139382,9 @@ function readVolume(config, delivery) {
|
|
|
139317
139382
|
/**
|
|
139318
139383
|
* The delivery parameters a rule may set for an Echo target (D938). The
|
|
139319
139384
|
* target's own config holds the default; these override it per notification.
|
|
139385
|
+
* Quiet vs announce is not one of them: it follows the notification's level.
|
|
139320
139386
|
*/
|
|
139321
139387
|
var DELIVERY_PARAMS = [{
|
|
139322
|
-
key: "mode",
|
|
139323
|
-
label: "Mode",
|
|
139324
|
-
labelKey: "import-alexa:echoTarget.s_echo.mode.label",
|
|
139325
|
-
type: "enum",
|
|
139326
|
-
options: [{
|
|
139327
|
-
value: "speak",
|
|
139328
|
-
label: "Speak (quiet hours apply)"
|
|
139329
|
-
}, {
|
|
139330
|
-
value: "announce",
|
|
139331
|
-
label: "Announce (chime, then speech)"
|
|
139332
|
-
}],
|
|
139333
|
-
default: "speak"
|
|
139334
|
-
}, {
|
|
139335
139388
|
key: "volume",
|
|
139336
139389
|
label: "Volume",
|
|
139337
139390
|
labelKey: "import-alexa:echoTarget.s_echo.volume.label",
|
|
@@ -139351,64 +139404,37 @@ function echoOptionLabel(device) {
|
|
|
139351
139404
|
if (kind.length > 0) parts.push(kind);
|
|
139352
139405
|
return parts.join(" · ");
|
|
139353
139406
|
}
|
|
139354
|
-
function echoField(devices, showWhen) {
|
|
139355
|
-
return {
|
|
139356
|
-
type: "select",
|
|
139357
|
-
key: "echoId",
|
|
139358
|
-
label: "Echo",
|
|
139359
|
-
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
139360
|
-
description: devices.length === 0 ? "No Echo found yet. Sign the Amazon account in on Integrations, Alexa." : void 0,
|
|
139361
|
-
descriptionKey: devices.length === 0 ? "import-alexa:echoTarget.s_echo.echoId.empty" : void 0,
|
|
139362
|
-
options: devices.map((device) => ({
|
|
139363
|
-
value: device.id,
|
|
139364
|
-
label: echoOptionLabel(device)
|
|
139365
|
-
})),
|
|
139366
|
-
...showWhen === void 0 ? {} : { showWhen }
|
|
139367
|
-
};
|
|
139368
|
-
}
|
|
139369
139407
|
/**
|
|
139370
139408
|
* The two pickers are not `required`: a required select shows its first option
|
|
139371
139409
|
* while the form still holds no value, so a save would drop the choice. Left
|
|
139372
139410
|
* optional they open on "Select", and `upsertTarget` refuses an empty pick.
|
|
139373
139411
|
*
|
|
139374
|
-
* The
|
|
139375
|
-
* so a new Echo shows up without a deploy.
|
|
139376
|
-
* field; with several, each account's Echoes appear when that account is chosen.
|
|
139412
|
+
* The account is a broker pick (existing, or created in place); the Echo list
|
|
139413
|
+
* is fetched from that account, so a new Echo shows up without a deploy.
|
|
139377
139414
|
*/
|
|
139378
|
-
function configSchema(
|
|
139379
|
-
const first = brokers[0];
|
|
139415
|
+
function configSchema() {
|
|
139380
139416
|
return { sections: [{
|
|
139381
139417
|
id: "echo",
|
|
139382
139418
|
title: "Echo",
|
|
139383
139419
|
titleKey: "import-alexa:echoTarget.s_echo.title",
|
|
139384
139420
|
fields: [
|
|
139385
|
-
...
|
|
139386
|
-
type: "select",
|
|
139421
|
+
...[{
|
|
139422
|
+
type: "broker-select",
|
|
139387
139423
|
key: "brokerId",
|
|
139388
139424
|
label: "Amazon account",
|
|
139389
139425
|
labelKey: "import-alexa:echoTarget.s_echo.brokerId.label",
|
|
139390
|
-
|
|
139391
|
-
|
|
139392
|
-
label: broker.name
|
|
139393
|
-
}))
|
|
139394
|
-
}, ...brokers.map((broker) => echoField(broker.devices, {
|
|
139395
|
-
field: "brokerId",
|
|
139396
|
-
equals: broker.id
|
|
139397
|
-
}))] : [echoField(first?.devices ?? [])],
|
|
139398
|
-
{
|
|
139426
|
+
brokerKind: ALEXA_BROKER_KIND
|
|
139427
|
+
}, {
|
|
139399
139428
|
type: "select",
|
|
139400
|
-
key: "
|
|
139401
|
-
label: "
|
|
139402
|
-
labelKey: "import-alexa:echoTarget.s_echo.
|
|
139403
|
-
|
|
139404
|
-
|
|
139405
|
-
|
|
139406
|
-
|
|
139407
|
-
}
|
|
139408
|
-
|
|
139409
|
-
label: "Announce (chime, then speech)"
|
|
139410
|
-
}]
|
|
139411
|
-
},
|
|
139429
|
+
key: "echoId",
|
|
139430
|
+
label: "Echo",
|
|
139431
|
+
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
139432
|
+
options: [],
|
|
139433
|
+
discoverOptions: {
|
|
139434
|
+
kind: ALEXA_SPEAK_KIND,
|
|
139435
|
+
dependsOn: ["brokerId"]
|
|
139436
|
+
}
|
|
139437
|
+
}],
|
|
139412
139438
|
{
|
|
139413
139439
|
type: "number",
|
|
139414
139440
|
key: "volume",
|
|
@@ -139428,8 +139454,8 @@ function configSchema(brokers) {
|
|
|
139428
139454
|
]
|
|
139429
139455
|
}] };
|
|
139430
139456
|
}
|
|
139431
|
-
/** The Alexa target kind as the Add target form sees it
|
|
139432
|
-
function alexaTargetKind(
|
|
139457
|
+
/** The Alexa target kind as the Add target form sees it. */
|
|
139458
|
+
function alexaTargetKind() {
|
|
139433
139459
|
return {
|
|
139434
139460
|
kind: ALEXA_SPEAK_KIND,
|
|
139435
139461
|
label: "Alexa",
|
|
@@ -139439,7 +139465,7 @@ function alexaTargetKind(brokers) {
|
|
|
139439
139465
|
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
139440
139466
|
supportsDiscovery: true,
|
|
139441
139467
|
destinationKeys: ["brokerId", "echoId"],
|
|
139442
|
-
configSchema: configSchema(
|
|
139468
|
+
configSchema: configSchema(),
|
|
139443
139469
|
deliveryParams: DELIVERY_PARAMS,
|
|
139444
139470
|
caps: {
|
|
139445
139471
|
attachments: {
|
|
@@ -139450,9 +139476,16 @@ function alexaTargetKind(brokers) {
|
|
|
139450
139476
|
actions: 0,
|
|
139451
139477
|
actionIcons: false,
|
|
139452
139478
|
levels: [{
|
|
139479
|
+
id: SILENT_LEVEL_ID,
|
|
139480
|
+
label: "Silent",
|
|
139481
|
+
ordinal: SILENT_PRIORITY,
|
|
139482
|
+
flags: { silent: true },
|
|
139483
|
+
description: "Spoken quietly: no chime, quiet hours apply."
|
|
139484
|
+
}, {
|
|
139453
139485
|
id: "default",
|
|
139454
|
-
label: "
|
|
139455
|
-
ordinal: 3
|
|
139486
|
+
label: "Announce",
|
|
139487
|
+
ordinal: 3,
|
|
139488
|
+
description: "Announced: chime, then speech."
|
|
139456
139489
|
}],
|
|
139457
139490
|
format: ["text"],
|
|
139458
139491
|
clickUrl: false,
|
|
@@ -139469,7 +139502,6 @@ function discoveredFrom(broker, device) {
|
|
|
139469
139502
|
config: {
|
|
139470
139503
|
brokerId: broker.id,
|
|
139471
139504
|
echoId: device.id,
|
|
139472
|
-
mode: "speak",
|
|
139473
139505
|
volume: 40,
|
|
139474
139506
|
respectDnd: true,
|
|
139475
139507
|
family: device.family,
|
|
@@ -139482,6 +139514,7 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139482
139514
|
let targets = ports.loadTargets();
|
|
139483
139515
|
let ledger = emptySpeechLedger();
|
|
139484
139516
|
let deliveries = [];
|
|
139517
|
+
const warnedLegacyMode = /* @__PURE__ */ new Set();
|
|
139485
139518
|
function record(row) {
|
|
139486
139519
|
deliveries = [...deliveries, row];
|
|
139487
139520
|
}
|
|
@@ -139623,7 +139656,14 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139623
139656
|
renderedAs: speech.renderedAs
|
|
139624
139657
|
};
|
|
139625
139658
|
}
|
|
139626
|
-
const mode =
|
|
139659
|
+
const mode = modeFor(notification);
|
|
139660
|
+
if (target.config["mode"] !== void 0 && !warnedLegacyMode.has(target.id)) {
|
|
139661
|
+
warnedLegacyMode.add(target.id);
|
|
139662
|
+
logger.warn("alexa target mode is ignored: quiet vs announce follows each notification", {
|
|
139663
|
+
...tags,
|
|
139664
|
+
meta: { targetId: target.id }
|
|
139665
|
+
});
|
|
139666
|
+
}
|
|
139627
139667
|
try {
|
|
139628
139668
|
const outcomes = await ports.speak({
|
|
139629
139669
|
brokerId: broker.id,
|
|
@@ -139701,7 +139741,7 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139701
139741
|
return deliveries;
|
|
139702
139742
|
},
|
|
139703
139743
|
async listTargetKinds() {
|
|
139704
|
-
return [alexaTargetKind(
|
|
139744
|
+
return [alexaTargetKind()];
|
|
139705
139745
|
},
|
|
139706
139746
|
async listTargets() {
|
|
139707
139747
|
return [...targets];
|
|
@@ -139711,7 +139751,20 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139711
139751
|
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
139712
139752
|
return found;
|
|
139713
139753
|
},
|
|
139714
|
-
async discoverTargets() {
|
|
139754
|
+
async discoverTargets(input) {
|
|
139755
|
+
const named = readString(input.config ?? {}, "brokerId");
|
|
139756
|
+
if (named.length > 0) {
|
|
139757
|
+
const owner = ports.brokers().find((broker) => broker.id === named);
|
|
139758
|
+
return (owner?.devices ?? []).map((device) => ({
|
|
139759
|
+
...discoveredFrom(owner ?? {
|
|
139760
|
+
id: named,
|
|
139761
|
+
name: named,
|
|
139762
|
+
ready: false,
|
|
139763
|
+
devices: []
|
|
139764
|
+
}, device),
|
|
139765
|
+
suggestedName: echoOptionLabel(device)
|
|
139766
|
+
}));
|
|
139767
|
+
}
|
|
139715
139768
|
const taken = new Set(targets.flatMap((row) => {
|
|
139716
139769
|
const owner = brokerFor(row.config);
|
|
139717
139770
|
return owner === void 0 ? [] : [`${owner.id}/${readString(row.config, "echoId")}`];
|
|
@@ -140040,7 +140093,6 @@ function targetsFrom(rows) {
|
|
|
140040
140093
|
config: {
|
|
140041
140094
|
...row.brokerId.length > 0 ? { brokerId: row.brokerId } : {},
|
|
140042
140095
|
echoId: row.echoId,
|
|
140043
|
-
mode: row.mode,
|
|
140044
140096
|
volume: row.volume,
|
|
140045
140097
|
respectDnd: row.respectDnd
|
|
140046
140098
|
}
|
|
@@ -140053,7 +140105,7 @@ function rowsFrom(targets) {
|
|
|
140053
140105
|
enabled: target.enabled,
|
|
140054
140106
|
brokerId: typeof target.config["brokerId"] === "string" ? target.config["brokerId"] : "",
|
|
140055
140107
|
echoId: typeof target.config["echoId"] === "string" ? target.config["echoId"] : "",
|
|
140056
|
-
mode:
|
|
140108
|
+
mode: "speak",
|
|
140057
140109
|
volume: typeof target.config["volume"] === "number" ? target.config["volume"] : 40,
|
|
140058
140110
|
respectDnd: target.config["respectDnd"] !== false
|
|
140059
140111
|
}));
|
package/dist/addon.mjs
CHANGED
|
@@ -57377,6 +57377,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
57377
57377
|
instanceId: string(),
|
|
57378
57378
|
cameraStableId: string(),
|
|
57379
57379
|
nodeId: string(),
|
|
57380
|
+
/**
|
|
57381
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57382
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57383
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57384
|
+
*/
|
|
57385
|
+
nodeInCluster: boolean(),
|
|
57380
57386
|
profileId: string(),
|
|
57381
57387
|
profileLabel: string(),
|
|
57382
57388
|
name: string(),
|
|
@@ -57426,6 +57432,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
57426
57432
|
auth: "admin"
|
|
57427
57433
|
}), method(object({
|
|
57428
57434
|
instanceId: string().min(1),
|
|
57435
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57436
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
57429
57437
|
name: string().trim().min(1).max(160).optional(),
|
|
57430
57438
|
executable: string().max(1024).optional(),
|
|
57431
57439
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -137138,7 +137146,21 @@ function isRateLimitError(message) {
|
|
|
137138
137146
|
const m = message.toLowerCase();
|
|
137139
137147
|
return m.includes("rate exceeded") || m.includes("throttl") || m.includes("too many requests") || m.includes("429");
|
|
137140
137148
|
}
|
|
137149
|
+
/** HTTP statuses that mean Amazon rejected the session (a re-link is needed). */
|
|
137150
|
+
var SESSION_DEAD_STATUSES = new Set([401, 403]);
|
|
137151
|
+
/** The library logs `No body (code=NNN)` on a body-less non-2xx; it is the only place the status shows. */
|
|
137152
|
+
var NO_BODY_CODE = /No body \(code=(\d{3})\)/;
|
|
137153
|
+
/** A failed Amazon call whose HTTP status is known (null when the library did not say). */
|
|
137154
|
+
var AlexaHttpError = class extends Error {
|
|
137155
|
+
statusCode;
|
|
137156
|
+
constructor(message, statusCode) {
|
|
137157
|
+
super(message);
|
|
137158
|
+
this.name = "AlexaHttpError";
|
|
137159
|
+
this.statusCode = statusCode;
|
|
137160
|
+
}
|
|
137161
|
+
};
|
|
137141
137162
|
var AlexaRemoteClient = class {
|
|
137163
|
+
#lastHttpStatus = null;
|
|
137142
137164
|
#options;
|
|
137143
137165
|
#logger;
|
|
137144
137166
|
#dataDir;
|
|
@@ -137158,7 +137180,10 @@ var AlexaRemoteClient = class {
|
|
|
137158
137180
|
async connect() {
|
|
137159
137181
|
const remote = new import_alexa_remote.default();
|
|
137160
137182
|
this.#remote = remote;
|
|
137161
|
-
const initOptions =
|
|
137183
|
+
const initOptions = {
|
|
137184
|
+
...await this.#buildInitOptions(),
|
|
137185
|
+
logger: (...args) => this.#captureLibraryLine(args)
|
|
137186
|
+
};
|
|
137162
137187
|
remote.on("cookie", () => {
|
|
137163
137188
|
this.#persistSession(remote);
|
|
137164
137189
|
});
|
|
@@ -137252,11 +137277,17 @@ var AlexaRemoteClient = class {
|
|
|
137252
137277
|
async #withBackoff(op, run) {
|
|
137253
137278
|
let lastError = null;
|
|
137254
137279
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) try {
|
|
137280
|
+
this.#lastHttpStatus = null;
|
|
137255
137281
|
return await run();
|
|
137256
137282
|
} catch (err) {
|
|
137257
137283
|
lastError = err;
|
|
137258
137284
|
const message = errMsg(err);
|
|
137259
137285
|
if (isAuthError(message)) throw new AlexaAuthError(message);
|
|
137286
|
+
if (message.toLowerCase().includes("no body")) {
|
|
137287
|
+
const status = this.#lastHttpStatus;
|
|
137288
|
+
if (status !== null && SESSION_DEAD_STATUSES.has(status)) throw new AlexaAuthError(`${message} (HTTP ${status})`);
|
|
137289
|
+
throw new AlexaHttpError(message, status);
|
|
137290
|
+
}
|
|
137260
137291
|
if (!isRateLimitError(message) || attempt === MAX_ATTEMPTS - 1) throw err;
|
|
137261
137292
|
const delay = this.#backoffDelay(attempt);
|
|
137262
137293
|
this.#logger?.warn("AlexaRemoteClient: rate-limited — backing off", { meta: {
|
|
@@ -137269,6 +137300,10 @@ var AlexaRemoteClient = class {
|
|
|
137269
137300
|
}
|
|
137270
137301
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`AlexaRemoteClient: ${op} failed`);
|
|
137271
137302
|
}
|
|
137303
|
+
#captureLibraryLine(args) {
|
|
137304
|
+
const match = NO_BODY_CODE.exec(args.map(String).join(" "));
|
|
137305
|
+
if (match?.[1] !== void 0) this.#lastHttpStatus = Number(match[1]);
|
|
137306
|
+
}
|
|
137272
137307
|
#backoffDelay(attempt) {
|
|
137273
137308
|
return Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_MAX_MS) + Math.floor(Math.random() * BACKOFF_BASE_MS);
|
|
137274
137309
|
}
|
|
@@ -137507,8 +137542,6 @@ var AlexaCommandSink = class {
|
|
|
137507
137542
|
};
|
|
137508
137543
|
/** The single in-process command sink shared across the addon. */
|
|
137509
137544
|
var alexaCommands = new AlexaCommandSink();
|
|
137510
|
-
//#endregion
|
|
137511
|
-
//#region src/alexa-integration-manager.ts
|
|
137512
137545
|
/**
|
|
137513
137546
|
* Wraps exactly one {@link AlexaClient} (= one Amazon account). Authenticates on
|
|
137514
137547
|
* `start()`, lists the appliance inventory, then runs a POLL loop (Alexa has no
|
|
@@ -137536,6 +137569,7 @@ var AlexaIntegrationManager = class {
|
|
|
137536
137569
|
#client = null;
|
|
137537
137570
|
#pollTimer = null;
|
|
137538
137571
|
#status = "disconnected";
|
|
137572
|
+
#pollFailures = 0;
|
|
137539
137573
|
#applianceCount = 0;
|
|
137540
137574
|
#lastCheckedAt = null;
|
|
137541
137575
|
#error = null;
|
|
@@ -137640,14 +137674,37 @@ var AlexaIntegrationManager = class {
|
|
|
137640
137674
|
alexaAccounts.recordStates(this.#id, values);
|
|
137641
137675
|
this.#lastCheckedAt = Date.now();
|
|
137642
137676
|
this.#onStatesPolled(this.#id, values);
|
|
137677
|
+
this.#pollFailures = 0;
|
|
137678
|
+
this.#error = null;
|
|
137679
|
+
if (this.#status === "error" || this.#status === "auth-failed") this.#transition("connected");
|
|
137643
137680
|
} catch (err) {
|
|
137644
137681
|
this.#error = errMsg(err);
|
|
137682
|
+
this.#pollFailures += 1;
|
|
137683
|
+
const httpStatus = err instanceof AlexaHttpError ? err.statusCode : null;
|
|
137645
137684
|
this.#logger.warn("AlexaIntegrationManager: poll failed", {
|
|
137646
137685
|
tags: { brokerId: this.#id },
|
|
137647
|
-
meta: {
|
|
137686
|
+
meta: {
|
|
137687
|
+
error: errMsg(err),
|
|
137688
|
+
consecutiveFailures: this.#pollFailures,
|
|
137689
|
+
httpStatus
|
|
137690
|
+
}
|
|
137648
137691
|
});
|
|
137692
|
+
if (this.#pollFailures >= 3 && this.#status !== "disconnected") this.#transition(err instanceof AlexaAuthError ? "auth-failed" : "error");
|
|
137649
137693
|
}
|
|
137650
137694
|
}
|
|
137695
|
+
#transition(next) {
|
|
137696
|
+
if (this.#status === next) return;
|
|
137697
|
+
const previous = this.#status;
|
|
137698
|
+
this.#status = next;
|
|
137699
|
+
this.#logger.info("AlexaIntegrationManager: status changed", {
|
|
137700
|
+
tags: { brokerId: this.#id },
|
|
137701
|
+
meta: {
|
|
137702
|
+
from: previous,
|
|
137703
|
+
to: next,
|
|
137704
|
+
error: this.#error
|
|
137705
|
+
}
|
|
137706
|
+
});
|
|
137707
|
+
}
|
|
137651
137708
|
/** Stop the poll loop, disconnect the client (FIRST), null it. */
|
|
137652
137709
|
async stop() {
|
|
137653
137710
|
if (this.#pollTimer !== null) {
|
|
@@ -137866,8 +137923,17 @@ var AlexaBrokerRegistry = class {
|
|
|
137866
137923
|
}
|
|
137867
137924
|
};
|
|
137868
137925
|
//#endregion
|
|
137926
|
+
//#region src/speak/constants.ts
|
|
137927
|
+
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
137928
|
+
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
137929
|
+
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
137930
|
+
/** The kind of the Amazon-account broker the Echoes are read from. */
|
|
137931
|
+
var ALEXA_BROKER_KIND = "alexa";
|
|
137932
|
+
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
137933
|
+
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
137934
|
+
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
137935
|
+
//#endregion
|
|
137869
137936
|
//#region src/alexa-broker-provider.ts
|
|
137870
|
-
var ALEXA_KIND = "alexa";
|
|
137871
137937
|
/**
|
|
137872
137938
|
* Construct the `broker` cap provider for the Alexa import addon. Pure builder.
|
|
137873
137939
|
* Mirrors `buildEcowittBrokerProvider`. The cookie + refresh token are secrets:
|
|
@@ -137885,7 +137951,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137885
137951
|
});
|
|
137886
137952
|
return {
|
|
137887
137953
|
list: async ({ kind }) => {
|
|
137888
|
-
if (kind && kind !==
|
|
137954
|
+
if (kind && kind !== "alexa") return [];
|
|
137889
137955
|
return registry.list().map((info) => ({
|
|
137890
137956
|
...info,
|
|
137891
137957
|
addonId: ownerAddonId
|
|
@@ -137901,7 +137967,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137901
137967
|
listProviders: async () => [{
|
|
137902
137968
|
addonId: ownerAddonId,
|
|
137903
137969
|
kinds: [{
|
|
137904
|
-
kind:
|
|
137970
|
+
kind: ALEXA_BROKER_KIND,
|
|
137905
137971
|
label: "Alexa",
|
|
137906
137972
|
signIn: {
|
|
137907
137973
|
basePath: "/addon/import-alexa/login",
|
|
@@ -137910,7 +137976,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137910
137976
|
}]
|
|
137911
137977
|
}],
|
|
137912
137978
|
add: async ({ kind, name, settings }) => {
|
|
137913
|
-
if (kind !==
|
|
137979
|
+
if (kind !== "alexa") throw new Error(`import-alexa: only kind '${ALEXA_BROKER_KIND}' is handled here (got '${kind}')`);
|
|
137914
137980
|
const entry = await registry.createEntry(name, settingsToAlexaConnection(settings));
|
|
137915
137981
|
await persistBrokers([...getBrokers(), entry]);
|
|
137916
137982
|
return { id: entry.id };
|
|
@@ -137968,11 +138034,11 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
137968
138034
|
return entry ? redact(entry) : null;
|
|
137969
138035
|
},
|
|
137970
138036
|
getSettingsSchema: async ({ kind }) => {
|
|
137971
|
-
if (kind !==
|
|
138037
|
+
if (kind !== "alexa") return null;
|
|
137972
138038
|
return buildConnectionFormSchema();
|
|
137973
138039
|
},
|
|
137974
138040
|
testSettings: async ({ kind, settings }) => {
|
|
137975
|
-
if (kind !==
|
|
138041
|
+
if (kind !== "alexa") return {
|
|
137976
138042
|
ok: false,
|
|
137977
138043
|
error: `unsupported kind: ${kind}`
|
|
137978
138044
|
};
|
|
@@ -139191,14 +139257,6 @@ var BrokerEcho = class {
|
|
|
139191
139257
|
}
|
|
139192
139258
|
};
|
|
139193
139259
|
//#endregion
|
|
139194
|
-
//#region src/speak/constants.ts
|
|
139195
|
-
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
139196
|
-
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
139197
|
-
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
139198
|
-
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
139199
|
-
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
139200
|
-
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
139201
|
-
//#endregion
|
|
139202
139260
|
//#region src/speak/rate-limit.ts
|
|
139203
139261
|
/**
|
|
139204
139262
|
* Per-Echo admission. A burst is dropped here, not queued: a queue would be
|
|
@@ -139302,9 +139360,16 @@ function readString(config, key) {
|
|
|
139302
139360
|
const value = config[key];
|
|
139303
139361
|
return typeof value === "string" ? value : "";
|
|
139304
139362
|
}
|
|
139305
|
-
/** The
|
|
139306
|
-
|
|
139307
|
-
|
|
139363
|
+
/** The level id an Echo notification uses for "quiet: no chime, no announcement". */
|
|
139364
|
+
var SILENT_LEVEL_ID = "silent";
|
|
139365
|
+
var SILENT_PRIORITY = 1;
|
|
139366
|
+
/**
|
|
139367
|
+
* Quiet or announce is decided PER NOTIFICATION, never per target: a silent
|
|
139368
|
+
* notification (the kind's silent level, or the lowest priority when no level
|
|
139369
|
+
* is named) is spoken quietly, hours-aware; every other one announces.
|
|
139370
|
+
*/
|
|
139371
|
+
function modeFor(notification) {
|
|
139372
|
+
return (notification.level !== void 0 ? notification.level === SILENT_LEVEL_ID : notification.priority <= SILENT_PRIORITY) ? "speak" : "announce";
|
|
139308
139373
|
}
|
|
139309
139374
|
function readVolume(config, delivery) {
|
|
139310
139375
|
const ruled = delivery?.["volume"];
|
|
@@ -139315,21 +139380,9 @@ function readVolume(config, delivery) {
|
|
|
139315
139380
|
/**
|
|
139316
139381
|
* The delivery parameters a rule may set for an Echo target (D938). The
|
|
139317
139382
|
* target's own config holds the default; these override it per notification.
|
|
139383
|
+
* Quiet vs announce is not one of them: it follows the notification's level.
|
|
139318
139384
|
*/
|
|
139319
139385
|
var DELIVERY_PARAMS = [{
|
|
139320
|
-
key: "mode",
|
|
139321
|
-
label: "Mode",
|
|
139322
|
-
labelKey: "import-alexa:echoTarget.s_echo.mode.label",
|
|
139323
|
-
type: "enum",
|
|
139324
|
-
options: [{
|
|
139325
|
-
value: "speak",
|
|
139326
|
-
label: "Speak (quiet hours apply)"
|
|
139327
|
-
}, {
|
|
139328
|
-
value: "announce",
|
|
139329
|
-
label: "Announce (chime, then speech)"
|
|
139330
|
-
}],
|
|
139331
|
-
default: "speak"
|
|
139332
|
-
}, {
|
|
139333
139386
|
key: "volume",
|
|
139334
139387
|
label: "Volume",
|
|
139335
139388
|
labelKey: "import-alexa:echoTarget.s_echo.volume.label",
|
|
@@ -139349,64 +139402,37 @@ function echoOptionLabel(device) {
|
|
|
139349
139402
|
if (kind.length > 0) parts.push(kind);
|
|
139350
139403
|
return parts.join(" · ");
|
|
139351
139404
|
}
|
|
139352
|
-
function echoField(devices, showWhen) {
|
|
139353
|
-
return {
|
|
139354
|
-
type: "select",
|
|
139355
|
-
key: "echoId",
|
|
139356
|
-
label: "Echo",
|
|
139357
|
-
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
139358
|
-
description: devices.length === 0 ? "No Echo found yet. Sign the Amazon account in on Integrations, Alexa." : void 0,
|
|
139359
|
-
descriptionKey: devices.length === 0 ? "import-alexa:echoTarget.s_echo.echoId.empty" : void 0,
|
|
139360
|
-
options: devices.map((device) => ({
|
|
139361
|
-
value: device.id,
|
|
139362
|
-
label: echoOptionLabel(device)
|
|
139363
|
-
})),
|
|
139364
|
-
...showWhen === void 0 ? {} : { showWhen }
|
|
139365
|
-
};
|
|
139366
|
-
}
|
|
139367
139405
|
/**
|
|
139368
139406
|
* The two pickers are not `required`: a required select shows its first option
|
|
139369
139407
|
* while the form still holds no value, so a save would drop the choice. Left
|
|
139370
139408
|
* optional they open on "Select", and `upsertTarget` refuses an empty pick.
|
|
139371
139409
|
*
|
|
139372
|
-
* The
|
|
139373
|
-
* so a new Echo shows up without a deploy.
|
|
139374
|
-
* field; with several, each account's Echoes appear when that account is chosen.
|
|
139410
|
+
* The account is a broker pick (existing, or created in place); the Echo list
|
|
139411
|
+
* is fetched from that account, so a new Echo shows up without a deploy.
|
|
139375
139412
|
*/
|
|
139376
|
-
function configSchema(
|
|
139377
|
-
const first = brokers[0];
|
|
139413
|
+
function configSchema() {
|
|
139378
139414
|
return { sections: [{
|
|
139379
139415
|
id: "echo",
|
|
139380
139416
|
title: "Echo",
|
|
139381
139417
|
titleKey: "import-alexa:echoTarget.s_echo.title",
|
|
139382
139418
|
fields: [
|
|
139383
|
-
...
|
|
139384
|
-
type: "select",
|
|
139419
|
+
...[{
|
|
139420
|
+
type: "broker-select",
|
|
139385
139421
|
key: "brokerId",
|
|
139386
139422
|
label: "Amazon account",
|
|
139387
139423
|
labelKey: "import-alexa:echoTarget.s_echo.brokerId.label",
|
|
139388
|
-
|
|
139389
|
-
|
|
139390
|
-
label: broker.name
|
|
139391
|
-
}))
|
|
139392
|
-
}, ...brokers.map((broker) => echoField(broker.devices, {
|
|
139393
|
-
field: "brokerId",
|
|
139394
|
-
equals: broker.id
|
|
139395
|
-
}))] : [echoField(first?.devices ?? [])],
|
|
139396
|
-
{
|
|
139424
|
+
brokerKind: ALEXA_BROKER_KIND
|
|
139425
|
+
}, {
|
|
139397
139426
|
type: "select",
|
|
139398
|
-
key: "
|
|
139399
|
-
label: "
|
|
139400
|
-
labelKey: "import-alexa:echoTarget.s_echo.
|
|
139401
|
-
|
|
139402
|
-
|
|
139403
|
-
|
|
139404
|
-
|
|
139405
|
-
}
|
|
139406
|
-
|
|
139407
|
-
label: "Announce (chime, then speech)"
|
|
139408
|
-
}]
|
|
139409
|
-
},
|
|
139427
|
+
key: "echoId",
|
|
139428
|
+
label: "Echo",
|
|
139429
|
+
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
139430
|
+
options: [],
|
|
139431
|
+
discoverOptions: {
|
|
139432
|
+
kind: ALEXA_SPEAK_KIND,
|
|
139433
|
+
dependsOn: ["brokerId"]
|
|
139434
|
+
}
|
|
139435
|
+
}],
|
|
139410
139436
|
{
|
|
139411
139437
|
type: "number",
|
|
139412
139438
|
key: "volume",
|
|
@@ -139426,8 +139452,8 @@ function configSchema(brokers) {
|
|
|
139426
139452
|
]
|
|
139427
139453
|
}] };
|
|
139428
139454
|
}
|
|
139429
|
-
/** The Alexa target kind as the Add target form sees it
|
|
139430
|
-
function alexaTargetKind(
|
|
139455
|
+
/** The Alexa target kind as the Add target form sees it. */
|
|
139456
|
+
function alexaTargetKind() {
|
|
139431
139457
|
return {
|
|
139432
139458
|
kind: ALEXA_SPEAK_KIND,
|
|
139433
139459
|
label: "Alexa",
|
|
@@ -139437,7 +139463,7 @@ function alexaTargetKind(brokers) {
|
|
|
139437
139463
|
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
139438
139464
|
supportsDiscovery: true,
|
|
139439
139465
|
destinationKeys: ["brokerId", "echoId"],
|
|
139440
|
-
configSchema: configSchema(
|
|
139466
|
+
configSchema: configSchema(),
|
|
139441
139467
|
deliveryParams: DELIVERY_PARAMS,
|
|
139442
139468
|
caps: {
|
|
139443
139469
|
attachments: {
|
|
@@ -139448,9 +139474,16 @@ function alexaTargetKind(brokers) {
|
|
|
139448
139474
|
actions: 0,
|
|
139449
139475
|
actionIcons: false,
|
|
139450
139476
|
levels: [{
|
|
139477
|
+
id: SILENT_LEVEL_ID,
|
|
139478
|
+
label: "Silent",
|
|
139479
|
+
ordinal: SILENT_PRIORITY,
|
|
139480
|
+
flags: { silent: true },
|
|
139481
|
+
description: "Spoken quietly: no chime, quiet hours apply."
|
|
139482
|
+
}, {
|
|
139451
139483
|
id: "default",
|
|
139452
|
-
label: "
|
|
139453
|
-
ordinal: 3
|
|
139484
|
+
label: "Announce",
|
|
139485
|
+
ordinal: 3,
|
|
139486
|
+
description: "Announced: chime, then speech."
|
|
139454
139487
|
}],
|
|
139455
139488
|
format: ["text"],
|
|
139456
139489
|
clickUrl: false,
|
|
@@ -139467,7 +139500,6 @@ function discoveredFrom(broker, device) {
|
|
|
139467
139500
|
config: {
|
|
139468
139501
|
brokerId: broker.id,
|
|
139469
139502
|
echoId: device.id,
|
|
139470
|
-
mode: "speak",
|
|
139471
139503
|
volume: 40,
|
|
139472
139504
|
respectDnd: true,
|
|
139473
139505
|
family: device.family,
|
|
@@ -139480,6 +139512,7 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139480
139512
|
let targets = ports.loadTargets();
|
|
139481
139513
|
let ledger = emptySpeechLedger();
|
|
139482
139514
|
let deliveries = [];
|
|
139515
|
+
const warnedLegacyMode = /* @__PURE__ */ new Set();
|
|
139483
139516
|
function record(row) {
|
|
139484
139517
|
deliveries = [...deliveries, row];
|
|
139485
139518
|
}
|
|
@@ -139621,7 +139654,14 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139621
139654
|
renderedAs: speech.renderedAs
|
|
139622
139655
|
};
|
|
139623
139656
|
}
|
|
139624
|
-
const mode =
|
|
139657
|
+
const mode = modeFor(notification);
|
|
139658
|
+
if (target.config["mode"] !== void 0 && !warnedLegacyMode.has(target.id)) {
|
|
139659
|
+
warnedLegacyMode.add(target.id);
|
|
139660
|
+
logger.warn("alexa target mode is ignored: quiet vs announce follows each notification", {
|
|
139661
|
+
...tags,
|
|
139662
|
+
meta: { targetId: target.id }
|
|
139663
|
+
});
|
|
139664
|
+
}
|
|
139625
139665
|
try {
|
|
139626
139666
|
const outcomes = await ports.speak({
|
|
139627
139667
|
brokerId: broker.id,
|
|
@@ -139699,7 +139739,7 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139699
139739
|
return deliveries;
|
|
139700
139740
|
},
|
|
139701
139741
|
async listTargetKinds() {
|
|
139702
|
-
return [alexaTargetKind(
|
|
139742
|
+
return [alexaTargetKind()];
|
|
139703
139743
|
},
|
|
139704
139744
|
async listTargets() {
|
|
139705
139745
|
return [...targets];
|
|
@@ -139709,7 +139749,20 @@ function createAlexaNotificationProvider(ports) {
|
|
|
139709
139749
|
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
139710
139750
|
return found;
|
|
139711
139751
|
},
|
|
139712
|
-
async discoverTargets() {
|
|
139752
|
+
async discoverTargets(input) {
|
|
139753
|
+
const named = readString(input.config ?? {}, "brokerId");
|
|
139754
|
+
if (named.length > 0) {
|
|
139755
|
+
const owner = ports.brokers().find((broker) => broker.id === named);
|
|
139756
|
+
return (owner?.devices ?? []).map((device) => ({
|
|
139757
|
+
...discoveredFrom(owner ?? {
|
|
139758
|
+
id: named,
|
|
139759
|
+
name: named,
|
|
139760
|
+
ready: false,
|
|
139761
|
+
devices: []
|
|
139762
|
+
}, device),
|
|
139763
|
+
suggestedName: echoOptionLabel(device)
|
|
139764
|
+
}));
|
|
139765
|
+
}
|
|
139713
139766
|
const taken = new Set(targets.flatMap((row) => {
|
|
139714
139767
|
const owner = brokerFor(row.config);
|
|
139715
139768
|
return owner === void 0 ? [] : [`${owner.id}/${readString(row.config, "echoId")}`];
|
|
@@ -140038,7 +140091,6 @@ function targetsFrom(rows) {
|
|
|
140038
140091
|
config: {
|
|
140039
140092
|
...row.brokerId.length > 0 ? { brokerId: row.brokerId } : {},
|
|
140040
140093
|
echoId: row.echoId,
|
|
140041
|
-
mode: row.mode,
|
|
140042
140094
|
volume: row.volume,
|
|
140043
140095
|
respectDnd: row.respectDnd
|
|
140044
140096
|
}
|
|
@@ -140051,7 +140103,7 @@ function rowsFrom(targets) {
|
|
|
140051
140103
|
enabled: target.enabled,
|
|
140052
140104
|
brokerId: typeof target.config["brokerId"] === "string" ? target.config["brokerId"] : "",
|
|
140053
140105
|
echoId: typeof target.config["echoId"] === "string" ? target.config["echoId"] : "",
|
|
140054
|
-
mode:
|
|
140106
|
+
mode: "speak",
|
|
140055
140107
|
volume: typeof target.config["volume"] === "number" ? target.config["volume"] : 40,
|
|
140056
140108
|
respectDnd: target.config["respectDnd"] !== false
|
|
140057
140109
|
}));
|
package/i18n/en.json
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"echoTarget.s_echo.title": "Echo",
|
|
15
15
|
"echoTarget.s_echo.brokerId.label": "Amazon account",
|
|
16
16
|
"echoTarget.s_echo.echoId.label": "Echo",
|
|
17
|
-
"echoTarget.s_echo.echoId.empty": "No Echo found yet. Sign the Amazon account in on Integrations, Alexa.",
|
|
18
|
-
"echoTarget.s_echo.mode.label": "Mode",
|
|
19
17
|
"echoTarget.s_echo.volume.label": "Volume",
|
|
20
18
|
"echoTarget.s_echo.respectDnd.label": "Respect Do Not Disturb",
|
|
21
19
|
"globalSettings": {
|
package/i18n/it.json
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"echoTarget.s_echo.title": "Echo",
|
|
15
15
|
"echoTarget.s_echo.brokerId.label": "Account Amazon",
|
|
16
16
|
"echoTarget.s_echo.echoId.label": "Echo",
|
|
17
|
-
"echoTarget.s_echo.echoId.empty": "Nessun Echo trovato. Accedi con l'account Amazon da Integrazioni, Alexa.",
|
|
18
|
-
"echoTarget.s_echo.mode.label": "Modalità",
|
|
19
17
|
"echoTarget.s_echo.volume.label": "Volume",
|
|
20
18
|
"echoTarget.s_echo.respectDnd.label": "Rispetta Non disturbare",
|
|
21
19
|
"globalSettings": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-import-alexa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.172",
|
|
4
4
|
"description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|