@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
|
@@ -5393,7 +5393,7 @@ var ZodIssueCode = {
|
|
|
5393
5393
|
var ZodFirstPartyTypeKind;
|
|
5394
5394
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5395
5395
|
//#endregion
|
|
5396
|
-
//#region ../types/dist/sleep-
|
|
5396
|
+
//#region ../types/dist/sleep-bwsTj221.mjs
|
|
5397
5397
|
/**
|
|
5398
5398
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5399
5399
|
* window to float samples (D455).
|
|
@@ -6644,7 +6644,6 @@ function createHandle(raw, opts) {
|
|
|
6644
6644
|
const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
|
|
6645
6645
|
const readAddonStoreResult = raw.readAddonStoreResult;
|
|
6646
6646
|
const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
|
|
6647
|
-
const readDeviceStoreBatch = raw.readDeviceStoreBatch;
|
|
6648
6647
|
const overrides = {
|
|
6649
6648
|
readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
|
|
6650
6649
|
...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
|
|
@@ -6668,12 +6667,12 @@ function createHandle(raw, opts) {
|
|
|
6668
6667
|
};
|
|
6669
6668
|
} } : {},
|
|
6670
6669
|
readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
|
|
6671
|
-
|
|
6672
|
-
const rows = await readDeviceStoreBatch
|
|
6670
|
+
readDeviceStoreBatch: async (deviceIds) => {
|
|
6671
|
+
const rows = await raw.readDeviceStoreBatch(deviceIds);
|
|
6673
6672
|
const out = /* @__PURE__ */ new Map();
|
|
6674
6673
|
for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
|
|
6675
6674
|
return out;
|
|
6676
|
-
}
|
|
6675
|
+
},
|
|
6677
6676
|
writeAddonStore: async (patch) => {
|
|
6678
6677
|
await writeAddon(patch);
|
|
6679
6678
|
},
|
|
@@ -7500,7 +7499,7 @@ var BaseAddon = class {
|
|
|
7500
7499
|
return this._addonSecretTree;
|
|
7501
7500
|
}
|
|
7502
7501
|
deviceSecretTree() {
|
|
7503
|
-
this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
|
|
7502
|
+
this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
|
|
7504
7503
|
return this._deviceSecretTree;
|
|
7505
7504
|
}
|
|
7506
7505
|
/**
|
|
@@ -7576,42 +7575,135 @@ var BaseAddon = class {
|
|
|
7576
7575
|
this[timerOf] = timer;
|
|
7577
7576
|
}
|
|
7578
7577
|
/**
|
|
7579
|
-
* Wrap `ctx.settings.readAddonStore()` with a
|
|
7580
|
-
*
|
|
7581
|
-
* swap, or any race where the SqliteSettingsBackend is between shutdown and
|
|
7582
|
-
* re-initialize) doesn't propagate up into `initialize()` and leave the
|
|
7578
|
+
* Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
|
|
7579
|
+
* settings-store outage doesn't propagate up into `initialize()` and leave the
|
|
7583
7580
|
* addon permanently broken.
|
|
7584
7581
|
*
|
|
7585
|
-
*
|
|
7586
|
-
*
|
|
7587
|
-
* `
|
|
7582
|
+
* Two kinds of outage, two budgets:
|
|
7583
|
+
* - the store's own restart fingerprints (`SqliteSettingsBackend not
|
|
7584
|
+
* initialized`, `provider not available`): a short budget, then the addon
|
|
7585
|
+
* falls back to `{}` (defaults) - its first successful patch rehydrates;
|
|
7586
|
+
* - a TRANSPORT failure (the hub is restarting while this runner boots:
|
|
7587
|
+
* `discovery deadline exceeded`, `parent could not route`, a refused or
|
|
7588
|
+
* timed-out connection). Since D870 a runner's settings read THROWS instead
|
|
7589
|
+
* of answering `{}`, so without a budget a hub restart ends in a respawn
|
|
7590
|
+
* loop. A long, bounded budget; only after it the error propagates - the
|
|
7591
|
+
* addon never runs on defaults for a store it could not ask.
|
|
7592
|
+
* Anything else propagates immediately so real bugs surface.
|
|
7588
7593
|
*/
|
|
7589
7594
|
async readAddonStoreWithRetry() {
|
|
7590
7595
|
const settings = this.rawSettings();
|
|
7591
7596
|
if (!settings) return {};
|
|
7592
|
-
const delaysMs = [
|
|
7593
|
-
150,
|
|
7594
|
-
350,
|
|
7595
|
-
600,
|
|
7596
|
-
900
|
|
7597
|
-
];
|
|
7598
7597
|
let lastErr;
|
|
7599
|
-
for (let attempt = 0
|
|
7598
|
+
for (let attempt = 0;; attempt++) try {
|
|
7600
7599
|
const stored = await settings.readAddonStore() ?? {};
|
|
7601
7600
|
this.settingsStoreReady = true;
|
|
7602
7601
|
return stored;
|
|
7603
7602
|
} catch (err) {
|
|
7604
7603
|
lastErr = err;
|
|
7605
|
-
const
|
|
7606
|
-
if (
|
|
7607
|
-
|
|
7608
|
-
|
|
7604
|
+
const kind = classifyStoreError(err);
|
|
7605
|
+
if (kind === "permanent") throw err;
|
|
7606
|
+
const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
|
|
7607
|
+
if (delay === void 0) {
|
|
7608
|
+
if (kind === "transport") throw err;
|
|
7609
|
+
break;
|
|
7610
|
+
}
|
|
7611
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
7609
7612
|
}
|
|
7610
7613
|
this.settingsStoreReady = false;
|
|
7611
7614
|
this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
|
|
7612
7615
|
return {};
|
|
7613
7616
|
}
|
|
7614
7617
|
};
|
|
7618
|
+
/** The settings store's own restart fingerprints. */
|
|
7619
|
+
var RESTART_RETRY_DELAYS_MS = [
|
|
7620
|
+
150,
|
|
7621
|
+
350,
|
|
7622
|
+
600,
|
|
7623
|
+
900
|
|
7624
|
+
];
|
|
7625
|
+
/** A hub restarting under a booting runner: ~100 s in total. */
|
|
7626
|
+
var TRANSPORT_RETRY_DELAYS_MS = [
|
|
7627
|
+
150,
|
|
7628
|
+
350,
|
|
7629
|
+
600,
|
|
7630
|
+
900,
|
|
7631
|
+
2e3,
|
|
7632
|
+
5e3,
|
|
7633
|
+
1e4,
|
|
7634
|
+
2e4,
|
|
7635
|
+
3e4,
|
|
7636
|
+
3e4
|
|
7637
|
+
];
|
|
7638
|
+
var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
|
|
7639
|
+
/** Node / socket error codes of a connection that did not happen or broke. */
|
|
7640
|
+
var TRANSPORT_CODES = new Set([
|
|
7641
|
+
"ECONNREFUSED",
|
|
7642
|
+
"ECONNRESET",
|
|
7643
|
+
"ETIMEDOUT",
|
|
7644
|
+
"EPIPE",
|
|
7645
|
+
"EHOSTUNREACH",
|
|
7646
|
+
"ENETUNREACH"
|
|
7647
|
+
]);
|
|
7648
|
+
/** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
|
|
7649
|
+
var UNAVAILABLE_TRPC_CODES = new Set([
|
|
7650
|
+
"TIMEOUT",
|
|
7651
|
+
"SERVICE_UNAVAILABLE",
|
|
7652
|
+
"BAD_GATEWAY",
|
|
7653
|
+
"GATEWAY_TIMEOUT"
|
|
7654
|
+
]);
|
|
7655
|
+
/** The exact phrases OUR transport raises when it cannot reach the hub. */
|
|
7656
|
+
var TRANSPORT_PHRASES = [
|
|
7657
|
+
"discovery deadline exceeded",
|
|
7658
|
+
"parent could not route child unowned cap call",
|
|
7659
|
+
"UDS request timed out after",
|
|
7660
|
+
"channel closed"
|
|
7661
|
+
];
|
|
7662
|
+
/** Moleculer error `type`s of a call that could not be served right now. */
|
|
7663
|
+
var MOLECULER_TRANSIENT_TYPES = new Set([
|
|
7664
|
+
"REQUEST_TIMEOUT",
|
|
7665
|
+
"SERVICE_NOT_FOUND",
|
|
7666
|
+
"SERVICE_NOT_AVAILABLE",
|
|
7667
|
+
"REQUEST_REJECTED",
|
|
7668
|
+
"BROKER_DISCONNECTED"
|
|
7669
|
+
]);
|
|
7670
|
+
function stringProp(value, key) {
|
|
7671
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7672
|
+
const found = Reflect.get(value, key);
|
|
7673
|
+
return typeof found === "string" ? found : void 0;
|
|
7674
|
+
}
|
|
7675
|
+
/** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
|
|
7676
|
+
function errorCode(err) {
|
|
7677
|
+
const out = {};
|
|
7678
|
+
const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
|
|
7679
|
+
const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
|
|
7680
|
+
const node = stringProp(err, "code") ?? stringProp(cause, "code");
|
|
7681
|
+
if (node !== void 0) out.node = node;
|
|
7682
|
+
const trpc = stringProp(data, "code");
|
|
7683
|
+
if (trpc !== void 0) out.trpc = trpc;
|
|
7684
|
+
return out;
|
|
7685
|
+
}
|
|
7686
|
+
/**
|
|
7687
|
+
* Is a settings read failure worth retrying, and with which budget? Only
|
|
7688
|
+
* EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
|
|
7689
|
+
* one of the exact phrases our own transport raises. A message that merely
|
|
7690
|
+
* contains "timed out" or "unreachable" is not evidence of anything, and
|
|
7691
|
+
* `settings store not wired` is a wiring bug that retrying cannot fix.
|
|
7692
|
+
*/
|
|
7693
|
+
function classifyStoreError(err) {
|
|
7694
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7695
|
+
if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
|
|
7696
|
+
const code = errorCode(err);
|
|
7697
|
+
if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
|
|
7698
|
+
if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
|
|
7699
|
+
if (typeof err === "object" && err !== null) {
|
|
7700
|
+
if (Reflect.get(err, "retryable") === true) return "transport";
|
|
7701
|
+
const type = stringProp(err, "type");
|
|
7702
|
+
if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
|
|
7703
|
+
}
|
|
7704
|
+
if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
|
|
7705
|
+
return "permanent";
|
|
7706
|
+
}
|
|
7615
7707
|
/**
|
|
7616
7708
|
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
7617
7709
|
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
@@ -8188,7 +8280,7 @@ object({
|
|
|
8188
8280
|
params: record(string(), union([string(), number()])).optional()
|
|
8189
8281
|
});
|
|
8190
8282
|
//#endregion
|
|
8191
|
-
//#region ../types/dist/err-msg
|
|
8283
|
+
//#region ../types/dist/err-msg--2RrYiLO.mjs
|
|
8192
8284
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
8193
8285
|
DeviceType["Camera"] = "camera";
|
|
8194
8286
|
DeviceType["Hub"] = "hub";
|
|
@@ -8457,6 +8549,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
8457
8549
|
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
8458
8550
|
* real Switch device for UI rendering / export adapters. */
|
|
8459
8551
|
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
8552
|
+
/** A smart plug: a `switch` whose source says it is an outlet (HA
|
|
8553
|
+
* `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
|
|
8554
|
+
DeviceRole["Outlet"] = "outlet";
|
|
8460
8555
|
/** Generic motion / occupancy / moving event source. Distinct from
|
|
8461
8556
|
* the camera accessory PirSensor role: that one is a camera child;
|
|
8462
8557
|
* this is a standalone HA / 3rd-party motion sensor. */
|
|
@@ -8534,7 +8629,7 @@ function errMsg(err) {
|
|
|
8534
8629
|
return String(err);
|
|
8535
8630
|
}
|
|
8536
8631
|
//#endregion
|
|
8537
|
-
//#region ../types/dist/composition-
|
|
8632
|
+
//#region ../types/dist/composition-DOwQB_Ua.mjs
|
|
8538
8633
|
/**
|
|
8539
8634
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
8540
8635
|
*
|
|
@@ -11995,11 +12090,10 @@ var ClipModelMetaSchema = object({
|
|
|
11995
12090
|
/**
|
|
11996
12091
|
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
11997
12092
|
* per index: two models share an index only when their vectors are ranked
|
|
11998
|
-
* against each other, which for CLIP means never across families.
|
|
11999
|
-
*
|
|
12000
|
-
* `modelId`); any other model gets its own.
|
|
12093
|
+
* against each other, which for CLIP means never across families. Every
|
|
12094
|
+
* model gets its own.
|
|
12001
12095
|
*/
|
|
12002
|
-
vectorIndex: string().regex(/^object-clip
|
|
12096
|
+
vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
|
|
12003
12097
|
/**
|
|
12004
12098
|
* The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
|
|
12005
12099
|
* must be unique per tokenizer: every model's siblings land in ONE flat
|
|
@@ -16140,6 +16234,28 @@ id: union([string(), number()]) }), {
|
|
|
16140
16234
|
kind: "mutation",
|
|
16141
16235
|
auth: "admin"
|
|
16142
16236
|
});
|
|
16237
|
+
/** One table of the settings database. */
|
|
16238
|
+
var StorageTableSchema = object({
|
|
16239
|
+
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
16240
|
+
name: string(),
|
|
16241
|
+
/** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
|
|
16242
|
+
owner: string().nullable(),
|
|
16243
|
+
rows: number().int().nonnegative(),
|
|
16244
|
+
/** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
|
|
16245
|
+
bytes: number().int().nonnegative().nullable(),
|
|
16246
|
+
/** Declared by an addon since this engine process started (a running addon claims it). */
|
|
16247
|
+
declaredThisBoot: boolean()
|
|
16248
|
+
});
|
|
16249
|
+
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
16250
|
+
collection: string().min(1),
|
|
16251
|
+
expectRows: number().int().nonnegative()
|
|
16252
|
+
}), object({
|
|
16253
|
+
dropped: literal(true),
|
|
16254
|
+
rows: number().int().nonnegative()
|
|
16255
|
+
}), {
|
|
16256
|
+
kind: "mutation",
|
|
16257
|
+
auth: "admin"
|
|
16258
|
+
});
|
|
16143
16259
|
/**
|
|
16144
16260
|
* `keychain` — the hub's encrypted store of secret values (D787).
|
|
16145
16261
|
*
|
|
@@ -16681,6 +16797,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
|
|
|
16681
16797
|
kind: "mutation",
|
|
16682
16798
|
auth: "admin",
|
|
16683
16799
|
caller: "required"
|
|
16800
|
+
}), method(object({
|
|
16801
|
+
ref: string().min(1),
|
|
16802
|
+
value: string().min(1)
|
|
16803
|
+
}), object({
|
|
16804
|
+
ref: string(),
|
|
16805
|
+
version: number().int()
|
|
16806
|
+
}), {
|
|
16807
|
+
kind: "mutation",
|
|
16808
|
+
auth: "admin",
|
|
16809
|
+
caller: "required"
|
|
16684
16810
|
}), method(object({ refs: array(string().min(1)).max(1e3) }), object({
|
|
16685
16811
|
deleted: array(string()).readonly(),
|
|
16686
16812
|
kept: array(object({
|
|
@@ -53138,7 +53264,7 @@ var HvacModeSchema = _enum([
|
|
|
53138
53264
|
"fan_only",
|
|
53139
53265
|
"dry"
|
|
53140
53266
|
]);
|
|
53141
|
-
object({
|
|
53267
|
+
var ClimateControlStatusSchema = object({
|
|
53142
53268
|
/** Active HVAC mode. */
|
|
53143
53269
|
mode: HvacModeSchema,
|
|
53144
53270
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -53179,6 +53305,12 @@ object({
|
|
|
53179
53305
|
/** Horizontal louver swing on/off. Null when the device has no
|
|
53180
53306
|
* horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
|
|
53181
53307
|
swingHorizontal: boolean().nullable(),
|
|
53308
|
+
/** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53309
|
+
minTemp: number().nullable().optional(),
|
|
53310
|
+
/** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53311
|
+
maxTemp: number().nullable().optional(),
|
|
53312
|
+
/** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
|
|
53313
|
+
tempStep: number().positive().nullable().optional(),
|
|
53182
53314
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
53183
53315
|
lastFetchedAt: number()
|
|
53184
53316
|
});
|
|
@@ -55019,7 +55151,19 @@ var TlsStatusSchema = object({
|
|
|
55019
55151
|
sans: array(string()),
|
|
55020
55152
|
caCertPem: string().nullable(),
|
|
55021
55153
|
reissueError: string().nullable(),
|
|
55022
|
-
restartRequired: boolean()
|
|
55154
|
+
restartRequired: boolean(),
|
|
55155
|
+
/** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
|
|
55156
|
+
caMissing: boolean(),
|
|
55157
|
+
/** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
|
|
55158
|
+
localCaConstrained: boolean().nullable(),
|
|
55159
|
+
/** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
|
|
55160
|
+
caReplacedAt: string().nullable(),
|
|
55161
|
+
/** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
|
|
55162
|
+
storedUpload: object({
|
|
55163
|
+
leafFingerprintSha256: string(),
|
|
55164
|
+
validTo: string(),
|
|
55165
|
+
hasCa: boolean()
|
|
55166
|
+
}).nullable()
|
|
55023
55167
|
});
|
|
55024
55168
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
55025
55169
|
/**
|
|
@@ -55075,6 +55219,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
55075
55219
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
55076
55220
|
kind: "mutation",
|
|
55077
55221
|
auth: "admin"
|
|
55222
|
+
}), method(_void(), TlsStatusSchema, {
|
|
55223
|
+
kind: "mutation",
|
|
55224
|
+
auth: "admin"
|
|
55078
55225
|
});
|
|
55079
55226
|
var LockControlStatusSchema = object({
|
|
55080
55227
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
@@ -64114,6 +64261,12 @@ Object.freeze({
|
|
|
64114
64261
|
addonId: null,
|
|
64115
64262
|
access: "create"
|
|
64116
64263
|
},
|
|
64264
|
+
"keychainAdmin.importSecret": {
|
|
64265
|
+
capName: "keychain-admin",
|
|
64266
|
+
capScope: "system",
|
|
64267
|
+
addonId: null,
|
|
64268
|
+
access: "create"
|
|
64269
|
+
},
|
|
64117
64270
|
"keychainAdmin.listAudit": {
|
|
64118
64271
|
capName: "keychain-admin",
|
|
64119
64272
|
capScope: "system",
|
|
@@ -64510,6 +64663,12 @@ Object.freeze({
|
|
|
64510
64663
|
addonId: null,
|
|
64511
64664
|
access: "create"
|
|
64512
64665
|
},
|
|
64666
|
+
"localNetwork.useStoredUploadedCertificate": {
|
|
64667
|
+
capName: "local-network",
|
|
64668
|
+
capScope: "system",
|
|
64669
|
+
addonId: null,
|
|
64670
|
+
access: "create"
|
|
64671
|
+
},
|
|
64513
64672
|
"lockControl.getStatus": {
|
|
64514
64673
|
capName: "lock-control",
|
|
64515
64674
|
capScope: "device",
|
|
@@ -67894,6 +68053,18 @@ Object.freeze({
|
|
|
67894
68053
|
addonId: null,
|
|
67895
68054
|
access: "create"
|
|
67896
68055
|
},
|
|
68056
|
+
"storageAdmin.dropCollection": {
|
|
68057
|
+
capName: "storage-admin",
|
|
68058
|
+
capScope: "system",
|
|
68059
|
+
addonId: null,
|
|
68060
|
+
access: "delete"
|
|
68061
|
+
},
|
|
68062
|
+
"storageAdmin.listTables": {
|
|
68063
|
+
capName: "storage-admin",
|
|
68064
|
+
capScope: "system",
|
|
68065
|
+
addonId: null,
|
|
68066
|
+
access: "view"
|
|
68067
|
+
},
|
|
67897
68068
|
"storageEvictable.evict": {
|
|
67898
68069
|
capName: "storage-evictable",
|
|
67899
68070
|
capScope: "system",
|
|
@@ -73998,6 +74169,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
|
|
|
73998
74169
|
await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
|
|
73999
74170
|
}
|
|
74000
74171
|
//#endregion
|
|
74172
|
+
//#region src/endpoints/adapters/climate-model.ts
|
|
74173
|
+
/**
|
|
74174
|
+
* What a `climate-control` device is to Alexa (D874) — pure, so the discovery
|
|
74175
|
+
* shape, the slice mapping and the directive handlers share one definition.
|
|
74176
|
+
*
|
|
74177
|
+
* `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
|
|
74178
|
+
* upperSetpoint (a device with a range),
|
|
74179
|
+
* thermostatMode HEAT / COOL / AUTO / ECO /
|
|
74180
|
+
* OFF
|
|
74181
|
+
* `Alexa.TemperatureSensor` the current temperature
|
|
74182
|
+
* `Alexa.PowerController` when the device can be turned off
|
|
74183
|
+
* `Alexa.ModeController` Climate.Mode only when the device has a mode
|
|
74184
|
+
* Amazon's thermostat vocabulary lacks
|
|
74185
|
+
* (dry, fan_only)
|
|
74186
|
+
* `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
|
|
74187
|
+
*
|
|
74188
|
+
* Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
|
|
74189
|
+
* mode is not a documented way to carry them: they travel through the
|
|
74190
|
+
* `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
|
|
74191
|
+
* while the device is in one of them.
|
|
74192
|
+
*
|
|
74193
|
+
* Temperatures are Celsius throughout (the cap's contract); Alexa converts for
|
|
74194
|
+
* the account.
|
|
74195
|
+
*/
|
|
74196
|
+
var INSTANCE_MODE = "Climate.Mode";
|
|
74197
|
+
var INSTANCE_FAN = "Climate.FanSpeed";
|
|
74198
|
+
var FAN_PREFIX = "Fan.";
|
|
74199
|
+
var DEFAULT_TEMP_STEP = .5;
|
|
74200
|
+
var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
|
|
74201
|
+
var ALEXA_ECO_PRESET = "eco";
|
|
74202
|
+
var SINGLE_SETPOINT_MODES = [
|
|
74203
|
+
"heat",
|
|
74204
|
+
"cool",
|
|
74205
|
+
"auto",
|
|
74206
|
+
"dry"
|
|
74207
|
+
];
|
|
74208
|
+
/** What TurnOn switches to, best first. */
|
|
74209
|
+
var TURN_ON_PREFERENCE = [
|
|
74210
|
+
"auto",
|
|
74211
|
+
"heat_cool",
|
|
74212
|
+
"cool",
|
|
74213
|
+
"heat"
|
|
74214
|
+
];
|
|
74215
|
+
var SliceShape$1 = ClimateControlStatusSchema.pick({
|
|
74216
|
+
mode: true,
|
|
74217
|
+
availableModes: true,
|
|
74218
|
+
fanMode: true,
|
|
74219
|
+
availableFanModes: true,
|
|
74220
|
+
preset: true,
|
|
74221
|
+
availablePresets: true,
|
|
74222
|
+
target: true,
|
|
74223
|
+
targetHigh: true,
|
|
74224
|
+
targetLow: true,
|
|
74225
|
+
currentTemp: true,
|
|
74226
|
+
minTemp: true,
|
|
74227
|
+
maxTemp: true,
|
|
74228
|
+
tempStep: true
|
|
74229
|
+
});
|
|
74230
|
+
/** The slice fields the adapter reads, or null when the slice is not a climate one. */
|
|
74231
|
+
function readClimateSlice(slice) {
|
|
74232
|
+
const parsed = SliceShape$1.safeParse(slice);
|
|
74233
|
+
return parsed.success ? parsed.data : null;
|
|
74234
|
+
}
|
|
74235
|
+
function boundsOf(slice) {
|
|
74236
|
+
const min = slice.minTemp ?? 7;
|
|
74237
|
+
const max = slice.maxTemp ?? 35;
|
|
74238
|
+
const step = slice.tempStep ?? .5;
|
|
74239
|
+
return min < max ? {
|
|
74240
|
+
min,
|
|
74241
|
+
max,
|
|
74242
|
+
step
|
|
74243
|
+
} : {
|
|
74244
|
+
min: 7,
|
|
74245
|
+
max: 35,
|
|
74246
|
+
step: DEFAULT_TEMP_STEP
|
|
74247
|
+
};
|
|
74248
|
+
}
|
|
74249
|
+
/** One decimal, the precision Alexa's own conversions keep. */
|
|
74250
|
+
function round1(value) {
|
|
74251
|
+
return Math.round(value * 10) / 10;
|
|
74252
|
+
}
|
|
74253
|
+
/**
|
|
74254
|
+
* Snap a setpoint to the device's step (anchored at its minimum), then clamp:
|
|
74255
|
+
* a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
|
|
74256
|
+
*/
|
|
74257
|
+
function snapToStep(value, bounds) {
|
|
74258
|
+
const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
|
|
74259
|
+
return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
|
|
74260
|
+
}
|
|
74261
|
+
function temperatureValue(celsius) {
|
|
74262
|
+
return {
|
|
74263
|
+
value: round1(celsius),
|
|
74264
|
+
scale: "CELSIUS"
|
|
74265
|
+
};
|
|
74266
|
+
}
|
|
74267
|
+
function hasMode(slice, mode) {
|
|
74268
|
+
return slice !== null && slice.availableModes.includes(mode);
|
|
74269
|
+
}
|
|
74270
|
+
/** Dual setpoints (a heat/cool range). */
|
|
74271
|
+
function supportsDual(slice, features) {
|
|
74272
|
+
return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
|
|
74273
|
+
}
|
|
74274
|
+
/** A single setpoint (everything but a range-only device). */
|
|
74275
|
+
function supportsSingle(slice) {
|
|
74276
|
+
return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
|
|
74277
|
+
}
|
|
74278
|
+
/** The device's own name for its `eco` preset, if it has one. */
|
|
74279
|
+
function ecoPresetOf(slice) {
|
|
74280
|
+
return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
|
|
74281
|
+
}
|
|
74282
|
+
/** Amazon thermostat modes the device can be put in, in a stable order. */
|
|
74283
|
+
function supportedThermostatModes(slice) {
|
|
74284
|
+
if (slice === null || slice.availableModes.length === 0) return [
|
|
74285
|
+
"HEAT",
|
|
74286
|
+
"COOL",
|
|
74287
|
+
"AUTO",
|
|
74288
|
+
"OFF"
|
|
74289
|
+
];
|
|
74290
|
+
const out = [];
|
|
74291
|
+
if (hasMode(slice, "heat")) out.push("HEAT");
|
|
74292
|
+
if (hasMode(slice, "cool")) out.push("COOL");
|
|
74293
|
+
if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
|
|
74294
|
+
if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
|
|
74295
|
+
if (hasMode(slice, "off")) out.push("OFF");
|
|
74296
|
+
return out;
|
|
74297
|
+
}
|
|
74298
|
+
/** The HVAC mode TurnOn / leaving OFF switches to. */
|
|
74299
|
+
function onModeOf(slice) {
|
|
74300
|
+
return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
|
|
74301
|
+
}
|
|
74302
|
+
function canPowerOff(slice) {
|
|
74303
|
+
return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
|
|
74304
|
+
}
|
|
74305
|
+
/** A device with a mode Amazon's thermostat vocabulary lacks. */
|
|
74306
|
+
function hasExtraModes(slice) {
|
|
74307
|
+
return hasMode(slice, "dry") || hasMode(slice, "fan_only");
|
|
74308
|
+
}
|
|
74309
|
+
var MODE_VALUE = {
|
|
74310
|
+
heat: "Mode.Heat",
|
|
74311
|
+
cool: "Mode.Cool",
|
|
74312
|
+
auto: "Mode.Auto",
|
|
74313
|
+
dry: "Mode.Dry",
|
|
74314
|
+
fan_only: "Mode.Fan",
|
|
74315
|
+
off: "Mode.Off"
|
|
74316
|
+
};
|
|
74317
|
+
var MODE_NAMES = {
|
|
74318
|
+
"Mode.Heat": ["Riscaldamento", "Heating"],
|
|
74319
|
+
"Mode.Cool": ["Raffrescamento", "Cooling"],
|
|
74320
|
+
"Mode.Auto": ["Automatico", "Auto"],
|
|
74321
|
+
"Mode.Dry": ["Deumidificazione", "Dry"],
|
|
74322
|
+
"Mode.Fan": ["Ventilazione", "Fan only"],
|
|
74323
|
+
"Mode.Off": ["Spento", "Off"]
|
|
74324
|
+
};
|
|
74325
|
+
/** The ModeController value a HVAC mode reports as (a range is "auto"). */
|
|
74326
|
+
function modeValueOf(mode) {
|
|
74327
|
+
return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
|
|
74328
|
+
}
|
|
74329
|
+
/** The HVAC mode a ModeController value asks for, among those the device has. */
|
|
74330
|
+
function hvacModeForValue(value, slice) {
|
|
74331
|
+
if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
|
|
74332
|
+
const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
|
|
74333
|
+
if (mode === void 0) return null;
|
|
74334
|
+
const candidate = SliceShape$1.shape.mode.safeParse(mode);
|
|
74335
|
+
return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
|
|
74336
|
+
}
|
|
74337
|
+
/** The ModeController values the device offers, in a stable order. */
|
|
74338
|
+
function modeValuesOf(slice) {
|
|
74339
|
+
return [
|
|
74340
|
+
"heat",
|
|
74341
|
+
"cool",
|
|
74342
|
+
"auto",
|
|
74343
|
+
"dry",
|
|
74344
|
+
"fan_only",
|
|
74345
|
+
"off"
|
|
74346
|
+
].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
|
|
74347
|
+
}
|
|
74348
|
+
function textNames(it, en) {
|
|
74349
|
+
return [{
|
|
74350
|
+
"@type": "text",
|
|
74351
|
+
value: {
|
|
74352
|
+
text: it,
|
|
74353
|
+
locale: "it-IT"
|
|
74354
|
+
}
|
|
74355
|
+
}, {
|
|
74356
|
+
"@type": "text",
|
|
74357
|
+
value: {
|
|
74358
|
+
text: en,
|
|
74359
|
+
locale: "en-US"
|
|
74360
|
+
}
|
|
74361
|
+
}];
|
|
74362
|
+
}
|
|
74363
|
+
function modeCapability(slice) {
|
|
74364
|
+
const values = modeValuesOf(slice);
|
|
74365
|
+
if (values.length < 2) return null;
|
|
74366
|
+
return {
|
|
74367
|
+
type: "AlexaInterface",
|
|
74368
|
+
interface: "Alexa.ModeController",
|
|
74369
|
+
version: "3",
|
|
74370
|
+
instance: INSTANCE_MODE,
|
|
74371
|
+
capabilityResources: { friendlyNames: [{
|
|
74372
|
+
"@type": "asset",
|
|
74373
|
+
value: { assetId: "Alexa.Setting.Mode" }
|
|
74374
|
+
}, ...textNames("Modalità", "Mode")] },
|
|
74375
|
+
properties: {
|
|
74376
|
+
supported: [{ name: "mode" }],
|
|
74377
|
+
proactivelyReported: true,
|
|
74378
|
+
retrievable: true
|
|
74379
|
+
},
|
|
74380
|
+
configuration: {
|
|
74381
|
+
ordered: false,
|
|
74382
|
+
supportedModes: values.map((value) => {
|
|
74383
|
+
const [it, en] = MODE_NAMES[value] ?? [value, value];
|
|
74384
|
+
return {
|
|
74385
|
+
value,
|
|
74386
|
+
modeResources: { friendlyNames: textNames(it, en) }
|
|
74387
|
+
};
|
|
74388
|
+
})
|
|
74389
|
+
}
|
|
74390
|
+
};
|
|
74391
|
+
}
|
|
74392
|
+
var FAN_ASSETS = {
|
|
74393
|
+
low: "Alexa.Value.Low",
|
|
74394
|
+
medium: "Alexa.Value.Medium",
|
|
74395
|
+
high: "Alexa.Value.High"
|
|
74396
|
+
};
|
|
74397
|
+
var FAN_TEXTS = {
|
|
74398
|
+
auto: ["Automatico", "Auto"],
|
|
74399
|
+
quiet: ["Silenzioso", "Quiet"],
|
|
74400
|
+
turbo: ["Turbo", "Turbo"],
|
|
74401
|
+
on: ["Acceso", "On"],
|
|
74402
|
+
off: ["Spento", "Off"],
|
|
74403
|
+
middle: ["Medio", "Medium"],
|
|
74404
|
+
mediumlow: ["Medio basso", "Medium low"],
|
|
74405
|
+
mediumhigh: ["Medio alto", "Medium high"],
|
|
74406
|
+
medium_low: ["Medio basso", "Medium low"],
|
|
74407
|
+
medium_high: ["Medio alto", "Medium high"]
|
|
74408
|
+
};
|
|
74409
|
+
function capitalize(word) {
|
|
74410
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
74411
|
+
}
|
|
74412
|
+
/** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
|
|
74413
|
+
function fanValueOf(fanMode) {
|
|
74414
|
+
const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
|
|
74415
|
+
return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
|
|
74416
|
+
}
|
|
74417
|
+
/** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
|
|
74418
|
+
function fanModesOf(slice) {
|
|
74419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74420
|
+
const out = [];
|
|
74421
|
+
for (const fanMode of slice.availableFanModes) {
|
|
74422
|
+
const value = fanValueOf(fanMode);
|
|
74423
|
+
if (value === null || seen.has(value)) continue;
|
|
74424
|
+
seen.add(value);
|
|
74425
|
+
out.push([value, fanMode]);
|
|
74426
|
+
}
|
|
74427
|
+
return out;
|
|
74428
|
+
}
|
|
74429
|
+
function hasFanModes(slice) {
|
|
74430
|
+
return slice !== null && fanModesOf(slice).length >= 2;
|
|
74431
|
+
}
|
|
74432
|
+
function fanFriendlyNames(fanMode) {
|
|
74433
|
+
const key = fanMode.toLowerCase();
|
|
74434
|
+
const asset = FAN_ASSETS[key];
|
|
74435
|
+
if (asset !== void 0) return [{
|
|
74436
|
+
"@type": "asset",
|
|
74437
|
+
value: { assetId: asset }
|
|
74438
|
+
}];
|
|
74439
|
+
const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
|
|
74440
|
+
return textNames(it, en);
|
|
74441
|
+
}
|
|
74442
|
+
function fanCapability(slice) {
|
|
74443
|
+
const modes = fanModesOf(slice);
|
|
74444
|
+
if (modes.length < 2) return null;
|
|
74445
|
+
return {
|
|
74446
|
+
type: "AlexaInterface",
|
|
74447
|
+
interface: "Alexa.ModeController",
|
|
74448
|
+
version: "3",
|
|
74449
|
+
instance: INSTANCE_FAN,
|
|
74450
|
+
capabilityResources: { friendlyNames: [{
|
|
74451
|
+
"@type": "asset",
|
|
74452
|
+
value: { assetId: "Alexa.Setting.FanSpeed" }
|
|
74453
|
+
}, ...textNames("Velocità ventola", "Fan speed")] },
|
|
74454
|
+
properties: {
|
|
74455
|
+
supported: [{ name: "mode" }],
|
|
74456
|
+
proactivelyReported: true,
|
|
74457
|
+
retrievable: true
|
|
74458
|
+
},
|
|
74459
|
+
configuration: {
|
|
74460
|
+
ordered: false,
|
|
74461
|
+
supportedModes: modes.map(([value, fanMode]) => ({
|
|
74462
|
+
value,
|
|
74463
|
+
modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
|
|
74464
|
+
}))
|
|
74465
|
+
}
|
|
74466
|
+
};
|
|
74467
|
+
}
|
|
74468
|
+
function propertiesBlock(names) {
|
|
74469
|
+
return {
|
|
74470
|
+
supported: names.map((name) => ({ name })),
|
|
74471
|
+
proactivelyReported: true,
|
|
74472
|
+
retrievable: true
|
|
74473
|
+
};
|
|
74474
|
+
}
|
|
74475
|
+
function thermostatCapability(slice, modes, features) {
|
|
74476
|
+
return {
|
|
74477
|
+
type: "AlexaInterface",
|
|
74478
|
+
interface: "Alexa.ThermostatController",
|
|
74479
|
+
version: "3.1",
|
|
74480
|
+
properties: propertiesBlock([
|
|
74481
|
+
...supportsSingle(slice) ? ["targetSetpoint"] : [],
|
|
74482
|
+
...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
|
|
74483
|
+
"thermostatMode"
|
|
74484
|
+
]),
|
|
74485
|
+
configuration: {
|
|
74486
|
+
supportedModes: modes,
|
|
74487
|
+
supportsScheduling: false
|
|
74488
|
+
}
|
|
74489
|
+
};
|
|
74490
|
+
}
|
|
74491
|
+
/** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
|
|
74492
|
+
function climateCategoryFor(deviceType) {
|
|
74493
|
+
return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
|
|
74494
|
+
}
|
|
74495
|
+
function describeClimate(device) {
|
|
74496
|
+
const slice = readClimateSlice(device.slices["climate-control"]);
|
|
74497
|
+
const modes = supportedThermostatModes(slice);
|
|
74498
|
+
if (modes.length === 0) return {
|
|
74499
|
+
kind: "skip",
|
|
74500
|
+
reason: "climate: the device offers no mode Alexa has a thermostat value for"
|
|
74501
|
+
};
|
|
74502
|
+
const extras = [];
|
|
74503
|
+
if (slice !== null) {
|
|
74504
|
+
const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
|
|
74505
|
+
const fan = hasFanModes(slice) ? fanCapability(slice) : null;
|
|
74506
|
+
if (mode !== null) extras.push(mode);
|
|
74507
|
+
if (fan !== null) extras.push(fan);
|
|
74508
|
+
}
|
|
74509
|
+
return {
|
|
74510
|
+
kind: "publish",
|
|
74511
|
+
capabilities: [
|
|
74512
|
+
thermostatCapability(slice, modes, device.features),
|
|
74513
|
+
{
|
|
74514
|
+
type: "AlexaInterface",
|
|
74515
|
+
interface: "Alexa.TemperatureSensor",
|
|
74516
|
+
version: "3",
|
|
74517
|
+
properties: propertiesBlock(["temperature"])
|
|
74518
|
+
},
|
|
74519
|
+
...canPowerOff(slice) ? [{
|
|
74520
|
+
type: "AlexaInterface",
|
|
74521
|
+
interface: "Alexa.PowerController",
|
|
74522
|
+
version: "3",
|
|
74523
|
+
properties: propertiesBlock(["powerState"])
|
|
74524
|
+
}] : [],
|
|
74525
|
+
...extras
|
|
74526
|
+
],
|
|
74527
|
+
category: climateCategoryFor(device.type)
|
|
74528
|
+
};
|
|
74529
|
+
}
|
|
74530
|
+
/** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
|
|
74531
|
+
function thermostatModeOf(slice) {
|
|
74532
|
+
if (slice.mode === "off") return "OFF";
|
|
74533
|
+
const eco = ecoPresetOf(slice);
|
|
74534
|
+
if (eco !== null && slice.preset === eco) return "ECO";
|
|
74535
|
+
switch (slice.mode) {
|
|
74536
|
+
case "heat": return "HEAT";
|
|
74537
|
+
case "cool": return "COOL";
|
|
74538
|
+
case "auto":
|
|
74539
|
+
case "heat_cool": return "AUTO";
|
|
74540
|
+
default: return null;
|
|
74541
|
+
}
|
|
74542
|
+
}
|
|
74543
|
+
function climateProperties(slice) {
|
|
74544
|
+
const s = readClimateSlice(slice);
|
|
74545
|
+
if (s === null) return [];
|
|
74546
|
+
const out = [];
|
|
74547
|
+
const thermostat = (name, value) => {
|
|
74548
|
+
out.push({
|
|
74549
|
+
namespace: "Alexa.ThermostatController",
|
|
74550
|
+
name,
|
|
74551
|
+
value
|
|
74552
|
+
});
|
|
74553
|
+
};
|
|
74554
|
+
if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
|
|
74555
|
+
thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
|
|
74556
|
+
thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
|
|
74557
|
+
} else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
|
|
74558
|
+
const mode = thermostatModeOf(s);
|
|
74559
|
+
if (mode !== null) thermostat("thermostatMode", mode);
|
|
74560
|
+
if (s.currentTemp !== null) out.push({
|
|
74561
|
+
namespace: "Alexa.TemperatureSensor",
|
|
74562
|
+
name: "temperature",
|
|
74563
|
+
value: temperatureValue(s.currentTemp)
|
|
74564
|
+
});
|
|
74565
|
+
if (canPowerOff(s)) out.push({
|
|
74566
|
+
namespace: "Alexa.PowerController",
|
|
74567
|
+
name: "powerState",
|
|
74568
|
+
value: s.mode === "off" ? "OFF" : "ON"
|
|
74569
|
+
});
|
|
74570
|
+
if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
|
|
74571
|
+
namespace: "Alexa.ModeController",
|
|
74572
|
+
instance: INSTANCE_MODE,
|
|
74573
|
+
name: "mode",
|
|
74574
|
+
value: modeValueOf(s.mode)
|
|
74575
|
+
});
|
|
74576
|
+
const fan = fanValueOf(s.fanMode);
|
|
74577
|
+
if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
|
|
74578
|
+
namespace: "Alexa.ModeController",
|
|
74579
|
+
instance: INSTANCE_FAN,
|
|
74580
|
+
name: "mode",
|
|
74581
|
+
value: fan
|
|
74582
|
+
});
|
|
74583
|
+
return out;
|
|
74584
|
+
}
|
|
74585
|
+
//#endregion
|
|
74586
|
+
//#region src/handlers/climate-controller.ts
|
|
74587
|
+
var CAP_CLIMATE = "climate-control";
|
|
74588
|
+
var CAP_SWITCH$1 = "switch";
|
|
74589
|
+
var UNCERTAINTY_MS = 500;
|
|
74590
|
+
var PRESET_NONE = "none";
|
|
74591
|
+
var ScaleSchema = _enum([
|
|
74592
|
+
"CELSIUS",
|
|
74593
|
+
"FAHRENHEIT",
|
|
74594
|
+
"KELVIN"
|
|
74595
|
+
]);
|
|
74596
|
+
var TemperatureSchema = object({
|
|
74597
|
+
value: number(),
|
|
74598
|
+
scale: ScaleSchema
|
|
74599
|
+
});
|
|
74600
|
+
var SetTemperaturePayload = object({
|
|
74601
|
+
targetSetpoint: TemperatureSchema.optional(),
|
|
74602
|
+
lowerSetpoint: TemperatureSchema.optional(),
|
|
74603
|
+
upperSetpoint: TemperatureSchema.optional()
|
|
74604
|
+
});
|
|
74605
|
+
var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
|
|
74606
|
+
var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
|
|
74607
|
+
var ModePayload = object({ mode: string() });
|
|
74608
|
+
function isRefusal(outcome) {
|
|
74609
|
+
return "refusal" in outcome;
|
|
74610
|
+
}
|
|
74611
|
+
function toCelsius(t) {
|
|
74612
|
+
if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
|
|
74613
|
+
if (t.scale === "KELVIN") return t.value - 273.15;
|
|
74614
|
+
return t.value;
|
|
74615
|
+
}
|
|
74616
|
+
/** A DELTA converts without the scale's offset. */
|
|
74617
|
+
function deltaToCelsius(t) {
|
|
74618
|
+
return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
|
|
74619
|
+
}
|
|
74620
|
+
function withPayload(envelope, extra) {
|
|
74621
|
+
return { event: {
|
|
74622
|
+
...envelope.event,
|
|
74623
|
+
payload: {
|
|
74624
|
+
...envelope.event.payload,
|
|
74625
|
+
...extra
|
|
74626
|
+
}
|
|
74627
|
+
} };
|
|
74628
|
+
}
|
|
74629
|
+
function outOfRange$1(header, bounds) {
|
|
74630
|
+
return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
|
|
74631
|
+
minimumValue: temperatureValue(bounds.min),
|
|
74632
|
+
maximumValue: temperatureValue(bounds.max)
|
|
74633
|
+
} }) };
|
|
74634
|
+
}
|
|
74635
|
+
function tooClose(header, bounds) {
|
|
74636
|
+
return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
|
|
74637
|
+
}
|
|
74638
|
+
function refuse$1(header, type, message) {
|
|
74639
|
+
return { refusal: errorEnvelope(header, type, message) };
|
|
74640
|
+
}
|
|
74641
|
+
function inRange(celsius, bounds) {
|
|
74642
|
+
const v = round1(celsius);
|
|
74643
|
+
return v >= bounds.min && v <= bounds.max;
|
|
74644
|
+
}
|
|
74645
|
+
var THERMOSTAT_PROPERTIES = new Set([
|
|
74646
|
+
"targetSetpoint",
|
|
74647
|
+
"lowerSetpoint",
|
|
74648
|
+
"upperSetpoint",
|
|
74649
|
+
"thermostatMode"
|
|
74650
|
+
]);
|
|
74651
|
+
function thermostatProperty(property) {
|
|
74652
|
+
return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
|
|
74653
|
+
}
|
|
74654
|
+
function commandsFor(api, deviceId) {
|
|
74655
|
+
return {
|
|
74656
|
+
setMode: (mode) => api.climateControl.setMode.mutate({
|
|
74657
|
+
deviceId,
|
|
74658
|
+
mode
|
|
74659
|
+
}),
|
|
74660
|
+
setPreset: (preset) => api.climateControl.setPreset.mutate({
|
|
74661
|
+
deviceId,
|
|
74662
|
+
preset
|
|
74663
|
+
}),
|
|
74664
|
+
setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
|
|
74665
|
+
deviceId,
|
|
74666
|
+
fanMode
|
|
74667
|
+
}),
|
|
74668
|
+
setTarget: (target) => api.climateControl.setTarget.mutate({
|
|
74669
|
+
deviceId,
|
|
74670
|
+
target
|
|
74671
|
+
}),
|
|
74672
|
+
setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
|
|
74673
|
+
deviceId,
|
|
74674
|
+
targetLow,
|
|
74675
|
+
targetHigh
|
|
74676
|
+
})
|
|
74677
|
+
};
|
|
74678
|
+
}
|
|
74679
|
+
function planRange(header, slice, cmd, low, high) {
|
|
74680
|
+
const bounds = boundsOf(slice);
|
|
74681
|
+
if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
|
|
74682
|
+
if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
|
|
74683
|
+
const targetLow = snapToStep(low, bounds);
|
|
74684
|
+
const targetHigh = snapToStep(high, bounds);
|
|
74685
|
+
if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
|
|
74686
|
+
return {
|
|
74687
|
+
commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
|
|
74688
|
+
projected: {
|
|
74689
|
+
...slice,
|
|
74690
|
+
mode: "heat_cool",
|
|
74691
|
+
targetLow,
|
|
74692
|
+
targetHigh
|
|
74693
|
+
},
|
|
74694
|
+
keep: thermostatProperty
|
|
74695
|
+
};
|
|
74696
|
+
}
|
|
74697
|
+
function planSingle(header, slice, cmd, celsius) {
|
|
74698
|
+
const bounds = boundsOf(slice);
|
|
74699
|
+
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");
|
|
74700
|
+
if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
|
|
74701
|
+
const target = snapToStep(celsius, bounds);
|
|
74702
|
+
return {
|
|
74703
|
+
commands: [() => cmd.setTarget(target)],
|
|
74704
|
+
projected: {
|
|
74705
|
+
...slice,
|
|
74706
|
+
target
|
|
74707
|
+
},
|
|
74708
|
+
keep: thermostatProperty
|
|
74709
|
+
};
|
|
74710
|
+
}
|
|
74711
|
+
function planSetTemperature(c, slice, cmd) {
|
|
74712
|
+
const parsed = SetTemperaturePayload.safeParse(c.payload);
|
|
74713
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74714
|
+
const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
|
|
74715
|
+
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");
|
|
74716
|
+
if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
|
|
74717
|
+
if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
|
|
74718
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
|
|
74719
|
+
}
|
|
74720
|
+
function planAdjust(c, slice, cmd) {
|
|
74721
|
+
const parsed = AdjustTemperaturePayload.safeParse(c.payload);
|
|
74722
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74723
|
+
const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
|
|
74724
|
+
if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
|
|
74725
|
+
if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
|
|
74726
|
+
return planSingle(c.header, slice, cmd, slice.target + delta);
|
|
74727
|
+
}
|
|
74728
|
+
var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
|
|
74729
|
+
function hvacModeFor(requested, slice) {
|
|
74730
|
+
const has = (m) => slice.availableModes.includes(m);
|
|
74731
|
+
switch (requested) {
|
|
74732
|
+
case "HEAT": return has("heat") ? "heat" : null;
|
|
74733
|
+
case "COOL": return has("cool") ? "cool" : null;
|
|
74734
|
+
case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
|
|
74735
|
+
case "OFF": return has("off") ? "off" : null;
|
|
74736
|
+
default: return null;
|
|
74737
|
+
}
|
|
74738
|
+
}
|
|
74739
|
+
/** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
|
|
74740
|
+
function presetClearing(slice) {
|
|
74741
|
+
return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
|
|
74742
|
+
}
|
|
74743
|
+
function planEco(c, slice, cmd) {
|
|
74744
|
+
const eco = ecoPresetOf(slice);
|
|
74745
|
+
if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
|
|
74746
|
+
const wake = slice.mode === "off" ? onModeOf(slice) : null;
|
|
74747
|
+
if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
|
|
74748
|
+
return {
|
|
74749
|
+
commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
|
|
74750
|
+
projected: {
|
|
74751
|
+
...slice,
|
|
74752
|
+
mode: wake ?? slice.mode,
|
|
74753
|
+
preset: eco
|
|
74754
|
+
},
|
|
74755
|
+
keep: thermostatProperty
|
|
74756
|
+
};
|
|
74757
|
+
}
|
|
74758
|
+
function planSetThermostatMode(c, slice, cmd) {
|
|
74759
|
+
const parsed = ThermostatModePayload.safeParse(c.payload);
|
|
74760
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
74761
|
+
const raw = parsed.data.thermostatMode;
|
|
74762
|
+
const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
|
|
74763
|
+
if (requested === "ECO") return planEco(c, slice, cmd);
|
|
74764
|
+
const mode = hvacModeFor(requested, slice);
|
|
74765
|
+
if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
|
|
74766
|
+
const eco = ecoPresetOf(slice);
|
|
74767
|
+
const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
|
|
74768
|
+
return {
|
|
74769
|
+
commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
|
|
74770
|
+
projected: {
|
|
74771
|
+
...slice,
|
|
74772
|
+
mode,
|
|
74773
|
+
preset: clear ?? slice.preset
|
|
74774
|
+
},
|
|
74775
|
+
keep: thermostatProperty
|
|
74776
|
+
};
|
|
74777
|
+
}
|
|
74778
|
+
function planPower(c, slice, cmd) {
|
|
74779
|
+
const turnOn = c.header.name === "TurnOn";
|
|
74780
|
+
const keep = (p) => p.namespace === "Alexa.PowerController";
|
|
74781
|
+
if (turnOn) {
|
|
74782
|
+
if (slice.mode !== "off") return {
|
|
74783
|
+
commands: [],
|
|
74784
|
+
projected: slice,
|
|
74785
|
+
keep
|
|
74786
|
+
};
|
|
74787
|
+
const mode = onModeOf(slice);
|
|
74788
|
+
if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
|
|
74789
|
+
return {
|
|
74790
|
+
commands: [() => cmd.setMode(mode)],
|
|
74791
|
+
projected: {
|
|
74792
|
+
...slice,
|
|
74793
|
+
mode
|
|
74794
|
+
},
|
|
74795
|
+
keep
|
|
74796
|
+
};
|
|
74797
|
+
}
|
|
74798
|
+
if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
|
|
74799
|
+
if (slice.mode === "off") return {
|
|
74800
|
+
commands: [],
|
|
74801
|
+
projected: slice,
|
|
74802
|
+
keep
|
|
74803
|
+
};
|
|
74804
|
+
return {
|
|
74805
|
+
commands: [() => cmd.setMode("off")],
|
|
74806
|
+
projected: {
|
|
74807
|
+
...slice,
|
|
74808
|
+
mode: "off"
|
|
74809
|
+
},
|
|
74810
|
+
keep
|
|
74811
|
+
};
|
|
74812
|
+
}
|
|
74813
|
+
function planModeController(c, slice, cmd) {
|
|
74814
|
+
const parsed = ModePayload.safeParse(c.payload);
|
|
74815
|
+
const instance = c.header.instance;
|
|
74816
|
+
if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
|
|
74817
|
+
const keep = (p) => p.instance === instance;
|
|
74818
|
+
if (instance === "Climate.Mode") {
|
|
74819
|
+
const mode = hvacModeForValue(parsed.data.mode, slice);
|
|
74820
|
+
if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
|
|
74821
|
+
return {
|
|
74822
|
+
commands: [() => cmd.setMode(mode)],
|
|
74823
|
+
projected: {
|
|
74824
|
+
...slice,
|
|
74825
|
+
mode
|
|
74826
|
+
},
|
|
74827
|
+
keep
|
|
74828
|
+
};
|
|
74829
|
+
}
|
|
74830
|
+
if (instance === "Climate.FanSpeed") {
|
|
74831
|
+
const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
|
|
74832
|
+
if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
|
|
74833
|
+
return {
|
|
74834
|
+
commands: [() => cmd.setFanMode(fan[1])],
|
|
74835
|
+
projected: {
|
|
74836
|
+
...slice,
|
|
74837
|
+
fanMode: fan[1]
|
|
74838
|
+
},
|
|
74839
|
+
keep
|
|
74840
|
+
};
|
|
74841
|
+
}
|
|
74842
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
|
|
74843
|
+
}
|
|
74844
|
+
function plan(c, slice, cmd) {
|
|
74845
|
+
switch (`${c.header.namespace}/${c.header.name}`) {
|
|
74846
|
+
case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
|
|
74847
|
+
case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
|
|
74848
|
+
case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
|
|
74849
|
+
case "Alexa.PowerController/TurnOn":
|
|
74850
|
+
case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
|
|
74851
|
+
case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
|
|
74852
|
+
default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
|
|
74853
|
+
}
|
|
74854
|
+
}
|
|
74855
|
+
async function readSlice$1(c, deps) {
|
|
74856
|
+
try {
|
|
74857
|
+
return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
|
|
74858
|
+
deviceId: c.deviceId,
|
|
74859
|
+
capName: CAP_CLIMATE
|
|
74860
|
+
}));
|
|
74861
|
+
} catch (err) {
|
|
74862
|
+
deps.logger.warn("alexa climate-controller: the climate state could not be read", {
|
|
74863
|
+
tags: { deviceId: c.deviceId },
|
|
74864
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
74865
|
+
});
|
|
74866
|
+
return null;
|
|
74867
|
+
}
|
|
74868
|
+
}
|
|
74869
|
+
function successResponse$1(c, properties) {
|
|
74870
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
74871
|
+
return {
|
|
74872
|
+
event: {
|
|
74873
|
+
header: {
|
|
74874
|
+
namespace: "Alexa",
|
|
74875
|
+
name: "Response",
|
|
74876
|
+
messageId: c.header.messageId,
|
|
74877
|
+
...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
|
|
74878
|
+
payloadVersion: "3"
|
|
74879
|
+
},
|
|
74880
|
+
endpoint: { endpointId: c.endpoint.endpointId },
|
|
74881
|
+
payload: {}
|
|
74882
|
+
},
|
|
74883
|
+
context: { properties: properties.map((p) => ({
|
|
74884
|
+
...p,
|
|
74885
|
+
timeOfSample: now,
|
|
74886
|
+
uncertaintyInMilliseconds: UNCERTAINTY_MS
|
|
74887
|
+
})) }
|
|
74888
|
+
};
|
|
74889
|
+
}
|
|
74890
|
+
/** The one handler behind every climate directive. */
|
|
74891
|
+
async function handleClimateDirective(c) {
|
|
74892
|
+
const deps = c.deps.climateController;
|
|
74893
|
+
const slice = await readSlice$1(c, deps);
|
|
74894
|
+
if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
|
|
74895
|
+
const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
|
|
74896
|
+
if (isRefusal(outcome)) return outcome.refusal;
|
|
74897
|
+
try {
|
|
74898
|
+
for (const command of outcome.commands) await command();
|
|
74899
|
+
} catch (err) {
|
|
74900
|
+
deps.logger.warn("alexa climate-controller: the climate command failed", {
|
|
74901
|
+
tags: { deviceId: c.deviceId },
|
|
74902
|
+
meta: {
|
|
74903
|
+
directive: c.header.name,
|
|
74904
|
+
error: err instanceof Error ? err.message : String(err)
|
|
74905
|
+
}
|
|
74906
|
+
});
|
|
74907
|
+
return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
|
|
74908
|
+
}
|
|
74909
|
+
deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
|
|
74910
|
+
tags: { deviceId: c.deviceId },
|
|
74911
|
+
meta: { commands: outcome.commands.length }
|
|
74912
|
+
});
|
|
74913
|
+
return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
|
|
74914
|
+
}
|
|
74915
|
+
/**
|
|
74916
|
+
* Last answer per device, per deps instance: a failed bindings read changes
|
|
74917
|
+
* nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
|
|
74918
|
+
* switch handler because one `getBindings` call failed.
|
|
74919
|
+
*/
|
|
74920
|
+
var lastPowerClaim = /* @__PURE__ */ new WeakMap();
|
|
74921
|
+
/** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
|
|
74922
|
+
async function handlesPower(c) {
|
|
74923
|
+
const deps = c.deps.climateController;
|
|
74924
|
+
const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
|
|
74925
|
+
lastPowerClaim.set(deps, known);
|
|
74926
|
+
try {
|
|
74927
|
+
const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
|
|
74928
|
+
const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
|
|
74929
|
+
known.set(c.deviceId, claim);
|
|
74930
|
+
return claim;
|
|
74931
|
+
} catch (err) {
|
|
74932
|
+
const previous = known.get(c.deviceId);
|
|
74933
|
+
deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
|
|
74934
|
+
tags: { deviceId: c.deviceId },
|
|
74935
|
+
meta: {
|
|
74936
|
+
error: err instanceof Error ? err.message : String(err),
|
|
74937
|
+
decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
|
|
74938
|
+
}
|
|
74939
|
+
});
|
|
74940
|
+
return previous ?? false;
|
|
74941
|
+
}
|
|
74942
|
+
}
|
|
74943
|
+
//#endregion
|
|
74001
74944
|
//#region src/endpoints/adapters/shared.ts
|
|
74002
74945
|
/** The base `Alexa` interface every endpoint declares. */
|
|
74003
74946
|
var BASE = {
|
|
@@ -74048,6 +74991,88 @@ function numericValueField(value) {
|
|
|
74048
74991
|
return typeof raw === "number" ? raw : null;
|
|
74049
74992
|
}
|
|
74050
74993
|
//#endregion
|
|
74994
|
+
//#region src/endpoints/adapters/temperature.ts
|
|
74995
|
+
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
74996
|
+
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
74997
|
+
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
74998
|
+
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
74999
|
+
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
75000
|
+
var temperatureAdapter = {
|
|
75001
|
+
cap: "temperature-sensor",
|
|
75002
|
+
stateCaps: ["temperature-sensor"],
|
|
75003
|
+
discoveryReads: [],
|
|
75004
|
+
describe: () => ({
|
|
75005
|
+
kind: "publish",
|
|
75006
|
+
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
75007
|
+
category: "TEMPERATURE_SENSOR"
|
|
75008
|
+
}),
|
|
75009
|
+
properties: (_cap, slice) => {
|
|
75010
|
+
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
75011
|
+
return p.success ? [{
|
|
75012
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75013
|
+
name: "temperature",
|
|
75014
|
+
value: {
|
|
75015
|
+
value: Math.round(p.data.celsius * 10) / 10,
|
|
75016
|
+
scale: "CELSIUS"
|
|
75017
|
+
}
|
|
75018
|
+
}] : [];
|
|
75019
|
+
},
|
|
75020
|
+
directives: [],
|
|
75021
|
+
shaping: () => ({
|
|
75022
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75023
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75024
|
+
numericOf: numericValueField
|
|
75025
|
+
})
|
|
75026
|
+
};
|
|
75027
|
+
//#endregion
|
|
75028
|
+
//#region src/endpoints/adapters/climate.ts
|
|
75029
|
+
var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
|
|
75030
|
+
var THERMOSTAT_DIRECTIVES = [
|
|
75031
|
+
"Alexa.ThermostatController/SetTargetTemperature",
|
|
75032
|
+
"Alexa.ThermostatController/AdjustTargetTemperature",
|
|
75033
|
+
"Alexa.ThermostatController/SetThermostatMode"
|
|
75034
|
+
].map((key) => ({
|
|
75035
|
+
key,
|
|
75036
|
+
needsReachableDevice: true,
|
|
75037
|
+
handle: handleClimateDirective
|
|
75038
|
+
}));
|
|
75039
|
+
var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
|
|
75040
|
+
key,
|
|
75041
|
+
needsReachableDevice: true,
|
|
75042
|
+
handle: handleClimateDirective,
|
|
75043
|
+
claims: handlesPower
|
|
75044
|
+
}));
|
|
75045
|
+
var MODE_DIRECTIVE = {
|
|
75046
|
+
key: "Alexa.ModeController/SetMode",
|
|
75047
|
+
needsReachableDevice: true,
|
|
75048
|
+
handle: handleClimateDirective,
|
|
75049
|
+
claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
|
|
75050
|
+
};
|
|
75051
|
+
/**
|
|
75052
|
+
* `climate-control` -> a thermostat (Home Assistant) or an air conditioner
|
|
75053
|
+
* (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
|
|
75054
|
+
* power, and the modes Amazon's vocabulary lacks and the fan mode as
|
|
75055
|
+
* ModeControllers. The current temperature is shaped like any temperature
|
|
75056
|
+
* sensor; setpoints, mode and fan report on every change.
|
|
75057
|
+
*/
|
|
75058
|
+
var climateAdapter = {
|
|
75059
|
+
cap: "climate-control",
|
|
75060
|
+
stateCaps: ["climate-control"],
|
|
75061
|
+
discoveryReads: ["climate-control"],
|
|
75062
|
+
describe: describeClimate,
|
|
75063
|
+
properties: (_cap, slice) => climateProperties(slice),
|
|
75064
|
+
directives: [
|
|
75065
|
+
...THERMOSTAT_DIRECTIVES,
|
|
75066
|
+
...POWER_DIRECTIVES$1,
|
|
75067
|
+
MODE_DIRECTIVE
|
|
75068
|
+
],
|
|
75069
|
+
shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
|
|
75070
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75071
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75072
|
+
numericOf: numericValueField
|
|
75073
|
+
} : null
|
|
75074
|
+
};
|
|
75075
|
+
//#endregion
|
|
74051
75076
|
//#region src/endpoints/adapters/contact.ts
|
|
74052
75077
|
/** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
|
|
74053
75078
|
var contactAdapter = {
|
|
@@ -74712,7 +75737,8 @@ var coverAdapter = {
|
|
|
74712
75737
|
].map((key) => ({
|
|
74713
75738
|
key,
|
|
74714
75739
|
needsReachableDevice: true,
|
|
74715
|
-
handle: handleCoverDirective
|
|
75740
|
+
handle: handleCoverDirective,
|
|
75741
|
+
claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
|
|
74716
75742
|
})),
|
|
74717
75743
|
shaping: (property) => property.namespace === "Alexa.RangeController" ? {
|
|
74718
75744
|
deadband: 1,
|
|
@@ -74720,40 +75746,6 @@ var coverAdapter = {
|
|
|
74720
75746
|
numericOf: plainNumber
|
|
74721
75747
|
} : { debounceMs: COVER_DEBOUNCE_MS }
|
|
74722
75748
|
};
|
|
74723
|
-
//#endregion
|
|
74724
|
-
//#region src/endpoints/adapters/temperature.ts
|
|
74725
|
-
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
74726
|
-
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
74727
|
-
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
74728
|
-
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
74729
|
-
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
74730
|
-
var temperatureAdapter = {
|
|
74731
|
-
cap: "temperature-sensor",
|
|
74732
|
-
stateCaps: ["temperature-sensor"],
|
|
74733
|
-
discoveryReads: [],
|
|
74734
|
-
describe: () => ({
|
|
74735
|
-
kind: "publish",
|
|
74736
|
-
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
74737
|
-
category: "TEMPERATURE_SENSOR"
|
|
74738
|
-
}),
|
|
74739
|
-
properties: (_cap, slice) => {
|
|
74740
|
-
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
74741
|
-
return p.success ? [{
|
|
74742
|
-
namespace: "Alexa.TemperatureSensor",
|
|
74743
|
-
name: "temperature",
|
|
74744
|
-
value: {
|
|
74745
|
-
value: Math.round(p.data.celsius * 10) / 10,
|
|
74746
|
-
scale: "CELSIUS"
|
|
74747
|
-
}
|
|
74748
|
-
}] : [];
|
|
74749
|
-
},
|
|
74750
|
-
directives: [],
|
|
74751
|
-
shaping: () => ({
|
|
74752
|
-
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
74753
|
-
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
74754
|
-
numericOf: numericValueField
|
|
74755
|
-
})
|
|
74756
|
-
};
|
|
74757
75749
|
/** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
|
|
74758
75750
|
var humidityAdapter = {
|
|
74759
75751
|
cap: "humidity-sensor",
|
|
@@ -75052,6 +76044,11 @@ var BRIGHTNESS_DIRECTIVE = {
|
|
|
75052
76044
|
needsReachableDevice: true,
|
|
75053
76045
|
handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
|
|
75054
76046
|
};
|
|
76047
|
+
/** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
|
|
76048
|
+
* smart plug; every other switch is a SWITCH. */
|
|
76049
|
+
function switchCategory(device) {
|
|
76050
|
+
return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
|
|
76051
|
+
}
|
|
75055
76052
|
//#endregion
|
|
75056
76053
|
//#region src/endpoints/adapters/index.ts
|
|
75057
76054
|
var GENERIC_ADAPTERS = [
|
|
@@ -75059,6 +76056,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75059
76056
|
contactAdapter,
|
|
75060
76057
|
motionAdapter,
|
|
75061
76058
|
presenceAdapter,
|
|
76059
|
+
climateAdapter,
|
|
75062
76060
|
temperatureAdapter,
|
|
75063
76061
|
humidityAdapter,
|
|
75064
76062
|
{
|
|
@@ -75070,7 +76068,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75070
76068
|
return {
|
|
75071
76069
|
kind: "publish",
|
|
75072
76070
|
capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
|
|
75073
|
-
category: dimmable || device.type === "light" ? "LIGHT" :
|
|
76071
|
+
category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
|
|
75074
76072
|
};
|
|
75075
76073
|
},
|
|
75076
76074
|
properties: (capName, slice) => {
|
|
@@ -75116,12 +76114,19 @@ function shapingFor(capName, property) {
|
|
|
75116
76114
|
function discoveryReadCaps(capNames) {
|
|
75117
76115
|
return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
|
|
75118
76116
|
}
|
|
75119
|
-
/**
|
|
75120
|
-
function
|
|
75121
|
-
|
|
75122
|
-
|
|
75123
|
-
|
|
75124
|
-
|
|
76117
|
+
/** Every directive an adapter declares for `Namespace/Name`, in registry order. */
|
|
76118
|
+
function genericDirectives(key) {
|
|
76119
|
+
return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
|
|
76120
|
+
}
|
|
76121
|
+
/**
|
|
76122
|
+
* The directive that serves this endpoint: of the adapters declaring the key,
|
|
76123
|
+
* in registry order, the first that claims it (two caps may answer one key -
|
|
76124
|
+
* power on a switch and on a thermostat). Null = nobody claims it.
|
|
76125
|
+
*/
|
|
76126
|
+
async function resolveGenericDirective(context) {
|
|
76127
|
+
const key = `${context.header.namespace}/${context.header.name}`;
|
|
76128
|
+
for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
|
|
76129
|
+
return null;
|
|
75125
76130
|
}
|
|
75126
76131
|
/** Every directive key the registry handles. */
|
|
75127
76132
|
var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
|
|
@@ -75163,9 +76168,9 @@ function planGenericDevice(device) {
|
|
|
75163
76168
|
category
|
|
75164
76169
|
};
|
|
75165
76170
|
}
|
|
75166
|
-
/** Device types this export may publish as a generic endpoint.
|
|
75167
|
-
*
|
|
75168
|
-
*
|
|
76171
|
+
/** Device types this export may publish as a generic endpoint. `thermostat`
|
|
76172
|
+
* (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
|
|
76173
|
+
* the `climate-control` cap (D874). */
|
|
75169
76174
|
var GENERIC_DEVICE_TYPES = [
|
|
75170
76175
|
"light",
|
|
75171
76176
|
"switch",
|
|
@@ -75174,11 +76179,13 @@ var GENERIC_DEVICE_TYPES = [
|
|
|
75174
76179
|
"lock",
|
|
75175
76180
|
"presence",
|
|
75176
76181
|
"thermostat",
|
|
76182
|
+
"climate",
|
|
75177
76183
|
"cover",
|
|
75178
76184
|
"generic"
|
|
75179
76185
|
];
|
|
75180
76186
|
function toEndpoint(input, plan) {
|
|
75181
|
-
const
|
|
76187
|
+
const override = input.alexaName?.trim() ?? "";
|
|
76188
|
+
const name = override.length > 0 ? override : input.name.trim();
|
|
75182
76189
|
return {
|
|
75183
76190
|
endpointId: `${input.deviceId}__dev`,
|
|
75184
76191
|
manufacturerName: "camstack",
|
|
@@ -75189,13 +76196,7 @@ function toEndpoint(input, plan) {
|
|
|
75189
76196
|
cookie: { stableId: input.stableId }
|
|
75190
76197
|
};
|
|
75191
76198
|
}
|
|
75192
|
-
/** Why a climate device is withheld until its adapter exists. */
|
|
75193
|
-
var THERMOSTAT_SKIP_REASON = "thermostat: Alexa climate control (Alexa.ThermostatController) has no adapter yet - not published until it does (D869, P3)";
|
|
75194
76199
|
function planGenericEndpoint(input) {
|
|
75195
|
-
if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
|
|
75196
|
-
kind: "skip",
|
|
75197
|
-
reasons: [THERMOSTAT_SKIP_REASON]
|
|
75198
|
-
};
|
|
75199
76200
|
const plan = planGenericDevice({
|
|
75200
76201
|
deviceId: input.deviceId,
|
|
75201
76202
|
type: input.type,
|
|
@@ -75211,6 +76212,74 @@ function planGenericEndpoint(input) {
|
|
|
75211
76212
|
return plan;
|
|
75212
76213
|
}
|
|
75213
76214
|
//#endregion
|
|
76215
|
+
//#region src/endpoints/alexa-name.ts
|
|
76216
|
+
/**
|
|
76217
|
+
* The per-device "Alexa name" (D875): what Alexa calls the device, when it
|
|
76218
|
+
* should not be the device's own name.
|
|
76219
|
+
*
|
|
76220
|
+
* Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
|
|
76221
|
+
* (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
|
|
76222
|
+
* id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
|
|
76223
|
+
* AddOrUpdateReport instead of creating a duplicate.
|
|
76224
|
+
*
|
|
76225
|
+
* Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
|
|
76226
|
+
* to 256 alphanumeric characters and spaces. Don't include special characters
|
|
76227
|
+
* or punctuation." Letters of any script count as alphanumeric, so an Italian
|
|
76228
|
+
* name keeps its accents. The validator rejects what Amazon would reject, with
|
|
76229
|
+
* a message the operator can act on.
|
|
76230
|
+
*/
|
|
76231
|
+
/** Per-device store key of the saved Alexa name. */
|
|
76232
|
+
var ALEXA_NAME_KEY = "alexaName";
|
|
76233
|
+
/** The device-settings form field key, namespaced per exporter (see the addon). */
|
|
76234
|
+
function alexaNameKey(deviceId) {
|
|
76235
|
+
return `alexa:${deviceId}:alexaName`;
|
|
76236
|
+
}
|
|
76237
|
+
/** Letters (any script, with their combining marks), digits and spaces. */
|
|
76238
|
+
var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
|
|
76239
|
+
/**
|
|
76240
|
+
* Validate an operator-typed Alexa name. Blank clears the override
|
|
76241
|
+
* (`name: null`, the device name is used).
|
|
76242
|
+
*/
|
|
76243
|
+
function validateAlexaName(raw) {
|
|
76244
|
+
if (raw === null || raw === void 0) return {
|
|
76245
|
+
ok: true,
|
|
76246
|
+
name: null
|
|
76247
|
+
};
|
|
76248
|
+
if (typeof raw !== "string") return {
|
|
76249
|
+
ok: false,
|
|
76250
|
+
error: "The Alexa name must be text."
|
|
76251
|
+
};
|
|
76252
|
+
const name = raw.trim().replace(/\s+/g, " ");
|
|
76253
|
+
if (name.length === 0) return {
|
|
76254
|
+
ok: true,
|
|
76255
|
+
name: null
|
|
76256
|
+
};
|
|
76257
|
+
if (name.length > 256) return {
|
|
76258
|
+
ok: false,
|
|
76259
|
+
error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
|
|
76260
|
+
};
|
|
76261
|
+
if (!ALLOWED_PATTERN.test(name)) return {
|
|
76262
|
+
ok: false,
|
|
76263
|
+
error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
|
|
76264
|
+
};
|
|
76265
|
+
return {
|
|
76266
|
+
ok: true,
|
|
76267
|
+
name
|
|
76268
|
+
};
|
|
76269
|
+
}
|
|
76270
|
+
var StoredNameSchema = string();
|
|
76271
|
+
/**
|
|
76272
|
+
* The saved Alexa name in a per-device store blob, or null (never saved, or a
|
|
76273
|
+
* stored value that no longer validates: the device name is used rather than
|
|
76274
|
+
* pushing a name Amazon would refuse).
|
|
76275
|
+
*/
|
|
76276
|
+
function readAlexaName(store) {
|
|
76277
|
+
const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
|
|
76278
|
+
if (!parsed.success) return null;
|
|
76279
|
+
const check = validateAlexaName(parsed.data);
|
|
76280
|
+
return check.ok ? check.name : null;
|
|
76281
|
+
}
|
|
76282
|
+
//#endregion
|
|
75214
76283
|
//#region src/handlers/security-panel-arm-state.ts
|
|
75215
76284
|
/**
|
|
75216
76285
|
* The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
|
|
@@ -75884,6 +76953,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75884
76953
|
deviceId: parentInfo.id,
|
|
75885
76954
|
stableId: parentInfo.stableId,
|
|
75886
76955
|
name: parentInfo.name,
|
|
76956
|
+
alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
|
|
75887
76957
|
type: String(device.type),
|
|
75888
76958
|
capNames: parentInfo.capNames,
|
|
75889
76959
|
role: parentInfo.role,
|
|
@@ -75942,11 +77012,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75942
77012
|
return [];
|
|
75943
77013
|
});
|
|
75944
77014
|
const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
|
|
77015
|
+
const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
|
|
75945
77016
|
const out = [];
|
|
75946
77017
|
out.push(buildParentCameraEndpoint({
|
|
75947
77018
|
deviceId: parentInfo.id,
|
|
75948
77019
|
stableId: parentInfo.stableId,
|
|
75949
|
-
friendlyName:
|
|
77020
|
+
friendlyName: cameraName,
|
|
75950
77021
|
isDoorbell,
|
|
75951
77022
|
hasIntercom,
|
|
75952
77023
|
hasMotion,
|
|
@@ -75960,7 +77031,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75960
77031
|
for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
|
|
75961
77032
|
deviceId: parentInfo.id,
|
|
75962
77033
|
stableId: parentInfo.stableId,
|
|
75963
|
-
cameraName
|
|
77034
|
+
cameraName,
|
|
75964
77035
|
detectionClass
|
|
75965
77036
|
}));
|
|
75966
77037
|
}
|
|
@@ -76454,6 +77525,36 @@ function validateInstanced(c, push) {
|
|
|
76454
77525
|
if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
|
|
76455
77526
|
}
|
|
76456
77527
|
//#endregion
|
|
77528
|
+
//#region src/endpoints/validate-thermostat.ts
|
|
77529
|
+
var PROPERTIES = new Set([
|
|
77530
|
+
"targetSetpoint",
|
|
77531
|
+
"lowerSetpoint",
|
|
77532
|
+
"upperSetpoint",
|
|
77533
|
+
"thermostatMode"
|
|
77534
|
+
]);
|
|
77535
|
+
var MODES = new Set([
|
|
77536
|
+
"AUTO",
|
|
77537
|
+
"COOL",
|
|
77538
|
+
"HEAT",
|
|
77539
|
+
"ECO",
|
|
77540
|
+
"OFF"
|
|
77541
|
+
]);
|
|
77542
|
+
function validateThermostat(c, push) {
|
|
77543
|
+
const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
|
|
77544
|
+
for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
|
|
77545
|
+
if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
|
|
77546
|
+
if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
|
|
77547
|
+
if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
|
|
77548
|
+
const modes = c.configuration?.["supportedModes"];
|
|
77549
|
+
if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
|
|
77550
|
+
else {
|
|
77551
|
+
for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
|
|
77552
|
+
if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
|
|
77553
|
+
}
|
|
77554
|
+
const scheduling = c.configuration?.["supportsScheduling"];
|
|
77555
|
+
if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
|
|
77556
|
+
}
|
|
77557
|
+
//#endregion
|
|
76457
77558
|
//#region src/endpoints/validate-endpoint.ts
|
|
76458
77559
|
/**
|
|
76459
77560
|
* Interfaces this skill may declare, with the version Amazon documents. An
|
|
@@ -76477,6 +77578,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
|
|
|
76477
77578
|
"Alexa.RangeController": "3",
|
|
76478
77579
|
"Alexa.SecurityPanelController": "3",
|
|
76479
77580
|
"Alexa.LockController": "3",
|
|
77581
|
+
"Alexa.ThermostatController": "3.1",
|
|
76480
77582
|
"Alexa.SmartVision.SnapshotProvider": "1.1",
|
|
76481
77583
|
"Alexa.SmartVision.ObjectDetectionSensor": "1.0",
|
|
76482
77584
|
"Alexa.DataController": "1.0"
|
|
@@ -76516,6 +77618,8 @@ var DISPLAY_CATEGORIES = new Set([
|
|
|
76516
77618
|
"SMARTLOCK",
|
|
76517
77619
|
"INTERIOR_BLIND",
|
|
76518
77620
|
"EXTERIOR_BLIND",
|
|
77621
|
+
"THERMOSTAT",
|
|
77622
|
+
"AIR_CONDITIONER",
|
|
76519
77623
|
"DOOR",
|
|
76520
77624
|
"GATE",
|
|
76521
77625
|
"GARAGE_DOOR",
|
|
@@ -76594,6 +77698,7 @@ function validateCapability(endpointId, c, out, declared) {
|
|
|
76594
77698
|
}
|
|
76595
77699
|
}
|
|
76596
77700
|
validateInstanced(c, push);
|
|
77701
|
+
if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
|
|
76597
77702
|
if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
|
|
76598
77703
|
if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
|
|
76599
77704
|
if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
|
|
@@ -78524,20 +79629,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
|
|
|
78524
79629
|
status: 200,
|
|
78525
79630
|
body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
|
|
78526
79631
|
};
|
|
78527
|
-
|
|
78528
|
-
if (adapterDirective !== void 0 && endpoint !== void 0) {
|
|
79632
|
+
if (genericDirectives(key).length > 0 && endpoint !== void 0) {
|
|
78529
79633
|
const target = parseCameraEndpointId(endpoint.endpointId);
|
|
78530
|
-
if (target.kind === "generic")
|
|
78531
|
-
|
|
78532
|
-
body: await adapterDirective.handle({
|
|
79634
|
+
if (target.kind === "generic") {
|
|
79635
|
+
const directiveContext = {
|
|
78533
79636
|
header,
|
|
78534
79637
|
endpoint,
|
|
78535
79638
|
payload,
|
|
78536
79639
|
deviceId: target.deviceId,
|
|
78537
79640
|
deps,
|
|
78538
79641
|
identity: callerIdentity
|
|
78539
|
-
}
|
|
78540
|
-
|
|
79642
|
+
};
|
|
79643
|
+
const adapterDirective = await resolveGenericDirective(directiveContext);
|
|
79644
|
+
if (adapterDirective === null) return {
|
|
79645
|
+
status: 200,
|
|
79646
|
+
body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
|
|
79647
|
+
};
|
|
79648
|
+
return {
|
|
79649
|
+
status: 200,
|
|
79650
|
+
body: await adapterDirective.handle(directiveContext)
|
|
79651
|
+
};
|
|
79652
|
+
}
|
|
78541
79653
|
}
|
|
78542
79654
|
if (key === "Alexa.ModeController/SetMode") return {
|
|
78543
79655
|
status: 200,
|
|
@@ -82680,6 +83792,7 @@ var SUPPORTED_KINDS = [
|
|
|
82680
83792
|
DeviceType.Lock,
|
|
82681
83793
|
DeviceType.Presence,
|
|
82682
83794
|
DeviceType.Thermostat,
|
|
83795
|
+
DeviceType.Climate,
|
|
82683
83796
|
DeviceType.Cover,
|
|
82684
83797
|
DeviceType.Generic
|
|
82685
83798
|
];
|
|
@@ -82818,6 +83931,10 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
82818
83931
|
ctx: this.ctx,
|
|
82819
83932
|
logger: this.ctx.logger
|
|
82820
83933
|
},
|
|
83934
|
+
climateController: {
|
|
83935
|
+
ctx: this.ctx,
|
|
83936
|
+
logger: this.ctx.logger
|
|
83937
|
+
},
|
|
82821
83938
|
coverController: {
|
|
82822
83939
|
ctx: this.ctx,
|
|
82823
83940
|
logger: this.ctx.logger,
|
|
@@ -83074,6 +84191,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83074
84191
|
logger: this.ctx.logger,
|
|
83075
84192
|
snapshots: this.config.snapshots,
|
|
83076
84193
|
...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
|
|
84194
|
+
alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
|
|
83077
84195
|
...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
|
|
83078
84196
|
});
|
|
83079
84197
|
}
|
|
@@ -83641,12 +84759,28 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83641
84759
|
value: enabled,
|
|
83642
84760
|
immediate: true
|
|
83643
84761
|
},
|
|
84762
|
+
await this.alexaNameField(deviceId, enabledKey),
|
|
83644
84763
|
...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
|
|
83645
84764
|
...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
|
|
83646
84765
|
]
|
|
83647
84766
|
}]
|
|
83648
84767
|
};
|
|
83649
84768
|
}
|
|
84769
|
+
/** D875: the "Alexa name" field, with the validation error of the saved value. */
|
|
84770
|
+
async alexaNameField(deviceId, enabledKey) {
|
|
84771
|
+
const saved = readAlexaName(await this.vision.readPrefs(deviceId));
|
|
84772
|
+
return {
|
|
84773
|
+
type: "text",
|
|
84774
|
+
key: alexaNameKey(deviceId),
|
|
84775
|
+
label: "Alexa name",
|
|
84776
|
+
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.`,
|
|
84777
|
+
value: saved ?? "",
|
|
84778
|
+
showWhen: {
|
|
84779
|
+
field: enabledKey,
|
|
84780
|
+
equals: true
|
|
84781
|
+
}
|
|
84782
|
+
};
|
|
84783
|
+
}
|
|
83650
84784
|
/**
|
|
83651
84785
|
* The camera's detection fields: which classes become Alexa sensors (D778;
|
|
83652
84786
|
* only the classes its pipeline produces are offered) and its object
|
|
@@ -83668,6 +84802,24 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83668
84802
|
})];
|
|
83669
84803
|
}
|
|
83670
84804
|
/**
|
|
84805
|
+
* D875: save the Alexa name from a device-settings patch. A name Amazon would
|
|
84806
|
+
* refuse throws BEFORE anything is written, so the form shows the reason and
|
|
84807
|
+
* the old name stays. True when the saved name changed.
|
|
84808
|
+
*/
|
|
84809
|
+
async applyAlexaNamePatch(deviceId, patch) {
|
|
84810
|
+
const key = alexaNameKey(deviceId);
|
|
84811
|
+
if (!(key in patch)) return false;
|
|
84812
|
+
const check = validateAlexaName(patch[key]);
|
|
84813
|
+
if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
|
|
84814
|
+
if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
|
|
84815
|
+
await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
|
|
84816
|
+
this.ctx.logger.info("export-alexa: Alexa name saved", {
|
|
84817
|
+
tags: { deviceId },
|
|
84818
|
+
meta: { name: check.name }
|
|
84819
|
+
});
|
|
84820
|
+
return true;
|
|
84821
|
+
}
|
|
84822
|
+
/**
|
|
83671
84823
|
* Persist the camera's detection preferences from a device-settings patch
|
|
83672
84824
|
* into this addon's per-device store. True when anything changed — the
|
|
83673
84825
|
* camera's Alexa shape then needs a reconcile.
|
|
@@ -83732,6 +84884,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83732
84884
|
const current = this.findEntry(deviceId);
|
|
83733
84885
|
const wasEnabled = current !== null;
|
|
83734
84886
|
const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
|
|
84887
|
+
const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
|
|
83735
84888
|
const enabledKey = `alexa:${deviceId}:enabled`;
|
|
83736
84889
|
const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
|
|
83737
84890
|
const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
|
|
@@ -83757,6 +84910,12 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83757
84910
|
const currentAllow = current?.settings?.allowTranscode !== false;
|
|
83758
84911
|
await this.patchEntrySettings(deviceId, nextSettings);
|
|
83759
84912
|
const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
|
|
84913
|
+
if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
|
|
84914
|
+
this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
|
|
84915
|
+
tags: { deviceId },
|
|
84916
|
+
meta: { error: errMsg(err) }
|
|
84917
|
+
});
|
|
84918
|
+
});
|
|
83760
84919
|
if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
|
|
83761
84920
|
this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
|
|
83762
84921
|
tags: { deviceId },
|