@camstack/addon-export-alexa 1.2.144 → 1.2.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/export-alexa.addon.js +1252 -93
- package/dist/export-alexa.addon.mjs +1252 -93
- package/package.json +1 -1
|
@@ -5422,7 +5422,7 @@ var ZodIssueCode = {
|
|
|
5422
5422
|
var ZodFirstPartyTypeKind;
|
|
5423
5423
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5424
5424
|
//#endregion
|
|
5425
|
-
//#region ../types/dist/sleep-
|
|
5425
|
+
//#region ../types/dist/sleep-bwsTj221.mjs
|
|
5426
5426
|
/**
|
|
5427
5427
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5428
5428
|
* window to float samples (D455).
|
|
@@ -6673,7 +6673,6 @@ function createHandle(raw, opts) {
|
|
|
6673
6673
|
const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
|
|
6674
6674
|
const readAddonStoreResult = raw.readAddonStoreResult;
|
|
6675
6675
|
const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
|
|
6676
|
-
const readDeviceStoreBatch = raw.readDeviceStoreBatch;
|
|
6677
6676
|
const overrides = {
|
|
6678
6677
|
readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
|
|
6679
6678
|
...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
|
|
@@ -6697,12 +6696,12 @@ function createHandle(raw, opts) {
|
|
|
6697
6696
|
};
|
|
6698
6697
|
} } : {},
|
|
6699
6698
|
readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
|
|
6700
|
-
|
|
6701
|
-
const rows = await readDeviceStoreBatch
|
|
6699
|
+
readDeviceStoreBatch: async (deviceIds) => {
|
|
6700
|
+
const rows = await raw.readDeviceStoreBatch(deviceIds);
|
|
6702
6701
|
const out = /* @__PURE__ */ new Map();
|
|
6703
6702
|
for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
|
|
6704
6703
|
return out;
|
|
6705
|
-
}
|
|
6704
|
+
},
|
|
6706
6705
|
writeAddonStore: async (patch) => {
|
|
6707
6706
|
await writeAddon(patch);
|
|
6708
6707
|
},
|
|
@@ -7529,7 +7528,7 @@ var BaseAddon = class {
|
|
|
7529
7528
|
return this._addonSecretTree;
|
|
7530
7529
|
}
|
|
7531
7530
|
deviceSecretTree() {
|
|
7532
|
-
this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
|
|
7531
|
+
this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
|
|
7533
7532
|
return this._deviceSecretTree;
|
|
7534
7533
|
}
|
|
7535
7534
|
/**
|
|
@@ -7605,42 +7604,135 @@ var BaseAddon = class {
|
|
|
7605
7604
|
this[timerOf] = timer;
|
|
7606
7605
|
}
|
|
7607
7606
|
/**
|
|
7608
|
-
* Wrap `ctx.settings.readAddonStore()` with a
|
|
7609
|
-
*
|
|
7610
|
-
* swap, or any race where the SqliteSettingsBackend is between shutdown and
|
|
7611
|
-
* re-initialize) doesn't propagate up into `initialize()` and leave the
|
|
7607
|
+
* Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
|
|
7608
|
+
* settings-store outage doesn't propagate up into `initialize()` and leave the
|
|
7612
7609
|
* addon permanently broken.
|
|
7613
7610
|
*
|
|
7614
|
-
*
|
|
7615
|
-
*
|
|
7616
|
-
* `
|
|
7611
|
+
* Two kinds of outage, two budgets:
|
|
7612
|
+
* - the store's own restart fingerprints (`SqliteSettingsBackend not
|
|
7613
|
+
* initialized`, `provider not available`): a short budget, then the addon
|
|
7614
|
+
* falls back to `{}` (defaults) - its first successful patch rehydrates;
|
|
7615
|
+
* - a TRANSPORT failure (the hub is restarting while this runner boots:
|
|
7616
|
+
* `discovery deadline exceeded`, `parent could not route`, a refused or
|
|
7617
|
+
* timed-out connection). Since D870 a runner's settings read THROWS instead
|
|
7618
|
+
* of answering `{}`, so without a budget a hub restart ends in a respawn
|
|
7619
|
+
* loop. A long, bounded budget; only after it the error propagates - the
|
|
7620
|
+
* addon never runs on defaults for a store it could not ask.
|
|
7621
|
+
* Anything else propagates immediately so real bugs surface.
|
|
7617
7622
|
*/
|
|
7618
7623
|
async readAddonStoreWithRetry() {
|
|
7619
7624
|
const settings = this.rawSettings();
|
|
7620
7625
|
if (!settings) return {};
|
|
7621
|
-
const delaysMs = [
|
|
7622
|
-
150,
|
|
7623
|
-
350,
|
|
7624
|
-
600,
|
|
7625
|
-
900
|
|
7626
|
-
];
|
|
7627
7626
|
let lastErr;
|
|
7628
|
-
for (let attempt = 0
|
|
7627
|
+
for (let attempt = 0;; attempt++) try {
|
|
7629
7628
|
const stored = await settings.readAddonStore() ?? {};
|
|
7630
7629
|
this.settingsStoreReady = true;
|
|
7631
7630
|
return stored;
|
|
7632
7631
|
} catch (err) {
|
|
7633
7632
|
lastErr = err;
|
|
7634
|
-
const
|
|
7635
|
-
if (
|
|
7636
|
-
|
|
7637
|
-
|
|
7633
|
+
const kind = classifyStoreError(err);
|
|
7634
|
+
if (kind === "permanent") throw err;
|
|
7635
|
+
const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
|
|
7636
|
+
if (delay === void 0) {
|
|
7637
|
+
if (kind === "transport") throw err;
|
|
7638
|
+
break;
|
|
7639
|
+
}
|
|
7640
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
7638
7641
|
}
|
|
7639
7642
|
this.settingsStoreReady = false;
|
|
7640
7643
|
this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
|
|
7641
7644
|
return {};
|
|
7642
7645
|
}
|
|
7643
7646
|
};
|
|
7647
|
+
/** The settings store's own restart fingerprints. */
|
|
7648
|
+
var RESTART_RETRY_DELAYS_MS = [
|
|
7649
|
+
150,
|
|
7650
|
+
350,
|
|
7651
|
+
600,
|
|
7652
|
+
900
|
|
7653
|
+
];
|
|
7654
|
+
/** A hub restarting under a booting runner: ~100 s in total. */
|
|
7655
|
+
var TRANSPORT_RETRY_DELAYS_MS = [
|
|
7656
|
+
150,
|
|
7657
|
+
350,
|
|
7658
|
+
600,
|
|
7659
|
+
900,
|
|
7660
|
+
2e3,
|
|
7661
|
+
5e3,
|
|
7662
|
+
1e4,
|
|
7663
|
+
2e4,
|
|
7664
|
+
3e4,
|
|
7665
|
+
3e4
|
|
7666
|
+
];
|
|
7667
|
+
var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
|
|
7668
|
+
/** Node / socket error codes of a connection that did not happen or broke. */
|
|
7669
|
+
var TRANSPORT_CODES = new Set([
|
|
7670
|
+
"ECONNREFUSED",
|
|
7671
|
+
"ECONNRESET",
|
|
7672
|
+
"ETIMEDOUT",
|
|
7673
|
+
"EPIPE",
|
|
7674
|
+
"EHOSTUNREACH",
|
|
7675
|
+
"ENETUNREACH"
|
|
7676
|
+
]);
|
|
7677
|
+
/** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
|
|
7678
|
+
var UNAVAILABLE_TRPC_CODES = new Set([
|
|
7679
|
+
"TIMEOUT",
|
|
7680
|
+
"SERVICE_UNAVAILABLE",
|
|
7681
|
+
"BAD_GATEWAY",
|
|
7682
|
+
"GATEWAY_TIMEOUT"
|
|
7683
|
+
]);
|
|
7684
|
+
/** The exact phrases OUR transport raises when it cannot reach the hub. */
|
|
7685
|
+
var TRANSPORT_PHRASES = [
|
|
7686
|
+
"discovery deadline exceeded",
|
|
7687
|
+
"parent could not route child unowned cap call",
|
|
7688
|
+
"UDS request timed out after",
|
|
7689
|
+
"channel closed"
|
|
7690
|
+
];
|
|
7691
|
+
/** Moleculer error `type`s of a call that could not be served right now. */
|
|
7692
|
+
var MOLECULER_TRANSIENT_TYPES = new Set([
|
|
7693
|
+
"REQUEST_TIMEOUT",
|
|
7694
|
+
"SERVICE_NOT_FOUND",
|
|
7695
|
+
"SERVICE_NOT_AVAILABLE",
|
|
7696
|
+
"REQUEST_REJECTED",
|
|
7697
|
+
"BROKER_DISCONNECTED"
|
|
7698
|
+
]);
|
|
7699
|
+
function stringProp(value, key) {
|
|
7700
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7701
|
+
const found = Reflect.get(value, key);
|
|
7702
|
+
return typeof found === "string" ? found : void 0;
|
|
7703
|
+
}
|
|
7704
|
+
/** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
|
|
7705
|
+
function errorCode(err) {
|
|
7706
|
+
const out = {};
|
|
7707
|
+
const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
|
|
7708
|
+
const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
|
|
7709
|
+
const node = stringProp(err, "code") ?? stringProp(cause, "code");
|
|
7710
|
+
if (node !== void 0) out.node = node;
|
|
7711
|
+
const trpc = stringProp(data, "code");
|
|
7712
|
+
if (trpc !== void 0) out.trpc = trpc;
|
|
7713
|
+
return out;
|
|
7714
|
+
}
|
|
7715
|
+
/**
|
|
7716
|
+
* Is a settings read failure worth retrying, and with which budget? Only
|
|
7717
|
+
* EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
|
|
7718
|
+
* one of the exact phrases our own transport raises. A message that merely
|
|
7719
|
+
* contains "timed out" or "unreachable" is not evidence of anything, and
|
|
7720
|
+
* `settings store not wired` is a wiring bug that retrying cannot fix.
|
|
7721
|
+
*/
|
|
7722
|
+
function classifyStoreError(err) {
|
|
7723
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7724
|
+
if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
|
|
7725
|
+
const code = errorCode(err);
|
|
7726
|
+
if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
|
|
7727
|
+
if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
|
|
7728
|
+
if (typeof err === "object" && err !== null) {
|
|
7729
|
+
if (Reflect.get(err, "retryable") === true) return "transport";
|
|
7730
|
+
const type = stringProp(err, "type");
|
|
7731
|
+
if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
|
|
7732
|
+
}
|
|
7733
|
+
if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
|
|
7734
|
+
return "permanent";
|
|
7735
|
+
}
|
|
7644
7736
|
/**
|
|
7645
7737
|
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
7646
7738
|
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
@@ -8217,7 +8309,7 @@ object({
|
|
|
8217
8309
|
params: record(string(), union([string(), number()])).optional()
|
|
8218
8310
|
});
|
|
8219
8311
|
//#endregion
|
|
8220
|
-
//#region ../types/dist/err-msg
|
|
8312
|
+
//#region ../types/dist/err-msg--2RrYiLO.mjs
|
|
8221
8313
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
8222
8314
|
DeviceType["Camera"] = "camera";
|
|
8223
8315
|
DeviceType["Hub"] = "hub";
|
|
@@ -8486,6 +8578,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
8486
8578
|
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
8487
8579
|
* real Switch device for UI rendering / export adapters. */
|
|
8488
8580
|
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
8581
|
+
/** A smart plug: a `switch` whose source says it is an outlet (HA
|
|
8582
|
+
* `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
|
|
8583
|
+
DeviceRole["Outlet"] = "outlet";
|
|
8489
8584
|
/** Generic motion / occupancy / moving event source. Distinct from
|
|
8490
8585
|
* the camera accessory PirSensor role: that one is a camera child;
|
|
8491
8586
|
* this is a standalone HA / 3rd-party motion sensor. */
|
|
@@ -8563,7 +8658,7 @@ function errMsg(err) {
|
|
|
8563
8658
|
return String(err);
|
|
8564
8659
|
}
|
|
8565
8660
|
//#endregion
|
|
8566
|
-
//#region ../types/dist/composition-
|
|
8661
|
+
//#region ../types/dist/composition-DOwQB_Ua.mjs
|
|
8567
8662
|
/**
|
|
8568
8663
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
8569
8664
|
*
|
|
@@ -12024,11 +12119,10 @@ var ClipModelMetaSchema = object({
|
|
|
12024
12119
|
/**
|
|
12025
12120
|
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
12026
12121
|
* per index: two models share an index only when their vectors are ranked
|
|
12027
|
-
* against each other, which for CLIP means never across families.
|
|
12028
|
-
*
|
|
12029
|
-
* `modelId`); any other model gets its own.
|
|
12122
|
+
* against each other, which for CLIP means never across families. Every
|
|
12123
|
+
* model gets its own.
|
|
12030
12124
|
*/
|
|
12031
|
-
vectorIndex: string().regex(/^object-clip
|
|
12125
|
+
vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
|
|
12032
12126
|
/**
|
|
12033
12127
|
* The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
|
|
12034
12128
|
* must be unique per tokenizer: every model's siblings land in ONE flat
|
|
@@ -16169,6 +16263,28 @@ id: union([string(), number()]) }), {
|
|
|
16169
16263
|
kind: "mutation",
|
|
16170
16264
|
auth: "admin"
|
|
16171
16265
|
});
|
|
16266
|
+
/** One table of the settings database. */
|
|
16267
|
+
var StorageTableSchema = object({
|
|
16268
|
+
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
16269
|
+
name: string(),
|
|
16270
|
+
/** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
|
|
16271
|
+
owner: string().nullable(),
|
|
16272
|
+
rows: number().int().nonnegative(),
|
|
16273
|
+
/** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
|
|
16274
|
+
bytes: number().int().nonnegative().nullable(),
|
|
16275
|
+
/** Declared by an addon since this engine process started (a running addon claims it). */
|
|
16276
|
+
declaredThisBoot: boolean()
|
|
16277
|
+
});
|
|
16278
|
+
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
16279
|
+
collection: string().min(1),
|
|
16280
|
+
expectRows: number().int().nonnegative()
|
|
16281
|
+
}), object({
|
|
16282
|
+
dropped: literal(true),
|
|
16283
|
+
rows: number().int().nonnegative()
|
|
16284
|
+
}), {
|
|
16285
|
+
kind: "mutation",
|
|
16286
|
+
auth: "admin"
|
|
16287
|
+
});
|
|
16172
16288
|
/**
|
|
16173
16289
|
* `keychain` — the hub's encrypted store of secret values (D787).
|
|
16174
16290
|
*
|
|
@@ -16710,6 +16826,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
|
|
|
16710
16826
|
kind: "mutation",
|
|
16711
16827
|
auth: "admin",
|
|
16712
16828
|
caller: "required"
|
|
16829
|
+
}), method(object({
|
|
16830
|
+
ref: string().min(1),
|
|
16831
|
+
value: string().min(1)
|
|
16832
|
+
}), object({
|
|
16833
|
+
ref: string(),
|
|
16834
|
+
version: number().int()
|
|
16835
|
+
}), {
|
|
16836
|
+
kind: "mutation",
|
|
16837
|
+
auth: "admin",
|
|
16838
|
+
caller: "required"
|
|
16713
16839
|
}), method(object({ refs: array(string().min(1)).max(1e3) }), object({
|
|
16714
16840
|
deleted: array(string()).readonly(),
|
|
16715
16841
|
kept: array(object({
|
|
@@ -53167,7 +53293,7 @@ var HvacModeSchema = _enum([
|
|
|
53167
53293
|
"fan_only",
|
|
53168
53294
|
"dry"
|
|
53169
53295
|
]);
|
|
53170
|
-
object({
|
|
53296
|
+
var ClimateControlStatusSchema = object({
|
|
53171
53297
|
/** Active HVAC mode. */
|
|
53172
53298
|
mode: HvacModeSchema,
|
|
53173
53299
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -53208,6 +53334,12 @@ object({
|
|
|
53208
53334
|
/** Horizontal louver swing on/off. Null when the device has no
|
|
53209
53335
|
* horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
|
|
53210
53336
|
swingHorizontal: boolean().nullable(),
|
|
53337
|
+
/** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53338
|
+
minTemp: number().nullable().optional(),
|
|
53339
|
+
/** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53340
|
+
maxTemp: number().nullable().optional(),
|
|
53341
|
+
/** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
|
|
53342
|
+
tempStep: number().positive().nullable().optional(),
|
|
53211
53343
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
53212
53344
|
lastFetchedAt: number()
|
|
53213
53345
|
});
|
|
@@ -55048,7 +55180,19 @@ var TlsStatusSchema = object({
|
|
|
55048
55180
|
sans: array(string()),
|
|
55049
55181
|
caCertPem: string().nullable(),
|
|
55050
55182
|
reissueError: string().nullable(),
|
|
55051
|
-
restartRequired: boolean()
|
|
55183
|
+
restartRequired: boolean(),
|
|
55184
|
+
/** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
|
|
55185
|
+
caMissing: boolean(),
|
|
55186
|
+
/** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
|
|
55187
|
+
localCaConstrained: boolean().nullable(),
|
|
55188
|
+
/** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
|
|
55189
|
+
caReplacedAt: string().nullable(),
|
|
55190
|
+
/** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
|
|
55191
|
+
storedUpload: object({
|
|
55192
|
+
leafFingerprintSha256: string(),
|
|
55193
|
+
validTo: string(),
|
|
55194
|
+
hasCa: boolean()
|
|
55195
|
+
}).nullable()
|
|
55052
55196
|
});
|
|
55053
55197
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
55054
55198
|
/**
|
|
@@ -55104,6 +55248,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
55104
55248
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
55105
55249
|
kind: "mutation",
|
|
55106
55250
|
auth: "admin"
|
|
55251
|
+
}), method(_void(), TlsStatusSchema, {
|
|
55252
|
+
kind: "mutation",
|
|
55253
|
+
auth: "admin"
|
|
55107
55254
|
});
|
|
55108
55255
|
var LockControlStatusSchema = object({
|
|
55109
55256
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
@@ -64143,6 +64290,12 @@ Object.freeze({
|
|
|
64143
64290
|
addonId: null,
|
|
64144
64291
|
access: "create"
|
|
64145
64292
|
},
|
|
64293
|
+
"keychainAdmin.importSecret": {
|
|
64294
|
+
capName: "keychain-admin",
|
|
64295
|
+
capScope: "system",
|
|
64296
|
+
addonId: null,
|
|
64297
|
+
access: "create"
|
|
64298
|
+
},
|
|
64146
64299
|
"keychainAdmin.listAudit": {
|
|
64147
64300
|
capName: "keychain-admin",
|
|
64148
64301
|
capScope: "system",
|
|
@@ -64539,6 +64692,12 @@ Object.freeze({
|
|
|
64539
64692
|
addonId: null,
|
|
64540
64693
|
access: "create"
|
|
64541
64694
|
},
|
|
64695
|
+
"localNetwork.useStoredUploadedCertificate": {
|
|
64696
|
+
capName: "local-network",
|
|
64697
|
+
capScope: "system",
|
|
64698
|
+
addonId: null,
|
|
64699
|
+
access: "create"
|
|
64700
|
+
},
|
|
64542
64701
|
"lockControl.getStatus": {
|
|
64543
64702
|
capName: "lock-control",
|
|
64544
64703
|
capScope: "device",
|
|
@@ -67923,6 +68082,18 @@ Object.freeze({
|
|
|
67923
68082
|
addonId: null,
|
|
67924
68083
|
access: "create"
|
|
67925
68084
|
},
|
|
68085
|
+
"storageAdmin.dropCollection": {
|
|
68086
|
+
capName: "storage-admin",
|
|
68087
|
+
capScope: "system",
|
|
68088
|
+
addonId: null,
|
|
68089
|
+
access: "delete"
|
|
68090
|
+
},
|
|
68091
|
+
"storageAdmin.listTables": {
|
|
68092
|
+
capName: "storage-admin",
|
|
68093
|
+
capScope: "system",
|
|
68094
|
+
addonId: null,
|
|
68095
|
+
access: "view"
|
|
68096
|
+
},
|
|
67926
68097
|
"storageEvictable.evict": {
|
|
67927
68098
|
capName: "storage-evictable",
|
|
67928
68099
|
capScope: "system",
|
|
@@ -74027,6 +74198,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
|
|
|
74027
74198
|
await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
|
|
74028
74199
|
}
|
|
74029
74200
|
//#endregion
|
|
74201
|
+
//#region src/endpoints/adapters/climate-model.ts
|
|
74202
|
+
/**
|
|
74203
|
+
* What a `climate-control` device is to Alexa (D874) — pure, so the discovery
|
|
74204
|
+
* shape, the slice mapping and the directive handlers share one definition.
|
|
74205
|
+
*
|
|
74206
|
+
* `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
|
|
74207
|
+
* upperSetpoint (a device with a range),
|
|
74208
|
+
* thermostatMode HEAT / COOL / AUTO / ECO /
|
|
74209
|
+
* OFF
|
|
74210
|
+
* `Alexa.TemperatureSensor` the current temperature
|
|
74211
|
+
* `Alexa.PowerController` when the device can be turned off
|
|
74212
|
+
* `Alexa.ModeController` Climate.Mode only when the device has a mode
|
|
74213
|
+
* Amazon's thermostat vocabulary lacks
|
|
74214
|
+
* (dry, fan_only)
|
|
74215
|
+
* `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
|
|
74216
|
+
*
|
|
74217
|
+
* Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
|
|
74218
|
+
* mode is not a documented way to carry them: they travel through the
|
|
74219
|
+
* `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
|
|
74220
|
+
* while the device is in one of them.
|
|
74221
|
+
*
|
|
74222
|
+
* Temperatures are Celsius throughout (the cap's contract); Alexa converts for
|
|
74223
|
+
* the account.
|
|
74224
|
+
*/
|
|
74225
|
+
var INSTANCE_MODE = "Climate.Mode";
|
|
74226
|
+
var INSTANCE_FAN = "Climate.FanSpeed";
|
|
74227
|
+
var FAN_PREFIX = "Fan.";
|
|
74228
|
+
var DEFAULT_TEMP_STEP = .5;
|
|
74229
|
+
var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
|
|
74230
|
+
var ALEXA_ECO_PRESET = "eco";
|
|
74231
|
+
var SINGLE_SETPOINT_MODES = [
|
|
74232
|
+
"heat",
|
|
74233
|
+
"cool",
|
|
74234
|
+
"auto",
|
|
74235
|
+
"dry"
|
|
74236
|
+
];
|
|
74237
|
+
/** What TurnOn switches to, best first. */
|
|
74238
|
+
var TURN_ON_PREFERENCE = [
|
|
74239
|
+
"auto",
|
|
74240
|
+
"heat_cool",
|
|
74241
|
+
"cool",
|
|
74242
|
+
"heat"
|
|
74243
|
+
];
|
|
74244
|
+
var SliceShape$1 = ClimateControlStatusSchema.pick({
|
|
74245
|
+
mode: true,
|
|
74246
|
+
availableModes: true,
|
|
74247
|
+
fanMode: true,
|
|
74248
|
+
availableFanModes: true,
|
|
74249
|
+
preset: true,
|
|
74250
|
+
availablePresets: true,
|
|
74251
|
+
target: true,
|
|
74252
|
+
targetHigh: true,
|
|
74253
|
+
targetLow: true,
|
|
74254
|
+
currentTemp: true,
|
|
74255
|
+
minTemp: true,
|
|
74256
|
+
maxTemp: true,
|
|
74257
|
+
tempStep: true
|
|
74258
|
+
});
|
|
74259
|
+
/** The slice fields the adapter reads, or null when the slice is not a climate one. */
|
|
74260
|
+
function readClimateSlice(slice) {
|
|
74261
|
+
const parsed = SliceShape$1.safeParse(slice);
|
|
74262
|
+
return parsed.success ? parsed.data : null;
|
|
74263
|
+
}
|
|
74264
|
+
function boundsOf(slice) {
|
|
74265
|
+
const min = slice.minTemp ?? 7;
|
|
74266
|
+
const max = slice.maxTemp ?? 35;
|
|
74267
|
+
const step = slice.tempStep ?? .5;
|
|
74268
|
+
return min < max ? {
|
|
74269
|
+
min,
|
|
74270
|
+
max,
|
|
74271
|
+
step
|
|
74272
|
+
} : {
|
|
74273
|
+
min: 7,
|
|
74274
|
+
max: 35,
|
|
74275
|
+
step: DEFAULT_TEMP_STEP
|
|
74276
|
+
};
|
|
74277
|
+
}
|
|
74278
|
+
/** One decimal, the precision Alexa's own conversions keep. */
|
|
74279
|
+
function round1(value) {
|
|
74280
|
+
return Math.round(value * 10) / 10;
|
|
74281
|
+
}
|
|
74282
|
+
/**
|
|
74283
|
+
* Snap a setpoint to the device's step (anchored at its minimum), then clamp:
|
|
74284
|
+
* a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
|
|
74285
|
+
*/
|
|
74286
|
+
function snapToStep(value, bounds) {
|
|
74287
|
+
const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
|
|
74288
|
+
return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
|
|
74289
|
+
}
|
|
74290
|
+
function temperatureValue(celsius) {
|
|
74291
|
+
return {
|
|
74292
|
+
value: round1(celsius),
|
|
74293
|
+
scale: "CELSIUS"
|
|
74294
|
+
};
|
|
74295
|
+
}
|
|
74296
|
+
function hasMode(slice, mode) {
|
|
74297
|
+
return slice !== null && slice.availableModes.includes(mode);
|
|
74298
|
+
}
|
|
74299
|
+
/** Dual setpoints (a heat/cool range). */
|
|
74300
|
+
function supportsDual(slice, features) {
|
|
74301
|
+
return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
|
|
74302
|
+
}
|
|
74303
|
+
/** A single setpoint (everything but a range-only device). */
|
|
74304
|
+
function supportsSingle(slice) {
|
|
74305
|
+
return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
|
|
74306
|
+
}
|
|
74307
|
+
/** The device's own name for its `eco` preset, if it has one. */
|
|
74308
|
+
function ecoPresetOf(slice) {
|
|
74309
|
+
return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
|
|
74310
|
+
}
|
|
74311
|
+
/** Amazon thermostat modes the device can be put in, in a stable order. */
|
|
74312
|
+
function supportedThermostatModes(slice) {
|
|
74313
|
+
if (slice === null || slice.availableModes.length === 0) return [
|
|
74314
|
+
"HEAT",
|
|
74315
|
+
"COOL",
|
|
74316
|
+
"AUTO",
|
|
74317
|
+
"OFF"
|
|
74318
|
+
];
|
|
74319
|
+
const out = [];
|
|
74320
|
+
if (hasMode(slice, "heat")) out.push("HEAT");
|
|
74321
|
+
if (hasMode(slice, "cool")) out.push("COOL");
|
|
74322
|
+
if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
|
|
74323
|
+
if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
|
|
74324
|
+
if (hasMode(slice, "off")) out.push("OFF");
|
|
74325
|
+
return out;
|
|
74326
|
+
}
|
|
74327
|
+
/** The HVAC mode TurnOn / leaving OFF switches to. */
|
|
74328
|
+
function onModeOf(slice) {
|
|
74329
|
+
return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
|
|
74330
|
+
}
|
|
74331
|
+
function canPowerOff(slice) {
|
|
74332
|
+
return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
|
|
74333
|
+
}
|
|
74334
|
+
/** A device with a mode Amazon's thermostat vocabulary lacks. */
|
|
74335
|
+
function hasExtraModes(slice) {
|
|
74336
|
+
return hasMode(slice, "dry") || hasMode(slice, "fan_only");
|
|
74337
|
+
}
|
|
74338
|
+
var MODE_VALUE = {
|
|
74339
|
+
heat: "Mode.Heat",
|
|
74340
|
+
cool: "Mode.Cool",
|
|
74341
|
+
auto: "Mode.Auto",
|
|
74342
|
+
dry: "Mode.Dry",
|
|
74343
|
+
fan_only: "Mode.Fan",
|
|
74344
|
+
off: "Mode.Off"
|
|
74345
|
+
};
|
|
74346
|
+
var MODE_NAMES = {
|
|
74347
|
+
"Mode.Heat": ["Riscaldamento", "Heating"],
|
|
74348
|
+
"Mode.Cool": ["Raffrescamento", "Cooling"],
|
|
74349
|
+
"Mode.Auto": ["Automatico", "Auto"],
|
|
74350
|
+
"Mode.Dry": ["Deumidificazione", "Dry"],
|
|
74351
|
+
"Mode.Fan": ["Ventilazione", "Fan only"],
|
|
74352
|
+
"Mode.Off": ["Spento", "Off"]
|
|
74353
|
+
};
|
|
74354
|
+
/** The ModeController value a HVAC mode reports as (a range is "auto"). */
|
|
74355
|
+
function modeValueOf(mode) {
|
|
74356
|
+
return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
|
|
74357
|
+
}
|
|
74358
|
+
/** The HVAC mode a ModeController value asks for, among those the device has. */
|
|
74359
|
+
function hvacModeForValue(value, slice) {
|
|
74360
|
+
if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
|
|
74361
|
+
const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
|
|
74362
|
+
if (mode === void 0) return null;
|
|
74363
|
+
const candidate = SliceShape$1.shape.mode.safeParse(mode);
|
|
74364
|
+
return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
|
|
74365
|
+
}
|
|
74366
|
+
/** The ModeController values the device offers, in a stable order. */
|
|
74367
|
+
function modeValuesOf(slice) {
|
|
74368
|
+
return [
|
|
74369
|
+
"heat",
|
|
74370
|
+
"cool",
|
|
74371
|
+
"auto",
|
|
74372
|
+
"dry",
|
|
74373
|
+
"fan_only",
|
|
74374
|
+
"off"
|
|
74375
|
+
].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
|
|
74376
|
+
}
|
|
74377
|
+
function textNames(it, en) {
|
|
74378
|
+
return [{
|
|
74379
|
+
"@type": "text",
|
|
74380
|
+
value: {
|
|
74381
|
+
text: it,
|
|
74382
|
+
locale: "it-IT"
|
|
74383
|
+
}
|
|
74384
|
+
}, {
|
|
74385
|
+
"@type": "text",
|
|
74386
|
+
value: {
|
|
74387
|
+
text: en,
|
|
74388
|
+
locale: "en-US"
|
|
74389
|
+
}
|
|
74390
|
+
}];
|
|
74391
|
+
}
|
|
74392
|
+
function modeCapability(slice) {
|
|
74393
|
+
const values = modeValuesOf(slice);
|
|
74394
|
+
if (values.length < 2) return null;
|
|
74395
|
+
return {
|
|
74396
|
+
type: "AlexaInterface",
|
|
74397
|
+
interface: "Alexa.ModeController",
|
|
74398
|
+
version: "3",
|
|
74399
|
+
instance: INSTANCE_MODE,
|
|
74400
|
+
capabilityResources: { friendlyNames: [{
|
|
74401
|
+
"@type": "asset",
|
|
74402
|
+
value: { assetId: "Alexa.Setting.Mode" }
|
|
74403
|
+
}, ...textNames("Modalità", "Mode")] },
|
|
74404
|
+
properties: {
|
|
74405
|
+
supported: [{ name: "mode" }],
|
|
74406
|
+
proactivelyReported: true,
|
|
74407
|
+
retrievable: true
|
|
74408
|
+
},
|
|
74409
|
+
configuration: {
|
|
74410
|
+
ordered: false,
|
|
74411
|
+
supportedModes: values.map((value) => {
|
|
74412
|
+
const [it, en] = MODE_NAMES[value] ?? [value, value];
|
|
74413
|
+
return {
|
|
74414
|
+
value,
|
|
74415
|
+
modeResources: { friendlyNames: textNames(it, en) }
|
|
74416
|
+
};
|
|
74417
|
+
})
|
|
74418
|
+
}
|
|
74419
|
+
};
|
|
74420
|
+
}
|
|
74421
|
+
var FAN_ASSETS = {
|
|
74422
|
+
low: "Alexa.Value.Low",
|
|
74423
|
+
medium: "Alexa.Value.Medium",
|
|
74424
|
+
high: "Alexa.Value.High"
|
|
74425
|
+
};
|
|
74426
|
+
var FAN_TEXTS = {
|
|
74427
|
+
auto: ["Automatico", "Auto"],
|
|
74428
|
+
quiet: ["Silenzioso", "Quiet"],
|
|
74429
|
+
turbo: ["Turbo", "Turbo"],
|
|
74430
|
+
on: ["Acceso", "On"],
|
|
74431
|
+
off: ["Spento", "Off"],
|
|
74432
|
+
middle: ["Medio", "Medium"],
|
|
74433
|
+
mediumlow: ["Medio basso", "Medium low"],
|
|
74434
|
+
mediumhigh: ["Medio alto", "Medium high"],
|
|
74435
|
+
medium_low: ["Medio basso", "Medium low"],
|
|
74436
|
+
medium_high: ["Medio alto", "Medium high"]
|
|
74437
|
+
};
|
|
74438
|
+
function capitalize(word) {
|
|
74439
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
74440
|
+
}
|
|
74441
|
+
/** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
|
|
74442
|
+
function fanValueOf(fanMode) {
|
|
74443
|
+
const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
|
|
74444
|
+
return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
|
|
74445
|
+
}
|
|
74446
|
+
/** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
|
|
74447
|
+
function fanModesOf(slice) {
|
|
74448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74449
|
+
const out = [];
|
|
74450
|
+
for (const fanMode of slice.availableFanModes) {
|
|
74451
|
+
const value = fanValueOf(fanMode);
|
|
74452
|
+
if (value === null || seen.has(value)) continue;
|
|
74453
|
+
seen.add(value);
|
|
74454
|
+
out.push([value, fanMode]);
|
|
74455
|
+
}
|
|
74456
|
+
return out;
|
|
74457
|
+
}
|
|
74458
|
+
function hasFanModes(slice) {
|
|
74459
|
+
return slice !== null && fanModesOf(slice).length >= 2;
|
|
74460
|
+
}
|
|
74461
|
+
function fanFriendlyNames(fanMode) {
|
|
74462
|
+
const key = fanMode.toLowerCase();
|
|
74463
|
+
const asset = FAN_ASSETS[key];
|
|
74464
|
+
if (asset !== void 0) return [{
|
|
74465
|
+
"@type": "asset",
|
|
74466
|
+
value: { assetId: asset }
|
|
74467
|
+
}];
|
|
74468
|
+
const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
|
|
74469
|
+
return textNames(it, en);
|
|
74470
|
+
}
|
|
74471
|
+
function fanCapability(slice) {
|
|
74472
|
+
const modes = fanModesOf(slice);
|
|
74473
|
+
if (modes.length < 2) return null;
|
|
74474
|
+
return {
|
|
74475
|
+
type: "AlexaInterface",
|
|
74476
|
+
interface: "Alexa.ModeController",
|
|
74477
|
+
version: "3",
|
|
74478
|
+
instance: INSTANCE_FAN,
|
|
74479
|
+
capabilityResources: { friendlyNames: [{
|
|
74480
|
+
"@type": "asset",
|
|
74481
|
+
value: { assetId: "Alexa.Setting.FanSpeed" }
|
|
74482
|
+
}, ...textNames("Velocità ventola", "Fan speed")] },
|
|
74483
|
+
properties: {
|
|
74484
|
+
supported: [{ name: "mode" }],
|
|
74485
|
+
proactivelyReported: true,
|
|
74486
|
+
retrievable: true
|
|
74487
|
+
},
|
|
74488
|
+
configuration: {
|
|
74489
|
+
ordered: false,
|
|
74490
|
+
supportedModes: modes.map(([value, fanMode]) => ({
|
|
74491
|
+
value,
|
|
74492
|
+
modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
|
|
74493
|
+
}))
|
|
74494
|
+
}
|
|
74495
|
+
};
|
|
74496
|
+
}
|
|
74497
|
+
function propertiesBlock(names) {
|
|
74498
|
+
return {
|
|
74499
|
+
supported: names.map((name) => ({ name })),
|
|
74500
|
+
proactivelyReported: true,
|
|
74501
|
+
retrievable: true
|
|
74502
|
+
};
|
|
74503
|
+
}
|
|
74504
|
+
function thermostatCapability(slice, modes, features) {
|
|
74505
|
+
return {
|
|
74506
|
+
type: "AlexaInterface",
|
|
74507
|
+
interface: "Alexa.ThermostatController",
|
|
74508
|
+
version: "3.1",
|
|
74509
|
+
properties: propertiesBlock([
|
|
74510
|
+
...supportsSingle(slice) ? ["targetSetpoint"] : [],
|
|
74511
|
+
...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
|
|
74512
|
+
"thermostatMode"
|
|
74513
|
+
]),
|
|
74514
|
+
configuration: {
|
|
74515
|
+
supportedModes: modes,
|
|
74516
|
+
supportsScheduling: false
|
|
74517
|
+
}
|
|
74518
|
+
};
|
|
74519
|
+
}
|
|
74520
|
+
/** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
|
|
74521
|
+
function climateCategoryFor(deviceType) {
|
|
74522
|
+
return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
|
|
74523
|
+
}
|
|
74524
|
+
function describeClimate(device) {
|
|
74525
|
+
const slice = readClimateSlice(device.slices["climate-control"]);
|
|
74526
|
+
const modes = supportedThermostatModes(slice);
|
|
74527
|
+
if (modes.length === 0) return {
|
|
74528
|
+
kind: "skip",
|
|
74529
|
+
reason: "climate: the device offers no mode Alexa has a thermostat value for"
|
|
74530
|
+
};
|
|
74531
|
+
const extras = [];
|
|
74532
|
+
if (slice !== null) {
|
|
74533
|
+
const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
|
|
74534
|
+
const fan = hasFanModes(slice) ? fanCapability(slice) : null;
|
|
74535
|
+
if (mode !== null) extras.push(mode);
|
|
74536
|
+
if (fan !== null) extras.push(fan);
|
|
74537
|
+
}
|
|
74538
|
+
return {
|
|
74539
|
+
kind: "publish",
|
|
74540
|
+
capabilities: [
|
|
74541
|
+
thermostatCapability(slice, modes, device.features),
|
|
74542
|
+
{
|
|
74543
|
+
type: "AlexaInterface",
|
|
74544
|
+
interface: "Alexa.TemperatureSensor",
|
|
74545
|
+
version: "3",
|
|
74546
|
+
properties: propertiesBlock(["temperature"])
|
|
74547
|
+
},
|
|
74548
|
+
...canPowerOff(slice) ? [{
|
|
74549
|
+
type: "AlexaInterface",
|
|
74550
|
+
interface: "Alexa.PowerController",
|
|
74551
|
+
version: "3",
|
|
74552
|
+
properties: propertiesBlock(["powerState"])
|
|
74553
|
+
}] : [],
|
|
74554
|
+
...extras
|
|
74555
|
+
],
|
|
74556
|
+
category: climateCategoryFor(device.type)
|
|
74557
|
+
};
|
|
74558
|
+
}
|
|
74559
|
+
/** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
|
|
74560
|
+
function thermostatModeOf(slice) {
|
|
74561
|
+
if (slice.mode === "off") return "OFF";
|
|
74562
|
+
const eco = ecoPresetOf(slice);
|
|
74563
|
+
if (eco !== null && slice.preset === eco) return "ECO";
|
|
74564
|
+
switch (slice.mode) {
|
|
74565
|
+
case "heat": return "HEAT";
|
|
74566
|
+
case "cool": return "COOL";
|
|
74567
|
+
case "auto":
|
|
74568
|
+
case "heat_cool": return "AUTO";
|
|
74569
|
+
default: return null;
|
|
74570
|
+
}
|
|
74571
|
+
}
|
|
74572
|
+
function climateProperties(slice) {
|
|
74573
|
+
const s = readClimateSlice(slice);
|
|
74574
|
+
if (s === null) return [];
|
|
74575
|
+
const out = [];
|
|
74576
|
+
const thermostat = (name, value) => {
|
|
74577
|
+
out.push({
|
|
74578
|
+
namespace: "Alexa.ThermostatController",
|
|
74579
|
+
name,
|
|
74580
|
+
value
|
|
74581
|
+
});
|
|
74582
|
+
};
|
|
74583
|
+
if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
|
|
74584
|
+
thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
|
|
74585
|
+
thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
|
|
74586
|
+
} else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
|
|
74587
|
+
const mode = thermostatModeOf(s);
|
|
74588
|
+
if (mode !== null) thermostat("thermostatMode", mode);
|
|
74589
|
+
if (s.currentTemp !== null) out.push({
|
|
74590
|
+
namespace: "Alexa.TemperatureSensor",
|
|
74591
|
+
name: "temperature",
|
|
74592
|
+
value: temperatureValue(s.currentTemp)
|
|
74593
|
+
});
|
|
74594
|
+
if (canPowerOff(s)) out.push({
|
|
74595
|
+
namespace: "Alexa.PowerController",
|
|
74596
|
+
name: "powerState",
|
|
74597
|
+
value: s.mode === "off" ? "OFF" : "ON"
|
|
74598
|
+
});
|
|
74599
|
+
if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
|
|
74600
|
+
namespace: "Alexa.ModeController",
|
|
74601
|
+
instance: INSTANCE_MODE,
|
|
74602
|
+
name: "mode",
|
|
74603
|
+
value: modeValueOf(s.mode)
|
|
74604
|
+
});
|
|
74605
|
+
const fan = fanValueOf(s.fanMode);
|
|
74606
|
+
if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
|
|
74607
|
+
namespace: "Alexa.ModeController",
|
|
74608
|
+
instance: INSTANCE_FAN,
|
|
74609
|
+
name: "mode",
|
|
74610
|
+
value: fan
|
|
74611
|
+
});
|
|
74612
|
+
return out;
|
|
74613
|
+
}
|
|
74614
|
+
//#endregion
|
|
74615
|
+
//#region src/handlers/climate-controller.ts
|
|
74616
|
+
var CAP_CLIMATE = "climate-control";
|
|
74617
|
+
var CAP_SWITCH$1 = "switch";
|
|
74618
|
+
var UNCERTAINTY_MS = 500;
|
|
74619
|
+
var PRESET_NONE = "none";
|
|
74620
|
+
var ScaleSchema = _enum([
|
|
74621
|
+
"CELSIUS",
|
|
74622
|
+
"FAHRENHEIT",
|
|
74623
|
+
"KELVIN"
|
|
74624
|
+
]);
|
|
74625
|
+
var TemperatureSchema = object({
|
|
74626
|
+
value: number(),
|
|
74627
|
+
scale: ScaleSchema
|
|
74628
|
+
});
|
|
74629
|
+
var SetTemperaturePayload = object({
|
|
74630
|
+
targetSetpoint: TemperatureSchema.optional(),
|
|
74631
|
+
lowerSetpoint: TemperatureSchema.optional(),
|
|
74632
|
+
upperSetpoint: TemperatureSchema.optional()
|
|
74633
|
+
});
|
|
74634
|
+
var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
|
|
74635
|
+
var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
|
|
74636
|
+
var ModePayload = object({ mode: string() });
|
|
74637
|
+
function isRefusal(outcome) {
|
|
74638
|
+
return "refusal" in outcome;
|
|
74639
|
+
}
|
|
74640
|
+
function toCelsius(t) {
|
|
74641
|
+
if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
|
|
74642
|
+
if (t.scale === "KELVIN") return t.value - 273.15;
|
|
74643
|
+
return t.value;
|
|
74644
|
+
}
|
|
74645
|
+
/** A DELTA converts without the scale's offset. */
|
|
74646
|
+
function deltaToCelsius(t) {
|
|
74647
|
+
return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
|
|
74648
|
+
}
|
|
74649
|
+
function withPayload(envelope, extra) {
|
|
74650
|
+
return { event: {
|
|
74651
|
+
...envelope.event,
|
|
74652
|
+
payload: {
|
|
74653
|
+
...envelope.event.payload,
|
|
74654
|
+
...extra
|
|
74655
|
+
}
|
|
74656
|
+
} };
|
|
74657
|
+
}
|
|
74658
|
+
function outOfRange$1(header, bounds) {
|
|
74659
|
+
return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
|
|
74660
|
+
minimumValue: temperatureValue(bounds.min),
|
|
74661
|
+
maximumValue: temperatureValue(bounds.max)
|
|
74662
|
+
} }) };
|
|
74663
|
+
}
|
|
74664
|
+
function tooClose(header, bounds) {
|
|
74665
|
+
return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
|
|
74666
|
+
}
|
|
74667
|
+
function refuse$1(header, type, message) {
|
|
74668
|
+
return { refusal: errorEnvelope(header, type, message) };
|
|
74669
|
+
}
|
|
74670
|
+
function inRange(celsius, bounds) {
|
|
74671
|
+
const v = round1(celsius);
|
|
74672
|
+
return v >= bounds.min && v <= bounds.max;
|
|
74673
|
+
}
|
|
74674
|
+
var THERMOSTAT_PROPERTIES = new Set([
|
|
74675
|
+
"targetSetpoint",
|
|
74676
|
+
"lowerSetpoint",
|
|
74677
|
+
"upperSetpoint",
|
|
74678
|
+
"thermostatMode"
|
|
74679
|
+
]);
|
|
74680
|
+
function thermostatProperty(property) {
|
|
74681
|
+
return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
|
|
74682
|
+
}
|
|
74683
|
+
function commandsFor(api, deviceId) {
|
|
74684
|
+
return {
|
|
74685
|
+
setMode: (mode) => api.climateControl.setMode.mutate({
|
|
74686
|
+
deviceId,
|
|
74687
|
+
mode
|
|
74688
|
+
}),
|
|
74689
|
+
setPreset: (preset) => api.climateControl.setPreset.mutate({
|
|
74690
|
+
deviceId,
|
|
74691
|
+
preset
|
|
74692
|
+
}),
|
|
74693
|
+
setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
|
|
74694
|
+
deviceId,
|
|
74695
|
+
fanMode
|
|
74696
|
+
}),
|
|
74697
|
+
setTarget: (target) => api.climateControl.setTarget.mutate({
|
|
74698
|
+
deviceId,
|
|
74699
|
+
target
|
|
74700
|
+
}),
|
|
74701
|
+
setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
|
|
74702
|
+
deviceId,
|
|
74703
|
+
targetLow,
|
|
74704
|
+
targetHigh
|
|
74705
|
+
})
|
|
74706
|
+
};
|
|
74707
|
+
}
|
|
74708
|
+
function planRange(header, slice, cmd, low, high) {
|
|
74709
|
+
const bounds = boundsOf(slice);
|
|
74710
|
+
if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
|
|
74711
|
+
if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
|
|
74712
|
+
const targetLow = snapToStep(low, bounds);
|
|
74713
|
+
const targetHigh = snapToStep(high, bounds);
|
|
74714
|
+
if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
|
|
74715
|
+
return {
|
|
74716
|
+
commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
|
|
74717
|
+
projected: {
|
|
74718
|
+
...slice,
|
|
74719
|
+
mode: "heat_cool",
|
|
74720
|
+
targetLow,
|
|
74721
|
+
targetHigh
|
|
74722
|
+
},
|
|
74723
|
+
keep: thermostatProperty
|
|
74724
|
+
};
|
|
74725
|
+
}
|
|
74726
|
+
function planSingle(header, slice, cmd, celsius) {
|
|
74727
|
+
const bounds = boundsOf(slice);
|
|
74728
|
+
if (!supportsSingle(slice) || slice.mode === "heat_cool") return refuse$1(header, "UNWILLING_TO_SET_VALUE", "the thermostat is in range mode: set a lower and an upper setpoint");
|
|
74729
|
+
if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
|
|
74730
|
+
const target = snapToStep(celsius, bounds);
|
|
74731
|
+
return {
|
|
74732
|
+
commands: [() => cmd.setTarget(target)],
|
|
74733
|
+
projected: {
|
|
74734
|
+
...slice,
|
|
74735
|
+
target
|
|
74736
|
+
},
|
|
74737
|
+
keep: thermostatProperty
|
|
74738
|
+
};
|
|
74739
|
+
}
|
|
74740
|
+
function planSetTemperature(c, slice, cmd) {
|
|
74741
|
+
const parsed = SetTemperaturePayload.safeParse(c.payload);
|
|
74742
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74743
|
+
const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
|
|
74744
|
+
if (targetSetpoint !== void 0 && lowerSetpoint !== void 0 && upperSetpoint !== void 0) return refuse$1(c.header, "TRIPLE_SETPOINTS_UNSUPPORTED", "a target AND a range were requested");
|
|
74745
|
+
if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
|
|
74746
|
+
if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
|
|
74747
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
|
|
74748
|
+
}
|
|
74749
|
+
function planAdjust(c, slice, cmd) {
|
|
74750
|
+
const parsed = AdjustTemperaturePayload.safeParse(c.payload);
|
|
74751
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74752
|
+
const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
|
|
74753
|
+
if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
|
|
74754
|
+
if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
|
|
74755
|
+
return planSingle(c.header, slice, cmd, slice.target + delta);
|
|
74756
|
+
}
|
|
74757
|
+
var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
|
|
74758
|
+
function hvacModeFor(requested, slice) {
|
|
74759
|
+
const has = (m) => slice.availableModes.includes(m);
|
|
74760
|
+
switch (requested) {
|
|
74761
|
+
case "HEAT": return has("heat") ? "heat" : null;
|
|
74762
|
+
case "COOL": return has("cool") ? "cool" : null;
|
|
74763
|
+
case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
|
|
74764
|
+
case "OFF": return has("off") ? "off" : null;
|
|
74765
|
+
default: return null;
|
|
74766
|
+
}
|
|
74767
|
+
}
|
|
74768
|
+
/** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
|
|
74769
|
+
function presetClearing(slice) {
|
|
74770
|
+
return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
|
|
74771
|
+
}
|
|
74772
|
+
function planEco(c, slice, cmd) {
|
|
74773
|
+
const eco = ecoPresetOf(slice);
|
|
74774
|
+
if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
|
|
74775
|
+
const wake = slice.mode === "off" ? onModeOf(slice) : null;
|
|
74776
|
+
if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
|
|
74777
|
+
return {
|
|
74778
|
+
commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
|
|
74779
|
+
projected: {
|
|
74780
|
+
...slice,
|
|
74781
|
+
mode: wake ?? slice.mode,
|
|
74782
|
+
preset: eco
|
|
74783
|
+
},
|
|
74784
|
+
keep: thermostatProperty
|
|
74785
|
+
};
|
|
74786
|
+
}
|
|
74787
|
+
function planSetThermostatMode(c, slice, cmd) {
|
|
74788
|
+
const parsed = ThermostatModePayload.safeParse(c.payload);
|
|
74789
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74790
|
+
const raw = parsed.data.thermostatMode;
|
|
74791
|
+
const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
|
|
74792
|
+
if (requested === "ECO") return planEco(c, slice, cmd);
|
|
74793
|
+
const mode = hvacModeFor(requested, slice);
|
|
74794
|
+
if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
|
|
74795
|
+
const eco = ecoPresetOf(slice);
|
|
74796
|
+
const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
|
|
74797
|
+
return {
|
|
74798
|
+
commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
|
|
74799
|
+
projected: {
|
|
74800
|
+
...slice,
|
|
74801
|
+
mode,
|
|
74802
|
+
preset: clear ?? slice.preset
|
|
74803
|
+
},
|
|
74804
|
+
keep: thermostatProperty
|
|
74805
|
+
};
|
|
74806
|
+
}
|
|
74807
|
+
function planPower(c, slice, cmd) {
|
|
74808
|
+
const turnOn = c.header.name === "TurnOn";
|
|
74809
|
+
const keep = (p) => p.namespace === "Alexa.PowerController";
|
|
74810
|
+
if (turnOn) {
|
|
74811
|
+
if (slice.mode !== "off") return {
|
|
74812
|
+
commands: [],
|
|
74813
|
+
projected: slice,
|
|
74814
|
+
keep
|
|
74815
|
+
};
|
|
74816
|
+
const mode = onModeOf(slice);
|
|
74817
|
+
if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
|
|
74818
|
+
return {
|
|
74819
|
+
commands: [() => cmd.setMode(mode)],
|
|
74820
|
+
projected: {
|
|
74821
|
+
...slice,
|
|
74822
|
+
mode
|
|
74823
|
+
},
|
|
74824
|
+
keep
|
|
74825
|
+
};
|
|
74826
|
+
}
|
|
74827
|
+
if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
|
|
74828
|
+
if (slice.mode === "off") return {
|
|
74829
|
+
commands: [],
|
|
74830
|
+
projected: slice,
|
|
74831
|
+
keep
|
|
74832
|
+
};
|
|
74833
|
+
return {
|
|
74834
|
+
commands: [() => cmd.setMode("off")],
|
|
74835
|
+
projected: {
|
|
74836
|
+
...slice,
|
|
74837
|
+
mode: "off"
|
|
74838
|
+
},
|
|
74839
|
+
keep
|
|
74840
|
+
};
|
|
74841
|
+
}
|
|
74842
|
+
function planModeController(c, slice, cmd) {
|
|
74843
|
+
const parsed = ModePayload.safeParse(c.payload);
|
|
74844
|
+
const instance = c.header.instance;
|
|
74845
|
+
if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
|
|
74846
|
+
const keep = (p) => p.instance === instance;
|
|
74847
|
+
if (instance === "Climate.Mode") {
|
|
74848
|
+
const mode = hvacModeForValue(parsed.data.mode, slice);
|
|
74849
|
+
if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
|
|
74850
|
+
return {
|
|
74851
|
+
commands: [() => cmd.setMode(mode)],
|
|
74852
|
+
projected: {
|
|
74853
|
+
...slice,
|
|
74854
|
+
mode
|
|
74855
|
+
},
|
|
74856
|
+
keep
|
|
74857
|
+
};
|
|
74858
|
+
}
|
|
74859
|
+
if (instance === "Climate.FanSpeed") {
|
|
74860
|
+
const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
|
|
74861
|
+
if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
|
|
74862
|
+
return {
|
|
74863
|
+
commands: [() => cmd.setFanMode(fan[1])],
|
|
74864
|
+
projected: {
|
|
74865
|
+
...slice,
|
|
74866
|
+
fanMode: fan[1]
|
|
74867
|
+
},
|
|
74868
|
+
keep
|
|
74869
|
+
};
|
|
74870
|
+
}
|
|
74871
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
|
|
74872
|
+
}
|
|
74873
|
+
function plan(c, slice, cmd) {
|
|
74874
|
+
switch (`${c.header.namespace}/${c.header.name}`) {
|
|
74875
|
+
case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
|
|
74876
|
+
case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
|
|
74877
|
+
case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
|
|
74878
|
+
case "Alexa.PowerController/TurnOn":
|
|
74879
|
+
case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
|
|
74880
|
+
case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
|
|
74881
|
+
default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
|
|
74882
|
+
}
|
|
74883
|
+
}
|
|
74884
|
+
async function readSlice$1(c, deps) {
|
|
74885
|
+
try {
|
|
74886
|
+
return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
|
|
74887
|
+
deviceId: c.deviceId,
|
|
74888
|
+
capName: CAP_CLIMATE
|
|
74889
|
+
}));
|
|
74890
|
+
} catch (err) {
|
|
74891
|
+
deps.logger.warn("alexa climate-controller: the climate state could not be read", {
|
|
74892
|
+
tags: { deviceId: c.deviceId },
|
|
74893
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
74894
|
+
});
|
|
74895
|
+
return null;
|
|
74896
|
+
}
|
|
74897
|
+
}
|
|
74898
|
+
function successResponse$1(c, properties) {
|
|
74899
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
74900
|
+
return {
|
|
74901
|
+
event: {
|
|
74902
|
+
header: {
|
|
74903
|
+
namespace: "Alexa",
|
|
74904
|
+
name: "Response",
|
|
74905
|
+
messageId: c.header.messageId,
|
|
74906
|
+
...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
|
|
74907
|
+
payloadVersion: "3"
|
|
74908
|
+
},
|
|
74909
|
+
endpoint: { endpointId: c.endpoint.endpointId },
|
|
74910
|
+
payload: {}
|
|
74911
|
+
},
|
|
74912
|
+
context: { properties: properties.map((p) => ({
|
|
74913
|
+
...p,
|
|
74914
|
+
timeOfSample: now,
|
|
74915
|
+
uncertaintyInMilliseconds: UNCERTAINTY_MS
|
|
74916
|
+
})) }
|
|
74917
|
+
};
|
|
74918
|
+
}
|
|
74919
|
+
/** The one handler behind every climate directive. */
|
|
74920
|
+
async function handleClimateDirective(c) {
|
|
74921
|
+
const deps = c.deps.climateController;
|
|
74922
|
+
const slice = await readSlice$1(c, deps);
|
|
74923
|
+
if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
|
|
74924
|
+
const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
|
|
74925
|
+
if (isRefusal(outcome)) return outcome.refusal;
|
|
74926
|
+
try {
|
|
74927
|
+
for (const command of outcome.commands) await command();
|
|
74928
|
+
} catch (err) {
|
|
74929
|
+
deps.logger.warn("alexa climate-controller: the climate command failed", {
|
|
74930
|
+
tags: { deviceId: c.deviceId },
|
|
74931
|
+
meta: {
|
|
74932
|
+
directive: c.header.name,
|
|
74933
|
+
error: err instanceof Error ? err.message : String(err)
|
|
74934
|
+
}
|
|
74935
|
+
});
|
|
74936
|
+
return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
|
|
74937
|
+
}
|
|
74938
|
+
deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
|
|
74939
|
+
tags: { deviceId: c.deviceId },
|
|
74940
|
+
meta: { commands: outcome.commands.length }
|
|
74941
|
+
});
|
|
74942
|
+
return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
|
|
74943
|
+
}
|
|
74944
|
+
/**
|
|
74945
|
+
* Last answer per device, per deps instance: a failed bindings read changes
|
|
74946
|
+
* nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
|
|
74947
|
+
* switch handler because one `getBindings` call failed.
|
|
74948
|
+
*/
|
|
74949
|
+
var lastPowerClaim = /* @__PURE__ */ new WeakMap();
|
|
74950
|
+
/** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
|
|
74951
|
+
async function handlesPower(c) {
|
|
74952
|
+
const deps = c.deps.climateController;
|
|
74953
|
+
const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
|
|
74954
|
+
lastPowerClaim.set(deps, known);
|
|
74955
|
+
try {
|
|
74956
|
+
const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
|
|
74957
|
+
const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
|
|
74958
|
+
known.set(c.deviceId, claim);
|
|
74959
|
+
return claim;
|
|
74960
|
+
} catch (err) {
|
|
74961
|
+
const previous = known.get(c.deviceId);
|
|
74962
|
+
deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
|
|
74963
|
+
tags: { deviceId: c.deviceId },
|
|
74964
|
+
meta: {
|
|
74965
|
+
error: err instanceof Error ? err.message : String(err),
|
|
74966
|
+
decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
|
|
74967
|
+
}
|
|
74968
|
+
});
|
|
74969
|
+
return previous ?? false;
|
|
74970
|
+
}
|
|
74971
|
+
}
|
|
74972
|
+
//#endregion
|
|
74030
74973
|
//#region src/endpoints/adapters/shared.ts
|
|
74031
74974
|
/** The base `Alexa` interface every endpoint declares. */
|
|
74032
74975
|
var BASE = {
|
|
@@ -74077,6 +75020,88 @@ function numericValueField(value) {
|
|
|
74077
75020
|
return typeof raw === "number" ? raw : null;
|
|
74078
75021
|
}
|
|
74079
75022
|
//#endregion
|
|
75023
|
+
//#region src/endpoints/adapters/temperature.ts
|
|
75024
|
+
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
75025
|
+
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
75026
|
+
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
75027
|
+
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
75028
|
+
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
75029
|
+
var temperatureAdapter = {
|
|
75030
|
+
cap: "temperature-sensor",
|
|
75031
|
+
stateCaps: ["temperature-sensor"],
|
|
75032
|
+
discoveryReads: [],
|
|
75033
|
+
describe: () => ({
|
|
75034
|
+
kind: "publish",
|
|
75035
|
+
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
75036
|
+
category: "TEMPERATURE_SENSOR"
|
|
75037
|
+
}),
|
|
75038
|
+
properties: (_cap, slice) => {
|
|
75039
|
+
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
75040
|
+
return p.success ? [{
|
|
75041
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75042
|
+
name: "temperature",
|
|
75043
|
+
value: {
|
|
75044
|
+
value: Math.round(p.data.celsius * 10) / 10,
|
|
75045
|
+
scale: "CELSIUS"
|
|
75046
|
+
}
|
|
75047
|
+
}] : [];
|
|
75048
|
+
},
|
|
75049
|
+
directives: [],
|
|
75050
|
+
shaping: () => ({
|
|
75051
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75052
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75053
|
+
numericOf: numericValueField
|
|
75054
|
+
})
|
|
75055
|
+
};
|
|
75056
|
+
//#endregion
|
|
75057
|
+
//#region src/endpoints/adapters/climate.ts
|
|
75058
|
+
var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
|
|
75059
|
+
var THERMOSTAT_DIRECTIVES = [
|
|
75060
|
+
"Alexa.ThermostatController/SetTargetTemperature",
|
|
75061
|
+
"Alexa.ThermostatController/AdjustTargetTemperature",
|
|
75062
|
+
"Alexa.ThermostatController/SetThermostatMode"
|
|
75063
|
+
].map((key) => ({
|
|
75064
|
+
key,
|
|
75065
|
+
needsReachableDevice: true,
|
|
75066
|
+
handle: handleClimateDirective
|
|
75067
|
+
}));
|
|
75068
|
+
var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
|
|
75069
|
+
key,
|
|
75070
|
+
needsReachableDevice: true,
|
|
75071
|
+
handle: handleClimateDirective,
|
|
75072
|
+
claims: handlesPower
|
|
75073
|
+
}));
|
|
75074
|
+
var MODE_DIRECTIVE = {
|
|
75075
|
+
key: "Alexa.ModeController/SetMode",
|
|
75076
|
+
needsReachableDevice: true,
|
|
75077
|
+
handle: handleClimateDirective,
|
|
75078
|
+
claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
|
|
75079
|
+
};
|
|
75080
|
+
/**
|
|
75081
|
+
* `climate-control` -> a thermostat (Home Assistant) or an air conditioner
|
|
75082
|
+
* (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
|
|
75083
|
+
* power, and the modes Amazon's vocabulary lacks and the fan mode as
|
|
75084
|
+
* ModeControllers. The current temperature is shaped like any temperature
|
|
75085
|
+
* sensor; setpoints, mode and fan report on every change.
|
|
75086
|
+
*/
|
|
75087
|
+
var climateAdapter = {
|
|
75088
|
+
cap: "climate-control",
|
|
75089
|
+
stateCaps: ["climate-control"],
|
|
75090
|
+
discoveryReads: ["climate-control"],
|
|
75091
|
+
describe: describeClimate,
|
|
75092
|
+
properties: (_cap, slice) => climateProperties(slice),
|
|
75093
|
+
directives: [
|
|
75094
|
+
...THERMOSTAT_DIRECTIVES,
|
|
75095
|
+
...POWER_DIRECTIVES$1,
|
|
75096
|
+
MODE_DIRECTIVE
|
|
75097
|
+
],
|
|
75098
|
+
shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
|
|
75099
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75100
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75101
|
+
numericOf: numericValueField
|
|
75102
|
+
} : null
|
|
75103
|
+
};
|
|
75104
|
+
//#endregion
|
|
74080
75105
|
//#region src/endpoints/adapters/contact.ts
|
|
74081
75106
|
/** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
|
|
74082
75107
|
var contactAdapter = {
|
|
@@ -74741,7 +75766,8 @@ var coverAdapter = {
|
|
|
74741
75766
|
].map((key) => ({
|
|
74742
75767
|
key,
|
|
74743
75768
|
needsReachableDevice: true,
|
|
74744
|
-
handle: handleCoverDirective
|
|
75769
|
+
handle: handleCoverDirective,
|
|
75770
|
+
claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
|
|
74745
75771
|
})),
|
|
74746
75772
|
shaping: (property) => property.namespace === "Alexa.RangeController" ? {
|
|
74747
75773
|
deadband: 1,
|
|
@@ -74749,40 +75775,6 @@ var coverAdapter = {
|
|
|
74749
75775
|
numericOf: plainNumber
|
|
74750
75776
|
} : { debounceMs: COVER_DEBOUNCE_MS }
|
|
74751
75777
|
};
|
|
74752
|
-
//#endregion
|
|
74753
|
-
//#region src/endpoints/adapters/temperature.ts
|
|
74754
|
-
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
74755
|
-
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
74756
|
-
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
74757
|
-
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
74758
|
-
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
74759
|
-
var temperatureAdapter = {
|
|
74760
|
-
cap: "temperature-sensor",
|
|
74761
|
-
stateCaps: ["temperature-sensor"],
|
|
74762
|
-
discoveryReads: [],
|
|
74763
|
-
describe: () => ({
|
|
74764
|
-
kind: "publish",
|
|
74765
|
-
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
74766
|
-
category: "TEMPERATURE_SENSOR"
|
|
74767
|
-
}),
|
|
74768
|
-
properties: (_cap, slice) => {
|
|
74769
|
-
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
74770
|
-
return p.success ? [{
|
|
74771
|
-
namespace: "Alexa.TemperatureSensor",
|
|
74772
|
-
name: "temperature",
|
|
74773
|
-
value: {
|
|
74774
|
-
value: Math.round(p.data.celsius * 10) / 10,
|
|
74775
|
-
scale: "CELSIUS"
|
|
74776
|
-
}
|
|
74777
|
-
}] : [];
|
|
74778
|
-
},
|
|
74779
|
-
directives: [],
|
|
74780
|
-
shaping: () => ({
|
|
74781
|
-
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
74782
|
-
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
74783
|
-
numericOf: numericValueField
|
|
74784
|
-
})
|
|
74785
|
-
};
|
|
74786
75778
|
/** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
|
|
74787
75779
|
var humidityAdapter = {
|
|
74788
75780
|
cap: "humidity-sensor",
|
|
@@ -75081,6 +76073,11 @@ var BRIGHTNESS_DIRECTIVE = {
|
|
|
75081
76073
|
needsReachableDevice: true,
|
|
75082
76074
|
handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
|
|
75083
76075
|
};
|
|
76076
|
+
/** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
|
|
76077
|
+
* smart plug; every other switch is a SWITCH. */
|
|
76078
|
+
function switchCategory(device) {
|
|
76079
|
+
return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
|
|
76080
|
+
}
|
|
75084
76081
|
//#endregion
|
|
75085
76082
|
//#region src/endpoints/adapters/index.ts
|
|
75086
76083
|
var GENERIC_ADAPTERS = [
|
|
@@ -75088,6 +76085,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75088
76085
|
contactAdapter,
|
|
75089
76086
|
motionAdapter,
|
|
75090
76087
|
presenceAdapter,
|
|
76088
|
+
climateAdapter,
|
|
75091
76089
|
temperatureAdapter,
|
|
75092
76090
|
humidityAdapter,
|
|
75093
76091
|
{
|
|
@@ -75099,7 +76097,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75099
76097
|
return {
|
|
75100
76098
|
kind: "publish",
|
|
75101
76099
|
capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
|
|
75102
|
-
category: dimmable || device.type === "light" ? "LIGHT" :
|
|
76100
|
+
category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
|
|
75103
76101
|
};
|
|
75104
76102
|
},
|
|
75105
76103
|
properties: (capName, slice) => {
|
|
@@ -75145,12 +76143,19 @@ function shapingFor(capName, property) {
|
|
|
75145
76143
|
function discoveryReadCaps(capNames) {
|
|
75146
76144
|
return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
|
|
75147
76145
|
}
|
|
75148
|
-
/**
|
|
75149
|
-
function
|
|
75150
|
-
|
|
75151
|
-
|
|
75152
|
-
|
|
75153
|
-
|
|
76146
|
+
/** Every directive an adapter declares for `Namespace/Name`, in registry order. */
|
|
76147
|
+
function genericDirectives(key) {
|
|
76148
|
+
return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
|
|
76149
|
+
}
|
|
76150
|
+
/**
|
|
76151
|
+
* The directive that serves this endpoint: of the adapters declaring the key,
|
|
76152
|
+
* in registry order, the first that claims it (two caps may answer one key -
|
|
76153
|
+
* power on a switch and on a thermostat). Null = nobody claims it.
|
|
76154
|
+
*/
|
|
76155
|
+
async function resolveGenericDirective(context) {
|
|
76156
|
+
const key = `${context.header.namespace}/${context.header.name}`;
|
|
76157
|
+
for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
|
|
76158
|
+
return null;
|
|
75154
76159
|
}
|
|
75155
76160
|
/** Every directive key the registry handles. */
|
|
75156
76161
|
var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
|
|
@@ -75192,9 +76197,9 @@ function planGenericDevice(device) {
|
|
|
75192
76197
|
category
|
|
75193
76198
|
};
|
|
75194
76199
|
}
|
|
75195
|
-
/** Device types this export may publish as a generic endpoint.
|
|
75196
|
-
*
|
|
75197
|
-
*
|
|
76200
|
+
/** Device types this export may publish as a generic endpoint. `thermostat`
|
|
76201
|
+
* (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
|
|
76202
|
+
* the `climate-control` cap (D874). */
|
|
75198
76203
|
var GENERIC_DEVICE_TYPES = [
|
|
75199
76204
|
"light",
|
|
75200
76205
|
"switch",
|
|
@@ -75203,11 +76208,13 @@ var GENERIC_DEVICE_TYPES = [
|
|
|
75203
76208
|
"lock",
|
|
75204
76209
|
"presence",
|
|
75205
76210
|
"thermostat",
|
|
76211
|
+
"climate",
|
|
75206
76212
|
"cover",
|
|
75207
76213
|
"generic"
|
|
75208
76214
|
];
|
|
75209
76215
|
function toEndpoint(input, plan) {
|
|
75210
|
-
const
|
|
76216
|
+
const override = input.alexaName?.trim() ?? "";
|
|
76217
|
+
const name = override.length > 0 ? override : input.name.trim();
|
|
75211
76218
|
return {
|
|
75212
76219
|
endpointId: `${input.deviceId}__dev`,
|
|
75213
76220
|
manufacturerName: "camstack",
|
|
@@ -75218,13 +76225,7 @@ function toEndpoint(input, plan) {
|
|
|
75218
76225
|
cookie: { stableId: input.stableId }
|
|
75219
76226
|
};
|
|
75220
76227
|
}
|
|
75221
|
-
/** Why a climate device is withheld until its adapter exists. */
|
|
75222
|
-
var THERMOSTAT_SKIP_REASON = "thermostat: Alexa climate control (Alexa.ThermostatController) has no adapter yet - not published until it does (D869, P3)";
|
|
75223
76228
|
function planGenericEndpoint(input) {
|
|
75224
|
-
if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
|
|
75225
|
-
kind: "skip",
|
|
75226
|
-
reasons: [THERMOSTAT_SKIP_REASON]
|
|
75227
|
-
};
|
|
75228
76229
|
const plan = planGenericDevice({
|
|
75229
76230
|
deviceId: input.deviceId,
|
|
75230
76231
|
type: input.type,
|
|
@@ -75240,6 +76241,74 @@ function planGenericEndpoint(input) {
|
|
|
75240
76241
|
return plan;
|
|
75241
76242
|
}
|
|
75242
76243
|
//#endregion
|
|
76244
|
+
//#region src/endpoints/alexa-name.ts
|
|
76245
|
+
/**
|
|
76246
|
+
* The per-device "Alexa name" (D875): what Alexa calls the device, when it
|
|
76247
|
+
* should not be the device's own name.
|
|
76248
|
+
*
|
|
76249
|
+
* Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
|
|
76250
|
+
* (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
|
|
76251
|
+
* id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
|
|
76252
|
+
* AddOrUpdateReport instead of creating a duplicate.
|
|
76253
|
+
*
|
|
76254
|
+
* Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
|
|
76255
|
+
* to 256 alphanumeric characters and spaces. Don't include special characters
|
|
76256
|
+
* or punctuation." Letters of any script count as alphanumeric, so an Italian
|
|
76257
|
+
* name keeps its accents. The validator rejects what Amazon would reject, with
|
|
76258
|
+
* a message the operator can act on.
|
|
76259
|
+
*/
|
|
76260
|
+
/** Per-device store key of the saved Alexa name. */
|
|
76261
|
+
var ALEXA_NAME_KEY = "alexaName";
|
|
76262
|
+
/** The device-settings form field key, namespaced per exporter (see the addon). */
|
|
76263
|
+
function alexaNameKey(deviceId) {
|
|
76264
|
+
return `alexa:${deviceId}:alexaName`;
|
|
76265
|
+
}
|
|
76266
|
+
/** Letters (any script, with their combining marks), digits and spaces. */
|
|
76267
|
+
var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
|
|
76268
|
+
/**
|
|
76269
|
+
* Validate an operator-typed Alexa name. Blank clears the override
|
|
76270
|
+
* (`name: null`, the device name is used).
|
|
76271
|
+
*/
|
|
76272
|
+
function validateAlexaName(raw) {
|
|
76273
|
+
if (raw === null || raw === void 0) return {
|
|
76274
|
+
ok: true,
|
|
76275
|
+
name: null
|
|
76276
|
+
};
|
|
76277
|
+
if (typeof raw !== "string") return {
|
|
76278
|
+
ok: false,
|
|
76279
|
+
error: "The Alexa name must be text."
|
|
76280
|
+
};
|
|
76281
|
+
const name = raw.trim().replace(/\s+/g, " ");
|
|
76282
|
+
if (name.length === 0) return {
|
|
76283
|
+
ok: true,
|
|
76284
|
+
name: null
|
|
76285
|
+
};
|
|
76286
|
+
if (name.length > 256) return {
|
|
76287
|
+
ok: false,
|
|
76288
|
+
error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
|
|
76289
|
+
};
|
|
76290
|
+
if (!ALLOWED_PATTERN.test(name)) return {
|
|
76291
|
+
ok: false,
|
|
76292
|
+
error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
|
|
76293
|
+
};
|
|
76294
|
+
return {
|
|
76295
|
+
ok: true,
|
|
76296
|
+
name
|
|
76297
|
+
};
|
|
76298
|
+
}
|
|
76299
|
+
var StoredNameSchema = string();
|
|
76300
|
+
/**
|
|
76301
|
+
* The saved Alexa name in a per-device store blob, or null (never saved, or a
|
|
76302
|
+
* stored value that no longer validates: the device name is used rather than
|
|
76303
|
+
* pushing a name Amazon would refuse).
|
|
76304
|
+
*/
|
|
76305
|
+
function readAlexaName(store) {
|
|
76306
|
+
const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
|
|
76307
|
+
if (!parsed.success) return null;
|
|
76308
|
+
const check = validateAlexaName(parsed.data);
|
|
76309
|
+
return check.ok ? check.name : null;
|
|
76310
|
+
}
|
|
76311
|
+
//#endregion
|
|
75243
76312
|
//#region src/handlers/security-panel-arm-state.ts
|
|
75244
76313
|
/**
|
|
75245
76314
|
* The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
|
|
@@ -75913,6 +76982,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75913
76982
|
deviceId: parentInfo.id,
|
|
75914
76983
|
stableId: parentInfo.stableId,
|
|
75915
76984
|
name: parentInfo.name,
|
|
76985
|
+
alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
|
|
75916
76986
|
type: String(device.type),
|
|
75917
76987
|
capNames: parentInfo.capNames,
|
|
75918
76988
|
role: parentInfo.role,
|
|
@@ -75971,11 +77041,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75971
77041
|
return [];
|
|
75972
77042
|
});
|
|
75973
77043
|
const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
|
|
77044
|
+
const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
|
|
75974
77045
|
const out = [];
|
|
75975
77046
|
out.push(buildParentCameraEndpoint({
|
|
75976
77047
|
deviceId: parentInfo.id,
|
|
75977
77048
|
stableId: parentInfo.stableId,
|
|
75978
|
-
friendlyName:
|
|
77049
|
+
friendlyName: cameraName,
|
|
75979
77050
|
isDoorbell,
|
|
75980
77051
|
hasIntercom,
|
|
75981
77052
|
hasMotion,
|
|
@@ -75989,7 +77060,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75989
77060
|
for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
|
|
75990
77061
|
deviceId: parentInfo.id,
|
|
75991
77062
|
stableId: parentInfo.stableId,
|
|
75992
|
-
cameraName
|
|
77063
|
+
cameraName,
|
|
75993
77064
|
detectionClass
|
|
75994
77065
|
}));
|
|
75995
77066
|
}
|
|
@@ -76483,6 +77554,36 @@ function validateInstanced(c, push) {
|
|
|
76483
77554
|
if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
|
|
76484
77555
|
}
|
|
76485
77556
|
//#endregion
|
|
77557
|
+
//#region src/endpoints/validate-thermostat.ts
|
|
77558
|
+
var PROPERTIES = new Set([
|
|
77559
|
+
"targetSetpoint",
|
|
77560
|
+
"lowerSetpoint",
|
|
77561
|
+
"upperSetpoint",
|
|
77562
|
+
"thermostatMode"
|
|
77563
|
+
]);
|
|
77564
|
+
var MODES = new Set([
|
|
77565
|
+
"AUTO",
|
|
77566
|
+
"COOL",
|
|
77567
|
+
"HEAT",
|
|
77568
|
+
"ECO",
|
|
77569
|
+
"OFF"
|
|
77570
|
+
]);
|
|
77571
|
+
function validateThermostat(c, push) {
|
|
77572
|
+
const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
|
|
77573
|
+
for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
|
|
77574
|
+
if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
|
|
77575
|
+
if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
|
|
77576
|
+
if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
|
|
77577
|
+
const modes = c.configuration?.["supportedModes"];
|
|
77578
|
+
if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
|
|
77579
|
+
else {
|
|
77580
|
+
for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
|
|
77581
|
+
if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
|
|
77582
|
+
}
|
|
77583
|
+
const scheduling = c.configuration?.["supportsScheduling"];
|
|
77584
|
+
if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
|
|
77585
|
+
}
|
|
77586
|
+
//#endregion
|
|
76486
77587
|
//#region src/endpoints/validate-endpoint.ts
|
|
76487
77588
|
/**
|
|
76488
77589
|
* Interfaces this skill may declare, with the version Amazon documents. An
|
|
@@ -76506,6 +77607,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
|
|
|
76506
77607
|
"Alexa.RangeController": "3",
|
|
76507
77608
|
"Alexa.SecurityPanelController": "3",
|
|
76508
77609
|
"Alexa.LockController": "3",
|
|
77610
|
+
"Alexa.ThermostatController": "3.1",
|
|
76509
77611
|
"Alexa.SmartVision.SnapshotProvider": "1.1",
|
|
76510
77612
|
"Alexa.SmartVision.ObjectDetectionSensor": "1.0",
|
|
76511
77613
|
"Alexa.DataController": "1.0"
|
|
@@ -76545,6 +77647,8 @@ var DISPLAY_CATEGORIES = new Set([
|
|
|
76545
77647
|
"SMARTLOCK",
|
|
76546
77648
|
"INTERIOR_BLIND",
|
|
76547
77649
|
"EXTERIOR_BLIND",
|
|
77650
|
+
"THERMOSTAT",
|
|
77651
|
+
"AIR_CONDITIONER",
|
|
76548
77652
|
"DOOR",
|
|
76549
77653
|
"GATE",
|
|
76550
77654
|
"GARAGE_DOOR",
|
|
@@ -76623,6 +77727,7 @@ function validateCapability(endpointId, c, out, declared) {
|
|
|
76623
77727
|
}
|
|
76624
77728
|
}
|
|
76625
77729
|
validateInstanced(c, push);
|
|
77730
|
+
if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
|
|
76626
77731
|
if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
|
|
76627
77732
|
if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
|
|
76628
77733
|
if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
|
|
@@ -78553,20 +79658,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
|
|
|
78553
79658
|
status: 200,
|
|
78554
79659
|
body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
|
|
78555
79660
|
};
|
|
78556
|
-
|
|
78557
|
-
if (adapterDirective !== void 0 && endpoint !== void 0) {
|
|
79661
|
+
if (genericDirectives(key).length > 0 && endpoint !== void 0) {
|
|
78558
79662
|
const target = parseCameraEndpointId(endpoint.endpointId);
|
|
78559
|
-
if (target.kind === "generic")
|
|
78560
|
-
|
|
78561
|
-
body: await adapterDirective.handle({
|
|
79663
|
+
if (target.kind === "generic") {
|
|
79664
|
+
const directiveContext = {
|
|
78562
79665
|
header,
|
|
78563
79666
|
endpoint,
|
|
78564
79667
|
payload,
|
|
78565
79668
|
deviceId: target.deviceId,
|
|
78566
79669
|
deps,
|
|
78567
79670
|
identity: callerIdentity
|
|
78568
|
-
}
|
|
78569
|
-
|
|
79671
|
+
};
|
|
79672
|
+
const adapterDirective = await resolveGenericDirective(directiveContext);
|
|
79673
|
+
if (adapterDirective === null) return {
|
|
79674
|
+
status: 200,
|
|
79675
|
+
body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
|
|
79676
|
+
};
|
|
79677
|
+
return {
|
|
79678
|
+
status: 200,
|
|
79679
|
+
body: await adapterDirective.handle(directiveContext)
|
|
79680
|
+
};
|
|
79681
|
+
}
|
|
78570
79682
|
}
|
|
78571
79683
|
if (key === "Alexa.ModeController/SetMode") return {
|
|
78572
79684
|
status: 200,
|
|
@@ -82709,6 +83821,7 @@ var SUPPORTED_KINDS = [
|
|
|
82709
83821
|
DeviceType.Lock,
|
|
82710
83822
|
DeviceType.Presence,
|
|
82711
83823
|
DeviceType.Thermostat,
|
|
83824
|
+
DeviceType.Climate,
|
|
82712
83825
|
DeviceType.Cover,
|
|
82713
83826
|
DeviceType.Generic
|
|
82714
83827
|
];
|
|
@@ -82847,6 +83960,10 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
82847
83960
|
ctx: this.ctx,
|
|
82848
83961
|
logger: this.ctx.logger
|
|
82849
83962
|
},
|
|
83963
|
+
climateController: {
|
|
83964
|
+
ctx: this.ctx,
|
|
83965
|
+
logger: this.ctx.logger
|
|
83966
|
+
},
|
|
82850
83967
|
coverController: {
|
|
82851
83968
|
ctx: this.ctx,
|
|
82852
83969
|
logger: this.ctx.logger,
|
|
@@ -83103,6 +84220,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83103
84220
|
logger: this.ctx.logger,
|
|
83104
84221
|
snapshots: this.config.snapshots,
|
|
83105
84222
|
...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
|
|
84223
|
+
alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
|
|
83106
84224
|
...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
|
|
83107
84225
|
});
|
|
83108
84226
|
}
|
|
@@ -83670,12 +84788,28 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83670
84788
|
value: enabled,
|
|
83671
84789
|
immediate: true
|
|
83672
84790
|
},
|
|
84791
|
+
await this.alexaNameField(deviceId, enabledKey),
|
|
83673
84792
|
...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
|
|
83674
84793
|
...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
|
|
83675
84794
|
]
|
|
83676
84795
|
}]
|
|
83677
84796
|
};
|
|
83678
84797
|
}
|
|
84798
|
+
/** D875: the "Alexa name" field, with the validation error of the saved value. */
|
|
84799
|
+
async alexaNameField(deviceId, enabledKey) {
|
|
84800
|
+
const saved = readAlexaName(await this.vision.readPrefs(deviceId));
|
|
84801
|
+
return {
|
|
84802
|
+
type: "text",
|
|
84803
|
+
key: alexaNameKey(deviceId),
|
|
84804
|
+
label: "Alexa name",
|
|
84805
|
+
description: `What Alexa calls this device. Leave empty to use the device name. Letters, digits and spaces only (no punctuation), up to 256 characters. Renaming updates the same Alexa device; it does not create a duplicate.`,
|
|
84806
|
+
value: saved ?? "",
|
|
84807
|
+
showWhen: {
|
|
84808
|
+
field: enabledKey,
|
|
84809
|
+
equals: true
|
|
84810
|
+
}
|
|
84811
|
+
};
|
|
84812
|
+
}
|
|
83679
84813
|
/**
|
|
83680
84814
|
* The camera's detection fields: which classes become Alexa sensors (D778;
|
|
83681
84815
|
* only the classes its pipeline produces are offered) and its object
|
|
@@ -83697,6 +84831,24 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83697
84831
|
})];
|
|
83698
84832
|
}
|
|
83699
84833
|
/**
|
|
84834
|
+
* D875: save the Alexa name from a device-settings patch. A name Amazon would
|
|
84835
|
+
* refuse throws BEFORE anything is written, so the form shows the reason and
|
|
84836
|
+
* the old name stays. True when the saved name changed.
|
|
84837
|
+
*/
|
|
84838
|
+
async applyAlexaNamePatch(deviceId, patch) {
|
|
84839
|
+
const key = alexaNameKey(deviceId);
|
|
84840
|
+
if (!(key in patch)) return false;
|
|
84841
|
+
const check = validateAlexaName(patch[key]);
|
|
84842
|
+
if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
|
|
84843
|
+
if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
|
|
84844
|
+
await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
|
|
84845
|
+
this.ctx.logger.info("export-alexa: Alexa name saved", {
|
|
84846
|
+
tags: { deviceId },
|
|
84847
|
+
meta: { name: check.name }
|
|
84848
|
+
});
|
|
84849
|
+
return true;
|
|
84850
|
+
}
|
|
84851
|
+
/**
|
|
83700
84852
|
* Persist the camera's detection preferences from a device-settings patch
|
|
83701
84853
|
* into this addon's per-device store. True when anything changed — the
|
|
83702
84854
|
* camera's Alexa shape then needs a reconcile.
|
|
@@ -83761,6 +84913,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83761
84913
|
const current = this.findEntry(deviceId);
|
|
83762
84914
|
const wasEnabled = current !== null;
|
|
83763
84915
|
const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
|
|
84916
|
+
const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
|
|
83764
84917
|
const enabledKey = `alexa:${deviceId}:enabled`;
|
|
83765
84918
|
const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
|
|
83766
84919
|
const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
|
|
@@ -83786,6 +84939,12 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83786
84939
|
const currentAllow = current?.settings?.allowTranscode !== false;
|
|
83787
84940
|
await this.patchEntrySettings(deviceId, nextSettings);
|
|
83788
84941
|
const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
|
|
84942
|
+
if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
|
|
84943
|
+
this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
|
|
84944
|
+
tags: { deviceId },
|
|
84945
|
+
meta: { error: errMsg(err) }
|
|
84946
|
+
});
|
|
84947
|
+
});
|
|
83789
84948
|
if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
|
|
83790
84949
|
this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
|
|
83791
84950
|
tags: { deviceId },
|