@camstack/addon-export-alexa 1.2.144 → 1.2.146
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 +2010 -112
- package/dist/export-alexa.addon.mjs +2010 -112
- package/lambda-template/broker.js +27 -7
- package/lambda-template/debug-trace.js +2 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import "node:fs";
|
|
|
5
5
|
import "node:path";
|
|
6
6
|
import "node:child_process";
|
|
7
7
|
import "node:zlib";
|
|
8
|
-
//#region ../types/dist/event-category-
|
|
8
|
+
//#region ../types/dist/event-category-BvOFi6PY.mjs
|
|
9
9
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
10
10
|
EventCategory["SystemBoot"] = "system.boot";
|
|
11
11
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -69,6 +69,24 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
69
69
|
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
70
70
|
*/
|
|
71
71
|
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
72
|
+
/**
|
|
73
|
+
* A runner whose addon refused to start with a typed reason (D877) - parked
|
|
74
|
+
* in `refused`, never counted by the crash breaker, retried on operator
|
|
75
|
+
* action. One event per refusal.
|
|
76
|
+
*/
|
|
77
|
+
EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
|
|
78
|
+
/**
|
|
79
|
+
* A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
|
|
80
|
+
* `refused`. In-process telemetry for the boot check (re-evaluate while any
|
|
81
|
+
* runner is still pending) and the alert centre (close a recovered runner).
|
|
82
|
+
*/
|
|
83
|
+
EventCategory["ProcessStateChanged"] = "process.state-changed";
|
|
84
|
+
/**
|
|
85
|
+
* The node's one-shot runner boot check (D877): planned runners against the
|
|
86
|
+
* process service, `degraded` when any is failed, refused or missing. The
|
|
87
|
+
* alert centre turns it into one alert and a direct critical push.
|
|
88
|
+
*/
|
|
89
|
+
EventCategory["SystemRunnerCheck"] = "system.runner-check";
|
|
72
90
|
EventCategory["AddonError"] = "addon.error";
|
|
73
91
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
74
92
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -5393,7 +5411,7 @@ var ZodIssueCode = {
|
|
|
5393
5411
|
var ZodFirstPartyTypeKind;
|
|
5394
5412
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5395
5413
|
//#endregion
|
|
5396
|
-
//#region ../types/dist/sleep-
|
|
5414
|
+
//#region ../types/dist/sleep-bZIhUvjd.mjs
|
|
5397
5415
|
/**
|
|
5398
5416
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5399
5417
|
* window to float samples (D455).
|
|
@@ -5510,7 +5528,7 @@ function isKeychainStoredField(field) {
|
|
|
5510
5528
|
var SECRET_CLEAR = { $secretClear: true };
|
|
5511
5529
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
5512
5530
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
5513
|
-
function isRecord$
|
|
5531
|
+
function isRecord$5(value) {
|
|
5514
5532
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5515
5533
|
}
|
|
5516
5534
|
function makeSecretMarker(ref) {
|
|
@@ -5521,12 +5539,12 @@ function makeSecretMarker(ref) {
|
|
|
5521
5539
|
}
|
|
5522
5540
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
5523
5541
|
function isSecretMarker(value) {
|
|
5524
|
-
if (!isRecord$
|
|
5542
|
+
if (!isRecord$5(value)) return false;
|
|
5525
5543
|
const ref = value["$secret"];
|
|
5526
5544
|
return typeof ref === "string" && ref.length > 0;
|
|
5527
5545
|
}
|
|
5528
5546
|
function isSecretClear(value) {
|
|
5529
|
-
return isRecord$
|
|
5547
|
+
return isRecord$5(value) && value["$secretClear"] === true;
|
|
5530
5548
|
}
|
|
5531
5549
|
/**
|
|
5532
5550
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -6136,7 +6154,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
6136
6154
|
}
|
|
6137
6155
|
};
|
|
6138
6156
|
var KEEP = { kind: "keep" };
|
|
6139
|
-
function isRecord$
|
|
6157
|
+
function isRecord$4(value) {
|
|
6140
6158
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6141
6159
|
}
|
|
6142
6160
|
function defaultMintId() {
|
|
@@ -6149,7 +6167,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
6149
6167
|
return out;
|
|
6150
6168
|
}
|
|
6151
6169
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
6152
|
-
else if (isRecord$
|
|
6170
|
+
else if (isRecord$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
6153
6171
|
return out;
|
|
6154
6172
|
}
|
|
6155
6173
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -6204,7 +6222,7 @@ function rowId(row, idKey) {
|
|
|
6204
6222
|
return null;
|
|
6205
6223
|
}
|
|
6206
6224
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
6207
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
6225
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4) : [];
|
|
6208
6226
|
if (!Array.isArray(incoming)) {
|
|
6209
6227
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
6210
6228
|
return {
|
|
@@ -6220,7 +6238,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
6220
6238
|
const kept = /* @__PURE__ */ new Set();
|
|
6221
6239
|
const rows = [];
|
|
6222
6240
|
for (const row of incoming) {
|
|
6223
|
-
if (!isRecord$
|
|
6241
|
+
if (!isRecord$4(row)) {
|
|
6224
6242
|
rows.push(row);
|
|
6225
6243
|
continue;
|
|
6226
6244
|
}
|
|
@@ -6245,7 +6263,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
6245
6263
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
6246
6264
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
6247
6265
|
case "object": {
|
|
6248
|
-
if (!isRecord$
|
|
6266
|
+
if (!isRecord$4(incoming)) {
|
|
6249
6267
|
if (incoming === void 0) return KEEP;
|
|
6250
6268
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
6251
6269
|
return {
|
|
@@ -6253,7 +6271,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
6253
6271
|
value: incoming
|
|
6254
6272
|
};
|
|
6255
6273
|
}
|
|
6256
|
-
const storedObj = isRecord$
|
|
6274
|
+
const storedObj = isRecord$4(stored) ? stored : {};
|
|
6257
6275
|
return {
|
|
6258
6276
|
kind: "value",
|
|
6259
6277
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -6376,7 +6394,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
6376
6394
|
value: items
|
|
6377
6395
|
};
|
|
6378
6396
|
}
|
|
6379
|
-
if (isRecord$
|
|
6397
|
+
if (isRecord$4(v)) {
|
|
6380
6398
|
const out = {};
|
|
6381
6399
|
for (const [k, child] of Object.entries(v)) {
|
|
6382
6400
|
const s = substitute(child);
|
|
@@ -6644,7 +6662,6 @@ function createHandle(raw, opts) {
|
|
|
6644
6662
|
const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
|
|
6645
6663
|
const readAddonStoreResult = raw.readAddonStoreResult;
|
|
6646
6664
|
const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
|
|
6647
|
-
const readDeviceStoreBatch = raw.readDeviceStoreBatch;
|
|
6648
6665
|
const overrides = {
|
|
6649
6666
|
readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
|
|
6650
6667
|
...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
|
|
@@ -6668,12 +6685,12 @@ function createHandle(raw, opts) {
|
|
|
6668
6685
|
};
|
|
6669
6686
|
} } : {},
|
|
6670
6687
|
readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
|
|
6671
|
-
|
|
6672
|
-
const rows = await readDeviceStoreBatch
|
|
6688
|
+
readDeviceStoreBatch: async (deviceIds) => {
|
|
6689
|
+
const rows = await raw.readDeviceStoreBatch(deviceIds);
|
|
6673
6690
|
const out = /* @__PURE__ */ new Map();
|
|
6674
6691
|
for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
|
|
6675
6692
|
return out;
|
|
6676
|
-
}
|
|
6693
|
+
},
|
|
6677
6694
|
writeAddonStore: async (patch) => {
|
|
6678
6695
|
await writeAddon(patch);
|
|
6679
6696
|
},
|
|
@@ -7300,7 +7317,7 @@ var BaseAddon = class {
|
|
|
7300
7317
|
if (!ctx) return;
|
|
7301
7318
|
const addonId = ctx.id;
|
|
7302
7319
|
setImmediate(() => {
|
|
7303
|
-
ctx.api.addons
|
|
7320
|
+
ctx.api.addons.restartAddon.mutate({ addonId }).then(() => {
|
|
7304
7321
|
ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
|
|
7305
7322
|
}).catch((err) => {
|
|
7306
7323
|
ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -7500,7 +7517,7 @@ var BaseAddon = class {
|
|
|
7500
7517
|
return this._addonSecretTree;
|
|
7501
7518
|
}
|
|
7502
7519
|
deviceSecretTree() {
|
|
7503
|
-
this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
|
|
7520
|
+
this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
|
|
7504
7521
|
return this._deviceSecretTree;
|
|
7505
7522
|
}
|
|
7506
7523
|
/**
|
|
@@ -7576,42 +7593,135 @@ var BaseAddon = class {
|
|
|
7576
7593
|
this[timerOf] = timer;
|
|
7577
7594
|
}
|
|
7578
7595
|
/**
|
|
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
|
|
7596
|
+
* Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
|
|
7597
|
+
* settings-store outage doesn't propagate up into `initialize()` and leave the
|
|
7583
7598
|
* addon permanently broken.
|
|
7584
7599
|
*
|
|
7585
|
-
*
|
|
7586
|
-
*
|
|
7587
|
-
* `
|
|
7600
|
+
* Two kinds of outage, two budgets:
|
|
7601
|
+
* - the store's own restart fingerprints (`SqliteSettingsBackend not
|
|
7602
|
+
* initialized`, `provider not available`): a short budget, then the addon
|
|
7603
|
+
* falls back to `{}` (defaults) - its first successful patch rehydrates;
|
|
7604
|
+
* - a TRANSPORT failure (the hub is restarting while this runner boots:
|
|
7605
|
+
* `discovery deadline exceeded`, `parent could not route`, a refused or
|
|
7606
|
+
* timed-out connection). Since D870 a runner's settings read THROWS instead
|
|
7607
|
+
* of answering `{}`, so without a budget a hub restart ends in a respawn
|
|
7608
|
+
* loop. A long, bounded budget; only after it the error propagates - the
|
|
7609
|
+
* addon never runs on defaults for a store it could not ask.
|
|
7610
|
+
* Anything else propagates immediately so real bugs surface.
|
|
7588
7611
|
*/
|
|
7589
7612
|
async readAddonStoreWithRetry() {
|
|
7590
7613
|
const settings = this.rawSettings();
|
|
7591
7614
|
if (!settings) return {};
|
|
7592
|
-
const delaysMs = [
|
|
7593
|
-
150,
|
|
7594
|
-
350,
|
|
7595
|
-
600,
|
|
7596
|
-
900
|
|
7597
|
-
];
|
|
7598
7615
|
let lastErr;
|
|
7599
|
-
for (let attempt = 0
|
|
7616
|
+
for (let attempt = 0;; attempt++) try {
|
|
7600
7617
|
const stored = await settings.readAddonStore() ?? {};
|
|
7601
7618
|
this.settingsStoreReady = true;
|
|
7602
7619
|
return stored;
|
|
7603
7620
|
} catch (err) {
|
|
7604
7621
|
lastErr = err;
|
|
7605
|
-
const
|
|
7606
|
-
if (
|
|
7607
|
-
|
|
7608
|
-
|
|
7622
|
+
const kind = classifyStoreError(err);
|
|
7623
|
+
if (kind === "permanent") throw err;
|
|
7624
|
+
const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
|
|
7625
|
+
if (delay === void 0) {
|
|
7626
|
+
if (kind === "transport") throw err;
|
|
7627
|
+
break;
|
|
7628
|
+
}
|
|
7629
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
7609
7630
|
}
|
|
7610
7631
|
this.settingsStoreReady = false;
|
|
7611
7632
|
this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
|
|
7612
7633
|
return {};
|
|
7613
7634
|
}
|
|
7614
7635
|
};
|
|
7636
|
+
/** The settings store's own restart fingerprints. */
|
|
7637
|
+
var RESTART_RETRY_DELAYS_MS = [
|
|
7638
|
+
150,
|
|
7639
|
+
350,
|
|
7640
|
+
600,
|
|
7641
|
+
900
|
|
7642
|
+
];
|
|
7643
|
+
/** A hub restarting under a booting runner: ~100 s in total. */
|
|
7644
|
+
var TRANSPORT_RETRY_DELAYS_MS = [
|
|
7645
|
+
150,
|
|
7646
|
+
350,
|
|
7647
|
+
600,
|
|
7648
|
+
900,
|
|
7649
|
+
2e3,
|
|
7650
|
+
5e3,
|
|
7651
|
+
1e4,
|
|
7652
|
+
2e4,
|
|
7653
|
+
3e4,
|
|
7654
|
+
3e4
|
|
7655
|
+
];
|
|
7656
|
+
var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
|
|
7657
|
+
/** Node / socket error codes of a connection that did not happen or broke. */
|
|
7658
|
+
var TRANSPORT_CODES = new Set([
|
|
7659
|
+
"ECONNREFUSED",
|
|
7660
|
+
"ECONNRESET",
|
|
7661
|
+
"ETIMEDOUT",
|
|
7662
|
+
"EPIPE",
|
|
7663
|
+
"EHOSTUNREACH",
|
|
7664
|
+
"ENETUNREACH"
|
|
7665
|
+
]);
|
|
7666
|
+
/** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
|
|
7667
|
+
var UNAVAILABLE_TRPC_CODES = new Set([
|
|
7668
|
+
"TIMEOUT",
|
|
7669
|
+
"SERVICE_UNAVAILABLE",
|
|
7670
|
+
"BAD_GATEWAY",
|
|
7671
|
+
"GATEWAY_TIMEOUT"
|
|
7672
|
+
]);
|
|
7673
|
+
/** The exact phrases OUR transport raises when it cannot reach the hub. */
|
|
7674
|
+
var TRANSPORT_PHRASES = [
|
|
7675
|
+
"discovery deadline exceeded",
|
|
7676
|
+
"parent could not route child unowned cap call",
|
|
7677
|
+
"UDS request timed out after",
|
|
7678
|
+
"channel closed"
|
|
7679
|
+
];
|
|
7680
|
+
/** Moleculer error `type`s of a call that could not be served right now. */
|
|
7681
|
+
var MOLECULER_TRANSIENT_TYPES = new Set([
|
|
7682
|
+
"REQUEST_TIMEOUT",
|
|
7683
|
+
"SERVICE_NOT_FOUND",
|
|
7684
|
+
"SERVICE_NOT_AVAILABLE",
|
|
7685
|
+
"REQUEST_REJECTED",
|
|
7686
|
+
"BROKER_DISCONNECTED"
|
|
7687
|
+
]);
|
|
7688
|
+
function stringProp(value, key) {
|
|
7689
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7690
|
+
const found = Reflect.get(value, key);
|
|
7691
|
+
return typeof found === "string" ? found : void 0;
|
|
7692
|
+
}
|
|
7693
|
+
/** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
|
|
7694
|
+
function errorCode(err) {
|
|
7695
|
+
const out = {};
|
|
7696
|
+
const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
|
|
7697
|
+
const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
|
|
7698
|
+
const node = stringProp(err, "code") ?? stringProp(cause, "code");
|
|
7699
|
+
if (node !== void 0) out.node = node;
|
|
7700
|
+
const trpc = stringProp(data, "code");
|
|
7701
|
+
if (trpc !== void 0) out.trpc = trpc;
|
|
7702
|
+
return out;
|
|
7703
|
+
}
|
|
7704
|
+
/**
|
|
7705
|
+
* Is a settings read failure worth retrying, and with which budget? Only
|
|
7706
|
+
* EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
|
|
7707
|
+
* one of the exact phrases our own transport raises. A message that merely
|
|
7708
|
+
* contains "timed out" or "unreachable" is not evidence of anything, and
|
|
7709
|
+
* `settings store not wired` is a wiring bug that retrying cannot fix.
|
|
7710
|
+
*/
|
|
7711
|
+
function classifyStoreError(err) {
|
|
7712
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7713
|
+
if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
|
|
7714
|
+
const code = errorCode(err);
|
|
7715
|
+
if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
|
|
7716
|
+
if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
|
|
7717
|
+
if (typeof err === "object" && err !== null) {
|
|
7718
|
+
if (Reflect.get(err, "retryable") === true) return "transport";
|
|
7719
|
+
const type = stringProp(err, "type");
|
|
7720
|
+
if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
|
|
7721
|
+
}
|
|
7722
|
+
if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
|
|
7723
|
+
return "permanent";
|
|
7724
|
+
}
|
|
7615
7725
|
/**
|
|
7616
7726
|
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
7617
7727
|
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
@@ -7701,6 +7811,89 @@ var PeerBytesTicketSchema = object({
|
|
|
7701
7811
|
*/
|
|
7702
7812
|
declaredBytes: number().int().nonnegative().nullable()
|
|
7703
7813
|
});
|
|
7814
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
7815
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
7816
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
7817
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
7818
|
+
var RECORD_CLASSES = [
|
|
7819
|
+
"config",
|
|
7820
|
+
"ledger",
|
|
7821
|
+
"audit",
|
|
7822
|
+
"mirror",
|
|
7823
|
+
"ephemeral"
|
|
7824
|
+
];
|
|
7825
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
7826
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
7827
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
7828
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
7829
|
+
/**
|
|
7830
|
+
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
7831
|
+
* Zod row schema that describes it, and the helper that turns the two into the
|
|
7832
|
+
* JSON the engine stores.
|
|
7833
|
+
*
|
|
7834
|
+
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
7835
|
+
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
7836
|
+
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
7837
|
+
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
7838
|
+
* never a field name (D879).
|
|
7839
|
+
*/
|
|
7840
|
+
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
7841
|
+
var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
|
|
7842
|
+
var RecordIndexSchema = object({
|
|
7843
|
+
name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
7844
|
+
fields: array(RecordFieldPathSchema).min(1).max(4),
|
|
7845
|
+
unique: boolean().optional()
|
|
7846
|
+
}).strict();
|
|
7847
|
+
var RecordRetentionSchema = union([
|
|
7848
|
+
object({
|
|
7849
|
+
ttlMs: number().int().positive(),
|
|
7850
|
+
from: _enum(["at", "write"])
|
|
7851
|
+
}).strict(),
|
|
7852
|
+
object({
|
|
7853
|
+
maxRows: number().int().positive().optional(),
|
|
7854
|
+
maxRowsPerDevice: number().int().positive().optional()
|
|
7855
|
+
}).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
|
|
7856
|
+
object({
|
|
7857
|
+
owner: string().min(1),
|
|
7858
|
+
reason: string().min(1)
|
|
7859
|
+
}).strict(),
|
|
7860
|
+
object({ none: string().min(1) }).strict()
|
|
7861
|
+
]);
|
|
7862
|
+
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
7863
|
+
var EntryDescriptorSchema = object({
|
|
7864
|
+
title: string().min(1),
|
|
7865
|
+
/** The record body field shown as the entry's name. */
|
|
7866
|
+
nameField: string().min(1),
|
|
7867
|
+
/** JSON Schema of the entry body; `secret: true` marks a password field. */
|
|
7868
|
+
fields: record(string(), unknown())
|
|
7869
|
+
}).strict();
|
|
7870
|
+
/** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
|
|
7871
|
+
var RecordKindDeclarationSchema = object({
|
|
7872
|
+
database: _enum(RECORD_DATABASES).default("settings"),
|
|
7873
|
+
class: _enum(RECORD_CLASSES),
|
|
7874
|
+
writeTrigger: string().min(1),
|
|
7875
|
+
device: _enum([
|
|
7876
|
+
"required",
|
|
7877
|
+
"optional",
|
|
7878
|
+
"none"
|
|
7879
|
+
]).default("none"),
|
|
7880
|
+
at: _enum(["required", "none"]).default("none"),
|
|
7881
|
+
indexes: array(RecordIndexSchema).max(8).default([]),
|
|
7882
|
+
retention: RecordRetentionSchema,
|
|
7883
|
+
deviceRemoval: _enum(["delete", "none"]).default("none"),
|
|
7884
|
+
maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
|
|
7885
|
+
/** Derived from the Zod schema; never written by hand. */
|
|
7886
|
+
secretPaths: array(string().min(1)).default([]),
|
|
7887
|
+
/** The id key of each array that carries a secret (`{ headers: 'name' }`). */
|
|
7888
|
+
secretArrayKeys: record(string(), string()).default({}),
|
|
7889
|
+
/** The legacy table this kind adopts from; the boot guard reads it (D853: no migration code). */
|
|
7890
|
+
adoptsFrom: string().min(1).optional(),
|
|
7891
|
+
/** Kinds that must agree through `transact` sit in one file under one family name. */
|
|
7892
|
+
transactFamily: string().min(1).optional(),
|
|
7893
|
+
entry: EntryDescriptorSchema.optional()
|
|
7894
|
+
}).strict();
|
|
7895
|
+
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
7896
|
+
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
7704
7897
|
/** Shared Zod schemas used across streaming capabilities. */
|
|
7705
7898
|
var CamProfileSchema = _enum([
|
|
7706
7899
|
"high",
|
|
@@ -8188,7 +8381,7 @@ object({
|
|
|
8188
8381
|
params: record(string(), union([string(), number()])).optional()
|
|
8189
8382
|
});
|
|
8190
8383
|
//#endregion
|
|
8191
|
-
//#region ../types/dist/err-msg
|
|
8384
|
+
//#region ../types/dist/err-msg--2RrYiLO.mjs
|
|
8192
8385
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
8193
8386
|
DeviceType["Camera"] = "camera";
|
|
8194
8387
|
DeviceType["Hub"] = "hub";
|
|
@@ -8457,6 +8650,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
8457
8650
|
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
8458
8651
|
* real Switch device for UI rendering / export adapters. */
|
|
8459
8652
|
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
8653
|
+
/** A smart plug: a `switch` whose source says it is an outlet (HA
|
|
8654
|
+
* `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
|
|
8655
|
+
DeviceRole["Outlet"] = "outlet";
|
|
8460
8656
|
/** Generic motion / occupancy / moving event source. Distinct from
|
|
8461
8657
|
* the camera accessory PirSensor role: that one is a camera child;
|
|
8462
8658
|
* this is a standalone HA / 3rd-party motion sensor. */
|
|
@@ -8534,7 +8730,7 @@ function errMsg(err) {
|
|
|
8534
8730
|
return String(err);
|
|
8535
8731
|
}
|
|
8536
8732
|
//#endregion
|
|
8537
|
-
//#region ../types/dist/composition-
|
|
8733
|
+
//#region ../types/dist/composition-DOwQB_Ua.mjs
|
|
8538
8734
|
/**
|
|
8539
8735
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
8540
8736
|
*
|
|
@@ -11995,11 +12191,10 @@ var ClipModelMetaSchema = object({
|
|
|
11995
12191
|
/**
|
|
11996
12192
|
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
11997
12193
|
* 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.
|
|
12194
|
+
* against each other, which for CLIP means never across families. Every
|
|
12195
|
+
* model gets its own.
|
|
12001
12196
|
*/
|
|
12002
|
-
vectorIndex: string().regex(/^object-clip
|
|
12197
|
+
vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
|
|
12003
12198
|
/**
|
|
12004
12199
|
* The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
|
|
12005
12200
|
* must be unique per tokenizer: every model's siblings land in ONE flat
|
|
@@ -15652,6 +15847,41 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
15652
15847
|
keyField: string()
|
|
15653
15848
|
}).optional()
|
|
15654
15849
|
})) });
|
|
15850
|
+
/**
|
|
15851
|
+
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
15852
|
+
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
15853
|
+
*
|
|
15854
|
+
* The owner runs it on its own rows, through its own write path; an operator
|
|
15855
|
+
* script (`scripts/hub/migrate-raw-secrets.ts`) calls the owner's cap method.
|
|
15856
|
+
* Dry run (`apply: false`) reports what WOULD move and changes nothing.
|
|
15857
|
+
*/
|
|
15858
|
+
var RawSecretMigrationInputSchema = object({
|
|
15859
|
+
/** `false` (the default of the script) = report only. */
|
|
15860
|
+
apply: boolean() });
|
|
15861
|
+
var RawSecretMigrationReportSchema = object({
|
|
15862
|
+
apply: boolean(),
|
|
15863
|
+
/** Rows that can carry a secret (a profile, an enrolment). */
|
|
15864
|
+
rows: number().int(),
|
|
15865
|
+
/** Rows whose secret was plaintext when the run started. */
|
|
15866
|
+
raw: number().int(),
|
|
15867
|
+
/** Rows that already held a keychain marker when the run started. */
|
|
15868
|
+
markers: number().int(),
|
|
15869
|
+
/** Plaintext rows moved by THIS run (always 0 on a dry run). */
|
|
15870
|
+
migrated: number().int(),
|
|
15871
|
+
/** Plaintext rows left in place because the move or its read-back failed. */
|
|
15872
|
+
failed: number().int(),
|
|
15873
|
+
/** After the run: rows still holding plaintext. */
|
|
15874
|
+
remainingRaw: number().int(),
|
|
15875
|
+
/** After the run: markers the keychain could not resolve right now. */
|
|
15876
|
+
unresolvedMarkers: number().int(),
|
|
15877
|
+
/**
|
|
15878
|
+
* A second, redundant copy of the secrets' rows that the run found (and, on
|
|
15879
|
+
* `apply`, deleted after verifying the first). Owners with a single copy omit it.
|
|
15880
|
+
*/
|
|
15881
|
+
redundantCopies: number().int().optional(),
|
|
15882
|
+
/** True when nothing is raw and every marker resolves. */
|
|
15883
|
+
verified: boolean()
|
|
15884
|
+
});
|
|
15655
15885
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
15656
15886
|
* agent is the reason placement is not fixed to the hub. */
|
|
15657
15887
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -15878,6 +16108,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
15878
16108
|
}), SettingsUpdateResultSchema, {
|
|
15879
16109
|
kind: "mutation",
|
|
15880
16110
|
auth: "admin"
|
|
16111
|
+
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
16112
|
+
kind: "mutation",
|
|
16113
|
+
auth: "admin"
|
|
15881
16114
|
}), method(object({}), object({ libs: array(object({
|
|
15882
16115
|
filePath: string(),
|
|
15883
16116
|
content: string()
|
|
@@ -16141,6 +16374,347 @@ id: union([string(), number()]) }), {
|
|
|
16141
16374
|
auth: "admin"
|
|
16142
16375
|
});
|
|
16143
16376
|
/**
|
|
16377
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
16378
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
16379
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
16380
|
+
*
|
|
16381
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
16382
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
16383
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
16384
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
16385
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
16386
|
+
* addons go through the owner's capability.
|
|
16387
|
+
*
|
|
16388
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
16389
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
16390
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
16391
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
16392
|
+
* row's own ref.
|
|
16393
|
+
*
|
|
16394
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
16395
|
+
*/
|
|
16396
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
16397
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
16398
|
+
var RecordBodySchema = record(string(), unknown());
|
|
16399
|
+
var RecordInputSchema = object({
|
|
16400
|
+
id: RecordIdSchema,
|
|
16401
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
16402
|
+
deviceId: number().int().optional(),
|
|
16403
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
16404
|
+
at: number().int().optional(),
|
|
16405
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
16406
|
+
expiresAt: number().int().optional(),
|
|
16407
|
+
data: RecordBodySchema
|
|
16408
|
+
});
|
|
16409
|
+
var RecordViewSchema = object({
|
|
16410
|
+
id: string(),
|
|
16411
|
+
deviceId: number().int().nullable(),
|
|
16412
|
+
at: number().nullable(),
|
|
16413
|
+
expiresAt: number().nullable(),
|
|
16414
|
+
rev: number().int(),
|
|
16415
|
+
updatedAt: number(),
|
|
16416
|
+
data: RecordBodySchema
|
|
16417
|
+
});
|
|
16418
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
16419
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
16420
|
+
string(),
|
|
16421
|
+
number(),
|
|
16422
|
+
boolean(),
|
|
16423
|
+
object({ in: array(union([
|
|
16424
|
+
string(),
|
|
16425
|
+
number(),
|
|
16426
|
+
boolean()
|
|
16427
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
16428
|
+
object({
|
|
16429
|
+
gt: union([string(), number()]).optional(),
|
|
16430
|
+
gte: union([string(), number()]).optional(),
|
|
16431
|
+
lt: union([string(), number()]).optional(),
|
|
16432
|
+
lte: union([string(), number()]).optional()
|
|
16433
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
16434
|
+
]));
|
|
16435
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
16436
|
+
var TimeWindowShape = {
|
|
16437
|
+
deviceId: number().int().optional(),
|
|
16438
|
+
atFrom: number().optional(),
|
|
16439
|
+
atTo: number().optional()
|
|
16440
|
+
};
|
|
16441
|
+
var RecordQueryInputSchema = object({
|
|
16442
|
+
...KindShape,
|
|
16443
|
+
...TimeWindowShape,
|
|
16444
|
+
where: RecordWhereSchema.optional(),
|
|
16445
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
16446
|
+
order: _enum(["at", "id"]).optional(),
|
|
16447
|
+
desc: boolean().optional(),
|
|
16448
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
16449
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
16450
|
+
cursor: string().min(1).optional()
|
|
16451
|
+
});
|
|
16452
|
+
var RecordQueryResultSchema = object({
|
|
16453
|
+
records: array(RecordViewSchema),
|
|
16454
|
+
nextCursor: string().nullable()
|
|
16455
|
+
});
|
|
16456
|
+
var RecordKindStateSchema = _enum([
|
|
16457
|
+
"created",
|
|
16458
|
+
"unchanged",
|
|
16459
|
+
"updated"
|
|
16460
|
+
]);
|
|
16461
|
+
var DeclareKindResultSchema = object({
|
|
16462
|
+
kindId: number().int(),
|
|
16463
|
+
state: RecordKindStateSchema,
|
|
16464
|
+
createdIndexes: array(string()),
|
|
16465
|
+
/** The boot guard: the kind is empty while its legacy table still holds rows. */
|
|
16466
|
+
adoptionPending: object({
|
|
16467
|
+
legacyTable: string(),
|
|
16468
|
+
legacyRows: number().int()
|
|
16469
|
+
}).nullable()
|
|
16470
|
+
});
|
|
16471
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
16472
|
+
var SecretRefsList = array(string());
|
|
16473
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
16474
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
16475
|
+
kind: "mutation",
|
|
16476
|
+
auth: "admin"
|
|
16477
|
+
}), method(object({
|
|
16478
|
+
...KindShape,
|
|
16479
|
+
id: RecordIdSchema
|
|
16480
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
16481
|
+
...KindShape,
|
|
16482
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16483
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
16484
|
+
...KindShape,
|
|
16485
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16486
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
16487
|
+
...KindShape,
|
|
16488
|
+
record: RecordInputSchema,
|
|
16489
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
16490
|
+
ifRev: number().int().min(0).optional()
|
|
16491
|
+
}), object({
|
|
16492
|
+
rev: number().int(),
|
|
16493
|
+
releasedRefs: SecretRefsList
|
|
16494
|
+
}), {
|
|
16495
|
+
kind: "mutation",
|
|
16496
|
+
auth: "admin"
|
|
16497
|
+
}), method(object({
|
|
16498
|
+
...KindShape,
|
|
16499
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16500
|
+
}), object({
|
|
16501
|
+
count: number().int(),
|
|
16502
|
+
releasedRefs: SecretRefsList
|
|
16503
|
+
}), {
|
|
16504
|
+
kind: "mutation",
|
|
16505
|
+
auth: "admin"
|
|
16506
|
+
}), method(object({
|
|
16507
|
+
...KindShape,
|
|
16508
|
+
id: RecordIdSchema,
|
|
16509
|
+
data: RecordBodySchema,
|
|
16510
|
+
at: number().int().nullable().optional(),
|
|
16511
|
+
expiresAt: number().int().nullable().optional(),
|
|
16512
|
+
ifRev: number().int().min(1).optional()
|
|
16513
|
+
}), object({
|
|
16514
|
+
rev: number().int(),
|
|
16515
|
+
releasedRefs: SecretRefsList
|
|
16516
|
+
}), {
|
|
16517
|
+
kind: "mutation",
|
|
16518
|
+
auth: "admin"
|
|
16519
|
+
}), method(object({
|
|
16520
|
+
...KindShape,
|
|
16521
|
+
id: RecordIdSchema
|
|
16522
|
+
}), object({
|
|
16523
|
+
deleted: boolean(),
|
|
16524
|
+
secretRefs: SecretRefsList
|
|
16525
|
+
}), {
|
|
16526
|
+
kind: "mutation",
|
|
16527
|
+
auth: "admin"
|
|
16528
|
+
}), method(object({
|
|
16529
|
+
...KindShape,
|
|
16530
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16531
|
+
}), object({
|
|
16532
|
+
deleted: number().int(),
|
|
16533
|
+
secretRefs: SecretRefsList
|
|
16534
|
+
}), {
|
|
16535
|
+
kind: "mutation",
|
|
16536
|
+
auth: "admin"
|
|
16537
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
16538
|
+
...KindShape,
|
|
16539
|
+
...TimeWindowShape,
|
|
16540
|
+
where: RecordWhereSchema.optional()
|
|
16541
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
16542
|
+
...KindShape,
|
|
16543
|
+
deviceId: number().int().optional(),
|
|
16544
|
+
from: number(),
|
|
16545
|
+
to: number(),
|
|
16546
|
+
bucketMs: number().int().positive()
|
|
16547
|
+
}), object({ buckets: array(object({
|
|
16548
|
+
bucketStart: number(),
|
|
16549
|
+
count: number().int()
|
|
16550
|
+
})) }), { auth: "admin" }), method(object({
|
|
16551
|
+
...KindShape,
|
|
16552
|
+
op: _enum([
|
|
16553
|
+
"sum",
|
|
16554
|
+
"min",
|
|
16555
|
+
"max",
|
|
16556
|
+
"count"
|
|
16557
|
+
]),
|
|
16558
|
+
field: RecordFieldPathSchema.optional(),
|
|
16559
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
16560
|
+
where: RecordWhereSchema.optional()
|
|
16561
|
+
}), object({ rows: array(object({
|
|
16562
|
+
group: union([
|
|
16563
|
+
string(),
|
|
16564
|
+
number(),
|
|
16565
|
+
_null()
|
|
16566
|
+
]),
|
|
16567
|
+
value: number().nullable()
|
|
16568
|
+
})) }), { auth: "admin" }), method(object({
|
|
16569
|
+
...KindShape,
|
|
16570
|
+
deviceId: number().int().optional(),
|
|
16571
|
+
atBefore: number().optional(),
|
|
16572
|
+
where: RecordWhereSchema.optional()
|
|
16573
|
+
}), object({
|
|
16574
|
+
deleted: number().int(),
|
|
16575
|
+
secretRefs: SecretRefsList
|
|
16576
|
+
}), {
|
|
16577
|
+
kind: "mutation",
|
|
16578
|
+
auth: "admin"
|
|
16579
|
+
}), method(object({
|
|
16580
|
+
deviceId: number().int(),
|
|
16581
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
16582
|
+
}), object({
|
|
16583
|
+
deleted: record(string(), number().int()),
|
|
16584
|
+
secretRefs: SecretRefsList
|
|
16585
|
+
}), {
|
|
16586
|
+
kind: "mutation",
|
|
16587
|
+
auth: "admin"
|
|
16588
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
16589
|
+
kind: "mutation",
|
|
16590
|
+
auth: "admin"
|
|
16591
|
+
});
|
|
16592
|
+
/** One table of the settings database. */
|
|
16593
|
+
var StorageTableSchema = object({
|
|
16594
|
+
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
16595
|
+
name: string(),
|
|
16596
|
+
/** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
|
|
16597
|
+
owner: string().nullable(),
|
|
16598
|
+
rows: number().int().nonnegative(),
|
|
16599
|
+
/** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
|
|
16600
|
+
bytes: number().int().nonnegative().nullable(),
|
|
16601
|
+
/** Declared by an addon since this engine process started (a running addon claims it). */
|
|
16602
|
+
declaredThisBoot: boolean()
|
|
16603
|
+
});
|
|
16604
|
+
/** One declared record kind (D881) as System -> Data shows it. */
|
|
16605
|
+
var StorageKindSchema = object({
|
|
16606
|
+
owner: string(),
|
|
16607
|
+
kind: string(),
|
|
16608
|
+
database: _enum(["settings", "events"]),
|
|
16609
|
+
class: string(),
|
|
16610
|
+
state: _enum([
|
|
16611
|
+
"declared",
|
|
16612
|
+
"orphaned",
|
|
16613
|
+
"dropped"
|
|
16614
|
+
]),
|
|
16615
|
+
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
16616
|
+
rows: number().int().nonnegative().nullable(),
|
|
16617
|
+
/** Rows x the average size of a sample; null for an empty kind. */
|
|
16618
|
+
bytesEstimate: number().int().nonnegative().nullable(),
|
|
16619
|
+
writeTrigger: string(),
|
|
16620
|
+
retention: RecordRetentionSchema,
|
|
16621
|
+
/** `true` when the kind is an entry kind (the generic entries UI lists it). */
|
|
16622
|
+
entry: boolean(),
|
|
16623
|
+
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
16624
|
+
declaration: RecordKindDeclarationSchema
|
|
16625
|
+
});
|
|
16626
|
+
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
16627
|
+
collection: string().min(1),
|
|
16628
|
+
expectRows: number().int().nonnegative()
|
|
16629
|
+
}), object({
|
|
16630
|
+
dropped: literal(true),
|
|
16631
|
+
rows: number().int().nonnegative()
|
|
16632
|
+
}), {
|
|
16633
|
+
kind: "mutation",
|
|
16634
|
+
auth: "admin"
|
|
16635
|
+
}), method(object({
|
|
16636
|
+
owner: string().min(1).optional(),
|
|
16637
|
+
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
16638
|
+
counts: boolean().optional()
|
|
16639
|
+
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
16640
|
+
owner: string().min(1),
|
|
16641
|
+
kind: RecordKindNameSchema
|
|
16642
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
16643
|
+
owner: string().min(1),
|
|
16644
|
+
kind: RecordKindNameSchema,
|
|
16645
|
+
records: array(RecordInputSchema).max(RECORD_BATCH_MAX),
|
|
16646
|
+
/** The LAST batch: the copy is complete, the boot guard stops asking. */
|
|
16647
|
+
complete: boolean().optional()
|
|
16648
|
+
}).refine((input) => input.records.length > 0 || input.complete === true, { message: "records must be non-empty unless complete is true" }), object({ adopted: number().int() }), {
|
|
16649
|
+
kind: "mutation",
|
|
16650
|
+
auth: "admin"
|
|
16651
|
+
}), method(object({
|
|
16652
|
+
collection: string().min(1),
|
|
16653
|
+
cursor: string().optional(),
|
|
16654
|
+
limit: number().int().min(1).max(1e3).optional()
|
|
16655
|
+
}), object({
|
|
16656
|
+
rows: array(object({
|
|
16657
|
+
id: string(),
|
|
16658
|
+
data: record(string(), unknown())
|
|
16659
|
+
})),
|
|
16660
|
+
nextCursor: string().nullable()
|
|
16661
|
+
}), { auth: "admin" }), method(object({ collection: string().min(1) }), object({ adopted: boolean() }), { auth: "admin" });
|
|
16662
|
+
/**
|
|
16663
|
+
* record-entries - the ONE admin surface for every list of user-created entries
|
|
16664
|
+
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
16665
|
+
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
16666
|
+
*
|
|
16667
|
+
* The UI is generic. `listEntryKinds` hands it, per kind, the JSON Schema of
|
|
16668
|
+
* the entry body with `secret: true` on every credential field, so a password
|
|
16669
|
+
* field is derived from the schema and never from a name. `putEntry` takes the
|
|
16670
|
+
* redaction token for an untouched secret (the stored value is kept), a string
|
|
16671
|
+
* for a new one, and the clear instruction to remove one. A read never carries
|
|
16672
|
+
* a secret value, and a locked keychain refuses the write with
|
|
16673
|
+
* `records/secret-unavailable` instead of storing plaintext.
|
|
16674
|
+
*
|
|
16675
|
+
* Hub-only, served in-process (it acts for each entry's owner on the keychain).
|
|
16676
|
+
*/
|
|
16677
|
+
var EntryKindInfoSchema = object({
|
|
16678
|
+
owner: string(),
|
|
16679
|
+
kind: RecordKindNameSchema,
|
|
16680
|
+
title: string(),
|
|
16681
|
+
nameField: string(),
|
|
16682
|
+
/** JSON Schema of the body; `secret: true` marks a password field. */
|
|
16683
|
+
fields: record(string(), unknown()),
|
|
16684
|
+
secretPaths: array(string()),
|
|
16685
|
+
rows: number().int().nonnegative()
|
|
16686
|
+
});
|
|
16687
|
+
var EntryViewSchema = object({
|
|
16688
|
+
id: RecordIdSchema,
|
|
16689
|
+
name: string(),
|
|
16690
|
+
rev: number().int(),
|
|
16691
|
+
updatedAt: number(),
|
|
16692
|
+
/** Secret fields hold the redaction token. */
|
|
16693
|
+
data: RecordBodySchema
|
|
16694
|
+
});
|
|
16695
|
+
var EntryAddress = {
|
|
16696
|
+
owner: string().min(1),
|
|
16697
|
+
kind: RecordKindNameSchema
|
|
16698
|
+
};
|
|
16699
|
+
method(object({}), object({ kinds: array(EntryKindInfoSchema) }), { auth: "admin" }), method(object({ ...EntryAddress }), object({ entries: array(EntryViewSchema) }), { auth: "admin" }), method(object({
|
|
16700
|
+
...EntryAddress,
|
|
16701
|
+
id: RecordIdSchema.optional(),
|
|
16702
|
+
data: RecordBodySchema,
|
|
16703
|
+
ifRev: number().int().min(1).optional()
|
|
16704
|
+
}), object({
|
|
16705
|
+
id: string(),
|
|
16706
|
+
rev: number().int()
|
|
16707
|
+
}), {
|
|
16708
|
+
kind: "mutation",
|
|
16709
|
+
auth: "admin"
|
|
16710
|
+
}), method(object({
|
|
16711
|
+
...EntryAddress,
|
|
16712
|
+
id: RecordIdSchema
|
|
16713
|
+
}), object({ deleted: boolean() }), {
|
|
16714
|
+
kind: "mutation",
|
|
16715
|
+
auth: "admin"
|
|
16716
|
+
});
|
|
16717
|
+
/**
|
|
16144
16718
|
* `keychain` — the hub's encrypted store of secret values (D787).
|
|
16145
16719
|
*
|
|
16146
16720
|
* Settings blobs never hold a secret: they hold a marker
|
|
@@ -16681,6 +17255,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
|
|
|
16681
17255
|
kind: "mutation",
|
|
16682
17256
|
auth: "admin",
|
|
16683
17257
|
caller: "required"
|
|
17258
|
+
}), method(object({
|
|
17259
|
+
ref: string().min(1),
|
|
17260
|
+
value: string().min(1)
|
|
17261
|
+
}), object({
|
|
17262
|
+
ref: string(),
|
|
17263
|
+
version: number().int()
|
|
17264
|
+
}), {
|
|
17265
|
+
kind: "mutation",
|
|
17266
|
+
auth: "admin",
|
|
17267
|
+
caller: "required"
|
|
16684
17268
|
}), method(object({ refs: array(string().min(1)).max(1e3) }), object({
|
|
16685
17269
|
deleted: array(string()).readonly(),
|
|
16686
17270
|
kept: array(object({
|
|
@@ -39175,6 +39759,9 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
|
|
|
39175
39759
|
}), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
|
|
39176
39760
|
kind: "mutation",
|
|
39177
39761
|
auth: "admin"
|
|
39762
|
+
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
39763
|
+
kind: "mutation",
|
|
39764
|
+
auth: "admin"
|
|
39178
39765
|
}), method(ProfileRefInputSchema, _void(), {
|
|
39179
39766
|
kind: "mutation",
|
|
39180
39767
|
auth: "admin"
|
|
@@ -49614,6 +50201,41 @@ var ServerRollbackInfoSchema = object({
|
|
|
49614
50201
|
atMs: number(),
|
|
49615
50202
|
reason: string()
|
|
49616
50203
|
});
|
|
50204
|
+
/**
|
|
50205
|
+
* The runner boot check (D877): the runners this node PLANNED (hub: one per
|
|
50206
|
+
* `resolveRunnerId` over its forked addons; agent: the runners its process
|
|
50207
|
+
* service holds, because only the hub plans) against the states the process
|
|
50208
|
+
* service reports. Kept in memory, recomputed by the same one-shot check after
|
|
50209
|
+
* every boot, served on `getServerPackageStatus`.
|
|
50210
|
+
*
|
|
50211
|
+
* `failed` is the D6 breaker's terminal state, `refused` a deterministic Init
|
|
50212
|
+
* refusal (an addon that said "I will not start, and here is why"), `missing` a
|
|
50213
|
+
* planned runner the process service has no entry for.
|
|
50214
|
+
*/
|
|
50215
|
+
var RunnerRefusalSchema = object({
|
|
50216
|
+
runnerId: string(),
|
|
50217
|
+
addonIds: array(string()),
|
|
50218
|
+
reason: string()
|
|
50219
|
+
});
|
|
50220
|
+
var RunnerBootCheckSchema = object({
|
|
50221
|
+
checkedAtMs: number(),
|
|
50222
|
+
/** Runners planned for this node. */
|
|
50223
|
+
expected: number(),
|
|
50224
|
+
/** Planned runners whose process is `running`. */
|
|
50225
|
+
running: number(),
|
|
50226
|
+
failed: array(string()),
|
|
50227
|
+
refused: array(RunnerRefusalSchema),
|
|
50228
|
+
missing: array(string()),
|
|
50229
|
+
/**
|
|
50230
|
+
* Runners still on their way: `starting`, or `crashed` waiting on a backoff
|
|
50231
|
+
* respawn. The check is FINAL only when this is empty; until then nothing is
|
|
50232
|
+
* pushed and the verdict is re-evaluated on the next runner state change.
|
|
50233
|
+
* Optional for version skew (absent = settled).
|
|
50234
|
+
*/
|
|
50235
|
+
pending: array(string()).optional(),
|
|
50236
|
+
/** True when the plan is the hub's (`missing` is then meaningful); false on an agent. */
|
|
50237
|
+
planKnown: boolean()
|
|
50238
|
+
});
|
|
49617
50239
|
var ServerPackageStatusSchema = object({
|
|
49618
50240
|
/** Root package name (`@camstack/server` on the hub). */
|
|
49619
50241
|
packageName: string(),
|
|
@@ -49676,7 +50298,14 @@ var ServerPackageStatusSchema = object({
|
|
|
49676
50298
|
* for version skew: an older provider's payload simply omits it, and an
|
|
49677
50299
|
* absent wrapper is unknown — never `native`.
|
|
49678
50300
|
*/
|
|
49679
|
-
wrapper: WrapperIdentitySchema.optional()
|
|
50301
|
+
wrapper: WrapperIdentitySchema.optional(),
|
|
50302
|
+
/**
|
|
50303
|
+
* What this node PLANNED to run against what its process service reports
|
|
50304
|
+
* (D877). Absent until the one-shot boot check has run (~60 s after boot) and
|
|
50305
|
+
* on a provider that predates it. Absent is "not checked yet", never "all
|
|
50306
|
+
* well": a consumer that needs the answer treats it as pending.
|
|
50307
|
+
*/
|
|
50308
|
+
runners: RunnerBootCheckSchema.optional()
|
|
49680
50309
|
});
|
|
49681
50310
|
var ServerUpdateCheckResultSchema = object({
|
|
49682
50311
|
packageName: string(),
|
|
@@ -49727,6 +50356,13 @@ var ServerUpdateOutcomeSchema = _enum([
|
|
|
49727
50356
|
"refused",
|
|
49728
50357
|
"failed"
|
|
49729
50358
|
]);
|
|
50359
|
+
/** One addon the pre-flight found the target version would refuse (D877). */
|
|
50360
|
+
var UpdateBlockerSchema = object({
|
|
50361
|
+
addonId: string(),
|
|
50362
|
+
reason: string(),
|
|
50363
|
+
/** What the operator does about it. */
|
|
50364
|
+
remedy: string()
|
|
50365
|
+
});
|
|
49730
50366
|
var ServerUpdateActionResultSchema = object({
|
|
49731
50367
|
accepted: boolean(),
|
|
49732
50368
|
targetVersion: string().nullable(),
|
|
@@ -49744,14 +50380,27 @@ var ServerUpdateActionResultSchema = object({
|
|
|
49744
50380
|
* The failure itself, verbatim enough to act on (the npm output for a
|
|
49745
50381
|
* staging install). Non-null only for `failed`.
|
|
49746
50382
|
*/
|
|
49747
|
-
error: string().nullable().optional()
|
|
50383
|
+
error: string().nullable().optional(),
|
|
50384
|
+
/**
|
|
50385
|
+
* The addons the target version would refuse to start on this node's live
|
|
50386
|
+
* store (D877), present when the pre-flight refused the update
|
|
50387
|
+
* (`reason: 'addon-would-refuse'`). Optional for version skew.
|
|
50388
|
+
*/
|
|
50389
|
+
blockers: array(UpdateBlockerSchema).optional()
|
|
49748
50390
|
});
|
|
49749
50391
|
method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
|
|
49750
50392
|
kind: "mutation",
|
|
49751
50393
|
auth: "admin"
|
|
49752
|
-
}), method(object({
|
|
49753
|
-
/** Explicit target version; omitted = latest from the registry. */
|
|
49754
|
-
version: string().optional()
|
|
50394
|
+
}), method(object({
|
|
50395
|
+
/** Explicit target version; omitted = latest from the registry. */
|
|
50396
|
+
version: string().optional(),
|
|
50397
|
+
/**
|
|
50398
|
+
* Apply even though the pre-flight (D877) found addons the target would
|
|
50399
|
+
* refuse to start. Off by default: those addons stay down until the
|
|
50400
|
+
* remedy is run, which is the incident this gate exists to prevent.
|
|
50401
|
+
*/
|
|
50402
|
+
acknowledgeRefusals: boolean().optional()
|
|
50403
|
+
}), ServerUpdateActionResultSchema, {
|
|
49755
50404
|
kind: "mutation",
|
|
49756
50405
|
auth: "admin",
|
|
49757
50406
|
timeoutMs: 16 * 6e4
|
|
@@ -52164,7 +52813,9 @@ var ManifestShapeSchema = object({
|
|
|
52164
52813
|
var AddonProcessSchema = object({
|
|
52165
52814
|
pid: number().optional(),
|
|
52166
52815
|
mode: string(),
|
|
52167
|
-
state: string()
|
|
52816
|
+
state: string(),
|
|
52817
|
+
/** Why the runner is `refused` (an addon's own words), when it is (D877). */
|
|
52818
|
+
reason: string().optional()
|
|
52168
52819
|
});
|
|
52169
52820
|
/**
|
|
52170
52821
|
* Per-addon health snapshot from the kernel's AddonHealthMonitor.
|
|
@@ -53138,7 +53789,7 @@ var HvacModeSchema = _enum([
|
|
|
53138
53789
|
"fan_only",
|
|
53139
53790
|
"dry"
|
|
53140
53791
|
]);
|
|
53141
|
-
object({
|
|
53792
|
+
var ClimateControlStatusSchema = object({
|
|
53142
53793
|
/** Active HVAC mode. */
|
|
53143
53794
|
mode: HvacModeSchema,
|
|
53144
53795
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -53179,6 +53830,12 @@ object({
|
|
|
53179
53830
|
/** Horizontal louver swing on/off. Null when the device has no
|
|
53180
53831
|
* horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
|
|
53181
53832
|
swingHorizontal: boolean().nullable(),
|
|
53833
|
+
/** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53834
|
+
minTemp: number().nullable().optional(),
|
|
53835
|
+
/** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53836
|
+
maxTemp: number().nullable().optional(),
|
|
53837
|
+
/** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
|
|
53838
|
+
tempStep: number().positive().nullable().optional(),
|
|
53182
53839
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
53183
53840
|
lastFetchedAt: number()
|
|
53184
53841
|
});
|
|
@@ -55019,7 +55676,19 @@ var TlsStatusSchema = object({
|
|
|
55019
55676
|
sans: array(string()),
|
|
55020
55677
|
caCertPem: string().nullable(),
|
|
55021
55678
|
reissueError: string().nullable(),
|
|
55022
|
-
restartRequired: boolean()
|
|
55679
|
+
restartRequired: boolean(),
|
|
55680
|
+
/** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
|
|
55681
|
+
caMissing: boolean(),
|
|
55682
|
+
/** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
|
|
55683
|
+
localCaConstrained: boolean().nullable(),
|
|
55684
|
+
/** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
|
|
55685
|
+
caReplacedAt: string().nullable(),
|
|
55686
|
+
/** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
|
|
55687
|
+
storedUpload: object({
|
|
55688
|
+
leafFingerprintSha256: string(),
|
|
55689
|
+
validTo: string(),
|
|
55690
|
+
hasCa: boolean()
|
|
55691
|
+
}).nullable()
|
|
55023
55692
|
});
|
|
55024
55693
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
55025
55694
|
/**
|
|
@@ -55075,6 +55744,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
55075
55744
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
55076
55745
|
kind: "mutation",
|
|
55077
55746
|
auth: "admin"
|
|
55747
|
+
}), method(_void(), TlsStatusSchema, {
|
|
55748
|
+
kind: "mutation",
|
|
55749
|
+
auth: "admin"
|
|
55078
55750
|
});
|
|
55079
55751
|
var LockControlStatusSchema = object({
|
|
55080
55752
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
@@ -60227,7 +60899,11 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
60227
60899
|
kind: "mutation",
|
|
60228
60900
|
auth: "admin",
|
|
60229
60901
|
access: "delete"
|
|
60230
|
-
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(
|
|
60902
|
+
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
60903
|
+
kind: "mutation",
|
|
60904
|
+
auth: "admin",
|
|
60905
|
+
access: "create"
|
|
60906
|
+
}), method(object({
|
|
60231
60907
|
userId: string(),
|
|
60232
60908
|
code: string()
|
|
60233
60909
|
}), object({ valid: boolean() }), {
|
|
@@ -62428,6 +63104,12 @@ Object.freeze({
|
|
|
62428
63104
|
addonId: null,
|
|
62429
63105
|
access: "view"
|
|
62430
63106
|
},
|
|
63107
|
+
"coreBlocks.migrateRawSecrets": {
|
|
63108
|
+
capName: "core-blocks",
|
|
63109
|
+
capScope: "system",
|
|
63110
|
+
addonId: null,
|
|
63111
|
+
access: "create"
|
|
63112
|
+
},
|
|
62431
63113
|
"coreBlocks.restart": {
|
|
62432
63114
|
capName: "core-blocks",
|
|
62433
63115
|
capScope: "system",
|
|
@@ -64114,6 +64796,12 @@ Object.freeze({
|
|
|
64114
64796
|
addonId: null,
|
|
64115
64797
|
access: "create"
|
|
64116
64798
|
},
|
|
64799
|
+
"keychainAdmin.importSecret": {
|
|
64800
|
+
capName: "keychain-admin",
|
|
64801
|
+
capScope: "system",
|
|
64802
|
+
addonId: null,
|
|
64803
|
+
access: "create"
|
|
64804
|
+
},
|
|
64117
64805
|
"keychainAdmin.listAudit": {
|
|
64118
64806
|
capName: "keychain-admin",
|
|
64119
64807
|
capScope: "system",
|
|
@@ -64282,6 +64970,12 @@ Object.freeze({
|
|
|
64282
64970
|
addonId: null,
|
|
64283
64971
|
access: "view"
|
|
64284
64972
|
},
|
|
64973
|
+
"llm.migrateRawSecrets": {
|
|
64974
|
+
capName: "llm",
|
|
64975
|
+
capScope: "system",
|
|
64976
|
+
addonId: null,
|
|
64977
|
+
access: "create"
|
|
64978
|
+
},
|
|
64285
64979
|
"llm.resolveModelRef": {
|
|
64286
64980
|
capName: "llm",
|
|
64287
64981
|
capScope: "system",
|
|
@@ -64510,6 +65204,12 @@ Object.freeze({
|
|
|
64510
65204
|
addonId: null,
|
|
64511
65205
|
access: "create"
|
|
64512
65206
|
},
|
|
65207
|
+
"localNetwork.useStoredUploadedCertificate": {
|
|
65208
|
+
capName: "local-network",
|
|
65209
|
+
capScope: "system",
|
|
65210
|
+
addonId: null,
|
|
65211
|
+
access: "create"
|
|
65212
|
+
},
|
|
64513
65213
|
"lockControl.getStatus": {
|
|
64514
65214
|
capName: "lock-control",
|
|
64515
65215
|
capScope: "device",
|
|
@@ -67162,6 +67862,30 @@ Object.freeze({
|
|
|
67162
67862
|
addonId: null,
|
|
67163
67863
|
access: "create"
|
|
67164
67864
|
},
|
|
67865
|
+
"recordEntries.deleteEntry": {
|
|
67866
|
+
capName: "record-entries",
|
|
67867
|
+
capScope: "system",
|
|
67868
|
+
addonId: null,
|
|
67869
|
+
access: "delete"
|
|
67870
|
+
},
|
|
67871
|
+
"recordEntries.listEntries": {
|
|
67872
|
+
capName: "record-entries",
|
|
67873
|
+
capScope: "system",
|
|
67874
|
+
addonId: null,
|
|
67875
|
+
access: "view"
|
|
67876
|
+
},
|
|
67877
|
+
"recordEntries.listEntryKinds": {
|
|
67878
|
+
capName: "record-entries",
|
|
67879
|
+
capScope: "system",
|
|
67880
|
+
addonId: null,
|
|
67881
|
+
access: "view"
|
|
67882
|
+
},
|
|
67883
|
+
"recordEntries.putEntry": {
|
|
67884
|
+
capName: "record-entries",
|
|
67885
|
+
capScope: "system",
|
|
67886
|
+
addonId: null,
|
|
67887
|
+
access: "create"
|
|
67888
|
+
},
|
|
67165
67889
|
"recording.getAvailability": {
|
|
67166
67890
|
capName: "recording",
|
|
67167
67891
|
capScope: "device",
|
|
@@ -67462,6 +68186,102 @@ Object.freeze({
|
|
|
67462
68186
|
addonId: null,
|
|
67463
68187
|
access: "view"
|
|
67464
68188
|
},
|
|
68189
|
+
"records.aggregate": {
|
|
68190
|
+
capName: "records",
|
|
68191
|
+
capScope: "system",
|
|
68192
|
+
addonId: null,
|
|
68193
|
+
access: "view"
|
|
68194
|
+
},
|
|
68195
|
+
"records.allocateId": {
|
|
68196
|
+
capName: "records",
|
|
68197
|
+
capScope: "system",
|
|
68198
|
+
addonId: null,
|
|
68199
|
+
access: "create"
|
|
68200
|
+
},
|
|
68201
|
+
"records.count": {
|
|
68202
|
+
capName: "records",
|
|
68203
|
+
capScope: "system",
|
|
68204
|
+
addonId: null,
|
|
68205
|
+
access: "view"
|
|
68206
|
+
},
|
|
68207
|
+
"records.declareKinds": {
|
|
68208
|
+
capName: "records",
|
|
68209
|
+
capScope: "system",
|
|
68210
|
+
addonId: null,
|
|
68211
|
+
access: "create"
|
|
68212
|
+
},
|
|
68213
|
+
"records.delete": {
|
|
68214
|
+
capName: "records",
|
|
68215
|
+
capScope: "system",
|
|
68216
|
+
addonId: null,
|
|
68217
|
+
access: "delete"
|
|
68218
|
+
},
|
|
68219
|
+
"records.deleteDevice": {
|
|
68220
|
+
capName: "records",
|
|
68221
|
+
capScope: "system",
|
|
68222
|
+
addonId: null,
|
|
68223
|
+
access: "delete"
|
|
68224
|
+
},
|
|
68225
|
+
"records.deleteMany": {
|
|
68226
|
+
capName: "records",
|
|
68227
|
+
capScope: "system",
|
|
68228
|
+
addonId: null,
|
|
68229
|
+
access: "delete"
|
|
68230
|
+
},
|
|
68231
|
+
"records.deleteWhere": {
|
|
68232
|
+
capName: "records",
|
|
68233
|
+
capScope: "system",
|
|
68234
|
+
addonId: null,
|
|
68235
|
+
access: "delete"
|
|
68236
|
+
},
|
|
68237
|
+
"records.get": {
|
|
68238
|
+
capName: "records",
|
|
68239
|
+
capScope: "system",
|
|
68240
|
+
addonId: null,
|
|
68241
|
+
access: "view"
|
|
68242
|
+
},
|
|
68243
|
+
"records.getMany": {
|
|
68244
|
+
capName: "records",
|
|
68245
|
+
capScope: "system",
|
|
68246
|
+
addonId: null,
|
|
68247
|
+
access: "view"
|
|
68248
|
+
},
|
|
68249
|
+
"records.histogram": {
|
|
68250
|
+
capName: "records",
|
|
68251
|
+
capScope: "system",
|
|
68252
|
+
addonId: null,
|
|
68253
|
+
access: "view"
|
|
68254
|
+
},
|
|
68255
|
+
"records.patch": {
|
|
68256
|
+
capName: "records",
|
|
68257
|
+
capScope: "system",
|
|
68258
|
+
addonId: null,
|
|
68259
|
+
access: "create"
|
|
68260
|
+
},
|
|
68261
|
+
"records.put": {
|
|
68262
|
+
capName: "records",
|
|
68263
|
+
capScope: "system",
|
|
68264
|
+
addonId: null,
|
|
68265
|
+
access: "create"
|
|
68266
|
+
},
|
|
68267
|
+
"records.putMany": {
|
|
68268
|
+
capName: "records",
|
|
68269
|
+
capScope: "system",
|
|
68270
|
+
addonId: null,
|
|
68271
|
+
access: "create"
|
|
68272
|
+
},
|
|
68273
|
+
"records.query": {
|
|
68274
|
+
capName: "records",
|
|
68275
|
+
capScope: "system",
|
|
68276
|
+
addonId: null,
|
|
68277
|
+
access: "view"
|
|
68278
|
+
},
|
|
68279
|
+
"records.secretRefs": {
|
|
68280
|
+
capName: "records",
|
|
68281
|
+
capScope: "system",
|
|
68282
|
+
addonId: null,
|
|
68283
|
+
access: "view"
|
|
68284
|
+
},
|
|
67465
68285
|
"sceneMonitor.captureReference": {
|
|
67466
68286
|
capName: "scene-monitor",
|
|
67467
68287
|
capScope: "device",
|
|
@@ -67894,6 +68714,48 @@ Object.freeze({
|
|
|
67894
68714
|
addonId: null,
|
|
67895
68715
|
access: "create"
|
|
67896
68716
|
},
|
|
68717
|
+
"storageAdmin.adoptRecords": {
|
|
68718
|
+
capName: "storage-admin",
|
|
68719
|
+
capScope: "system",
|
|
68720
|
+
addonId: null,
|
|
68721
|
+
access: "create"
|
|
68722
|
+
},
|
|
68723
|
+
"storageAdmin.dropCollection": {
|
|
68724
|
+
capName: "storage-admin",
|
|
68725
|
+
capScope: "system",
|
|
68726
|
+
addonId: null,
|
|
68727
|
+
access: "delete"
|
|
68728
|
+
},
|
|
68729
|
+
"storageAdmin.isAdoptedLegacyTable": {
|
|
68730
|
+
capName: "storage-admin",
|
|
68731
|
+
capScope: "system",
|
|
68732
|
+
addonId: null,
|
|
68733
|
+
access: "view"
|
|
68734
|
+
},
|
|
68735
|
+
"storageAdmin.listKinds": {
|
|
68736
|
+
capName: "storage-admin",
|
|
68737
|
+
capScope: "system",
|
|
68738
|
+
addonId: null,
|
|
68739
|
+
access: "view"
|
|
68740
|
+
},
|
|
68741
|
+
"storageAdmin.listTables": {
|
|
68742
|
+
capName: "storage-admin",
|
|
68743
|
+
capScope: "system",
|
|
68744
|
+
addonId: null,
|
|
68745
|
+
access: "view"
|
|
68746
|
+
},
|
|
68747
|
+
"storageAdmin.readLegacyCollection": {
|
|
68748
|
+
capName: "storage-admin",
|
|
68749
|
+
capScope: "system",
|
|
68750
|
+
addonId: null,
|
|
68751
|
+
access: "view"
|
|
68752
|
+
},
|
|
68753
|
+
"storageAdmin.readRecords": {
|
|
68754
|
+
capName: "storage-admin",
|
|
68755
|
+
capScope: "system",
|
|
68756
|
+
addonId: null,
|
|
68757
|
+
access: "view"
|
|
68758
|
+
},
|
|
67897
68759
|
"storageEvictable.evict": {
|
|
67898
68760
|
capName: "storage-evictable",
|
|
67899
68761
|
capScope: "system",
|
|
@@ -68806,6 +69668,12 @@ Object.freeze({
|
|
|
68806
69668
|
addonId: null,
|
|
68807
69669
|
access: "view"
|
|
68808
69670
|
},
|
|
69671
|
+
"userManagement.migrateRawSecrets": {
|
|
69672
|
+
capName: "user-management",
|
|
69673
|
+
capScope: "system",
|
|
69674
|
+
addonId: null,
|
|
69675
|
+
access: "create"
|
|
69676
|
+
},
|
|
68809
69677
|
"userManagement.oauthExchangeCode": {
|
|
68810
69678
|
capName: "user-management",
|
|
68811
69679
|
capScope: "system",
|
|
@@ -71482,6 +72350,37 @@ Object.freeze({
|
|
|
71482
72350
|
form: "single",
|
|
71483
72351
|
optional: false
|
|
71484
72352
|
}],
|
|
72353
|
+
"records.count": [{
|
|
72354
|
+
name: "deviceId",
|
|
72355
|
+
form: "single",
|
|
72356
|
+
optional: true
|
|
72357
|
+
}],
|
|
72358
|
+
"records.deleteDevice": [{
|
|
72359
|
+
name: "deviceId",
|
|
72360
|
+
form: "single",
|
|
72361
|
+
optional: false
|
|
72362
|
+
}],
|
|
72363
|
+
"records.deleteWhere": [{
|
|
72364
|
+
name: "deviceId",
|
|
72365
|
+
form: "single",
|
|
72366
|
+
optional: true
|
|
72367
|
+
}],
|
|
72368
|
+
"records.histogram": [{
|
|
72369
|
+
name: "deviceId",
|
|
72370
|
+
form: "single",
|
|
72371
|
+
optional: true
|
|
72372
|
+
}],
|
|
72373
|
+
"records.putMany": [{
|
|
72374
|
+
name: "records",
|
|
72375
|
+
form: "object-array",
|
|
72376
|
+
optional: false,
|
|
72377
|
+
itemField: "deviceId"
|
|
72378
|
+
}],
|
|
72379
|
+
"records.query": [{
|
|
72380
|
+
name: "deviceId",
|
|
72381
|
+
form: "single",
|
|
72382
|
+
optional: true
|
|
72383
|
+
}],
|
|
71485
72384
|
"sceneMonitor.captureReference": [{
|
|
71486
72385
|
name: "deviceId",
|
|
71487
72386
|
form: "single",
|
|
@@ -71598,6 +72497,17 @@ Object.freeze({
|
|
|
71598
72497
|
form: "single",
|
|
71599
72498
|
optional: false
|
|
71600
72499
|
}],
|
|
72500
|
+
"storageAdmin.adoptRecords": [{
|
|
72501
|
+
name: "records",
|
|
72502
|
+
form: "object-array",
|
|
72503
|
+
optional: false,
|
|
72504
|
+
itemField: "deviceId"
|
|
72505
|
+
}],
|
|
72506
|
+
"storageAdmin.readRecords": [{
|
|
72507
|
+
name: "deviceId",
|
|
72508
|
+
form: "single",
|
|
72509
|
+
optional: true
|
|
72510
|
+
}],
|
|
71601
72511
|
"streamBroker.acquireEgressTranscode": [{
|
|
71602
72512
|
name: "deviceId",
|
|
71603
72513
|
form: "single",
|
|
@@ -73998,6 +74908,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
|
|
|
73998
74908
|
await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
|
|
73999
74909
|
}
|
|
74000
74910
|
//#endregion
|
|
74911
|
+
//#region src/endpoints/adapters/climate-model.ts
|
|
74912
|
+
/**
|
|
74913
|
+
* What a `climate-control` device is to Alexa (D874) — pure, so the discovery
|
|
74914
|
+
* shape, the slice mapping and the directive handlers share one definition.
|
|
74915
|
+
*
|
|
74916
|
+
* `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
|
|
74917
|
+
* upperSetpoint (a device with a range),
|
|
74918
|
+
* thermostatMode HEAT / COOL / AUTO / ECO /
|
|
74919
|
+
* OFF
|
|
74920
|
+
* `Alexa.TemperatureSensor` the current temperature
|
|
74921
|
+
* `Alexa.PowerController` when the device can be turned off
|
|
74922
|
+
* `Alexa.ModeController` Climate.Mode only when the device has a mode
|
|
74923
|
+
* Amazon's thermostat vocabulary lacks
|
|
74924
|
+
* (dry, fan_only)
|
|
74925
|
+
* `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
|
|
74926
|
+
*
|
|
74927
|
+
* Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
|
|
74928
|
+
* mode is not a documented way to carry them: they travel through the
|
|
74929
|
+
* `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
|
|
74930
|
+
* while the device is in one of them.
|
|
74931
|
+
*
|
|
74932
|
+
* Temperatures are Celsius throughout (the cap's contract); Alexa converts for
|
|
74933
|
+
* the account.
|
|
74934
|
+
*/
|
|
74935
|
+
var INSTANCE_MODE = "Climate.Mode";
|
|
74936
|
+
var INSTANCE_FAN = "Climate.FanSpeed";
|
|
74937
|
+
var FAN_PREFIX = "Fan.";
|
|
74938
|
+
var DEFAULT_TEMP_STEP = .5;
|
|
74939
|
+
var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
|
|
74940
|
+
var ALEXA_ECO_PRESET = "eco";
|
|
74941
|
+
var SINGLE_SETPOINT_MODES = [
|
|
74942
|
+
"heat",
|
|
74943
|
+
"cool",
|
|
74944
|
+
"auto",
|
|
74945
|
+
"dry"
|
|
74946
|
+
];
|
|
74947
|
+
/** What TurnOn switches to, best first. */
|
|
74948
|
+
var TURN_ON_PREFERENCE = [
|
|
74949
|
+
"auto",
|
|
74950
|
+
"heat_cool",
|
|
74951
|
+
"cool",
|
|
74952
|
+
"heat"
|
|
74953
|
+
];
|
|
74954
|
+
var SliceShape$1 = ClimateControlStatusSchema.pick({
|
|
74955
|
+
mode: true,
|
|
74956
|
+
availableModes: true,
|
|
74957
|
+
fanMode: true,
|
|
74958
|
+
availableFanModes: true,
|
|
74959
|
+
preset: true,
|
|
74960
|
+
availablePresets: true,
|
|
74961
|
+
target: true,
|
|
74962
|
+
targetHigh: true,
|
|
74963
|
+
targetLow: true,
|
|
74964
|
+
currentTemp: true,
|
|
74965
|
+
minTemp: true,
|
|
74966
|
+
maxTemp: true,
|
|
74967
|
+
tempStep: true
|
|
74968
|
+
});
|
|
74969
|
+
/** The slice fields the adapter reads, or null when the slice is not a climate one. */
|
|
74970
|
+
function readClimateSlice(slice) {
|
|
74971
|
+
const parsed = SliceShape$1.safeParse(slice);
|
|
74972
|
+
return parsed.success ? parsed.data : null;
|
|
74973
|
+
}
|
|
74974
|
+
function boundsOf(slice) {
|
|
74975
|
+
const min = slice.minTemp ?? 7;
|
|
74976
|
+
const max = slice.maxTemp ?? 35;
|
|
74977
|
+
const step = slice.tempStep ?? .5;
|
|
74978
|
+
return min < max ? {
|
|
74979
|
+
min,
|
|
74980
|
+
max,
|
|
74981
|
+
step
|
|
74982
|
+
} : {
|
|
74983
|
+
min: 7,
|
|
74984
|
+
max: 35,
|
|
74985
|
+
step: DEFAULT_TEMP_STEP
|
|
74986
|
+
};
|
|
74987
|
+
}
|
|
74988
|
+
/** One decimal, the precision Alexa's own conversions keep. */
|
|
74989
|
+
function round1(value) {
|
|
74990
|
+
return Math.round(value * 10) / 10;
|
|
74991
|
+
}
|
|
74992
|
+
/**
|
|
74993
|
+
* Snap a setpoint to the device's step (anchored at its minimum), then clamp:
|
|
74994
|
+
* a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
|
|
74995
|
+
*/
|
|
74996
|
+
function snapToStep(value, bounds) {
|
|
74997
|
+
const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
|
|
74998
|
+
return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
|
|
74999
|
+
}
|
|
75000
|
+
function temperatureValue(celsius) {
|
|
75001
|
+
return {
|
|
75002
|
+
value: round1(celsius),
|
|
75003
|
+
scale: "CELSIUS"
|
|
75004
|
+
};
|
|
75005
|
+
}
|
|
75006
|
+
function hasMode(slice, mode) {
|
|
75007
|
+
return slice !== null && slice.availableModes.includes(mode);
|
|
75008
|
+
}
|
|
75009
|
+
/** Dual setpoints (a heat/cool range). */
|
|
75010
|
+
function supportsDual(slice, features) {
|
|
75011
|
+
return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
|
|
75012
|
+
}
|
|
75013
|
+
/** A single setpoint (everything but a range-only device). */
|
|
75014
|
+
function supportsSingle(slice) {
|
|
75015
|
+
return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
|
|
75016
|
+
}
|
|
75017
|
+
/** The device's own name for its `eco` preset, if it has one. */
|
|
75018
|
+
function ecoPresetOf(slice) {
|
|
75019
|
+
return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
|
|
75020
|
+
}
|
|
75021
|
+
/** Amazon thermostat modes the device can be put in, in a stable order. */
|
|
75022
|
+
function supportedThermostatModes(slice) {
|
|
75023
|
+
if (slice === null || slice.availableModes.length === 0) return [
|
|
75024
|
+
"HEAT",
|
|
75025
|
+
"COOL",
|
|
75026
|
+
"AUTO",
|
|
75027
|
+
"OFF"
|
|
75028
|
+
];
|
|
75029
|
+
const out = [];
|
|
75030
|
+
if (hasMode(slice, "heat")) out.push("HEAT");
|
|
75031
|
+
if (hasMode(slice, "cool")) out.push("COOL");
|
|
75032
|
+
if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
|
|
75033
|
+
if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
|
|
75034
|
+
if (hasMode(slice, "off")) out.push("OFF");
|
|
75035
|
+
return out;
|
|
75036
|
+
}
|
|
75037
|
+
/** The HVAC mode TurnOn / leaving OFF switches to. */
|
|
75038
|
+
function onModeOf(slice) {
|
|
75039
|
+
return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
|
|
75040
|
+
}
|
|
75041
|
+
function canPowerOff(slice) {
|
|
75042
|
+
return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
|
|
75043
|
+
}
|
|
75044
|
+
/** A device with a mode Amazon's thermostat vocabulary lacks. */
|
|
75045
|
+
function hasExtraModes(slice) {
|
|
75046
|
+
return hasMode(slice, "dry") || hasMode(slice, "fan_only");
|
|
75047
|
+
}
|
|
75048
|
+
var MODE_VALUE = {
|
|
75049
|
+
heat: "Mode.Heat",
|
|
75050
|
+
cool: "Mode.Cool",
|
|
75051
|
+
auto: "Mode.Auto",
|
|
75052
|
+
dry: "Mode.Dry",
|
|
75053
|
+
fan_only: "Mode.Fan",
|
|
75054
|
+
off: "Mode.Off"
|
|
75055
|
+
};
|
|
75056
|
+
var MODE_NAMES = {
|
|
75057
|
+
"Mode.Heat": ["Riscaldamento", "Heating"],
|
|
75058
|
+
"Mode.Cool": ["Raffrescamento", "Cooling"],
|
|
75059
|
+
"Mode.Auto": ["Automatico", "Auto"],
|
|
75060
|
+
"Mode.Dry": ["Deumidificazione", "Dry"],
|
|
75061
|
+
"Mode.Fan": ["Ventilazione", "Fan only"],
|
|
75062
|
+
"Mode.Off": ["Spento", "Off"]
|
|
75063
|
+
};
|
|
75064
|
+
/** The ModeController value a HVAC mode reports as (a range is "auto"). */
|
|
75065
|
+
function modeValueOf(mode) {
|
|
75066
|
+
return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
|
|
75067
|
+
}
|
|
75068
|
+
/** The HVAC mode a ModeController value asks for, among those the device has. */
|
|
75069
|
+
function hvacModeForValue(value, slice) {
|
|
75070
|
+
if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
|
|
75071
|
+
const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
|
|
75072
|
+
if (mode === void 0) return null;
|
|
75073
|
+
const candidate = SliceShape$1.shape.mode.safeParse(mode);
|
|
75074
|
+
return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
|
|
75075
|
+
}
|
|
75076
|
+
/** The ModeController values the device offers, in a stable order. */
|
|
75077
|
+
function modeValuesOf(slice) {
|
|
75078
|
+
return [
|
|
75079
|
+
"heat",
|
|
75080
|
+
"cool",
|
|
75081
|
+
"auto",
|
|
75082
|
+
"dry",
|
|
75083
|
+
"fan_only",
|
|
75084
|
+
"off"
|
|
75085
|
+
].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
|
|
75086
|
+
}
|
|
75087
|
+
function textNames(it, en) {
|
|
75088
|
+
return [{
|
|
75089
|
+
"@type": "text",
|
|
75090
|
+
value: {
|
|
75091
|
+
text: it,
|
|
75092
|
+
locale: "it-IT"
|
|
75093
|
+
}
|
|
75094
|
+
}, {
|
|
75095
|
+
"@type": "text",
|
|
75096
|
+
value: {
|
|
75097
|
+
text: en,
|
|
75098
|
+
locale: "en-US"
|
|
75099
|
+
}
|
|
75100
|
+
}];
|
|
75101
|
+
}
|
|
75102
|
+
function modeCapability(slice) {
|
|
75103
|
+
const values = modeValuesOf(slice);
|
|
75104
|
+
if (values.length < 2) return null;
|
|
75105
|
+
return {
|
|
75106
|
+
type: "AlexaInterface",
|
|
75107
|
+
interface: "Alexa.ModeController",
|
|
75108
|
+
version: "3",
|
|
75109
|
+
instance: INSTANCE_MODE,
|
|
75110
|
+
capabilityResources: { friendlyNames: [{
|
|
75111
|
+
"@type": "asset",
|
|
75112
|
+
value: { assetId: "Alexa.Setting.Mode" }
|
|
75113
|
+
}, ...textNames("Modalità", "Mode")] },
|
|
75114
|
+
properties: {
|
|
75115
|
+
supported: [{ name: "mode" }],
|
|
75116
|
+
proactivelyReported: true,
|
|
75117
|
+
retrievable: true
|
|
75118
|
+
},
|
|
75119
|
+
configuration: {
|
|
75120
|
+
ordered: false,
|
|
75121
|
+
supportedModes: values.map((value) => {
|
|
75122
|
+
const [it, en] = MODE_NAMES[value] ?? [value, value];
|
|
75123
|
+
return {
|
|
75124
|
+
value,
|
|
75125
|
+
modeResources: { friendlyNames: textNames(it, en) }
|
|
75126
|
+
};
|
|
75127
|
+
})
|
|
75128
|
+
}
|
|
75129
|
+
};
|
|
75130
|
+
}
|
|
75131
|
+
var FAN_ASSETS = {
|
|
75132
|
+
low: "Alexa.Value.Low",
|
|
75133
|
+
medium: "Alexa.Value.Medium",
|
|
75134
|
+
high: "Alexa.Value.High"
|
|
75135
|
+
};
|
|
75136
|
+
var FAN_TEXTS = {
|
|
75137
|
+
auto: ["Automatico", "Auto"],
|
|
75138
|
+
quiet: ["Silenzioso", "Quiet"],
|
|
75139
|
+
turbo: ["Turbo", "Turbo"],
|
|
75140
|
+
on: ["Acceso", "On"],
|
|
75141
|
+
off: ["Spento", "Off"],
|
|
75142
|
+
middle: ["Medio", "Medium"],
|
|
75143
|
+
mediumlow: ["Medio basso", "Medium low"],
|
|
75144
|
+
mediumhigh: ["Medio alto", "Medium high"],
|
|
75145
|
+
medium_low: ["Medio basso", "Medium low"],
|
|
75146
|
+
medium_high: ["Medio alto", "Medium high"]
|
|
75147
|
+
};
|
|
75148
|
+
function capitalize(word) {
|
|
75149
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
75150
|
+
}
|
|
75151
|
+
/** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
|
|
75152
|
+
function fanValueOf(fanMode) {
|
|
75153
|
+
const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
|
|
75154
|
+
return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
|
|
75155
|
+
}
|
|
75156
|
+
/** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
|
|
75157
|
+
function fanModesOf(slice) {
|
|
75158
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75159
|
+
const out = [];
|
|
75160
|
+
for (const fanMode of slice.availableFanModes) {
|
|
75161
|
+
const value = fanValueOf(fanMode);
|
|
75162
|
+
if (value === null || seen.has(value)) continue;
|
|
75163
|
+
seen.add(value);
|
|
75164
|
+
out.push([value, fanMode]);
|
|
75165
|
+
}
|
|
75166
|
+
return out;
|
|
75167
|
+
}
|
|
75168
|
+
function hasFanModes(slice) {
|
|
75169
|
+
return slice !== null && fanModesOf(slice).length >= 2;
|
|
75170
|
+
}
|
|
75171
|
+
function fanFriendlyNames(fanMode) {
|
|
75172
|
+
const key = fanMode.toLowerCase();
|
|
75173
|
+
const asset = FAN_ASSETS[key];
|
|
75174
|
+
if (asset !== void 0) return [{
|
|
75175
|
+
"@type": "asset",
|
|
75176
|
+
value: { assetId: asset }
|
|
75177
|
+
}];
|
|
75178
|
+
const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
|
|
75179
|
+
return textNames(it, en);
|
|
75180
|
+
}
|
|
75181
|
+
function fanCapability(slice) {
|
|
75182
|
+
const modes = fanModesOf(slice);
|
|
75183
|
+
if (modes.length < 2) return null;
|
|
75184
|
+
return {
|
|
75185
|
+
type: "AlexaInterface",
|
|
75186
|
+
interface: "Alexa.ModeController",
|
|
75187
|
+
version: "3",
|
|
75188
|
+
instance: INSTANCE_FAN,
|
|
75189
|
+
capabilityResources: { friendlyNames: [{
|
|
75190
|
+
"@type": "asset",
|
|
75191
|
+
value: { assetId: "Alexa.Setting.FanSpeed" }
|
|
75192
|
+
}, ...textNames("Velocità ventola", "Fan speed")] },
|
|
75193
|
+
properties: {
|
|
75194
|
+
supported: [{ name: "mode" }],
|
|
75195
|
+
proactivelyReported: true,
|
|
75196
|
+
retrievable: true
|
|
75197
|
+
},
|
|
75198
|
+
configuration: {
|
|
75199
|
+
ordered: false,
|
|
75200
|
+
supportedModes: modes.map(([value, fanMode]) => ({
|
|
75201
|
+
value,
|
|
75202
|
+
modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
|
|
75203
|
+
}))
|
|
75204
|
+
}
|
|
75205
|
+
};
|
|
75206
|
+
}
|
|
75207
|
+
function propertiesBlock(names) {
|
|
75208
|
+
return {
|
|
75209
|
+
supported: names.map((name) => ({ name })),
|
|
75210
|
+
proactivelyReported: true,
|
|
75211
|
+
retrievable: true
|
|
75212
|
+
};
|
|
75213
|
+
}
|
|
75214
|
+
function thermostatCapability(slice, modes, features) {
|
|
75215
|
+
return {
|
|
75216
|
+
type: "AlexaInterface",
|
|
75217
|
+
interface: "Alexa.ThermostatController",
|
|
75218
|
+
version: "3.1",
|
|
75219
|
+
properties: propertiesBlock([
|
|
75220
|
+
...supportsSingle(slice) ? ["targetSetpoint"] : [],
|
|
75221
|
+
...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
|
|
75222
|
+
"thermostatMode"
|
|
75223
|
+
]),
|
|
75224
|
+
configuration: {
|
|
75225
|
+
supportedModes: modes,
|
|
75226
|
+
supportsScheduling: false
|
|
75227
|
+
}
|
|
75228
|
+
};
|
|
75229
|
+
}
|
|
75230
|
+
/** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
|
|
75231
|
+
function climateCategoryFor(deviceType) {
|
|
75232
|
+
return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
|
|
75233
|
+
}
|
|
75234
|
+
function describeClimate(device) {
|
|
75235
|
+
const slice = readClimateSlice(device.slices["climate-control"]);
|
|
75236
|
+
const modes = supportedThermostatModes(slice);
|
|
75237
|
+
if (modes.length === 0) return {
|
|
75238
|
+
kind: "skip",
|
|
75239
|
+
reason: "climate: the device offers no mode Alexa has a thermostat value for"
|
|
75240
|
+
};
|
|
75241
|
+
const extras = [];
|
|
75242
|
+
if (slice !== null) {
|
|
75243
|
+
const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
|
|
75244
|
+
const fan = hasFanModes(slice) ? fanCapability(slice) : null;
|
|
75245
|
+
if (mode !== null) extras.push(mode);
|
|
75246
|
+
if (fan !== null) extras.push(fan);
|
|
75247
|
+
}
|
|
75248
|
+
return {
|
|
75249
|
+
kind: "publish",
|
|
75250
|
+
capabilities: [
|
|
75251
|
+
thermostatCapability(slice, modes, device.features),
|
|
75252
|
+
{
|
|
75253
|
+
type: "AlexaInterface",
|
|
75254
|
+
interface: "Alexa.TemperatureSensor",
|
|
75255
|
+
version: "3",
|
|
75256
|
+
properties: propertiesBlock(["temperature"])
|
|
75257
|
+
},
|
|
75258
|
+
...canPowerOff(slice) ? [{
|
|
75259
|
+
type: "AlexaInterface",
|
|
75260
|
+
interface: "Alexa.PowerController",
|
|
75261
|
+
version: "3",
|
|
75262
|
+
properties: propertiesBlock(["powerState"])
|
|
75263
|
+
}] : [],
|
|
75264
|
+
...extras
|
|
75265
|
+
],
|
|
75266
|
+
category: climateCategoryFor(device.type)
|
|
75267
|
+
};
|
|
75268
|
+
}
|
|
75269
|
+
/** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
|
|
75270
|
+
function thermostatModeOf(slice) {
|
|
75271
|
+
if (slice.mode === "off") return "OFF";
|
|
75272
|
+
const eco = ecoPresetOf(slice);
|
|
75273
|
+
if (eco !== null && slice.preset === eco) return "ECO";
|
|
75274
|
+
switch (slice.mode) {
|
|
75275
|
+
case "heat": return "HEAT";
|
|
75276
|
+
case "cool": return "COOL";
|
|
75277
|
+
case "auto":
|
|
75278
|
+
case "heat_cool": return "AUTO";
|
|
75279
|
+
default: return null;
|
|
75280
|
+
}
|
|
75281
|
+
}
|
|
75282
|
+
function climateProperties(slice) {
|
|
75283
|
+
const s = readClimateSlice(slice);
|
|
75284
|
+
if (s === null) return [];
|
|
75285
|
+
const out = [];
|
|
75286
|
+
const thermostat = (name, value) => {
|
|
75287
|
+
out.push({
|
|
75288
|
+
namespace: "Alexa.ThermostatController",
|
|
75289
|
+
name,
|
|
75290
|
+
value
|
|
75291
|
+
});
|
|
75292
|
+
};
|
|
75293
|
+
if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
|
|
75294
|
+
thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
|
|
75295
|
+
thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
|
|
75296
|
+
} else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
|
|
75297
|
+
const mode = thermostatModeOf(s);
|
|
75298
|
+
if (mode !== null) thermostat("thermostatMode", mode);
|
|
75299
|
+
if (s.currentTemp !== null) out.push({
|
|
75300
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75301
|
+
name: "temperature",
|
|
75302
|
+
value: temperatureValue(s.currentTemp)
|
|
75303
|
+
});
|
|
75304
|
+
if (canPowerOff(s)) out.push({
|
|
75305
|
+
namespace: "Alexa.PowerController",
|
|
75306
|
+
name: "powerState",
|
|
75307
|
+
value: s.mode === "off" ? "OFF" : "ON"
|
|
75308
|
+
});
|
|
75309
|
+
if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
|
|
75310
|
+
namespace: "Alexa.ModeController",
|
|
75311
|
+
instance: INSTANCE_MODE,
|
|
75312
|
+
name: "mode",
|
|
75313
|
+
value: modeValueOf(s.mode)
|
|
75314
|
+
});
|
|
75315
|
+
const fan = fanValueOf(s.fanMode);
|
|
75316
|
+
if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
|
|
75317
|
+
namespace: "Alexa.ModeController",
|
|
75318
|
+
instance: INSTANCE_FAN,
|
|
75319
|
+
name: "mode",
|
|
75320
|
+
value: fan
|
|
75321
|
+
});
|
|
75322
|
+
return out;
|
|
75323
|
+
}
|
|
75324
|
+
//#endregion
|
|
75325
|
+
//#region src/handlers/climate-controller.ts
|
|
75326
|
+
var CAP_CLIMATE = "climate-control";
|
|
75327
|
+
var CAP_SWITCH$1 = "switch";
|
|
75328
|
+
var UNCERTAINTY_MS = 500;
|
|
75329
|
+
var PRESET_NONE = "none";
|
|
75330
|
+
var ScaleSchema = _enum([
|
|
75331
|
+
"CELSIUS",
|
|
75332
|
+
"FAHRENHEIT",
|
|
75333
|
+
"KELVIN"
|
|
75334
|
+
]);
|
|
75335
|
+
var TemperatureSchema = object({
|
|
75336
|
+
value: number(),
|
|
75337
|
+
scale: ScaleSchema
|
|
75338
|
+
});
|
|
75339
|
+
var SetTemperaturePayload = object({
|
|
75340
|
+
targetSetpoint: TemperatureSchema.optional(),
|
|
75341
|
+
lowerSetpoint: TemperatureSchema.optional(),
|
|
75342
|
+
upperSetpoint: TemperatureSchema.optional()
|
|
75343
|
+
});
|
|
75344
|
+
var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
|
|
75345
|
+
var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
|
|
75346
|
+
var ModePayload = object({ mode: string() });
|
|
75347
|
+
function isRefusal(outcome) {
|
|
75348
|
+
return "refusal" in outcome;
|
|
75349
|
+
}
|
|
75350
|
+
function toCelsius(t) {
|
|
75351
|
+
if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
|
|
75352
|
+
if (t.scale === "KELVIN") return t.value - 273.15;
|
|
75353
|
+
return t.value;
|
|
75354
|
+
}
|
|
75355
|
+
/** A DELTA converts without the scale's offset. */
|
|
75356
|
+
function deltaToCelsius(t) {
|
|
75357
|
+
return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
|
|
75358
|
+
}
|
|
75359
|
+
function withPayload(envelope, extra) {
|
|
75360
|
+
return { event: {
|
|
75361
|
+
...envelope.event,
|
|
75362
|
+
payload: {
|
|
75363
|
+
...envelope.event.payload,
|
|
75364
|
+
...extra
|
|
75365
|
+
}
|
|
75366
|
+
} };
|
|
75367
|
+
}
|
|
75368
|
+
function outOfRange$1(header, bounds) {
|
|
75369
|
+
return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
|
|
75370
|
+
minimumValue: temperatureValue(bounds.min),
|
|
75371
|
+
maximumValue: temperatureValue(bounds.max)
|
|
75372
|
+
} }) };
|
|
75373
|
+
}
|
|
75374
|
+
function tooClose(header, bounds) {
|
|
75375
|
+
return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
|
|
75376
|
+
}
|
|
75377
|
+
function refuse$1(header, type, message) {
|
|
75378
|
+
return { refusal: errorEnvelope(header, type, message) };
|
|
75379
|
+
}
|
|
75380
|
+
function inRange(celsius, bounds) {
|
|
75381
|
+
const v = round1(celsius);
|
|
75382
|
+
return v >= bounds.min && v <= bounds.max;
|
|
75383
|
+
}
|
|
75384
|
+
var THERMOSTAT_PROPERTIES = new Set([
|
|
75385
|
+
"targetSetpoint",
|
|
75386
|
+
"lowerSetpoint",
|
|
75387
|
+
"upperSetpoint",
|
|
75388
|
+
"thermostatMode"
|
|
75389
|
+
]);
|
|
75390
|
+
function thermostatProperty(property) {
|
|
75391
|
+
return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
|
|
75392
|
+
}
|
|
75393
|
+
function commandsFor(api, deviceId) {
|
|
75394
|
+
return {
|
|
75395
|
+
setMode: (mode) => api.climateControl.setMode.mutate({
|
|
75396
|
+
deviceId,
|
|
75397
|
+
mode
|
|
75398
|
+
}),
|
|
75399
|
+
setPreset: (preset) => api.climateControl.setPreset.mutate({
|
|
75400
|
+
deviceId,
|
|
75401
|
+
preset
|
|
75402
|
+
}),
|
|
75403
|
+
setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
|
|
75404
|
+
deviceId,
|
|
75405
|
+
fanMode
|
|
75406
|
+
}),
|
|
75407
|
+
setTarget: (target) => api.climateControl.setTarget.mutate({
|
|
75408
|
+
deviceId,
|
|
75409
|
+
target
|
|
75410
|
+
}),
|
|
75411
|
+
setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
|
|
75412
|
+
deviceId,
|
|
75413
|
+
targetLow,
|
|
75414
|
+
targetHigh
|
|
75415
|
+
})
|
|
75416
|
+
};
|
|
75417
|
+
}
|
|
75418
|
+
function planRange(header, slice, cmd, low, high) {
|
|
75419
|
+
const bounds = boundsOf(slice);
|
|
75420
|
+
if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
|
|
75421
|
+
if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
|
|
75422
|
+
const targetLow = snapToStep(low, bounds);
|
|
75423
|
+
const targetHigh = snapToStep(high, bounds);
|
|
75424
|
+
if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
|
|
75425
|
+
return {
|
|
75426
|
+
commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
|
|
75427
|
+
projected: {
|
|
75428
|
+
...slice,
|
|
75429
|
+
mode: "heat_cool",
|
|
75430
|
+
targetLow,
|
|
75431
|
+
targetHigh
|
|
75432
|
+
},
|
|
75433
|
+
keep: thermostatProperty
|
|
75434
|
+
};
|
|
75435
|
+
}
|
|
75436
|
+
function planSingle(header, slice, cmd, celsius) {
|
|
75437
|
+
const bounds = boundsOf(slice);
|
|
75438
|
+
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");
|
|
75439
|
+
if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
|
|
75440
|
+
const target = snapToStep(celsius, bounds);
|
|
75441
|
+
return {
|
|
75442
|
+
commands: [() => cmd.setTarget(target)],
|
|
75443
|
+
projected: {
|
|
75444
|
+
...slice,
|
|
75445
|
+
target
|
|
75446
|
+
},
|
|
75447
|
+
keep: thermostatProperty
|
|
75448
|
+
};
|
|
75449
|
+
}
|
|
75450
|
+
function planSetTemperature(c, slice, cmd) {
|
|
75451
|
+
const parsed = SetTemperaturePayload.safeParse(c.payload);
|
|
75452
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75453
|
+
const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
|
|
75454
|
+
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");
|
|
75455
|
+
if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
|
|
75456
|
+
if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
|
|
75457
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
|
|
75458
|
+
}
|
|
75459
|
+
function planAdjust(c, slice, cmd) {
|
|
75460
|
+
const parsed = AdjustTemperaturePayload.safeParse(c.payload);
|
|
75461
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75462
|
+
const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
|
|
75463
|
+
if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
|
|
75464
|
+
if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
|
|
75465
|
+
return planSingle(c.header, slice, cmd, slice.target + delta);
|
|
75466
|
+
}
|
|
75467
|
+
var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
|
|
75468
|
+
function hvacModeFor(requested, slice) {
|
|
75469
|
+
const has = (m) => slice.availableModes.includes(m);
|
|
75470
|
+
switch (requested) {
|
|
75471
|
+
case "HEAT": return has("heat") ? "heat" : null;
|
|
75472
|
+
case "COOL": return has("cool") ? "cool" : null;
|
|
75473
|
+
case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
|
|
75474
|
+
case "OFF": return has("off") ? "off" : null;
|
|
75475
|
+
default: return null;
|
|
75476
|
+
}
|
|
75477
|
+
}
|
|
75478
|
+
/** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
|
|
75479
|
+
function presetClearing(slice) {
|
|
75480
|
+
return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
|
|
75481
|
+
}
|
|
75482
|
+
function planEco(c, slice, cmd) {
|
|
75483
|
+
const eco = ecoPresetOf(slice);
|
|
75484
|
+
if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
|
|
75485
|
+
const wake = slice.mode === "off" ? onModeOf(slice) : null;
|
|
75486
|
+
if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
|
|
75487
|
+
return {
|
|
75488
|
+
commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
|
|
75489
|
+
projected: {
|
|
75490
|
+
...slice,
|
|
75491
|
+
mode: wake ?? slice.mode,
|
|
75492
|
+
preset: eco
|
|
75493
|
+
},
|
|
75494
|
+
keep: thermostatProperty
|
|
75495
|
+
};
|
|
75496
|
+
}
|
|
75497
|
+
function planSetThermostatMode(c, slice, cmd) {
|
|
75498
|
+
const parsed = ThermostatModePayload.safeParse(c.payload);
|
|
75499
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75500
|
+
const raw = parsed.data.thermostatMode;
|
|
75501
|
+
const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
|
|
75502
|
+
if (requested === "ECO") return planEco(c, slice, cmd);
|
|
75503
|
+
const mode = hvacModeFor(requested, slice);
|
|
75504
|
+
if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
|
|
75505
|
+
const eco = ecoPresetOf(slice);
|
|
75506
|
+
const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
|
|
75507
|
+
return {
|
|
75508
|
+
commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
|
|
75509
|
+
projected: {
|
|
75510
|
+
...slice,
|
|
75511
|
+
mode,
|
|
75512
|
+
preset: clear ?? slice.preset
|
|
75513
|
+
},
|
|
75514
|
+
keep: thermostatProperty
|
|
75515
|
+
};
|
|
75516
|
+
}
|
|
75517
|
+
function planPower(c, slice, cmd) {
|
|
75518
|
+
const turnOn = c.header.name === "TurnOn";
|
|
75519
|
+
const keep = (p) => p.namespace === "Alexa.PowerController";
|
|
75520
|
+
if (turnOn) {
|
|
75521
|
+
if (slice.mode !== "off") return {
|
|
75522
|
+
commands: [],
|
|
75523
|
+
projected: slice,
|
|
75524
|
+
keep
|
|
75525
|
+
};
|
|
75526
|
+
const mode = onModeOf(slice);
|
|
75527
|
+
if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
|
|
75528
|
+
return {
|
|
75529
|
+
commands: [() => cmd.setMode(mode)],
|
|
75530
|
+
projected: {
|
|
75531
|
+
...slice,
|
|
75532
|
+
mode
|
|
75533
|
+
},
|
|
75534
|
+
keep
|
|
75535
|
+
};
|
|
75536
|
+
}
|
|
75537
|
+
if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
|
|
75538
|
+
if (slice.mode === "off") return {
|
|
75539
|
+
commands: [],
|
|
75540
|
+
projected: slice,
|
|
75541
|
+
keep
|
|
75542
|
+
};
|
|
75543
|
+
return {
|
|
75544
|
+
commands: [() => cmd.setMode("off")],
|
|
75545
|
+
projected: {
|
|
75546
|
+
...slice,
|
|
75547
|
+
mode: "off"
|
|
75548
|
+
},
|
|
75549
|
+
keep
|
|
75550
|
+
};
|
|
75551
|
+
}
|
|
75552
|
+
function planModeController(c, slice, cmd) {
|
|
75553
|
+
const parsed = ModePayload.safeParse(c.payload);
|
|
75554
|
+
const instance = c.header.instance;
|
|
75555
|
+
if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
|
|
75556
|
+
const keep = (p) => p.instance === instance;
|
|
75557
|
+
if (instance === "Climate.Mode") {
|
|
75558
|
+
const mode = hvacModeForValue(parsed.data.mode, slice);
|
|
75559
|
+
if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
|
|
75560
|
+
return {
|
|
75561
|
+
commands: [() => cmd.setMode(mode)],
|
|
75562
|
+
projected: {
|
|
75563
|
+
...slice,
|
|
75564
|
+
mode
|
|
75565
|
+
},
|
|
75566
|
+
keep
|
|
75567
|
+
};
|
|
75568
|
+
}
|
|
75569
|
+
if (instance === "Climate.FanSpeed") {
|
|
75570
|
+
const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
|
|
75571
|
+
if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
|
|
75572
|
+
return {
|
|
75573
|
+
commands: [() => cmd.setFanMode(fan[1])],
|
|
75574
|
+
projected: {
|
|
75575
|
+
...slice,
|
|
75576
|
+
fanMode: fan[1]
|
|
75577
|
+
},
|
|
75578
|
+
keep
|
|
75579
|
+
};
|
|
75580
|
+
}
|
|
75581
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
|
|
75582
|
+
}
|
|
75583
|
+
function plan(c, slice, cmd) {
|
|
75584
|
+
switch (`${c.header.namespace}/${c.header.name}`) {
|
|
75585
|
+
case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
|
|
75586
|
+
case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
|
|
75587
|
+
case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
|
|
75588
|
+
case "Alexa.PowerController/TurnOn":
|
|
75589
|
+
case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
|
|
75590
|
+
case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
|
|
75591
|
+
default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
|
|
75592
|
+
}
|
|
75593
|
+
}
|
|
75594
|
+
async function readSlice$1(c, deps) {
|
|
75595
|
+
try {
|
|
75596
|
+
return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
|
|
75597
|
+
deviceId: c.deviceId,
|
|
75598
|
+
capName: CAP_CLIMATE
|
|
75599
|
+
}));
|
|
75600
|
+
} catch (err) {
|
|
75601
|
+
deps.logger.warn("alexa climate-controller: the climate state could not be read", {
|
|
75602
|
+
tags: { deviceId: c.deviceId },
|
|
75603
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
75604
|
+
});
|
|
75605
|
+
return null;
|
|
75606
|
+
}
|
|
75607
|
+
}
|
|
75608
|
+
function successResponse$1(c, properties) {
|
|
75609
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
75610
|
+
return {
|
|
75611
|
+
event: {
|
|
75612
|
+
header: {
|
|
75613
|
+
namespace: "Alexa",
|
|
75614
|
+
name: "Response",
|
|
75615
|
+
messageId: c.header.messageId,
|
|
75616
|
+
...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
|
|
75617
|
+
payloadVersion: "3"
|
|
75618
|
+
},
|
|
75619
|
+
endpoint: { endpointId: c.endpoint.endpointId },
|
|
75620
|
+
payload: {}
|
|
75621
|
+
},
|
|
75622
|
+
context: { properties: properties.map((p) => ({
|
|
75623
|
+
...p,
|
|
75624
|
+
timeOfSample: now,
|
|
75625
|
+
uncertaintyInMilliseconds: UNCERTAINTY_MS
|
|
75626
|
+
})) }
|
|
75627
|
+
};
|
|
75628
|
+
}
|
|
75629
|
+
/** The one handler behind every climate directive. */
|
|
75630
|
+
async function handleClimateDirective(c) {
|
|
75631
|
+
const deps = c.deps.climateController;
|
|
75632
|
+
const slice = await readSlice$1(c, deps);
|
|
75633
|
+
if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
|
|
75634
|
+
const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
|
|
75635
|
+
if (isRefusal(outcome)) return outcome.refusal;
|
|
75636
|
+
try {
|
|
75637
|
+
for (const command of outcome.commands) await command();
|
|
75638
|
+
} catch (err) {
|
|
75639
|
+
deps.logger.warn("alexa climate-controller: the climate command failed", {
|
|
75640
|
+
tags: { deviceId: c.deviceId },
|
|
75641
|
+
meta: {
|
|
75642
|
+
directive: c.header.name,
|
|
75643
|
+
error: err instanceof Error ? err.message : String(err)
|
|
75644
|
+
}
|
|
75645
|
+
});
|
|
75646
|
+
return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
|
|
75647
|
+
}
|
|
75648
|
+
deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
|
|
75649
|
+
tags: { deviceId: c.deviceId },
|
|
75650
|
+
meta: { commands: outcome.commands.length }
|
|
75651
|
+
});
|
|
75652
|
+
return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
|
|
75653
|
+
}
|
|
75654
|
+
/**
|
|
75655
|
+
* Last answer per device, per deps instance: a failed bindings read changes
|
|
75656
|
+
* nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
|
|
75657
|
+
* switch handler because one `getBindings` call failed.
|
|
75658
|
+
*/
|
|
75659
|
+
var lastPowerClaim = /* @__PURE__ */ new WeakMap();
|
|
75660
|
+
/** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
|
|
75661
|
+
async function handlesPower(c) {
|
|
75662
|
+
const deps = c.deps.climateController;
|
|
75663
|
+
const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
|
|
75664
|
+
lastPowerClaim.set(deps, known);
|
|
75665
|
+
try {
|
|
75666
|
+
const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
|
|
75667
|
+
const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
|
|
75668
|
+
known.set(c.deviceId, claim);
|
|
75669
|
+
return claim;
|
|
75670
|
+
} catch (err) {
|
|
75671
|
+
const previous = known.get(c.deviceId);
|
|
75672
|
+
deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
|
|
75673
|
+
tags: { deviceId: c.deviceId },
|
|
75674
|
+
meta: {
|
|
75675
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75676
|
+
decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
|
|
75677
|
+
}
|
|
75678
|
+
});
|
|
75679
|
+
return previous ?? false;
|
|
75680
|
+
}
|
|
75681
|
+
}
|
|
75682
|
+
//#endregion
|
|
74001
75683
|
//#region src/endpoints/adapters/shared.ts
|
|
74002
75684
|
/** The base `Alexa` interface every endpoint declares. */
|
|
74003
75685
|
var BASE = {
|
|
@@ -74048,6 +75730,88 @@ function numericValueField(value) {
|
|
|
74048
75730
|
return typeof raw === "number" ? raw : null;
|
|
74049
75731
|
}
|
|
74050
75732
|
//#endregion
|
|
75733
|
+
//#region src/endpoints/adapters/temperature.ts
|
|
75734
|
+
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
75735
|
+
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
75736
|
+
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
75737
|
+
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
75738
|
+
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
75739
|
+
var temperatureAdapter = {
|
|
75740
|
+
cap: "temperature-sensor",
|
|
75741
|
+
stateCaps: ["temperature-sensor"],
|
|
75742
|
+
discoveryReads: [],
|
|
75743
|
+
describe: () => ({
|
|
75744
|
+
kind: "publish",
|
|
75745
|
+
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
75746
|
+
category: "TEMPERATURE_SENSOR"
|
|
75747
|
+
}),
|
|
75748
|
+
properties: (_cap, slice) => {
|
|
75749
|
+
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
75750
|
+
return p.success ? [{
|
|
75751
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75752
|
+
name: "temperature",
|
|
75753
|
+
value: {
|
|
75754
|
+
value: Math.round(p.data.celsius * 10) / 10,
|
|
75755
|
+
scale: "CELSIUS"
|
|
75756
|
+
}
|
|
75757
|
+
}] : [];
|
|
75758
|
+
},
|
|
75759
|
+
directives: [],
|
|
75760
|
+
shaping: () => ({
|
|
75761
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75762
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75763
|
+
numericOf: numericValueField
|
|
75764
|
+
})
|
|
75765
|
+
};
|
|
75766
|
+
//#endregion
|
|
75767
|
+
//#region src/endpoints/adapters/climate.ts
|
|
75768
|
+
var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
|
|
75769
|
+
var THERMOSTAT_DIRECTIVES = [
|
|
75770
|
+
"Alexa.ThermostatController/SetTargetTemperature",
|
|
75771
|
+
"Alexa.ThermostatController/AdjustTargetTemperature",
|
|
75772
|
+
"Alexa.ThermostatController/SetThermostatMode"
|
|
75773
|
+
].map((key) => ({
|
|
75774
|
+
key,
|
|
75775
|
+
needsReachableDevice: true,
|
|
75776
|
+
handle: handleClimateDirective
|
|
75777
|
+
}));
|
|
75778
|
+
var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
|
|
75779
|
+
key,
|
|
75780
|
+
needsReachableDevice: true,
|
|
75781
|
+
handle: handleClimateDirective,
|
|
75782
|
+
claims: handlesPower
|
|
75783
|
+
}));
|
|
75784
|
+
var MODE_DIRECTIVE = {
|
|
75785
|
+
key: "Alexa.ModeController/SetMode",
|
|
75786
|
+
needsReachableDevice: true,
|
|
75787
|
+
handle: handleClimateDirective,
|
|
75788
|
+
claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
|
|
75789
|
+
};
|
|
75790
|
+
/**
|
|
75791
|
+
* `climate-control` -> a thermostat (Home Assistant) or an air conditioner
|
|
75792
|
+
* (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
|
|
75793
|
+
* power, and the modes Amazon's vocabulary lacks and the fan mode as
|
|
75794
|
+
* ModeControllers. The current temperature is shaped like any temperature
|
|
75795
|
+
* sensor; setpoints, mode and fan report on every change.
|
|
75796
|
+
*/
|
|
75797
|
+
var climateAdapter = {
|
|
75798
|
+
cap: "climate-control",
|
|
75799
|
+
stateCaps: ["climate-control"],
|
|
75800
|
+
discoveryReads: ["climate-control"],
|
|
75801
|
+
describe: describeClimate,
|
|
75802
|
+
properties: (_cap, slice) => climateProperties(slice),
|
|
75803
|
+
directives: [
|
|
75804
|
+
...THERMOSTAT_DIRECTIVES,
|
|
75805
|
+
...POWER_DIRECTIVES$1,
|
|
75806
|
+
MODE_DIRECTIVE
|
|
75807
|
+
],
|
|
75808
|
+
shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
|
|
75809
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75810
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75811
|
+
numericOf: numericValueField
|
|
75812
|
+
} : null
|
|
75813
|
+
};
|
|
75814
|
+
//#endregion
|
|
74051
75815
|
//#region src/endpoints/adapters/contact.ts
|
|
74052
75816
|
/** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
|
|
74053
75817
|
var contactAdapter = {
|
|
@@ -74712,7 +76476,8 @@ var coverAdapter = {
|
|
|
74712
76476
|
].map((key) => ({
|
|
74713
76477
|
key,
|
|
74714
76478
|
needsReachableDevice: true,
|
|
74715
|
-
handle: handleCoverDirective
|
|
76479
|
+
handle: handleCoverDirective,
|
|
76480
|
+
claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
|
|
74716
76481
|
})),
|
|
74717
76482
|
shaping: (property) => property.namespace === "Alexa.RangeController" ? {
|
|
74718
76483
|
deadband: 1,
|
|
@@ -74720,40 +76485,6 @@ var coverAdapter = {
|
|
|
74720
76485
|
numericOf: plainNumber
|
|
74721
76486
|
} : { debounceMs: COVER_DEBOUNCE_MS }
|
|
74722
76487
|
};
|
|
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
76488
|
/** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
|
|
74758
76489
|
var humidityAdapter = {
|
|
74759
76490
|
cap: "humidity-sensor",
|
|
@@ -75052,6 +76783,11 @@ var BRIGHTNESS_DIRECTIVE = {
|
|
|
75052
76783
|
needsReachableDevice: true,
|
|
75053
76784
|
handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
|
|
75054
76785
|
};
|
|
76786
|
+
/** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
|
|
76787
|
+
* smart plug; every other switch is a SWITCH. */
|
|
76788
|
+
function switchCategory(device) {
|
|
76789
|
+
return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
|
|
76790
|
+
}
|
|
75055
76791
|
//#endregion
|
|
75056
76792
|
//#region src/endpoints/adapters/index.ts
|
|
75057
76793
|
var GENERIC_ADAPTERS = [
|
|
@@ -75059,6 +76795,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75059
76795
|
contactAdapter,
|
|
75060
76796
|
motionAdapter,
|
|
75061
76797
|
presenceAdapter,
|
|
76798
|
+
climateAdapter,
|
|
75062
76799
|
temperatureAdapter,
|
|
75063
76800
|
humidityAdapter,
|
|
75064
76801
|
{
|
|
@@ -75070,7 +76807,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75070
76807
|
return {
|
|
75071
76808
|
kind: "publish",
|
|
75072
76809
|
capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
|
|
75073
|
-
category: dimmable || device.type === "light" ? "LIGHT" :
|
|
76810
|
+
category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
|
|
75074
76811
|
};
|
|
75075
76812
|
},
|
|
75076
76813
|
properties: (capName, slice) => {
|
|
@@ -75116,12 +76853,19 @@ function shapingFor(capName, property) {
|
|
|
75116
76853
|
function discoveryReadCaps(capNames) {
|
|
75117
76854
|
return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
|
|
75118
76855
|
}
|
|
75119
|
-
/**
|
|
75120
|
-
function
|
|
75121
|
-
|
|
75122
|
-
|
|
75123
|
-
|
|
75124
|
-
|
|
76856
|
+
/** Every directive an adapter declares for `Namespace/Name`, in registry order. */
|
|
76857
|
+
function genericDirectives(key) {
|
|
76858
|
+
return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
|
|
76859
|
+
}
|
|
76860
|
+
/**
|
|
76861
|
+
* The directive that serves this endpoint: of the adapters declaring the key,
|
|
76862
|
+
* in registry order, the first that claims it (two caps may answer one key -
|
|
76863
|
+
* power on a switch and on a thermostat). Null = nobody claims it.
|
|
76864
|
+
*/
|
|
76865
|
+
async function resolveGenericDirective(context) {
|
|
76866
|
+
const key = `${context.header.namespace}/${context.header.name}`;
|
|
76867
|
+
for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
|
|
76868
|
+
return null;
|
|
75125
76869
|
}
|
|
75126
76870
|
/** Every directive key the registry handles. */
|
|
75127
76871
|
var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
|
|
@@ -75163,9 +76907,9 @@ function planGenericDevice(device) {
|
|
|
75163
76907
|
category
|
|
75164
76908
|
};
|
|
75165
76909
|
}
|
|
75166
|
-
/** Device types this export may publish as a generic endpoint.
|
|
75167
|
-
*
|
|
75168
|
-
*
|
|
76910
|
+
/** Device types this export may publish as a generic endpoint. `thermostat`
|
|
76911
|
+
* (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
|
|
76912
|
+
* the `climate-control` cap (D874). */
|
|
75169
76913
|
var GENERIC_DEVICE_TYPES = [
|
|
75170
76914
|
"light",
|
|
75171
76915
|
"switch",
|
|
@@ -75174,11 +76918,13 @@ var GENERIC_DEVICE_TYPES = [
|
|
|
75174
76918
|
"lock",
|
|
75175
76919
|
"presence",
|
|
75176
76920
|
"thermostat",
|
|
76921
|
+
"climate",
|
|
75177
76922
|
"cover",
|
|
75178
76923
|
"generic"
|
|
75179
76924
|
];
|
|
75180
76925
|
function toEndpoint(input, plan) {
|
|
75181
|
-
const
|
|
76926
|
+
const override = input.alexaName?.trim() ?? "";
|
|
76927
|
+
const name = override.length > 0 ? override : input.name.trim();
|
|
75182
76928
|
return {
|
|
75183
76929
|
endpointId: `${input.deviceId}__dev`,
|
|
75184
76930
|
manufacturerName: "camstack",
|
|
@@ -75189,13 +76935,7 @@ function toEndpoint(input, plan) {
|
|
|
75189
76935
|
cookie: { stableId: input.stableId }
|
|
75190
76936
|
};
|
|
75191
76937
|
}
|
|
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
76938
|
function planGenericEndpoint(input) {
|
|
75195
|
-
if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
|
|
75196
|
-
kind: "skip",
|
|
75197
|
-
reasons: [THERMOSTAT_SKIP_REASON]
|
|
75198
|
-
};
|
|
75199
76939
|
const plan = planGenericDevice({
|
|
75200
76940
|
deviceId: input.deviceId,
|
|
75201
76941
|
type: input.type,
|
|
@@ -75211,6 +76951,74 @@ function planGenericEndpoint(input) {
|
|
|
75211
76951
|
return plan;
|
|
75212
76952
|
}
|
|
75213
76953
|
//#endregion
|
|
76954
|
+
//#region src/endpoints/alexa-name.ts
|
|
76955
|
+
/**
|
|
76956
|
+
* The per-device "Alexa name" (D875): what Alexa calls the device, when it
|
|
76957
|
+
* should not be the device's own name.
|
|
76958
|
+
*
|
|
76959
|
+
* Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
|
|
76960
|
+
* (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
|
|
76961
|
+
* id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
|
|
76962
|
+
* AddOrUpdateReport instead of creating a duplicate.
|
|
76963
|
+
*
|
|
76964
|
+
* Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
|
|
76965
|
+
* to 256 alphanumeric characters and spaces. Don't include special characters
|
|
76966
|
+
* or punctuation." Letters of any script count as alphanumeric, so an Italian
|
|
76967
|
+
* name keeps its accents. The validator rejects what Amazon would reject, with
|
|
76968
|
+
* a message the operator can act on.
|
|
76969
|
+
*/
|
|
76970
|
+
/** Per-device store key of the saved Alexa name. */
|
|
76971
|
+
var ALEXA_NAME_KEY = "alexaName";
|
|
76972
|
+
/** The device-settings form field key, namespaced per exporter (see the addon). */
|
|
76973
|
+
function alexaNameKey(deviceId) {
|
|
76974
|
+
return `alexa:${deviceId}:alexaName`;
|
|
76975
|
+
}
|
|
76976
|
+
/** Letters (any script, with their combining marks), digits and spaces. */
|
|
76977
|
+
var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
|
|
76978
|
+
/**
|
|
76979
|
+
* Validate an operator-typed Alexa name. Blank clears the override
|
|
76980
|
+
* (`name: null`, the device name is used).
|
|
76981
|
+
*/
|
|
76982
|
+
function validateAlexaName(raw) {
|
|
76983
|
+
if (raw === null || raw === void 0) return {
|
|
76984
|
+
ok: true,
|
|
76985
|
+
name: null
|
|
76986
|
+
};
|
|
76987
|
+
if (typeof raw !== "string") return {
|
|
76988
|
+
ok: false,
|
|
76989
|
+
error: "The Alexa name must be text."
|
|
76990
|
+
};
|
|
76991
|
+
const name = raw.trim().replace(/\s+/g, " ");
|
|
76992
|
+
if (name.length === 0) return {
|
|
76993
|
+
ok: true,
|
|
76994
|
+
name: null
|
|
76995
|
+
};
|
|
76996
|
+
if (name.length > 256) return {
|
|
76997
|
+
ok: false,
|
|
76998
|
+
error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
|
|
76999
|
+
};
|
|
77000
|
+
if (!ALLOWED_PATTERN.test(name)) return {
|
|
77001
|
+
ok: false,
|
|
77002
|
+
error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
|
|
77003
|
+
};
|
|
77004
|
+
return {
|
|
77005
|
+
ok: true,
|
|
77006
|
+
name
|
|
77007
|
+
};
|
|
77008
|
+
}
|
|
77009
|
+
var StoredNameSchema = string();
|
|
77010
|
+
/**
|
|
77011
|
+
* The saved Alexa name in a per-device store blob, or null (never saved, or a
|
|
77012
|
+
* stored value that no longer validates: the device name is used rather than
|
|
77013
|
+
* pushing a name Amazon would refuse).
|
|
77014
|
+
*/
|
|
77015
|
+
function readAlexaName(store) {
|
|
77016
|
+
const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
|
|
77017
|
+
if (!parsed.success) return null;
|
|
77018
|
+
const check = validateAlexaName(parsed.data);
|
|
77019
|
+
return check.ok ? check.name : null;
|
|
77020
|
+
}
|
|
77021
|
+
//#endregion
|
|
75214
77022
|
//#region src/handlers/security-panel-arm-state.ts
|
|
75215
77023
|
/**
|
|
75216
77024
|
* The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
|
|
@@ -75884,6 +77692,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75884
77692
|
deviceId: parentInfo.id,
|
|
75885
77693
|
stableId: parentInfo.stableId,
|
|
75886
77694
|
name: parentInfo.name,
|
|
77695
|
+
alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
|
|
75887
77696
|
type: String(device.type),
|
|
75888
77697
|
capNames: parentInfo.capNames,
|
|
75889
77698
|
role: parentInfo.role,
|
|
@@ -75942,11 +77751,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75942
77751
|
return [];
|
|
75943
77752
|
});
|
|
75944
77753
|
const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
|
|
77754
|
+
const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
|
|
75945
77755
|
const out = [];
|
|
75946
77756
|
out.push(buildParentCameraEndpoint({
|
|
75947
77757
|
deviceId: parentInfo.id,
|
|
75948
77758
|
stableId: parentInfo.stableId,
|
|
75949
|
-
friendlyName:
|
|
77759
|
+
friendlyName: cameraName,
|
|
75950
77760
|
isDoorbell,
|
|
75951
77761
|
hasIntercom,
|
|
75952
77762
|
hasMotion,
|
|
@@ -75960,7 +77770,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75960
77770
|
for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
|
|
75961
77771
|
deviceId: parentInfo.id,
|
|
75962
77772
|
stableId: parentInfo.stableId,
|
|
75963
|
-
cameraName
|
|
77773
|
+
cameraName,
|
|
75964
77774
|
detectionClass
|
|
75965
77775
|
}));
|
|
75966
77776
|
}
|
|
@@ -76454,6 +78264,36 @@ function validateInstanced(c, push) {
|
|
|
76454
78264
|
if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
|
|
76455
78265
|
}
|
|
76456
78266
|
//#endregion
|
|
78267
|
+
//#region src/endpoints/validate-thermostat.ts
|
|
78268
|
+
var PROPERTIES = new Set([
|
|
78269
|
+
"targetSetpoint",
|
|
78270
|
+
"lowerSetpoint",
|
|
78271
|
+
"upperSetpoint",
|
|
78272
|
+
"thermostatMode"
|
|
78273
|
+
]);
|
|
78274
|
+
var MODES = new Set([
|
|
78275
|
+
"AUTO",
|
|
78276
|
+
"COOL",
|
|
78277
|
+
"HEAT",
|
|
78278
|
+
"ECO",
|
|
78279
|
+
"OFF"
|
|
78280
|
+
]);
|
|
78281
|
+
function validateThermostat(c, push) {
|
|
78282
|
+
const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
|
|
78283
|
+
for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
|
|
78284
|
+
if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
|
|
78285
|
+
if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
|
|
78286
|
+
if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
|
|
78287
|
+
const modes = c.configuration?.["supportedModes"];
|
|
78288
|
+
if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
|
|
78289
|
+
else {
|
|
78290
|
+
for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
|
|
78291
|
+
if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
|
|
78292
|
+
}
|
|
78293
|
+
const scheduling = c.configuration?.["supportsScheduling"];
|
|
78294
|
+
if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
|
|
78295
|
+
}
|
|
78296
|
+
//#endregion
|
|
76457
78297
|
//#region src/endpoints/validate-endpoint.ts
|
|
76458
78298
|
/**
|
|
76459
78299
|
* Interfaces this skill may declare, with the version Amazon documents. An
|
|
@@ -76477,6 +78317,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
|
|
|
76477
78317
|
"Alexa.RangeController": "3",
|
|
76478
78318
|
"Alexa.SecurityPanelController": "3",
|
|
76479
78319
|
"Alexa.LockController": "3",
|
|
78320
|
+
"Alexa.ThermostatController": "3.1",
|
|
76480
78321
|
"Alexa.SmartVision.SnapshotProvider": "1.1",
|
|
76481
78322
|
"Alexa.SmartVision.ObjectDetectionSensor": "1.0",
|
|
76482
78323
|
"Alexa.DataController": "1.0"
|
|
@@ -76516,6 +78357,8 @@ var DISPLAY_CATEGORIES = new Set([
|
|
|
76516
78357
|
"SMARTLOCK",
|
|
76517
78358
|
"INTERIOR_BLIND",
|
|
76518
78359
|
"EXTERIOR_BLIND",
|
|
78360
|
+
"THERMOSTAT",
|
|
78361
|
+
"AIR_CONDITIONER",
|
|
76519
78362
|
"DOOR",
|
|
76520
78363
|
"GATE",
|
|
76521
78364
|
"GARAGE_DOOR",
|
|
@@ -76594,6 +78437,7 @@ function validateCapability(endpointId, c, out, declared) {
|
|
|
76594
78437
|
}
|
|
76595
78438
|
}
|
|
76596
78439
|
validateInstanced(c, push);
|
|
78440
|
+
if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
|
|
76597
78441
|
if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
|
|
76598
78442
|
if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
|
|
76599
78443
|
if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
|
|
@@ -78524,20 +80368,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
|
|
|
78524
80368
|
status: 200,
|
|
78525
80369
|
body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
|
|
78526
80370
|
};
|
|
78527
|
-
|
|
78528
|
-
if (adapterDirective !== void 0 && endpoint !== void 0) {
|
|
80371
|
+
if (genericDirectives(key).length > 0 && endpoint !== void 0) {
|
|
78529
80372
|
const target = parseCameraEndpointId(endpoint.endpointId);
|
|
78530
|
-
if (target.kind === "generic")
|
|
78531
|
-
|
|
78532
|
-
body: await adapterDirective.handle({
|
|
80373
|
+
if (target.kind === "generic") {
|
|
80374
|
+
const directiveContext = {
|
|
78533
80375
|
header,
|
|
78534
80376
|
endpoint,
|
|
78535
80377
|
payload,
|
|
78536
80378
|
deviceId: target.deviceId,
|
|
78537
80379
|
deps,
|
|
78538
80380
|
identity: callerIdentity
|
|
78539
|
-
}
|
|
78540
|
-
|
|
80381
|
+
};
|
|
80382
|
+
const adapterDirective = await resolveGenericDirective(directiveContext);
|
|
80383
|
+
if (adapterDirective === null) return {
|
|
80384
|
+
status: 200,
|
|
80385
|
+
body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
|
|
80386
|
+
};
|
|
80387
|
+
return {
|
|
80388
|
+
status: 200,
|
|
80389
|
+
body: await adapterDirective.handle(directiveContext)
|
|
80390
|
+
};
|
|
80391
|
+
}
|
|
78541
80392
|
}
|
|
78542
80393
|
if (key === "Alexa.ModeController/SetMode") return {
|
|
78543
80394
|
status: 200,
|
|
@@ -82680,6 +84531,7 @@ var SUPPORTED_KINDS = [
|
|
|
82680
84531
|
DeviceType.Lock,
|
|
82681
84532
|
DeviceType.Presence,
|
|
82682
84533
|
DeviceType.Thermostat,
|
|
84534
|
+
DeviceType.Climate,
|
|
82683
84535
|
DeviceType.Cover,
|
|
82684
84536
|
DeviceType.Generic
|
|
82685
84537
|
];
|
|
@@ -82818,6 +84670,10 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
82818
84670
|
ctx: this.ctx,
|
|
82819
84671
|
logger: this.ctx.logger
|
|
82820
84672
|
},
|
|
84673
|
+
climateController: {
|
|
84674
|
+
ctx: this.ctx,
|
|
84675
|
+
logger: this.ctx.logger
|
|
84676
|
+
},
|
|
82821
84677
|
coverController: {
|
|
82822
84678
|
ctx: this.ctx,
|
|
82823
84679
|
logger: this.ctx.logger,
|
|
@@ -83074,6 +84930,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83074
84930
|
logger: this.ctx.logger,
|
|
83075
84931
|
snapshots: this.config.snapshots,
|
|
83076
84932
|
...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
|
|
84933
|
+
alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
|
|
83077
84934
|
...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
|
|
83078
84935
|
});
|
|
83079
84936
|
}
|
|
@@ -83641,12 +85498,28 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83641
85498
|
value: enabled,
|
|
83642
85499
|
immediate: true
|
|
83643
85500
|
},
|
|
85501
|
+
await this.alexaNameField(deviceId, enabledKey),
|
|
83644
85502
|
...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
|
|
83645
85503
|
...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
|
|
83646
85504
|
]
|
|
83647
85505
|
}]
|
|
83648
85506
|
};
|
|
83649
85507
|
}
|
|
85508
|
+
/** D875: the "Alexa name" field, with the validation error of the saved value. */
|
|
85509
|
+
async alexaNameField(deviceId, enabledKey) {
|
|
85510
|
+
const saved = readAlexaName(await this.vision.readPrefs(deviceId));
|
|
85511
|
+
return {
|
|
85512
|
+
type: "text",
|
|
85513
|
+
key: alexaNameKey(deviceId),
|
|
85514
|
+
label: "Alexa name",
|
|
85515
|
+
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.`,
|
|
85516
|
+
value: saved ?? "",
|
|
85517
|
+
showWhen: {
|
|
85518
|
+
field: enabledKey,
|
|
85519
|
+
equals: true
|
|
85520
|
+
}
|
|
85521
|
+
};
|
|
85522
|
+
}
|
|
83650
85523
|
/**
|
|
83651
85524
|
* The camera's detection fields: which classes become Alexa sensors (D778;
|
|
83652
85525
|
* only the classes its pipeline produces are offered) and its object
|
|
@@ -83668,6 +85541,24 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83668
85541
|
})];
|
|
83669
85542
|
}
|
|
83670
85543
|
/**
|
|
85544
|
+
* D875: save the Alexa name from a device-settings patch. A name Amazon would
|
|
85545
|
+
* refuse throws BEFORE anything is written, so the form shows the reason and
|
|
85546
|
+
* the old name stays. True when the saved name changed.
|
|
85547
|
+
*/
|
|
85548
|
+
async applyAlexaNamePatch(deviceId, patch) {
|
|
85549
|
+
const key = alexaNameKey(deviceId);
|
|
85550
|
+
if (!(key in patch)) return false;
|
|
85551
|
+
const check = validateAlexaName(patch[key]);
|
|
85552
|
+
if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
|
|
85553
|
+
if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
|
|
85554
|
+
await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
|
|
85555
|
+
this.ctx.logger.info("export-alexa: Alexa name saved", {
|
|
85556
|
+
tags: { deviceId },
|
|
85557
|
+
meta: { name: check.name }
|
|
85558
|
+
});
|
|
85559
|
+
return true;
|
|
85560
|
+
}
|
|
85561
|
+
/**
|
|
83671
85562
|
* Persist the camera's detection preferences from a device-settings patch
|
|
83672
85563
|
* into this addon's per-device store. True when anything changed — the
|
|
83673
85564
|
* camera's Alexa shape then needs a reconcile.
|
|
@@ -83732,6 +85623,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83732
85623
|
const current = this.findEntry(deviceId);
|
|
83733
85624
|
const wasEnabled = current !== null;
|
|
83734
85625
|
const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
|
|
85626
|
+
const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
|
|
83735
85627
|
const enabledKey = `alexa:${deviceId}:enabled`;
|
|
83736
85628
|
const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
|
|
83737
85629
|
const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
|
|
@@ -83757,6 +85649,12 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83757
85649
|
const currentAllow = current?.settings?.allowTranscode !== false;
|
|
83758
85650
|
await this.patchEntrySettings(deviceId, nextSettings);
|
|
83759
85651
|
const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
|
|
85652
|
+
if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
|
|
85653
|
+
this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
|
|
85654
|
+
tags: { deviceId },
|
|
85655
|
+
meta: { error: errMsg(err) }
|
|
85656
|
+
});
|
|
85657
|
+
});
|
|
83760
85658
|
if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
|
|
83761
85659
|
this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
|
|
83762
85660
|
tags: { deviceId },
|