@camstack/addon-import-alexa 0.2.148 → 0.2.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MaskShapeCanvas-B6q5RDLi.mjs +13807 -0
- package/dist/MotionZonesSettings-BD57OAIs.mjs +309 -0
- package/dist/PrivacyMaskSettings-ZxdPEelf.mjs +368 -0
- package/dist/SceneMonitorEditor--KqtKX1B.mjs +846 -0
- package/dist/_stub.js +39665 -0
- package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CdRL7bFZ.mjs +156 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.js-BmoaT3cr.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-I8eNxvdp.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-Bct5qZia.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BHqOw8oh.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-B3qxfxBT.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react__loadShare__.js-DwXXYAlk.mjs +70 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CUiVYNMY.mjs +26 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom__loadShare__.js-CIPmAyPi.mjs +25 -0
- package/dist/_virtual_mf___mfe_internal__addon_import_alexa_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-tXm1x5EZ.mjs +25 -0
- package/dist/addon-import-alexa.css +3 -0
- package/dist/addon.js +2211 -160
- package/dist/addon.mjs +2227 -179
- package/dist/hostInit-_k3o3MHN.mjs +129 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +2 -2
- package/dist/player-overlays-CJBC2I_E.mjs +51 -0
- package/dist/remoteEntry.js +2 -0
- package/dist/remoteEntry.ssr.js +33 -0
- package/dist/responsive-N99niSJu.mjs +125 -0
- package/dist/rolldown-runtime-HEgqtunE.mjs +20 -0
- package/dist/scene-monitor-copy-DeuPVYmS.mjs +227 -0
- package/dist/square-CnLcG0Zc.mjs +45 -0
- package/dist/trash-2-BzimUQsl.mjs +25 -0
- package/dist/ui-text-B5YO4hoJ.mjs +34 -0
- package/dist/virtualExposes-lZT06jeg.mjs +2 -0
- package/dist/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_import_alexa_page__remoteEntry_js-CsdMNhUS.mjs +2857 -0
- package/dist/virtual_mf-exposes-ssr___mfe_internal__addon_import_alexa_page__remoteEntry_js-CViESMdd.mjs +10 -0
- package/dist/virtual_mf-exposes___mfe_internal__addon_import_alexa_page__remoteEntry_js-C3qJCjtV.mjs +27 -0
- package/i18n/en.json +6 -1
- package/i18n/it.json +6 -1
- package/package.json +26 -4
- package/python/NOTICE +4 -0
- package/python/alexa_gateway.py +199 -0
- package/python/alexa_protocol.py +110 -0
- package/python/alexa_sequences.py +74 -0
- package/python/alexa_worker.py +50 -0
- package/python/requirements.txt +3 -0
- package/python/test_alexa_protocol.py +168 -0
package/dist/addon.mjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { networkInterfaces, userInfo } from "node:os";
|
|
5
|
+
import * as fs$1 from "node:fs";
|
|
6
|
+
import { existsSync, promises } from "node:fs";
|
|
7
|
+
import * as path$1 from "node:path";
|
|
8
|
+
import path, { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
12
|
+
import "node:zlib";
|
|
6
13
|
//#region \0rolldown/runtime.js
|
|
7
14
|
var __create = Object.create;
|
|
8
15
|
var __defProp = Object.defineProperty;
|
|
@@ -5614,7 +5621,7 @@ var ZodIssueCode = {
|
|
|
5614
5621
|
var ZodFirstPartyTypeKind;
|
|
5615
5622
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5616
5623
|
//#endregion
|
|
5617
|
-
//#region ../types/dist/sleep-
|
|
5624
|
+
//#region ../types/dist/sleep-0sD_LHLI.mjs
|
|
5618
5625
|
/**
|
|
5619
5626
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5620
5627
|
* window to float samples (D455).
|
|
@@ -8852,6 +8859,28 @@ function event(data) {
|
|
|
8852
8859
|
var StaticDirOutputSchema$1 = object({ staticDir: string() });
|
|
8853
8860
|
var VersionOutputSchema$1 = object({ version: string() });
|
|
8854
8861
|
method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
|
|
8862
|
+
/**
|
|
8863
|
+
* Identity — preserves literal types for downstream inference.
|
|
8864
|
+
*
|
|
8865
|
+
* The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
|
|
8866
|
+
* TypeScript does not widen each entry's literal `kind`/`auth` fields to
|
|
8867
|
+
* the broader unions declared on `CustomActionSpec`'s default generics.
|
|
8868
|
+
* Shape validity is enforced separately by the `customAction(...)` helper
|
|
8869
|
+
* whose return type is already a `CustomActionSpec<...>`.
|
|
8870
|
+
*/
|
|
8871
|
+
function defineCustomActions(spec) {
|
|
8872
|
+
return spec;
|
|
8873
|
+
}
|
|
8874
|
+
function customAction(input, output, options) {
|
|
8875
|
+
return {
|
|
8876
|
+
input,
|
|
8877
|
+
output,
|
|
8878
|
+
kind: options?.kind ?? "query",
|
|
8879
|
+
auth: options?.auth ?? "protected",
|
|
8880
|
+
scope: options?.scope ?? { kind: "system" },
|
|
8881
|
+
...options?.caller ? { caller: "required" } : {}
|
|
8882
|
+
};
|
|
8883
|
+
}
|
|
8855
8884
|
var StaticDirOutputSchema = object({ staticDir: string() });
|
|
8856
8885
|
var VersionOutputSchema = object({ version: string() });
|
|
8857
8886
|
method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
|
|
@@ -9017,6 +9046,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9017
9046
|
"login-method",
|
|
9018
9047
|
"media-player",
|
|
9019
9048
|
"mesh-network",
|
|
9049
|
+
"meter",
|
|
9020
9050
|
"metrics-provider",
|
|
9021
9051
|
"model-convert",
|
|
9022
9052
|
"model-distributor",
|
|
@@ -10074,6 +10104,10 @@ Object.freeze({
|
|
|
10074
10104
|
]
|
|
10075
10105
|
}
|
|
10076
10106
|
],
|
|
10107
|
+
"meter": [{
|
|
10108
|
+
"path": "value",
|
|
10109
|
+
"valueType": "number"
|
|
10110
|
+
}],
|
|
10077
10111
|
"motion": [
|
|
10078
10112
|
{
|
|
10079
10113
|
"path": "detected",
|
|
@@ -10817,7 +10851,7 @@ object({
|
|
|
10817
10851
|
params: record(string(), union([string(), number()])).optional()
|
|
10818
10852
|
});
|
|
10819
10853
|
//#endregion
|
|
10820
|
-
//#region ../types/dist/err-msg-
|
|
10854
|
+
//#region ../types/dist/err-msg-DIorGYeC.mjs
|
|
10821
10855
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
10822
10856
|
DeviceType["Camera"] = "camera";
|
|
10823
10857
|
DeviceType["Hub"] = "hub";
|
|
@@ -10911,8 +10945,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
10911
10945
|
DeviceType["PetFeeder"] = "pet-feeder";
|
|
10912
10946
|
/** Built-in dishwasher — program lifecycle, door / remote-start gates,
|
|
10913
10947
|
* salt and rinse-aid levels. Installed with the `dishwasher` cap.
|
|
10914
|
-
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10948
|
+
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10949
|
+
* A plug-built dishwasher carries running-state and power instead. */
|
|
10915
10950
|
DeviceType["Dishwasher"] = "dishwasher";
|
|
10951
|
+
/** Washing machine. A plug-built one is running while its power stays
|
|
10952
|
+
* above a threshold. No program cap — that surface is the dishwasher's. */
|
|
10953
|
+
DeviceType["Washer"] = "washer";
|
|
10954
|
+
/** Tumble dryer. Same plug-built running surface as {@link Washer}. */
|
|
10955
|
+
DeviceType["Dryer"] = "dryer";
|
|
10916
10956
|
return DeviceType;
|
|
10917
10957
|
}({});
|
|
10918
10958
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -11170,7 +11210,7 @@ function errMsg(err) {
|
|
|
11170
11210
|
return String(err);
|
|
11171
11211
|
}
|
|
11172
11212
|
//#endregion
|
|
11173
|
-
//#region ../types/dist/composition-
|
|
11213
|
+
//#region ../types/dist/composition-CpETZ4HL.mjs
|
|
11174
11214
|
/**
|
|
11175
11215
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11176
11216
|
*
|
|
@@ -11926,6 +11966,97 @@ function validateExpressionSource(src) {
|
|
|
11926
11966
|
return null;
|
|
11927
11967
|
}
|
|
11928
11968
|
/**
|
|
11969
|
+
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
11970
|
+
*
|
|
11971
|
+
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
11972
|
+
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
11973
|
+
* a cycle, so running starts only after 30 s above the line.
|
|
11974
|
+
*
|
|
11975
|
+
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
11976
|
+
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
11977
|
+
* report "finished" in the middle of the program, so the stop dwell is
|
|
11978
|
+
* 10 minutes on every type. The number is stored per type so one machine
|
|
11979
|
+
* can move later without moving the others.
|
|
11980
|
+
*/
|
|
11981
|
+
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
11982
|
+
var APPLIANCE_KINDS = [
|
|
11983
|
+
"washer",
|
|
11984
|
+
"dryer",
|
|
11985
|
+
"dishwasher"
|
|
11986
|
+
];
|
|
11987
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
11988
|
+
/**
|
|
11989
|
+
* Statistics and integrals over a history read (D944).
|
|
11990
|
+
*
|
|
11991
|
+
* Pure: the same points always give the same answer, so a restart recomputes
|
|
11992
|
+
* rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
|
|
11993
|
+
* never the mean of the samples. A hole with no held value is not integrated
|
|
11994
|
+
* as zero. A truncated read is not published.
|
|
11995
|
+
*/
|
|
11996
|
+
var STATISTIC_STATS = [
|
|
11997
|
+
"min",
|
|
11998
|
+
"max",
|
|
11999
|
+
"mean",
|
|
12000
|
+
"sum",
|
|
12001
|
+
"count",
|
|
12002
|
+
"change"
|
|
12003
|
+
];
|
|
12004
|
+
/**
|
|
12005
|
+
* The range a statistics or integral helper asks history for (D944).
|
|
12006
|
+
*
|
|
12007
|
+
* Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
|
|
12008
|
+
* or 25 hours across a DST change. A tier is eligible only when its retention
|
|
12009
|
+
* is strictly longer than the longest that range can be. Forever qualifies.
|
|
12010
|
+
* Equal to a tier's retention is a refusal, not a short answer.
|
|
12011
|
+
*/
|
|
12012
|
+
var MINUTE_MS = 6e4;
|
|
12013
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
12014
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
12015
|
+
var ROLLING_SPANS = [
|
|
12016
|
+
"1h",
|
|
12017
|
+
"24h",
|
|
12018
|
+
"2d",
|
|
12019
|
+
"7d",
|
|
12020
|
+
"30d",
|
|
12021
|
+
"custom"
|
|
12022
|
+
];
|
|
12023
|
+
var CALENDAR_PERIODS = [
|
|
12024
|
+
"today",
|
|
12025
|
+
"yesterday",
|
|
12026
|
+
"this-week",
|
|
12027
|
+
"this-month"
|
|
12028
|
+
];
|
|
12029
|
+
var RESET_PERIODS = [
|
|
12030
|
+
"never",
|
|
12031
|
+
"daily",
|
|
12032
|
+
"weekly",
|
|
12033
|
+
"monthly"
|
|
12034
|
+
];
|
|
12035
|
+
5 * MINUTE_MS;
|
|
12036
|
+
24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
|
|
12037
|
+
7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
|
|
12038
|
+
var StatisticWindowSchema = discriminatedUnion("kind", [object({
|
|
12039
|
+
kind: literal("rolling"),
|
|
12040
|
+
span: _enum(ROLLING_SPANS),
|
|
12041
|
+
customMinutes: number().int().min(5).optional()
|
|
12042
|
+
}).check((payload) => {
|
|
12043
|
+
if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
|
|
12044
|
+
code: "custom",
|
|
12045
|
+
message: "a custom range needs a duration in minutes",
|
|
12046
|
+
path: ["customMinutes"],
|
|
12047
|
+
input: payload.value
|
|
12048
|
+
});
|
|
12049
|
+
if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
|
|
12050
|
+
code: "custom",
|
|
12051
|
+
message: "only a custom range carries a duration",
|
|
12052
|
+
path: ["customMinutes"],
|
|
12053
|
+
input: payload.value
|
|
12054
|
+
});
|
|
12055
|
+
}), object({
|
|
12056
|
+
kind: literal("calendar"),
|
|
12057
|
+
period: _enum(CALENDAR_PERIODS)
|
|
12058
|
+
})]);
|
|
12059
|
+
/**
|
|
11929
12060
|
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
11930
12061
|
* id. Split out so helper schemas can name it without a cycle through
|
|
11931
12062
|
* `composition.ts` (D659).
|
|
@@ -11981,14 +12112,37 @@ var HelperMemberSchema = object({
|
|
|
11981
12112
|
cap: string().min(1),
|
|
11982
12113
|
fieldPath: string().min(1)
|
|
11983
12114
|
});
|
|
11984
|
-
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
12115
|
+
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
12116
|
+
object({
|
|
12117
|
+
kind: literal("group"),
|
|
12118
|
+
templateId: GroupTemplateIdSchema,
|
|
12119
|
+
aggregate: AggregateReducerSchema,
|
|
12120
|
+
members: array(HelperMemberSchema).min(1).max(64),
|
|
12121
|
+
/** Actuator groups only. Readings leave this unset. */
|
|
12122
|
+
active: ActiveCriterionSchema.optional()
|
|
12123
|
+
}),
|
|
12124
|
+
object({
|
|
12125
|
+
kind: literal("appliance"),
|
|
12126
|
+
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12127
|
+
appliance: _enum(APPLIANCE_KINDS),
|
|
12128
|
+
thresholdWatts: number().positive(),
|
|
12129
|
+
startDwellMs: number().int().nonnegative(),
|
|
12130
|
+
stopDwellMs: number().int().nonnegative(),
|
|
12131
|
+
member: HelperMemberSchema
|
|
12132
|
+
}),
|
|
12133
|
+
object({
|
|
12134
|
+
kind: literal("statistic"),
|
|
12135
|
+
stat: _enum(STATISTIC_STATS),
|
|
12136
|
+
window: StatisticWindowSchema,
|
|
12137
|
+
source: HelperMemberSchema
|
|
12138
|
+
}),
|
|
12139
|
+
object({
|
|
12140
|
+
kind: literal("integral"),
|
|
12141
|
+
reset: _enum(RESET_PERIODS),
|
|
12142
|
+
output: _enum(["energy", "meter"]),
|
|
12143
|
+
source: HelperMemberSchema
|
|
12144
|
+
})
|
|
12145
|
+
]);
|
|
11992
12146
|
/** A composed device's stableId is this prefix plus the block id. */
|
|
11993
12147
|
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
11994
12148
|
/** Longest snippet a `code` source may carry (slice 4). */
|
|
@@ -12062,6 +12216,26 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
|
12062
12216
|
"omit",
|
|
12063
12217
|
"offline"
|
|
12064
12218
|
])
|
|
12219
|
+
}),
|
|
12220
|
+
object({
|
|
12221
|
+
kind: literal("series"),
|
|
12222
|
+
read: CompositionFieldReadSchema,
|
|
12223
|
+
/** `value` is the statistic or the integral. The others are the gap counts. */
|
|
12224
|
+
publish: _enum([
|
|
12225
|
+
"value",
|
|
12226
|
+
"included",
|
|
12227
|
+
"excluded",
|
|
12228
|
+
"total"
|
|
12229
|
+
]),
|
|
12230
|
+
/**
|
|
12231
|
+
* What an unanswered `value` does. Counts ignore this. `offline` leaves the
|
|
12232
|
+
* device unseeded until history answers, so a restart cannot publish 0.
|
|
12233
|
+
*/
|
|
12234
|
+
empty: _enum([
|
|
12235
|
+
"null",
|
|
12236
|
+
"omit",
|
|
12237
|
+
"offline"
|
|
12238
|
+
])
|
|
12065
12239
|
})
|
|
12066
12240
|
]);
|
|
12067
12241
|
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
@@ -15822,7 +15996,24 @@ var AddonPageDeclarationSchema = object({
|
|
|
15822
15996
|
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
15823
15997
|
sectionLabel: string().optional()
|
|
15824
15998
|
});
|
|
15825
|
-
|
|
15999
|
+
var addonPagesSourceCapability = {
|
|
16000
|
+
name: "addon-pages-source",
|
|
16001
|
+
scope: "system",
|
|
16002
|
+
mode: "collection",
|
|
16003
|
+
internal: true,
|
|
16004
|
+
methods: {
|
|
16005
|
+
/**
|
|
16006
|
+
* `internal: true` did not gate the mount (see the 2026-08-26 note on
|
|
16007
|
+
* `data-store-provider`) — this was reachable on the AppRouter by ANY
|
|
16008
|
+
* authenticated session at the default `auth: 'protected'`. The only
|
|
16009
|
+
* consumer, the `addon-pages-aggregator` builtin, reads it via
|
|
16010
|
+
* `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
|
|
16011
|
+
* only, never tRPC (per `ctx.capabilities` seeing only the local
|
|
16012
|
+
* process). The public, enriched listing admin-ui actually consumes is
|
|
16013
|
+
* the separate `addon-pages` cap, unaffected by this change.
|
|
16014
|
+
*/
|
|
16015
|
+
listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
|
|
16016
|
+
};
|
|
15826
16017
|
var AddonHttpRouteSchema = object({
|
|
15827
16018
|
method: _enum([
|
|
15828
16019
|
"GET",
|
|
@@ -18511,7 +18702,8 @@ var CompositionProblemSchema = object({
|
|
|
18511
18702
|
"native-capability-replace",
|
|
18512
18703
|
"mixed-quantity",
|
|
18513
18704
|
"helper-mismatch",
|
|
18514
|
-
"member-constraint"
|
|
18705
|
+
"member-constraint",
|
|
18706
|
+
"retention"
|
|
18515
18707
|
]),
|
|
18516
18708
|
path: string(),
|
|
18517
18709
|
message: string()
|
|
@@ -18751,7 +18943,7 @@ Object.freeze({
|
|
|
18751
18943
|
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18752
18944
|
* Data, not a form default written into the block.
|
|
18753
18945
|
*/
|
|
18754
|
-
var NOW = {
|
|
18946
|
+
var NOW$1 = {
|
|
18755
18947
|
fieldPath: "lastFetchedAt",
|
|
18756
18948
|
expr: "now"
|
|
18757
18949
|
};
|
|
@@ -18797,7 +18989,7 @@ var GROUP_TEMPLATES = [
|
|
|
18797
18989
|
quantity: "temperature",
|
|
18798
18990
|
defaultAggregate: "mean",
|
|
18799
18991
|
empty: "null",
|
|
18800
|
-
companions: [NOW]
|
|
18992
|
+
companions: [NOW$1]
|
|
18801
18993
|
}),
|
|
18802
18994
|
numeric({
|
|
18803
18995
|
id: "humidity",
|
|
@@ -18808,7 +19000,7 @@ var GROUP_TEMPLATES = [
|
|
|
18808
19000
|
quantity: "humidity",
|
|
18809
19001
|
defaultAggregate: "mean",
|
|
18810
19002
|
empty: "null",
|
|
18811
|
-
companions: [NOW]
|
|
19003
|
+
companions: [NOW$1]
|
|
18812
19004
|
}),
|
|
18813
19005
|
numeric({
|
|
18814
19006
|
id: "pressure",
|
|
@@ -18819,7 +19011,7 @@ var GROUP_TEMPLATES = [
|
|
|
18819
19011
|
quantity: "pressure",
|
|
18820
19012
|
defaultAggregate: "mean",
|
|
18821
19013
|
empty: "null",
|
|
18822
|
-
companions: [NOW]
|
|
19014
|
+
companions: [NOW$1]
|
|
18823
19015
|
}),
|
|
18824
19016
|
numeric({
|
|
18825
19017
|
id: "illuminance",
|
|
@@ -18830,7 +19022,7 @@ var GROUP_TEMPLATES = [
|
|
|
18830
19022
|
quantity: "illuminance",
|
|
18831
19023
|
defaultAggregate: "mean",
|
|
18832
19024
|
empty: "null",
|
|
18833
|
-
companions: [NOW]
|
|
19025
|
+
companions: [NOW$1]
|
|
18834
19026
|
}),
|
|
18835
19027
|
numeric({
|
|
18836
19028
|
id: "power",
|
|
@@ -18841,7 +19033,7 @@ var GROUP_TEMPLATES = [
|
|
|
18841
19033
|
quantity: "power",
|
|
18842
19034
|
defaultAggregate: "sum",
|
|
18843
19035
|
empty: "omit",
|
|
18844
|
-
companions: [NOW]
|
|
19036
|
+
companions: [NOW$1]
|
|
18845
19037
|
}),
|
|
18846
19038
|
numeric({
|
|
18847
19039
|
id: "energy",
|
|
@@ -18852,7 +19044,7 @@ var GROUP_TEMPLATES = [
|
|
|
18852
19044
|
quantity: "energy",
|
|
18853
19045
|
defaultAggregate: "sum",
|
|
18854
19046
|
empty: "omit",
|
|
18855
|
-
companions: [NOW]
|
|
19047
|
+
companions: [NOW$1]
|
|
18856
19048
|
}),
|
|
18857
19049
|
numeric({
|
|
18858
19050
|
id: "battery",
|
|
@@ -18887,7 +19079,7 @@ var GROUP_TEMPLATES = [
|
|
|
18887
19079
|
quantity: "device",
|
|
18888
19080
|
defaultAggregate: "mean",
|
|
18889
19081
|
empty: "null",
|
|
18890
|
-
companions: [NOW]
|
|
19082
|
+
companions: [NOW$1]
|
|
18891
19083
|
}),
|
|
18892
19084
|
binary({
|
|
18893
19085
|
id: "motion",
|
|
@@ -19016,6 +19208,7 @@ function actuator(input) {
|
|
|
19016
19208
|
}
|
|
19017
19209
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
19018
19210
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
19211
|
+
DeviceType.Camera;
|
|
19019
19212
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
19020
19213
|
/**
|
|
19021
19214
|
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
@@ -44581,57 +44774,79 @@ var ProbeTargetConfigResultSchema = object({
|
|
|
44581
44774
|
stateInstance: string().optional(),
|
|
44582
44775
|
error: string().optional()
|
|
44583
44776
|
});
|
|
44584
|
-
|
|
44585
|
-
|
|
44586
|
-
|
|
44587
|
-
|
|
44588
|
-
|
|
44589
|
-
|
|
44590
|
-
|
|
44591
|
-
|
|
44592
|
-
|
|
44593
|
-
|
|
44594
|
-
|
|
44595
|
-
|
|
44596
|
-
|
|
44597
|
-
|
|
44598
|
-
|
|
44599
|
-
|
|
44600
|
-
|
|
44601
|
-
|
|
44602
|
-
|
|
44603
|
-
|
|
44604
|
-
|
|
44605
|
-
|
|
44606
|
-
|
|
44607
|
-
}),
|
|
44608
|
-
|
|
44609
|
-
|
|
44610
|
-
|
|
44611
|
-
|
|
44612
|
-
|
|
44613
|
-
}),
|
|
44614
|
-
|
|
44615
|
-
|
|
44616
|
-
|
|
44617
|
-
|
|
44618
|
-
|
|
44619
|
-
|
|
44620
|
-
|
|
44621
|
-
|
|
44622
|
-
|
|
44623
|
-
|
|
44624
|
-
|
|
44625
|
-
|
|
44626
|
-
|
|
44627
|
-
|
|
44628
|
-
|
|
44629
|
-
|
|
44630
|
-
|
|
44631
|
-
|
|
44632
|
-
|
|
44633
|
-
|
|
44634
|
-
|
|
44777
|
+
var notificationOutputCapability = {
|
|
44778
|
+
name: "notification-output",
|
|
44779
|
+
scope: "system",
|
|
44780
|
+
mode: "collection",
|
|
44781
|
+
methods: {
|
|
44782
|
+
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
44783
|
+
listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
|
|
44784
|
+
/**
|
|
44785
|
+
* One target WITH secrets — admin edit / duplicate hydration only.
|
|
44786
|
+
* `listTargets` stays redacted; the editor loads this so password fields
|
|
44787
|
+
* are filled (masked in the input) and a save can round-trip the token
|
|
44788
|
+
* when a destination changes (Duplicate with a new chatId, etc.).
|
|
44789
|
+
*/
|
|
44790
|
+
getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
|
|
44791
|
+
discoverTargets: method(object({
|
|
44792
|
+
kind: string(),
|
|
44793
|
+
config: record(string(), unknown()).optional(),
|
|
44794
|
+
/**
|
|
44795
|
+
* When editing / duplicating, the blank password field never round-trips
|
|
44796
|
+
* the stored token. Pass the source target id so the provider can restore
|
|
44797
|
+
* secrets server-side before dialling (same merge as upsert).
|
|
44798
|
+
*/
|
|
44799
|
+
targetId: string().optional()
|
|
44800
|
+
}), array(DiscoveredTargetSchema), { auth: "admin" }),
|
|
44801
|
+
probeTargetConfig: method(object({
|
|
44802
|
+
kind: string(),
|
|
44803
|
+
config: record(string(), unknown()),
|
|
44804
|
+
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
44805
|
+
targetId: string().optional()
|
|
44806
|
+
}), ProbeTargetConfigResultSchema, {
|
|
44807
|
+
kind: "mutation",
|
|
44808
|
+
auth: "admin"
|
|
44809
|
+
}),
|
|
44810
|
+
send: method(object({
|
|
44811
|
+
targetId: string(),
|
|
44812
|
+
notification: NotificationSchema
|
|
44813
|
+
}), SendResultSchema, {
|
|
44814
|
+
kind: "mutation",
|
|
44815
|
+
auth: "admin"
|
|
44816
|
+
}),
|
|
44817
|
+
testTarget: method(object({
|
|
44818
|
+
targetId: string(),
|
|
44819
|
+
sample: NotificationSchema.optional()
|
|
44820
|
+
}), TestResultSchema, {
|
|
44821
|
+
kind: "mutation",
|
|
44822
|
+
auth: "admin"
|
|
44823
|
+
}),
|
|
44824
|
+
upsertTarget: method(object({
|
|
44825
|
+
target: TargetSchema,
|
|
44826
|
+
/**
|
|
44827
|
+
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
44828
|
+
* existing row. The UI never holds the stored token; blank / redacted
|
|
44829
|
+
* password fields are restored from this source (D634 still applies if
|
|
44830
|
+
* destinations diverge without a re-typed secret).
|
|
44831
|
+
*/
|
|
44832
|
+
cloneSecretsFrom: string().optional()
|
|
44833
|
+
}), TargetSchema, {
|
|
44834
|
+
kind: "mutation",
|
|
44835
|
+
auth: "admin"
|
|
44836
|
+
}),
|
|
44837
|
+
deleteTarget: method(object({ targetId: string() }), _void(), {
|
|
44838
|
+
kind: "mutation",
|
|
44839
|
+
auth: "admin"
|
|
44840
|
+
}),
|
|
44841
|
+
setTargetEnabled: method(object({
|
|
44842
|
+
targetId: string(),
|
|
44843
|
+
enabled: boolean()
|
|
44844
|
+
}), _void(), {
|
|
44845
|
+
kind: "mutation",
|
|
44846
|
+
auth: "admin"
|
|
44847
|
+
})
|
|
44848
|
+
}
|
|
44849
|
+
};
|
|
44635
44850
|
new Set([
|
|
44636
44851
|
{
|
|
44637
44852
|
id: "person",
|
|
@@ -61881,6 +62096,44 @@ var lockControlCapability = {
|
|
|
61881
62096
|
volatileStateFields: ["lastChangedAt"]
|
|
61882
62097
|
};
|
|
61883
62098
|
/**
|
|
62099
|
+
* A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
|
|
62100
|
+
* so dashboards keep one series; every other instantaneous reading lands here.
|
|
62101
|
+
* `value` is the time integral in the source quantity's accumulated base
|
|
62102
|
+
* (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
|
|
62103
|
+
*/
|
|
62104
|
+
var MeterStatusSchema = object({
|
|
62105
|
+
value: number().nullable().meta(readingMeta("device", { label: "Total" })),
|
|
62106
|
+
lastFetchedAt: number()
|
|
62107
|
+
});
|
|
62108
|
+
var meterCapability = {
|
|
62109
|
+
name: "meter",
|
|
62110
|
+
scope: "device",
|
|
62111
|
+
deviceNative: true,
|
|
62112
|
+
mode: "singleton",
|
|
62113
|
+
deviceTypes: [DeviceType.Sensor],
|
|
62114
|
+
methods: {},
|
|
62115
|
+
status: {
|
|
62116
|
+
schema: MeterStatusSchema,
|
|
62117
|
+
kind: "push"
|
|
62118
|
+
},
|
|
62119
|
+
runtimeState: MeterStatusSchema,
|
|
62120
|
+
/**
|
|
62121
|
+
* Runtime-state durability: **restored** — the total is a cache of the
|
|
62122
|
+
* history integral. A restart shows it only until the history pass
|
|
62123
|
+
* recomputes the open period; it is not an in-memory buffer.
|
|
62124
|
+
*
|
|
62125
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
62126
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
62127
|
+
*/
|
|
62128
|
+
durability: "restored",
|
|
62129
|
+
history: { fields: { value: {
|
|
62130
|
+
kind: "counter",
|
|
62131
|
+
deadband: .001,
|
|
62132
|
+
minIntervalMs: 6e4
|
|
62133
|
+
} } },
|
|
62134
|
+
volatileStateFields: ["lastFetchedAt"]
|
|
62135
|
+
};
|
|
62136
|
+
/**
|
|
61884
62137
|
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
61885
62138
|
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
61886
62139
|
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
@@ -64407,7 +64660,12 @@ var powerMeterCapability = {
|
|
|
64407
64660
|
scope: "device",
|
|
64408
64661
|
deviceNative: true,
|
|
64409
64662
|
mode: "singleton",
|
|
64410
|
-
deviceTypes: [
|
|
64663
|
+
deviceTypes: [
|
|
64664
|
+
DeviceType.Sensor,
|
|
64665
|
+
DeviceType.Washer,
|
|
64666
|
+
DeviceType.Dryer,
|
|
64667
|
+
DeviceType.Dishwasher
|
|
64668
|
+
],
|
|
64411
64669
|
methods: {},
|
|
64412
64670
|
status: {
|
|
64413
64671
|
schema: PowerMeterStatusSchema,
|
|
@@ -65145,6 +65403,8 @@ var runningCustomize = {
|
|
|
65145
65403
|
stateField: "running"
|
|
65146
65404
|
},
|
|
65147
65405
|
deviceTypes: [
|
|
65406
|
+
DeviceType.Washer,
|
|
65407
|
+
DeviceType.Dryer,
|
|
65148
65408
|
DeviceType.Dishwasher,
|
|
65149
65409
|
DeviceType.Vacuum,
|
|
65150
65410
|
DeviceType.LawnMower,
|
|
@@ -69848,7 +70108,7 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
|
|
|
69848
70108
|
auth: "admin"
|
|
69849
70109
|
});
|
|
69850
70110
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
69851
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
70111
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
69852
70112
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
69853
70113
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
69854
70114
|
new Set(CAP_NAMES);
|
|
@@ -70188,6 +70448,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
70188
70448
|
lawnMowerControl: lawnMowerControlCapability,
|
|
70189
70449
|
lockControl: lockControlCapability,
|
|
70190
70450
|
mediaPlayer: mediaPlayerCapability,
|
|
70451
|
+
meter: meterCapability,
|
|
70191
70452
|
motion: motionCapability,
|
|
70192
70453
|
motionTrigger: motionTriggerCapability,
|
|
70193
70454
|
motionZones: motionZonesCapability,
|
|
@@ -71447,7 +71708,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
71447
71708
|
return toDeviceSummary(device, this.addonId);
|
|
71448
71709
|
}
|
|
71449
71710
|
};
|
|
71450
|
-
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
|
|
71711
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
71451
71712
|
new Set(Object.values(DeviceType));
|
|
71452
71713
|
Object.freeze({
|
|
71453
71714
|
"accessories.getStatus": {
|
|
@@ -75636,6 +75897,13 @@ Object.freeze({
|
|
|
75636
75897
|
access: "create",
|
|
75637
75898
|
role: "admin"
|
|
75638
75899
|
},
|
|
75900
|
+
"meter.getStatus": {
|
|
75901
|
+
capName: "meter",
|
|
75902
|
+
capScope: "device",
|
|
75903
|
+
addonId: null,
|
|
75904
|
+
access: "view",
|
|
75905
|
+
role: "viewer"
|
|
75906
|
+
},
|
|
75639
75907
|
"metricsProvider.collectSnapshot": {
|
|
75640
75908
|
capName: "metrics-provider",
|
|
75641
75909
|
capScope: "system",
|
|
@@ -83734,7 +84002,39 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
83734
84002
|
phrase: t("When {device} finishes", "Quando {device} finisce"),
|
|
83735
84003
|
icon: "square"
|
|
83736
84004
|
}
|
|
83737
|
-
}
|
|
84005
|
+
},
|
|
84006
|
+
overrides: { byType: {
|
|
84007
|
+
[DeviceType.Washer]: {
|
|
84008
|
+
state: { running: {
|
|
84009
|
+
whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
|
|
84010
|
+
whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
|
|
84011
|
+
} },
|
|
84012
|
+
events: {
|
|
84013
|
+
onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
|
|
84014
|
+
onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
|
|
84015
|
+
}
|
|
84016
|
+
},
|
|
84017
|
+
[DeviceType.Dryer]: {
|
|
84018
|
+
state: { running: {
|
|
84019
|
+
whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
|
|
84020
|
+
whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
|
|
84021
|
+
} },
|
|
84022
|
+
events: {
|
|
84023
|
+
onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
|
|
84024
|
+
onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
|
|
84025
|
+
}
|
|
84026
|
+
},
|
|
84027
|
+
[DeviceType.Dishwasher]: {
|
|
84028
|
+
state: { running: {
|
|
84029
|
+
whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
|
|
84030
|
+
whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
|
|
84031
|
+
} },
|
|
84032
|
+
events: {
|
|
84033
|
+
onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
|
|
84034
|
+
onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
|
|
84035
|
+
}
|
|
84036
|
+
}
|
|
84037
|
+
} }
|
|
83738
84038
|
},
|
|
83739
84039
|
update: {
|
|
83740
84040
|
scope: "device",
|
|
@@ -84562,6 +84862,17 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
84562
84862
|
}],
|
|
84563
84863
|
events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
|
|
84564
84864
|
},
|
|
84865
|
+
meter: {
|
|
84866
|
+
scope: "device",
|
|
84867
|
+
noun: t("Meter", "Contatore"),
|
|
84868
|
+
state: [{
|
|
84869
|
+
kind: "numeric",
|
|
84870
|
+
field: "value",
|
|
84871
|
+
quantity: t("the total", "il totale"),
|
|
84872
|
+
label: t("Total", "Totale"),
|
|
84873
|
+
icon: "gauge"
|
|
84874
|
+
}]
|
|
84875
|
+
},
|
|
84565
84876
|
"numeric-sensor": {
|
|
84566
84877
|
scope: "device",
|
|
84567
84878
|
noun: t("Sensor", "Sensore"),
|
|
@@ -85034,8 +85345,47 @@ function projectAction(cap, action) {
|
|
|
85034
85345
|
};
|
|
85035
85346
|
}
|
|
85036
85347
|
Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
|
|
85037
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
|
|
85348
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
85038
85349
|
DeviceFeature.BatteryOperated;
|
|
85350
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
85351
|
+
var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
|
|
85352
|
+
var HOMEKIT_REASON = "HomeKit has no laundry-appliance service. The export tab would pair an accessory with nothing on it: running-state is withheld until a null reading can be unavailable, and power is not a HomeKit service this export honours.";
|
|
85353
|
+
var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
|
|
85354
|
+
var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
|
|
85355
|
+
var MATTER_REASON = "No Matter export is built. The closest device types are Laundry Washer (0x0073) and Laundry Dryer (0x0074); a power threshold is not the Laundry Washer Mode cluster.";
|
|
85356
|
+
function withheld(reason) {
|
|
85357
|
+
return {
|
|
85358
|
+
[DeviceType.Washer]: {
|
|
85359
|
+
exported: false,
|
|
85360
|
+
reason
|
|
85361
|
+
},
|
|
85362
|
+
[DeviceType.Dryer]: {
|
|
85363
|
+
exported: false,
|
|
85364
|
+
reason
|
|
85365
|
+
},
|
|
85366
|
+
[DeviceType.Dishwasher]: {
|
|
85367
|
+
exported: false,
|
|
85368
|
+
reason
|
|
85369
|
+
}
|
|
85370
|
+
};
|
|
85371
|
+
}
|
|
85372
|
+
function offered(received) {
|
|
85373
|
+
return {
|
|
85374
|
+
[DeviceType.Washer]: {
|
|
85375
|
+
exported: true,
|
|
85376
|
+
as: received
|
|
85377
|
+
},
|
|
85378
|
+
[DeviceType.Dryer]: {
|
|
85379
|
+
exported: true,
|
|
85380
|
+
as: received
|
|
85381
|
+
},
|
|
85382
|
+
[DeviceType.Dishwasher]: {
|
|
85383
|
+
exported: true,
|
|
85384
|
+
as: received
|
|
85385
|
+
}
|
|
85386
|
+
};
|
|
85387
|
+
}
|
|
85388
|
+
offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
|
|
85039
85389
|
Object.freeze({
|
|
85040
85390
|
"accessories.getStatus": [{
|
|
85041
85391
|
name: "deviceId",
|
|
@@ -86212,6 +86562,11 @@ Object.freeze({
|
|
|
86212
86562
|
form: "single",
|
|
86213
86563
|
optional: false
|
|
86214
86564
|
}],
|
|
86565
|
+
"meter.getStatus": [{
|
|
86566
|
+
name: "deviceId",
|
|
86567
|
+
form: "single",
|
|
86568
|
+
optional: false
|
|
86569
|
+
}],
|
|
86215
86570
|
"motion.getStatus": [{
|
|
86216
86571
|
name: "deviceId",
|
|
86217
86572
|
form: "single",
|
|
@@ -87819,7 +88174,6 @@ Object.freeze({
|
|
|
87819
88174
|
"network-access": "ingress",
|
|
87820
88175
|
"smtp-provider": "email"
|
|
87821
88176
|
});
|
|
87822
|
-
DeviceType.Camera;
|
|
87823
88177
|
/**
|
|
87824
88178
|
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
87825
88179
|
* `z.infer`, they do not restate the shape.
|
|
@@ -87965,6 +88319,213 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
87965
88319
|
samplingSeconds: 10
|
|
87966
88320
|
};
|
|
87967
88321
|
new Set(["devices", "classes"]);
|
|
88322
|
+
/** Strip a small, safe subset of Markdown down to plain text. */
|
|
88323
|
+
function markdownToText(md) {
|
|
88324
|
+
return md.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]*)`/g, "$1").replace(/^\s{0,3}#{1,6}\s*/gm, "").replace(/^\s{0,3}>\s?/gm, "").replace(/^\s{0,3}[-*+]\s+/gm, "").replace(/^\s{0,3}\d+\.\s+/gm, "").trim();
|
|
88325
|
+
}
|
|
88326
|
+
var HTML_ENTITIES = [
|
|
88327
|
+
[/ /g, " "],
|
|
88328
|
+
[/&/g, "&"],
|
|
88329
|
+
[/</g, "<"],
|
|
88330
|
+
[/>/g, ">"],
|
|
88331
|
+
[/"/g, "\""],
|
|
88332
|
+
[/'/g, "'"]
|
|
88333
|
+
];
|
|
88334
|
+
/** Strip HTML tags, decode a few entities, map block/br elements to newlines. */
|
|
88335
|
+
function htmlToText(html) {
|
|
88336
|
+
let out = html.replace(/<\s*br\s*\/?\s*>/gi, "\n").replace(/<\s*\/\s*(p|div|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]+>/g, "");
|
|
88337
|
+
for (const [re, rep] of HTML_ENTITIES) out = out.replace(re, rep);
|
|
88338
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
88339
|
+
}
|
|
88340
|
+
var HTML_ESCAPES = [
|
|
88341
|
+
[/&/g, "&"],
|
|
88342
|
+
[/</g, "<"],
|
|
88343
|
+
[/>/g, ">"],
|
|
88344
|
+
[/"/g, """],
|
|
88345
|
+
[/'/g, "'"]
|
|
88346
|
+
];
|
|
88347
|
+
function escapeHtml(text) {
|
|
88348
|
+
let out = text;
|
|
88349
|
+
for (const [re, rep] of HTML_ESCAPES) out = out.replace(re, rep);
|
|
88350
|
+
return out;
|
|
88351
|
+
}
|
|
88352
|
+
/** Plain text → HTML-lite (escape + newlines to <br>). */
|
|
88353
|
+
function textToHtml(text) {
|
|
88354
|
+
return escapeHtml(text).replace(/\n/g, "<br>");
|
|
88355
|
+
}
|
|
88356
|
+
/** Markdown → HTML-lite (bold/italic/code/links; used e.g. for Pushover). */
|
|
88357
|
+
function markdownToHtmlLite(md) {
|
|
88358
|
+
return escapeHtml(md).replace(/(\*\*|__)(.*?)\1/g, "<b>$2</b>").replace(/(\*|_)(.*?)\1/g, "<i>$2</i>").replace(/`([^`]*)`/g, "<code>$1</code>").replace(/\[([^\]]*)\]\(([^)]*)\)/g, "<a href=\"$2\">$1</a>").replace(/\n/g, "<br>");
|
|
88359
|
+
}
|
|
88360
|
+
/**
|
|
88361
|
+
* Preference order when the source format is NOT supported — first supported
|
|
88362
|
+
* entry wins. Encodes the Apprise-style "degrade DOWN, but preserve rendering
|
|
88363
|
+
* where a richer target exists" policy:
|
|
88364
|
+
* - markdown → html (html-lite) before text (strip)
|
|
88365
|
+
* - html → text before markdown
|
|
88366
|
+
* - text → markdown before html (text is valid markdown)
|
|
88367
|
+
*/
|
|
88368
|
+
var DEGRADE_ORDER = {
|
|
88369
|
+
text: [
|
|
88370
|
+
"text",
|
|
88371
|
+
"markdown",
|
|
88372
|
+
"html"
|
|
88373
|
+
],
|
|
88374
|
+
markdown: [
|
|
88375
|
+
"markdown",
|
|
88376
|
+
"html",
|
|
88377
|
+
"text"
|
|
88378
|
+
],
|
|
88379
|
+
html: [
|
|
88380
|
+
"html",
|
|
88381
|
+
"text",
|
|
88382
|
+
"markdown"
|
|
88383
|
+
]
|
|
88384
|
+
};
|
|
88385
|
+
/**
|
|
88386
|
+
* Pick the best target format the kind supports for a given source format.
|
|
88387
|
+
*
|
|
88388
|
+
* TOLERANT of a source outside the union. `NotificationSchema.format` carries
|
|
88389
|
+
* `.default('text')`, but that default only runs where the schema is PARSED —
|
|
88390
|
+
* the tRPC router. A cap call arriving from another addon (`ctx.api`) skips
|
|
88391
|
+
* that parse, so `format` reaches here `undefined`, and the bare
|
|
88392
|
+
* `DEGRADE_ORDER[source]` threw `is not iterable`. That broke the one promise
|
|
88393
|
+
* the degrade path makes ("never throws — degradation is reported, never
|
|
88394
|
+
* surfaced as an error") and dead-lettered every Notification-Center delivery
|
|
88395
|
+
* for a day (2026-07-30). An unknown source degrades like `text`.
|
|
88396
|
+
*/
|
|
88397
|
+
function resolveFormat(source, supported) {
|
|
88398
|
+
for (const candidate of DEGRADE_ORDER[source] ?? DEGRADE_ORDER.text) if (supported.includes(candidate)) return candidate;
|
|
88399
|
+
return supported[0] ?? "text";
|
|
88400
|
+
}
|
|
88401
|
+
/** Transcode a body between formats. */
|
|
88402
|
+
function transcodeBody(body, from, to) {
|
|
88403
|
+
if (from === to) return body;
|
|
88404
|
+
if (to === "text") {
|
|
88405
|
+
if (from === "markdown") return markdownToText(body);
|
|
88406
|
+
if (from === "html") return htmlToText(body);
|
|
88407
|
+
}
|
|
88408
|
+
if (to === "html") {
|
|
88409
|
+
if (from === "text") return textToHtml(body);
|
|
88410
|
+
if (from === "markdown") return markdownToHtmlLite(body);
|
|
88411
|
+
}
|
|
88412
|
+
if (to === "markdown") {
|
|
88413
|
+
if (from === "text") return body;
|
|
88414
|
+
if (from === "html") return htmlToText(body);
|
|
88415
|
+
}
|
|
88416
|
+
return body;
|
|
88417
|
+
}
|
|
88418
|
+
function toResolvedLevel(level) {
|
|
88419
|
+
return {
|
|
88420
|
+
id: level.id,
|
|
88421
|
+
ordinal: level.ordinal,
|
|
88422
|
+
...level.flags ? { flags: level.flags } : {},
|
|
88423
|
+
...level.requires ? { requires: level.requires } : {}
|
|
88424
|
+
};
|
|
88425
|
+
}
|
|
88426
|
+
function resolveLevel(levels, n) {
|
|
88427
|
+
if (levels.length === 0) return null;
|
|
88428
|
+
if (n.level !== void 0) {
|
|
88429
|
+
const explicit = levels.find((l) => l.id === n.level);
|
|
88430
|
+
if (explicit) return toResolvedLevel(explicit);
|
|
88431
|
+
}
|
|
88432
|
+
const withOrdinal = levels.filter((l) => l.ordinal !== null);
|
|
88433
|
+
if (withOrdinal.length === 0) return toResolvedLevel(levels[0]);
|
|
88434
|
+
const priority = n.priority;
|
|
88435
|
+
return toResolvedLevel([...withOrdinal].sort((a, b) => {
|
|
88436
|
+
const da = Math.abs((a.ordinal ?? 0) - priority);
|
|
88437
|
+
const db = Math.abs((b.ordinal ?? 0) - priority);
|
|
88438
|
+
if (da !== db) return da - db;
|
|
88439
|
+
return (b.ordinal ?? 0) - (a.ordinal ?? 0);
|
|
88440
|
+
})[0]);
|
|
88441
|
+
}
|
|
88442
|
+
function splitBody(body, maxLen) {
|
|
88443
|
+
if (!Number.isFinite(maxLen) || maxLen <= 0 || body.length <= maxLen) return [body];
|
|
88444
|
+
const parts = [];
|
|
88445
|
+
for (let i = 0; i < body.length; i += maxLen) parts.push(body.slice(i, i + maxLen));
|
|
88446
|
+
return parts;
|
|
88447
|
+
}
|
|
88448
|
+
/**
|
|
88449
|
+
* Prepare a canonical notification for one kind. Never throws — degradation is
|
|
88450
|
+
* always reported in `renderedAs`, never surfaced as an error.
|
|
88451
|
+
*/
|
|
88452
|
+
function prepareNotification(caps, n) {
|
|
88453
|
+
const dropped = [];
|
|
88454
|
+
const sourceFormat = n.format;
|
|
88455
|
+
const format = resolveFormat(sourceFormat, caps.format);
|
|
88456
|
+
if (format !== sourceFormat) dropped.push("format");
|
|
88457
|
+
const transcoded = transcodeBody(n.body, sourceFormat, format);
|
|
88458
|
+
const level = resolveLevel(caps.levels, n);
|
|
88459
|
+
const inAttachments = n.attachments ?? [];
|
|
88460
|
+
const attachments = [];
|
|
88461
|
+
for (const att of inAttachments) {
|
|
88462
|
+
if (!caps.attachments.mediaTypes.includes(att.mediaType)) {
|
|
88463
|
+
dropped.push(`attachment:${att.mediaType}`);
|
|
88464
|
+
continue;
|
|
88465
|
+
}
|
|
88466
|
+
if (caps.attachments.mode === "url" && att.url === void 0) {
|
|
88467
|
+
dropped.push("attachment:noUrl");
|
|
88468
|
+
continue;
|
|
88469
|
+
}
|
|
88470
|
+
if (att.bytes !== void 0 && caps.attachments.maxBytes !== void 0 && att.bytes.length > caps.attachments.maxBytes) {
|
|
88471
|
+
dropped.push("attachment:maxBytes");
|
|
88472
|
+
continue;
|
|
88473
|
+
}
|
|
88474
|
+
if (attachments.length >= caps.attachments.max) {
|
|
88475
|
+
dropped.push("attachment:max");
|
|
88476
|
+
continue;
|
|
88477
|
+
}
|
|
88478
|
+
const needsFetch = caps.attachments.mode === "bytes" && att.bytes === void 0 && att.url !== void 0;
|
|
88479
|
+
attachments.push({
|
|
88480
|
+
mediaType: att.mediaType,
|
|
88481
|
+
...att.url !== void 0 ? { url: att.url } : {},
|
|
88482
|
+
...att.bytes !== void 0 ? { bytes: att.bytes } : {},
|
|
88483
|
+
...att.mime !== void 0 ? { mime: att.mime } : {},
|
|
88484
|
+
...att.name !== void 0 ? { name: att.name } : {},
|
|
88485
|
+
needsFetch
|
|
88486
|
+
});
|
|
88487
|
+
}
|
|
88488
|
+
const inActions = n.actions ?? [];
|
|
88489
|
+
const kept = inActions.slice(0, Math.max(0, caps.actions));
|
|
88490
|
+
if (inActions.length > kept.length) dropped.push("actions");
|
|
88491
|
+
const iconsSupported = caps.actionIcons === true;
|
|
88492
|
+
if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
|
|
88493
|
+
const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
|
|
88494
|
+
let clickUrl = null;
|
|
88495
|
+
if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
|
|
88496
|
+
else dropped.push("clickUrl");
|
|
88497
|
+
let sound = null;
|
|
88498
|
+
if (n.sound !== void 0) if (caps.sound) sound = n.sound;
|
|
88499
|
+
else dropped.push("sound");
|
|
88500
|
+
let ttl = null;
|
|
88501
|
+
if (n.ttl !== void 0) if (caps.ttl) ttl = n.ttl;
|
|
88502
|
+
else dropped.push("ttl");
|
|
88503
|
+
const tag = n.tag ?? null;
|
|
88504
|
+
const bodyParts = splitBody(transcoded, caps.bodyMaxLen);
|
|
88505
|
+
const truncated = bodyParts.length > 1;
|
|
88506
|
+
const renderedAs = {
|
|
88507
|
+
level: level?.id ?? "",
|
|
88508
|
+
format,
|
|
88509
|
+
attachmentsSent: attachments.length,
|
|
88510
|
+
actionsSent: actions.length,
|
|
88511
|
+
truncated,
|
|
88512
|
+
dropped
|
|
88513
|
+
};
|
|
88514
|
+
return {
|
|
88515
|
+
body: bodyParts[0] ?? "",
|
|
88516
|
+
...n.title !== void 0 ? { title: n.title } : {},
|
|
88517
|
+
format,
|
|
88518
|
+
level,
|
|
88519
|
+
attachments,
|
|
88520
|
+
actions,
|
|
88521
|
+
clickUrl,
|
|
88522
|
+
sound,
|
|
88523
|
+
ttl,
|
|
88524
|
+
tag,
|
|
88525
|
+
bodyParts,
|
|
88526
|
+
renderedAs
|
|
88527
|
+
};
|
|
88528
|
+
}
|
|
87968
88529
|
new Set(NC_LEGACY_SYSTEM_EVENT_KINDS);
|
|
87969
88530
|
/**
|
|
87970
88531
|
* Every filter, for the callers that must cover all of them (guards, tests).
|
|
@@ -89186,12 +89747,55 @@ var alexaProxyLoginSchema = object({
|
|
|
89186
89747
|
proxyHost: string().default("").describe("Host/IP the browser uses to reach the login proxy (blank = auto-detect)"),
|
|
89187
89748
|
proxyPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(3456)).describe("Port the local Amazon-login proxy listens on (must be reachable)")
|
|
89188
89749
|
});
|
|
89750
|
+
/** Amazon account used to speak on Echo devices. The session blob is the cookie jar. */
|
|
89751
|
+
var alexaSpeakSettingsSchema = object({
|
|
89752
|
+
email: string().default(""),
|
|
89753
|
+
password: string().default("").describe("Amazon password").meta({ secret: true }),
|
|
89754
|
+
region: _enum([
|
|
89755
|
+
"NA",
|
|
89756
|
+
"EU",
|
|
89757
|
+
"FE"
|
|
89758
|
+
]).default("EU"),
|
|
89759
|
+
session: string().default("").describe("Alexa session blob").meta({ secret: true }),
|
|
89760
|
+
targets: array(object({
|
|
89761
|
+
id: string(),
|
|
89762
|
+
name: string(),
|
|
89763
|
+
enabled: boolean(),
|
|
89764
|
+
echoId: string(),
|
|
89765
|
+
mode: _enum(["speak", "announce"]),
|
|
89766
|
+
volume: number(),
|
|
89767
|
+
respectDnd: boolean()
|
|
89768
|
+
})).default([])
|
|
89769
|
+
});
|
|
89770
|
+
function isRecord$2(value) {
|
|
89771
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89772
|
+
}
|
|
89773
|
+
/** Resolved settings. A keychain marker (an object) is an empty secret, not a password. */
|
|
89774
|
+
function readSpeakSettings(config) {
|
|
89775
|
+
const raw = isRecord$2(config.speak) ? config.speak : {};
|
|
89776
|
+
const region = raw["region"];
|
|
89777
|
+
const targets = raw["targets"];
|
|
89778
|
+
return alexaSpeakSettingsSchema.parse({
|
|
89779
|
+
email: typeof raw["email"] === "string" ? raw["email"] : "",
|
|
89780
|
+
password: typeof raw["password"] === "string" ? raw["password"] : "",
|
|
89781
|
+
session: typeof raw["session"] === "string" ? raw["session"] : "",
|
|
89782
|
+
...region === "NA" || region === "EU" || region === "FE" ? { region } : {},
|
|
89783
|
+
...Array.isArray(targets) ? { targets } : {}
|
|
89784
|
+
});
|
|
89785
|
+
}
|
|
89189
89786
|
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
89190
89787
|
var alexaAddonConfigSchema = object({
|
|
89191
89788
|
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
89192
89789
|
proxyLogin: alexaProxyLoginSchema.default({
|
|
89193
89790
|
proxyHost: "",
|
|
89194
89791
|
proxyPort: 3456
|
|
89792
|
+
}),
|
|
89793
|
+
speak: alexaSpeakSettingsSchema.default({
|
|
89794
|
+
email: "",
|
|
89795
|
+
password: "",
|
|
89796
|
+
region: "EU",
|
|
89797
|
+
session: "",
|
|
89798
|
+
targets: []
|
|
89195
89799
|
})
|
|
89196
89800
|
});
|
|
89197
89801
|
/** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
|
|
@@ -112965,17 +113569,17 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
112965
113569
|
* @private
|
|
112966
113570
|
*/
|
|
112967
113571
|
var debug = require_src$2()("express:view");
|
|
112968
|
-
var path$
|
|
112969
|
-
var fs$
|
|
113572
|
+
var path$8 = __require("path");
|
|
113573
|
+
var fs$5 = __require("fs");
|
|
112970
113574
|
/**
|
|
112971
113575
|
* Module variables.
|
|
112972
113576
|
* @private
|
|
112973
113577
|
*/
|
|
112974
|
-
var dirname = path$
|
|
112975
|
-
var basename = path$
|
|
112976
|
-
var extname = path$
|
|
112977
|
-
var join = path$
|
|
112978
|
-
var resolve = path$
|
|
113578
|
+
var dirname = path$8.dirname;
|
|
113579
|
+
var basename = path$8.basename;
|
|
113580
|
+
var extname = path$8.extname;
|
|
113581
|
+
var join = path$8.join;
|
|
113582
|
+
var resolve = path$8.resolve;
|
|
112979
113583
|
/**
|
|
112980
113584
|
* Module exports.
|
|
112981
113585
|
* @public
|
|
@@ -113072,7 +113676,7 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
113072
113676
|
function tryStat(path) {
|
|
113073
113677
|
debug("stat \"%s\"", path);
|
|
113074
113678
|
try {
|
|
113075
|
-
return fs$
|
|
113679
|
+
return fs$5.statSync(path);
|
|
113076
113680
|
} catch (e) {
|
|
113077
113681
|
return;
|
|
113078
113682
|
}
|
|
@@ -115242,7 +115846,7 @@ var init_types = __esmMin((() => {
|
|
|
115242
115846
|
//#region ../../node_modules/send/node_modules/mime/mime.js
|
|
115243
115847
|
var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
115244
115848
|
__require("path");
|
|
115245
|
-
var fs$
|
|
115849
|
+
var fs$4 = __require("fs");
|
|
115246
115850
|
function Mime() {
|
|
115247
115851
|
this.types = Object.create(null);
|
|
115248
115852
|
this.extensions = Object.create(null);
|
|
@@ -115277,7 +115881,7 @@ var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115277
115881
|
Mime.prototype.load = function(file) {
|
|
115278
115882
|
this._loading = file;
|
|
115279
115883
|
var map = {};
|
|
115280
|
-
fs$
|
|
115884
|
+
fs$4.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
|
|
115281
115885
|
var fields = line.replace(/\s*#.*|^\s*|\s*$/g, "").split(/\s+/);
|
|
115282
115886
|
map[fields.shift()] = fields;
|
|
115283
115887
|
});
|
|
@@ -115553,12 +116157,12 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115553
116157
|
var escapeHtml = require_escape_html();
|
|
115554
116158
|
var etag = require_etag();
|
|
115555
116159
|
var fresh = require_fresh();
|
|
115556
|
-
var fs$
|
|
116160
|
+
var fs$3 = __require("fs");
|
|
115557
116161
|
var mime = require_mime();
|
|
115558
116162
|
var ms = require_ms();
|
|
115559
116163
|
var onFinished = require_on_finished();
|
|
115560
116164
|
var parseRange = require_range_parser();
|
|
115561
|
-
var path$
|
|
116165
|
+
var path$7 = __require("path");
|
|
115562
116166
|
var statuses = require_statuses();
|
|
115563
116167
|
var Stream = __require("stream");
|
|
115564
116168
|
var util$4 = __require("util");
|
|
@@ -115566,11 +116170,11 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115566
116170
|
* Path function references.
|
|
115567
116171
|
* @private
|
|
115568
116172
|
*/
|
|
115569
|
-
var extname = path$
|
|
115570
|
-
var join = path$
|
|
115571
|
-
var normalize = path$
|
|
115572
|
-
var resolve = path$
|
|
115573
|
-
var sep = path$
|
|
116173
|
+
var extname = path$7.extname;
|
|
116174
|
+
var join = path$7.join;
|
|
116175
|
+
var normalize = path$7.normalize;
|
|
116176
|
+
var resolve = path$7.resolve;
|
|
116177
|
+
var sep = path$7.sep;
|
|
115574
116178
|
/**
|
|
115575
116179
|
* Regular expression for identifying a bytes Range header.
|
|
115576
116180
|
* @private
|
|
@@ -116018,7 +116622,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116018
116622
|
var i = 0;
|
|
116019
116623
|
var self = this;
|
|
116020
116624
|
debug("stat \"%s\"", path);
|
|
116021
|
-
fs$
|
|
116625
|
+
fs$3.stat(path, function onstat(err, stat) {
|
|
116022
116626
|
if (err && err.code === "ENOENT" && !extname(path) && path[path.length - 1] !== sep) return next(err);
|
|
116023
116627
|
if (err) return self.onStatError(err);
|
|
116024
116628
|
if (stat.isDirectory()) return self.redirect(path);
|
|
@@ -116029,7 +116633,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116029
116633
|
if (self._extensions.length <= i) return err ? self.onStatError(err) : self.error(404);
|
|
116030
116634
|
var p = path + "." + self._extensions[i++];
|
|
116031
116635
|
debug("stat \"%s\"", p);
|
|
116032
|
-
fs$
|
|
116636
|
+
fs$3.stat(p, function(err, stat) {
|
|
116033
116637
|
if (err) return next(err);
|
|
116034
116638
|
if (stat.isDirectory()) return next();
|
|
116035
116639
|
self.emit("file", p, stat);
|
|
@@ -116053,7 +116657,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116053
116657
|
}
|
|
116054
116658
|
var p = join(path, self._index[i]);
|
|
116055
116659
|
debug("stat \"%s\"", p);
|
|
116056
|
-
fs$
|
|
116660
|
+
fs$3.stat(p, function(err, stat) {
|
|
116057
116661
|
if (err) return next(err);
|
|
116058
116662
|
if (stat.isDirectory()) return next();
|
|
116059
116663
|
self.emit("file", p, stat);
|
|
@@ -116072,7 +116676,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116072
116676
|
SendStream.prototype.stream = function stream(path, options) {
|
|
116073
116677
|
var self = this;
|
|
116074
116678
|
var res = this.res;
|
|
116075
|
-
var stream = fs$
|
|
116679
|
+
var stream = fs$3.createReadStream(path, options);
|
|
116076
116680
|
this.emit("stream", stream);
|
|
116077
116681
|
stream.pipe(res);
|
|
116078
116682
|
function cleanup() {
|
|
@@ -117512,7 +118116,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
117512
118116
|
var deprecate = require_depd()("express");
|
|
117513
118117
|
var flatten = require_array_flatten();
|
|
117514
118118
|
var merge = require_utils_merge();
|
|
117515
|
-
var resolve$
|
|
118119
|
+
var resolve$2 = __require("path").resolve;
|
|
117516
118120
|
var setPrototypeOf = require_setprototypeof();
|
|
117517
118121
|
/**
|
|
117518
118122
|
* Module variables.
|
|
@@ -117575,7 +118179,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
117575
118179
|
this.mountpath = "/";
|
|
117576
118180
|
this.locals.settings = this.settings;
|
|
117577
118181
|
this.set("view", View);
|
|
117578
|
-
this.set("views", resolve$
|
|
118182
|
+
this.set("views", resolve$2("views"));
|
|
117579
118183
|
this.set("jsonp callback name", "callback");
|
|
117580
118184
|
if (env === "production") this.enable("view cache");
|
|
117581
118185
|
Object.defineProperty(this, "router", { get: function() {
|
|
@@ -119593,7 +120197,7 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
119593
120197
|
var http$3 = __require("http");
|
|
119594
120198
|
var isAbsolute = require_utils$2().isAbsolute;
|
|
119595
120199
|
var onFinished = require_on_finished();
|
|
119596
|
-
var path$
|
|
120200
|
+
var path$6 = __require("path");
|
|
119597
120201
|
var statuses = require_statuses();
|
|
119598
120202
|
var merge = require_utils_merge();
|
|
119599
120203
|
var sign = require_cookie_signature().sign;
|
|
@@ -119602,9 +120206,9 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
119602
120206
|
var setCharset = require_utils$2().setCharset;
|
|
119603
120207
|
var cookie = require_cookie();
|
|
119604
120208
|
var send = require_send();
|
|
119605
|
-
var extname = path$
|
|
120209
|
+
var extname = path$6.extname;
|
|
119606
120210
|
var mime = send.mime;
|
|
119607
|
-
var resolve = path$
|
|
120211
|
+
var resolve = path$6.resolve;
|
|
119608
120212
|
var vary = require_vary();
|
|
119609
120213
|
/**
|
|
119610
120214
|
* Response prototype.
|
|
@@ -120436,7 +121040,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
120436
121040
|
var encodeUrl = require_encodeurl();
|
|
120437
121041
|
var escapeHtml = require_escape_html();
|
|
120438
121042
|
var parseUrl = require_parseurl();
|
|
120439
|
-
var resolve = __require("path").resolve;
|
|
121043
|
+
var resolve$1 = __require("path").resolve;
|
|
120440
121044
|
var send = require_send();
|
|
120441
121045
|
var url$6 = __require("url");
|
|
120442
121046
|
/**
|
|
@@ -120460,7 +121064,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
120460
121064
|
var setHeaders = opts.setHeaders;
|
|
120461
121065
|
if (setHeaders && typeof setHeaders !== "function") throw new TypeError("option setHeaders must be function");
|
|
120462
121066
|
opts.maxage = opts.maxage || opts.maxAge || 0;
|
|
120463
|
-
opts.root = resolve(root);
|
|
121067
|
+
opts.root = resolve$1(root);
|
|
120464
121068
|
var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
|
|
120465
121069
|
return function serveStatic(req, res, next) {
|
|
120466
121070
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -121586,7 +122190,7 @@ var require_has_flag = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
121586
122190
|
//#endregion
|
|
121587
122191
|
//#region ../../node_modules/supports-color/index.js
|
|
121588
122192
|
var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
121589
|
-
var os$
|
|
122193
|
+
var os$3 = __require("os");
|
|
121590
122194
|
var tty$1 = __require("tty");
|
|
121591
122195
|
var hasFlag = require_has_flag();
|
|
121592
122196
|
var { env } = process;
|
|
@@ -121622,7 +122226,7 @@ var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
121622
122226
|
const min = forceColor || 0;
|
|
121623
122227
|
if (env.TERM === "dumb") return min;
|
|
121624
122228
|
if (process.platform === "win32") {
|
|
121625
|
-
const osRelease = os$
|
|
122229
|
+
const osRelease = os$3.release().split(".");
|
|
121626
122230
|
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
121627
122231
|
return 1;
|
|
121628
122232
|
}
|
|
@@ -124129,7 +124733,7 @@ var require_braces = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124129
124733
|
//#endregion
|
|
124130
124734
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/constants.js
|
|
124131
124735
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
124132
|
-
var path$
|
|
124736
|
+
var path$5 = __require("path");
|
|
124133
124737
|
var WIN_SLASH = "\\\\/";
|
|
124134
124738
|
var WIN_NO_SLASH = `[^${WIN_SLASH}]`;
|
|
124135
124739
|
var DEFAULT_MAX_EXTGLOB_RECURSION = 0;
|
|
@@ -124254,7 +124858,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124254
124858
|
CHAR_UNDERSCORE: 95,
|
|
124255
124859
|
CHAR_VERTICAL_LINE: 124,
|
|
124256
124860
|
CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279,
|
|
124257
|
-
SEP: path$
|
|
124861
|
+
SEP: path$5.sep,
|
|
124258
124862
|
/**
|
|
124259
124863
|
* Create EXTGLOB_CHARS
|
|
124260
124864
|
*/
|
|
@@ -124298,7 +124902,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124298
124902
|
//#endregion
|
|
124299
124903
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/utils.js
|
|
124300
124904
|
var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
124301
|
-
var path$
|
|
124905
|
+
var path$4 = __require("path");
|
|
124302
124906
|
var win32 = process.platform === "win32";
|
|
124303
124907
|
var { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = require_constants();
|
|
124304
124908
|
exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
|
|
@@ -124318,7 +124922,7 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
124318
124922
|
};
|
|
124319
124923
|
exports.isWindows = (options) => {
|
|
124320
124924
|
if (options && typeof options.windows === "boolean") return options.windows;
|
|
124321
|
-
return win32 === true || path$
|
|
124925
|
+
return win32 === true || path$4.sep === "\\";
|
|
124322
124926
|
};
|
|
124323
124927
|
exports.escapeLast = (input, char, lastIdx) => {
|
|
124324
124928
|
const idx = input.lastIndexOf(char, lastIdx);
|
|
@@ -125688,7 +126292,7 @@ var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
125688
126292
|
//#endregion
|
|
125689
126293
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/picomatch.js
|
|
125690
126294
|
var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
125691
|
-
var path$
|
|
126295
|
+
var path$3 = __require("path");
|
|
125692
126296
|
var scan = require_scan();
|
|
125693
126297
|
var parse = require_parse();
|
|
125694
126298
|
var utils = require_utils();
|
|
@@ -125827,7 +126431,7 @@ var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
125827
126431
|
* @api public
|
|
125828
126432
|
*/
|
|
125829
126433
|
picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => {
|
|
125830
|
-
return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$
|
|
126434
|
+
return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$3.basename(input));
|
|
125831
126435
|
};
|
|
125832
126436
|
/**
|
|
125833
126437
|
* Returns true if **any** of the given glob `patterns` match the specified `string`.
|
|
@@ -126912,8 +127516,8 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126912
127516
|
var proxy = require_dist().createProxyMiddleware;
|
|
126913
127517
|
var querystring$2 = __require("querystring");
|
|
126914
127518
|
var cookieTools = require_cookie$1();
|
|
126915
|
-
var fs = __require("fs");
|
|
126916
|
-
var path$
|
|
127519
|
+
var fs$2 = __require("fs");
|
|
127520
|
+
var path$2 = __require("path");
|
|
126917
127521
|
var crypto$1 = __require("crypto");
|
|
126918
127522
|
var FORMERDATA_STORE_VERSION = 4;
|
|
126919
127523
|
function addCookies(Cookie, headers) {
|
|
@@ -126944,11 +127548,11 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126944
127548
|
}
|
|
126945
127549
|
function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
126946
127550
|
const initialCookies = {};
|
|
126947
|
-
const formerDataStorePath = _options.formerDataStorePath || path$
|
|
127551
|
+
const formerDataStorePath = _options.formerDataStorePath || path$2.join(__dirname, "formerDataStore.json");
|
|
126948
127552
|
let formerDataStoreValid = false;
|
|
126949
127553
|
if (!_options.formerRegistrationData) try {
|
|
126950
|
-
if (fs.existsSync(formerDataStorePath)) {
|
|
126951
|
-
const formerDataStore = JSON.parse(fs.readFileSync(path$
|
|
127554
|
+
if (fs$2.existsSync(formerDataStorePath)) {
|
|
127555
|
+
const formerDataStore = JSON.parse(fs$2.readFileSync(path$2.join(__dirname, "formerDataStore.json"), "utf8"));
|
|
126952
127556
|
if (typeof formerDataStore === "object" && formerDataStore.storeVersion === FORMERDATA_STORE_VERSION) {
|
|
126953
127557
|
_options.formerRegistrationData = _options.formerRegistrationData || {};
|
|
126954
127558
|
_options.formerRegistrationData.frc = _options.formerRegistrationData.frc || formerDataStore.frc;
|
|
@@ -126989,7 +127593,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126989
127593
|
"map-md": initialCookies["map-md"],
|
|
126990
127594
|
"frc": initialCookies.frc
|
|
126991
127595
|
};
|
|
126992
|
-
fs.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
|
|
127596
|
+
fs$2.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
|
|
126993
127597
|
} catch (_err) {}
|
|
126994
127598
|
function base64URLEncode(str) {
|
|
126995
127599
|
return str.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
@@ -127199,7 +127803,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127199
127803
|
var https$1 = __require("https");
|
|
127200
127804
|
var querystring$1 = __require("querystring");
|
|
127201
127805
|
var url = __require("url");
|
|
127202
|
-
var os$
|
|
127806
|
+
var os$2 = __require("os");
|
|
127203
127807
|
var cookieTools = require_cookie$1();
|
|
127204
127808
|
var amazonProxy = require_proxy();
|
|
127205
127809
|
var defaultAmazonPage = "amazon.de";
|
|
@@ -127312,7 +127916,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127312
127916
|
else if (amazonDomain !== "com") _options.baseAmazonPageHandle = "";
|
|
127313
127917
|
else _options.baseAmazonPageHandle = "";
|
|
127314
127918
|
}
|
|
127315
|
-
if (!_options.userAgent) if (os$
|
|
127919
|
+
if (!_options.userAgent) if (os$2.platform() === "win32") _options.userAgent = defaultUserAgent;
|
|
127316
127920
|
else _options.userAgent = defaultUserAgentLinux;
|
|
127317
127921
|
_options.logger && _options.logger(`Alexa-Cookie: Use as User-Agent: ${_options.userAgent}`);
|
|
127318
127922
|
_options.acceptLanguage = _options.acceptLanguage || defaultAcceptLanguage;
|
|
@@ -127836,7 +128440,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127836
128440
|
var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
127837
128441
|
var https = __require("https");
|
|
127838
128442
|
var querystring = __require("querystring");
|
|
127839
|
-
var os = __require("os");
|
|
128443
|
+
var os$1 = __require("os");
|
|
127840
128444
|
var extend = require_extend();
|
|
127841
128445
|
var AlexaHttp2Push = require_alexa_http2push();
|
|
127842
128446
|
var { v1: uuidv1 } = require_cjs();
|
|
@@ -127981,7 +128585,7 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
|
|
|
127981
128585
|
init(cookie, callback) {
|
|
127982
128586
|
if (typeof cookie === "object") {
|
|
127983
128587
|
this._options = cookie;
|
|
127984
|
-
if (!this._options.userAgent) if (os.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
128588
|
+
if (!this._options.userAgent) if (os$1.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
127985
128589
|
else this._options.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
127986
128590
|
if (this._options.apiUserAgentPostfix === void 0) this._options.apiUserAgentPostfix = `AlexaRemote/${__require(fsPath.join(__dirname, "package.json")).version}`;
|
|
127987
128591
|
this._options.amazonPage = this._options.amazonPage || "amazon.de";
|
|
@@ -132679,13 +133283,1365 @@ var AlexaBrokerStore = class AlexaBrokerStore {
|
|
|
132679
133283
|
}
|
|
132680
133284
|
};
|
|
132681
133285
|
//#endregion
|
|
133286
|
+
//#region src/speak/constants.ts
|
|
133287
|
+
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
133288
|
+
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
133289
|
+
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
133290
|
+
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
133291
|
+
var OTP_ALERT_ID = "import-alexa.speak-otp-required";
|
|
133292
|
+
var ALEXA_REGIONS = [
|
|
133293
|
+
"NA",
|
|
133294
|
+
"EU",
|
|
133295
|
+
"FE"
|
|
133296
|
+
];
|
|
133297
|
+
//#endregion
|
|
133298
|
+
//#region src/speak/worker-protocol.ts
|
|
133299
|
+
/** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
|
|
133300
|
+
var EchoDeviceSchema = object({
|
|
133301
|
+
id: string().min(1),
|
|
133302
|
+
name: string(),
|
|
133303
|
+
family: string(),
|
|
133304
|
+
online: boolean(),
|
|
133305
|
+
kind: _enum(["device", "group"]),
|
|
133306
|
+
dnd: boolean(),
|
|
133307
|
+
deviceType: string()
|
|
133308
|
+
});
|
|
133309
|
+
var LoginStatusSchema = _enum([
|
|
133310
|
+
"ok",
|
|
133311
|
+
"otp_needed",
|
|
133312
|
+
"captcha",
|
|
133313
|
+
"failed"
|
|
133314
|
+
]);
|
|
133315
|
+
var SessionStatusSchema = object({
|
|
133316
|
+
loggedIn: boolean(),
|
|
133317
|
+
ageSec: number().int().nonnegative(),
|
|
133318
|
+
lastSuccessAt: number().int().nullable()
|
|
133319
|
+
});
|
|
133320
|
+
var TargetDeliverySchema = object({
|
|
133321
|
+
targetId: string(),
|
|
133322
|
+
ok: boolean(),
|
|
133323
|
+
error: string().optional()
|
|
133324
|
+
});
|
|
133325
|
+
var WorkerLineSchema = union([object({
|
|
133326
|
+
id: string(),
|
|
133327
|
+
ok: literal(true),
|
|
133328
|
+
result: unknown().optional()
|
|
133329
|
+
}), object({
|
|
133330
|
+
id: string(),
|
|
133331
|
+
ok: literal(false),
|
|
133332
|
+
error: string()
|
|
133333
|
+
})]);
|
|
133334
|
+
function requestLine(id, request) {
|
|
133335
|
+
return `${JSON.stringify({
|
|
133336
|
+
id,
|
|
133337
|
+
...request
|
|
133338
|
+
})}\n`;
|
|
133339
|
+
}
|
|
133340
|
+
function parseLoginResult(result) {
|
|
133341
|
+
const parsed = object({
|
|
133342
|
+
status: LoginStatusSchema,
|
|
133343
|
+
error: string().optional()
|
|
133344
|
+
}).safeParse(result);
|
|
133345
|
+
if (!parsed.success) return {
|
|
133346
|
+
status: "failed",
|
|
133347
|
+
error: "worker returned an unexpected login result"
|
|
133348
|
+
};
|
|
133349
|
+
return parsed.data;
|
|
133350
|
+
}
|
|
133351
|
+
function parseDevices(result) {
|
|
133352
|
+
const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
|
|
133353
|
+
if (!parsed.success) throw new Error("worker returned an unexpected device list");
|
|
133354
|
+
return parsed.data.devices;
|
|
133355
|
+
}
|
|
133356
|
+
function parseDeliveries(result) {
|
|
133357
|
+
const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
|
|
133358
|
+
if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
|
|
133359
|
+
return parsed.data.outcomes;
|
|
133360
|
+
}
|
|
133361
|
+
function parseSessionStatus(result) {
|
|
133362
|
+
const parsed = SessionStatusSchema.safeParse(result);
|
|
133363
|
+
if (!parsed.success) throw new Error("worker returned an unexpected session status");
|
|
133364
|
+
return parsed.data;
|
|
133365
|
+
}
|
|
133366
|
+
//#endregion
|
|
133367
|
+
//#region src/speak/actions.ts
|
|
133368
|
+
var SpeakStatusSchema = object({
|
|
133369
|
+
phase: _enum([
|
|
133370
|
+
"sign-in",
|
|
133371
|
+
"otp",
|
|
133372
|
+
"captcha",
|
|
133373
|
+
"devices"
|
|
133374
|
+
]),
|
|
133375
|
+
email: string(),
|
|
133376
|
+
region: _enum(ALEXA_REGIONS),
|
|
133377
|
+
devices: array(EchoDeviceSchema),
|
|
133378
|
+
session: SessionStatusSchema
|
|
133379
|
+
});
|
|
133380
|
+
var LoginResultSchema = object({
|
|
133381
|
+
status: LoginStatusSchema,
|
|
133382
|
+
error: string().optional()
|
|
133383
|
+
});
|
|
133384
|
+
var alexaSpeakActions = defineCustomActions({
|
|
133385
|
+
speakStatus: customAction(object({}), SpeakStatusSchema, {
|
|
133386
|
+
kind: "mutation",
|
|
133387
|
+
auth: "admin"
|
|
133388
|
+
}),
|
|
133389
|
+
loginStart: customAction(object({
|
|
133390
|
+
email: string().min(1),
|
|
133391
|
+
password: string().min(1).meta({ secret: true }),
|
|
133392
|
+
region: _enum(ALEXA_REGIONS)
|
|
133393
|
+
}), LoginResultSchema, {
|
|
133394
|
+
kind: "mutation",
|
|
133395
|
+
auth: "admin"
|
|
133396
|
+
}),
|
|
133397
|
+
loginOtp: customAction(object({ code: string().min(1).max(12) }), LoginResultSchema, {
|
|
133398
|
+
kind: "mutation",
|
|
133399
|
+
auth: "admin"
|
|
133400
|
+
}),
|
|
133401
|
+
sendTest: customAction(object({
|
|
133402
|
+
echoIds: array(string().min(1)).min(1),
|
|
133403
|
+
mode: _enum(["speak", "announce"]),
|
|
133404
|
+
volume: number().int().min(0).max(100),
|
|
133405
|
+
text: string().min(1).max(450)
|
|
133406
|
+
}), object({ outcomes: array(object({
|
|
133407
|
+
echoId: string(),
|
|
133408
|
+
ok: boolean(),
|
|
133409
|
+
error: string().optional()
|
|
133410
|
+
})) }), {
|
|
133411
|
+
kind: "mutation",
|
|
133412
|
+
auth: "admin"
|
|
133413
|
+
})
|
|
133414
|
+
});
|
|
133415
|
+
//#endregion
|
|
133416
|
+
//#region src/speak/otp-alert.ts
|
|
133417
|
+
/**
|
|
133418
|
+
* A login that needs a new one-time code raises ONE operator alert.
|
|
133419
|
+
* Further failures in the same episode do not raise another.
|
|
133420
|
+
*/
|
|
133421
|
+
function buildOtpAlert(now, reason) {
|
|
133422
|
+
const otp = reason === "otp";
|
|
133423
|
+
return {
|
|
133424
|
+
id: OTP_ALERT_ID,
|
|
133425
|
+
category: EventCategory.AddonError,
|
|
133426
|
+
severity: "error",
|
|
133427
|
+
title: otp ? "Alexa needs a new sign-in code" : "Alexa sign-in needs a captcha",
|
|
133428
|
+
message: otp ? "Amazon asked for a one-time code before Echo devices can speak. Open Alexa → Speak on Echo and enter the code." : "Amazon asked for a captcha. Wait a few minutes, then sign in again on Alexa → Speak on Echo.",
|
|
133429
|
+
action: {
|
|
133430
|
+
label: "Sign in",
|
|
133431
|
+
href: "/addon/import-alexa",
|
|
133432
|
+
navigation: "app"
|
|
133433
|
+
},
|
|
133434
|
+
status: "active",
|
|
133435
|
+
read: false,
|
|
133436
|
+
createdAt: now,
|
|
133437
|
+
updatedAt: now,
|
|
133438
|
+
source: {
|
|
133439
|
+
type: "addon",
|
|
133440
|
+
id: "import-alexa"
|
|
133441
|
+
},
|
|
133442
|
+
metadata: { episodeKey: reason }
|
|
133443
|
+
};
|
|
133444
|
+
}
|
|
133445
|
+
var OtpAttention = class {
|
|
133446
|
+
ports;
|
|
133447
|
+
open = null;
|
|
133448
|
+
constructor(ports) {
|
|
133449
|
+
this.ports = ports;
|
|
133450
|
+
}
|
|
133451
|
+
/** First need in an episode emits the alert. A repeat does not. */
|
|
133452
|
+
async needed(reason) {
|
|
133453
|
+
if (this.open !== null) return "already-open";
|
|
133454
|
+
this.open = reason;
|
|
133455
|
+
await this.ports.emit(buildOtpAlert(this.ports.now(), reason));
|
|
133456
|
+
return "raised";
|
|
133457
|
+
}
|
|
133458
|
+
async cleared() {
|
|
133459
|
+
if (this.open === null) return;
|
|
133460
|
+
this.open = null;
|
|
133461
|
+
await this.ports.resolve(OTP_ALERT_ID);
|
|
133462
|
+
}
|
|
133463
|
+
};
|
|
133464
|
+
//#endregion
|
|
133465
|
+
//#region src/speak/rate-limit.ts
|
|
133466
|
+
/**
|
|
133467
|
+
* Per-Echo admission. A burst is dropped here, not queued: a queue would be
|
|
133468
|
+
* minutes of speech long after the event.
|
|
133469
|
+
*/
|
|
133470
|
+
function emptySpeechLedger() {
|
|
133471
|
+
return { byEcho: {} };
|
|
133472
|
+
}
|
|
133473
|
+
/**
|
|
133474
|
+
* Identical text inside the collapse window is one utterance.
|
|
133475
|
+
* A different text inside the gap is refused until the gap elapses.
|
|
133476
|
+
* Admitted calls replace that Echo's memory; refusals leave it as it was.
|
|
133477
|
+
*/
|
|
133478
|
+
function admitSpeech(ledger, echoId, text, now) {
|
|
133479
|
+
const previous = ledger.byEcho[echoId];
|
|
133480
|
+
if (previous !== void 0 && previous.lastText === text && now - previous.lastSpokenAt < 6e4) return {
|
|
133481
|
+
decision: "collapsed",
|
|
133482
|
+
ledger
|
|
133483
|
+
};
|
|
133484
|
+
if (previous !== void 0 && now - previous.lastSpokenAt < 2e4) return {
|
|
133485
|
+
decision: "rate_limited",
|
|
133486
|
+
ledger
|
|
133487
|
+
};
|
|
133488
|
+
return {
|
|
133489
|
+
decision: "admit",
|
|
133490
|
+
ledger: { byEcho: {
|
|
133491
|
+
...ledger.byEcho,
|
|
133492
|
+
[echoId]: {
|
|
133493
|
+
lastSpokenAt: now,
|
|
133494
|
+
lastText: text
|
|
133495
|
+
}
|
|
133496
|
+
} }
|
|
133497
|
+
};
|
|
133498
|
+
}
|
|
133499
|
+
//#endregion
|
|
133500
|
+
//#region src/speak/speech-text.ts
|
|
133501
|
+
var ALEXA_SPEECH_CAPS = {
|
|
133502
|
+
attachments: {
|
|
133503
|
+
mediaTypes: [],
|
|
133504
|
+
mode: "url",
|
|
133505
|
+
max: 0
|
|
133506
|
+
},
|
|
133507
|
+
actions: 0,
|
|
133508
|
+
actionIcons: false,
|
|
133509
|
+
levels: [{
|
|
133510
|
+
id: "default",
|
|
133511
|
+
label: "Default",
|
|
133512
|
+
ordinal: 3
|
|
133513
|
+
}],
|
|
133514
|
+
format: ["text"],
|
|
133515
|
+
clickUrl: false,
|
|
133516
|
+
sound: false,
|
|
133517
|
+
ttl: false,
|
|
133518
|
+
bodyMaxLen: 450
|
|
133519
|
+
};
|
|
133520
|
+
function collapseWhitespace(value) {
|
|
133521
|
+
return value.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
133522
|
+
}
|
|
133523
|
+
/** Markup that survived the degrade engine must not be read aloud. */
|
|
133524
|
+
function toPlainSpeech(value) {
|
|
133525
|
+
return collapseWhitespace(htmlToText(markdownToText(value)));
|
|
133526
|
+
}
|
|
133527
|
+
/**
|
|
133528
|
+
* Title then body, as one sentence, never longer than {@link SPEECH_MAX_CHARS}.
|
|
133529
|
+
* An empty result is an empty string — the caller refuses to send it.
|
|
133530
|
+
*/
|
|
133531
|
+
function speechFromNotification(notification) {
|
|
133532
|
+
const prepared = prepareNotification(ALEXA_SPEECH_CAPS, notification);
|
|
133533
|
+
const title = prepared.title !== void 0 ? toPlainSpeech(prepared.title) : "";
|
|
133534
|
+
const body = toPlainSpeech(prepared.body);
|
|
133535
|
+
const joined = title.length > 0 && body.length > 0 ? `${title}. ${body}` : title || body;
|
|
133536
|
+
const text = joined.length > 450 ? joined.slice(0, 450).trimEnd() : joined;
|
|
133537
|
+
const truncated = prepared.renderedAs.truncated || text.length < joined.length;
|
|
133538
|
+
return {
|
|
133539
|
+
text,
|
|
133540
|
+
renderedAs: {
|
|
133541
|
+
...prepared.renderedAs,
|
|
133542
|
+
truncated,
|
|
133543
|
+
format: "text"
|
|
133544
|
+
}
|
|
133545
|
+
};
|
|
133546
|
+
}
|
|
133547
|
+
//#endregion
|
|
133548
|
+
//#region src/speak/provider.ts
|
|
133549
|
+
function quietLogger$1() {
|
|
133550
|
+
const logger = {
|
|
133551
|
+
debug() {},
|
|
133552
|
+
info() {},
|
|
133553
|
+
warn() {},
|
|
133554
|
+
error() {},
|
|
133555
|
+
child() {
|
|
133556
|
+
return logger;
|
|
133557
|
+
},
|
|
133558
|
+
withTags() {
|
|
133559
|
+
return logger;
|
|
133560
|
+
}
|
|
133561
|
+
};
|
|
133562
|
+
return logger;
|
|
133563
|
+
}
|
|
133564
|
+
function readString(config, key) {
|
|
133565
|
+
const value = config[key];
|
|
133566
|
+
return typeof value === "string" ? value : "";
|
|
133567
|
+
}
|
|
133568
|
+
function readMode(config) {
|
|
133569
|
+
return config["mode"] === "announce" ? "announce" : "speak";
|
|
133570
|
+
}
|
|
133571
|
+
function readVolume(config) {
|
|
133572
|
+
const value = config["volume"];
|
|
133573
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 40;
|
|
133574
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
133575
|
+
}
|
|
133576
|
+
function respectDnd(config) {
|
|
133577
|
+
return config["respectDnd"] !== false;
|
|
133578
|
+
}
|
|
133579
|
+
function configSchema() {
|
|
133580
|
+
return { sections: [{
|
|
133581
|
+
id: "echo",
|
|
133582
|
+
title: "Echo",
|
|
133583
|
+
titleKey: "import-alexa:echoTarget.s_echo.title",
|
|
133584
|
+
fields: [
|
|
133585
|
+
{
|
|
133586
|
+
type: "text",
|
|
133587
|
+
key: "echoId",
|
|
133588
|
+
label: "Echo id",
|
|
133589
|
+
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
133590
|
+
required: true
|
|
133591
|
+
},
|
|
133592
|
+
{
|
|
133593
|
+
type: "select",
|
|
133594
|
+
key: "mode",
|
|
133595
|
+
label: "Mode",
|
|
133596
|
+
labelKey: "import-alexa:echoTarget.s_echo.mode.label",
|
|
133597
|
+
default: "speak",
|
|
133598
|
+
options: [{
|
|
133599
|
+
value: "speak",
|
|
133600
|
+
label: "Speak (quiet hours apply)"
|
|
133601
|
+
}, {
|
|
133602
|
+
value: "announce",
|
|
133603
|
+
label: "Announce (chime, then speech)"
|
|
133604
|
+
}]
|
|
133605
|
+
},
|
|
133606
|
+
{
|
|
133607
|
+
type: "number",
|
|
133608
|
+
key: "volume",
|
|
133609
|
+
label: "Volume",
|
|
133610
|
+
labelKey: "import-alexa:echoTarget.s_echo.volume.label",
|
|
133611
|
+
min: 0,
|
|
133612
|
+
max: 100,
|
|
133613
|
+
default: 40
|
|
133614
|
+
},
|
|
133615
|
+
{
|
|
133616
|
+
type: "boolean",
|
|
133617
|
+
key: "respectDnd",
|
|
133618
|
+
label: "Respect Do Not Disturb",
|
|
133619
|
+
labelKey: "import-alexa:echoTarget.s_echo.respectDnd.label",
|
|
133620
|
+
default: true
|
|
133621
|
+
}
|
|
133622
|
+
]
|
|
133623
|
+
}] };
|
|
133624
|
+
}
|
|
133625
|
+
function kindDescriptor() {
|
|
133626
|
+
return {
|
|
133627
|
+
kind: ALEXA_SPEAK_KIND,
|
|
133628
|
+
label: "Alexa",
|
|
133629
|
+
icon: "megaphone",
|
|
133630
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
133631
|
+
supportsDiscovery: true,
|
|
133632
|
+
destinationKeys: ["echoId"],
|
|
133633
|
+
configSchema: configSchema(),
|
|
133634
|
+
caps: {
|
|
133635
|
+
attachments: {
|
|
133636
|
+
mediaTypes: [],
|
|
133637
|
+
mode: "url",
|
|
133638
|
+
max: 0
|
|
133639
|
+
},
|
|
133640
|
+
actions: 0,
|
|
133641
|
+
actionIcons: false,
|
|
133642
|
+
levels: [{
|
|
133643
|
+
id: "default",
|
|
133644
|
+
label: "Default",
|
|
133645
|
+
ordinal: 3
|
|
133646
|
+
}],
|
|
133647
|
+
format: ["text"],
|
|
133648
|
+
clickUrl: false,
|
|
133649
|
+
sound: false,
|
|
133650
|
+
ttl: false,
|
|
133651
|
+
bodyMaxLen: 450
|
|
133652
|
+
}
|
|
133653
|
+
};
|
|
133654
|
+
}
|
|
133655
|
+
function discoveredFrom(device) {
|
|
133656
|
+
return {
|
|
133657
|
+
kind: ALEXA_SPEAK_KIND,
|
|
133658
|
+
suggestedName: device.name.length > 0 ? device.name : device.id,
|
|
133659
|
+
config: {
|
|
133660
|
+
echoId: device.id,
|
|
133661
|
+
mode: "speak",
|
|
133662
|
+
volume: 40,
|
|
133663
|
+
respectDnd: true,
|
|
133664
|
+
family: device.family,
|
|
133665
|
+
deviceKind: device.kind
|
|
133666
|
+
}
|
|
133667
|
+
};
|
|
133668
|
+
}
|
|
133669
|
+
function createAlexaNotificationProvider(ports) {
|
|
133670
|
+
const logger = ports.logger ?? quietLogger$1();
|
|
133671
|
+
let targets = ports.loadTargets();
|
|
133672
|
+
let ledger = emptySpeechLedger();
|
|
133673
|
+
let deliveries = [];
|
|
133674
|
+
function record(row) {
|
|
133675
|
+
deliveries = [...deliveries, row];
|
|
133676
|
+
}
|
|
133677
|
+
function deviceFor(echoId) {
|
|
133678
|
+
return ports.devices().find((device) => device.id === echoId);
|
|
133679
|
+
}
|
|
133680
|
+
function persist(next) {
|
|
133681
|
+
targets = next;
|
|
133682
|
+
ports.saveTargets(next);
|
|
133683
|
+
}
|
|
133684
|
+
async function deliver(target, notification) {
|
|
133685
|
+
const speech = speechFromNotification(notification);
|
|
133686
|
+
const echoId = readString(target.config, "echoId");
|
|
133687
|
+
const deviceId = notification.deviceId;
|
|
133688
|
+
const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
|
|
133689
|
+
if (echoId.length === 0) {
|
|
133690
|
+
record({
|
|
133691
|
+
targetId: target.id,
|
|
133692
|
+
outcome: "failed",
|
|
133693
|
+
at: ports.now(),
|
|
133694
|
+
error: "target has no echo id"
|
|
133695
|
+
});
|
|
133696
|
+
logger.warn("alexa speak dropped: target has no echo id", {
|
|
133697
|
+
...tags,
|
|
133698
|
+
meta: { targetId: target.id }
|
|
133699
|
+
});
|
|
133700
|
+
return {
|
|
133701
|
+
success: false,
|
|
133702
|
+
error: "target has no echo id",
|
|
133703
|
+
renderedAs: speech.renderedAs
|
|
133704
|
+
};
|
|
133705
|
+
}
|
|
133706
|
+
if (speech.text.length === 0) {
|
|
133707
|
+
record({
|
|
133708
|
+
targetId: target.id,
|
|
133709
|
+
outcome: "empty",
|
|
133710
|
+
at: ports.now()
|
|
133711
|
+
});
|
|
133712
|
+
logger.info("alexa speak dropped: empty text", {
|
|
133713
|
+
...tags,
|
|
133714
|
+
meta: {
|
|
133715
|
+
targetId: target.id,
|
|
133716
|
+
echoId
|
|
133717
|
+
}
|
|
133718
|
+
});
|
|
133719
|
+
return {
|
|
133720
|
+
success: true,
|
|
133721
|
+
renderedAs: speech.renderedAs
|
|
133722
|
+
};
|
|
133723
|
+
}
|
|
133724
|
+
const device = deviceFor(echoId);
|
|
133725
|
+
if (device !== void 0 && respectDnd(target.config) && device.dnd) {
|
|
133726
|
+
record({
|
|
133727
|
+
targetId: target.id,
|
|
133728
|
+
outcome: "dnd",
|
|
133729
|
+
at: ports.now()
|
|
133730
|
+
});
|
|
133731
|
+
logger.info("alexa speak skipped: do not disturb", {
|
|
133732
|
+
...tags,
|
|
133733
|
+
meta: {
|
|
133734
|
+
targetId: target.id,
|
|
133735
|
+
echoId
|
|
133736
|
+
}
|
|
133737
|
+
});
|
|
133738
|
+
return {
|
|
133739
|
+
success: true,
|
|
133740
|
+
renderedAs: speech.renderedAs
|
|
133741
|
+
};
|
|
133742
|
+
}
|
|
133743
|
+
if (device !== void 0 && !device.online) {
|
|
133744
|
+
record({
|
|
133745
|
+
targetId: target.id,
|
|
133746
|
+
outcome: "offline",
|
|
133747
|
+
at: ports.now()
|
|
133748
|
+
});
|
|
133749
|
+
logger.info("alexa speak not delivered: echo offline", {
|
|
133750
|
+
...tags,
|
|
133751
|
+
meta: {
|
|
133752
|
+
targetId: target.id,
|
|
133753
|
+
echoId
|
|
133754
|
+
}
|
|
133755
|
+
});
|
|
133756
|
+
return {
|
|
133757
|
+
success: false,
|
|
133758
|
+
error: "echo offline",
|
|
133759
|
+
renderedAs: speech.renderedAs
|
|
133760
|
+
};
|
|
133761
|
+
}
|
|
133762
|
+
const admitted = admitSpeech(ledger, echoId, speech.text, ports.now());
|
|
133763
|
+
ledger = admitted.ledger;
|
|
133764
|
+
if (admitted.decision !== "admit") {
|
|
133765
|
+
record({
|
|
133766
|
+
targetId: target.id,
|
|
133767
|
+
outcome: admitted.decision,
|
|
133768
|
+
at: ports.now()
|
|
133769
|
+
});
|
|
133770
|
+
logger.info("alexa speak skipped", {
|
|
133771
|
+
...tags,
|
|
133772
|
+
meta: {
|
|
133773
|
+
targetId: target.id,
|
|
133774
|
+
echoId,
|
|
133775
|
+
outcome: admitted.decision
|
|
133776
|
+
}
|
|
133777
|
+
});
|
|
133778
|
+
return {
|
|
133779
|
+
success: true,
|
|
133780
|
+
renderedAs: speech.renderedAs
|
|
133781
|
+
};
|
|
133782
|
+
}
|
|
133783
|
+
const mode = readMode(target.config);
|
|
133784
|
+
try {
|
|
133785
|
+
const outcomes = await ports.speak({
|
|
133786
|
+
mode,
|
|
133787
|
+
targets: [echoId],
|
|
133788
|
+
text: speech.text,
|
|
133789
|
+
volume: readVolume(target.config)
|
|
133790
|
+
});
|
|
133791
|
+
const one = outcomes.find((row) => row.targetId === echoId) ?? outcomes[0];
|
|
133792
|
+
if (one === void 0 || !one.ok) {
|
|
133793
|
+
const error = one?.error ?? "speak failed";
|
|
133794
|
+
record({
|
|
133795
|
+
targetId: target.id,
|
|
133796
|
+
outcome: "failed",
|
|
133797
|
+
at: ports.now(),
|
|
133798
|
+
error
|
|
133799
|
+
});
|
|
133800
|
+
logger.warn("alexa speak failed", {
|
|
133801
|
+
...tags,
|
|
133802
|
+
meta: {
|
|
133803
|
+
targetId: target.id,
|
|
133804
|
+
echoId,
|
|
133805
|
+
error
|
|
133806
|
+
}
|
|
133807
|
+
});
|
|
133808
|
+
return {
|
|
133809
|
+
success: false,
|
|
133810
|
+
error,
|
|
133811
|
+
renderedAs: speech.renderedAs
|
|
133812
|
+
};
|
|
133813
|
+
}
|
|
133814
|
+
const outcome = mode === "announce" ? "announced" : "spoken";
|
|
133815
|
+
record({
|
|
133816
|
+
targetId: target.id,
|
|
133817
|
+
outcome,
|
|
133818
|
+
at: ports.now()
|
|
133819
|
+
});
|
|
133820
|
+
logger.info("alexa speak delivered", {
|
|
133821
|
+
...tags,
|
|
133822
|
+
meta: {
|
|
133823
|
+
targetId: target.id,
|
|
133824
|
+
echoId,
|
|
133825
|
+
outcome
|
|
133826
|
+
}
|
|
133827
|
+
});
|
|
133828
|
+
return {
|
|
133829
|
+
success: true,
|
|
133830
|
+
renderedAs: speech.renderedAs
|
|
133831
|
+
};
|
|
133832
|
+
} catch (err) {
|
|
133833
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
133834
|
+
record({
|
|
133835
|
+
targetId: target.id,
|
|
133836
|
+
outcome: "failed",
|
|
133837
|
+
at: ports.now(),
|
|
133838
|
+
error
|
|
133839
|
+
});
|
|
133840
|
+
logger.warn("alexa speak failed", {
|
|
133841
|
+
...tags,
|
|
133842
|
+
meta: {
|
|
133843
|
+
targetId: target.id,
|
|
133844
|
+
echoId,
|
|
133845
|
+
error
|
|
133846
|
+
}
|
|
133847
|
+
});
|
|
133848
|
+
return {
|
|
133849
|
+
success: false,
|
|
133850
|
+
error,
|
|
133851
|
+
renderedAs: speech.renderedAs
|
|
133852
|
+
};
|
|
133853
|
+
}
|
|
133854
|
+
}
|
|
133855
|
+
return {
|
|
133856
|
+
get deliveries() {
|
|
133857
|
+
return deliveries;
|
|
133858
|
+
},
|
|
133859
|
+
async listTargetKinds() {
|
|
133860
|
+
return [kindDescriptor()];
|
|
133861
|
+
},
|
|
133862
|
+
async listTargets() {
|
|
133863
|
+
return [...targets];
|
|
133864
|
+
},
|
|
133865
|
+
async getTarget(input) {
|
|
133866
|
+
const found = targets.find((target) => target.id === input.targetId);
|
|
133867
|
+
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
133868
|
+
return found;
|
|
133869
|
+
},
|
|
133870
|
+
async discoverTargets() {
|
|
133871
|
+
return ports.devices().map(discoveredFrom);
|
|
133872
|
+
},
|
|
133873
|
+
async probeTargetConfig() {
|
|
133874
|
+
return ports.loggedIn() ? {
|
|
133875
|
+
ok: true,
|
|
133876
|
+
resolvedUrl: "alexa://session"
|
|
133877
|
+
} : {
|
|
133878
|
+
ok: false,
|
|
133879
|
+
resolvedUrl: "alexa://session",
|
|
133880
|
+
error: "not signed in"
|
|
133881
|
+
};
|
|
133882
|
+
},
|
|
133883
|
+
async send(input) {
|
|
133884
|
+
const target = targets.find((row) => row.id === input.targetId);
|
|
133885
|
+
if (target === void 0) return {
|
|
133886
|
+
success: false,
|
|
133887
|
+
error: `target not found: ${input.targetId}`
|
|
133888
|
+
};
|
|
133889
|
+
if (!target.enabled) return {
|
|
133890
|
+
success: false,
|
|
133891
|
+
error: "target disabled"
|
|
133892
|
+
};
|
|
133893
|
+
return deliver(target, input.notification);
|
|
133894
|
+
},
|
|
133895
|
+
async testTarget(input) {
|
|
133896
|
+
const found = targets.find((row) => row.id === input.targetId);
|
|
133897
|
+
if (found === void 0) return {
|
|
133898
|
+
success: false,
|
|
133899
|
+
error: `target not found: ${input.targetId}`
|
|
133900
|
+
};
|
|
133901
|
+
if (!found.enabled) return {
|
|
133902
|
+
success: false,
|
|
133903
|
+
error: "target disabled"
|
|
133904
|
+
};
|
|
133905
|
+
return deliver(found, input.sample ?? {
|
|
133906
|
+
body: "This is a CamStack test notification.",
|
|
133907
|
+
title: "CamStack",
|
|
133908
|
+
format: "text",
|
|
133909
|
+
priority: 3
|
|
133910
|
+
});
|
|
133911
|
+
},
|
|
133912
|
+
async upsertTarget(input) {
|
|
133913
|
+
const target = {
|
|
133914
|
+
...input.target,
|
|
133915
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
133916
|
+
kind: ALEXA_SPEAK_KIND
|
|
133917
|
+
};
|
|
133918
|
+
persist([...targets.filter((row) => row.id !== target.id), target]);
|
|
133919
|
+
return target;
|
|
133920
|
+
},
|
|
133921
|
+
async deleteTarget(input) {
|
|
133922
|
+
persist(targets.filter((row) => row.id !== input.targetId));
|
|
133923
|
+
},
|
|
133924
|
+
async setTargetEnabled(input) {
|
|
133925
|
+
persist(targets.map((row) => row.id === input.targetId ? {
|
|
133926
|
+
...row,
|
|
133927
|
+
enabled: input.enabled
|
|
133928
|
+
} : row));
|
|
133929
|
+
}
|
|
133930
|
+
};
|
|
133931
|
+
}
|
|
133932
|
+
//#endregion
|
|
133933
|
+
//#region src/speak/python-paths.ts
|
|
133934
|
+
/** Locate bundled `python/alexa_worker.py` from dist/ or from src/ under test. */
|
|
133935
|
+
function resolveAlexaWorkerScript(fromModuleUrl = import.meta.url) {
|
|
133936
|
+
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
133937
|
+
const candidates = [
|
|
133938
|
+
join(here, "../python/alexa_worker.py"),
|
|
133939
|
+
join(here, "../../python/alexa_worker.py"),
|
|
133940
|
+
join(here, "../../../python/alexa_worker.py")
|
|
133941
|
+
];
|
|
133942
|
+
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
133943
|
+
throw new Error(`import-alexa: alexa_worker.py not found. Searched:\n${candidates.join("\n")}`);
|
|
133944
|
+
}
|
|
133945
|
+
process.platform, process.env["SystemRoot"];
|
|
133946
|
+
/**
|
|
133947
|
+
* Files only their owner may read: the one place a secret-bearing file is
|
|
133948
|
+
* restricted, on every OS.
|
|
133949
|
+
*
|
|
133950
|
+
* `mode: 0o600` is a POSIX idea. On Windows Node maps it to the read-only
|
|
133951
|
+
* attribute and nothing else, and `chmod` is a no-op for permissions, so a
|
|
133952
|
+
* "0600" `agent.json` (the mesh private key) would have
|
|
133953
|
+
* been readable by every local user. Windows has its own tool for this:
|
|
133954
|
+
* `icacls`. This module removes the inherited ACL entries and grants access to
|
|
133955
|
+
* the current user and SYSTEM only — the Windows reading of "0600".
|
|
133956
|
+
*
|
|
133957
|
+
* ZERO imports beyond Node built-ins, on purpose: the Electron shell bundles
|
|
133958
|
+
* this single file (see `packages/desktop/tsup.config.ts`) instead of the
|
|
133959
|
+
* whole `@camstack/types` barrel.
|
|
133960
|
+
*
|
|
133961
|
+
* Failure is NOT swallowed. A secret file that could not be restricted is a
|
|
133962
|
+
* secret file the caller must know about, so `OwnerOnlyError` propagates; the
|
|
133963
|
+
* write order below makes sure nothing sensitive is in the file yet when that
|
|
133964
|
+
* happens (the temp file is empty until it has been restricted).
|
|
133965
|
+
*
|
|
133966
|
+
* Out of scope on purpose: files that are only tidy, not secret (the job
|
|
133967
|
+
* journal, the addon manifest) keep a plain `mode`, since one `icacls` per
|
|
133968
|
+
* write would be a process spawn on a hot path.
|
|
133969
|
+
*/
|
|
133970
|
+
/** `*S-1-5-18` is SYSTEM in every locale — a name like "SYSTEM" is not. */
|
|
133971
|
+
var WINDOWS_SYSTEM_SID = "*S-1-5-18";
|
|
133972
|
+
var FILE_MODE = 384;
|
|
133973
|
+
var DIR_MODE = 448;
|
|
133974
|
+
var OwnerOnlyError = class extends Error {
|
|
133975
|
+
target;
|
|
133976
|
+
constructor(target, detail) {
|
|
133977
|
+
super(`could not restrict ${target} to its owner: ${detail}`);
|
|
133978
|
+
this.target = target;
|
|
133979
|
+
this.name = "OwnerOnlyError";
|
|
133980
|
+
}
|
|
133981
|
+
};
|
|
133982
|
+
function currentPrincipal() {
|
|
133983
|
+
const user = process.env["USERNAME"] ?? userInfo().username;
|
|
133984
|
+
const domain = process.env["USERDOMAIN"];
|
|
133985
|
+
return domain !== void 0 && domain.length > 0 ? `${domain}\\${user}` : user;
|
|
133986
|
+
}
|
|
133987
|
+
/** Resolved per call, not at import: a platform stubbed by a test must be seen. */
|
|
133988
|
+
function resolveDeps(overrides) {
|
|
133989
|
+
return {
|
|
133990
|
+
platform: process.platform,
|
|
133991
|
+
exec: (file, args) => {
|
|
133992
|
+
execFileSync(file, [...args], {
|
|
133993
|
+
stdio: "pipe",
|
|
133994
|
+
windowsHide: true
|
|
133995
|
+
});
|
|
133996
|
+
},
|
|
133997
|
+
principal: currentPrincipal,
|
|
133998
|
+
systemRoot: process.env["SystemRoot"],
|
|
133999
|
+
...overrides
|
|
134000
|
+
};
|
|
134001
|
+
}
|
|
134002
|
+
/** The icacls argv for one path: no inheritance, owner + SYSTEM only. */
|
|
134003
|
+
function icaclsArgs(target, principal, isDirectory) {
|
|
134004
|
+
const inherit = isDirectory ? "(OI)(CI)" : "";
|
|
134005
|
+
return [
|
|
134006
|
+
target,
|
|
134007
|
+
"/inheritance:r",
|
|
134008
|
+
"/grant:r",
|
|
134009
|
+
`${principal}:${inherit}F`,
|
|
134010
|
+
"/grant:r",
|
|
134011
|
+
`${WINDOWS_SYSTEM_SID}:${inherit}F`
|
|
134012
|
+
];
|
|
134013
|
+
}
|
|
134014
|
+
function restrictWindows(target, isDirectory, deps) {
|
|
134015
|
+
const icacls = path$1.win32.join(deps.systemRoot ?? "C:\\Windows", "System32", "icacls.exe");
|
|
134016
|
+
try {
|
|
134017
|
+
deps.exec(icacls, icaclsArgs(target, deps.principal(), isDirectory));
|
|
134018
|
+
} catch (err) {
|
|
134019
|
+
throw new OwnerOnlyError(target, err instanceof Error ? err.message : String(err));
|
|
134020
|
+
}
|
|
134021
|
+
}
|
|
134022
|
+
/** Restrict an existing file to its owner (chmod 0600 / owner+SYSTEM ACL). */
|
|
134023
|
+
function restrictFileToOwnerSync(target, overrides = {}) {
|
|
134024
|
+
const deps = resolveDeps(overrides);
|
|
134025
|
+
if (deps.platform === "win32") {
|
|
134026
|
+
restrictWindows(target, false, deps);
|
|
134027
|
+
return;
|
|
134028
|
+
}
|
|
134029
|
+
fs$1.chmodSync(target, FILE_MODE);
|
|
134030
|
+
}
|
|
134031
|
+
/** Restrict an existing directory to its owner (chmod 0700 / owner+SYSTEM, inherited). */
|
|
134032
|
+
function restrictDirToOwnerSync(target, overrides = {}) {
|
|
134033
|
+
const deps = resolveDeps(overrides);
|
|
134034
|
+
if (deps.platform === "win32") {
|
|
134035
|
+
restrictWindows(target, true, deps);
|
|
134036
|
+
return;
|
|
134037
|
+
}
|
|
134038
|
+
fs$1.chmodSync(target, DIR_MODE);
|
|
134039
|
+
}
|
|
134040
|
+
/**
|
|
134041
|
+
* Atomically write a secret-bearing file that only its owner can read.
|
|
134042
|
+
*
|
|
134043
|
+
* Order matters: the temp file is created EMPTY, restricted, and only then
|
|
134044
|
+
* given its content, so there is no moment at which the secret exists under a
|
|
134045
|
+
* permissive ACL. The rename carries the (already restricted) ACL with it.
|
|
134046
|
+
*/
|
|
134047
|
+
function writeOwnerOnlyFileSync(target, data, overrides = {}) {
|
|
134048
|
+
fs$1.mkdirSync(path$1.dirname(target), { recursive: true });
|
|
134049
|
+
const tmp = `${target}.tmp.${process.pid}`;
|
|
134050
|
+
fs$1.writeFileSync(tmp, "", { mode: FILE_MODE });
|
|
134051
|
+
try {
|
|
134052
|
+
restrictFileToOwnerSync(tmp, overrides);
|
|
134053
|
+
fs$1.writeFileSync(tmp, data);
|
|
134054
|
+
fs$1.renameSync(tmp, target);
|
|
134055
|
+
} catch (err) {
|
|
134056
|
+
fs$1.rmSync(tmp, { force: true });
|
|
134057
|
+
throw err;
|
|
134058
|
+
}
|
|
134059
|
+
}
|
|
134060
|
+
//#endregion
|
|
134061
|
+
//#region src/speak/secret-file.ts
|
|
134062
|
+
/**
|
|
134063
|
+
* Hand a password or a saved Amazon session to the Python worker as an
|
|
134064
|
+
* owner-only file. The path travels on stdin. The bytes never travel on
|
|
134065
|
+
* argv or in the environment, and they are removed when the call settles.
|
|
134066
|
+
*/
|
|
134067
|
+
var REDACTED = "‹redacted›";
|
|
134068
|
+
function decodeSession(sessionB64) {
|
|
134069
|
+
if (sessionB64.length === 0) return Buffer.alloc(0);
|
|
134070
|
+
return Buffer.from(sessionB64, "base64");
|
|
134071
|
+
}
|
|
134072
|
+
async function readSessionB64(sessionPath) {
|
|
134073
|
+
const bytes = await fs.readFile(sessionPath);
|
|
134074
|
+
if (bytes.length === 0) return "";
|
|
134075
|
+
return bytes.toString("base64");
|
|
134076
|
+
}
|
|
134077
|
+
/**
|
|
134078
|
+
* Create a 0700 directory, write the password and the session 0600, run
|
|
134079
|
+
* `body`, then delete the directory whether `body` succeeded or threw.
|
|
134080
|
+
* Returns the session file as base64 so the caller can store it in the keychain.
|
|
134081
|
+
*/
|
|
134082
|
+
async function withSpeakSecretFiles(input, body) {
|
|
134083
|
+
const dir = await fs.mkdtemp(path$1.join(os.tmpdir(), "camstack-alexa-"));
|
|
134084
|
+
const paths = {
|
|
134085
|
+
dir,
|
|
134086
|
+
passwordPath: path$1.join(dir, "password"),
|
|
134087
|
+
sessionPath: path$1.join(dir, "session")
|
|
134088
|
+
};
|
|
134089
|
+
try {
|
|
134090
|
+
restrictDirToOwnerSync(dir);
|
|
134091
|
+
writeOwnerOnlyFileSync(paths.passwordPath, input.password);
|
|
134092
|
+
writeOwnerOnlyFileSync(paths.sessionPath, decodeSession(input.sessionB64));
|
|
134093
|
+
await body(paths);
|
|
134094
|
+
return { sessionB64: await readSessionB64(paths.sessionPath) };
|
|
134095
|
+
} finally {
|
|
134096
|
+
await fs.rm(dir, {
|
|
134097
|
+
recursive: true,
|
|
134098
|
+
force: true
|
|
134099
|
+
});
|
|
134100
|
+
}
|
|
134101
|
+
}
|
|
134102
|
+
/** Replace every occurrence of each non-empty secret. Used before a log line is emitted. */
|
|
134103
|
+
function scrubSecrets(text, secrets) {
|
|
134104
|
+
let out = text;
|
|
134105
|
+
for (const secret of secrets) {
|
|
134106
|
+
if (secret.length === 0) continue;
|
|
134107
|
+
out = out.split(secret).join(REDACTED);
|
|
134108
|
+
}
|
|
134109
|
+
return out;
|
|
134110
|
+
}
|
|
134111
|
+
//#endregion
|
|
134112
|
+
//#region src/speak/setup-flow.ts
|
|
134113
|
+
/** Where the next screen is. A failed password stays on sign-in. A failed code stays on the code step. */
|
|
134114
|
+
function reduceSetup(phase, event) {
|
|
134115
|
+
if (event.type === "reset") return "sign-in";
|
|
134116
|
+
if (event.status === "ok") return "devices";
|
|
134117
|
+
if (event.status === "otp_needed") return "otp";
|
|
134118
|
+
if (event.status === "captcha") return "captcha";
|
|
134119
|
+
return phase;
|
|
134120
|
+
}
|
|
134121
|
+
//#endregion
|
|
134122
|
+
//#region src/speak/worker-client.ts
|
|
134123
|
+
/**
|
|
134124
|
+
* One Python process for the life of the addon. Requests are one JSON line
|
|
134125
|
+
* on stdin; responses are one JSON line on stdout, matched by id.
|
|
134126
|
+
* A crash restarts the process until the same bound CrashSupervisor uses,
|
|
134127
|
+
* then requests fail instead of looping.
|
|
134128
|
+
*/
|
|
134129
|
+
/** Pure: a new timestamp list, never a mutated one. */
|
|
134130
|
+
function recordWorkerCrash(times, now, windowMs, maxCrashes) {
|
|
134131
|
+
const cutoff = now - windowMs;
|
|
134132
|
+
const recent = [...times.filter((t) => t >= cutoff), now];
|
|
134133
|
+
if (recent.length >= maxCrashes) return {
|
|
134134
|
+
action: "failed",
|
|
134135
|
+
times: recent
|
|
134136
|
+
};
|
|
134137
|
+
return {
|
|
134138
|
+
action: "respawn",
|
|
134139
|
+
times: recent
|
|
134140
|
+
};
|
|
134141
|
+
}
|
|
134142
|
+
function quietLogger() {
|
|
134143
|
+
const logger = {
|
|
134144
|
+
debug() {},
|
|
134145
|
+
info() {},
|
|
134146
|
+
warn() {},
|
|
134147
|
+
error() {},
|
|
134148
|
+
child() {
|
|
134149
|
+
return logger;
|
|
134150
|
+
},
|
|
134151
|
+
withTags() {
|
|
134152
|
+
return logger;
|
|
134153
|
+
}
|
|
134154
|
+
};
|
|
134155
|
+
return logger;
|
|
134156
|
+
}
|
|
134157
|
+
function wrapChild(child) {
|
|
134158
|
+
const stdin = child.stdin;
|
|
134159
|
+
const stdout = child.stdout;
|
|
134160
|
+
const stderr = child.stderr;
|
|
134161
|
+
if (stdin === null || stdout === null || stderr === null) throw new Error("alexa worker spawn did not provide pipes");
|
|
134162
|
+
return {
|
|
134163
|
+
stdin: { write(chunk) {
|
|
134164
|
+
stdin.write(chunk);
|
|
134165
|
+
} },
|
|
134166
|
+
stdout: { on(event, listener) {
|
|
134167
|
+
stdout.on(event, listener);
|
|
134168
|
+
} },
|
|
134169
|
+
stderr: { on(event, listener) {
|
|
134170
|
+
stderr.on(event, listener);
|
|
134171
|
+
} },
|
|
134172
|
+
on(event, listener) {
|
|
134173
|
+
if (event === "close") child.on("close", listener);
|
|
134174
|
+
else child.on("error", listener);
|
|
134175
|
+
},
|
|
134176
|
+
kill(signal) {
|
|
134177
|
+
child.kill(signal);
|
|
134178
|
+
}
|
|
134179
|
+
};
|
|
134180
|
+
}
|
|
134181
|
+
function defaultSpawn(pythonPath, scriptPath, env) {
|
|
134182
|
+
return wrapChild(spawn(pythonPath, [scriptPath], {
|
|
134183
|
+
stdio: [
|
|
134184
|
+
"pipe",
|
|
134185
|
+
"pipe",
|
|
134186
|
+
"pipe"
|
|
134187
|
+
],
|
|
134188
|
+
env
|
|
134189
|
+
}));
|
|
134190
|
+
}
|
|
134191
|
+
function startAlexaWorker(deps) {
|
|
134192
|
+
const logger = deps.logger ?? quietLogger();
|
|
134193
|
+
const now = deps.now ?? (() => Date.now());
|
|
134194
|
+
const schedule = deps.schedule ?? ((fn, delayMs) => {
|
|
134195
|
+
setTimeout(fn, delayMs);
|
|
134196
|
+
});
|
|
134197
|
+
const spawnChild = deps.spawnChild ?? defaultSpawn;
|
|
134198
|
+
const maxCrashes = deps.maxCrashes ?? 5;
|
|
134199
|
+
const windowMs = deps.windowMs ?? 6e4;
|
|
134200
|
+
const env = deps.env ?? process.env;
|
|
134201
|
+
let child = null;
|
|
134202
|
+
let buffer = "";
|
|
134203
|
+
let nextId = 1;
|
|
134204
|
+
let stopped = false;
|
|
134205
|
+
let failed = false;
|
|
134206
|
+
let crashTimes = [];
|
|
134207
|
+
let secrets = [];
|
|
134208
|
+
let pending = null;
|
|
134209
|
+
const queue = [];
|
|
134210
|
+
function scrub(text) {
|
|
134211
|
+
return scrubSecrets(text, secrets);
|
|
134212
|
+
}
|
|
134213
|
+
function failPending(err) {
|
|
134214
|
+
const current = pending;
|
|
134215
|
+
pending = null;
|
|
134216
|
+
current?.reject(err);
|
|
134217
|
+
}
|
|
134218
|
+
function attach(spawned) {
|
|
134219
|
+
child = spawned;
|
|
134220
|
+
buffer = "";
|
|
134221
|
+
let settled = false;
|
|
134222
|
+
const finish = (code) => {
|
|
134223
|
+
if (settled || child !== spawned) return;
|
|
134224
|
+
settled = true;
|
|
134225
|
+
onExit(code);
|
|
134226
|
+
};
|
|
134227
|
+
spawned.stdout.on("data", (chunk) => {
|
|
134228
|
+
buffer += chunk.toString("utf8");
|
|
134229
|
+
const lines = buffer.split("\n");
|
|
134230
|
+
buffer = lines.pop() ?? "";
|
|
134231
|
+
for (const line of lines) {
|
|
134232
|
+
if (line.trim().length === 0) continue;
|
|
134233
|
+
acceptLine(line);
|
|
134234
|
+
}
|
|
134235
|
+
});
|
|
134236
|
+
spawned.stderr.on("data", (chunk) => {
|
|
134237
|
+
const text = scrub(chunk.toString("utf8")).trim();
|
|
134238
|
+
if (text.length === 0) return;
|
|
134239
|
+
logger.warn("alexa worker stderr", { meta: { text: text.slice(0, 500) } });
|
|
134240
|
+
});
|
|
134241
|
+
spawned.on("error", (err) => {
|
|
134242
|
+
logger.warn("alexa worker spawn error", { meta: { error: scrub(err.message) } });
|
|
134243
|
+
finish(null);
|
|
134244
|
+
});
|
|
134245
|
+
spawned.on("close", (code) => {
|
|
134246
|
+
finish(code);
|
|
134247
|
+
});
|
|
134248
|
+
}
|
|
134249
|
+
function acceptLine(line) {
|
|
134250
|
+
let parsed;
|
|
134251
|
+
try {
|
|
134252
|
+
parsed = JSON.parse(line);
|
|
134253
|
+
} catch (err) {
|
|
134254
|
+
logger.warn("alexa worker non-JSON line", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134255
|
+
return;
|
|
134256
|
+
}
|
|
134257
|
+
const checked = WorkerLineSchema.safeParse(parsed);
|
|
134258
|
+
if (!checked.success) {
|
|
134259
|
+
logger.warn("alexa worker line failed schema", { meta: { error: checked.error.message } });
|
|
134260
|
+
return;
|
|
134261
|
+
}
|
|
134262
|
+
const message = checked.data;
|
|
134263
|
+
if (pending === null || pending.id !== message.id) {
|
|
134264
|
+
logger.warn("alexa worker line for an unknown request", { meta: { id: message.id } });
|
|
134265
|
+
return;
|
|
134266
|
+
}
|
|
134267
|
+
const current = pending;
|
|
134268
|
+
pending = null;
|
|
134269
|
+
if (message.ok) current.resolve(message.result);
|
|
134270
|
+
else current.reject(new Error(scrub(message.error)));
|
|
134271
|
+
pump();
|
|
134272
|
+
}
|
|
134273
|
+
function onExit(code) {
|
|
134274
|
+
child = null;
|
|
134275
|
+
if (stopped) {
|
|
134276
|
+
failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134277
|
+
return;
|
|
134278
|
+
}
|
|
134279
|
+
failPending(/* @__PURE__ */ new Error(`alexa worker exited (${code ?? "null"})`));
|
|
134280
|
+
const decision = recordWorkerCrash(crashTimes, now(), windowMs, maxCrashes);
|
|
134281
|
+
crashTimes = decision.times;
|
|
134282
|
+
if (decision.action === "failed") {
|
|
134283
|
+
failed = true;
|
|
134284
|
+
logger.error("alexa worker restart bound reached", { meta: { crashes: crashTimes.length } });
|
|
134285
|
+
const waiting = queue.splice(0, queue.length);
|
|
134286
|
+
for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
|
|
134287
|
+
return;
|
|
134288
|
+
}
|
|
134289
|
+
logger.warn("alexa worker exited; restarting", { meta: {
|
|
134290
|
+
code,
|
|
134291
|
+
crashes: crashTimes.length
|
|
134292
|
+
} });
|
|
134293
|
+
schedule(() => {
|
|
134294
|
+
if (stopped || failed) return;
|
|
134295
|
+
launch();
|
|
134296
|
+
pump();
|
|
134297
|
+
}, Math.min(3e4, 500 * 2 ** Math.min(6, crashTimes.length - 1)));
|
|
134298
|
+
}
|
|
134299
|
+
function launch() {
|
|
134300
|
+
if (stopped || failed) return;
|
|
134301
|
+
attach(spawnChild(deps.pythonPath, deps.scriptPath, env));
|
|
134302
|
+
}
|
|
134303
|
+
function pump() {
|
|
134304
|
+
if (pending !== null || child === null || failed || stopped) return;
|
|
134305
|
+
const next = queue.shift();
|
|
134306
|
+
if (next === void 0) return;
|
|
134307
|
+
pending = next.pending;
|
|
134308
|
+
child.stdin.write(requestLine(next.pending.id, next.request));
|
|
134309
|
+
}
|
|
134310
|
+
function request(body) {
|
|
134311
|
+
if (failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
|
|
134312
|
+
if (stopped) return Promise.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134313
|
+
const id = String(nextId);
|
|
134314
|
+
nextId += 1;
|
|
134315
|
+
return new Promise((resolve, reject) => {
|
|
134316
|
+
queue.push({
|
|
134317
|
+
request: body,
|
|
134318
|
+
pending: {
|
|
134319
|
+
id,
|
|
134320
|
+
resolve,
|
|
134321
|
+
reject
|
|
134322
|
+
}
|
|
134323
|
+
});
|
|
134324
|
+
if (child === null) launch();
|
|
134325
|
+
pump();
|
|
134326
|
+
});
|
|
134327
|
+
}
|
|
134328
|
+
return {
|
|
134329
|
+
request,
|
|
134330
|
+
noteSecrets(next) {
|
|
134331
|
+
secrets = [...next];
|
|
134332
|
+
},
|
|
134333
|
+
stop() {
|
|
134334
|
+
stopped = true;
|
|
134335
|
+
const current = child;
|
|
134336
|
+
child = null;
|
|
134337
|
+
current?.kill("SIGTERM");
|
|
134338
|
+
failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134339
|
+
const waiting = queue.splice(0, queue.length);
|
|
134340
|
+
for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134341
|
+
},
|
|
134342
|
+
get failed() {
|
|
134343
|
+
return failed;
|
|
134344
|
+
},
|
|
134345
|
+
get crashesInWindow() {
|
|
134346
|
+
return crashTimes.length;
|
|
134347
|
+
}
|
|
134348
|
+
};
|
|
134349
|
+
}
|
|
134350
|
+
//#endregion
|
|
134351
|
+
//#region src/speak/controller.ts
|
|
134352
|
+
/**
|
|
134353
|
+
* Owns the one Python worker, the keychain session, the Echo cache and the
|
|
134354
|
+
* notification provider. A login that needs a code raises one alert.
|
|
134355
|
+
*/
|
|
134356
|
+
function targetsFrom(settings) {
|
|
134357
|
+
return settings.targets.map((row) => ({
|
|
134358
|
+
id: row.id,
|
|
134359
|
+
name: row.name,
|
|
134360
|
+
kind: ALEXA_SPEAK_KIND,
|
|
134361
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
134362
|
+
enabled: row.enabled,
|
|
134363
|
+
config: {
|
|
134364
|
+
echoId: row.echoId,
|
|
134365
|
+
mode: row.mode,
|
|
134366
|
+
volume: row.volume,
|
|
134367
|
+
respectDnd: row.respectDnd
|
|
134368
|
+
}
|
|
134369
|
+
}));
|
|
134370
|
+
}
|
|
134371
|
+
function withTargets(settings, targets) {
|
|
134372
|
+
return {
|
|
134373
|
+
...settings,
|
|
134374
|
+
targets: targets.map((target) => ({
|
|
134375
|
+
id: target.id,
|
|
134376
|
+
name: target.name,
|
|
134377
|
+
enabled: target.enabled,
|
|
134378
|
+
echoId: typeof target.config["echoId"] === "string" ? target.config["echoId"] : "",
|
|
134379
|
+
mode: target.config["mode"] === "announce" ? "announce" : "speak",
|
|
134380
|
+
volume: typeof target.config["volume"] === "number" ? target.config["volume"] : 40,
|
|
134381
|
+
respectDnd: target.config["respectDnd"] !== false
|
|
134382
|
+
}))
|
|
134383
|
+
};
|
|
134384
|
+
}
|
|
134385
|
+
function usablePassword(value) {
|
|
134386
|
+
if (value.length === 0 || isSecretMarker(value)) return "";
|
|
134387
|
+
return value;
|
|
134388
|
+
}
|
|
134389
|
+
var AlexaSpeakController = class {
|
|
134390
|
+
host;
|
|
134391
|
+
provider;
|
|
134392
|
+
settings;
|
|
134393
|
+
worker = null;
|
|
134394
|
+
devices = [];
|
|
134395
|
+
phase = "sign-in";
|
|
134396
|
+
timer = null;
|
|
134397
|
+
attention;
|
|
134398
|
+
now;
|
|
134399
|
+
constructor(host) {
|
|
134400
|
+
this.host = host;
|
|
134401
|
+
this.now = host.now ?? (() => Date.now());
|
|
134402
|
+
this.settings = readSpeakSettings(host.readConfig());
|
|
134403
|
+
this.phase = this.settings.session.length > 0 ? "devices" : "sign-in";
|
|
134404
|
+
this.attention = new OtpAttention({
|
|
134405
|
+
now: this.now,
|
|
134406
|
+
emit: (alert) => host.emitAlert(alert),
|
|
134407
|
+
resolve: (id) => host.resolveAlert(id)
|
|
134408
|
+
});
|
|
134409
|
+
this.provider = createAlexaNotificationProvider({
|
|
134410
|
+
devices: () => this.devices,
|
|
134411
|
+
loggedIn: () => this.phase === "devices",
|
|
134412
|
+
now: this.now,
|
|
134413
|
+
logger: host.logger,
|
|
134414
|
+
loadTargets: () => targetsFrom(this.settings),
|
|
134415
|
+
saveTargets: (targets) => {
|
|
134416
|
+
this.settings = withTargets(this.settings, targets);
|
|
134417
|
+
this.persist();
|
|
134418
|
+
},
|
|
134419
|
+
speak: (command) => this.utter(command.mode, command.targets, command.text, command.volume)
|
|
134420
|
+
});
|
|
134421
|
+
}
|
|
134422
|
+
async start() {
|
|
134423
|
+
const pythonPath = await this.host.ensurePython();
|
|
134424
|
+
if (pythonPath === null) {
|
|
134425
|
+
this.host.logger.warn("alexa speak: embedded Python unavailable; Echo speech will not run");
|
|
134426
|
+
return;
|
|
134427
|
+
}
|
|
134428
|
+
this.worker = startAlexaWorker({
|
|
134429
|
+
pythonPath,
|
|
134430
|
+
scriptPath: resolveAlexaWorkerScript(),
|
|
134431
|
+
logger: this.host.logger,
|
|
134432
|
+
now: this.now
|
|
134433
|
+
});
|
|
134434
|
+
this.noteSecrets();
|
|
134435
|
+
if (this.settings.session.length > 0) await this.loginWithFiles().catch((err) => {
|
|
134436
|
+
this.host.logger.warn("alexa speak: stored session did not restore", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134437
|
+
});
|
|
134438
|
+
this.timer = setInterval(() => {
|
|
134439
|
+
this.refreshDevices();
|
|
134440
|
+
}, DEVICE_REFRESH_MS);
|
|
134441
|
+
}
|
|
134442
|
+
stop() {
|
|
134443
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
134444
|
+
this.timer = null;
|
|
134445
|
+
this.worker?.stop();
|
|
134446
|
+
this.worker = null;
|
|
134447
|
+
}
|
|
134448
|
+
handlers() {
|
|
134449
|
+
return {
|
|
134450
|
+
speakStatus: async () => this.status(),
|
|
134451
|
+
loginStart: async (input) => {
|
|
134452
|
+
this.settings = {
|
|
134453
|
+
...this.settings,
|
|
134454
|
+
email: input.email,
|
|
134455
|
+
password: input.password,
|
|
134456
|
+
region: input.region
|
|
134457
|
+
};
|
|
134458
|
+
await this.persist();
|
|
134459
|
+
this.noteSecrets();
|
|
134460
|
+
return this.loginWithFiles();
|
|
134461
|
+
},
|
|
134462
|
+
loginOtp: async (input) => this.otpWithFiles(input.code),
|
|
134463
|
+
sendTest: async (input) => this.sendTest(input)
|
|
134464
|
+
};
|
|
134465
|
+
}
|
|
134466
|
+
noteSecrets() {
|
|
134467
|
+
const password = usablePassword(this.settings.password);
|
|
134468
|
+
this.worker?.noteSecrets(password.length > 0 ? [password] : []);
|
|
134469
|
+
}
|
|
134470
|
+
async persist() {
|
|
134471
|
+
try {
|
|
134472
|
+
await this.host.writeSpeak(this.settings);
|
|
134473
|
+
} catch (err) {
|
|
134474
|
+
this.host.logger.warn("alexa speak: could not store settings", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134475
|
+
}
|
|
134476
|
+
}
|
|
134477
|
+
workerRequest(request) {
|
|
134478
|
+
const worker = this.worker;
|
|
134479
|
+
if (worker === null || worker.failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker is not running"));
|
|
134480
|
+
return worker.request(request);
|
|
134481
|
+
}
|
|
134482
|
+
async loginWithFiles() {
|
|
134483
|
+
let outcome = {
|
|
134484
|
+
status: "failed",
|
|
134485
|
+
error: "alexa worker is not running"
|
|
134486
|
+
};
|
|
134487
|
+
const held = await withSpeakSecretFiles({
|
|
134488
|
+
password: usablePassword(this.settings.password),
|
|
134489
|
+
sessionB64: this.settings.session
|
|
134490
|
+
}, async (paths) => {
|
|
134491
|
+
outcome = await this.applyLogin(await this.loginRequest(paths));
|
|
134492
|
+
});
|
|
134493
|
+
if (held.sessionB64 !== this.settings.session) {
|
|
134494
|
+
this.settings = {
|
|
134495
|
+
...this.settings,
|
|
134496
|
+
session: held.sessionB64
|
|
134497
|
+
};
|
|
134498
|
+
await this.persist();
|
|
134499
|
+
}
|
|
134500
|
+
return outcome;
|
|
134501
|
+
}
|
|
134502
|
+
async otpWithFiles(code) {
|
|
134503
|
+
let outcome = {
|
|
134504
|
+
status: "failed",
|
|
134505
|
+
error: "alexa worker is not running"
|
|
134506
|
+
};
|
|
134507
|
+
const held = await withSpeakSecretFiles({
|
|
134508
|
+
password: "",
|
|
134509
|
+
sessionB64: this.settings.session
|
|
134510
|
+
}, async (paths) => {
|
|
134511
|
+
outcome = await this.applyLogin(await this.workerRequest({
|
|
134512
|
+
op: "login_otp",
|
|
134513
|
+
code,
|
|
134514
|
+
sessionPath: paths.sessionPath
|
|
134515
|
+
}));
|
|
134516
|
+
});
|
|
134517
|
+
if (held.sessionB64 !== this.settings.session) {
|
|
134518
|
+
this.settings = {
|
|
134519
|
+
...this.settings,
|
|
134520
|
+
session: held.sessionB64
|
|
134521
|
+
};
|
|
134522
|
+
await this.persist();
|
|
134523
|
+
}
|
|
134524
|
+
return outcome;
|
|
134525
|
+
}
|
|
134526
|
+
loginRequest(paths) {
|
|
134527
|
+
return this.workerRequest({
|
|
134528
|
+
op: "login_start",
|
|
134529
|
+
email: this.settings.email,
|
|
134530
|
+
passwordPath: paths.passwordPath,
|
|
134531
|
+
sessionPath: paths.sessionPath,
|
|
134532
|
+
region: this.settings.region
|
|
134533
|
+
});
|
|
134534
|
+
}
|
|
134535
|
+
async applyLogin(raw) {
|
|
134536
|
+
const outcome = parseLoginResult(raw);
|
|
134537
|
+
this.phase = reduceSetup(this.phase, {
|
|
134538
|
+
type: "login",
|
|
134539
|
+
status: outcome.status
|
|
134540
|
+
});
|
|
134541
|
+
if (outcome.status === "otp_needed") await this.attention.needed("otp");
|
|
134542
|
+
else if (outcome.status === "captcha") await this.attention.needed("captcha");
|
|
134543
|
+
else if (outcome.status === "ok") {
|
|
134544
|
+
await this.attention.cleared();
|
|
134545
|
+
await this.refreshDevices();
|
|
134546
|
+
}
|
|
134547
|
+
return outcome;
|
|
134548
|
+
}
|
|
134549
|
+
async refreshDevices() {
|
|
134550
|
+
if (this.worker === null || this.phase !== "devices") return;
|
|
134551
|
+
try {
|
|
134552
|
+
this.devices = [...parseDevices(await this.workerRequest({ op: "devices" }))];
|
|
134553
|
+
} catch (err) {
|
|
134554
|
+
this.host.logger.warn("alexa speak: device refresh failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134555
|
+
}
|
|
134556
|
+
}
|
|
134557
|
+
async utter(mode, targets, text, volume) {
|
|
134558
|
+
const op = mode === "announce" ? "announce" : "speak";
|
|
134559
|
+
return parseDeliveries(await this.workerRequest({
|
|
134560
|
+
op,
|
|
134561
|
+
targets,
|
|
134562
|
+
text,
|
|
134563
|
+
volume
|
|
134564
|
+
}));
|
|
134565
|
+
}
|
|
134566
|
+
async session() {
|
|
134567
|
+
if (this.worker === null) return {
|
|
134568
|
+
loggedIn: false,
|
|
134569
|
+
ageSec: 0,
|
|
134570
|
+
lastSuccessAt: null
|
|
134571
|
+
};
|
|
134572
|
+
try {
|
|
134573
|
+
return parseSessionStatus(await this.workerRequest({ op: "session_status" }));
|
|
134574
|
+
} catch {
|
|
134575
|
+
return {
|
|
134576
|
+
loggedIn: this.phase === "devices",
|
|
134577
|
+
ageSec: 0,
|
|
134578
|
+
lastSuccessAt: null
|
|
134579
|
+
};
|
|
134580
|
+
}
|
|
134581
|
+
}
|
|
134582
|
+
async status() {
|
|
134583
|
+
return {
|
|
134584
|
+
phase: this.phase,
|
|
134585
|
+
email: this.settings.email,
|
|
134586
|
+
region: this.settings.region,
|
|
134587
|
+
devices: [...this.devices],
|
|
134588
|
+
session: await this.session()
|
|
134589
|
+
};
|
|
134590
|
+
}
|
|
134591
|
+
async sendTest(input) {
|
|
134592
|
+
const outcomes = await this.utter(input.mode, input.echoIds, input.text, input.volume);
|
|
134593
|
+
const known = new Map(this.devices.map((device) => [device.id, device]));
|
|
134594
|
+
const next = [...targetsFrom(this.settings)];
|
|
134595
|
+
for (const echoId of input.echoIds) {
|
|
134596
|
+
if (next.some((row) => row.config["echoId"] === echoId)) continue;
|
|
134597
|
+
const device = known.get(echoId);
|
|
134598
|
+
next.push({
|
|
134599
|
+
id: `echo-${echoId}`,
|
|
134600
|
+
name: device?.name ?? echoId,
|
|
134601
|
+
kind: ALEXA_SPEAK_KIND,
|
|
134602
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
134603
|
+
enabled: true,
|
|
134604
|
+
config: {
|
|
134605
|
+
echoId,
|
|
134606
|
+
mode: input.mode,
|
|
134607
|
+
volume: input.volume,
|
|
134608
|
+
respectDnd: true
|
|
134609
|
+
}
|
|
134610
|
+
});
|
|
134611
|
+
}
|
|
134612
|
+
this.settings = withTargets(this.settings, next);
|
|
134613
|
+
await this.persist();
|
|
134614
|
+
return { outcomes: outcomes.map((row) => ({
|
|
134615
|
+
echoId: row.targetId,
|
|
134616
|
+
ok: row.ok,
|
|
134617
|
+
...row.error !== void 0 ? { error: row.error } : {}
|
|
134618
|
+
})) };
|
|
134619
|
+
}
|
|
134620
|
+
};
|
|
134621
|
+
//#endregion
|
|
132682
134622
|
//#region src/addon.ts
|
|
134623
|
+
var SPEAK_PAGE = {
|
|
134624
|
+
id: "speak",
|
|
134625
|
+
label: "Speak on Echo",
|
|
134626
|
+
icon: "megaphone",
|
|
134627
|
+
path: "/addon/import-alexa",
|
|
134628
|
+
remoteName: "addon_import_alexa_page",
|
|
134629
|
+
bundle: "remoteEntry.js",
|
|
134630
|
+
section: "administration"
|
|
134631
|
+
};
|
|
132683
134632
|
/** Default config — a fresh install starts with no accounts. */
|
|
132684
134633
|
var DEFAULTS = {
|
|
132685
134634
|
brokers: [],
|
|
132686
134635
|
proxyLogin: {
|
|
132687
134636
|
proxyHost: "",
|
|
132688
134637
|
proxyPort: 3456
|
|
134638
|
+
},
|
|
134639
|
+
speak: {
|
|
134640
|
+
email: "",
|
|
134641
|
+
password: "",
|
|
134642
|
+
region: "EU",
|
|
134643
|
+
session: "",
|
|
134644
|
+
targets: []
|
|
132689
134645
|
}
|
|
132690
134646
|
};
|
|
132691
134647
|
/** Device types an imported Alexa appliance can materialise as. */
|
|
@@ -132727,6 +134683,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132727
134683
|
proxyLogin = null;
|
|
132728
134684
|
brokerStore = null;
|
|
132729
134685
|
activeBrokers = [];
|
|
134686
|
+
speak = null;
|
|
134687
|
+
secretStatePaths = ["speak.password", "speak.session"];
|
|
132730
134688
|
constructor() {
|
|
132731
134689
|
super({ ...DEFAULTS });
|
|
132732
134690
|
}
|
|
@@ -132780,21 +134738,62 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132780
134738
|
proxyLogin: this.proxyLogin,
|
|
132781
134739
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
132782
134740
|
});
|
|
132783
|
-
|
|
132784
|
-
|
|
132785
|
-
|
|
132786
|
-
|
|
132787
|
-
|
|
134741
|
+
const speak = new AlexaSpeakController({
|
|
134742
|
+
logger: this.ctx.logger.child("speak"),
|
|
134743
|
+
readConfig: () => this.config,
|
|
134744
|
+
writeSpeak: async (next) => {
|
|
134745
|
+
await this.updateGlobalSettings({ speak: next });
|
|
132788
134746
|
},
|
|
132789
|
-
{
|
|
132790
|
-
|
|
132791
|
-
|
|
134747
|
+
emitAlert: async (alert) => {
|
|
134748
|
+
const emit = this.ctx.api.alerts?.emit;
|
|
134749
|
+
if (emit === void 0) {
|
|
134750
|
+
this.ctx.logger.warn("alexa speak: OTP alert skipped — alerts capability missing");
|
|
134751
|
+
return;
|
|
134752
|
+
}
|
|
134753
|
+
await emit.mutate(alert);
|
|
132792
134754
|
},
|
|
132793
|
-
{
|
|
132794
|
-
|
|
132795
|
-
|
|
132796
|
-
|
|
132797
|
-
|
|
134755
|
+
resolveAlert: async (alertId) => {
|
|
134756
|
+
const dismiss = this.ctx.api.alerts?.dismiss;
|
|
134757
|
+
if (dismiss === void 0) return;
|
|
134758
|
+
await dismiss.mutate({ alertId });
|
|
134759
|
+
},
|
|
134760
|
+
ensurePython: () => this.ctx.deps.ensurePython()
|
|
134761
|
+
});
|
|
134762
|
+
this.speak = speak;
|
|
134763
|
+
speak.start().catch((err) => {
|
|
134764
|
+
this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134765
|
+
});
|
|
134766
|
+
const pages = {
|
|
134767
|
+
id: this.addonId,
|
|
134768
|
+
listPages: () => [SPEAK_PAGE]
|
|
134769
|
+
};
|
|
134770
|
+
return {
|
|
134771
|
+
providers: [
|
|
134772
|
+
...regs,
|
|
134773
|
+
{
|
|
134774
|
+
capability: brokerCapability,
|
|
134775
|
+
provider: this.buildBrokerProvider()
|
|
134776
|
+
},
|
|
134777
|
+
{
|
|
134778
|
+
capability: deviceAdoptionCapability,
|
|
134779
|
+
provider: this.buildAdoptionProvider()
|
|
134780
|
+
},
|
|
134781
|
+
{
|
|
134782
|
+
capability: addonRoutesCapability,
|
|
134783
|
+
provider: buildAddonRouteProvider("import-alexa", loginRoutes)
|
|
134784
|
+
},
|
|
134785
|
+
{
|
|
134786
|
+
capability: notificationOutputCapability,
|
|
134787
|
+
provider: speak.provider
|
|
134788
|
+
},
|
|
134789
|
+
{
|
|
134790
|
+
capability: addonPagesSourceCapability,
|
|
134791
|
+
provider: pages
|
|
134792
|
+
}
|
|
134793
|
+
],
|
|
134794
|
+
customActions: alexaSpeakActions,
|
|
134795
|
+
actionHandlers: speak.handlers()
|
|
134796
|
+
};
|
|
132798
134797
|
}
|
|
132799
134798
|
async loadPersistedBrokers() {
|
|
132800
134799
|
const store = this.brokerStore;
|
|
@@ -132859,6 +134858,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132859
134858
|
}
|
|
132860
134859
|
}
|
|
132861
134860
|
async onShutdown() {
|
|
134861
|
+
this.speak?.stop();
|
|
134862
|
+
this.speak = null;
|
|
132862
134863
|
alexaCommands.clear();
|
|
132863
134864
|
try {
|
|
132864
134865
|
await this.proxyLogin?.dispose();
|
|
@@ -133093,43 +135094,90 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
133093
135094
|
});
|
|
133094
135095
|
}
|
|
133095
135096
|
globalSettingsSchema() {
|
|
133096
|
-
return this.schema({ sections: [
|
|
133097
|
-
|
|
133098
|
-
|
|
133099
|
-
|
|
133100
|
-
|
|
133101
|
-
|
|
133102
|
-
|
|
133103
|
-
|
|
133104
|
-
|
|
133105
|
-
|
|
133106
|
-
|
|
133107
|
-
|
|
133108
|
-
|
|
133109
|
-
|
|
133110
|
-
|
|
133111
|
-
|
|
133112
|
-
|
|
133113
|
-
|
|
133114
|
-
|
|
133115
|
-
|
|
133116
|
-
|
|
133117
|
-
|
|
133118
|
-
|
|
133119
|
-
|
|
133120
|
-
|
|
133121
|
-
|
|
133122
|
-
|
|
133123
|
-
|
|
133124
|
-
|
|
133125
|
-
|
|
133126
|
-
|
|
133127
|
-
|
|
133128
|
-
|
|
133129
|
-
|
|
133130
|
-
|
|
133131
|
-
|
|
133132
|
-
|
|
135097
|
+
return this.schema({ sections: [
|
|
135098
|
+
{
|
|
135099
|
+
id: "alexa-account",
|
|
135100
|
+
title: "Amazon Alexa account",
|
|
135101
|
+
titleKey: "import-alexa:globalSettings.s_alexa-account.title",
|
|
135102
|
+
description: "Alexa accounts are managed in the External systems → Brokers tab.",
|
|
135103
|
+
descriptionKey: "import-alexa:globalSettings.s_alexa-account.description",
|
|
135104
|
+
columns: 1,
|
|
135105
|
+
fields: [{
|
|
135106
|
+
type: "info",
|
|
135107
|
+
key: "accountHelp",
|
|
135108
|
+
label: "Accounts are managed separately",
|
|
135109
|
+
labelKey: "import-alexa:globalSettings.s_alexa-account.accountHelp.label",
|
|
135110
|
+
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."
|
|
135111
|
+
}]
|
|
135112
|
+
},
|
|
135113
|
+
{
|
|
135114
|
+
id: "alexa-proxy-login",
|
|
135115
|
+
title: "Interactive login (proxy)",
|
|
135116
|
+
titleKey: "import-alexa:globalSettings.s_alexa-proxy-login.title",
|
|
135117
|
+
description: "The \"Login to Amazon\" button in onboarding opens a local proxy that captures the cookie after you complete the real Amazon login (incl. 2FA). Because Amazon URLs are rewritten to this exact host:port, it must be a directly-reachable address — leave the host blank to auto-detect the server IP, and make sure the port is reachable from the browser you log in from.",
|
|
135118
|
+
descriptionKey: "import-alexa:globalSettings.s_alexa-proxy-login.description",
|
|
135119
|
+
columns: 2,
|
|
135120
|
+
fields: [{
|
|
135121
|
+
type: "text",
|
|
135122
|
+
key: "proxyLogin.proxyHost",
|
|
135123
|
+
label: "Login proxy host/IP (blank = auto-detect)",
|
|
135124
|
+
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyHost.label",
|
|
135125
|
+
placeholder: "auto-detect"
|
|
135126
|
+
}, {
|
|
135127
|
+
type: "number",
|
|
135128
|
+
key: "proxyLogin.proxyPort",
|
|
135129
|
+
label: "Login proxy port",
|
|
135130
|
+
labelKey: "import-alexa:globalSettings.s_alexa-proxy-login.proxyLogin_proxyPort.label",
|
|
135131
|
+
min: 1,
|
|
135132
|
+
max: 65535,
|
|
135133
|
+
default: 3456
|
|
135134
|
+
}]
|
|
135135
|
+
},
|
|
135136
|
+
{
|
|
135137
|
+
id: "alexa-speak",
|
|
135138
|
+
title: "Speak on Echo",
|
|
135139
|
+
titleKey: "import-alexa:globalSettings.s_alexa-speak.title",
|
|
135140
|
+
description: "Sign in on the Speak on Echo page, pick the devices, and use them as a notification target. The text is the rule template, read aloud.",
|
|
135141
|
+
descriptionKey: "import-alexa:globalSettings.s_alexa-speak.description",
|
|
135142
|
+
columns: 1,
|
|
135143
|
+
fields: [
|
|
135144
|
+
{
|
|
135145
|
+
type: "text",
|
|
135146
|
+
key: "speak.email",
|
|
135147
|
+
label: "Amazon email",
|
|
135148
|
+
labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_email.label",
|
|
135149
|
+
inputType: "email"
|
|
135150
|
+
},
|
|
135151
|
+
{
|
|
135152
|
+
type: "password",
|
|
135153
|
+
key: "speak.password",
|
|
135154
|
+
label: "Amazon password",
|
|
135155
|
+
labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_password.label"
|
|
135156
|
+
},
|
|
135157
|
+
{
|
|
135158
|
+
type: "select",
|
|
135159
|
+
key: "speak.region",
|
|
135160
|
+
label: "Region",
|
|
135161
|
+
labelKey: "import-alexa:globalSettings.s_alexa-speak.speak_region.label",
|
|
135162
|
+
default: "EU",
|
|
135163
|
+
options: [
|
|
135164
|
+
{
|
|
135165
|
+
value: "NA",
|
|
135166
|
+
label: "North America"
|
|
135167
|
+
},
|
|
135168
|
+
{
|
|
135169
|
+
value: "EU",
|
|
135170
|
+
label: "Europe"
|
|
135171
|
+
},
|
|
135172
|
+
{
|
|
135173
|
+
value: "FE",
|
|
135174
|
+
label: "Far East"
|
|
135175
|
+
}
|
|
135176
|
+
]
|
|
135177
|
+
}
|
|
135178
|
+
]
|
|
135179
|
+
}
|
|
135180
|
+
] });
|
|
133133
135181
|
}
|
|
133134
135182
|
async supportsManualCreation() {
|
|
133135
135183
|
return false;
|
|
@@ -133163,4 +135211,4 @@ function capsEqual(a, b) {
|
|
|
133163
135211
|
return true;
|
|
133164
135212
|
}
|
|
133165
135213
|
//#endregion
|
|
133166
|
-
export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
|
|
135214
|
+
export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, alexaSpeakActions as customActions, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
|