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