@camstack/addon-import-alexa 0.2.148 → 0.2.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/assets/alexa-echo.LICENSE.md +13 -0
- package/assets/alexa-echo.svg +1 -0
- package/dist/MaskShapeCanvas-B6q5RDLi.mjs +13807 -0
- package/dist/MotionZonesSettings-Dxo0EAzN.mjs +309 -0
- package/dist/PrivacyMaskSettings-DwWxpZrj.mjs +368 -0
- package/dist/SceneMonitorEditor-CC0dk5TR.mjs +846 -0
- package/dist/_stub.js +39566 -0
- package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_import_alexa_page-CcQjQJmj.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-Msberbw_.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 +2708 -149
- package/dist/addon.mjs +2724 -168
- package/dist/hostInit-CRUwUtSm.mjs +129 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +2 -2
- package/dist/player-overlays-DNpsEqAe.mjs +51 -0
- package/dist/remoteEntry.js +2 -0
- package/dist/remoteEntry.ssr.js +33 -0
- package/dist/responsive-B82v3AbM.mjs +125 -0
- package/dist/rolldown-runtime-HEgqtunE.mjs +20 -0
- package/dist/scene-monitor-copy-CPnjwzw6.mjs +227 -0
- package/dist/square-YTl_L1_3.mjs +45 -0
- package/dist/trash-2-DCHugLVt.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-DMdgQOgz.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 +8 -1
- package/i18n/it.json +8 -1
- package/package.json +26 -4
- package/python/NOTICE +4 -0
- package/python/alexa_gateway.py +226 -0
- package/python/alexa_protocol.py +115 -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_gateway.py +141 -0
- package/python/test_alexa_protocol.py +177 -0
package/dist/addon.mjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { networkInterfaces, userInfo } from "node:os";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { existsSync, promises } from "node:fs";
|
|
7
|
+
import * as path$1 from "node:path";
|
|
8
|
+
import path, { dirname, join } from "node:path";
|
|
9
|
+
import * as fs$1 from "node:fs/promises";
|
|
10
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
11
|
+
import "node:zlib";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
6
13
|
//#region \0rolldown/runtime.js
|
|
7
14
|
var __create = Object.create;
|
|
8
15
|
var __defProp = Object.defineProperty;
|
|
@@ -5614,7 +5621,7 @@ var ZodIssueCode = {
|
|
|
5614
5621
|
var ZodFirstPartyTypeKind;
|
|
5615
5622
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5616
5623
|
//#endregion
|
|
5617
|
-
//#region ../types/dist/sleep
|
|
5624
|
+
//#region ../types/dist/sleep--jXS9hWo.mjs
|
|
5618
5625
|
/**
|
|
5619
5626
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5620
5627
|
* window to float samples (D455).
|
|
@@ -8852,6 +8859,28 @@ function event(data) {
|
|
|
8852
8859
|
var StaticDirOutputSchema$1 = object({ staticDir: string() });
|
|
8853
8860
|
var VersionOutputSchema$1 = object({ version: string() });
|
|
8854
8861
|
method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
|
|
8862
|
+
/**
|
|
8863
|
+
* Identity — preserves literal types for downstream inference.
|
|
8864
|
+
*
|
|
8865
|
+
* The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
|
|
8866
|
+
* TypeScript does not widen each entry's literal `kind`/`auth` fields to
|
|
8867
|
+
* the broader unions declared on `CustomActionSpec`'s default generics.
|
|
8868
|
+
* Shape validity is enforced separately by the `customAction(...)` helper
|
|
8869
|
+
* whose return type is already a `CustomActionSpec<...>`.
|
|
8870
|
+
*/
|
|
8871
|
+
function defineCustomActions(spec) {
|
|
8872
|
+
return spec;
|
|
8873
|
+
}
|
|
8874
|
+
function customAction(input, output, options) {
|
|
8875
|
+
return {
|
|
8876
|
+
input,
|
|
8877
|
+
output,
|
|
8878
|
+
kind: options?.kind ?? "query",
|
|
8879
|
+
auth: options?.auth ?? "protected",
|
|
8880
|
+
scope: options?.scope ?? { kind: "system" },
|
|
8881
|
+
...options?.caller ? { caller: "required" } : {}
|
|
8882
|
+
};
|
|
8883
|
+
}
|
|
8855
8884
|
var StaticDirOutputSchema = object({ staticDir: string() });
|
|
8856
8885
|
var VersionOutputSchema = object({ version: string() });
|
|
8857
8886
|
method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
|
|
@@ -8995,6 +9024,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
8995
9024
|
"filesystem-browse",
|
|
8996
9025
|
"flood",
|
|
8997
9026
|
"gas",
|
|
9027
|
+
"group-active",
|
|
8998
9028
|
"group-commands",
|
|
8999
9029
|
"group-status",
|
|
9000
9030
|
"heating-valve",
|
|
@@ -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",
|
|
@@ -9815,6 +9846,10 @@ Object.freeze({
|
|
|
9815
9846
|
"path": "detected",
|
|
9816
9847
|
"valueType": "boolean"
|
|
9817
9848
|
}],
|
|
9849
|
+
"group-active": [{
|
|
9850
|
+
"path": "active",
|
|
9851
|
+
"valueType": "boolean"
|
|
9852
|
+
}],
|
|
9818
9853
|
"group-status": [
|
|
9819
9854
|
{
|
|
9820
9855
|
"path": "included",
|
|
@@ -10074,6 +10109,10 @@ Object.freeze({
|
|
|
10074
10109
|
]
|
|
10075
10110
|
}
|
|
10076
10111
|
],
|
|
10112
|
+
"meter": [{
|
|
10113
|
+
"path": "value",
|
|
10114
|
+
"valueType": "number"
|
|
10115
|
+
}],
|
|
10077
10116
|
"motion": [
|
|
10078
10117
|
{
|
|
10079
10118
|
"path": "detected",
|
|
@@ -10817,7 +10856,7 @@ object({
|
|
|
10817
10856
|
params: record(string(), union([string(), number()])).optional()
|
|
10818
10857
|
});
|
|
10819
10858
|
//#endregion
|
|
10820
|
-
//#region ../types/dist/err-msg-
|
|
10859
|
+
//#region ../types/dist/err-msg-DIorGYeC.mjs
|
|
10821
10860
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
10822
10861
|
DeviceType["Camera"] = "camera";
|
|
10823
10862
|
DeviceType["Hub"] = "hub";
|
|
@@ -10911,8 +10950,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
10911
10950
|
DeviceType["PetFeeder"] = "pet-feeder";
|
|
10912
10951
|
/** Built-in dishwasher — program lifecycle, door / remote-start gates,
|
|
10913
10952
|
* salt and rinse-aid levels. Installed with the `dishwasher` cap.
|
|
10914
|
-
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10953
|
+
* Sources: native Bosch Home Connect, reusable by similar integrations.
|
|
10954
|
+
* A plug-built dishwasher carries running-state and power instead. */
|
|
10915
10955
|
DeviceType["Dishwasher"] = "dishwasher";
|
|
10956
|
+
/** Washing machine. A plug-built one is running while its power stays
|
|
10957
|
+
* above a threshold. No program cap — that surface is the dishwasher's. */
|
|
10958
|
+
DeviceType["Washer"] = "washer";
|
|
10959
|
+
/** Tumble dryer. Same plug-built running surface as {@link Washer}. */
|
|
10960
|
+
DeviceType["Dryer"] = "dryer";
|
|
10916
10961
|
return DeviceType;
|
|
10917
10962
|
}({});
|
|
10918
10963
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -11170,7 +11215,7 @@ function errMsg(err) {
|
|
|
11170
11215
|
return String(err);
|
|
11171
11216
|
}
|
|
11172
11217
|
//#endregion
|
|
11173
|
-
//#region ../types/dist/composition-
|
|
11218
|
+
//#region ../types/dist/composition-Bb8iwHEJ.mjs
|
|
11174
11219
|
/**
|
|
11175
11220
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11176
11221
|
*
|
|
@@ -11485,6 +11530,13 @@ var table = {
|
|
|
11485
11530
|
apply: () => {
|
|
11486
11531
|
throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
|
|
11487
11532
|
}
|
|
11533
|
+
},
|
|
11534
|
+
schmitt: {
|
|
11535
|
+
minArgs: 3,
|
|
11536
|
+
maxArgs: 5,
|
|
11537
|
+
apply: () => {
|
|
11538
|
+
throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
|
|
11539
|
+
}
|
|
11488
11540
|
}
|
|
11489
11541
|
};
|
|
11490
11542
|
Object.freeze(Object.assign(Object.create(null), table));
|
|
@@ -11926,6 +11978,99 @@ function validateExpressionSource(src) {
|
|
|
11926
11978
|
return null;
|
|
11927
11979
|
}
|
|
11928
11980
|
/**
|
|
11981
|
+
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
11982
|
+
*
|
|
11983
|
+
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
11984
|
+
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
11985
|
+
* a cycle, so running starts only after 30 s above the line.
|
|
11986
|
+
*
|
|
11987
|
+
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
11988
|
+
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
11989
|
+
* report "finished" in the middle of the program, so the stop dwell is
|
|
11990
|
+
* 10 minutes on every type. The number is stored per type so one machine
|
|
11991
|
+
* can move later without moving the others.
|
|
11992
|
+
*/
|
|
11993
|
+
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
11994
|
+
var APPLIANCE_KINDS = [
|
|
11995
|
+
"washer",
|
|
11996
|
+
"dryer",
|
|
11997
|
+
"dishwasher"
|
|
11998
|
+
];
|
|
11999
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12000
|
+
/**
|
|
12001
|
+
* Statistics and integrals over a history read (D944).
|
|
12002
|
+
*
|
|
12003
|
+
* Pure: the same points always give the same answer, so a restart recomputes
|
|
12004
|
+
* rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
|
|
12005
|
+
* never the mean of the samples. A hole with no held value is not integrated
|
|
12006
|
+
* as zero. A truncated read is not published.
|
|
12007
|
+
*/
|
|
12008
|
+
var STATISTIC_STATS = [
|
|
12009
|
+
"min",
|
|
12010
|
+
"max",
|
|
12011
|
+
"mean",
|
|
12012
|
+
"sum",
|
|
12013
|
+
"count",
|
|
12014
|
+
"change"
|
|
12015
|
+
];
|
|
12016
|
+
/**
|
|
12017
|
+
* The range a statistics or integral helper asks history for (D944).
|
|
12018
|
+
*
|
|
12019
|
+
* Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
|
|
12020
|
+
* or 25 hours across a DST change. A tier is eligible only when its retention
|
|
12021
|
+
* is strictly longer than the longest that range can be. Forever qualifies.
|
|
12022
|
+
* Equal to a tier's retention is a refusal, not a short answer.
|
|
12023
|
+
*/
|
|
12024
|
+
var MINUTE_MS = 6e4;
|
|
12025
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
12026
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
12027
|
+
var ROLLING_SPANS = [
|
|
12028
|
+
"1h",
|
|
12029
|
+
"24h",
|
|
12030
|
+
"2d",
|
|
12031
|
+
"7d",
|
|
12032
|
+
"30d",
|
|
12033
|
+
"custom"
|
|
12034
|
+
];
|
|
12035
|
+
var CALENDAR_PERIODS = [
|
|
12036
|
+
"today",
|
|
12037
|
+
"yesterday",
|
|
12038
|
+
"this-week",
|
|
12039
|
+
"this-month"
|
|
12040
|
+
];
|
|
12041
|
+
var RESET_PERIODS = [
|
|
12042
|
+
"never",
|
|
12043
|
+
"daily",
|
|
12044
|
+
"weekly",
|
|
12045
|
+
"monthly"
|
|
12046
|
+
];
|
|
12047
|
+
5 * MINUTE_MS;
|
|
12048
|
+
24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
|
|
12049
|
+
7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
|
|
12050
|
+
var rollingWindowObject = object({
|
|
12051
|
+
kind: literal("rolling"),
|
|
12052
|
+
span: _enum(ROLLING_SPANS),
|
|
12053
|
+
customMinutes: number().int().min(5).optional()
|
|
12054
|
+
}).check((payload) => {
|
|
12055
|
+
if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
|
|
12056
|
+
code: "custom",
|
|
12057
|
+
message: "a custom range needs a duration in minutes",
|
|
12058
|
+
path: ["customMinutes"],
|
|
12059
|
+
input: payload.value
|
|
12060
|
+
});
|
|
12061
|
+
if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
|
|
12062
|
+
code: "custom",
|
|
12063
|
+
message: "only a custom range carries a duration",
|
|
12064
|
+
path: ["customMinutes"],
|
|
12065
|
+
input: payload.value
|
|
12066
|
+
});
|
|
12067
|
+
});
|
|
12068
|
+
var RollingWindowSchema = rollingWindowObject;
|
|
12069
|
+
var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
|
|
12070
|
+
kind: literal("calendar"),
|
|
12071
|
+
period: _enum(CALENDAR_PERIODS)
|
|
12072
|
+
})]);
|
|
12073
|
+
/**
|
|
11929
12074
|
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
11930
12075
|
* id. Split out so helper schemas can name it without a cycle through
|
|
11931
12076
|
* `composition.ts` (D659).
|
|
@@ -11951,7 +12096,13 @@ var GROUP_TEMPLATE_IDS = [
|
|
|
11951
12096
|
"occupancy",
|
|
11952
12097
|
"binary",
|
|
11953
12098
|
"switch",
|
|
11954
|
-
"light"
|
|
12099
|
+
"light",
|
|
12100
|
+
"fan",
|
|
12101
|
+
"cover",
|
|
12102
|
+
"valve",
|
|
12103
|
+
"lock",
|
|
12104
|
+
"media-player",
|
|
12105
|
+
"climate"
|
|
11955
12106
|
];
|
|
11956
12107
|
var NUMERIC_AGGREGATES = [
|
|
11957
12108
|
"mean",
|
|
@@ -11981,14 +12132,73 @@ var HelperMemberSchema = object({
|
|
|
11981
12132
|
cap: string().min(1),
|
|
11982
12133
|
fieldPath: string().min(1)
|
|
11983
12134
|
});
|
|
11984
|
-
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
12135
|
+
var HelperSpecSchema = discriminatedUnion("kind", [
|
|
12136
|
+
object({
|
|
12137
|
+
kind: literal("group"),
|
|
12138
|
+
templateId: GroupTemplateIdSchema,
|
|
12139
|
+
aggregate: AggregateReducerSchema,
|
|
12140
|
+
members: array(HelperMemberSchema).min(1).max(64),
|
|
12141
|
+
/** Actuator groups only. Readings leave this unset. */
|
|
12142
|
+
active: ActiveCriterionSchema.optional()
|
|
12143
|
+
}),
|
|
12144
|
+
object({
|
|
12145
|
+
kind: literal("appliance"),
|
|
12146
|
+
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12147
|
+
appliance: _enum(APPLIANCE_KINDS),
|
|
12148
|
+
thresholdWatts: number().positive(),
|
|
12149
|
+
startDwellMs: number().int().nonnegative(),
|
|
12150
|
+
stopDwellMs: number().int().nonnegative(),
|
|
12151
|
+
member: HelperMemberSchema
|
|
12152
|
+
}),
|
|
12153
|
+
object({
|
|
12154
|
+
kind: literal("statistic"),
|
|
12155
|
+
stat: _enum(STATISTIC_STATS),
|
|
12156
|
+
window: StatisticWindowSchema,
|
|
12157
|
+
source: HelperMemberSchema
|
|
12158
|
+
}),
|
|
12159
|
+
object({
|
|
12160
|
+
kind: literal("integral"),
|
|
12161
|
+
reset: _enum(RESET_PERIODS),
|
|
12162
|
+
output: _enum(["energy", "meter"]),
|
|
12163
|
+
source: HelperMemberSchema
|
|
12164
|
+
}),
|
|
12165
|
+
object({
|
|
12166
|
+
kind: literal("threshold"),
|
|
12167
|
+
source: HelperMemberSchema,
|
|
12168
|
+
high: number(),
|
|
12169
|
+
low: number(),
|
|
12170
|
+
onForMs: number().int().nonnegative(),
|
|
12171
|
+
offForMs: number().int().nonnegative()
|
|
12172
|
+
}),
|
|
12173
|
+
object({
|
|
12174
|
+
kind: literal("arithmetic"),
|
|
12175
|
+
op: _enum([
|
|
12176
|
+
"sum",
|
|
12177
|
+
"difference",
|
|
12178
|
+
"ratio"
|
|
12179
|
+
]),
|
|
12180
|
+
inputs: array(HelperMemberSchema).min(2).max(64)
|
|
12181
|
+
}),
|
|
12182
|
+
object({
|
|
12183
|
+
kind: literal("expression"),
|
|
12184
|
+
expr: string().min(1),
|
|
12185
|
+
inputs: array(object({
|
|
12186
|
+
name: string().min(1),
|
|
12187
|
+
member: HelperMemberSchema
|
|
12188
|
+
})).min(1).max(64),
|
|
12189
|
+
output: _enum(["number", "boolean"])
|
|
12190
|
+
}),
|
|
12191
|
+
object({
|
|
12192
|
+
kind: literal("derivative"),
|
|
12193
|
+
window: RollingWindowSchema,
|
|
12194
|
+
per: _enum([
|
|
12195
|
+
"second",
|
|
12196
|
+
"minute",
|
|
12197
|
+
"hour"
|
|
12198
|
+
]),
|
|
12199
|
+
source: HelperMemberSchema
|
|
12200
|
+
})
|
|
12201
|
+
]);
|
|
11992
12202
|
/** A composed device's stableId is this prefix plus the block id. */
|
|
11993
12203
|
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
11994
12204
|
/** Longest snippet a `code` source may carry (slice 4). */
|
|
@@ -12049,6 +12259,12 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
|
12049
12259
|
"all"
|
|
12050
12260
|
]),
|
|
12051
12261
|
reads: array(CompositionFieldReadSchema).min(1).max(64),
|
|
12262
|
+
/**
|
|
12263
|
+
* Set when the reads are an enum state, not a yes/no: the listed states
|
|
12264
|
+
* count as active (a cover is `open`). A state outside the list is inactive;
|
|
12265
|
+
* a read that is not a state at all is left out. Only `any` / `all` use it.
|
|
12266
|
+
*/
|
|
12267
|
+
activeWhen: array(string().min(1)).min(1).optional(),
|
|
12052
12268
|
/** `value` is the reading; the others are the member counts. */
|
|
12053
12269
|
publish: _enum([
|
|
12054
12270
|
"value",
|
|
@@ -12062,6 +12278,26 @@ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
|
12062
12278
|
"omit",
|
|
12063
12279
|
"offline"
|
|
12064
12280
|
])
|
|
12281
|
+
}),
|
|
12282
|
+
object({
|
|
12283
|
+
kind: literal("series"),
|
|
12284
|
+
read: CompositionFieldReadSchema,
|
|
12285
|
+
/** `value` is the statistic or the integral. The others are the gap counts. */
|
|
12286
|
+
publish: _enum([
|
|
12287
|
+
"value",
|
|
12288
|
+
"included",
|
|
12289
|
+
"excluded",
|
|
12290
|
+
"total"
|
|
12291
|
+
]),
|
|
12292
|
+
/**
|
|
12293
|
+
* What an unanswered `value` does. Counts ignore this. `offline` leaves the
|
|
12294
|
+
* device unseeded until history answers, so a restart cannot publish 0.
|
|
12295
|
+
*/
|
|
12296
|
+
empty: _enum([
|
|
12297
|
+
"null",
|
|
12298
|
+
"omit",
|
|
12299
|
+
"offline"
|
|
12300
|
+
])
|
|
12065
12301
|
})
|
|
12066
12302
|
]);
|
|
12067
12303
|
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
@@ -15822,7 +16058,24 @@ var AddonPageDeclarationSchema = object({
|
|
|
15822
16058
|
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
15823
16059
|
sectionLabel: string().optional()
|
|
15824
16060
|
});
|
|
15825
|
-
|
|
16061
|
+
var addonPagesSourceCapability = {
|
|
16062
|
+
name: "addon-pages-source",
|
|
16063
|
+
scope: "system",
|
|
16064
|
+
mode: "collection",
|
|
16065
|
+
internal: true,
|
|
16066
|
+
methods: {
|
|
16067
|
+
/**
|
|
16068
|
+
* `internal: true` did not gate the mount (see the 2026-08-26 note on
|
|
16069
|
+
* `data-store-provider`) — this was reachable on the AppRouter by ANY
|
|
16070
|
+
* authenticated session at the default `auth: 'protected'`. The only
|
|
16071
|
+
* consumer, the `addon-pages-aggregator` builtin, reads it via
|
|
16072
|
+
* `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
|
|
16073
|
+
* only, never tRPC (per `ctx.capabilities` seeing only the local
|
|
16074
|
+
* process). The public, enriched listing admin-ui actually consumes is
|
|
16075
|
+
* the separate `addon-pages` cap, unaffected by this change.
|
|
16076
|
+
*/
|
|
16077
|
+
listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
|
|
16078
|
+
};
|
|
15826
16079
|
var AddonHttpRouteSchema = object({
|
|
15827
16080
|
method: _enum([
|
|
15828
16081
|
"GET",
|
|
@@ -18511,7 +18764,8 @@ var CompositionProblemSchema = object({
|
|
|
18511
18764
|
"native-capability-replace",
|
|
18512
18765
|
"mixed-quantity",
|
|
18513
18766
|
"helper-mismatch",
|
|
18514
|
-
"member-constraint"
|
|
18767
|
+
"member-constraint",
|
|
18768
|
+
"retention"
|
|
18515
18769
|
]),
|
|
18516
18770
|
path: string(),
|
|
18517
18771
|
message: string()
|
|
@@ -18745,13 +18999,17 @@ Object.freeze({
|
|
|
18745
18999
|
latchedAt: {
|
|
18746
19000
|
kind: "fixed",
|
|
18747
19001
|
result: ["number", "null"]
|
|
19002
|
+
},
|
|
19003
|
+
schmitt: {
|
|
19004
|
+
kind: "fixed",
|
|
19005
|
+
result: ["boolean", "null"]
|
|
18748
19006
|
}
|
|
18749
19007
|
});
|
|
18750
19008
|
/**
|
|
18751
19009
|
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18752
19010
|
* Data, not a form default written into the block.
|
|
18753
19011
|
*/
|
|
18754
|
-
var NOW = {
|
|
19012
|
+
var NOW$2 = {
|
|
18755
19013
|
fieldPath: "lastFetchedAt",
|
|
18756
19014
|
expr: "now"
|
|
18757
19015
|
};
|
|
@@ -18765,6 +19023,8 @@ function numeric(input) {
|
|
|
18765
19023
|
family: "numeric",
|
|
18766
19024
|
deviceType: DeviceType.Sensor,
|
|
18767
19025
|
memberDeviceType: DeviceType.Sensor,
|
|
19026
|
+
memberDeviceTypes: [DeviceType.Sensor],
|
|
19027
|
+
activeWhen: void 0,
|
|
18768
19028
|
commandCaps: [],
|
|
18769
19029
|
baseUnit: baseUnitOf(input.quantity),
|
|
18770
19030
|
aggregates: NUMERIC_AGGREGATES,
|
|
@@ -18780,6 +19040,8 @@ function binary(input) {
|
|
|
18780
19040
|
family: "binary",
|
|
18781
19041
|
deviceType: DeviceType.Sensor,
|
|
18782
19042
|
memberDeviceType: DeviceType.Sensor,
|
|
19043
|
+
memberDeviceTypes: [DeviceType.Sensor],
|
|
19044
|
+
activeWhen: void 0,
|
|
18783
19045
|
commandCaps: [],
|
|
18784
19046
|
quantity: "none",
|
|
18785
19047
|
baseUnit: void 0,
|
|
@@ -18797,7 +19059,7 @@ var GROUP_TEMPLATES = [
|
|
|
18797
19059
|
quantity: "temperature",
|
|
18798
19060
|
defaultAggregate: "mean",
|
|
18799
19061
|
empty: "null",
|
|
18800
|
-
companions: [NOW]
|
|
19062
|
+
companions: [NOW$2]
|
|
18801
19063
|
}),
|
|
18802
19064
|
numeric({
|
|
18803
19065
|
id: "humidity",
|
|
@@ -18808,7 +19070,7 @@ var GROUP_TEMPLATES = [
|
|
|
18808
19070
|
quantity: "humidity",
|
|
18809
19071
|
defaultAggregate: "mean",
|
|
18810
19072
|
empty: "null",
|
|
18811
|
-
companions: [NOW]
|
|
19073
|
+
companions: [NOW$2]
|
|
18812
19074
|
}),
|
|
18813
19075
|
numeric({
|
|
18814
19076
|
id: "pressure",
|
|
@@ -18819,7 +19081,7 @@ var GROUP_TEMPLATES = [
|
|
|
18819
19081
|
quantity: "pressure",
|
|
18820
19082
|
defaultAggregate: "mean",
|
|
18821
19083
|
empty: "null",
|
|
18822
|
-
companions: [NOW]
|
|
19084
|
+
companions: [NOW$2]
|
|
18823
19085
|
}),
|
|
18824
19086
|
numeric({
|
|
18825
19087
|
id: "illuminance",
|
|
@@ -18830,7 +19092,7 @@ var GROUP_TEMPLATES = [
|
|
|
18830
19092
|
quantity: "illuminance",
|
|
18831
19093
|
defaultAggregate: "mean",
|
|
18832
19094
|
empty: "null",
|
|
18833
|
-
companions: [NOW]
|
|
19095
|
+
companions: [NOW$2]
|
|
18834
19096
|
}),
|
|
18835
19097
|
numeric({
|
|
18836
19098
|
id: "power",
|
|
@@ -18841,7 +19103,7 @@ var GROUP_TEMPLATES = [
|
|
|
18841
19103
|
quantity: "power",
|
|
18842
19104
|
defaultAggregate: "sum",
|
|
18843
19105
|
empty: "omit",
|
|
18844
|
-
companions: [NOW]
|
|
19106
|
+
companions: [NOW$2]
|
|
18845
19107
|
}),
|
|
18846
19108
|
numeric({
|
|
18847
19109
|
id: "energy",
|
|
@@ -18852,7 +19114,7 @@ var GROUP_TEMPLATES = [
|
|
|
18852
19114
|
quantity: "energy",
|
|
18853
19115
|
defaultAggregate: "sum",
|
|
18854
19116
|
empty: "omit",
|
|
18855
|
-
companions: [NOW]
|
|
19117
|
+
companions: [NOW$2]
|
|
18856
19118
|
}),
|
|
18857
19119
|
numeric({
|
|
18858
19120
|
id: "battery",
|
|
@@ -18887,7 +19149,7 @@ var GROUP_TEMPLATES = [
|
|
|
18887
19149
|
quantity: "device",
|
|
18888
19150
|
defaultAggregate: "mean",
|
|
18889
19151
|
empty: "null",
|
|
18890
|
-
companions: [NOW]
|
|
19152
|
+
companions: [NOW$2]
|
|
18891
19153
|
}),
|
|
18892
19154
|
binary({
|
|
18893
19155
|
id: "motion",
|
|
@@ -18981,6 +19243,11 @@ var GROUP_TEMPLATES = [
|
|
|
18981
19243
|
summary: "On when at least one switch is on, or when all of them are. Offline and unknown members are left out of that decision, never read as off, and still receive the command.",
|
|
18982
19244
|
deviceType: DeviceType.Switch,
|
|
18983
19245
|
outputCap: "switch",
|
|
19246
|
+
outputField: "on",
|
|
19247
|
+
member: {
|
|
19248
|
+
cap: "switch",
|
|
19249
|
+
fieldPath: "on"
|
|
19250
|
+
},
|
|
18984
19251
|
commandCaps: ["switch"]
|
|
18985
19252
|
}),
|
|
18986
19253
|
actuator({
|
|
@@ -18988,34 +19255,136 @@ var GROUP_TEMPLATES = [
|
|
|
18988
19255
|
summary: "On when at least one light is on, or when all of them are. Brightness and colour are offered only when every member has them. A command is not sent to a member that lacks it.",
|
|
18989
19256
|
deviceType: DeviceType.Light,
|
|
18990
19257
|
outputCap: "switch",
|
|
19258
|
+
outputField: "on",
|
|
19259
|
+
member: {
|
|
19260
|
+
cap: "switch",
|
|
19261
|
+
fieldPath: "on"
|
|
19262
|
+
},
|
|
18991
19263
|
commandCaps: [
|
|
18992
19264
|
"switch",
|
|
18993
19265
|
"brightness",
|
|
18994
19266
|
"color"
|
|
18995
19267
|
]
|
|
19268
|
+
}),
|
|
19269
|
+
actuator({
|
|
19270
|
+
id: "fan",
|
|
19271
|
+
summary: "On when at least one fan is on, or when all of them are. Speed, preset, direction and oscillation are offered only when every fan has them. A command is not sent to a fan that lacks it.",
|
|
19272
|
+
deviceType: DeviceType.Fan,
|
|
19273
|
+
outputCap: "switch",
|
|
19274
|
+
outputField: "on",
|
|
19275
|
+
member: {
|
|
19276
|
+
cap: "switch",
|
|
19277
|
+
fieldPath: "on"
|
|
19278
|
+
},
|
|
19279
|
+
commandCaps: ["switch", "fan-control"]
|
|
19280
|
+
}),
|
|
19281
|
+
actuator({
|
|
19282
|
+
id: "cover",
|
|
19283
|
+
summary: "Open when at least one cover is open or part-open, or only when all of them are. Position and tilt are offered only when every cover has them. A cover that reports nothing is left out, never read as closed.",
|
|
19284
|
+
deviceType: DeviceType.Cover,
|
|
19285
|
+
outputCap: "group-active",
|
|
19286
|
+
outputField: "active",
|
|
19287
|
+
member: {
|
|
19288
|
+
cap: "cover",
|
|
19289
|
+
fieldPath: "state"
|
|
19290
|
+
},
|
|
19291
|
+
activeWhen: [
|
|
19292
|
+
"open",
|
|
19293
|
+
"opening",
|
|
19294
|
+
"stopped"
|
|
19295
|
+
],
|
|
19296
|
+
commandCaps: ["cover"]
|
|
19297
|
+
}),
|
|
19298
|
+
actuator({
|
|
19299
|
+
id: "valve",
|
|
19300
|
+
summary: "Open when at least one valve is open or part-open, or only when all of them are. Position is offered only when every valve has it. A valve that reports nothing is left out, never read as closed.",
|
|
19301
|
+
deviceType: DeviceType.Valve,
|
|
19302
|
+
outputCap: "group-active",
|
|
19303
|
+
outputField: "active",
|
|
19304
|
+
member: {
|
|
19305
|
+
cap: "valve",
|
|
19306
|
+
fieldPath: "state"
|
|
19307
|
+
},
|
|
19308
|
+
activeWhen: [
|
|
19309
|
+
"open",
|
|
19310
|
+
"opening",
|
|
19311
|
+
"stopped"
|
|
19312
|
+
],
|
|
19313
|
+
commandCaps: ["valve"]
|
|
19314
|
+
}),
|
|
19315
|
+
actuator({
|
|
19316
|
+
id: "lock",
|
|
19317
|
+
summary: "Locked when at least one lock is locked, or only when all of them are. A jammed or moving lock is not locked. Locking is one tap; unlocking the group needs a confirmation every time, and nothing is sent without it.",
|
|
19318
|
+
deviceType: DeviceType.Lock,
|
|
19319
|
+
outputCap: "group-active",
|
|
19320
|
+
outputField: "active",
|
|
19321
|
+
member: {
|
|
19322
|
+
cap: "lock-control",
|
|
19323
|
+
fieldPath: "state"
|
|
19324
|
+
},
|
|
19325
|
+
activeWhen: ["locked"],
|
|
19326
|
+
commandCaps: ["lock-control"]
|
|
19327
|
+
}),
|
|
19328
|
+
actuator({
|
|
19329
|
+
id: "media-player",
|
|
19330
|
+
summary: "Playing when at least one player is playing, or only when all of them are. Volume is offered only when every player has it. A player that reports nothing is left out, never read as stopped.",
|
|
19331
|
+
deviceType: DeviceType.MediaPlayer,
|
|
19332
|
+
outputCap: "group-active",
|
|
19333
|
+
outputField: "active",
|
|
19334
|
+
member: {
|
|
19335
|
+
cap: "media-player",
|
|
19336
|
+
fieldPath: "state"
|
|
19337
|
+
},
|
|
19338
|
+
activeWhen: ["playing", "buffering"],
|
|
19339
|
+
commandCaps: ["media-player"]
|
|
19340
|
+
}),
|
|
19341
|
+
actuator({
|
|
19342
|
+
id: "climate",
|
|
19343
|
+
summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
|
|
19344
|
+
deviceType: DeviceType.Thermostat,
|
|
19345
|
+
memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
19346
|
+
outputCap: "group-active",
|
|
19347
|
+
outputField: "active",
|
|
19348
|
+
member: {
|
|
19349
|
+
cap: "climate-control",
|
|
19350
|
+
fieldPath: "mode"
|
|
19351
|
+
},
|
|
19352
|
+
activeWhen: [
|
|
19353
|
+
"heat",
|
|
19354
|
+
"cool",
|
|
19355
|
+
"auto",
|
|
19356
|
+
"heat_cool",
|
|
19357
|
+
"fan_only",
|
|
19358
|
+
"dry"
|
|
19359
|
+
],
|
|
19360
|
+
commandCaps: ["climate-control"]
|
|
18996
19361
|
})
|
|
18997
19362
|
];
|
|
18998
19363
|
function actuator(input) {
|
|
18999
19364
|
return {
|
|
19000
|
-
|
|
19365
|
+
id: input.id,
|
|
19366
|
+
summary: input.summary,
|
|
19367
|
+
deviceType: input.deviceType,
|
|
19368
|
+
outputCap: input.outputCap,
|
|
19369
|
+
outputField: input.outputField,
|
|
19370
|
+
commandCaps: input.commandCaps,
|
|
19001
19371
|
family: "actuator",
|
|
19002
19372
|
role: void 0,
|
|
19003
19373
|
memberDeviceType: input.deviceType,
|
|
19004
|
-
|
|
19374
|
+
memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
|
|
19375
|
+
activeWhen: input.activeWhen,
|
|
19005
19376
|
quantity: "none",
|
|
19006
19377
|
baseUnit: void 0,
|
|
19007
19378
|
aggregates: BINARY_AGGREGATES,
|
|
19008
19379
|
defaultAggregate: "any",
|
|
19009
19380
|
empty: "offline",
|
|
19010
|
-
accept: [
|
|
19011
|
-
cap: "switch",
|
|
19012
|
-
fieldPath: "on"
|
|
19013
|
-
}],
|
|
19381
|
+
accept: [input.member],
|
|
19014
19382
|
companions: [CHANGED]
|
|
19015
19383
|
};
|
|
19016
19384
|
}
|
|
19017
19385
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
19018
19386
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
19387
|
+
DeviceType.Camera;
|
|
19019
19388
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
19020
19389
|
/**
|
|
19021
19390
|
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
@@ -44581,57 +44950,79 @@ var ProbeTargetConfigResultSchema = object({
|
|
|
44581
44950
|
stateInstance: string().optional(),
|
|
44582
44951
|
error: string().optional()
|
|
44583
44952
|
});
|
|
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
|
-
|
|
44953
|
+
var notificationOutputCapability = {
|
|
44954
|
+
name: "notification-output",
|
|
44955
|
+
scope: "system",
|
|
44956
|
+
mode: "collection",
|
|
44957
|
+
methods: {
|
|
44958
|
+
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
44959
|
+
listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
|
|
44960
|
+
/**
|
|
44961
|
+
* One target WITH secrets — admin edit / duplicate hydration only.
|
|
44962
|
+
* `listTargets` stays redacted; the editor loads this so password fields
|
|
44963
|
+
* are filled (masked in the input) and a save can round-trip the token
|
|
44964
|
+
* when a destination changes (Duplicate with a new chatId, etc.).
|
|
44965
|
+
*/
|
|
44966
|
+
getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
|
|
44967
|
+
discoverTargets: method(object({
|
|
44968
|
+
kind: string(),
|
|
44969
|
+
config: record(string(), unknown()).optional(),
|
|
44970
|
+
/**
|
|
44971
|
+
* When editing / duplicating, the blank password field never round-trips
|
|
44972
|
+
* the stored token. Pass the source target id so the provider can restore
|
|
44973
|
+
* secrets server-side before dialling (same merge as upsert).
|
|
44974
|
+
*/
|
|
44975
|
+
targetId: string().optional()
|
|
44976
|
+
}), array(DiscoveredTargetSchema), { auth: "admin" }),
|
|
44977
|
+
probeTargetConfig: method(object({
|
|
44978
|
+
kind: string(),
|
|
44979
|
+
config: record(string(), unknown()),
|
|
44980
|
+
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
44981
|
+
targetId: string().optional()
|
|
44982
|
+
}), ProbeTargetConfigResultSchema, {
|
|
44983
|
+
kind: "mutation",
|
|
44984
|
+
auth: "admin"
|
|
44985
|
+
}),
|
|
44986
|
+
send: method(object({
|
|
44987
|
+
targetId: string(),
|
|
44988
|
+
notification: NotificationSchema
|
|
44989
|
+
}), SendResultSchema, {
|
|
44990
|
+
kind: "mutation",
|
|
44991
|
+
auth: "admin"
|
|
44992
|
+
}),
|
|
44993
|
+
testTarget: method(object({
|
|
44994
|
+
targetId: string(),
|
|
44995
|
+
sample: NotificationSchema.optional()
|
|
44996
|
+
}), TestResultSchema, {
|
|
44997
|
+
kind: "mutation",
|
|
44998
|
+
auth: "admin"
|
|
44999
|
+
}),
|
|
45000
|
+
upsertTarget: method(object({
|
|
45001
|
+
target: TargetSchema,
|
|
45002
|
+
/**
|
|
45003
|
+
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
45004
|
+
* existing row. The UI never holds the stored token; blank / redacted
|
|
45005
|
+
* password fields are restored from this source (D634 still applies if
|
|
45006
|
+
* destinations diverge without a re-typed secret).
|
|
45007
|
+
*/
|
|
45008
|
+
cloneSecretsFrom: string().optional()
|
|
45009
|
+
}), TargetSchema, {
|
|
45010
|
+
kind: "mutation",
|
|
45011
|
+
auth: "admin"
|
|
45012
|
+
}),
|
|
45013
|
+
deleteTarget: method(object({ targetId: string() }), _void(), {
|
|
45014
|
+
kind: "mutation",
|
|
45015
|
+
auth: "admin"
|
|
45016
|
+
}),
|
|
45017
|
+
setTargetEnabled: method(object({
|
|
45018
|
+
targetId: string(),
|
|
45019
|
+
enabled: boolean()
|
|
45020
|
+
}), _void(), {
|
|
45021
|
+
kind: "mutation",
|
|
45022
|
+
auth: "admin"
|
|
45023
|
+
})
|
|
45024
|
+
}
|
|
45025
|
+
};
|
|
44635
45026
|
new Set([
|
|
44636
45027
|
{
|
|
44637
45028
|
id: "person",
|
|
@@ -46926,7 +47317,20 @@ var NcRuleInputSchema = object({
|
|
|
46926
47317
|
* save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
|
|
46927
47318
|
* path does not run Zod defaults.
|
|
46928
47319
|
*/
|
|
46929
|
-
groupingSec: number().int().min(5).max(60).optional()
|
|
47320
|
+
groupingSec: number().int().min(5).max(60).optional(),
|
|
47321
|
+
/**
|
|
47322
|
+
* ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
|
|
47323
|
+
* carries "Correct" / "Wrong" buttons (and, where the channel has room, a
|
|
47324
|
+
* "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
|
|
47325
|
+
* same mark the Library writes, and records whose notification it was and
|
|
47326
|
+
* when. A channel with no buttons gets the same two choices as links.
|
|
47327
|
+
*
|
|
47328
|
+
* `.optional()`, never `.default()` (a Zod default does not run on the addon
|
|
47329
|
+
* cap path): absent and `false` both mean off, which is what every rule
|
|
47330
|
+
* authored before this field meant. An editor turns it off by writing
|
|
47331
|
+
* `false`, so it needs no `clear` key.
|
|
47332
|
+
*/
|
|
47333
|
+
askVerdict: boolean().optional()
|
|
46930
47334
|
});
|
|
46931
47335
|
/**
|
|
46932
47336
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -60523,7 +60927,17 @@ var groupCommandsCapability = {
|
|
|
60523
60927
|
scope: "device",
|
|
60524
60928
|
deviceNative: true,
|
|
60525
60929
|
mode: "singleton",
|
|
60526
|
-
deviceTypes: [
|
|
60930
|
+
deviceTypes: [
|
|
60931
|
+
DeviceType.Switch,
|
|
60932
|
+
DeviceType.Light,
|
|
60933
|
+
DeviceType.Fan,
|
|
60934
|
+
DeviceType.Cover,
|
|
60935
|
+
DeviceType.Valve,
|
|
60936
|
+
DeviceType.Lock,
|
|
60937
|
+
DeviceType.MediaPlayer,
|
|
60938
|
+
DeviceType.Thermostat,
|
|
60939
|
+
DeviceType.Climate
|
|
60940
|
+
],
|
|
60527
60941
|
methods: {},
|
|
60528
60942
|
status: {
|
|
60529
60943
|
schema: GroupCommandsSchema,
|
|
@@ -60541,6 +60955,49 @@ var groupCommandsCapability = {
|
|
|
60541
60955
|
history: "none"
|
|
60542
60956
|
};
|
|
60543
60957
|
/**
|
|
60958
|
+
* Whether a helper actuator group counts as active (D944): at least one
|
|
60959
|
+
* included member is, or every included member is. What "active" means is the
|
|
60960
|
+
* type's: a cover is open, a lock is locked, a media player is playing, a
|
|
60961
|
+
* climate device is not off. A group of lights, switches and fans reports
|
|
60962
|
+
* `switch.on` instead; this cap is for the types whose own state is not a
|
|
60963
|
+
* yes/no. Unknown members are left out (`group-status` counts them) and an
|
|
60964
|
+
* empty included set takes the device offline, never `active: false`.
|
|
60965
|
+
*/
|
|
60966
|
+
var GroupActiveSchema = object({
|
|
60967
|
+
active: boolean().meta({ label: "Active" }),
|
|
60968
|
+
/** Ms epoch when the slice was last updated. */
|
|
60969
|
+
lastChangedAt: number()
|
|
60970
|
+
});
|
|
60971
|
+
var groupActiveCapability = {
|
|
60972
|
+
name: "group-active",
|
|
60973
|
+
scope: "device",
|
|
60974
|
+
deviceNative: true,
|
|
60975
|
+
mode: "singleton",
|
|
60976
|
+
deviceTypes: [
|
|
60977
|
+
DeviceType.Cover,
|
|
60978
|
+
DeviceType.Valve,
|
|
60979
|
+
DeviceType.Lock,
|
|
60980
|
+
DeviceType.MediaPlayer,
|
|
60981
|
+
DeviceType.Climate,
|
|
60982
|
+
DeviceType.Thermostat
|
|
60983
|
+
],
|
|
60984
|
+
methods: {},
|
|
60985
|
+
status: {
|
|
60986
|
+
schema: GroupActiveSchema,
|
|
60987
|
+
kind: "push"
|
|
60988
|
+
},
|
|
60989
|
+
runtimeState: GroupActiveSchema,
|
|
60990
|
+
/**
|
|
60991
|
+
* Runtime-state durability: **session** — recomputed from the members on the
|
|
60992
|
+
* next source event; a restored value would outlive the members it named.
|
|
60993
|
+
*
|
|
60994
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
60995
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
60996
|
+
*/
|
|
60997
|
+
durability: "session",
|
|
60998
|
+
history: "none"
|
|
60999
|
+
};
|
|
61000
|
+
/**
|
|
60544
61001
|
* How many members of a helper group answered (D944). Counts are integers,
|
|
60545
61002
|
* not measurements: zero included is a real count, never a stand-in reading.
|
|
60546
61003
|
* The reading itself stays on the output capability. History is none — the
|
|
@@ -60571,7 +61028,14 @@ var groupStatusCapability = {
|
|
|
60571
61028
|
deviceTypes: [
|
|
60572
61029
|
DeviceType.Sensor,
|
|
60573
61030
|
DeviceType.Switch,
|
|
60574
|
-
DeviceType.Light
|
|
61031
|
+
DeviceType.Light,
|
|
61032
|
+
DeviceType.Fan,
|
|
61033
|
+
DeviceType.Cover,
|
|
61034
|
+
DeviceType.Valve,
|
|
61035
|
+
DeviceType.Lock,
|
|
61036
|
+
DeviceType.MediaPlayer,
|
|
61037
|
+
DeviceType.Thermostat,
|
|
61038
|
+
DeviceType.Climate
|
|
60575
61039
|
],
|
|
60576
61040
|
methods: {},
|
|
60577
61041
|
status: {
|
|
@@ -61844,7 +62308,14 @@ var lockControlCapability = {
|
|
|
61844
62308
|
}),
|
|
61845
62309
|
unlock: method(object({
|
|
61846
62310
|
deviceId: number().int().nonnegative(),
|
|
61847
|
-
code: string().min(1).optional()
|
|
62311
|
+
code: string().min(1).optional(),
|
|
62312
|
+
/**
|
|
62313
|
+
* Honoured by a helper lock GROUP only (D944): unlocking several doors
|
|
62314
|
+
* with one call is refused unless the caller says it was confirmed. A
|
|
62315
|
+
* single lock ignores it. The refusal lives in the callee, so no
|
|
62316
|
+
* automation or API caller reaches the doors by skipping a dialog.
|
|
62317
|
+
*/
|
|
62318
|
+
confirm: boolean().optional()
|
|
61848
62319
|
}), _void(), {
|
|
61849
62320
|
kind: "mutation",
|
|
61850
62321
|
auth: "protected"
|
|
@@ -61881,6 +62352,44 @@ var lockControlCapability = {
|
|
|
61881
62352
|
volatileStateFields: ["lastChangedAt"]
|
|
61882
62353
|
};
|
|
61883
62354
|
/**
|
|
62355
|
+
* A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
|
|
62356
|
+
* so dashboards keep one series; every other instantaneous reading lands here.
|
|
62357
|
+
* `value` is the time integral in the source quantity's accumulated base
|
|
62358
|
+
* (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
|
|
62359
|
+
*/
|
|
62360
|
+
var MeterStatusSchema = object({
|
|
62361
|
+
value: number().nullable().meta(readingMeta("device", { label: "Total" })),
|
|
62362
|
+
lastFetchedAt: number()
|
|
62363
|
+
});
|
|
62364
|
+
var meterCapability = {
|
|
62365
|
+
name: "meter",
|
|
62366
|
+
scope: "device",
|
|
62367
|
+
deviceNative: true,
|
|
62368
|
+
mode: "singleton",
|
|
62369
|
+
deviceTypes: [DeviceType.Sensor],
|
|
62370
|
+
methods: {},
|
|
62371
|
+
status: {
|
|
62372
|
+
schema: MeterStatusSchema,
|
|
62373
|
+
kind: "push"
|
|
62374
|
+
},
|
|
62375
|
+
runtimeState: MeterStatusSchema,
|
|
62376
|
+
/**
|
|
62377
|
+
* Runtime-state durability: **restored** — the total is a cache of the
|
|
62378
|
+
* history integral. A restart shows it only until the history pass
|
|
62379
|
+
* recomputes the open period; it is not an in-memory buffer.
|
|
62380
|
+
*
|
|
62381
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
62382
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
62383
|
+
*/
|
|
62384
|
+
durability: "restored",
|
|
62385
|
+
history: { fields: { value: {
|
|
62386
|
+
kind: "counter",
|
|
62387
|
+
deadband: .001,
|
|
62388
|
+
minIntervalMs: 6e4
|
|
62389
|
+
} } },
|
|
62390
|
+
volatileStateFields: ["lastFetchedAt"]
|
|
62391
|
+
};
|
|
62392
|
+
/**
|
|
61884
62393
|
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
61885
62394
|
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
61886
62395
|
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
@@ -64407,7 +64916,12 @@ var powerMeterCapability = {
|
|
|
64407
64916
|
scope: "device",
|
|
64408
64917
|
deviceNative: true,
|
|
64409
64918
|
mode: "singleton",
|
|
64410
|
-
deviceTypes: [
|
|
64919
|
+
deviceTypes: [
|
|
64920
|
+
DeviceType.Sensor,
|
|
64921
|
+
DeviceType.Washer,
|
|
64922
|
+
DeviceType.Dryer,
|
|
64923
|
+
DeviceType.Dishwasher
|
|
64924
|
+
],
|
|
64411
64925
|
methods: {},
|
|
64412
64926
|
status: {
|
|
64413
64927
|
schema: PowerMeterStatusSchema,
|
|
@@ -65145,6 +65659,8 @@ var runningCustomize = {
|
|
|
65145
65659
|
stateField: "running"
|
|
65146
65660
|
},
|
|
65147
65661
|
deviceTypes: [
|
|
65662
|
+
DeviceType.Washer,
|
|
65663
|
+
DeviceType.Dryer,
|
|
65148
65664
|
DeviceType.Dishwasher,
|
|
65149
65665
|
DeviceType.Vacuum,
|
|
65150
65666
|
DeviceType.LawnMower,
|
|
@@ -67634,7 +68150,8 @@ var switchCapability = {
|
|
|
67634
68150
|
deviceTypes: [
|
|
67635
68151
|
DeviceType.Switch,
|
|
67636
68152
|
DeviceType.Siren,
|
|
67637
|
-
DeviceType.Light
|
|
68153
|
+
DeviceType.Light,
|
|
68154
|
+
DeviceType.Fan
|
|
67638
68155
|
],
|
|
67639
68156
|
methods: { setState: method(object({
|
|
67640
68157
|
deviceId: number(),
|
|
@@ -69847,8 +70364,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
|
|
|
69847
70364
|
kind: "mutation",
|
|
69848
70365
|
auth: "admin"
|
|
69849
70366
|
});
|
|
70367
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
69850
70368
|
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;
|
|
70369
|
+
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
70370
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
69853
70371
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
69854
70372
|
new Set(CAP_NAMES);
|
|
@@ -70177,6 +70695,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
70177
70695
|
featureProbe: featureProbeCapability,
|
|
70178
70696
|
flood: floodCapability,
|
|
70179
70697
|
gas: gasCapability,
|
|
70698
|
+
groupActive: groupActiveCapability,
|
|
70180
70699
|
groupCommands: groupCommandsCapability,
|
|
70181
70700
|
groupStatus: groupStatusCapability,
|
|
70182
70701
|
heatingValve: heatingValveCapability,
|
|
@@ -70188,6 +70707,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
70188
70707
|
lawnMowerControl: lawnMowerControlCapability,
|
|
70189
70708
|
lockControl: lockControlCapability,
|
|
70190
70709
|
mediaPlayer: mediaPlayerCapability,
|
|
70710
|
+
meter: meterCapability,
|
|
70191
70711
|
motion: motionCapability,
|
|
70192
70712
|
motionTrigger: motionTriggerCapability,
|
|
70193
70713
|
motionZones: motionZonesCapability,
|
|
@@ -71447,7 +71967,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
71447
71967
|
return toDeviceSummary(device, this.addonId);
|
|
71448
71968
|
}
|
|
71449
71969
|
};
|
|
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;
|
|
71970
|
+
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
71971
|
new Set(Object.values(DeviceType));
|
|
71452
71972
|
Object.freeze({
|
|
71453
71973
|
"accessories.getStatus": {
|
|
@@ -74670,6 +75190,13 @@ Object.freeze({
|
|
|
74670
75190
|
access: "view",
|
|
74671
75191
|
role: "viewer"
|
|
74672
75192
|
},
|
|
75193
|
+
"groupActive.getStatus": {
|
|
75194
|
+
capName: "group-active",
|
|
75195
|
+
capScope: "device",
|
|
75196
|
+
addonId: null,
|
|
75197
|
+
access: "view",
|
|
75198
|
+
role: "viewer"
|
|
75199
|
+
},
|
|
74673
75200
|
"groupCommands.getStatus": {
|
|
74674
75201
|
capName: "group-commands",
|
|
74675
75202
|
capScope: "device",
|
|
@@ -75636,6 +76163,13 @@ Object.freeze({
|
|
|
75636
76163
|
access: "create",
|
|
75637
76164
|
role: "admin"
|
|
75638
76165
|
},
|
|
76166
|
+
"meter.getStatus": {
|
|
76167
|
+
capName: "meter",
|
|
76168
|
+
capScope: "device",
|
|
76169
|
+
addonId: null,
|
|
76170
|
+
access: "view",
|
|
76171
|
+
role: "viewer"
|
|
76172
|
+
},
|
|
75639
76173
|
"metricsProvider.collectSnapshot": {
|
|
75640
76174
|
capName: "metrics-provider",
|
|
75641
76175
|
capScope: "system",
|
|
@@ -83734,7 +84268,39 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
83734
84268
|
phrase: t("When {device} finishes", "Quando {device} finisce"),
|
|
83735
84269
|
icon: "square"
|
|
83736
84270
|
}
|
|
83737
|
-
}
|
|
84271
|
+
},
|
|
84272
|
+
overrides: { byType: {
|
|
84273
|
+
[DeviceType.Washer]: {
|
|
84274
|
+
state: { running: {
|
|
84275
|
+
whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
|
|
84276
|
+
whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
|
|
84277
|
+
} },
|
|
84278
|
+
events: {
|
|
84279
|
+
onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
|
|
84280
|
+
onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
|
|
84281
|
+
}
|
|
84282
|
+
},
|
|
84283
|
+
[DeviceType.Dryer]: {
|
|
84284
|
+
state: { running: {
|
|
84285
|
+
whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
|
|
84286
|
+
whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
|
|
84287
|
+
} },
|
|
84288
|
+
events: {
|
|
84289
|
+
onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
|
|
84290
|
+
onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
|
|
84291
|
+
}
|
|
84292
|
+
},
|
|
84293
|
+
[DeviceType.Dishwasher]: {
|
|
84294
|
+
state: { running: {
|
|
84295
|
+
whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
|
|
84296
|
+
whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
|
|
84297
|
+
} },
|
|
84298
|
+
events: {
|
|
84299
|
+
onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
|
|
84300
|
+
onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
|
|
84301
|
+
}
|
|
84302
|
+
}
|
|
84303
|
+
} }
|
|
83738
84304
|
},
|
|
83739
84305
|
update: {
|
|
83740
84306
|
scope: "device",
|
|
@@ -84506,6 +85072,10 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
84506
85072
|
icon: "flame"
|
|
84507
85073
|
}]
|
|
84508
85074
|
},
|
|
85075
|
+
"group-active": {
|
|
85076
|
+
scope: "device",
|
|
85077
|
+
noun: t("Group state", "Stato del gruppo")
|
|
85078
|
+
},
|
|
84509
85079
|
"group-commands": {
|
|
84510
85080
|
scope: "device",
|
|
84511
85081
|
noun: t("Group command", "Comando di gruppo")
|
|
@@ -84562,6 +85132,17 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
84562
85132
|
}],
|
|
84563
85133
|
events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
|
|
84564
85134
|
},
|
|
85135
|
+
meter: {
|
|
85136
|
+
scope: "device",
|
|
85137
|
+
noun: t("Meter", "Contatore"),
|
|
85138
|
+
state: [{
|
|
85139
|
+
kind: "numeric",
|
|
85140
|
+
field: "value",
|
|
85141
|
+
quantity: t("the total", "il totale"),
|
|
85142
|
+
label: t("Total", "Totale"),
|
|
85143
|
+
icon: "gauge"
|
|
85144
|
+
}]
|
|
85145
|
+
},
|
|
84565
85146
|
"numeric-sensor": {
|
|
84566
85147
|
scope: "device",
|
|
84567
85148
|
noun: t("Sensor", "Sensore"),
|
|
@@ -85034,8 +85615,47 @@ function projectAction(cap, action) {
|
|
|
85034
85615
|
};
|
|
85035
85616
|
}
|
|
85036
85617
|
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;
|
|
85618
|
+
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
85619
|
DeviceFeature.BatteryOperated;
|
|
85620
|
+
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
85621
|
+
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.";
|
|
85622
|
+
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.";
|
|
85623
|
+
var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
|
|
85624
|
+
var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
|
|
85625
|
+
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.";
|
|
85626
|
+
function withheld(reason) {
|
|
85627
|
+
return {
|
|
85628
|
+
[DeviceType.Washer]: {
|
|
85629
|
+
exported: false,
|
|
85630
|
+
reason
|
|
85631
|
+
},
|
|
85632
|
+
[DeviceType.Dryer]: {
|
|
85633
|
+
exported: false,
|
|
85634
|
+
reason
|
|
85635
|
+
},
|
|
85636
|
+
[DeviceType.Dishwasher]: {
|
|
85637
|
+
exported: false,
|
|
85638
|
+
reason
|
|
85639
|
+
}
|
|
85640
|
+
};
|
|
85641
|
+
}
|
|
85642
|
+
function offered(received) {
|
|
85643
|
+
return {
|
|
85644
|
+
[DeviceType.Washer]: {
|
|
85645
|
+
exported: true,
|
|
85646
|
+
as: received
|
|
85647
|
+
},
|
|
85648
|
+
[DeviceType.Dryer]: {
|
|
85649
|
+
exported: true,
|
|
85650
|
+
as: received
|
|
85651
|
+
},
|
|
85652
|
+
[DeviceType.Dishwasher]: {
|
|
85653
|
+
exported: true,
|
|
85654
|
+
as: received
|
|
85655
|
+
}
|
|
85656
|
+
};
|
|
85657
|
+
}
|
|
85658
|
+
offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
|
|
85039
85659
|
Object.freeze({
|
|
85040
85660
|
"accessories.getStatus": [{
|
|
85041
85661
|
name: "deviceId",
|
|
@@ -86002,6 +86622,11 @@ Object.freeze({
|
|
|
86002
86622
|
form: "single",
|
|
86003
86623
|
optional: false
|
|
86004
86624
|
}],
|
|
86625
|
+
"groupActive.getStatus": [{
|
|
86626
|
+
name: "deviceId",
|
|
86627
|
+
form: "single",
|
|
86628
|
+
optional: false
|
|
86629
|
+
}],
|
|
86005
86630
|
"groupCommands.getStatus": [{
|
|
86006
86631
|
name: "deviceId",
|
|
86007
86632
|
form: "single",
|
|
@@ -86212,6 +86837,11 @@ Object.freeze({
|
|
|
86212
86837
|
form: "single",
|
|
86213
86838
|
optional: false
|
|
86214
86839
|
}],
|
|
86840
|
+
"meter.getStatus": [{
|
|
86841
|
+
name: "deviceId",
|
|
86842
|
+
form: "single",
|
|
86843
|
+
optional: false
|
|
86844
|
+
}],
|
|
86215
86845
|
"motion.getStatus": [{
|
|
86216
86846
|
name: "deviceId",
|
|
86217
86847
|
form: "single",
|
|
@@ -87819,7 +88449,6 @@ Object.freeze({
|
|
|
87819
88449
|
"network-access": "ingress",
|
|
87820
88450
|
"smtp-provider": "email"
|
|
87821
88451
|
});
|
|
87822
|
-
DeviceType.Camera;
|
|
87823
88452
|
/**
|
|
87824
88453
|
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
87825
88454
|
* `z.infer`, they do not restate the shape.
|
|
@@ -87965,6 +88594,213 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
87965
88594
|
samplingSeconds: 10
|
|
87966
88595
|
};
|
|
87967
88596
|
new Set(["devices", "classes"]);
|
|
88597
|
+
/** Strip a small, safe subset of Markdown down to plain text. */
|
|
88598
|
+
function markdownToText(md) {
|
|
88599
|
+
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();
|
|
88600
|
+
}
|
|
88601
|
+
var HTML_ENTITIES = [
|
|
88602
|
+
[/ /g, " "],
|
|
88603
|
+
[/&/g, "&"],
|
|
88604
|
+
[/</g, "<"],
|
|
88605
|
+
[/>/g, ">"],
|
|
88606
|
+
[/"/g, "\""],
|
|
88607
|
+
[/'/g, "'"]
|
|
88608
|
+
];
|
|
88609
|
+
/** Strip HTML tags, decode a few entities, map block/br elements to newlines. */
|
|
88610
|
+
function htmlToText(html) {
|
|
88611
|
+
let out = html.replace(/<\s*br\s*\/?\s*>/gi, "\n").replace(/<\s*\/\s*(p|div|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]+>/g, "");
|
|
88612
|
+
for (const [re, rep] of HTML_ENTITIES) out = out.replace(re, rep);
|
|
88613
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
88614
|
+
}
|
|
88615
|
+
var HTML_ESCAPES = [
|
|
88616
|
+
[/&/g, "&"],
|
|
88617
|
+
[/</g, "<"],
|
|
88618
|
+
[/>/g, ">"],
|
|
88619
|
+
[/"/g, """],
|
|
88620
|
+
[/'/g, "'"]
|
|
88621
|
+
];
|
|
88622
|
+
function escapeHtml(text) {
|
|
88623
|
+
let out = text;
|
|
88624
|
+
for (const [re, rep] of HTML_ESCAPES) out = out.replace(re, rep);
|
|
88625
|
+
return out;
|
|
88626
|
+
}
|
|
88627
|
+
/** Plain text → HTML-lite (escape + newlines to <br>). */
|
|
88628
|
+
function textToHtml(text) {
|
|
88629
|
+
return escapeHtml(text).replace(/\n/g, "<br>");
|
|
88630
|
+
}
|
|
88631
|
+
/** Markdown → HTML-lite (bold/italic/code/links; used e.g. for Pushover). */
|
|
88632
|
+
function markdownToHtmlLite(md) {
|
|
88633
|
+
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>");
|
|
88634
|
+
}
|
|
88635
|
+
/**
|
|
88636
|
+
* Preference order when the source format is NOT supported — first supported
|
|
88637
|
+
* entry wins. Encodes the Apprise-style "degrade DOWN, but preserve rendering
|
|
88638
|
+
* where a richer target exists" policy:
|
|
88639
|
+
* - markdown → html (html-lite) before text (strip)
|
|
88640
|
+
* - html → text before markdown
|
|
88641
|
+
* - text → markdown before html (text is valid markdown)
|
|
88642
|
+
*/
|
|
88643
|
+
var DEGRADE_ORDER = {
|
|
88644
|
+
text: [
|
|
88645
|
+
"text",
|
|
88646
|
+
"markdown",
|
|
88647
|
+
"html"
|
|
88648
|
+
],
|
|
88649
|
+
markdown: [
|
|
88650
|
+
"markdown",
|
|
88651
|
+
"html",
|
|
88652
|
+
"text"
|
|
88653
|
+
],
|
|
88654
|
+
html: [
|
|
88655
|
+
"html",
|
|
88656
|
+
"text",
|
|
88657
|
+
"markdown"
|
|
88658
|
+
]
|
|
88659
|
+
};
|
|
88660
|
+
/**
|
|
88661
|
+
* Pick the best target format the kind supports for a given source format.
|
|
88662
|
+
*
|
|
88663
|
+
* TOLERANT of a source outside the union. `NotificationSchema.format` carries
|
|
88664
|
+
* `.default('text')`, but that default only runs where the schema is PARSED —
|
|
88665
|
+
* the tRPC router. A cap call arriving from another addon (`ctx.api`) skips
|
|
88666
|
+
* that parse, so `format` reaches here `undefined`, and the bare
|
|
88667
|
+
* `DEGRADE_ORDER[source]` threw `is not iterable`. That broke the one promise
|
|
88668
|
+
* the degrade path makes ("never throws — degradation is reported, never
|
|
88669
|
+
* surfaced as an error") and dead-lettered every Notification-Center delivery
|
|
88670
|
+
* for a day (2026-07-30). An unknown source degrades like `text`.
|
|
88671
|
+
*/
|
|
88672
|
+
function resolveFormat(source, supported) {
|
|
88673
|
+
for (const candidate of DEGRADE_ORDER[source] ?? DEGRADE_ORDER.text) if (supported.includes(candidate)) return candidate;
|
|
88674
|
+
return supported[0] ?? "text";
|
|
88675
|
+
}
|
|
88676
|
+
/** Transcode a body between formats. */
|
|
88677
|
+
function transcodeBody(body, from, to) {
|
|
88678
|
+
if (from === to) return body;
|
|
88679
|
+
if (to === "text") {
|
|
88680
|
+
if (from === "markdown") return markdownToText(body);
|
|
88681
|
+
if (from === "html") return htmlToText(body);
|
|
88682
|
+
}
|
|
88683
|
+
if (to === "html") {
|
|
88684
|
+
if (from === "text") return textToHtml(body);
|
|
88685
|
+
if (from === "markdown") return markdownToHtmlLite(body);
|
|
88686
|
+
}
|
|
88687
|
+
if (to === "markdown") {
|
|
88688
|
+
if (from === "text") return body;
|
|
88689
|
+
if (from === "html") return htmlToText(body);
|
|
88690
|
+
}
|
|
88691
|
+
return body;
|
|
88692
|
+
}
|
|
88693
|
+
function toResolvedLevel(level) {
|
|
88694
|
+
return {
|
|
88695
|
+
id: level.id,
|
|
88696
|
+
ordinal: level.ordinal,
|
|
88697
|
+
...level.flags ? { flags: level.flags } : {},
|
|
88698
|
+
...level.requires ? { requires: level.requires } : {}
|
|
88699
|
+
};
|
|
88700
|
+
}
|
|
88701
|
+
function resolveLevel(levels, n) {
|
|
88702
|
+
if (levels.length === 0) return null;
|
|
88703
|
+
if (n.level !== void 0) {
|
|
88704
|
+
const explicit = levels.find((l) => l.id === n.level);
|
|
88705
|
+
if (explicit) return toResolvedLevel(explicit);
|
|
88706
|
+
}
|
|
88707
|
+
const withOrdinal = levels.filter((l) => l.ordinal !== null);
|
|
88708
|
+
if (withOrdinal.length === 0) return toResolvedLevel(levels[0]);
|
|
88709
|
+
const priority = n.priority;
|
|
88710
|
+
return toResolvedLevel([...withOrdinal].sort((a, b) => {
|
|
88711
|
+
const da = Math.abs((a.ordinal ?? 0) - priority);
|
|
88712
|
+
const db = Math.abs((b.ordinal ?? 0) - priority);
|
|
88713
|
+
if (da !== db) return da - db;
|
|
88714
|
+
return (b.ordinal ?? 0) - (a.ordinal ?? 0);
|
|
88715
|
+
})[0]);
|
|
88716
|
+
}
|
|
88717
|
+
function splitBody(body, maxLen) {
|
|
88718
|
+
if (!Number.isFinite(maxLen) || maxLen <= 0 || body.length <= maxLen) return [body];
|
|
88719
|
+
const parts = [];
|
|
88720
|
+
for (let i = 0; i < body.length; i += maxLen) parts.push(body.slice(i, i + maxLen));
|
|
88721
|
+
return parts;
|
|
88722
|
+
}
|
|
88723
|
+
/**
|
|
88724
|
+
* Prepare a canonical notification for one kind. Never throws — degradation is
|
|
88725
|
+
* always reported in `renderedAs`, never surfaced as an error.
|
|
88726
|
+
*/
|
|
88727
|
+
function prepareNotification(caps, n) {
|
|
88728
|
+
const dropped = [];
|
|
88729
|
+
const sourceFormat = n.format;
|
|
88730
|
+
const format = resolveFormat(sourceFormat, caps.format);
|
|
88731
|
+
if (format !== sourceFormat) dropped.push("format");
|
|
88732
|
+
const transcoded = transcodeBody(n.body, sourceFormat, format);
|
|
88733
|
+
const level = resolveLevel(caps.levels, n);
|
|
88734
|
+
const inAttachments = n.attachments ?? [];
|
|
88735
|
+
const attachments = [];
|
|
88736
|
+
for (const att of inAttachments) {
|
|
88737
|
+
if (!caps.attachments.mediaTypes.includes(att.mediaType)) {
|
|
88738
|
+
dropped.push(`attachment:${att.mediaType}`);
|
|
88739
|
+
continue;
|
|
88740
|
+
}
|
|
88741
|
+
if (caps.attachments.mode === "url" && att.url === void 0) {
|
|
88742
|
+
dropped.push("attachment:noUrl");
|
|
88743
|
+
continue;
|
|
88744
|
+
}
|
|
88745
|
+
if (att.bytes !== void 0 && caps.attachments.maxBytes !== void 0 && att.bytes.length > caps.attachments.maxBytes) {
|
|
88746
|
+
dropped.push("attachment:maxBytes");
|
|
88747
|
+
continue;
|
|
88748
|
+
}
|
|
88749
|
+
if (attachments.length >= caps.attachments.max) {
|
|
88750
|
+
dropped.push("attachment:max");
|
|
88751
|
+
continue;
|
|
88752
|
+
}
|
|
88753
|
+
const needsFetch = caps.attachments.mode === "bytes" && att.bytes === void 0 && att.url !== void 0;
|
|
88754
|
+
attachments.push({
|
|
88755
|
+
mediaType: att.mediaType,
|
|
88756
|
+
...att.url !== void 0 ? { url: att.url } : {},
|
|
88757
|
+
...att.bytes !== void 0 ? { bytes: att.bytes } : {},
|
|
88758
|
+
...att.mime !== void 0 ? { mime: att.mime } : {},
|
|
88759
|
+
...att.name !== void 0 ? { name: att.name } : {},
|
|
88760
|
+
needsFetch
|
|
88761
|
+
});
|
|
88762
|
+
}
|
|
88763
|
+
const inActions = n.actions ?? [];
|
|
88764
|
+
const kept = inActions.slice(0, Math.max(0, caps.actions));
|
|
88765
|
+
if (inActions.length > kept.length) dropped.push("actions");
|
|
88766
|
+
const iconsSupported = caps.actionIcons === true;
|
|
88767
|
+
if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
|
|
88768
|
+
const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
|
|
88769
|
+
let clickUrl = null;
|
|
88770
|
+
if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
|
|
88771
|
+
else dropped.push("clickUrl");
|
|
88772
|
+
let sound = null;
|
|
88773
|
+
if (n.sound !== void 0) if (caps.sound) sound = n.sound;
|
|
88774
|
+
else dropped.push("sound");
|
|
88775
|
+
let ttl = null;
|
|
88776
|
+
if (n.ttl !== void 0) if (caps.ttl) ttl = n.ttl;
|
|
88777
|
+
else dropped.push("ttl");
|
|
88778
|
+
const tag = n.tag ?? null;
|
|
88779
|
+
const bodyParts = splitBody(transcoded, caps.bodyMaxLen);
|
|
88780
|
+
const truncated = bodyParts.length > 1;
|
|
88781
|
+
const renderedAs = {
|
|
88782
|
+
level: level?.id ?? "",
|
|
88783
|
+
format,
|
|
88784
|
+
attachmentsSent: attachments.length,
|
|
88785
|
+
actionsSent: actions.length,
|
|
88786
|
+
truncated,
|
|
88787
|
+
dropped
|
|
88788
|
+
};
|
|
88789
|
+
return {
|
|
88790
|
+
body: bodyParts[0] ?? "",
|
|
88791
|
+
...n.title !== void 0 ? { title: n.title } : {},
|
|
88792
|
+
format,
|
|
88793
|
+
level,
|
|
88794
|
+
attachments,
|
|
88795
|
+
actions,
|
|
88796
|
+
clickUrl,
|
|
88797
|
+
sound,
|
|
88798
|
+
ttl,
|
|
88799
|
+
tag,
|
|
88800
|
+
bodyParts,
|
|
88801
|
+
renderedAs
|
|
88802
|
+
};
|
|
88803
|
+
}
|
|
87968
88804
|
new Set(NC_LEGACY_SYSTEM_EVENT_KINDS);
|
|
87969
88805
|
/**
|
|
87970
88806
|
* Every filter, for the callers that must cover all of them (guards, tests).
|
|
@@ -89186,12 +90022,62 @@ var alexaProxyLoginSchema = object({
|
|
|
89186
90022
|
proxyHost: string().default("").describe("Host/IP the browser uses to reach the login proxy (blank = auto-detect)"),
|
|
89187
90023
|
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
90024
|
});
|
|
90025
|
+
/**
|
|
90026
|
+
* Echo speech settings. Speech signs in as an import broker, so the targets
|
|
90027
|
+
* are what lives here. `email`, `password`, `region` and `session` are the OLD
|
|
90028
|
+
* standalone login: they are read once to migrate it onto the single broker
|
|
90029
|
+
* (see `speak/migration.ts`) and written back empty.
|
|
90030
|
+
*/
|
|
90031
|
+
var alexaSpeakSettingsSchema = object({
|
|
90032
|
+
email: string().default(""),
|
|
90033
|
+
password: string().default("").describe("Legacy Amazon password").meta({ secret: true }),
|
|
90034
|
+
region: _enum([
|
|
90035
|
+
"NA",
|
|
90036
|
+
"EU",
|
|
90037
|
+
"FE"
|
|
90038
|
+
]).default("EU"),
|
|
90039
|
+
session: string().default("").describe("Legacy Alexa session blob").meta({ secret: true }),
|
|
90040
|
+
targets: array(object({
|
|
90041
|
+
id: string(),
|
|
90042
|
+
name: string(),
|
|
90043
|
+
enabled: boolean(),
|
|
90044
|
+
/** The broker (Amazon account) the Echo belongs to; '' until one is chosen. */
|
|
90045
|
+
brokerId: string().default(""),
|
|
90046
|
+
echoId: string(),
|
|
90047
|
+
mode: _enum(["speak", "announce"]),
|
|
90048
|
+
volume: number(),
|
|
90049
|
+
respectDnd: boolean()
|
|
90050
|
+
})).default([])
|
|
90051
|
+
});
|
|
90052
|
+
function isRecord$3(value) {
|
|
90053
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90054
|
+
}
|
|
90055
|
+
/** Resolved settings. A keychain marker (an object) is an empty secret, not a password. */
|
|
90056
|
+
function readSpeakSettings(config) {
|
|
90057
|
+
const raw = isRecord$3(config.speak) ? config.speak : {};
|
|
90058
|
+
const region = raw["region"];
|
|
90059
|
+
const targets = raw["targets"];
|
|
90060
|
+
return alexaSpeakSettingsSchema.parse({
|
|
90061
|
+
email: typeof raw["email"] === "string" ? raw["email"] : "",
|
|
90062
|
+
password: typeof raw["password"] === "string" ? raw["password"] : "",
|
|
90063
|
+
session: typeof raw["session"] === "string" ? raw["session"] : "",
|
|
90064
|
+
...region === "NA" || region === "EU" || region === "FE" ? { region } : {},
|
|
90065
|
+
...Array.isArray(targets) ? { targets } : {}
|
|
90066
|
+
});
|
|
90067
|
+
}
|
|
89189
90068
|
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
89190
90069
|
var alexaAddonConfigSchema = object({
|
|
89191
90070
|
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
89192
90071
|
proxyLogin: alexaProxyLoginSchema.default({
|
|
89193
90072
|
proxyHost: "",
|
|
89194
90073
|
proxyPort: 3456
|
|
90074
|
+
}),
|
|
90075
|
+
speak: alexaSpeakSettingsSchema.default({
|
|
90076
|
+
email: "",
|
|
90077
|
+
password: "",
|
|
90078
|
+
region: "EU",
|
|
90079
|
+
session: "",
|
|
90080
|
+
targets: []
|
|
89195
90081
|
})
|
|
89196
90082
|
});
|
|
89197
90083
|
/** Narrow an `unknown` to a shallow plain-object record (cast-free boundary). */
|
|
@@ -112965,17 +113851,17 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
112965
113851
|
* @private
|
|
112966
113852
|
*/
|
|
112967
113853
|
var debug = require_src$2()("express:view");
|
|
112968
|
-
var path$
|
|
112969
|
-
var fs$
|
|
113854
|
+
var path$8 = __require("path");
|
|
113855
|
+
var fs$5 = __require("fs");
|
|
112970
113856
|
/**
|
|
112971
113857
|
* Module variables.
|
|
112972
113858
|
* @private
|
|
112973
113859
|
*/
|
|
112974
|
-
var dirname = path$
|
|
112975
|
-
var basename = path$
|
|
112976
|
-
var extname = path$
|
|
112977
|
-
var join = path$
|
|
112978
|
-
var resolve = path$
|
|
113860
|
+
var dirname = path$8.dirname;
|
|
113861
|
+
var basename = path$8.basename;
|
|
113862
|
+
var extname = path$8.extname;
|
|
113863
|
+
var join = path$8.join;
|
|
113864
|
+
var resolve = path$8.resolve;
|
|
112979
113865
|
/**
|
|
112980
113866
|
* Module exports.
|
|
112981
113867
|
* @public
|
|
@@ -113072,7 +113958,7 @@ var require_view = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
113072
113958
|
function tryStat(path) {
|
|
113073
113959
|
debug("stat \"%s\"", path);
|
|
113074
113960
|
try {
|
|
113075
|
-
return fs$
|
|
113961
|
+
return fs$5.statSync(path);
|
|
113076
113962
|
} catch (e) {
|
|
113077
113963
|
return;
|
|
113078
113964
|
}
|
|
@@ -115242,7 +116128,7 @@ var init_types = __esmMin((() => {
|
|
|
115242
116128
|
//#region ../../node_modules/send/node_modules/mime/mime.js
|
|
115243
116129
|
var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
115244
116130
|
__require("path");
|
|
115245
|
-
var fs$
|
|
116131
|
+
var fs$4 = __require("fs");
|
|
115246
116132
|
function Mime() {
|
|
115247
116133
|
this.types = Object.create(null);
|
|
115248
116134
|
this.extensions = Object.create(null);
|
|
@@ -115277,7 +116163,7 @@ var require_mime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115277
116163
|
Mime.prototype.load = function(file) {
|
|
115278
116164
|
this._loading = file;
|
|
115279
116165
|
var map = {};
|
|
115280
|
-
fs$
|
|
116166
|
+
fs$4.readFileSync(file, "ascii").split(/[\r\n]+/).forEach(function(line) {
|
|
115281
116167
|
var fields = line.replace(/\s*#.*|^\s*|\s*$/g, "").split(/\s+/);
|
|
115282
116168
|
map[fields.shift()] = fields;
|
|
115283
116169
|
});
|
|
@@ -115553,12 +116439,12 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115553
116439
|
var escapeHtml = require_escape_html();
|
|
115554
116440
|
var etag = require_etag();
|
|
115555
116441
|
var fresh = require_fresh();
|
|
115556
|
-
var fs$
|
|
116442
|
+
var fs$3 = __require("fs");
|
|
115557
116443
|
var mime = require_mime();
|
|
115558
116444
|
var ms = require_ms();
|
|
115559
116445
|
var onFinished = require_on_finished();
|
|
115560
116446
|
var parseRange = require_range_parser();
|
|
115561
|
-
var path$
|
|
116447
|
+
var path$7 = __require("path");
|
|
115562
116448
|
var statuses = require_statuses();
|
|
115563
116449
|
var Stream = __require("stream");
|
|
115564
116450
|
var util$4 = __require("util");
|
|
@@ -115566,11 +116452,11 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
115566
116452
|
* Path function references.
|
|
115567
116453
|
* @private
|
|
115568
116454
|
*/
|
|
115569
|
-
var extname = path$
|
|
115570
|
-
var join = path$
|
|
115571
|
-
var normalize = path$
|
|
115572
|
-
var resolve = path$
|
|
115573
|
-
var sep = path$
|
|
116455
|
+
var extname = path$7.extname;
|
|
116456
|
+
var join = path$7.join;
|
|
116457
|
+
var normalize = path$7.normalize;
|
|
116458
|
+
var resolve = path$7.resolve;
|
|
116459
|
+
var sep = path$7.sep;
|
|
115574
116460
|
/**
|
|
115575
116461
|
* Regular expression for identifying a bytes Range header.
|
|
115576
116462
|
* @private
|
|
@@ -116018,7 +116904,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116018
116904
|
var i = 0;
|
|
116019
116905
|
var self = this;
|
|
116020
116906
|
debug("stat \"%s\"", path);
|
|
116021
|
-
fs$
|
|
116907
|
+
fs$3.stat(path, function onstat(err, stat) {
|
|
116022
116908
|
if (err && err.code === "ENOENT" && !extname(path) && path[path.length - 1] !== sep) return next(err);
|
|
116023
116909
|
if (err) return self.onStatError(err);
|
|
116024
116910
|
if (stat.isDirectory()) return self.redirect(path);
|
|
@@ -116029,7 +116915,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116029
116915
|
if (self._extensions.length <= i) return err ? self.onStatError(err) : self.error(404);
|
|
116030
116916
|
var p = path + "." + self._extensions[i++];
|
|
116031
116917
|
debug("stat \"%s\"", p);
|
|
116032
|
-
fs$
|
|
116918
|
+
fs$3.stat(p, function(err, stat) {
|
|
116033
116919
|
if (err) return next(err);
|
|
116034
116920
|
if (stat.isDirectory()) return next();
|
|
116035
116921
|
self.emit("file", p, stat);
|
|
@@ -116053,7 +116939,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116053
116939
|
}
|
|
116054
116940
|
var p = join(path, self._index[i]);
|
|
116055
116941
|
debug("stat \"%s\"", p);
|
|
116056
|
-
fs$
|
|
116942
|
+
fs$3.stat(p, function(err, stat) {
|
|
116057
116943
|
if (err) return next(err);
|
|
116058
116944
|
if (stat.isDirectory()) return next();
|
|
116059
116945
|
self.emit("file", p, stat);
|
|
@@ -116072,7 +116958,7 @@ var require_send = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
116072
116958
|
SendStream.prototype.stream = function stream(path, options) {
|
|
116073
116959
|
var self = this;
|
|
116074
116960
|
var res = this.res;
|
|
116075
|
-
var stream = fs$
|
|
116961
|
+
var stream = fs$3.createReadStream(path, options);
|
|
116076
116962
|
this.emit("stream", stream);
|
|
116077
116963
|
stream.pipe(res);
|
|
116078
116964
|
function cleanup() {
|
|
@@ -117512,7 +118398,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
117512
118398
|
var deprecate = require_depd()("express");
|
|
117513
118399
|
var flatten = require_array_flatten();
|
|
117514
118400
|
var merge = require_utils_merge();
|
|
117515
|
-
var resolve$
|
|
118401
|
+
var resolve$2 = __require("path").resolve;
|
|
117516
118402
|
var setPrototypeOf = require_setprototypeof();
|
|
117517
118403
|
/**
|
|
117518
118404
|
* Module variables.
|
|
@@ -117575,7 +118461,7 @@ var require_application = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
117575
118461
|
this.mountpath = "/";
|
|
117576
118462
|
this.locals.settings = this.settings;
|
|
117577
118463
|
this.set("view", View);
|
|
117578
|
-
this.set("views", resolve$
|
|
118464
|
+
this.set("views", resolve$2("views"));
|
|
117579
118465
|
this.set("jsonp callback name", "callback");
|
|
117580
118466
|
if (env === "production") this.enable("view cache");
|
|
117581
118467
|
Object.defineProperty(this, "router", { get: function() {
|
|
@@ -119593,7 +120479,7 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
119593
120479
|
var http$3 = __require("http");
|
|
119594
120480
|
var isAbsolute = require_utils$2().isAbsolute;
|
|
119595
120481
|
var onFinished = require_on_finished();
|
|
119596
|
-
var path$
|
|
120482
|
+
var path$6 = __require("path");
|
|
119597
120483
|
var statuses = require_statuses();
|
|
119598
120484
|
var merge = require_utils_merge();
|
|
119599
120485
|
var sign = require_cookie_signature().sign;
|
|
@@ -119602,9 +120488,9 @@ var require_response = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
119602
120488
|
var setCharset = require_utils$2().setCharset;
|
|
119603
120489
|
var cookie = require_cookie();
|
|
119604
120490
|
var send = require_send();
|
|
119605
|
-
var extname = path$
|
|
120491
|
+
var extname = path$6.extname;
|
|
119606
120492
|
var mime = send.mime;
|
|
119607
|
-
var resolve = path$
|
|
120493
|
+
var resolve = path$6.resolve;
|
|
119608
120494
|
var vary = require_vary();
|
|
119609
120495
|
/**
|
|
119610
120496
|
* Response prototype.
|
|
@@ -120436,7 +121322,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
120436
121322
|
var encodeUrl = require_encodeurl();
|
|
120437
121323
|
var escapeHtml = require_escape_html();
|
|
120438
121324
|
var parseUrl = require_parseurl();
|
|
120439
|
-
var resolve = __require("path").resolve;
|
|
121325
|
+
var resolve$1 = __require("path").resolve;
|
|
120440
121326
|
var send = require_send();
|
|
120441
121327
|
var url$6 = __require("url");
|
|
120442
121328
|
/**
|
|
@@ -120460,7 +121346,7 @@ var require_serve_static = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
120460
121346
|
var setHeaders = opts.setHeaders;
|
|
120461
121347
|
if (setHeaders && typeof setHeaders !== "function") throw new TypeError("option setHeaders must be function");
|
|
120462
121348
|
opts.maxage = opts.maxage || opts.maxAge || 0;
|
|
120463
|
-
opts.root = resolve(root);
|
|
121349
|
+
opts.root = resolve$1(root);
|
|
120464
121350
|
var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
|
|
120465
121351
|
return function serveStatic(req, res, next) {
|
|
120466
121352
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -121586,7 +122472,7 @@ var require_has_flag = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
121586
122472
|
//#endregion
|
|
121587
122473
|
//#region ../../node_modules/supports-color/index.js
|
|
121588
122474
|
var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
121589
|
-
var os$
|
|
122475
|
+
var os$3 = __require("os");
|
|
121590
122476
|
var tty$1 = __require("tty");
|
|
121591
122477
|
var hasFlag = require_has_flag();
|
|
121592
122478
|
var { env } = process;
|
|
@@ -121622,7 +122508,7 @@ var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
121622
122508
|
const min = forceColor || 0;
|
|
121623
122509
|
if (env.TERM === "dumb") return min;
|
|
121624
122510
|
if (process.platform === "win32") {
|
|
121625
|
-
const osRelease = os$
|
|
122511
|
+
const osRelease = os$3.release().split(".");
|
|
121626
122512
|
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
121627
122513
|
return 1;
|
|
121628
122514
|
}
|
|
@@ -124129,7 +125015,7 @@ var require_braces = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124129
125015
|
//#endregion
|
|
124130
125016
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/constants.js
|
|
124131
125017
|
var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
124132
|
-
var path$
|
|
125018
|
+
var path$5 = __require("path");
|
|
124133
125019
|
var WIN_SLASH = "\\\\/";
|
|
124134
125020
|
var WIN_NO_SLASH = `[^${WIN_SLASH}]`;
|
|
124135
125021
|
var DEFAULT_MAX_EXTGLOB_RECURSION = 0;
|
|
@@ -124254,7 +125140,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124254
125140
|
CHAR_UNDERSCORE: 95,
|
|
124255
125141
|
CHAR_VERTICAL_LINE: 124,
|
|
124256
125142
|
CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279,
|
|
124257
|
-
SEP: path$
|
|
125143
|
+
SEP: path$5.sep,
|
|
124258
125144
|
/**
|
|
124259
125145
|
* Create EXTGLOB_CHARS
|
|
124260
125146
|
*/
|
|
@@ -124298,7 +125184,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
124298
125184
|
//#endregion
|
|
124299
125185
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/utils.js
|
|
124300
125186
|
var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
124301
|
-
var path$
|
|
125187
|
+
var path$4 = __require("path");
|
|
124302
125188
|
var win32 = process.platform === "win32";
|
|
124303
125189
|
var { REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL } = require_constants();
|
|
124304
125190
|
exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
|
|
@@ -124318,7 +125204,7 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
124318
125204
|
};
|
|
124319
125205
|
exports.isWindows = (options) => {
|
|
124320
125206
|
if (options && typeof options.windows === "boolean") return options.windows;
|
|
124321
|
-
return win32 === true || path$
|
|
125207
|
+
return win32 === true || path$4.sep === "\\";
|
|
124322
125208
|
};
|
|
124323
125209
|
exports.escapeLast = (input, char, lastIdx) => {
|
|
124324
125210
|
const idx = input.lastIndexOf(char, lastIdx);
|
|
@@ -125688,7 +126574,7 @@ var require_parse = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
125688
126574
|
//#endregion
|
|
125689
126575
|
//#region ../../node_modules/micromatch/node_modules/picomatch/lib/picomatch.js
|
|
125690
126576
|
var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
125691
|
-
var path$
|
|
126577
|
+
var path$3 = __require("path");
|
|
125692
126578
|
var scan = require_scan();
|
|
125693
126579
|
var parse = require_parse();
|
|
125694
126580
|
var utils = require_utils();
|
|
@@ -125827,7 +126713,7 @@ var require_picomatch$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
125827
126713
|
* @api public
|
|
125828
126714
|
*/
|
|
125829
126715
|
picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => {
|
|
125830
|
-
return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$
|
|
126716
|
+
return (glob instanceof RegExp ? glob : picomatch.makeRe(glob, options)).test(path$3.basename(input));
|
|
125831
126717
|
};
|
|
125832
126718
|
/**
|
|
125833
126719
|
* Returns true if **any** of the given glob `patterns` match the specified `string`.
|
|
@@ -126912,8 +127798,8 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126912
127798
|
var proxy = require_dist().createProxyMiddleware;
|
|
126913
127799
|
var querystring$2 = __require("querystring");
|
|
126914
127800
|
var cookieTools = require_cookie$1();
|
|
126915
|
-
var fs = __require("fs");
|
|
126916
|
-
var path$
|
|
127801
|
+
var fs$2 = __require("fs");
|
|
127802
|
+
var path$2 = __require("path");
|
|
126917
127803
|
var crypto$1 = __require("crypto");
|
|
126918
127804
|
var FORMERDATA_STORE_VERSION = 4;
|
|
126919
127805
|
function addCookies(Cookie, headers) {
|
|
@@ -126944,11 +127830,11 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126944
127830
|
}
|
|
126945
127831
|
function initAmazonProxy(_options, callbackCookie, callbackListening) {
|
|
126946
127832
|
const initialCookies = {};
|
|
126947
|
-
const formerDataStorePath = _options.formerDataStorePath || path$
|
|
127833
|
+
const formerDataStorePath = _options.formerDataStorePath || path$2.join(__dirname, "formerDataStore.json");
|
|
126948
127834
|
let formerDataStoreValid = false;
|
|
126949
127835
|
if (!_options.formerRegistrationData) try {
|
|
126950
|
-
if (fs.existsSync(formerDataStorePath)) {
|
|
126951
|
-
const formerDataStore = JSON.parse(fs.readFileSync(path$
|
|
127836
|
+
if (fs$2.existsSync(formerDataStorePath)) {
|
|
127837
|
+
const formerDataStore = JSON.parse(fs$2.readFileSync(path$2.join(__dirname, "formerDataStore.json"), "utf8"));
|
|
126952
127838
|
if (typeof formerDataStore === "object" && formerDataStore.storeVersion === FORMERDATA_STORE_VERSION) {
|
|
126953
127839
|
_options.formerRegistrationData = _options.formerRegistrationData || {};
|
|
126954
127840
|
_options.formerRegistrationData.frc = _options.formerRegistrationData.frc || formerDataStore.frc;
|
|
@@ -126989,7 +127875,7 @@ var require_proxy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
126989
127875
|
"map-md": initialCookies["map-md"],
|
|
126990
127876
|
"frc": initialCookies.frc
|
|
126991
127877
|
};
|
|
126992
|
-
fs.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
|
|
127878
|
+
fs$2.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), "utf8");
|
|
126993
127879
|
} catch (_err) {}
|
|
126994
127880
|
function base64URLEncode(str) {
|
|
126995
127881
|
return str.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
@@ -127199,7 +128085,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127199
128085
|
var https$1 = __require("https");
|
|
127200
128086
|
var querystring$1 = __require("querystring");
|
|
127201
128087
|
var url = __require("url");
|
|
127202
|
-
var os$
|
|
128088
|
+
var os$2 = __require("os");
|
|
127203
128089
|
var cookieTools = require_cookie$1();
|
|
127204
128090
|
var amazonProxy = require_proxy();
|
|
127205
128091
|
var defaultAmazonPage = "amazon.de";
|
|
@@ -127312,7 +128198,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127312
128198
|
else if (amazonDomain !== "com") _options.baseAmazonPageHandle = "";
|
|
127313
128199
|
else _options.baseAmazonPageHandle = "";
|
|
127314
128200
|
}
|
|
127315
|
-
if (!_options.userAgent) if (os$
|
|
128201
|
+
if (!_options.userAgent) if (os$2.platform() === "win32") _options.userAgent = defaultUserAgent;
|
|
127316
128202
|
else _options.userAgent = defaultUserAgentLinux;
|
|
127317
128203
|
_options.logger && _options.logger(`Alexa-Cookie: Use as User-Agent: ${_options.userAgent}`);
|
|
127318
128204
|
_options.acceptLanguage = _options.acceptLanguage || defaultAcceptLanguage;
|
|
@@ -127836,7 +128722,7 @@ var require_alexa_cookie = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
127836
128722
|
var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
127837
128723
|
var https = __require("https");
|
|
127838
128724
|
var querystring = __require("querystring");
|
|
127839
|
-
var os = __require("os");
|
|
128725
|
+
var os$1 = __require("os");
|
|
127840
128726
|
var extend = require_extend();
|
|
127841
128727
|
var AlexaHttp2Push = require_alexa_http2push();
|
|
127842
128728
|
var { v1: uuidv1 } = require_cjs();
|
|
@@ -127981,7 +128867,7 @@ var import_alexa_remote = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
|
|
|
127981
128867
|
init(cookie, callback) {
|
|
127982
128868
|
if (typeof cookie === "object") {
|
|
127983
128869
|
this._options = cookie;
|
|
127984
|
-
if (!this._options.userAgent) if (os.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
128870
|
+
if (!this._options.userAgent) if (os$1.platform() === "win32") this._options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
127985
128871
|
else this._options.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
|
|
127986
128872
|
if (this._options.apiUserAgentPostfix === void 0) this._options.apiUserAgentPostfix = `AlexaRemote/${__require(fsPath.join(__dirname, "package.json")).version}`;
|
|
127987
128873
|
this._options.amazonPage = this._options.amazonPage || "amazon.de";
|
|
@@ -131284,15 +132170,15 @@ function parseCookieMaterial(raw) {
|
|
|
131284
132170
|
}
|
|
131285
132171
|
}
|
|
131286
132172
|
/** True when `value` is a non-null, non-array plain object (cast-free guard). */
|
|
131287
|
-
function isRecord$
|
|
132173
|
+
function isRecord$2(value) {
|
|
131288
132174
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
131289
132175
|
}
|
|
131290
132176
|
/** Narrow a loaded JSON file to its `cookieData` (string | object), or null. */
|
|
131291
132177
|
function readCookieData(json) {
|
|
131292
|
-
if (!isRecord$
|
|
132178
|
+
if (!isRecord$2(json)) return null;
|
|
131293
132179
|
const value = json["cookieData"];
|
|
131294
132180
|
if (typeof value === "string") return value;
|
|
131295
|
-
if (isRecord$
|
|
132181
|
+
if (isRecord$2(value)) return value;
|
|
131296
132182
|
return null;
|
|
131297
132183
|
}
|
|
131298
132184
|
//#endregion
|
|
@@ -132267,7 +133153,7 @@ function mergeAlexaMotionStatus(cached, live, nowMs) {
|
|
|
132267
133153
|
};
|
|
132268
133154
|
}
|
|
132269
133155
|
/** True when `v` is a non-null, non-array object (cast-free guard). */
|
|
132270
|
-
function isRecord(v) {
|
|
133156
|
+
function isRecord$1(v) {
|
|
132271
133157
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
132272
133158
|
}
|
|
132273
133159
|
/**
|
|
@@ -132276,7 +133162,7 @@ function isRecord(v) {
|
|
|
132276
133162
|
*/
|
|
132277
133163
|
function decodeTemperatureCelsius(value) {
|
|
132278
133164
|
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
132279
|
-
if (!isRecord(value)) return null;
|
|
133165
|
+
if (!isRecord$1(value)) return null;
|
|
132280
133166
|
const raw = value["value"];
|
|
132281
133167
|
const n = typeof raw === "number" ? raw : Number(raw);
|
|
132282
133168
|
if (!Number.isFinite(n)) return null;
|
|
@@ -132287,7 +133173,7 @@ function decodeTemperatureCelsius(value) {
|
|
|
132287
133173
|
function decodeHumidityPercent(value) {
|
|
132288
133174
|
const direct = decodePercentage(value);
|
|
132289
133175
|
if (direct !== null) return direct;
|
|
132290
|
-
if (!isRecord(value)) return null;
|
|
133176
|
+
if (!isRecord$1(value)) return null;
|
|
132291
133177
|
return decodePercentage(value["value"]);
|
|
132292
133178
|
}
|
|
132293
133179
|
//#endregion
|
|
@@ -132623,6 +133509,11 @@ var alexaBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
132623
133509
|
}),
|
|
132624
133510
|
writeTrigger: "an operator links or removes an Alexa account broker"
|
|
132625
133511
|
});
|
|
133512
|
+
var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
|
|
133513
|
+
title: "Alexa Echo speech sessions",
|
|
133514
|
+
schema: entryBody({ session: string().default("").meta({ secret: true }) }),
|
|
133515
|
+
writeTrigger: "an Echo worker signs in as an Alexa account broker"
|
|
133516
|
+
});
|
|
132626
133517
|
//#endregion
|
|
132627
133518
|
//#region src/alexa-broker-store.ts
|
|
132628
133519
|
function rowToEntry(id, data) {
|
|
@@ -132679,13 +133570,1627 @@ var AlexaBrokerStore = class AlexaBrokerStore {
|
|
|
132679
133570
|
}
|
|
132680
133571
|
};
|
|
132681
133572
|
//#endregion
|
|
133573
|
+
//#region src/speak/constants.ts
|
|
133574
|
+
/** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
|
|
133575
|
+
var ALEXA_SPEAK_ADDON_ID = "import-alexa";
|
|
133576
|
+
var ALEXA_SPEAK_KIND = "alexa-echo";
|
|
133577
|
+
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
133578
|
+
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
133579
|
+
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
133580
|
+
//#endregion
|
|
133581
|
+
//#region src/speak/worker-protocol.ts
|
|
133582
|
+
/** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
|
|
133583
|
+
var EchoDeviceSchema = object({
|
|
133584
|
+
id: string().min(1),
|
|
133585
|
+
name: string(),
|
|
133586
|
+
family: string(),
|
|
133587
|
+
online: boolean(),
|
|
133588
|
+
kind: _enum(["device", "group"]),
|
|
133589
|
+
dnd: boolean(),
|
|
133590
|
+
deviceType: string(),
|
|
133591
|
+
/** The room the account files the device under, when Amazon names one. */
|
|
133592
|
+
room: string().optional()
|
|
133593
|
+
});
|
|
133594
|
+
var LoginStatusSchema = _enum(["ok", "failed"]);
|
|
133595
|
+
var SessionStatusSchema = object({
|
|
133596
|
+
loggedIn: boolean(),
|
|
133597
|
+
ageSec: number().int().nonnegative(),
|
|
133598
|
+
lastSuccessAt: number().int().nullable()
|
|
133599
|
+
});
|
|
133600
|
+
var TargetDeliverySchema = object({
|
|
133601
|
+
targetId: string(),
|
|
133602
|
+
ok: boolean(),
|
|
133603
|
+
error: string().optional()
|
|
133604
|
+
});
|
|
133605
|
+
var WorkerLineSchema = union([object({
|
|
133606
|
+
id: string(),
|
|
133607
|
+
ok: literal(true),
|
|
133608
|
+
result: unknown().optional()
|
|
133609
|
+
}), object({
|
|
133610
|
+
id: string(),
|
|
133611
|
+
ok: literal(false),
|
|
133612
|
+
error: string()
|
|
133613
|
+
})]);
|
|
133614
|
+
function requestLine(id, request) {
|
|
133615
|
+
return `${JSON.stringify({
|
|
133616
|
+
id,
|
|
133617
|
+
...request
|
|
133618
|
+
})}\n`;
|
|
133619
|
+
}
|
|
133620
|
+
function parseLoginResult(result) {
|
|
133621
|
+
const parsed = object({
|
|
133622
|
+
status: LoginStatusSchema,
|
|
133623
|
+
error: string().optional()
|
|
133624
|
+
}).safeParse(result);
|
|
133625
|
+
if (!parsed.success) return {
|
|
133626
|
+
status: "failed",
|
|
133627
|
+
error: "worker returned an unexpected login result"
|
|
133628
|
+
};
|
|
133629
|
+
return parsed.data;
|
|
133630
|
+
}
|
|
133631
|
+
function parseDevices(result) {
|
|
133632
|
+
const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
|
|
133633
|
+
if (!parsed.success) throw new Error("worker returned an unexpected device list");
|
|
133634
|
+
return parsed.data.devices;
|
|
133635
|
+
}
|
|
133636
|
+
function parseDeliveries(result) {
|
|
133637
|
+
const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
|
|
133638
|
+
if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
|
|
133639
|
+
return parsed.data.outcomes;
|
|
133640
|
+
}
|
|
133641
|
+
function parseSessionStatus(result) {
|
|
133642
|
+
const parsed = SessionStatusSchema.safeParse(result);
|
|
133643
|
+
if (!parsed.success) throw new Error("worker returned an unexpected session status");
|
|
133644
|
+
return parsed.data;
|
|
133645
|
+
}
|
|
133646
|
+
//#endregion
|
|
133647
|
+
//#region src/speak/actions.ts
|
|
133648
|
+
var BrokerEchoStateSchema = _enum([
|
|
133649
|
+
"starting",
|
|
133650
|
+
"ready",
|
|
133651
|
+
"broker-not-signed-in",
|
|
133652
|
+
"failed"
|
|
133653
|
+
]);
|
|
133654
|
+
var SpeakStatusSchema = object({
|
|
133655
|
+
brokers: array(object({
|
|
133656
|
+
id: string(),
|
|
133657
|
+
name: string(),
|
|
133658
|
+
state: BrokerEchoStateSchema,
|
|
133659
|
+
error: string().optional(),
|
|
133660
|
+
devices: array(EchoDeviceSchema),
|
|
133661
|
+
session: SessionStatusSchema
|
|
133662
|
+
})),
|
|
133663
|
+
/** Targets that name no Amazon account yet (they cannot speak until one is chosen). */
|
|
133664
|
+
unassignedTargets: number().int().nonnegative(),
|
|
133665
|
+
/** What the old standalone login left behind, named; '' when nothing is pending. */
|
|
133666
|
+
notice: _enum([
|
|
133667
|
+
"",
|
|
133668
|
+
"no-broker",
|
|
133669
|
+
"choose-broker"
|
|
133670
|
+
])
|
|
133671
|
+
});
|
|
133672
|
+
var alexaSpeakActions = defineCustomActions({
|
|
133673
|
+
speakStatus: customAction(object({}), SpeakStatusSchema, {
|
|
133674
|
+
kind: "mutation",
|
|
133675
|
+
auth: "admin"
|
|
133676
|
+
}),
|
|
133677
|
+
sendTest: customAction(object({
|
|
133678
|
+
brokerId: string().min(1),
|
|
133679
|
+
echoIds: array(string().min(1)).min(1),
|
|
133680
|
+
mode: _enum(["speak", "announce"]),
|
|
133681
|
+
volume: number().int().min(0).max(100),
|
|
133682
|
+
text: string().min(1).max(450)
|
|
133683
|
+
}), object({ outcomes: array(object({
|
|
133684
|
+
echoId: string(),
|
|
133685
|
+
ok: boolean(),
|
|
133686
|
+
error: string().optional()
|
|
133687
|
+
})) }), {
|
|
133688
|
+
kind: "mutation",
|
|
133689
|
+
auth: "admin"
|
|
133690
|
+
})
|
|
133691
|
+
});
|
|
133692
|
+
//#endregion
|
|
133693
|
+
//#region src/speak/broker-seed.ts
|
|
133694
|
+
/**
|
|
133695
|
+
* What the Echo worker needs from an import broker to act as the same Amazon
|
|
133696
|
+
* account: the refresh token of the broker's own device registration. The
|
|
133697
|
+
* broker keeps that registration fresh (it persists `cookieData` after every
|
|
133698
|
+
* refresh), so the freshest copy is the persisted session file, then the
|
|
133699
|
+
* stored cookie blob, then a bare refresh token. A raw cookie string carries
|
|
133700
|
+
* no refresh token and gives no seed.
|
|
133701
|
+
*/
|
|
133702
|
+
/** The registration name alexapy sends when the broker did not record one. */
|
|
133703
|
+
var DEFAULT_DEVICE_APP_NAME = "Alexa Media Player";
|
|
133704
|
+
function isRecord(value) {
|
|
133705
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133706
|
+
}
|
|
133707
|
+
/** The registration blob alexa-remote2 hands back, from an object or its JSON text. */
|
|
133708
|
+
function asRegistration(value) {
|
|
133709
|
+
if (isRecord(value)) return value;
|
|
133710
|
+
if (typeof value !== "string") return null;
|
|
133711
|
+
const trimmed = value.trim();
|
|
133712
|
+
if (!trimmed.startsWith("{")) return null;
|
|
133713
|
+
try {
|
|
133714
|
+
const parsed = JSON.parse(trimmed);
|
|
133715
|
+
return isRecord(parsed) ? parsed : null;
|
|
133716
|
+
} catch {
|
|
133717
|
+
return null;
|
|
133718
|
+
}
|
|
133719
|
+
}
|
|
133720
|
+
function seedFromRegistration(value) {
|
|
133721
|
+
const registration = asRegistration(value);
|
|
133722
|
+
if (registration === null) return null;
|
|
133723
|
+
const token = registration["refreshToken"];
|
|
133724
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
133725
|
+
const name = registration["deviceAppName"];
|
|
133726
|
+
return {
|
|
133727
|
+
refreshToken: token,
|
|
133728
|
+
deviceAppName: typeof name === "string" && name.length > 0 ? name : DEFAULT_DEVICE_APP_NAME
|
|
133729
|
+
};
|
|
133730
|
+
}
|
|
133731
|
+
async function readPersistedRegistration(dataDir, brokerId) {
|
|
133732
|
+
if (dataDir === void 0) return null;
|
|
133733
|
+
try {
|
|
133734
|
+
const raw = await fs$1.readFile(path$1.join(dataDir, `${brokerId}.${SESSION_FILE}`), "utf8");
|
|
133735
|
+
const json = JSON.parse(raw);
|
|
133736
|
+
return isRecord(json) ? json["cookieData"] : null;
|
|
133737
|
+
} catch {
|
|
133738
|
+
return null;
|
|
133739
|
+
}
|
|
133740
|
+
}
|
|
133741
|
+
async function readBrokerSeed(dataDir, entry) {
|
|
133742
|
+
const persisted = seedFromRegistration(await readPersistedRegistration(dataDir, entry.id));
|
|
133743
|
+
if (persisted !== null) return persisted;
|
|
133744
|
+
const stored = seedFromRegistration(entry.connection.cookie);
|
|
133745
|
+
if (stored !== null) return stored;
|
|
133746
|
+
const bare = entry.connection.refreshToken;
|
|
133747
|
+
if (bare.length > 0) return {
|
|
133748
|
+
refreshToken: bare,
|
|
133749
|
+
deviceAppName: DEFAULT_DEVICE_APP_NAME
|
|
133750
|
+
};
|
|
133751
|
+
return null;
|
|
133752
|
+
}
|
|
133753
|
+
/** Amazon domain to the region the worker uses for the spoken-sequence locale. */
|
|
133754
|
+
function regionOfDomain(domain) {
|
|
133755
|
+
const tail = domain.toLowerCase().replace(/^(www|alexa)\./, "");
|
|
133756
|
+
if (tail === "amazon.com") return "NA";
|
|
133757
|
+
if (tail === "amazon.co.jp" || tail === "amazon.com.au" || tail === "amazon.in") return "FE";
|
|
133758
|
+
return "EU";
|
|
133759
|
+
}
|
|
133760
|
+
process.platform, process.env["SystemRoot"];
|
|
133761
|
+
/**
|
|
133762
|
+
* Files only their owner may read: the one place a secret-bearing file is
|
|
133763
|
+
* restricted, on every OS.
|
|
133764
|
+
*
|
|
133765
|
+
* `mode: 0o600` is a POSIX idea. On Windows Node maps it to the read-only
|
|
133766
|
+
* attribute and nothing else, and `chmod` is a no-op for permissions, so a
|
|
133767
|
+
* "0600" `agent.json` (the mesh private key) would have
|
|
133768
|
+
* been readable by every local user. Windows has its own tool for this:
|
|
133769
|
+
* `icacls`. This module removes the inherited ACL entries and grants access to
|
|
133770
|
+
* the current user and SYSTEM only — the Windows reading of "0600".
|
|
133771
|
+
*
|
|
133772
|
+
* ZERO imports beyond Node built-ins, on purpose: the Electron shell bundles
|
|
133773
|
+
* this single file (see `packages/desktop/tsup.config.ts`) instead of the
|
|
133774
|
+
* whole `@camstack/types` barrel.
|
|
133775
|
+
*
|
|
133776
|
+
* Failure is NOT swallowed. A secret file that could not be restricted is a
|
|
133777
|
+
* secret file the caller must know about, so `OwnerOnlyError` propagates; the
|
|
133778
|
+
* write order below makes sure nothing sensitive is in the file yet when that
|
|
133779
|
+
* happens (the temp file is empty until it has been restricted).
|
|
133780
|
+
*
|
|
133781
|
+
* Out of scope on purpose: files that are only tidy, not secret (the job
|
|
133782
|
+
* journal, the addon manifest) keep a plain `mode`, since one `icacls` per
|
|
133783
|
+
* write would be a process spawn on a hot path.
|
|
133784
|
+
*/
|
|
133785
|
+
/** `*S-1-5-18` is SYSTEM in every locale — a name like "SYSTEM" is not. */
|
|
133786
|
+
var WINDOWS_SYSTEM_SID = "*S-1-5-18";
|
|
133787
|
+
var FILE_MODE = 384;
|
|
133788
|
+
var DIR_MODE = 448;
|
|
133789
|
+
var OwnerOnlyError = class extends Error {
|
|
133790
|
+
target;
|
|
133791
|
+
constructor(target, detail) {
|
|
133792
|
+
super(`could not restrict ${target} to its owner: ${detail}`);
|
|
133793
|
+
this.target = target;
|
|
133794
|
+
this.name = "OwnerOnlyError";
|
|
133795
|
+
}
|
|
133796
|
+
};
|
|
133797
|
+
function currentPrincipal() {
|
|
133798
|
+
const user = process.env["USERNAME"] ?? userInfo().username;
|
|
133799
|
+
const domain = process.env["USERDOMAIN"];
|
|
133800
|
+
return domain !== void 0 && domain.length > 0 ? `${domain}\\${user}` : user;
|
|
133801
|
+
}
|
|
133802
|
+
/** Resolved per call, not at import: a platform stubbed by a test must be seen. */
|
|
133803
|
+
function resolveDeps(overrides) {
|
|
133804
|
+
return {
|
|
133805
|
+
platform: process.platform,
|
|
133806
|
+
exec: (file, args) => {
|
|
133807
|
+
execFileSync(file, [...args], {
|
|
133808
|
+
stdio: "pipe",
|
|
133809
|
+
windowsHide: true
|
|
133810
|
+
});
|
|
133811
|
+
},
|
|
133812
|
+
principal: currentPrincipal,
|
|
133813
|
+
systemRoot: process.env["SystemRoot"],
|
|
133814
|
+
...overrides
|
|
133815
|
+
};
|
|
133816
|
+
}
|
|
133817
|
+
/** The icacls argv for one path: no inheritance, owner + SYSTEM only. */
|
|
133818
|
+
function icaclsArgs(target, principal, isDirectory) {
|
|
133819
|
+
const inherit = isDirectory ? "(OI)(CI)" : "";
|
|
133820
|
+
return [
|
|
133821
|
+
target,
|
|
133822
|
+
"/inheritance:r",
|
|
133823
|
+
"/grant:r",
|
|
133824
|
+
`${principal}:${inherit}F`,
|
|
133825
|
+
"/grant:r",
|
|
133826
|
+
`${WINDOWS_SYSTEM_SID}:${inherit}F`
|
|
133827
|
+
];
|
|
133828
|
+
}
|
|
133829
|
+
function restrictWindows(target, isDirectory, deps) {
|
|
133830
|
+
const icacls = path$1.win32.join(deps.systemRoot ?? "C:\\Windows", "System32", "icacls.exe");
|
|
133831
|
+
try {
|
|
133832
|
+
deps.exec(icacls, icaclsArgs(target, deps.principal(), isDirectory));
|
|
133833
|
+
} catch (err) {
|
|
133834
|
+
throw new OwnerOnlyError(target, err instanceof Error ? err.message : String(err));
|
|
133835
|
+
}
|
|
133836
|
+
}
|
|
133837
|
+
/** Restrict an existing file to its owner (chmod 0600 / owner+SYSTEM ACL). */
|
|
133838
|
+
function restrictFileToOwnerSync(target, overrides = {}) {
|
|
133839
|
+
const deps = resolveDeps(overrides);
|
|
133840
|
+
if (deps.platform === "win32") {
|
|
133841
|
+
restrictWindows(target, false, deps);
|
|
133842
|
+
return;
|
|
133843
|
+
}
|
|
133844
|
+
fs.chmodSync(target, FILE_MODE);
|
|
133845
|
+
}
|
|
133846
|
+
/** Restrict an existing directory to its owner (chmod 0700 / owner+SYSTEM, inherited). */
|
|
133847
|
+
function restrictDirToOwnerSync(target, overrides = {}) {
|
|
133848
|
+
const deps = resolveDeps(overrides);
|
|
133849
|
+
if (deps.platform === "win32") {
|
|
133850
|
+
restrictWindows(target, true, deps);
|
|
133851
|
+
return;
|
|
133852
|
+
}
|
|
133853
|
+
fs.chmodSync(target, DIR_MODE);
|
|
133854
|
+
}
|
|
133855
|
+
/**
|
|
133856
|
+
* Atomically write a secret-bearing file that only its owner can read.
|
|
133857
|
+
*
|
|
133858
|
+
* Order matters: the temp file is created EMPTY, restricted, and only then
|
|
133859
|
+
* given its content, so there is no moment at which the secret exists under a
|
|
133860
|
+
* permissive ACL. The rename carries the (already restricted) ACL with it.
|
|
133861
|
+
*/
|
|
133862
|
+
function writeOwnerOnlyFileSync(target, data, overrides = {}) {
|
|
133863
|
+
fs.mkdirSync(path$1.dirname(target), { recursive: true });
|
|
133864
|
+
const tmp = `${target}.tmp.${process.pid}`;
|
|
133865
|
+
fs.writeFileSync(tmp, "", { mode: FILE_MODE });
|
|
133866
|
+
try {
|
|
133867
|
+
restrictFileToOwnerSync(tmp, overrides);
|
|
133868
|
+
fs.writeFileSync(tmp, data);
|
|
133869
|
+
fs.renameSync(tmp, target);
|
|
133870
|
+
} catch (err) {
|
|
133871
|
+
fs.rmSync(tmp, { force: true });
|
|
133872
|
+
throw err;
|
|
133873
|
+
}
|
|
133874
|
+
}
|
|
133875
|
+
//#endregion
|
|
133876
|
+
//#region src/speak/secret-file.ts
|
|
133877
|
+
/**
|
|
133878
|
+
* Hand a refresh token or a saved Amazon session to the Python worker as an
|
|
133879
|
+
* owner-only file. The path travels on stdin. The bytes never travel on
|
|
133880
|
+
* argv or in the environment, and they are removed when the call settles.
|
|
133881
|
+
*/
|
|
133882
|
+
var REDACTED = "‹redacted›";
|
|
133883
|
+
function decodeSession(sessionB64) {
|
|
133884
|
+
if (sessionB64.length === 0) return Buffer.alloc(0);
|
|
133885
|
+
return Buffer.from(sessionB64, "base64");
|
|
133886
|
+
}
|
|
133887
|
+
async function readSessionB64(sessionPath) {
|
|
133888
|
+
const bytes = await fs$1.readFile(sessionPath);
|
|
133889
|
+
if (bytes.length === 0) return "";
|
|
133890
|
+
return bytes.toString("base64");
|
|
133891
|
+
}
|
|
133892
|
+
/**
|
|
133893
|
+
* Create a 0700 directory, write the refresh token and the session 0600, run
|
|
133894
|
+
* `body`, then delete the directory whether `body` succeeded or threw.
|
|
133895
|
+
* Returns the session file as base64 so the caller can store it in the keychain.
|
|
133896
|
+
*/
|
|
133897
|
+
async function withSpeakSecretFiles(input, body) {
|
|
133898
|
+
const dir = await fs$1.mkdtemp(path$1.join(os.tmpdir(), "camstack-alexa-"));
|
|
133899
|
+
const paths = {
|
|
133900
|
+
dir,
|
|
133901
|
+
refreshTokenPath: path$1.join(dir, "refresh-token"),
|
|
133902
|
+
sessionPath: path$1.join(dir, "session")
|
|
133903
|
+
};
|
|
133904
|
+
try {
|
|
133905
|
+
restrictDirToOwnerSync(dir);
|
|
133906
|
+
writeOwnerOnlyFileSync(paths.refreshTokenPath, input.refreshToken);
|
|
133907
|
+
writeOwnerOnlyFileSync(paths.sessionPath, decodeSession(input.sessionB64));
|
|
133908
|
+
await body(paths);
|
|
133909
|
+
return { sessionB64: await readSessionB64(paths.sessionPath) };
|
|
133910
|
+
} finally {
|
|
133911
|
+
await fs$1.rm(dir, {
|
|
133912
|
+
recursive: true,
|
|
133913
|
+
force: true
|
|
133914
|
+
});
|
|
133915
|
+
}
|
|
133916
|
+
}
|
|
133917
|
+
/** Replace every occurrence of each non-empty secret. Used before a log line is emitted. */
|
|
133918
|
+
function scrubSecrets(text, secrets) {
|
|
133919
|
+
let out = text;
|
|
133920
|
+
for (const secret of secrets) {
|
|
133921
|
+
if (secret.length === 0) continue;
|
|
133922
|
+
out = out.split(secret).join(REDACTED);
|
|
133923
|
+
}
|
|
133924
|
+
return out;
|
|
133925
|
+
}
|
|
133926
|
+
//#endregion
|
|
133927
|
+
//#region src/speak/broker-echo.ts
|
|
133928
|
+
var BrokerEcho = class {
|
|
133929
|
+
deps;
|
|
133930
|
+
currentState = "starting";
|
|
133931
|
+
currentError = null;
|
|
133932
|
+
currentDevices = [];
|
|
133933
|
+
constructor(deps) {
|
|
133934
|
+
this.deps = deps;
|
|
133935
|
+
}
|
|
133936
|
+
get id() {
|
|
133937
|
+
return this.deps.brokerId;
|
|
133938
|
+
}
|
|
133939
|
+
get name() {
|
|
133940
|
+
return this.deps.brokerName;
|
|
133941
|
+
}
|
|
133942
|
+
get state() {
|
|
133943
|
+
return this.currentState;
|
|
133944
|
+
}
|
|
133945
|
+
get error() {
|
|
133946
|
+
return this.currentError;
|
|
133947
|
+
}
|
|
133948
|
+
get devices() {
|
|
133949
|
+
return this.currentDevices;
|
|
133950
|
+
}
|
|
133951
|
+
stop() {
|
|
133952
|
+
this.deps.worker.stop();
|
|
133953
|
+
}
|
|
133954
|
+
/** Sign the worker in as the broker's account. Never throws; the outcome is `state`. */
|
|
133955
|
+
async signIn() {
|
|
133956
|
+
const { brokerId, worker, sessions, logger } = this.deps;
|
|
133957
|
+
const tags = { tags: { brokerId } };
|
|
133958
|
+
let secrets = [];
|
|
133959
|
+
try {
|
|
133960
|
+
const seed = await this.deps.readSeed();
|
|
133961
|
+
const stored = await sessions.get(brokerId);
|
|
133962
|
+
if (seed === null && stored.length === 0) {
|
|
133963
|
+
this.currentState = "broker-not-signed-in";
|
|
133964
|
+
this.currentError = null;
|
|
133965
|
+
logger.warn("alexa speak: the broker has no sign-in to share yet", tags);
|
|
133966
|
+
return;
|
|
133967
|
+
}
|
|
133968
|
+
secrets = seed === null ? [] : [seed.refreshToken];
|
|
133969
|
+
worker.noteSecrets(secrets);
|
|
133970
|
+
let outcome = parseLoginResult({
|
|
133971
|
+
status: "failed",
|
|
133972
|
+
error: "worker gave no answer"
|
|
133973
|
+
});
|
|
133974
|
+
const held = await withSpeakSecretFiles({
|
|
133975
|
+
refreshToken: seed?.refreshToken ?? "",
|
|
133976
|
+
sessionB64: stored
|
|
133977
|
+
}, async (paths) => {
|
|
133978
|
+
outcome = parseLoginResult(await worker.request({
|
|
133979
|
+
op: "login_broker",
|
|
133980
|
+
label: brokerId,
|
|
133981
|
+
refreshTokenPath: seed === null ? "" : paths.refreshTokenPath,
|
|
133982
|
+
deviceAppName: seed?.deviceAppName ?? "",
|
|
133983
|
+
domain: this.deps.domain,
|
|
133984
|
+
sessionPath: paths.sessionPath,
|
|
133985
|
+
region: regionOfDomain(this.deps.domain)
|
|
133986
|
+
}));
|
|
133987
|
+
});
|
|
133988
|
+
if (held.sessionB64.length > 0 && held.sessionB64 !== stored) await sessions.put(brokerId, held.sessionB64);
|
|
133989
|
+
this.currentError = outcome.status === "ok" ? null : outcome.error ?? "sign-in failed";
|
|
133990
|
+
this.currentState = outcome.status === "ok" ? "ready" : "failed";
|
|
133991
|
+
if (outcome.status !== "ok") {
|
|
133992
|
+
logger.warn("alexa speak: sign-in as the broker failed", {
|
|
133993
|
+
...tags,
|
|
133994
|
+
meta: { error: this.currentError }
|
|
133995
|
+
});
|
|
133996
|
+
return;
|
|
133997
|
+
}
|
|
133998
|
+
await this.refreshDevices();
|
|
133999
|
+
} catch (err) {
|
|
134000
|
+
this.currentState = "failed";
|
|
134001
|
+
this.currentError = scrubSecrets(err instanceof Error ? err.message : String(err), secrets);
|
|
134002
|
+
logger.warn("alexa speak: sign-in as the broker failed", {
|
|
134003
|
+
...tags,
|
|
134004
|
+
meta: { error: this.currentError }
|
|
134005
|
+
});
|
|
134006
|
+
}
|
|
134007
|
+
}
|
|
134008
|
+
/** A session that is not ready tries to sign in again before it gives up. */
|
|
134009
|
+
async ensureReady() {
|
|
134010
|
+
if (this.currentState !== "ready") await this.signIn();
|
|
134011
|
+
return this.currentState === "ready";
|
|
134012
|
+
}
|
|
134013
|
+
async refreshDevices() {
|
|
134014
|
+
if (this.currentState !== "ready") return;
|
|
134015
|
+
try {
|
|
134016
|
+
this.currentDevices = [...parseDevices(await this.deps.worker.request({ op: "devices" }))];
|
|
134017
|
+
} catch (err) {
|
|
134018
|
+
this.deps.logger.warn("alexa speak: device refresh failed", {
|
|
134019
|
+
tags: { brokerId: this.deps.brokerId },
|
|
134020
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
134021
|
+
});
|
|
134022
|
+
}
|
|
134023
|
+
}
|
|
134024
|
+
async utter(op, targets, text, volume) {
|
|
134025
|
+
if (!await this.ensureReady()) throw new Error(this.notReadyReason());
|
|
134026
|
+
return parseDeliveries(await this.deps.worker.request({
|
|
134027
|
+
op,
|
|
134028
|
+
targets,
|
|
134029
|
+
text,
|
|
134030
|
+
volume
|
|
134031
|
+
}));
|
|
134032
|
+
}
|
|
134033
|
+
async session() {
|
|
134034
|
+
if (this.deps.worker.failed) return {
|
|
134035
|
+
loggedIn: false,
|
|
134036
|
+
ageSec: 0,
|
|
134037
|
+
lastSuccessAt: null
|
|
134038
|
+
};
|
|
134039
|
+
try {
|
|
134040
|
+
return parseSessionStatus(await this.deps.worker.request({ op: "session_status" }));
|
|
134041
|
+
} catch {
|
|
134042
|
+
return {
|
|
134043
|
+
loggedIn: this.currentState === "ready",
|
|
134044
|
+
ageSec: 0,
|
|
134045
|
+
lastSuccessAt: null
|
|
134046
|
+
};
|
|
134047
|
+
}
|
|
134048
|
+
}
|
|
134049
|
+
notReadyReason() {
|
|
134050
|
+
if (this.currentState === "broker-not-signed-in") return `account '${this.deps.brokerName}' is not signed in on its broker`;
|
|
134051
|
+
return this.currentError ?? "alexa session is not ready";
|
|
134052
|
+
}
|
|
134053
|
+
};
|
|
134054
|
+
//#endregion
|
|
134055
|
+
//#region src/speak/migration.ts
|
|
134056
|
+
function legacyLoginOf(settings) {
|
|
134057
|
+
return {
|
|
134058
|
+
email: settings.email,
|
|
134059
|
+
password: settings.password,
|
|
134060
|
+
session: settings.session
|
|
134061
|
+
};
|
|
134062
|
+
}
|
|
134063
|
+
function planLegacyMigration(legacy, brokerIds, targets) {
|
|
134064
|
+
const present = legacy.email.length > 0 || legacy.password.length > 0 || legacy.session.length > 0;
|
|
134065
|
+
const only = brokerIds.length === 1 ? brokerIds[0] ?? null : null;
|
|
134066
|
+
if (!present || only === null) return {
|
|
134067
|
+
present,
|
|
134068
|
+
attachTo: null,
|
|
134069
|
+
targets
|
|
134070
|
+
};
|
|
134071
|
+
return {
|
|
134072
|
+
present,
|
|
134073
|
+
attachTo: legacy.session.length > 0 ? only : null,
|
|
134074
|
+
targets: targets.map((row) => row.brokerId.length > 0 ? row : {
|
|
134075
|
+
...row,
|
|
134076
|
+
brokerId: only
|
|
134077
|
+
})
|
|
134078
|
+
};
|
|
134079
|
+
}
|
|
134080
|
+
/** What the page must tell the operator about targets that name no account. */
|
|
134081
|
+
function noticeFor(targets, brokerIds) {
|
|
134082
|
+
if (!targets.some((row) => row.brokerId.length === 0)) return "";
|
|
134083
|
+
if (brokerIds.length === 0) return "no-broker";
|
|
134084
|
+
return brokerIds.length > 1 ? "choose-broker" : "";
|
|
134085
|
+
}
|
|
134086
|
+
function unassignedCount(targets) {
|
|
134087
|
+
return targets.filter((row) => row.brokerId.length === 0).length;
|
|
134088
|
+
}
|
|
134089
|
+
//#endregion
|
|
134090
|
+
//#region src/speak/rate-limit.ts
|
|
134091
|
+
/**
|
|
134092
|
+
* Per-Echo admission. A burst is dropped here, not queued: a queue would be
|
|
134093
|
+
* minutes of speech long after the event.
|
|
134094
|
+
*/
|
|
134095
|
+
function emptySpeechLedger() {
|
|
134096
|
+
return { byEcho: {} };
|
|
134097
|
+
}
|
|
134098
|
+
/**
|
|
134099
|
+
* Identical text inside the collapse window is one utterance.
|
|
134100
|
+
* A different text inside the gap is refused until the gap elapses.
|
|
134101
|
+
* Admitted calls replace that Echo's memory; refusals leave it as it was.
|
|
134102
|
+
*/
|
|
134103
|
+
function admitSpeech(ledger, echoId, text, now) {
|
|
134104
|
+
const previous = ledger.byEcho[echoId];
|
|
134105
|
+
if (previous !== void 0 && previous.lastText === text && now - previous.lastSpokenAt < 6e4) return {
|
|
134106
|
+
decision: "collapsed",
|
|
134107
|
+
ledger
|
|
134108
|
+
};
|
|
134109
|
+
if (previous !== void 0 && now - previous.lastSpokenAt < 2e4) return {
|
|
134110
|
+
decision: "rate_limited",
|
|
134111
|
+
ledger
|
|
134112
|
+
};
|
|
134113
|
+
return {
|
|
134114
|
+
decision: "admit",
|
|
134115
|
+
ledger: { byEcho: {
|
|
134116
|
+
...ledger.byEcho,
|
|
134117
|
+
[echoId]: {
|
|
134118
|
+
lastSpokenAt: now,
|
|
134119
|
+
lastText: text
|
|
134120
|
+
}
|
|
134121
|
+
} }
|
|
134122
|
+
};
|
|
134123
|
+
}
|
|
134124
|
+
//#endregion
|
|
134125
|
+
//#region src/speak/speech-text.ts
|
|
134126
|
+
var ALEXA_SPEECH_CAPS = {
|
|
134127
|
+
attachments: {
|
|
134128
|
+
mediaTypes: [],
|
|
134129
|
+
mode: "url",
|
|
134130
|
+
max: 0
|
|
134131
|
+
},
|
|
134132
|
+
actions: 0,
|
|
134133
|
+
actionIcons: false,
|
|
134134
|
+
levels: [{
|
|
134135
|
+
id: "default",
|
|
134136
|
+
label: "Default",
|
|
134137
|
+
ordinal: 3
|
|
134138
|
+
}],
|
|
134139
|
+
format: ["text"],
|
|
134140
|
+
clickUrl: false,
|
|
134141
|
+
sound: false,
|
|
134142
|
+
ttl: false,
|
|
134143
|
+
bodyMaxLen: 450
|
|
134144
|
+
};
|
|
134145
|
+
function collapseWhitespace(value) {
|
|
134146
|
+
return value.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
134147
|
+
}
|
|
134148
|
+
/** Markup that survived the degrade engine must not be read aloud. */
|
|
134149
|
+
function toPlainSpeech(value) {
|
|
134150
|
+
return collapseWhitespace(htmlToText(markdownToText(value)));
|
|
134151
|
+
}
|
|
134152
|
+
/**
|
|
134153
|
+
* Title then body, as one sentence, never longer than {@link SPEECH_MAX_CHARS}.
|
|
134154
|
+
* An empty result is an empty string — the caller refuses to send it.
|
|
134155
|
+
*/
|
|
134156
|
+
function speechFromNotification(notification) {
|
|
134157
|
+
const prepared = prepareNotification(ALEXA_SPEECH_CAPS, notification);
|
|
134158
|
+
const title = prepared.title !== void 0 ? toPlainSpeech(prepared.title) : "";
|
|
134159
|
+
const body = toPlainSpeech(prepared.body);
|
|
134160
|
+
const joined = title.length > 0 && body.length > 0 ? `${title}. ${body}` : title || body;
|
|
134161
|
+
const text = joined.length > 450 ? joined.slice(0, 450).trimEnd() : joined;
|
|
134162
|
+
const truncated = prepared.renderedAs.truncated || text.length < joined.length;
|
|
134163
|
+
return {
|
|
134164
|
+
text,
|
|
134165
|
+
renderedAs: {
|
|
134166
|
+
...prepared.renderedAs,
|
|
134167
|
+
truncated,
|
|
134168
|
+
format: "text"
|
|
134169
|
+
}
|
|
134170
|
+
};
|
|
134171
|
+
}
|
|
134172
|
+
//#endregion
|
|
134173
|
+
//#region src/speak/provider.ts
|
|
134174
|
+
function quietLogger$1() {
|
|
134175
|
+
const logger = {
|
|
134176
|
+
debug() {},
|
|
134177
|
+
info() {},
|
|
134178
|
+
warn() {},
|
|
134179
|
+
error() {},
|
|
134180
|
+
child() {
|
|
134181
|
+
return logger;
|
|
134182
|
+
},
|
|
134183
|
+
withTags() {
|
|
134184
|
+
return logger;
|
|
134185
|
+
}
|
|
134186
|
+
};
|
|
134187
|
+
return logger;
|
|
134188
|
+
}
|
|
134189
|
+
function readString(config, key) {
|
|
134190
|
+
const value = config[key];
|
|
134191
|
+
return typeof value === "string" ? value : "";
|
|
134192
|
+
}
|
|
134193
|
+
function readMode(config) {
|
|
134194
|
+
return config["mode"] === "announce" ? "announce" : "speak";
|
|
134195
|
+
}
|
|
134196
|
+
function readVolume(config) {
|
|
134197
|
+
const value = config["volume"];
|
|
134198
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 40;
|
|
134199
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
134200
|
+
}
|
|
134201
|
+
function respectDnd(config) {
|
|
134202
|
+
return config["respectDnd"] !== false;
|
|
134203
|
+
}
|
|
134204
|
+
/** "Kitchen · Living room · ECHO" — the name first, then what tells two Echoes apart. */
|
|
134205
|
+
function echoOptionLabel(device) {
|
|
134206
|
+
const kind = device.kind === "group" ? "Group" : device.family;
|
|
134207
|
+
const parts = [device.name.length > 0 ? device.name : device.id];
|
|
134208
|
+
if (device.room !== void 0 && device.room.length > 0) parts.push(device.room);
|
|
134209
|
+
if (kind.length > 0) parts.push(kind);
|
|
134210
|
+
return parts.join(" · ");
|
|
134211
|
+
}
|
|
134212
|
+
function echoField(devices, showWhen) {
|
|
134213
|
+
return {
|
|
134214
|
+
type: "select",
|
|
134215
|
+
key: "echoId",
|
|
134216
|
+
label: "Echo",
|
|
134217
|
+
labelKey: "import-alexa:echoTarget.s_echo.echoId.label",
|
|
134218
|
+
description: devices.length === 0 ? "No Echo found yet. Sign the Amazon account in on Integrations, Alexa." : void 0,
|
|
134219
|
+
descriptionKey: devices.length === 0 ? "import-alexa:echoTarget.s_echo.echoId.empty" : void 0,
|
|
134220
|
+
options: devices.map((device) => ({
|
|
134221
|
+
value: device.id,
|
|
134222
|
+
label: echoOptionLabel(device)
|
|
134223
|
+
})),
|
|
134224
|
+
...showWhen === void 0 ? {} : { showWhen }
|
|
134225
|
+
};
|
|
134226
|
+
}
|
|
134227
|
+
/**
|
|
134228
|
+
* The two pickers are not `required`: a required select shows its first option
|
|
134229
|
+
* while the form still holds no value, so a save would drop the choice. Left
|
|
134230
|
+
* optional they open on "Select", and `upsertTarget` refuses an empty pick.
|
|
134231
|
+
*
|
|
134232
|
+
* The Echo picker is built from the accounts' own device lists on every call,
|
|
134233
|
+
* so a new Echo shows up without a deploy. With one account there is no account
|
|
134234
|
+
* field; with several, each account's Echoes appear when that account is chosen.
|
|
134235
|
+
*/
|
|
134236
|
+
function configSchema(brokers) {
|
|
134237
|
+
const first = brokers[0];
|
|
134238
|
+
return { sections: [{
|
|
134239
|
+
id: "echo",
|
|
134240
|
+
title: "Echo",
|
|
134241
|
+
titleKey: "import-alexa:echoTarget.s_echo.title",
|
|
134242
|
+
fields: [
|
|
134243
|
+
...brokers.length > 1 ? [{
|
|
134244
|
+
type: "select",
|
|
134245
|
+
key: "brokerId",
|
|
134246
|
+
label: "Amazon account",
|
|
134247
|
+
labelKey: "import-alexa:echoTarget.s_echo.brokerId.label",
|
|
134248
|
+
options: brokers.map((broker) => ({
|
|
134249
|
+
value: broker.id,
|
|
134250
|
+
label: broker.name
|
|
134251
|
+
}))
|
|
134252
|
+
}, ...brokers.map((broker) => echoField(broker.devices, {
|
|
134253
|
+
field: "brokerId",
|
|
134254
|
+
equals: broker.id
|
|
134255
|
+
}))] : [echoField(first?.devices ?? [])],
|
|
134256
|
+
{
|
|
134257
|
+
type: "select",
|
|
134258
|
+
key: "mode",
|
|
134259
|
+
label: "Mode",
|
|
134260
|
+
labelKey: "import-alexa:echoTarget.s_echo.mode.label",
|
|
134261
|
+
default: "speak",
|
|
134262
|
+
options: [{
|
|
134263
|
+
value: "speak",
|
|
134264
|
+
label: "Speak (quiet hours apply)"
|
|
134265
|
+
}, {
|
|
134266
|
+
value: "announce",
|
|
134267
|
+
label: "Announce (chime, then speech)"
|
|
134268
|
+
}]
|
|
134269
|
+
},
|
|
134270
|
+
{
|
|
134271
|
+
type: "number",
|
|
134272
|
+
key: "volume",
|
|
134273
|
+
label: "Volume",
|
|
134274
|
+
labelKey: "import-alexa:echoTarget.s_echo.volume.label",
|
|
134275
|
+
min: 0,
|
|
134276
|
+
max: 100,
|
|
134277
|
+
default: 40
|
|
134278
|
+
},
|
|
134279
|
+
{
|
|
134280
|
+
type: "boolean",
|
|
134281
|
+
key: "respectDnd",
|
|
134282
|
+
label: "Respect Do Not Disturb",
|
|
134283
|
+
labelKey: "import-alexa:echoTarget.s_echo.respectDnd.label",
|
|
134284
|
+
default: true
|
|
134285
|
+
}
|
|
134286
|
+
]
|
|
134287
|
+
}] };
|
|
134288
|
+
}
|
|
134289
|
+
/** The Alexa target kind as the Add target form sees it, for the accounts as they are now. */
|
|
134290
|
+
function alexaTargetKind(brokers) {
|
|
134291
|
+
return {
|
|
134292
|
+
kind: ALEXA_SPEAK_KIND,
|
|
134293
|
+
label: "Alexa",
|
|
134294
|
+
icon: "megaphone",
|
|
134295
|
+
iconUrl: ALEXA_SPEAK_ICON_URL,
|
|
134296
|
+
iconMediaType: "image/svg+xml",
|
|
134297
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
134298
|
+
supportsDiscovery: true,
|
|
134299
|
+
destinationKeys: ["brokerId", "echoId"],
|
|
134300
|
+
configSchema: configSchema(brokers),
|
|
134301
|
+
caps: {
|
|
134302
|
+
attachments: {
|
|
134303
|
+
mediaTypes: [],
|
|
134304
|
+
mode: "url",
|
|
134305
|
+
max: 0
|
|
134306
|
+
},
|
|
134307
|
+
actions: 0,
|
|
134308
|
+
actionIcons: false,
|
|
134309
|
+
levels: [{
|
|
134310
|
+
id: "default",
|
|
134311
|
+
label: "Default",
|
|
134312
|
+
ordinal: 3
|
|
134313
|
+
}],
|
|
134314
|
+
format: ["text"],
|
|
134315
|
+
clickUrl: false,
|
|
134316
|
+
sound: false,
|
|
134317
|
+
ttl: false,
|
|
134318
|
+
bodyMaxLen: 450
|
|
134319
|
+
}
|
|
134320
|
+
};
|
|
134321
|
+
}
|
|
134322
|
+
function discoveredFrom(broker, device) {
|
|
134323
|
+
return {
|
|
134324
|
+
kind: ALEXA_SPEAK_KIND,
|
|
134325
|
+
suggestedName: device.name.length > 0 ? device.name : device.id,
|
|
134326
|
+
config: {
|
|
134327
|
+
brokerId: broker.id,
|
|
134328
|
+
echoId: device.id,
|
|
134329
|
+
mode: "speak",
|
|
134330
|
+
volume: 40,
|
|
134331
|
+
respectDnd: true,
|
|
134332
|
+
family: device.family,
|
|
134333
|
+
deviceKind: device.kind
|
|
134334
|
+
}
|
|
134335
|
+
};
|
|
134336
|
+
}
|
|
134337
|
+
function createAlexaNotificationProvider(ports) {
|
|
134338
|
+
const logger = ports.logger ?? quietLogger$1();
|
|
134339
|
+
let targets = ports.loadTargets();
|
|
134340
|
+
let ledger = emptySpeechLedger();
|
|
134341
|
+
let deliveries = [];
|
|
134342
|
+
function record(row) {
|
|
134343
|
+
deliveries = [...deliveries, row];
|
|
134344
|
+
}
|
|
134345
|
+
/**
|
|
134346
|
+
* The account a target speaks through: the one it names; else the only one
|
|
134347
|
+
* there is; else the one whose Echoes include the target's Echo.
|
|
134348
|
+
*/
|
|
134349
|
+
function brokerFor(config) {
|
|
134350
|
+
const all = ports.brokers();
|
|
134351
|
+
const named = readString(config, "brokerId");
|
|
134352
|
+
if (named.length > 0) return all.find((broker) => broker.id === named);
|
|
134353
|
+
if (all.length === 1) return all[0];
|
|
134354
|
+
const echoId = readString(config, "echoId");
|
|
134355
|
+
const owners = all.filter((broker) => broker.devices.some((device) => device.id === echoId));
|
|
134356
|
+
return owners.length === 1 ? owners[0] : void 0;
|
|
134357
|
+
}
|
|
134358
|
+
function persist(next) {
|
|
134359
|
+
targets = next;
|
|
134360
|
+
ports.saveTargets(next);
|
|
134361
|
+
}
|
|
134362
|
+
async function deliver(target, notification) {
|
|
134363
|
+
const speech = speechFromNotification(notification);
|
|
134364
|
+
const echoId = readString(target.config, "echoId");
|
|
134365
|
+
const deviceId = notification.deviceId;
|
|
134366
|
+
const tags = deviceId !== void 0 ? { tags: { deviceId } } : {};
|
|
134367
|
+
if (echoId.length === 0) {
|
|
134368
|
+
record({
|
|
134369
|
+
targetId: target.id,
|
|
134370
|
+
outcome: "failed",
|
|
134371
|
+
at: ports.now(),
|
|
134372
|
+
error: "target has no echo id"
|
|
134373
|
+
});
|
|
134374
|
+
logger.warn("alexa speak dropped: target has no echo id", {
|
|
134375
|
+
...tags,
|
|
134376
|
+
meta: { targetId: target.id }
|
|
134377
|
+
});
|
|
134378
|
+
return {
|
|
134379
|
+
success: false,
|
|
134380
|
+
error: "target has no echo id",
|
|
134381
|
+
renderedAs: speech.renderedAs
|
|
134382
|
+
};
|
|
134383
|
+
}
|
|
134384
|
+
if (speech.text.length === 0) {
|
|
134385
|
+
record({
|
|
134386
|
+
targetId: target.id,
|
|
134387
|
+
outcome: "empty",
|
|
134388
|
+
at: ports.now()
|
|
134389
|
+
});
|
|
134390
|
+
logger.info("alexa speak dropped: empty text", {
|
|
134391
|
+
...tags,
|
|
134392
|
+
meta: {
|
|
134393
|
+
targetId: target.id,
|
|
134394
|
+
echoId
|
|
134395
|
+
}
|
|
134396
|
+
});
|
|
134397
|
+
return {
|
|
134398
|
+
success: true,
|
|
134399
|
+
renderedAs: speech.renderedAs
|
|
134400
|
+
};
|
|
134401
|
+
}
|
|
134402
|
+
const broker = brokerFor(target.config);
|
|
134403
|
+
if (broker === void 0) {
|
|
134404
|
+
const error = "target has no Amazon account";
|
|
134405
|
+
record({
|
|
134406
|
+
targetId: target.id,
|
|
134407
|
+
outcome: "failed",
|
|
134408
|
+
at: ports.now(),
|
|
134409
|
+
error
|
|
134410
|
+
});
|
|
134411
|
+
logger.warn("alexa speak dropped: target names no Amazon account", {
|
|
134412
|
+
...tags,
|
|
134413
|
+
meta: {
|
|
134414
|
+
targetId: target.id,
|
|
134415
|
+
echoId
|
|
134416
|
+
}
|
|
134417
|
+
});
|
|
134418
|
+
return {
|
|
134419
|
+
success: false,
|
|
134420
|
+
error,
|
|
134421
|
+
renderedAs: speech.renderedAs
|
|
134422
|
+
};
|
|
134423
|
+
}
|
|
134424
|
+
const device = broker.devices.find((row) => row.id === echoId);
|
|
134425
|
+
if (device !== void 0 && respectDnd(target.config) && device.dnd) {
|
|
134426
|
+
record({
|
|
134427
|
+
targetId: target.id,
|
|
134428
|
+
outcome: "dnd",
|
|
134429
|
+
at: ports.now()
|
|
134430
|
+
});
|
|
134431
|
+
logger.info("alexa speak skipped: do not disturb", {
|
|
134432
|
+
...tags,
|
|
134433
|
+
meta: {
|
|
134434
|
+
targetId: target.id,
|
|
134435
|
+
echoId
|
|
134436
|
+
}
|
|
134437
|
+
});
|
|
134438
|
+
return {
|
|
134439
|
+
success: true,
|
|
134440
|
+
renderedAs: speech.renderedAs
|
|
134441
|
+
};
|
|
134442
|
+
}
|
|
134443
|
+
if (device !== void 0 && !device.online) {
|
|
134444
|
+
record({
|
|
134445
|
+
targetId: target.id,
|
|
134446
|
+
outcome: "offline",
|
|
134447
|
+
at: ports.now()
|
|
134448
|
+
});
|
|
134449
|
+
logger.info("alexa speak not delivered: echo offline", {
|
|
134450
|
+
...tags,
|
|
134451
|
+
meta: {
|
|
134452
|
+
targetId: target.id,
|
|
134453
|
+
echoId
|
|
134454
|
+
}
|
|
134455
|
+
});
|
|
134456
|
+
return {
|
|
134457
|
+
success: false,
|
|
134458
|
+
error: "echo offline",
|
|
134459
|
+
renderedAs: speech.renderedAs
|
|
134460
|
+
};
|
|
134461
|
+
}
|
|
134462
|
+
const admitted = admitSpeech(ledger, echoId, speech.text, ports.now());
|
|
134463
|
+
ledger = admitted.ledger;
|
|
134464
|
+
if (admitted.decision !== "admit") {
|
|
134465
|
+
record({
|
|
134466
|
+
targetId: target.id,
|
|
134467
|
+
outcome: admitted.decision,
|
|
134468
|
+
at: ports.now()
|
|
134469
|
+
});
|
|
134470
|
+
logger.info("alexa speak skipped", {
|
|
134471
|
+
...tags,
|
|
134472
|
+
meta: {
|
|
134473
|
+
targetId: target.id,
|
|
134474
|
+
echoId,
|
|
134475
|
+
outcome: admitted.decision
|
|
134476
|
+
}
|
|
134477
|
+
});
|
|
134478
|
+
return {
|
|
134479
|
+
success: true,
|
|
134480
|
+
renderedAs: speech.renderedAs
|
|
134481
|
+
};
|
|
134482
|
+
}
|
|
134483
|
+
const mode = readMode(target.config);
|
|
134484
|
+
try {
|
|
134485
|
+
const outcomes = await ports.speak({
|
|
134486
|
+
brokerId: broker.id,
|
|
134487
|
+
mode,
|
|
134488
|
+
targets: [echoId],
|
|
134489
|
+
text: speech.text,
|
|
134490
|
+
volume: readVolume(target.config)
|
|
134491
|
+
});
|
|
134492
|
+
const one = outcomes.find((row) => row.targetId === echoId) ?? outcomes[0];
|
|
134493
|
+
if (one === void 0 || !one.ok) {
|
|
134494
|
+
const error = one?.error ?? "speak failed";
|
|
134495
|
+
record({
|
|
134496
|
+
targetId: target.id,
|
|
134497
|
+
outcome: "failed",
|
|
134498
|
+
at: ports.now(),
|
|
134499
|
+
error
|
|
134500
|
+
});
|
|
134501
|
+
logger.warn("alexa speak failed", {
|
|
134502
|
+
...tags,
|
|
134503
|
+
meta: {
|
|
134504
|
+
targetId: target.id,
|
|
134505
|
+
echoId,
|
|
134506
|
+
error
|
|
134507
|
+
}
|
|
134508
|
+
});
|
|
134509
|
+
return {
|
|
134510
|
+
success: false,
|
|
134511
|
+
error,
|
|
134512
|
+
renderedAs: speech.renderedAs
|
|
134513
|
+
};
|
|
134514
|
+
}
|
|
134515
|
+
const outcome = mode === "announce" ? "announced" : "spoken";
|
|
134516
|
+
record({
|
|
134517
|
+
targetId: target.id,
|
|
134518
|
+
outcome,
|
|
134519
|
+
at: ports.now()
|
|
134520
|
+
});
|
|
134521
|
+
logger.info("alexa speak delivered", {
|
|
134522
|
+
...tags,
|
|
134523
|
+
meta: {
|
|
134524
|
+
targetId: target.id,
|
|
134525
|
+
echoId,
|
|
134526
|
+
outcome
|
|
134527
|
+
}
|
|
134528
|
+
});
|
|
134529
|
+
return {
|
|
134530
|
+
success: true,
|
|
134531
|
+
renderedAs: speech.renderedAs
|
|
134532
|
+
};
|
|
134533
|
+
} catch (err) {
|
|
134534
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
134535
|
+
record({
|
|
134536
|
+
targetId: target.id,
|
|
134537
|
+
outcome: "failed",
|
|
134538
|
+
at: ports.now(),
|
|
134539
|
+
error
|
|
134540
|
+
});
|
|
134541
|
+
logger.warn("alexa speak failed", {
|
|
134542
|
+
...tags,
|
|
134543
|
+
meta: {
|
|
134544
|
+
targetId: target.id,
|
|
134545
|
+
echoId,
|
|
134546
|
+
error
|
|
134547
|
+
}
|
|
134548
|
+
});
|
|
134549
|
+
return {
|
|
134550
|
+
success: false,
|
|
134551
|
+
error,
|
|
134552
|
+
renderedAs: speech.renderedAs
|
|
134553
|
+
};
|
|
134554
|
+
}
|
|
134555
|
+
}
|
|
134556
|
+
return {
|
|
134557
|
+
get deliveries() {
|
|
134558
|
+
return deliveries;
|
|
134559
|
+
},
|
|
134560
|
+
async listTargetKinds() {
|
|
134561
|
+
return [alexaTargetKind(ports.brokers())];
|
|
134562
|
+
},
|
|
134563
|
+
async listTargets() {
|
|
134564
|
+
return [...targets];
|
|
134565
|
+
},
|
|
134566
|
+
async getTarget(input) {
|
|
134567
|
+
const found = targets.find((target) => target.id === input.targetId);
|
|
134568
|
+
if (found === void 0) throw new Error(`target not found: ${input.targetId}`);
|
|
134569
|
+
return found;
|
|
134570
|
+
},
|
|
134571
|
+
async discoverTargets() {
|
|
134572
|
+
const taken = new Set(targets.flatMap((row) => {
|
|
134573
|
+
const owner = brokerFor(row.config);
|
|
134574
|
+
return owner === void 0 ? [] : [`${owner.id}/${readString(row.config, "echoId")}`];
|
|
134575
|
+
}));
|
|
134576
|
+
return ports.brokers().flatMap((broker) => broker.devices.filter((device) => !taken.has(`${broker.id}/${device.id}`)).map((device) => discoveredFrom(broker, device)));
|
|
134577
|
+
},
|
|
134578
|
+
async probeTargetConfig(input) {
|
|
134579
|
+
const broker = brokerFor(input.config);
|
|
134580
|
+
return broker !== void 0 && broker.ready ? {
|
|
134581
|
+
ok: true,
|
|
134582
|
+
resolvedUrl: "alexa://session"
|
|
134583
|
+
} : {
|
|
134584
|
+
ok: false,
|
|
134585
|
+
resolvedUrl: "alexa://session",
|
|
134586
|
+
error: "the Amazon account is not signed in on its broker"
|
|
134587
|
+
};
|
|
134588
|
+
},
|
|
134589
|
+
async send(input) {
|
|
134590
|
+
const target = targets.find((row) => row.id === input.targetId);
|
|
134591
|
+
if (target === void 0) return {
|
|
134592
|
+
success: false,
|
|
134593
|
+
error: `target not found: ${input.targetId}`
|
|
134594
|
+
};
|
|
134595
|
+
if (!target.enabled) return {
|
|
134596
|
+
success: false,
|
|
134597
|
+
error: "target disabled"
|
|
134598
|
+
};
|
|
134599
|
+
return deliver(target, input.notification);
|
|
134600
|
+
},
|
|
134601
|
+
async testTarget(input) {
|
|
134602
|
+
const found = targets.find((row) => row.id === input.targetId);
|
|
134603
|
+
if (found === void 0) return {
|
|
134604
|
+
success: false,
|
|
134605
|
+
error: `target not found: ${input.targetId}`
|
|
134606
|
+
};
|
|
134607
|
+
if (!found.enabled) return {
|
|
134608
|
+
success: false,
|
|
134609
|
+
error: "target disabled"
|
|
134610
|
+
};
|
|
134611
|
+
return deliver(found, input.sample ?? {
|
|
134612
|
+
body: "This is a CamStack test notification.",
|
|
134613
|
+
title: "CamStack",
|
|
134614
|
+
format: "text",
|
|
134615
|
+
priority: 3
|
|
134616
|
+
});
|
|
134617
|
+
},
|
|
134618
|
+
async upsertTarget(input) {
|
|
134619
|
+
const owner = brokerFor(input.target.config);
|
|
134620
|
+
if (readString(input.target.config, "echoId").length === 0) throw new Error("Pick the Echo that should speak");
|
|
134621
|
+
if (owner === void 0) throw new Error("Pick the Amazon account this Echo belongs to");
|
|
134622
|
+
const pinned = readString(input.target.config, "brokerId").length === 0 ? {
|
|
134623
|
+
...input.target.config,
|
|
134624
|
+
brokerId: owner.id
|
|
134625
|
+
} : input.target.config;
|
|
134626
|
+
const target = {
|
|
134627
|
+
...input.target,
|
|
134628
|
+
config: pinned,
|
|
134629
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
134630
|
+
kind: ALEXA_SPEAK_KIND
|
|
134631
|
+
};
|
|
134632
|
+
persist([...targets.filter((row) => row.id !== target.id), target]);
|
|
134633
|
+
return target;
|
|
134634
|
+
},
|
|
134635
|
+
async deleteTarget(input) {
|
|
134636
|
+
persist(targets.filter((row) => row.id !== input.targetId));
|
|
134637
|
+
},
|
|
134638
|
+
async setTargetEnabled(input) {
|
|
134639
|
+
persist(targets.map((row) => row.id === input.targetId ? {
|
|
134640
|
+
...row,
|
|
134641
|
+
enabled: input.enabled
|
|
134642
|
+
} : row));
|
|
134643
|
+
}
|
|
134644
|
+
};
|
|
134645
|
+
}
|
|
134646
|
+
//#endregion
|
|
134647
|
+
//#region src/speak/python-paths.ts
|
|
134648
|
+
/** Locate bundled `python/alexa_worker.py` from dist/ or from src/ under test. */
|
|
134649
|
+
function resolveAlexaWorkerScript(fromModuleUrl = import.meta.url) {
|
|
134650
|
+
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
134651
|
+
const candidates = [
|
|
134652
|
+
join(here, "../python/alexa_worker.py"),
|
|
134653
|
+
join(here, "../../python/alexa_worker.py"),
|
|
134654
|
+
join(here, "../../../python/alexa_worker.py")
|
|
134655
|
+
];
|
|
134656
|
+
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
134657
|
+
throw new Error(`import-alexa: alexa_worker.py not found. Searched:\n${candidates.join("\n")}`);
|
|
134658
|
+
}
|
|
134659
|
+
//#endregion
|
|
134660
|
+
//#region src/speak/worker-client.ts
|
|
134661
|
+
/**
|
|
134662
|
+
* One Python process per broker, for the life of the addon. Requests are one JSON line
|
|
134663
|
+
* on stdin; responses are one JSON line on stdout, matched by id.
|
|
134664
|
+
* A crash restarts the process until the same bound CrashSupervisor uses,
|
|
134665
|
+
* then requests fail instead of looping.
|
|
134666
|
+
*/
|
|
134667
|
+
/** Pure: a new timestamp list, never a mutated one. */
|
|
134668
|
+
function recordWorkerCrash(times, now, windowMs, maxCrashes) {
|
|
134669
|
+
const cutoff = now - windowMs;
|
|
134670
|
+
const recent = [...times.filter((t) => t >= cutoff), now];
|
|
134671
|
+
if (recent.length >= maxCrashes) return {
|
|
134672
|
+
action: "failed",
|
|
134673
|
+
times: recent
|
|
134674
|
+
};
|
|
134675
|
+
return {
|
|
134676
|
+
action: "respawn",
|
|
134677
|
+
times: recent
|
|
134678
|
+
};
|
|
134679
|
+
}
|
|
134680
|
+
function quietLogger() {
|
|
134681
|
+
const logger = {
|
|
134682
|
+
debug() {},
|
|
134683
|
+
info() {},
|
|
134684
|
+
warn() {},
|
|
134685
|
+
error() {},
|
|
134686
|
+
child() {
|
|
134687
|
+
return logger;
|
|
134688
|
+
},
|
|
134689
|
+
withTags() {
|
|
134690
|
+
return logger;
|
|
134691
|
+
}
|
|
134692
|
+
};
|
|
134693
|
+
return logger;
|
|
134694
|
+
}
|
|
134695
|
+
function wrapChild(child) {
|
|
134696
|
+
const stdin = child.stdin;
|
|
134697
|
+
const stdout = child.stdout;
|
|
134698
|
+
const stderr = child.stderr;
|
|
134699
|
+
if (stdin === null || stdout === null || stderr === null) throw new Error("alexa worker spawn did not provide pipes");
|
|
134700
|
+
return {
|
|
134701
|
+
stdin: { write(chunk) {
|
|
134702
|
+
stdin.write(chunk);
|
|
134703
|
+
} },
|
|
134704
|
+
stdout: { on(event, listener) {
|
|
134705
|
+
stdout.on(event, listener);
|
|
134706
|
+
} },
|
|
134707
|
+
stderr: { on(event, listener) {
|
|
134708
|
+
stderr.on(event, listener);
|
|
134709
|
+
} },
|
|
134710
|
+
on(event, listener) {
|
|
134711
|
+
if (event === "close") child.on("close", listener);
|
|
134712
|
+
else child.on("error", listener);
|
|
134713
|
+
},
|
|
134714
|
+
kill(signal) {
|
|
134715
|
+
child.kill(signal);
|
|
134716
|
+
}
|
|
134717
|
+
};
|
|
134718
|
+
}
|
|
134719
|
+
function defaultSpawn(pythonPath, scriptPath, env) {
|
|
134720
|
+
return wrapChild(spawn(pythonPath, [scriptPath], {
|
|
134721
|
+
stdio: [
|
|
134722
|
+
"pipe",
|
|
134723
|
+
"pipe",
|
|
134724
|
+
"pipe"
|
|
134725
|
+
],
|
|
134726
|
+
env
|
|
134727
|
+
}));
|
|
134728
|
+
}
|
|
134729
|
+
function startAlexaWorker(deps) {
|
|
134730
|
+
const logger = deps.logger ?? quietLogger();
|
|
134731
|
+
const now = deps.now ?? (() => Date.now());
|
|
134732
|
+
const schedule = deps.schedule ?? ((fn, delayMs) => {
|
|
134733
|
+
setTimeout(fn, delayMs);
|
|
134734
|
+
});
|
|
134735
|
+
const spawnChild = deps.spawnChild ?? defaultSpawn;
|
|
134736
|
+
const maxCrashes = deps.maxCrashes ?? 5;
|
|
134737
|
+
const windowMs = deps.windowMs ?? 6e4;
|
|
134738
|
+
const env = deps.env ?? process.env;
|
|
134739
|
+
let child = null;
|
|
134740
|
+
let buffer = "";
|
|
134741
|
+
let nextId = 1;
|
|
134742
|
+
let stopped = false;
|
|
134743
|
+
let failed = false;
|
|
134744
|
+
let crashTimes = [];
|
|
134745
|
+
let secrets = [];
|
|
134746
|
+
let pending = null;
|
|
134747
|
+
const queue = [];
|
|
134748
|
+
function scrub(text) {
|
|
134749
|
+
return scrubSecrets(text, secrets);
|
|
134750
|
+
}
|
|
134751
|
+
function failPending(err) {
|
|
134752
|
+
const current = pending;
|
|
134753
|
+
pending = null;
|
|
134754
|
+
current?.reject(err);
|
|
134755
|
+
}
|
|
134756
|
+
function attach(spawned) {
|
|
134757
|
+
child = spawned;
|
|
134758
|
+
buffer = "";
|
|
134759
|
+
let settled = false;
|
|
134760
|
+
const finish = (code) => {
|
|
134761
|
+
if (settled || child !== spawned) return;
|
|
134762
|
+
settled = true;
|
|
134763
|
+
onExit(code);
|
|
134764
|
+
};
|
|
134765
|
+
spawned.stdout.on("data", (chunk) => {
|
|
134766
|
+
buffer += chunk.toString("utf8");
|
|
134767
|
+
const lines = buffer.split("\n");
|
|
134768
|
+
buffer = lines.pop() ?? "";
|
|
134769
|
+
for (const line of lines) {
|
|
134770
|
+
if (line.trim().length === 0) continue;
|
|
134771
|
+
acceptLine(line);
|
|
134772
|
+
}
|
|
134773
|
+
});
|
|
134774
|
+
spawned.stderr.on("data", (chunk) => {
|
|
134775
|
+
const text = scrub(chunk.toString("utf8")).trim();
|
|
134776
|
+
if (text.length === 0) return;
|
|
134777
|
+
logger.warn("alexa worker stderr", { meta: { text: text.slice(0, 500) } });
|
|
134778
|
+
});
|
|
134779
|
+
spawned.on("error", (err) => {
|
|
134780
|
+
logger.warn("alexa worker spawn error", { meta: { error: scrub(err.message) } });
|
|
134781
|
+
finish(null);
|
|
134782
|
+
});
|
|
134783
|
+
spawned.on("close", (code) => {
|
|
134784
|
+
finish(code);
|
|
134785
|
+
});
|
|
134786
|
+
}
|
|
134787
|
+
function acceptLine(line) {
|
|
134788
|
+
let parsed;
|
|
134789
|
+
try {
|
|
134790
|
+
parsed = JSON.parse(line);
|
|
134791
|
+
} catch (err) {
|
|
134792
|
+
logger.warn("alexa worker non-JSON line", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134793
|
+
return;
|
|
134794
|
+
}
|
|
134795
|
+
const checked = WorkerLineSchema.safeParse(parsed);
|
|
134796
|
+
if (!checked.success) {
|
|
134797
|
+
logger.warn("alexa worker line failed schema", { meta: { error: checked.error.message } });
|
|
134798
|
+
return;
|
|
134799
|
+
}
|
|
134800
|
+
const message = checked.data;
|
|
134801
|
+
if (pending === null || pending.id !== message.id) {
|
|
134802
|
+
logger.warn("alexa worker line for an unknown request", { meta: { id: message.id } });
|
|
134803
|
+
return;
|
|
134804
|
+
}
|
|
134805
|
+
const current = pending;
|
|
134806
|
+
pending = null;
|
|
134807
|
+
if (message.ok) current.resolve(message.result);
|
|
134808
|
+
else current.reject(new Error(scrub(message.error)));
|
|
134809
|
+
pump();
|
|
134810
|
+
}
|
|
134811
|
+
function onExit(code) {
|
|
134812
|
+
child = null;
|
|
134813
|
+
if (stopped) {
|
|
134814
|
+
failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134815
|
+
return;
|
|
134816
|
+
}
|
|
134817
|
+
failPending(/* @__PURE__ */ new Error(`alexa worker exited (${code ?? "null"})`));
|
|
134818
|
+
const decision = recordWorkerCrash(crashTimes, now(), windowMs, maxCrashes);
|
|
134819
|
+
crashTimes = decision.times;
|
|
134820
|
+
if (decision.action === "failed") {
|
|
134821
|
+
failed = true;
|
|
134822
|
+
logger.error("alexa worker restart bound reached", { meta: { crashes: crashTimes.length } });
|
|
134823
|
+
const waiting = queue.splice(0, queue.length);
|
|
134824
|
+
for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
|
|
134825
|
+
return;
|
|
134826
|
+
}
|
|
134827
|
+
logger.warn("alexa worker exited; restarting", { meta: {
|
|
134828
|
+
code,
|
|
134829
|
+
crashes: crashTimes.length
|
|
134830
|
+
} });
|
|
134831
|
+
schedule(() => {
|
|
134832
|
+
if (stopped || failed) return;
|
|
134833
|
+
launch();
|
|
134834
|
+
pump();
|
|
134835
|
+
}, Math.min(3e4, 500 * 2 ** Math.min(6, crashTimes.length - 1)));
|
|
134836
|
+
}
|
|
134837
|
+
function launch() {
|
|
134838
|
+
if (stopped || failed) return;
|
|
134839
|
+
attach(spawnChild(deps.pythonPath, deps.scriptPath, env));
|
|
134840
|
+
}
|
|
134841
|
+
function pump() {
|
|
134842
|
+
if (pending !== null || child === null || failed || stopped) return;
|
|
134843
|
+
const next = queue.shift();
|
|
134844
|
+
if (next === void 0) return;
|
|
134845
|
+
pending = next.pending;
|
|
134846
|
+
child.stdin.write(requestLine(next.pending.id, next.request));
|
|
134847
|
+
}
|
|
134848
|
+
function request(body) {
|
|
134849
|
+
if (failed) return Promise.reject(/* @__PURE__ */ new Error("alexa worker restart bound reached"));
|
|
134850
|
+
if (stopped) return Promise.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134851
|
+
const id = String(nextId);
|
|
134852
|
+
nextId += 1;
|
|
134853
|
+
return new Promise((resolve, reject) => {
|
|
134854
|
+
queue.push({
|
|
134855
|
+
request: body,
|
|
134856
|
+
pending: {
|
|
134857
|
+
id,
|
|
134858
|
+
resolve,
|
|
134859
|
+
reject
|
|
134860
|
+
}
|
|
134861
|
+
});
|
|
134862
|
+
if (child === null) launch();
|
|
134863
|
+
pump();
|
|
134864
|
+
});
|
|
134865
|
+
}
|
|
134866
|
+
return {
|
|
134867
|
+
request,
|
|
134868
|
+
noteSecrets(next) {
|
|
134869
|
+
secrets = [...next];
|
|
134870
|
+
},
|
|
134871
|
+
stop() {
|
|
134872
|
+
stopped = true;
|
|
134873
|
+
const current = child;
|
|
134874
|
+
child = null;
|
|
134875
|
+
current?.kill("SIGTERM");
|
|
134876
|
+
failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134877
|
+
const waiting = queue.splice(0, queue.length);
|
|
134878
|
+
for (const item of waiting) item.pending.reject(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
134879
|
+
},
|
|
134880
|
+
get failed() {
|
|
134881
|
+
return failed;
|
|
134882
|
+
},
|
|
134883
|
+
get crashesInWindow() {
|
|
134884
|
+
return crashTimes.length;
|
|
134885
|
+
}
|
|
134886
|
+
};
|
|
134887
|
+
}
|
|
134888
|
+
//#endregion
|
|
134889
|
+
//#region src/speak/controller.ts
|
|
134890
|
+
function targetsFrom(settings) {
|
|
134891
|
+
return settings.targets.map((row) => ({
|
|
134892
|
+
id: row.id,
|
|
134893
|
+
name: row.name,
|
|
134894
|
+
kind: ALEXA_SPEAK_KIND,
|
|
134895
|
+
addonId: ALEXA_SPEAK_ADDON_ID,
|
|
134896
|
+
enabled: row.enabled,
|
|
134897
|
+
config: {
|
|
134898
|
+
...row.brokerId.length > 0 ? { brokerId: row.brokerId } : {},
|
|
134899
|
+
echoId: row.echoId,
|
|
134900
|
+
mode: row.mode,
|
|
134901
|
+
volume: row.volume,
|
|
134902
|
+
respectDnd: row.respectDnd
|
|
134903
|
+
}
|
|
134904
|
+
}));
|
|
134905
|
+
}
|
|
134906
|
+
function rowsFrom(targets) {
|
|
134907
|
+
return targets.map((target) => ({
|
|
134908
|
+
id: target.id,
|
|
134909
|
+
name: target.name,
|
|
134910
|
+
enabled: target.enabled,
|
|
134911
|
+
brokerId: typeof target.config["brokerId"] === "string" ? target.config["brokerId"] : "",
|
|
134912
|
+
echoId: typeof target.config["echoId"] === "string" ? target.config["echoId"] : "",
|
|
134913
|
+
mode: target.config["mode"] === "announce" ? "announce" : "speak",
|
|
134914
|
+
volume: typeof target.config["volume"] === "number" ? target.config["volume"] : 40,
|
|
134915
|
+
respectDnd: target.config["respectDnd"] !== false
|
|
134916
|
+
}));
|
|
134917
|
+
}
|
|
134918
|
+
var AlexaSpeakController = class {
|
|
134919
|
+
host;
|
|
134920
|
+
provider;
|
|
134921
|
+
settings;
|
|
134922
|
+
pythonPath = null;
|
|
134923
|
+
echoes = /* @__PURE__ */ new Map();
|
|
134924
|
+
timer = null;
|
|
134925
|
+
queue = Promise.resolve();
|
|
134926
|
+
now;
|
|
134927
|
+
constructor(host) {
|
|
134928
|
+
this.host = host;
|
|
134929
|
+
this.now = host.now ?? (() => Date.now());
|
|
134930
|
+
this.settings = readSpeakSettings(host.readConfig());
|
|
134931
|
+
this.provider = createAlexaNotificationProvider({
|
|
134932
|
+
brokers: () => this.brokerViews(),
|
|
134933
|
+
now: this.now,
|
|
134934
|
+
logger: host.logger,
|
|
134935
|
+
loadTargets: () => targetsFrom(this.settings),
|
|
134936
|
+
saveTargets: (targets) => {
|
|
134937
|
+
this.settings = {
|
|
134938
|
+
...this.settings,
|
|
134939
|
+
targets: [...rowsFrom(targets)]
|
|
134940
|
+
};
|
|
134941
|
+
this.persist();
|
|
134942
|
+
},
|
|
134943
|
+
speak: (command) => this.utter(command.brokerId, command.mode, command.targets, command.text, command.volume)
|
|
134944
|
+
});
|
|
134945
|
+
}
|
|
134946
|
+
async start() {
|
|
134947
|
+
this.pythonPath = await this.host.ensurePython();
|
|
134948
|
+
if (this.pythonPath === null) {
|
|
134949
|
+
this.host.logger.warn("alexa speak: embedded Python unavailable; Echo speech will not run");
|
|
134950
|
+
return;
|
|
134951
|
+
}
|
|
134952
|
+
await this.migrateLegacyLogin();
|
|
134953
|
+
await this.brokersChanged();
|
|
134954
|
+
this.timer = setInterval(() => {
|
|
134955
|
+
this.refreshAll();
|
|
134956
|
+
}, DEVICE_REFRESH_MS);
|
|
134957
|
+
}
|
|
134958
|
+
stop() {
|
|
134959
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
134960
|
+
this.timer = null;
|
|
134961
|
+
for (const echo of this.echoes.values()) echo.stop();
|
|
134962
|
+
this.echoes.clear();
|
|
134963
|
+
}
|
|
134964
|
+
/** The addon calls this whenever its list of brokers may have changed. */
|
|
134965
|
+
brokersChanged() {
|
|
134966
|
+
this.queue = this.queue.then(() => this.reconcile()).catch(() => void 0);
|
|
134967
|
+
return this.queue;
|
|
134968
|
+
}
|
|
134969
|
+
handlers() {
|
|
134970
|
+
return {
|
|
134971
|
+
speakStatus: async () => this.status(),
|
|
134972
|
+
sendTest: async (input) => this.sendTest(input)
|
|
134973
|
+
};
|
|
134974
|
+
}
|
|
134975
|
+
brokerViews() {
|
|
134976
|
+
return this.host.listBrokers().map((ref) => {
|
|
134977
|
+
const echo = this.echoes.get(ref.id);
|
|
134978
|
+
return {
|
|
134979
|
+
id: ref.id,
|
|
134980
|
+
name: ref.name,
|
|
134981
|
+
ready: echo?.state === "ready",
|
|
134982
|
+
devices: echo?.devices ?? []
|
|
134983
|
+
};
|
|
134984
|
+
});
|
|
134985
|
+
}
|
|
134986
|
+
async persist() {
|
|
134987
|
+
try {
|
|
134988
|
+
await this.host.writeSpeak(this.settings);
|
|
134989
|
+
} catch (err) {
|
|
134990
|
+
this.host.logger.warn("alexa speak: could not store settings", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
134991
|
+
}
|
|
134992
|
+
}
|
|
134993
|
+
/**
|
|
134994
|
+
* Drain the old standalone login once. With exactly one broker its jar and
|
|
134995
|
+
* its targets move onto that broker; otherwise the jar is dropped and the
|
|
134996
|
+
* targets wait for the operator to choose (the status names that state).
|
|
134997
|
+
*/
|
|
134998
|
+
async migrateLegacyLogin() {
|
|
134999
|
+
const brokers = this.host.listBrokers();
|
|
135000
|
+
const plan = planLegacyMigration(legacyLoginOf(this.settings), brokers.map((ref) => ref.id), this.settings.targets);
|
|
135001
|
+
if (!plan.present) return;
|
|
135002
|
+
if (plan.attachTo !== null) await this.host.sessions.put(plan.attachTo, this.settings.session);
|
|
135003
|
+
this.settings = {
|
|
135004
|
+
...this.settings,
|
|
135005
|
+
email: "",
|
|
135006
|
+
password: "",
|
|
135007
|
+
session: "",
|
|
135008
|
+
targets: [...plan.targets]
|
|
135009
|
+
};
|
|
135010
|
+
await this.persist();
|
|
135011
|
+
await this.host.clearLegacySecrets();
|
|
135012
|
+
this.host.logger.info("alexa speak: the old standalone login was retired", { meta: {
|
|
135013
|
+
attachedTo: plan.attachTo ?? "none",
|
|
135014
|
+
brokers: brokers.length,
|
|
135015
|
+
targets: plan.targets.length
|
|
135016
|
+
} });
|
|
135017
|
+
}
|
|
135018
|
+
makeWorker(brokerId, pythonPath) {
|
|
135019
|
+
if (this.host.startWorker !== void 0) return this.host.startWorker(brokerId, pythonPath);
|
|
135020
|
+
return startAlexaWorker({
|
|
135021
|
+
pythonPath,
|
|
135022
|
+
scriptPath: resolveAlexaWorkerScript(),
|
|
135023
|
+
logger: this.host.logger.withTags({ brokerId }),
|
|
135024
|
+
now: this.now
|
|
135025
|
+
});
|
|
135026
|
+
}
|
|
135027
|
+
async reconcile() {
|
|
135028
|
+
const pythonPath = this.pythonPath;
|
|
135029
|
+
if (pythonPath === null) return;
|
|
135030
|
+
const refs = this.host.listBrokers();
|
|
135031
|
+
const live = new Set(refs.map((ref) => ref.id));
|
|
135032
|
+
for (const [id, echo] of this.echoes) {
|
|
135033
|
+
if (live.has(id)) continue;
|
|
135034
|
+
echo.stop();
|
|
135035
|
+
this.echoes.delete(id);
|
|
135036
|
+
await this.host.sessions.delete(id).catch((err) => {
|
|
135037
|
+
this.host.logger.warn("alexa speak: could not drop the session of a removed account", {
|
|
135038
|
+
tags: { brokerId: id },
|
|
135039
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
135040
|
+
});
|
|
135041
|
+
});
|
|
135042
|
+
}
|
|
135043
|
+
const fresh = refs.filter((ref) => !this.echoes.has(ref.id));
|
|
135044
|
+
for (const ref of fresh) this.echoes.set(ref.id, new BrokerEcho({
|
|
135045
|
+
brokerId: ref.id,
|
|
135046
|
+
brokerName: ref.name,
|
|
135047
|
+
domain: ref.amazonDomain,
|
|
135048
|
+
worker: this.makeWorker(ref.id, pythonPath),
|
|
135049
|
+
readSeed: () => this.host.readSeed(ref.id),
|
|
135050
|
+
sessions: this.host.sessions,
|
|
135051
|
+
logger: this.host.logger
|
|
135052
|
+
}));
|
|
135053
|
+
await Promise.all(refs.map(async (ref) => {
|
|
135054
|
+
const echo = this.echoes.get(ref.id);
|
|
135055
|
+
if (echo !== void 0 && (fresh.includes(ref) || echo.state !== "ready")) await echo.signIn();
|
|
135056
|
+
}));
|
|
135057
|
+
}
|
|
135058
|
+
async refreshAll() {
|
|
135059
|
+
for (const echo of this.echoes.values()) if (echo.state === "ready") await echo.refreshDevices();
|
|
135060
|
+
else await echo.signIn();
|
|
135061
|
+
}
|
|
135062
|
+
async utter(brokerId, mode, targets, text, volume) {
|
|
135063
|
+
const echo = this.echoes.get(brokerId);
|
|
135064
|
+
if (echo === void 0) throw new Error("no Echo session for this Amazon account");
|
|
135065
|
+
return echo.utter(mode === "announce" ? "announce" : "speak", targets, text, volume);
|
|
135066
|
+
}
|
|
135067
|
+
async status() {
|
|
135068
|
+
const brokers = this.host.listBrokers();
|
|
135069
|
+
const rows = await Promise.all(brokers.map(async (ref) => {
|
|
135070
|
+
const echo = this.echoes.get(ref.id);
|
|
135071
|
+
return {
|
|
135072
|
+
id: ref.id,
|
|
135073
|
+
name: ref.name,
|
|
135074
|
+
state: echo?.state ?? "failed",
|
|
135075
|
+
...echo === void 0 ? { error: "embedded Python unavailable" } : echo.error === null ? {} : { error: echo.error },
|
|
135076
|
+
devices: [...echo?.devices ?? []],
|
|
135077
|
+
session: echo === void 0 ? {
|
|
135078
|
+
loggedIn: false,
|
|
135079
|
+
ageSec: 0,
|
|
135080
|
+
lastSuccessAt: null
|
|
135081
|
+
} : await echo.session()
|
|
135082
|
+
};
|
|
135083
|
+
}));
|
|
135084
|
+
const targets = this.settings.targets;
|
|
135085
|
+
const ids = brokers.map((ref) => ref.id);
|
|
135086
|
+
return {
|
|
135087
|
+
brokers: rows,
|
|
135088
|
+
unassignedTargets: unassignedCount(targets),
|
|
135089
|
+
notice: noticeFor(targets, ids)
|
|
135090
|
+
};
|
|
135091
|
+
}
|
|
135092
|
+
async sendTest(input) {
|
|
135093
|
+
const outcomes = await this.utter(input.brokerId, input.mode, input.echoIds, input.text, input.volume);
|
|
135094
|
+
const known = new Map((this.echoes.get(input.brokerId)?.devices ?? []).map((device) => [device.id, device]));
|
|
135095
|
+
const next = [...this.settings.targets];
|
|
135096
|
+
for (const echoId of input.echoIds) {
|
|
135097
|
+
if (next.some((row) => row.echoId === echoId && row.brokerId === input.brokerId)) continue;
|
|
135098
|
+
next.push({
|
|
135099
|
+
id: `echo-${input.brokerId}-${echoId}`,
|
|
135100
|
+
name: known.get(echoId)?.name ?? echoId,
|
|
135101
|
+
enabled: true,
|
|
135102
|
+
brokerId: input.brokerId,
|
|
135103
|
+
echoId,
|
|
135104
|
+
mode: input.mode,
|
|
135105
|
+
volume: input.volume,
|
|
135106
|
+
respectDnd: true
|
|
135107
|
+
});
|
|
135108
|
+
}
|
|
135109
|
+
this.settings = {
|
|
135110
|
+
...this.settings,
|
|
135111
|
+
targets: next
|
|
135112
|
+
};
|
|
135113
|
+
await this.persist();
|
|
135114
|
+
return { outcomes: outcomes.map((row) => ({
|
|
135115
|
+
echoId: row.targetId,
|
|
135116
|
+
ok: row.ok,
|
|
135117
|
+
...row.error !== void 0 ? { error: row.error } : {}
|
|
135118
|
+
})) };
|
|
135119
|
+
}
|
|
135120
|
+
};
|
|
135121
|
+
//#endregion
|
|
135122
|
+
//#region src/speak/icon-routes.ts
|
|
135123
|
+
var ALEXA_ICON_SVG = "<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#00CAFF\"><title>Amazon Alexa</title><path d=\"M12 0C5.37 0 0 5.37 0 12C0 18.09 4.53 23.11 10.4 23.9V21.5A1.59 1.59 0 0 0 9.32 19.97A8.41 8.41 0 0 1 3.6 11.8A8.37 8.37 0 0 1 12.09 3.6A8.4 8.4 0 0 1 20.4 12.31L20.39 12.38A8.68 8.68 0 0 1 20.36 12.76C20.36 12.83 20.35 12.9 20.34 12.96C20.34 13.04 20.33 13.12 20.32 13.19L20.3 13.29C19.27 20.07 10.45 23.87 10.4 23.9C10.92 23.97 11.46 24 12 24C18.63 24 24 18.63 24 12S18.63 0 12 0Z\"/></svg>";
|
|
135124
|
+
/** One day; the mark is immutable per addon version. */
|
|
135125
|
+
var CACHE_CONTROL = "public, max-age=86400, immutable";
|
|
135126
|
+
function speakIconRoutes() {
|
|
135127
|
+
return [{
|
|
135128
|
+
method: "GET",
|
|
135129
|
+
path: `/icons/${ALEXA_SPEAK_KIND}`,
|
|
135130
|
+
access: "public",
|
|
135131
|
+
description: "Alexa brand mark (image/svg+xml) for the Alexa notification target.",
|
|
135132
|
+
handler: async (_request, reply) => {
|
|
135133
|
+
reply.code(200).type("image/svg+xml").header("cache-control", CACHE_CONTROL).send(ALEXA_ICON_SVG);
|
|
135134
|
+
}
|
|
135135
|
+
}];
|
|
135136
|
+
}
|
|
135137
|
+
//#endregion
|
|
135138
|
+
//#region src/speak/session-store.ts
|
|
135139
|
+
/** The Echo worker's cookie jar per broker, in the records store (secret field, keychain-backed). */
|
|
135140
|
+
var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
|
|
135141
|
+
kind;
|
|
135142
|
+
constructor(kind) {
|
|
135143
|
+
this.kind = kind;
|
|
135144
|
+
}
|
|
135145
|
+
static of(records) {
|
|
135146
|
+
return new RecordsSpeakSessionStore(records.kind(alexaSpeakSessionsEntryKind));
|
|
135147
|
+
}
|
|
135148
|
+
async get(brokerId) {
|
|
135149
|
+
try {
|
|
135150
|
+
const resolved = await this.kind.getResolved(brokerId);
|
|
135151
|
+
return resolved === null ? "" : resolved.data.session;
|
|
135152
|
+
} catch (err) {
|
|
135153
|
+
if (err instanceof RecordSecretUnavailableError) return "";
|
|
135154
|
+
throw err;
|
|
135155
|
+
}
|
|
135156
|
+
}
|
|
135157
|
+
async put(brokerId, sessionB64) {
|
|
135158
|
+
await this.kind.put({
|
|
135159
|
+
id: brokerId,
|
|
135160
|
+
data: {
|
|
135161
|
+
name: brokerId,
|
|
135162
|
+
session: sessionB64
|
|
135163
|
+
}
|
|
135164
|
+
});
|
|
135165
|
+
}
|
|
135166
|
+
async delete(brokerId) {
|
|
135167
|
+
await this.kind.delete(brokerId);
|
|
135168
|
+
}
|
|
135169
|
+
};
|
|
135170
|
+
//#endregion
|
|
132682
135171
|
//#region src/addon.ts
|
|
135172
|
+
var SPEAK_PAGE = {
|
|
135173
|
+
id: "speak",
|
|
135174
|
+
label: "Speak on Echo",
|
|
135175
|
+
icon: "megaphone",
|
|
135176
|
+
path: "/addon/import-alexa",
|
|
135177
|
+
remoteName: "addon_import_alexa_page",
|
|
135178
|
+
bundle: "remoteEntry.js",
|
|
135179
|
+
section: "administration"
|
|
135180
|
+
};
|
|
132683
135181
|
/** Default config — a fresh install starts with no accounts. */
|
|
132684
135182
|
var DEFAULTS = {
|
|
132685
135183
|
brokers: [],
|
|
132686
135184
|
proxyLogin: {
|
|
132687
135185
|
proxyHost: "",
|
|
132688
135186
|
proxyPort: 3456
|
|
135187
|
+
},
|
|
135188
|
+
speak: {
|
|
135189
|
+
email: "",
|
|
135190
|
+
password: "",
|
|
135191
|
+
region: "EU",
|
|
135192
|
+
session: "",
|
|
135193
|
+
targets: []
|
|
132689
135194
|
}
|
|
132690
135195
|
};
|
|
132691
135196
|
/** Device types an imported Alexa appliance can materialise as. */
|
|
@@ -132727,6 +135232,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132727
135232
|
proxyLogin = null;
|
|
132728
135233
|
brokerStore = null;
|
|
132729
135234
|
activeBrokers = [];
|
|
135235
|
+
speak = null;
|
|
135236
|
+
secretStatePaths = ["speak.password", "speak.session"];
|
|
132730
135237
|
constructor() {
|
|
132731
135238
|
super({ ...DEFAULTS });
|
|
132732
135239
|
}
|
|
@@ -132734,7 +135241,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132734
135241
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
132735
135242
|
const records = this.ctx.records;
|
|
132736
135243
|
if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
|
|
132737
|
-
await records.declare({
|
|
135244
|
+
await records.declare({
|
|
135245
|
+
brokers: alexaBrokersEntryKind,
|
|
135246
|
+
speakSessions: alexaSpeakSessionsEntryKind
|
|
135247
|
+
});
|
|
132738
135248
|
this.brokerStore = AlexaBrokerStore.of(records);
|
|
132739
135249
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
132740
135250
|
this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
|
|
@@ -132775,26 +135285,71 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132775
135285
|
this.ctx.logger.info("Alexa: import addon initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
132776
135286
|
await this.reconcileIntegrationsToBrokers();
|
|
132777
135287
|
this.subscribeIntegrationLifecycle();
|
|
132778
|
-
const loginRoutes = buildAlexaLoginRoutes({
|
|
135288
|
+
const loginRoutes = [...buildAlexaLoginRoutes({
|
|
132779
135289
|
logger: this.ctx.logger,
|
|
132780
135290
|
proxyLogin: this.proxyLogin,
|
|
132781
135291
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
132782
|
-
});
|
|
132783
|
-
|
|
132784
|
-
|
|
132785
|
-
|
|
132786
|
-
|
|
132787
|
-
|
|
135292
|
+
}), ...speakIconRoutes()];
|
|
135293
|
+
const speak = new AlexaSpeakController({
|
|
135294
|
+
logger: this.ctx.logger.child("speak"),
|
|
135295
|
+
readConfig: () => this.config,
|
|
135296
|
+
writeSpeak: async (next) => {
|
|
135297
|
+
await this.updateGlobalSettings({ speak: next });
|
|
132788
135298
|
},
|
|
132789
|
-
{
|
|
132790
|
-
|
|
132791
|
-
|
|
135299
|
+
listBrokers: () => this.activeBrokers.map((entry) => ({
|
|
135300
|
+
id: entry.id,
|
|
135301
|
+
name: entry.name,
|
|
135302
|
+
amazonDomain: entry.connection.amazonDomain
|
|
135303
|
+
})),
|
|
135304
|
+
readSeed: async (brokerId) => {
|
|
135305
|
+
const entry = this.activeBrokers.find((row) => row.id === brokerId);
|
|
135306
|
+
return entry === void 0 ? null : readBrokerSeed(this.ctx.dataDir, entry);
|
|
132792
135307
|
},
|
|
132793
|
-
|
|
132794
|
-
|
|
132795
|
-
|
|
132796
|
-
|
|
132797
|
-
|
|
135308
|
+
sessions: RecordsSpeakSessionStore.of(records),
|
|
135309
|
+
clearLegacySecrets: async () => {
|
|
135310
|
+
await this.updateGlobalSettingsAsOperator({ speak: {
|
|
135311
|
+
...this.config.speak,
|
|
135312
|
+
password: SECRET_CLEAR,
|
|
135313
|
+
session: SECRET_CLEAR
|
|
135314
|
+
} });
|
|
135315
|
+
},
|
|
135316
|
+
ensurePython: () => this.ctx.deps.ensurePython()
|
|
135317
|
+
});
|
|
135318
|
+
this.speak = speak;
|
|
135319
|
+
speak.start().catch((err) => {
|
|
135320
|
+
this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
135321
|
+
});
|
|
135322
|
+
const pages = {
|
|
135323
|
+
id: this.addonId,
|
|
135324
|
+
listPages: () => [SPEAK_PAGE]
|
|
135325
|
+
};
|
|
135326
|
+
return {
|
|
135327
|
+
providers: [
|
|
135328
|
+
...regs,
|
|
135329
|
+
{
|
|
135330
|
+
capability: brokerCapability,
|
|
135331
|
+
provider: this.buildBrokerProvider()
|
|
135332
|
+
},
|
|
135333
|
+
{
|
|
135334
|
+
capability: deviceAdoptionCapability,
|
|
135335
|
+
provider: this.buildAdoptionProvider()
|
|
135336
|
+
},
|
|
135337
|
+
{
|
|
135338
|
+
capability: addonRoutesCapability,
|
|
135339
|
+
provider: buildAddonRouteProvider("import-alexa", loginRoutes)
|
|
135340
|
+
},
|
|
135341
|
+
{
|
|
135342
|
+
capability: notificationOutputCapability,
|
|
135343
|
+
provider: speak.provider
|
|
135344
|
+
},
|
|
135345
|
+
{
|
|
135346
|
+
capability: addonPagesSourceCapability,
|
|
135347
|
+
provider: pages
|
|
135348
|
+
}
|
|
135349
|
+
],
|
|
135350
|
+
customActions: alexaSpeakActions,
|
|
135351
|
+
actionHandlers: speak.handlers()
|
|
135352
|
+
};
|
|
132798
135353
|
}
|
|
132799
135354
|
async loadPersistedBrokers() {
|
|
132800
135355
|
const store = this.brokerStore;
|
|
@@ -132807,11 +135362,9 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132807
135362
|
async persistBrokerEntries(entries) {
|
|
132808
135363
|
this.activeBrokers = [...entries];
|
|
132809
135364
|
const store = this.brokerStore;
|
|
132810
|
-
if (store !== null)
|
|
132811
|
-
|
|
132812
|
-
|
|
132813
|
-
}
|
|
132814
|
-
await this.updateGlobalSettings({ brokers: [...entries] });
|
|
135365
|
+
if (store !== null) await store.replaceAll(entries);
|
|
135366
|
+
else await this.updateGlobalSettings({ brokers: [...entries] });
|
|
135367
|
+
this.speak?.brokersChanged();
|
|
132815
135368
|
}
|
|
132816
135369
|
async onConfigChanged() {
|
|
132817
135370
|
const reg = this.registry;
|
|
@@ -132857,8 +135410,11 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
132857
135410
|
} });
|
|
132858
135411
|
}
|
|
132859
135412
|
}
|
|
135413
|
+
this.speak?.brokersChanged();
|
|
132860
135414
|
}
|
|
132861
135415
|
async onShutdown() {
|
|
135416
|
+
this.speak?.stop();
|
|
135417
|
+
this.speak = null;
|
|
132862
135418
|
alexaCommands.clear();
|
|
132863
135419
|
try {
|
|
132864
135420
|
await this.proxyLogin?.dispose();
|
|
@@ -133163,4 +135719,4 @@ function capsEqual(a, b) {
|
|
|
133163
135719
|
return true;
|
|
133164
135720
|
}
|
|
133165
135721
|
//#endregion
|
|
133166
|
-
export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
|
|
135722
|
+
export { AlexaImportAddon, AlexaClientNotWiredError as a, alexaAddonConfigSchema as c, alexaSpeakActions as customActions, connectionToCredentials as d, settingsToAlexaConnection as f, AlexaAuthError as i, alexaConnectionSchema as l, mappableAppliance as n, StubAlexaClient as o, mapAlexaCapabilitiesToCamStack as r, createAlexaClient as s, buildAlexaApplianceCandidate as t, buildConnectionFormSchema as u };
|