@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
|
@@ -34,7 +34,7 @@ let node_path = require("node:path");
|
|
|
34
34
|
node_path = __toESM(node_path, 1);
|
|
35
35
|
require("node:child_process");
|
|
36
36
|
require("node:zlib");
|
|
37
|
-
//#region ../types/dist/event-category-
|
|
37
|
+
//#region ../types/dist/event-category-BvOFi6PY.mjs
|
|
38
38
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
39
39
|
EventCategory["SystemBoot"] = "system.boot";
|
|
40
40
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -98,6 +98,24 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
98
98
|
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
99
99
|
*/
|
|
100
100
|
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
101
|
+
/**
|
|
102
|
+
* A runner whose addon refused to start with a typed reason (D877) - parked
|
|
103
|
+
* in `refused`, never counted by the crash breaker, retried on operator
|
|
104
|
+
* action. One event per refusal.
|
|
105
|
+
*/
|
|
106
|
+
EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
|
|
107
|
+
/**
|
|
108
|
+
* A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
|
|
109
|
+
* `refused`. In-process telemetry for the boot check (re-evaluate while any
|
|
110
|
+
* runner is still pending) and the alert centre (close a recovered runner).
|
|
111
|
+
*/
|
|
112
|
+
EventCategory["ProcessStateChanged"] = "process.state-changed";
|
|
113
|
+
/**
|
|
114
|
+
* The node's one-shot runner boot check (D877): planned runners against the
|
|
115
|
+
* process service, `degraded` when any is failed, refused or missing. The
|
|
116
|
+
* alert centre turns it into one alert and a direct critical push.
|
|
117
|
+
*/
|
|
118
|
+
EventCategory["SystemRunnerCheck"] = "system.runner-check";
|
|
101
119
|
EventCategory["AddonError"] = "addon.error";
|
|
102
120
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
103
121
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -5422,7 +5440,7 @@ var ZodIssueCode = {
|
|
|
5422
5440
|
var ZodFirstPartyTypeKind;
|
|
5423
5441
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5424
5442
|
//#endregion
|
|
5425
|
-
//#region ../types/dist/sleep-
|
|
5443
|
+
//#region ../types/dist/sleep-bZIhUvjd.mjs
|
|
5426
5444
|
/**
|
|
5427
5445
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5428
5446
|
* window to float samples (D455).
|
|
@@ -5539,7 +5557,7 @@ function isKeychainStoredField(field) {
|
|
|
5539
5557
|
var SECRET_CLEAR = { $secretClear: true };
|
|
5540
5558
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
5541
5559
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
5542
|
-
function isRecord$
|
|
5560
|
+
function isRecord$5(value) {
|
|
5543
5561
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5544
5562
|
}
|
|
5545
5563
|
function makeSecretMarker(ref) {
|
|
@@ -5550,12 +5568,12 @@ function makeSecretMarker(ref) {
|
|
|
5550
5568
|
}
|
|
5551
5569
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
5552
5570
|
function isSecretMarker(value) {
|
|
5553
|
-
if (!isRecord$
|
|
5571
|
+
if (!isRecord$5(value)) return false;
|
|
5554
5572
|
const ref = value["$secret"];
|
|
5555
5573
|
return typeof ref === "string" && ref.length > 0;
|
|
5556
5574
|
}
|
|
5557
5575
|
function isSecretClear(value) {
|
|
5558
|
-
return isRecord$
|
|
5576
|
+
return isRecord$5(value) && value["$secretClear"] === true;
|
|
5559
5577
|
}
|
|
5560
5578
|
/**
|
|
5561
5579
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -6165,7 +6183,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
6165
6183
|
}
|
|
6166
6184
|
};
|
|
6167
6185
|
var KEEP = { kind: "keep" };
|
|
6168
|
-
function isRecord$
|
|
6186
|
+
function isRecord$4(value) {
|
|
6169
6187
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6170
6188
|
}
|
|
6171
6189
|
function defaultMintId() {
|
|
@@ -6178,7 +6196,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
6178
6196
|
return out;
|
|
6179
6197
|
}
|
|
6180
6198
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
6181
|
-
else if (isRecord$
|
|
6199
|
+
else if (isRecord$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
6182
6200
|
return out;
|
|
6183
6201
|
}
|
|
6184
6202
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -6233,7 +6251,7 @@ function rowId(row, idKey) {
|
|
|
6233
6251
|
return null;
|
|
6234
6252
|
}
|
|
6235
6253
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
6236
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
6254
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4) : [];
|
|
6237
6255
|
if (!Array.isArray(incoming)) {
|
|
6238
6256
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
6239
6257
|
return {
|
|
@@ -6249,7 +6267,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
6249
6267
|
const kept = /* @__PURE__ */ new Set();
|
|
6250
6268
|
const rows = [];
|
|
6251
6269
|
for (const row of incoming) {
|
|
6252
|
-
if (!isRecord$
|
|
6270
|
+
if (!isRecord$4(row)) {
|
|
6253
6271
|
rows.push(row);
|
|
6254
6272
|
continue;
|
|
6255
6273
|
}
|
|
@@ -6274,7 +6292,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
6274
6292
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
6275
6293
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
6276
6294
|
case "object": {
|
|
6277
|
-
if (!isRecord$
|
|
6295
|
+
if (!isRecord$4(incoming)) {
|
|
6278
6296
|
if (incoming === void 0) return KEEP;
|
|
6279
6297
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
6280
6298
|
return {
|
|
@@ -6282,7 +6300,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
6282
6300
|
value: incoming
|
|
6283
6301
|
};
|
|
6284
6302
|
}
|
|
6285
|
-
const storedObj = isRecord$
|
|
6303
|
+
const storedObj = isRecord$4(stored) ? stored : {};
|
|
6286
6304
|
return {
|
|
6287
6305
|
kind: "value",
|
|
6288
6306
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -6405,7 +6423,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
6405
6423
|
value: items
|
|
6406
6424
|
};
|
|
6407
6425
|
}
|
|
6408
|
-
if (isRecord$
|
|
6426
|
+
if (isRecord$4(v)) {
|
|
6409
6427
|
const out = {};
|
|
6410
6428
|
for (const [k, child] of Object.entries(v)) {
|
|
6411
6429
|
const s = substitute(child);
|
|
@@ -6673,7 +6691,6 @@ function createHandle(raw, opts) {
|
|
|
6673
6691
|
const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
|
|
6674
6692
|
const readAddonStoreResult = raw.readAddonStoreResult;
|
|
6675
6693
|
const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
|
|
6676
|
-
const readDeviceStoreBatch = raw.readDeviceStoreBatch;
|
|
6677
6694
|
const overrides = {
|
|
6678
6695
|
readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
|
|
6679
6696
|
...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
|
|
@@ -6697,12 +6714,12 @@ function createHandle(raw, opts) {
|
|
|
6697
6714
|
};
|
|
6698
6715
|
} } : {},
|
|
6699
6716
|
readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
|
|
6700
|
-
|
|
6701
|
-
const rows = await readDeviceStoreBatch
|
|
6717
|
+
readDeviceStoreBatch: async (deviceIds) => {
|
|
6718
|
+
const rows = await raw.readDeviceStoreBatch(deviceIds);
|
|
6702
6719
|
const out = /* @__PURE__ */ new Map();
|
|
6703
6720
|
for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
|
|
6704
6721
|
return out;
|
|
6705
|
-
}
|
|
6722
|
+
},
|
|
6706
6723
|
writeAddonStore: async (patch) => {
|
|
6707
6724
|
await writeAddon(patch);
|
|
6708
6725
|
},
|
|
@@ -7329,7 +7346,7 @@ var BaseAddon = class {
|
|
|
7329
7346
|
if (!ctx) return;
|
|
7330
7347
|
const addonId = ctx.id;
|
|
7331
7348
|
setImmediate(() => {
|
|
7332
|
-
ctx.api.addons
|
|
7349
|
+
ctx.api.addons.restartAddon.mutate({ addonId }).then(() => {
|
|
7333
7350
|
ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
|
|
7334
7351
|
}).catch((err) => {
|
|
7335
7352
|
ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -7529,7 +7546,7 @@ var BaseAddon = class {
|
|
|
7529
7546
|
return this._addonSecretTree;
|
|
7530
7547
|
}
|
|
7531
7548
|
deviceSecretTree() {
|
|
7532
|
-
this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
|
|
7549
|
+
this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
|
|
7533
7550
|
return this._deviceSecretTree;
|
|
7534
7551
|
}
|
|
7535
7552
|
/**
|
|
@@ -7605,42 +7622,135 @@ var BaseAddon = class {
|
|
|
7605
7622
|
this[timerOf] = timer;
|
|
7606
7623
|
}
|
|
7607
7624
|
/**
|
|
7608
|
-
* Wrap `ctx.settings.readAddonStore()` with a
|
|
7609
|
-
*
|
|
7610
|
-
* swap, or any race where the SqliteSettingsBackend is between shutdown and
|
|
7611
|
-
* re-initialize) doesn't propagate up into `initialize()` and leave the
|
|
7625
|
+
* Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
|
|
7626
|
+
* settings-store outage doesn't propagate up into `initialize()` and leave the
|
|
7612
7627
|
* addon permanently broken.
|
|
7613
7628
|
*
|
|
7614
|
-
*
|
|
7615
|
-
*
|
|
7616
|
-
* `
|
|
7629
|
+
* Two kinds of outage, two budgets:
|
|
7630
|
+
* - the store's own restart fingerprints (`SqliteSettingsBackend not
|
|
7631
|
+
* initialized`, `provider not available`): a short budget, then the addon
|
|
7632
|
+
* falls back to `{}` (defaults) - its first successful patch rehydrates;
|
|
7633
|
+
* - a TRANSPORT failure (the hub is restarting while this runner boots:
|
|
7634
|
+
* `discovery deadline exceeded`, `parent could not route`, a refused or
|
|
7635
|
+
* timed-out connection). Since D870 a runner's settings read THROWS instead
|
|
7636
|
+
* of answering `{}`, so without a budget a hub restart ends in a respawn
|
|
7637
|
+
* loop. A long, bounded budget; only after it the error propagates - the
|
|
7638
|
+
* addon never runs on defaults for a store it could not ask.
|
|
7639
|
+
* Anything else propagates immediately so real bugs surface.
|
|
7617
7640
|
*/
|
|
7618
7641
|
async readAddonStoreWithRetry() {
|
|
7619
7642
|
const settings = this.rawSettings();
|
|
7620
7643
|
if (!settings) return {};
|
|
7621
|
-
const delaysMs = [
|
|
7622
|
-
150,
|
|
7623
|
-
350,
|
|
7624
|
-
600,
|
|
7625
|
-
900
|
|
7626
|
-
];
|
|
7627
7644
|
let lastErr;
|
|
7628
|
-
for (let attempt = 0
|
|
7645
|
+
for (let attempt = 0;; attempt++) try {
|
|
7629
7646
|
const stored = await settings.readAddonStore() ?? {};
|
|
7630
7647
|
this.settingsStoreReady = true;
|
|
7631
7648
|
return stored;
|
|
7632
7649
|
} catch (err) {
|
|
7633
7650
|
lastErr = err;
|
|
7634
|
-
const
|
|
7635
|
-
if (
|
|
7636
|
-
|
|
7637
|
-
|
|
7651
|
+
const kind = classifyStoreError(err);
|
|
7652
|
+
if (kind === "permanent") throw err;
|
|
7653
|
+
const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
|
|
7654
|
+
if (delay === void 0) {
|
|
7655
|
+
if (kind === "transport") throw err;
|
|
7656
|
+
break;
|
|
7657
|
+
}
|
|
7658
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
7638
7659
|
}
|
|
7639
7660
|
this.settingsStoreReady = false;
|
|
7640
7661
|
this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
|
|
7641
7662
|
return {};
|
|
7642
7663
|
}
|
|
7643
7664
|
};
|
|
7665
|
+
/** The settings store's own restart fingerprints. */
|
|
7666
|
+
var RESTART_RETRY_DELAYS_MS = [
|
|
7667
|
+
150,
|
|
7668
|
+
350,
|
|
7669
|
+
600,
|
|
7670
|
+
900
|
|
7671
|
+
];
|
|
7672
|
+
/** A hub restarting under a booting runner: ~100 s in total. */
|
|
7673
|
+
var TRANSPORT_RETRY_DELAYS_MS = [
|
|
7674
|
+
150,
|
|
7675
|
+
350,
|
|
7676
|
+
600,
|
|
7677
|
+
900,
|
|
7678
|
+
2e3,
|
|
7679
|
+
5e3,
|
|
7680
|
+
1e4,
|
|
7681
|
+
2e4,
|
|
7682
|
+
3e4,
|
|
7683
|
+
3e4
|
|
7684
|
+
];
|
|
7685
|
+
var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
|
|
7686
|
+
/** Node / socket error codes of a connection that did not happen or broke. */
|
|
7687
|
+
var TRANSPORT_CODES = new Set([
|
|
7688
|
+
"ECONNREFUSED",
|
|
7689
|
+
"ECONNRESET",
|
|
7690
|
+
"ETIMEDOUT",
|
|
7691
|
+
"EPIPE",
|
|
7692
|
+
"EHOSTUNREACH",
|
|
7693
|
+
"ENETUNREACH"
|
|
7694
|
+
]);
|
|
7695
|
+
/** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
|
|
7696
|
+
var UNAVAILABLE_TRPC_CODES = new Set([
|
|
7697
|
+
"TIMEOUT",
|
|
7698
|
+
"SERVICE_UNAVAILABLE",
|
|
7699
|
+
"BAD_GATEWAY",
|
|
7700
|
+
"GATEWAY_TIMEOUT"
|
|
7701
|
+
]);
|
|
7702
|
+
/** The exact phrases OUR transport raises when it cannot reach the hub. */
|
|
7703
|
+
var TRANSPORT_PHRASES = [
|
|
7704
|
+
"discovery deadline exceeded",
|
|
7705
|
+
"parent could not route child unowned cap call",
|
|
7706
|
+
"UDS request timed out after",
|
|
7707
|
+
"channel closed"
|
|
7708
|
+
];
|
|
7709
|
+
/** Moleculer error `type`s of a call that could not be served right now. */
|
|
7710
|
+
var MOLECULER_TRANSIENT_TYPES = new Set([
|
|
7711
|
+
"REQUEST_TIMEOUT",
|
|
7712
|
+
"SERVICE_NOT_FOUND",
|
|
7713
|
+
"SERVICE_NOT_AVAILABLE",
|
|
7714
|
+
"REQUEST_REJECTED",
|
|
7715
|
+
"BROKER_DISCONNECTED"
|
|
7716
|
+
]);
|
|
7717
|
+
function stringProp(value, key) {
|
|
7718
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
7719
|
+
const found = Reflect.get(value, key);
|
|
7720
|
+
return typeof found === "string" ? found : void 0;
|
|
7721
|
+
}
|
|
7722
|
+
/** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
|
|
7723
|
+
function errorCode(err) {
|
|
7724
|
+
const out = {};
|
|
7725
|
+
const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
|
|
7726
|
+
const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
|
|
7727
|
+
const node = stringProp(err, "code") ?? stringProp(cause, "code");
|
|
7728
|
+
if (node !== void 0) out.node = node;
|
|
7729
|
+
const trpc = stringProp(data, "code");
|
|
7730
|
+
if (trpc !== void 0) out.trpc = trpc;
|
|
7731
|
+
return out;
|
|
7732
|
+
}
|
|
7733
|
+
/**
|
|
7734
|
+
* Is a settings read failure worth retrying, and with which budget? Only
|
|
7735
|
+
* EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
|
|
7736
|
+
* one of the exact phrases our own transport raises. A message that merely
|
|
7737
|
+
* contains "timed out" or "unreachable" is not evidence of anything, and
|
|
7738
|
+
* `settings store not wired` is a wiring bug that retrying cannot fix.
|
|
7739
|
+
*/
|
|
7740
|
+
function classifyStoreError(err) {
|
|
7741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7742
|
+
if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
|
|
7743
|
+
const code = errorCode(err);
|
|
7744
|
+
if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
|
|
7745
|
+
if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
|
|
7746
|
+
if (typeof err === "object" && err !== null) {
|
|
7747
|
+
if (Reflect.get(err, "retryable") === true) return "transport";
|
|
7748
|
+
const type = stringProp(err, "type");
|
|
7749
|
+
if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
|
|
7750
|
+
}
|
|
7751
|
+
if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
|
|
7752
|
+
return "permanent";
|
|
7753
|
+
}
|
|
7644
7754
|
/**
|
|
7645
7755
|
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
7646
7756
|
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
@@ -7730,6 +7840,89 @@ var PeerBytesTicketSchema = object({
|
|
|
7730
7840
|
*/
|
|
7731
7841
|
declaredBytes: number().int().nonnegative().nullable()
|
|
7732
7842
|
});
|
|
7843
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
7844
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
7845
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
7846
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
7847
|
+
var RECORD_CLASSES = [
|
|
7848
|
+
"config",
|
|
7849
|
+
"ledger",
|
|
7850
|
+
"audit",
|
|
7851
|
+
"mirror",
|
|
7852
|
+
"ephemeral"
|
|
7853
|
+
];
|
|
7854
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
7855
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
7856
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
7857
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
7858
|
+
/**
|
|
7859
|
+
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
7860
|
+
* Zod row schema that describes it, and the helper that turns the two into the
|
|
7861
|
+
* JSON the engine stores.
|
|
7862
|
+
*
|
|
7863
|
+
* A kind is declared ONCE, next to the code that owns it, and the engine's
|
|
7864
|
+
* reconciler creates its indexes - nothing creates DDL at runtime from a write.
|
|
7865
|
+
* Everything the generic machinery needs (secret fields, the entry descriptor)
|
|
7866
|
+
* is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
|
|
7867
|
+
* never a field name (D879).
|
|
7868
|
+
*/
|
|
7869
|
+
/** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
|
|
7870
|
+
var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
|
|
7871
|
+
var RecordIndexSchema = object({
|
|
7872
|
+
name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
7873
|
+
fields: array(RecordFieldPathSchema).min(1).max(4),
|
|
7874
|
+
unique: boolean().optional()
|
|
7875
|
+
}).strict();
|
|
7876
|
+
var RecordRetentionSchema = union([
|
|
7877
|
+
object({
|
|
7878
|
+
ttlMs: number().int().positive(),
|
|
7879
|
+
from: _enum(["at", "write"])
|
|
7880
|
+
}).strict(),
|
|
7881
|
+
object({
|
|
7882
|
+
maxRows: number().int().positive().optional(),
|
|
7883
|
+
maxRowsPerDevice: number().int().positive().optional()
|
|
7884
|
+
}).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
|
|
7885
|
+
object({
|
|
7886
|
+
owner: string().min(1),
|
|
7887
|
+
reason: string().min(1)
|
|
7888
|
+
}).strict(),
|
|
7889
|
+
object({ none: string().min(1) }).strict()
|
|
7890
|
+
]);
|
|
7891
|
+
/** The descriptor a generic entries UI reads (D881 "generic entries"). */
|
|
7892
|
+
var EntryDescriptorSchema = object({
|
|
7893
|
+
title: string().min(1),
|
|
7894
|
+
/** The record body field shown as the entry's name. */
|
|
7895
|
+
nameField: string().min(1),
|
|
7896
|
+
/** JSON Schema of the entry body; `secret: true` marks a password field. */
|
|
7897
|
+
fields: record(string(), unknown())
|
|
7898
|
+
}).strict();
|
|
7899
|
+
/** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
|
|
7900
|
+
var RecordKindDeclarationSchema = object({
|
|
7901
|
+
database: _enum(RECORD_DATABASES).default("settings"),
|
|
7902
|
+
class: _enum(RECORD_CLASSES),
|
|
7903
|
+
writeTrigger: string().min(1),
|
|
7904
|
+
device: _enum([
|
|
7905
|
+
"required",
|
|
7906
|
+
"optional",
|
|
7907
|
+
"none"
|
|
7908
|
+
]).default("none"),
|
|
7909
|
+
at: _enum(["required", "none"]).default("none"),
|
|
7910
|
+
indexes: array(RecordIndexSchema).max(8).default([]),
|
|
7911
|
+
retention: RecordRetentionSchema,
|
|
7912
|
+
deviceRemoval: _enum(["delete", "none"]).default("none"),
|
|
7913
|
+
maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
|
|
7914
|
+
/** Derived from the Zod schema; never written by hand. */
|
|
7915
|
+
secretPaths: array(string().min(1)).default([]),
|
|
7916
|
+
/** The id key of each array that carries a secret (`{ headers: 'name' }`). */
|
|
7917
|
+
secretArrayKeys: record(string(), string()).default({}),
|
|
7918
|
+
/** The legacy table this kind adopts from; the boot guard reads it (D853: no migration code). */
|
|
7919
|
+
adoptsFrom: string().min(1).optional(),
|
|
7920
|
+
/** Kinds that must agree through `transact` sit in one file under one family name. */
|
|
7921
|
+
transactFamily: string().min(1).optional(),
|
|
7922
|
+
entry: EntryDescriptorSchema.optional()
|
|
7923
|
+
}).strict();
|
|
7924
|
+
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
7925
|
+
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
7733
7926
|
/** Shared Zod schemas used across streaming capabilities. */
|
|
7734
7927
|
var CamProfileSchema = _enum([
|
|
7735
7928
|
"high",
|
|
@@ -8217,7 +8410,7 @@ object({
|
|
|
8217
8410
|
params: record(string(), union([string(), number()])).optional()
|
|
8218
8411
|
});
|
|
8219
8412
|
//#endregion
|
|
8220
|
-
//#region ../types/dist/err-msg
|
|
8413
|
+
//#region ../types/dist/err-msg--2RrYiLO.mjs
|
|
8221
8414
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
8222
8415
|
DeviceType["Camera"] = "camera";
|
|
8223
8416
|
DeviceType["Hub"] = "hub";
|
|
@@ -8486,6 +8679,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
8486
8679
|
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
8487
8680
|
* real Switch device for UI rendering / export adapters. */
|
|
8488
8681
|
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
8682
|
+
/** A smart plug: a `switch` whose source says it is an outlet (HA
|
|
8683
|
+
* `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
|
|
8684
|
+
DeviceRole["Outlet"] = "outlet";
|
|
8489
8685
|
/** Generic motion / occupancy / moving event source. Distinct from
|
|
8490
8686
|
* the camera accessory PirSensor role: that one is a camera child;
|
|
8491
8687
|
* this is a standalone HA / 3rd-party motion sensor. */
|
|
@@ -8563,7 +8759,7 @@ function errMsg(err) {
|
|
|
8563
8759
|
return String(err);
|
|
8564
8760
|
}
|
|
8565
8761
|
//#endregion
|
|
8566
|
-
//#region ../types/dist/composition-
|
|
8762
|
+
//#region ../types/dist/composition-DOwQB_Ua.mjs
|
|
8567
8763
|
/**
|
|
8568
8764
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
8569
8765
|
*
|
|
@@ -12024,11 +12220,10 @@ var ClipModelMetaSchema = object({
|
|
|
12024
12220
|
/**
|
|
12025
12221
|
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
12026
12222
|
* per index: two models share an index only when their vectors are ranked
|
|
12027
|
-
* against each other, which for CLIP means never across families.
|
|
12028
|
-
*
|
|
12029
|
-
* `modelId`); any other model gets its own.
|
|
12223
|
+
* against each other, which for CLIP means never across families. Every
|
|
12224
|
+
* model gets its own.
|
|
12030
12225
|
*/
|
|
12031
|
-
vectorIndex: string().regex(/^object-clip
|
|
12226
|
+
vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
|
|
12032
12227
|
/**
|
|
12033
12228
|
* The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
|
|
12034
12229
|
* must be unique per tokenizer: every model's siblings land in ONE flat
|
|
@@ -15681,6 +15876,41 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
15681
15876
|
keyField: string()
|
|
15682
15877
|
}).optional()
|
|
15683
15878
|
})) });
|
|
15879
|
+
/**
|
|
15880
|
+
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
15881
|
+
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
15882
|
+
*
|
|
15883
|
+
* The owner runs it on its own rows, through its own write path; an operator
|
|
15884
|
+
* script (`scripts/hub/migrate-raw-secrets.ts`) calls the owner's cap method.
|
|
15885
|
+
* Dry run (`apply: false`) reports what WOULD move and changes nothing.
|
|
15886
|
+
*/
|
|
15887
|
+
var RawSecretMigrationInputSchema = object({
|
|
15888
|
+
/** `false` (the default of the script) = report only. */
|
|
15889
|
+
apply: boolean() });
|
|
15890
|
+
var RawSecretMigrationReportSchema = object({
|
|
15891
|
+
apply: boolean(),
|
|
15892
|
+
/** Rows that can carry a secret (a profile, an enrolment). */
|
|
15893
|
+
rows: number().int(),
|
|
15894
|
+
/** Rows whose secret was plaintext when the run started. */
|
|
15895
|
+
raw: number().int(),
|
|
15896
|
+
/** Rows that already held a keychain marker when the run started. */
|
|
15897
|
+
markers: number().int(),
|
|
15898
|
+
/** Plaintext rows moved by THIS run (always 0 on a dry run). */
|
|
15899
|
+
migrated: number().int(),
|
|
15900
|
+
/** Plaintext rows left in place because the move or its read-back failed. */
|
|
15901
|
+
failed: number().int(),
|
|
15902
|
+
/** After the run: rows still holding plaintext. */
|
|
15903
|
+
remainingRaw: number().int(),
|
|
15904
|
+
/** After the run: markers the keychain could not resolve right now. */
|
|
15905
|
+
unresolvedMarkers: number().int(),
|
|
15906
|
+
/**
|
|
15907
|
+
* A second, redundant copy of the secrets' rows that the run found (and, on
|
|
15908
|
+
* `apply`, deleted after verifying the first). Owners with a single copy omit it.
|
|
15909
|
+
*/
|
|
15910
|
+
redundantCopies: number().int().optional(),
|
|
15911
|
+
/** True when nothing is raw and every marker resolves. */
|
|
15912
|
+
verified: boolean()
|
|
15913
|
+
});
|
|
15684
15914
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
15685
15915
|
* agent is the reason placement is not fixed to the hub. */
|
|
15686
15916
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -15907,6 +16137,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
15907
16137
|
}), SettingsUpdateResultSchema, {
|
|
15908
16138
|
kind: "mutation",
|
|
15909
16139
|
auth: "admin"
|
|
16140
|
+
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
16141
|
+
kind: "mutation",
|
|
16142
|
+
auth: "admin"
|
|
15910
16143
|
}), method(object({}), object({ libs: array(object({
|
|
15911
16144
|
filePath: string(),
|
|
15912
16145
|
content: string()
|
|
@@ -16170,6 +16403,347 @@ id: union([string(), number()]) }), {
|
|
|
16170
16403
|
auth: "admin"
|
|
16171
16404
|
});
|
|
16172
16405
|
/**
|
|
16406
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
16407
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
16408
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
16409
|
+
*
|
|
16410
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
16411
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
16412
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
16413
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
16414
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
16415
|
+
* addons go through the owner's capability.
|
|
16416
|
+
*
|
|
16417
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
16418
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
16419
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
16420
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
16421
|
+
* row's own ref.
|
|
16422
|
+
*
|
|
16423
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
16424
|
+
*/
|
|
16425
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
16426
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
16427
|
+
var RecordBodySchema = record(string(), unknown());
|
|
16428
|
+
var RecordInputSchema = object({
|
|
16429
|
+
id: RecordIdSchema,
|
|
16430
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
16431
|
+
deviceId: number().int().optional(),
|
|
16432
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
16433
|
+
at: number().int().optional(),
|
|
16434
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
16435
|
+
expiresAt: number().int().optional(),
|
|
16436
|
+
data: RecordBodySchema
|
|
16437
|
+
});
|
|
16438
|
+
var RecordViewSchema = object({
|
|
16439
|
+
id: string(),
|
|
16440
|
+
deviceId: number().int().nullable(),
|
|
16441
|
+
at: number().nullable(),
|
|
16442
|
+
expiresAt: number().nullable(),
|
|
16443
|
+
rev: number().int(),
|
|
16444
|
+
updatedAt: number(),
|
|
16445
|
+
data: RecordBodySchema
|
|
16446
|
+
});
|
|
16447
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
16448
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
16449
|
+
string(),
|
|
16450
|
+
number(),
|
|
16451
|
+
boolean(),
|
|
16452
|
+
object({ in: array(union([
|
|
16453
|
+
string(),
|
|
16454
|
+
number(),
|
|
16455
|
+
boolean()
|
|
16456
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
16457
|
+
object({
|
|
16458
|
+
gt: union([string(), number()]).optional(),
|
|
16459
|
+
gte: union([string(), number()]).optional(),
|
|
16460
|
+
lt: union([string(), number()]).optional(),
|
|
16461
|
+
lte: union([string(), number()]).optional()
|
|
16462
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
16463
|
+
]));
|
|
16464
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
16465
|
+
var TimeWindowShape = {
|
|
16466
|
+
deviceId: number().int().optional(),
|
|
16467
|
+
atFrom: number().optional(),
|
|
16468
|
+
atTo: number().optional()
|
|
16469
|
+
};
|
|
16470
|
+
var RecordQueryInputSchema = object({
|
|
16471
|
+
...KindShape,
|
|
16472
|
+
...TimeWindowShape,
|
|
16473
|
+
where: RecordWhereSchema.optional(),
|
|
16474
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
16475
|
+
order: _enum(["at", "id"]).optional(),
|
|
16476
|
+
desc: boolean().optional(),
|
|
16477
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
16478
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
16479
|
+
cursor: string().min(1).optional()
|
|
16480
|
+
});
|
|
16481
|
+
var RecordQueryResultSchema = object({
|
|
16482
|
+
records: array(RecordViewSchema),
|
|
16483
|
+
nextCursor: string().nullable()
|
|
16484
|
+
});
|
|
16485
|
+
var RecordKindStateSchema = _enum([
|
|
16486
|
+
"created",
|
|
16487
|
+
"unchanged",
|
|
16488
|
+
"updated"
|
|
16489
|
+
]);
|
|
16490
|
+
var DeclareKindResultSchema = object({
|
|
16491
|
+
kindId: number().int(),
|
|
16492
|
+
state: RecordKindStateSchema,
|
|
16493
|
+
createdIndexes: array(string()),
|
|
16494
|
+
/** The boot guard: the kind is empty while its legacy table still holds rows. */
|
|
16495
|
+
adoptionPending: object({
|
|
16496
|
+
legacyTable: string(),
|
|
16497
|
+
legacyRows: number().int()
|
|
16498
|
+
}).nullable()
|
|
16499
|
+
});
|
|
16500
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
16501
|
+
var SecretRefsList = array(string());
|
|
16502
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
16503
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
16504
|
+
kind: "mutation",
|
|
16505
|
+
auth: "admin"
|
|
16506
|
+
}), method(object({
|
|
16507
|
+
...KindShape,
|
|
16508
|
+
id: RecordIdSchema
|
|
16509
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
16510
|
+
...KindShape,
|
|
16511
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16512
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
16513
|
+
...KindShape,
|
|
16514
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16515
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
16516
|
+
...KindShape,
|
|
16517
|
+
record: RecordInputSchema,
|
|
16518
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
16519
|
+
ifRev: number().int().min(0).optional()
|
|
16520
|
+
}), object({
|
|
16521
|
+
rev: number().int(),
|
|
16522
|
+
releasedRefs: SecretRefsList
|
|
16523
|
+
}), {
|
|
16524
|
+
kind: "mutation",
|
|
16525
|
+
auth: "admin"
|
|
16526
|
+
}), method(object({
|
|
16527
|
+
...KindShape,
|
|
16528
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16529
|
+
}), object({
|
|
16530
|
+
count: number().int(),
|
|
16531
|
+
releasedRefs: SecretRefsList
|
|
16532
|
+
}), {
|
|
16533
|
+
kind: "mutation",
|
|
16534
|
+
auth: "admin"
|
|
16535
|
+
}), method(object({
|
|
16536
|
+
...KindShape,
|
|
16537
|
+
id: RecordIdSchema,
|
|
16538
|
+
data: RecordBodySchema,
|
|
16539
|
+
at: number().int().nullable().optional(),
|
|
16540
|
+
expiresAt: number().int().nullable().optional(),
|
|
16541
|
+
ifRev: number().int().min(1).optional()
|
|
16542
|
+
}), object({
|
|
16543
|
+
rev: number().int(),
|
|
16544
|
+
releasedRefs: SecretRefsList
|
|
16545
|
+
}), {
|
|
16546
|
+
kind: "mutation",
|
|
16547
|
+
auth: "admin"
|
|
16548
|
+
}), method(object({
|
|
16549
|
+
...KindShape,
|
|
16550
|
+
id: RecordIdSchema
|
|
16551
|
+
}), object({
|
|
16552
|
+
deleted: boolean(),
|
|
16553
|
+
secretRefs: SecretRefsList
|
|
16554
|
+
}), {
|
|
16555
|
+
kind: "mutation",
|
|
16556
|
+
auth: "admin"
|
|
16557
|
+
}), method(object({
|
|
16558
|
+
...KindShape,
|
|
16559
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
16560
|
+
}), object({
|
|
16561
|
+
deleted: number().int(),
|
|
16562
|
+
secretRefs: SecretRefsList
|
|
16563
|
+
}), {
|
|
16564
|
+
kind: "mutation",
|
|
16565
|
+
auth: "admin"
|
|
16566
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
16567
|
+
...KindShape,
|
|
16568
|
+
...TimeWindowShape,
|
|
16569
|
+
where: RecordWhereSchema.optional()
|
|
16570
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
16571
|
+
...KindShape,
|
|
16572
|
+
deviceId: number().int().optional(),
|
|
16573
|
+
from: number(),
|
|
16574
|
+
to: number(),
|
|
16575
|
+
bucketMs: number().int().positive()
|
|
16576
|
+
}), object({ buckets: array(object({
|
|
16577
|
+
bucketStart: number(),
|
|
16578
|
+
count: number().int()
|
|
16579
|
+
})) }), { auth: "admin" }), method(object({
|
|
16580
|
+
...KindShape,
|
|
16581
|
+
op: _enum([
|
|
16582
|
+
"sum",
|
|
16583
|
+
"min",
|
|
16584
|
+
"max",
|
|
16585
|
+
"count"
|
|
16586
|
+
]),
|
|
16587
|
+
field: RecordFieldPathSchema.optional(),
|
|
16588
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
16589
|
+
where: RecordWhereSchema.optional()
|
|
16590
|
+
}), object({ rows: array(object({
|
|
16591
|
+
group: union([
|
|
16592
|
+
string(),
|
|
16593
|
+
number(),
|
|
16594
|
+
_null()
|
|
16595
|
+
]),
|
|
16596
|
+
value: number().nullable()
|
|
16597
|
+
})) }), { auth: "admin" }), method(object({
|
|
16598
|
+
...KindShape,
|
|
16599
|
+
deviceId: number().int().optional(),
|
|
16600
|
+
atBefore: number().optional(),
|
|
16601
|
+
where: RecordWhereSchema.optional()
|
|
16602
|
+
}), object({
|
|
16603
|
+
deleted: number().int(),
|
|
16604
|
+
secretRefs: SecretRefsList
|
|
16605
|
+
}), {
|
|
16606
|
+
kind: "mutation",
|
|
16607
|
+
auth: "admin"
|
|
16608
|
+
}), method(object({
|
|
16609
|
+
deviceId: number().int(),
|
|
16610
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
16611
|
+
}), object({
|
|
16612
|
+
deleted: record(string(), number().int()),
|
|
16613
|
+
secretRefs: SecretRefsList
|
|
16614
|
+
}), {
|
|
16615
|
+
kind: "mutation",
|
|
16616
|
+
auth: "admin"
|
|
16617
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
16618
|
+
kind: "mutation",
|
|
16619
|
+
auth: "admin"
|
|
16620
|
+
});
|
|
16621
|
+
/** One table of the settings database. */
|
|
16622
|
+
var StorageTableSchema = object({
|
|
16623
|
+
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
16624
|
+
name: string(),
|
|
16625
|
+
/** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
|
|
16626
|
+
owner: string().nullable(),
|
|
16627
|
+
rows: number().int().nonnegative(),
|
|
16628
|
+
/** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
|
|
16629
|
+
bytes: number().int().nonnegative().nullable(),
|
|
16630
|
+
/** Declared by an addon since this engine process started (a running addon claims it). */
|
|
16631
|
+
declaredThisBoot: boolean()
|
|
16632
|
+
});
|
|
16633
|
+
/** One declared record kind (D881) as System -> Data shows it. */
|
|
16634
|
+
var StorageKindSchema = object({
|
|
16635
|
+
owner: string(),
|
|
16636
|
+
kind: string(),
|
|
16637
|
+
database: _enum(["settings", "events"]),
|
|
16638
|
+
class: string(),
|
|
16639
|
+
state: _enum([
|
|
16640
|
+
"declared",
|
|
16641
|
+
"orphaned",
|
|
16642
|
+
"dropped"
|
|
16643
|
+
]),
|
|
16644
|
+
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
16645
|
+
rows: number().int().nonnegative().nullable(),
|
|
16646
|
+
/** Rows x the average size of a sample; null for an empty kind. */
|
|
16647
|
+
bytesEstimate: number().int().nonnegative().nullable(),
|
|
16648
|
+
writeTrigger: string(),
|
|
16649
|
+
retention: RecordRetentionSchema,
|
|
16650
|
+
/** `true` when the kind is an entry kind (the generic entries UI lists it). */
|
|
16651
|
+
entry: boolean(),
|
|
16652
|
+
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
16653
|
+
declaration: RecordKindDeclarationSchema
|
|
16654
|
+
});
|
|
16655
|
+
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
16656
|
+
collection: string().min(1),
|
|
16657
|
+
expectRows: number().int().nonnegative()
|
|
16658
|
+
}), object({
|
|
16659
|
+
dropped: literal(true),
|
|
16660
|
+
rows: number().int().nonnegative()
|
|
16661
|
+
}), {
|
|
16662
|
+
kind: "mutation",
|
|
16663
|
+
auth: "admin"
|
|
16664
|
+
}), method(object({
|
|
16665
|
+
owner: string().min(1).optional(),
|
|
16666
|
+
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
16667
|
+
counts: boolean().optional()
|
|
16668
|
+
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
16669
|
+
owner: string().min(1),
|
|
16670
|
+
kind: RecordKindNameSchema
|
|
16671
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
16672
|
+
owner: string().min(1),
|
|
16673
|
+
kind: RecordKindNameSchema,
|
|
16674
|
+
records: array(RecordInputSchema).max(RECORD_BATCH_MAX),
|
|
16675
|
+
/** The LAST batch: the copy is complete, the boot guard stops asking. */
|
|
16676
|
+
complete: boolean().optional()
|
|
16677
|
+
}).refine((input) => input.records.length > 0 || input.complete === true, { message: "records must be non-empty unless complete is true" }), object({ adopted: number().int() }), {
|
|
16678
|
+
kind: "mutation",
|
|
16679
|
+
auth: "admin"
|
|
16680
|
+
}), method(object({
|
|
16681
|
+
collection: string().min(1),
|
|
16682
|
+
cursor: string().optional(),
|
|
16683
|
+
limit: number().int().min(1).max(1e3).optional()
|
|
16684
|
+
}), object({
|
|
16685
|
+
rows: array(object({
|
|
16686
|
+
id: string(),
|
|
16687
|
+
data: record(string(), unknown())
|
|
16688
|
+
})),
|
|
16689
|
+
nextCursor: string().nullable()
|
|
16690
|
+
}), { auth: "admin" }), method(object({ collection: string().min(1) }), object({ adopted: boolean() }), { auth: "admin" });
|
|
16691
|
+
/**
|
|
16692
|
+
* record-entries - the ONE admin surface for every list of user-created entries
|
|
16693
|
+
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
16694
|
+
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
16695
|
+
*
|
|
16696
|
+
* The UI is generic. `listEntryKinds` hands it, per kind, the JSON Schema of
|
|
16697
|
+
* the entry body with `secret: true` on every credential field, so a password
|
|
16698
|
+
* field is derived from the schema and never from a name. `putEntry` takes the
|
|
16699
|
+
* redaction token for an untouched secret (the stored value is kept), a string
|
|
16700
|
+
* for a new one, and the clear instruction to remove one. A read never carries
|
|
16701
|
+
* a secret value, and a locked keychain refuses the write with
|
|
16702
|
+
* `records/secret-unavailable` instead of storing plaintext.
|
|
16703
|
+
*
|
|
16704
|
+
* Hub-only, served in-process (it acts for each entry's owner on the keychain).
|
|
16705
|
+
*/
|
|
16706
|
+
var EntryKindInfoSchema = object({
|
|
16707
|
+
owner: string(),
|
|
16708
|
+
kind: RecordKindNameSchema,
|
|
16709
|
+
title: string(),
|
|
16710
|
+
nameField: string(),
|
|
16711
|
+
/** JSON Schema of the body; `secret: true` marks a password field. */
|
|
16712
|
+
fields: record(string(), unknown()),
|
|
16713
|
+
secretPaths: array(string()),
|
|
16714
|
+
rows: number().int().nonnegative()
|
|
16715
|
+
});
|
|
16716
|
+
var EntryViewSchema = object({
|
|
16717
|
+
id: RecordIdSchema,
|
|
16718
|
+
name: string(),
|
|
16719
|
+
rev: number().int(),
|
|
16720
|
+
updatedAt: number(),
|
|
16721
|
+
/** Secret fields hold the redaction token. */
|
|
16722
|
+
data: RecordBodySchema
|
|
16723
|
+
});
|
|
16724
|
+
var EntryAddress = {
|
|
16725
|
+
owner: string().min(1),
|
|
16726
|
+
kind: RecordKindNameSchema
|
|
16727
|
+
};
|
|
16728
|
+
method(object({}), object({ kinds: array(EntryKindInfoSchema) }), { auth: "admin" }), method(object({ ...EntryAddress }), object({ entries: array(EntryViewSchema) }), { auth: "admin" }), method(object({
|
|
16729
|
+
...EntryAddress,
|
|
16730
|
+
id: RecordIdSchema.optional(),
|
|
16731
|
+
data: RecordBodySchema,
|
|
16732
|
+
ifRev: number().int().min(1).optional()
|
|
16733
|
+
}), object({
|
|
16734
|
+
id: string(),
|
|
16735
|
+
rev: number().int()
|
|
16736
|
+
}), {
|
|
16737
|
+
kind: "mutation",
|
|
16738
|
+
auth: "admin"
|
|
16739
|
+
}), method(object({
|
|
16740
|
+
...EntryAddress,
|
|
16741
|
+
id: RecordIdSchema
|
|
16742
|
+
}), object({ deleted: boolean() }), {
|
|
16743
|
+
kind: "mutation",
|
|
16744
|
+
auth: "admin"
|
|
16745
|
+
});
|
|
16746
|
+
/**
|
|
16173
16747
|
* `keychain` — the hub's encrypted store of secret values (D787).
|
|
16174
16748
|
*
|
|
16175
16749
|
* Settings blobs never hold a secret: they hold a marker
|
|
@@ -16710,6 +17284,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
|
|
|
16710
17284
|
kind: "mutation",
|
|
16711
17285
|
auth: "admin",
|
|
16712
17286
|
caller: "required"
|
|
17287
|
+
}), method(object({
|
|
17288
|
+
ref: string().min(1),
|
|
17289
|
+
value: string().min(1)
|
|
17290
|
+
}), object({
|
|
17291
|
+
ref: string(),
|
|
17292
|
+
version: number().int()
|
|
17293
|
+
}), {
|
|
17294
|
+
kind: "mutation",
|
|
17295
|
+
auth: "admin",
|
|
17296
|
+
caller: "required"
|
|
16713
17297
|
}), method(object({ refs: array(string().min(1)).max(1e3) }), object({
|
|
16714
17298
|
deleted: array(string()).readonly(),
|
|
16715
17299
|
kept: array(object({
|
|
@@ -39204,6 +39788,9 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
|
|
|
39204
39788
|
}), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
|
|
39205
39789
|
kind: "mutation",
|
|
39206
39790
|
auth: "admin"
|
|
39791
|
+
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
39792
|
+
kind: "mutation",
|
|
39793
|
+
auth: "admin"
|
|
39207
39794
|
}), method(ProfileRefInputSchema, _void(), {
|
|
39208
39795
|
kind: "mutation",
|
|
39209
39796
|
auth: "admin"
|
|
@@ -49643,6 +50230,41 @@ var ServerRollbackInfoSchema = object({
|
|
|
49643
50230
|
atMs: number(),
|
|
49644
50231
|
reason: string()
|
|
49645
50232
|
});
|
|
50233
|
+
/**
|
|
50234
|
+
* The runner boot check (D877): the runners this node PLANNED (hub: one per
|
|
50235
|
+
* `resolveRunnerId` over its forked addons; agent: the runners its process
|
|
50236
|
+
* service holds, because only the hub plans) against the states the process
|
|
50237
|
+
* service reports. Kept in memory, recomputed by the same one-shot check after
|
|
50238
|
+
* every boot, served on `getServerPackageStatus`.
|
|
50239
|
+
*
|
|
50240
|
+
* `failed` is the D6 breaker's terminal state, `refused` a deterministic Init
|
|
50241
|
+
* refusal (an addon that said "I will not start, and here is why"), `missing` a
|
|
50242
|
+
* planned runner the process service has no entry for.
|
|
50243
|
+
*/
|
|
50244
|
+
var RunnerRefusalSchema = object({
|
|
50245
|
+
runnerId: string(),
|
|
50246
|
+
addonIds: array(string()),
|
|
50247
|
+
reason: string()
|
|
50248
|
+
});
|
|
50249
|
+
var RunnerBootCheckSchema = object({
|
|
50250
|
+
checkedAtMs: number(),
|
|
50251
|
+
/** Runners planned for this node. */
|
|
50252
|
+
expected: number(),
|
|
50253
|
+
/** Planned runners whose process is `running`. */
|
|
50254
|
+
running: number(),
|
|
50255
|
+
failed: array(string()),
|
|
50256
|
+
refused: array(RunnerRefusalSchema),
|
|
50257
|
+
missing: array(string()),
|
|
50258
|
+
/**
|
|
50259
|
+
* Runners still on their way: `starting`, or `crashed` waiting on a backoff
|
|
50260
|
+
* respawn. The check is FINAL only when this is empty; until then nothing is
|
|
50261
|
+
* pushed and the verdict is re-evaluated on the next runner state change.
|
|
50262
|
+
* Optional for version skew (absent = settled).
|
|
50263
|
+
*/
|
|
50264
|
+
pending: array(string()).optional(),
|
|
50265
|
+
/** True when the plan is the hub's (`missing` is then meaningful); false on an agent. */
|
|
50266
|
+
planKnown: boolean()
|
|
50267
|
+
});
|
|
49646
50268
|
var ServerPackageStatusSchema = object({
|
|
49647
50269
|
/** Root package name (`@camstack/server` on the hub). */
|
|
49648
50270
|
packageName: string(),
|
|
@@ -49705,7 +50327,14 @@ var ServerPackageStatusSchema = object({
|
|
|
49705
50327
|
* for version skew: an older provider's payload simply omits it, and an
|
|
49706
50328
|
* absent wrapper is unknown — never `native`.
|
|
49707
50329
|
*/
|
|
49708
|
-
wrapper: WrapperIdentitySchema.optional()
|
|
50330
|
+
wrapper: WrapperIdentitySchema.optional(),
|
|
50331
|
+
/**
|
|
50332
|
+
* What this node PLANNED to run against what its process service reports
|
|
50333
|
+
* (D877). Absent until the one-shot boot check has run (~60 s after boot) and
|
|
50334
|
+
* on a provider that predates it. Absent is "not checked yet", never "all
|
|
50335
|
+
* well": a consumer that needs the answer treats it as pending.
|
|
50336
|
+
*/
|
|
50337
|
+
runners: RunnerBootCheckSchema.optional()
|
|
49709
50338
|
});
|
|
49710
50339
|
var ServerUpdateCheckResultSchema = object({
|
|
49711
50340
|
packageName: string(),
|
|
@@ -49756,6 +50385,13 @@ var ServerUpdateOutcomeSchema = _enum([
|
|
|
49756
50385
|
"refused",
|
|
49757
50386
|
"failed"
|
|
49758
50387
|
]);
|
|
50388
|
+
/** One addon the pre-flight found the target version would refuse (D877). */
|
|
50389
|
+
var UpdateBlockerSchema = object({
|
|
50390
|
+
addonId: string(),
|
|
50391
|
+
reason: string(),
|
|
50392
|
+
/** What the operator does about it. */
|
|
50393
|
+
remedy: string()
|
|
50394
|
+
});
|
|
49759
50395
|
var ServerUpdateActionResultSchema = object({
|
|
49760
50396
|
accepted: boolean(),
|
|
49761
50397
|
targetVersion: string().nullable(),
|
|
@@ -49773,14 +50409,27 @@ var ServerUpdateActionResultSchema = object({
|
|
|
49773
50409
|
* The failure itself, verbatim enough to act on (the npm output for a
|
|
49774
50410
|
* staging install). Non-null only for `failed`.
|
|
49775
50411
|
*/
|
|
49776
|
-
error: string().nullable().optional()
|
|
50412
|
+
error: string().nullable().optional(),
|
|
50413
|
+
/**
|
|
50414
|
+
* The addons the target version would refuse to start on this node's live
|
|
50415
|
+
* store (D877), present when the pre-flight refused the update
|
|
50416
|
+
* (`reason: 'addon-would-refuse'`). Optional for version skew.
|
|
50417
|
+
*/
|
|
50418
|
+
blockers: array(UpdateBlockerSchema).optional()
|
|
49777
50419
|
});
|
|
49778
50420
|
method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
|
|
49779
50421
|
kind: "mutation",
|
|
49780
50422
|
auth: "admin"
|
|
49781
|
-
}), method(object({
|
|
49782
|
-
/** Explicit target version; omitted = latest from the registry. */
|
|
49783
|
-
version: string().optional()
|
|
50423
|
+
}), method(object({
|
|
50424
|
+
/** Explicit target version; omitted = latest from the registry. */
|
|
50425
|
+
version: string().optional(),
|
|
50426
|
+
/**
|
|
50427
|
+
* Apply even though the pre-flight (D877) found addons the target would
|
|
50428
|
+
* refuse to start. Off by default: those addons stay down until the
|
|
50429
|
+
* remedy is run, which is the incident this gate exists to prevent.
|
|
50430
|
+
*/
|
|
50431
|
+
acknowledgeRefusals: boolean().optional()
|
|
50432
|
+
}), ServerUpdateActionResultSchema, {
|
|
49784
50433
|
kind: "mutation",
|
|
49785
50434
|
auth: "admin",
|
|
49786
50435
|
timeoutMs: 16 * 6e4
|
|
@@ -52193,7 +52842,9 @@ var ManifestShapeSchema = object({
|
|
|
52193
52842
|
var AddonProcessSchema = object({
|
|
52194
52843
|
pid: number().optional(),
|
|
52195
52844
|
mode: string(),
|
|
52196
|
-
state: string()
|
|
52845
|
+
state: string(),
|
|
52846
|
+
/** Why the runner is `refused` (an addon's own words), when it is (D877). */
|
|
52847
|
+
reason: string().optional()
|
|
52197
52848
|
});
|
|
52198
52849
|
/**
|
|
52199
52850
|
* Per-addon health snapshot from the kernel's AddonHealthMonitor.
|
|
@@ -53167,7 +53818,7 @@ var HvacModeSchema = _enum([
|
|
|
53167
53818
|
"fan_only",
|
|
53168
53819
|
"dry"
|
|
53169
53820
|
]);
|
|
53170
|
-
object({
|
|
53821
|
+
var ClimateControlStatusSchema = object({
|
|
53171
53822
|
/** Active HVAC mode. */
|
|
53172
53823
|
mode: HvacModeSchema,
|
|
53173
53824
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -53208,6 +53859,12 @@ object({
|
|
|
53208
53859
|
/** Horizontal louver swing on/off. Null when the device has no
|
|
53209
53860
|
* horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
|
|
53210
53861
|
swingHorizontal: boolean().nullable(),
|
|
53862
|
+
/** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53863
|
+
minTemp: number().nullable().optional(),
|
|
53864
|
+
/** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
|
|
53865
|
+
maxTemp: number().nullable().optional(),
|
|
53866
|
+
/** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
|
|
53867
|
+
tempStep: number().positive().nullable().optional(),
|
|
53211
53868
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
53212
53869
|
lastFetchedAt: number()
|
|
53213
53870
|
});
|
|
@@ -55048,7 +55705,19 @@ var TlsStatusSchema = object({
|
|
|
55048
55705
|
sans: array(string()),
|
|
55049
55706
|
caCertPem: string().nullable(),
|
|
55050
55707
|
reissueError: string().nullable(),
|
|
55051
|
-
restartRequired: boolean()
|
|
55708
|
+
restartRequired: boolean(),
|
|
55709
|
+
/** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
|
|
55710
|
+
caMissing: boolean(),
|
|
55711
|
+
/** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
|
|
55712
|
+
localCaConstrained: boolean().nullable(),
|
|
55713
|
+
/** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
|
|
55714
|
+
caReplacedAt: string().nullable(),
|
|
55715
|
+
/** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
|
|
55716
|
+
storedUpload: object({
|
|
55717
|
+
leafFingerprintSha256: string(),
|
|
55718
|
+
validTo: string(),
|
|
55719
|
+
hasCa: boolean()
|
|
55720
|
+
}).nullable()
|
|
55052
55721
|
});
|
|
55053
55722
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
55054
55723
|
/**
|
|
@@ -55104,6 +55773,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
55104
55773
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
55105
55774
|
kind: "mutation",
|
|
55106
55775
|
auth: "admin"
|
|
55776
|
+
}), method(_void(), TlsStatusSchema, {
|
|
55777
|
+
kind: "mutation",
|
|
55778
|
+
auth: "admin"
|
|
55107
55779
|
});
|
|
55108
55780
|
var LockControlStatusSchema = object({
|
|
55109
55781
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
@@ -60256,7 +60928,11 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
60256
60928
|
kind: "mutation",
|
|
60257
60929
|
auth: "admin",
|
|
60258
60930
|
access: "delete"
|
|
60259
|
-
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(
|
|
60931
|
+
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
60932
|
+
kind: "mutation",
|
|
60933
|
+
auth: "admin",
|
|
60934
|
+
access: "create"
|
|
60935
|
+
}), method(object({
|
|
60260
60936
|
userId: string(),
|
|
60261
60937
|
code: string()
|
|
60262
60938
|
}), object({ valid: boolean() }), {
|
|
@@ -62457,6 +63133,12 @@ Object.freeze({
|
|
|
62457
63133
|
addonId: null,
|
|
62458
63134
|
access: "view"
|
|
62459
63135
|
},
|
|
63136
|
+
"coreBlocks.migrateRawSecrets": {
|
|
63137
|
+
capName: "core-blocks",
|
|
63138
|
+
capScope: "system",
|
|
63139
|
+
addonId: null,
|
|
63140
|
+
access: "create"
|
|
63141
|
+
},
|
|
62460
63142
|
"coreBlocks.restart": {
|
|
62461
63143
|
capName: "core-blocks",
|
|
62462
63144
|
capScope: "system",
|
|
@@ -64143,6 +64825,12 @@ Object.freeze({
|
|
|
64143
64825
|
addonId: null,
|
|
64144
64826
|
access: "create"
|
|
64145
64827
|
},
|
|
64828
|
+
"keychainAdmin.importSecret": {
|
|
64829
|
+
capName: "keychain-admin",
|
|
64830
|
+
capScope: "system",
|
|
64831
|
+
addonId: null,
|
|
64832
|
+
access: "create"
|
|
64833
|
+
},
|
|
64146
64834
|
"keychainAdmin.listAudit": {
|
|
64147
64835
|
capName: "keychain-admin",
|
|
64148
64836
|
capScope: "system",
|
|
@@ -64311,6 +64999,12 @@ Object.freeze({
|
|
|
64311
64999
|
addonId: null,
|
|
64312
65000
|
access: "view"
|
|
64313
65001
|
},
|
|
65002
|
+
"llm.migrateRawSecrets": {
|
|
65003
|
+
capName: "llm",
|
|
65004
|
+
capScope: "system",
|
|
65005
|
+
addonId: null,
|
|
65006
|
+
access: "create"
|
|
65007
|
+
},
|
|
64314
65008
|
"llm.resolveModelRef": {
|
|
64315
65009
|
capName: "llm",
|
|
64316
65010
|
capScope: "system",
|
|
@@ -64539,6 +65233,12 @@ Object.freeze({
|
|
|
64539
65233
|
addonId: null,
|
|
64540
65234
|
access: "create"
|
|
64541
65235
|
},
|
|
65236
|
+
"localNetwork.useStoredUploadedCertificate": {
|
|
65237
|
+
capName: "local-network",
|
|
65238
|
+
capScope: "system",
|
|
65239
|
+
addonId: null,
|
|
65240
|
+
access: "create"
|
|
65241
|
+
},
|
|
64542
65242
|
"lockControl.getStatus": {
|
|
64543
65243
|
capName: "lock-control",
|
|
64544
65244
|
capScope: "device",
|
|
@@ -67191,6 +67891,30 @@ Object.freeze({
|
|
|
67191
67891
|
addonId: null,
|
|
67192
67892
|
access: "create"
|
|
67193
67893
|
},
|
|
67894
|
+
"recordEntries.deleteEntry": {
|
|
67895
|
+
capName: "record-entries",
|
|
67896
|
+
capScope: "system",
|
|
67897
|
+
addonId: null,
|
|
67898
|
+
access: "delete"
|
|
67899
|
+
},
|
|
67900
|
+
"recordEntries.listEntries": {
|
|
67901
|
+
capName: "record-entries",
|
|
67902
|
+
capScope: "system",
|
|
67903
|
+
addonId: null,
|
|
67904
|
+
access: "view"
|
|
67905
|
+
},
|
|
67906
|
+
"recordEntries.listEntryKinds": {
|
|
67907
|
+
capName: "record-entries",
|
|
67908
|
+
capScope: "system",
|
|
67909
|
+
addonId: null,
|
|
67910
|
+
access: "view"
|
|
67911
|
+
},
|
|
67912
|
+
"recordEntries.putEntry": {
|
|
67913
|
+
capName: "record-entries",
|
|
67914
|
+
capScope: "system",
|
|
67915
|
+
addonId: null,
|
|
67916
|
+
access: "create"
|
|
67917
|
+
},
|
|
67194
67918
|
"recording.getAvailability": {
|
|
67195
67919
|
capName: "recording",
|
|
67196
67920
|
capScope: "device",
|
|
@@ -67491,6 +68215,102 @@ Object.freeze({
|
|
|
67491
68215
|
addonId: null,
|
|
67492
68216
|
access: "view"
|
|
67493
68217
|
},
|
|
68218
|
+
"records.aggregate": {
|
|
68219
|
+
capName: "records",
|
|
68220
|
+
capScope: "system",
|
|
68221
|
+
addonId: null,
|
|
68222
|
+
access: "view"
|
|
68223
|
+
},
|
|
68224
|
+
"records.allocateId": {
|
|
68225
|
+
capName: "records",
|
|
68226
|
+
capScope: "system",
|
|
68227
|
+
addonId: null,
|
|
68228
|
+
access: "create"
|
|
68229
|
+
},
|
|
68230
|
+
"records.count": {
|
|
68231
|
+
capName: "records",
|
|
68232
|
+
capScope: "system",
|
|
68233
|
+
addonId: null,
|
|
68234
|
+
access: "view"
|
|
68235
|
+
},
|
|
68236
|
+
"records.declareKinds": {
|
|
68237
|
+
capName: "records",
|
|
68238
|
+
capScope: "system",
|
|
68239
|
+
addonId: null,
|
|
68240
|
+
access: "create"
|
|
68241
|
+
},
|
|
68242
|
+
"records.delete": {
|
|
68243
|
+
capName: "records",
|
|
68244
|
+
capScope: "system",
|
|
68245
|
+
addonId: null,
|
|
68246
|
+
access: "delete"
|
|
68247
|
+
},
|
|
68248
|
+
"records.deleteDevice": {
|
|
68249
|
+
capName: "records",
|
|
68250
|
+
capScope: "system",
|
|
68251
|
+
addonId: null,
|
|
68252
|
+
access: "delete"
|
|
68253
|
+
},
|
|
68254
|
+
"records.deleteMany": {
|
|
68255
|
+
capName: "records",
|
|
68256
|
+
capScope: "system",
|
|
68257
|
+
addonId: null,
|
|
68258
|
+
access: "delete"
|
|
68259
|
+
},
|
|
68260
|
+
"records.deleteWhere": {
|
|
68261
|
+
capName: "records",
|
|
68262
|
+
capScope: "system",
|
|
68263
|
+
addonId: null,
|
|
68264
|
+
access: "delete"
|
|
68265
|
+
},
|
|
68266
|
+
"records.get": {
|
|
68267
|
+
capName: "records",
|
|
68268
|
+
capScope: "system",
|
|
68269
|
+
addonId: null,
|
|
68270
|
+
access: "view"
|
|
68271
|
+
},
|
|
68272
|
+
"records.getMany": {
|
|
68273
|
+
capName: "records",
|
|
68274
|
+
capScope: "system",
|
|
68275
|
+
addonId: null,
|
|
68276
|
+
access: "view"
|
|
68277
|
+
},
|
|
68278
|
+
"records.histogram": {
|
|
68279
|
+
capName: "records",
|
|
68280
|
+
capScope: "system",
|
|
68281
|
+
addonId: null,
|
|
68282
|
+
access: "view"
|
|
68283
|
+
},
|
|
68284
|
+
"records.patch": {
|
|
68285
|
+
capName: "records",
|
|
68286
|
+
capScope: "system",
|
|
68287
|
+
addonId: null,
|
|
68288
|
+
access: "create"
|
|
68289
|
+
},
|
|
68290
|
+
"records.put": {
|
|
68291
|
+
capName: "records",
|
|
68292
|
+
capScope: "system",
|
|
68293
|
+
addonId: null,
|
|
68294
|
+
access: "create"
|
|
68295
|
+
},
|
|
68296
|
+
"records.putMany": {
|
|
68297
|
+
capName: "records",
|
|
68298
|
+
capScope: "system",
|
|
68299
|
+
addonId: null,
|
|
68300
|
+
access: "create"
|
|
68301
|
+
},
|
|
68302
|
+
"records.query": {
|
|
68303
|
+
capName: "records",
|
|
68304
|
+
capScope: "system",
|
|
68305
|
+
addonId: null,
|
|
68306
|
+
access: "view"
|
|
68307
|
+
},
|
|
68308
|
+
"records.secretRefs": {
|
|
68309
|
+
capName: "records",
|
|
68310
|
+
capScope: "system",
|
|
68311
|
+
addonId: null,
|
|
68312
|
+
access: "view"
|
|
68313
|
+
},
|
|
67494
68314
|
"sceneMonitor.captureReference": {
|
|
67495
68315
|
capName: "scene-monitor",
|
|
67496
68316
|
capScope: "device",
|
|
@@ -67923,6 +68743,48 @@ Object.freeze({
|
|
|
67923
68743
|
addonId: null,
|
|
67924
68744
|
access: "create"
|
|
67925
68745
|
},
|
|
68746
|
+
"storageAdmin.adoptRecords": {
|
|
68747
|
+
capName: "storage-admin",
|
|
68748
|
+
capScope: "system",
|
|
68749
|
+
addonId: null,
|
|
68750
|
+
access: "create"
|
|
68751
|
+
},
|
|
68752
|
+
"storageAdmin.dropCollection": {
|
|
68753
|
+
capName: "storage-admin",
|
|
68754
|
+
capScope: "system",
|
|
68755
|
+
addonId: null,
|
|
68756
|
+
access: "delete"
|
|
68757
|
+
},
|
|
68758
|
+
"storageAdmin.isAdoptedLegacyTable": {
|
|
68759
|
+
capName: "storage-admin",
|
|
68760
|
+
capScope: "system",
|
|
68761
|
+
addonId: null,
|
|
68762
|
+
access: "view"
|
|
68763
|
+
},
|
|
68764
|
+
"storageAdmin.listKinds": {
|
|
68765
|
+
capName: "storage-admin",
|
|
68766
|
+
capScope: "system",
|
|
68767
|
+
addonId: null,
|
|
68768
|
+
access: "view"
|
|
68769
|
+
},
|
|
68770
|
+
"storageAdmin.listTables": {
|
|
68771
|
+
capName: "storage-admin",
|
|
68772
|
+
capScope: "system",
|
|
68773
|
+
addonId: null,
|
|
68774
|
+
access: "view"
|
|
68775
|
+
},
|
|
68776
|
+
"storageAdmin.readLegacyCollection": {
|
|
68777
|
+
capName: "storage-admin",
|
|
68778
|
+
capScope: "system",
|
|
68779
|
+
addonId: null,
|
|
68780
|
+
access: "view"
|
|
68781
|
+
},
|
|
68782
|
+
"storageAdmin.readRecords": {
|
|
68783
|
+
capName: "storage-admin",
|
|
68784
|
+
capScope: "system",
|
|
68785
|
+
addonId: null,
|
|
68786
|
+
access: "view"
|
|
68787
|
+
},
|
|
67926
68788
|
"storageEvictable.evict": {
|
|
67927
68789
|
capName: "storage-evictable",
|
|
67928
68790
|
capScope: "system",
|
|
@@ -68835,6 +69697,12 @@ Object.freeze({
|
|
|
68835
69697
|
addonId: null,
|
|
68836
69698
|
access: "view"
|
|
68837
69699
|
},
|
|
69700
|
+
"userManagement.migrateRawSecrets": {
|
|
69701
|
+
capName: "user-management",
|
|
69702
|
+
capScope: "system",
|
|
69703
|
+
addonId: null,
|
|
69704
|
+
access: "create"
|
|
69705
|
+
},
|
|
68838
69706
|
"userManagement.oauthExchangeCode": {
|
|
68839
69707
|
capName: "user-management",
|
|
68840
69708
|
capScope: "system",
|
|
@@ -71511,6 +72379,37 @@ Object.freeze({
|
|
|
71511
72379
|
form: "single",
|
|
71512
72380
|
optional: false
|
|
71513
72381
|
}],
|
|
72382
|
+
"records.count": [{
|
|
72383
|
+
name: "deviceId",
|
|
72384
|
+
form: "single",
|
|
72385
|
+
optional: true
|
|
72386
|
+
}],
|
|
72387
|
+
"records.deleteDevice": [{
|
|
72388
|
+
name: "deviceId",
|
|
72389
|
+
form: "single",
|
|
72390
|
+
optional: false
|
|
72391
|
+
}],
|
|
72392
|
+
"records.deleteWhere": [{
|
|
72393
|
+
name: "deviceId",
|
|
72394
|
+
form: "single",
|
|
72395
|
+
optional: true
|
|
72396
|
+
}],
|
|
72397
|
+
"records.histogram": [{
|
|
72398
|
+
name: "deviceId",
|
|
72399
|
+
form: "single",
|
|
72400
|
+
optional: true
|
|
72401
|
+
}],
|
|
72402
|
+
"records.putMany": [{
|
|
72403
|
+
name: "records",
|
|
72404
|
+
form: "object-array",
|
|
72405
|
+
optional: false,
|
|
72406
|
+
itemField: "deviceId"
|
|
72407
|
+
}],
|
|
72408
|
+
"records.query": [{
|
|
72409
|
+
name: "deviceId",
|
|
72410
|
+
form: "single",
|
|
72411
|
+
optional: true
|
|
72412
|
+
}],
|
|
71514
72413
|
"sceneMonitor.captureReference": [{
|
|
71515
72414
|
name: "deviceId",
|
|
71516
72415
|
form: "single",
|
|
@@ -71627,6 +72526,17 @@ Object.freeze({
|
|
|
71627
72526
|
form: "single",
|
|
71628
72527
|
optional: false
|
|
71629
72528
|
}],
|
|
72529
|
+
"storageAdmin.adoptRecords": [{
|
|
72530
|
+
name: "records",
|
|
72531
|
+
form: "object-array",
|
|
72532
|
+
optional: false,
|
|
72533
|
+
itemField: "deviceId"
|
|
72534
|
+
}],
|
|
72535
|
+
"storageAdmin.readRecords": [{
|
|
72536
|
+
name: "deviceId",
|
|
72537
|
+
form: "single",
|
|
72538
|
+
optional: true
|
|
72539
|
+
}],
|
|
71630
72540
|
"streamBroker.acquireEgressTranscode": [{
|
|
71631
72541
|
name: "deviceId",
|
|
71632
72542
|
form: "single",
|
|
@@ -74027,6 +74937,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
|
|
|
74027
74937
|
await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
|
|
74028
74938
|
}
|
|
74029
74939
|
//#endregion
|
|
74940
|
+
//#region src/endpoints/adapters/climate-model.ts
|
|
74941
|
+
/**
|
|
74942
|
+
* What a `climate-control` device is to Alexa (D874) — pure, so the discovery
|
|
74943
|
+
* shape, the slice mapping and the directive handlers share one definition.
|
|
74944
|
+
*
|
|
74945
|
+
* `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
|
|
74946
|
+
* upperSetpoint (a device with a range),
|
|
74947
|
+
* thermostatMode HEAT / COOL / AUTO / ECO /
|
|
74948
|
+
* OFF
|
|
74949
|
+
* `Alexa.TemperatureSensor` the current temperature
|
|
74950
|
+
* `Alexa.PowerController` when the device can be turned off
|
|
74951
|
+
* `Alexa.ModeController` Climate.Mode only when the device has a mode
|
|
74952
|
+
* Amazon's thermostat vocabulary lacks
|
|
74953
|
+
* (dry, fan_only)
|
|
74954
|
+
* `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
|
|
74955
|
+
*
|
|
74956
|
+
* Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
|
|
74957
|
+
* mode is not a documented way to carry them: they travel through the
|
|
74958
|
+
* `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
|
|
74959
|
+
* while the device is in one of them.
|
|
74960
|
+
*
|
|
74961
|
+
* Temperatures are Celsius throughout (the cap's contract); Alexa converts for
|
|
74962
|
+
* the account.
|
|
74963
|
+
*/
|
|
74964
|
+
var INSTANCE_MODE = "Climate.Mode";
|
|
74965
|
+
var INSTANCE_FAN = "Climate.FanSpeed";
|
|
74966
|
+
var FAN_PREFIX = "Fan.";
|
|
74967
|
+
var DEFAULT_TEMP_STEP = .5;
|
|
74968
|
+
var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
|
|
74969
|
+
var ALEXA_ECO_PRESET = "eco";
|
|
74970
|
+
var SINGLE_SETPOINT_MODES = [
|
|
74971
|
+
"heat",
|
|
74972
|
+
"cool",
|
|
74973
|
+
"auto",
|
|
74974
|
+
"dry"
|
|
74975
|
+
];
|
|
74976
|
+
/** What TurnOn switches to, best first. */
|
|
74977
|
+
var TURN_ON_PREFERENCE = [
|
|
74978
|
+
"auto",
|
|
74979
|
+
"heat_cool",
|
|
74980
|
+
"cool",
|
|
74981
|
+
"heat"
|
|
74982
|
+
];
|
|
74983
|
+
var SliceShape$1 = ClimateControlStatusSchema.pick({
|
|
74984
|
+
mode: true,
|
|
74985
|
+
availableModes: true,
|
|
74986
|
+
fanMode: true,
|
|
74987
|
+
availableFanModes: true,
|
|
74988
|
+
preset: true,
|
|
74989
|
+
availablePresets: true,
|
|
74990
|
+
target: true,
|
|
74991
|
+
targetHigh: true,
|
|
74992
|
+
targetLow: true,
|
|
74993
|
+
currentTemp: true,
|
|
74994
|
+
minTemp: true,
|
|
74995
|
+
maxTemp: true,
|
|
74996
|
+
tempStep: true
|
|
74997
|
+
});
|
|
74998
|
+
/** The slice fields the adapter reads, or null when the slice is not a climate one. */
|
|
74999
|
+
function readClimateSlice(slice) {
|
|
75000
|
+
const parsed = SliceShape$1.safeParse(slice);
|
|
75001
|
+
return parsed.success ? parsed.data : null;
|
|
75002
|
+
}
|
|
75003
|
+
function boundsOf(slice) {
|
|
75004
|
+
const min = slice.minTemp ?? 7;
|
|
75005
|
+
const max = slice.maxTemp ?? 35;
|
|
75006
|
+
const step = slice.tempStep ?? .5;
|
|
75007
|
+
return min < max ? {
|
|
75008
|
+
min,
|
|
75009
|
+
max,
|
|
75010
|
+
step
|
|
75011
|
+
} : {
|
|
75012
|
+
min: 7,
|
|
75013
|
+
max: 35,
|
|
75014
|
+
step: DEFAULT_TEMP_STEP
|
|
75015
|
+
};
|
|
75016
|
+
}
|
|
75017
|
+
/** One decimal, the precision Alexa's own conversions keep. */
|
|
75018
|
+
function round1(value) {
|
|
75019
|
+
return Math.round(value * 10) / 10;
|
|
75020
|
+
}
|
|
75021
|
+
/**
|
|
75022
|
+
* Snap a setpoint to the device's step (anchored at its minimum), then clamp:
|
|
75023
|
+
* a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
|
|
75024
|
+
*/
|
|
75025
|
+
function snapToStep(value, bounds) {
|
|
75026
|
+
const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
|
|
75027
|
+
return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
|
|
75028
|
+
}
|
|
75029
|
+
function temperatureValue(celsius) {
|
|
75030
|
+
return {
|
|
75031
|
+
value: round1(celsius),
|
|
75032
|
+
scale: "CELSIUS"
|
|
75033
|
+
};
|
|
75034
|
+
}
|
|
75035
|
+
function hasMode(slice, mode) {
|
|
75036
|
+
return slice !== null && slice.availableModes.includes(mode);
|
|
75037
|
+
}
|
|
75038
|
+
/** Dual setpoints (a heat/cool range). */
|
|
75039
|
+
function supportsDual(slice, features) {
|
|
75040
|
+
return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
|
|
75041
|
+
}
|
|
75042
|
+
/** A single setpoint (everything but a range-only device). */
|
|
75043
|
+
function supportsSingle(slice) {
|
|
75044
|
+
return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
|
|
75045
|
+
}
|
|
75046
|
+
/** The device's own name for its `eco` preset, if it has one. */
|
|
75047
|
+
function ecoPresetOf(slice) {
|
|
75048
|
+
return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
|
|
75049
|
+
}
|
|
75050
|
+
/** Amazon thermostat modes the device can be put in, in a stable order. */
|
|
75051
|
+
function supportedThermostatModes(slice) {
|
|
75052
|
+
if (slice === null || slice.availableModes.length === 0) return [
|
|
75053
|
+
"HEAT",
|
|
75054
|
+
"COOL",
|
|
75055
|
+
"AUTO",
|
|
75056
|
+
"OFF"
|
|
75057
|
+
];
|
|
75058
|
+
const out = [];
|
|
75059
|
+
if (hasMode(slice, "heat")) out.push("HEAT");
|
|
75060
|
+
if (hasMode(slice, "cool")) out.push("COOL");
|
|
75061
|
+
if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
|
|
75062
|
+
if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
|
|
75063
|
+
if (hasMode(slice, "off")) out.push("OFF");
|
|
75064
|
+
return out;
|
|
75065
|
+
}
|
|
75066
|
+
/** The HVAC mode TurnOn / leaving OFF switches to. */
|
|
75067
|
+
function onModeOf(slice) {
|
|
75068
|
+
return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
|
|
75069
|
+
}
|
|
75070
|
+
function canPowerOff(slice) {
|
|
75071
|
+
return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
|
|
75072
|
+
}
|
|
75073
|
+
/** A device with a mode Amazon's thermostat vocabulary lacks. */
|
|
75074
|
+
function hasExtraModes(slice) {
|
|
75075
|
+
return hasMode(slice, "dry") || hasMode(slice, "fan_only");
|
|
75076
|
+
}
|
|
75077
|
+
var MODE_VALUE = {
|
|
75078
|
+
heat: "Mode.Heat",
|
|
75079
|
+
cool: "Mode.Cool",
|
|
75080
|
+
auto: "Mode.Auto",
|
|
75081
|
+
dry: "Mode.Dry",
|
|
75082
|
+
fan_only: "Mode.Fan",
|
|
75083
|
+
off: "Mode.Off"
|
|
75084
|
+
};
|
|
75085
|
+
var MODE_NAMES = {
|
|
75086
|
+
"Mode.Heat": ["Riscaldamento", "Heating"],
|
|
75087
|
+
"Mode.Cool": ["Raffrescamento", "Cooling"],
|
|
75088
|
+
"Mode.Auto": ["Automatico", "Auto"],
|
|
75089
|
+
"Mode.Dry": ["Deumidificazione", "Dry"],
|
|
75090
|
+
"Mode.Fan": ["Ventilazione", "Fan only"],
|
|
75091
|
+
"Mode.Off": ["Spento", "Off"]
|
|
75092
|
+
};
|
|
75093
|
+
/** The ModeController value a HVAC mode reports as (a range is "auto"). */
|
|
75094
|
+
function modeValueOf(mode) {
|
|
75095
|
+
return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
|
|
75096
|
+
}
|
|
75097
|
+
/** The HVAC mode a ModeController value asks for, among those the device has. */
|
|
75098
|
+
function hvacModeForValue(value, slice) {
|
|
75099
|
+
if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
|
|
75100
|
+
const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
|
|
75101
|
+
if (mode === void 0) return null;
|
|
75102
|
+
const candidate = SliceShape$1.shape.mode.safeParse(mode);
|
|
75103
|
+
return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
|
|
75104
|
+
}
|
|
75105
|
+
/** The ModeController values the device offers, in a stable order. */
|
|
75106
|
+
function modeValuesOf(slice) {
|
|
75107
|
+
return [
|
|
75108
|
+
"heat",
|
|
75109
|
+
"cool",
|
|
75110
|
+
"auto",
|
|
75111
|
+
"dry",
|
|
75112
|
+
"fan_only",
|
|
75113
|
+
"off"
|
|
75114
|
+
].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
|
|
75115
|
+
}
|
|
75116
|
+
function textNames(it, en) {
|
|
75117
|
+
return [{
|
|
75118
|
+
"@type": "text",
|
|
75119
|
+
value: {
|
|
75120
|
+
text: it,
|
|
75121
|
+
locale: "it-IT"
|
|
75122
|
+
}
|
|
75123
|
+
}, {
|
|
75124
|
+
"@type": "text",
|
|
75125
|
+
value: {
|
|
75126
|
+
text: en,
|
|
75127
|
+
locale: "en-US"
|
|
75128
|
+
}
|
|
75129
|
+
}];
|
|
75130
|
+
}
|
|
75131
|
+
function modeCapability(slice) {
|
|
75132
|
+
const values = modeValuesOf(slice);
|
|
75133
|
+
if (values.length < 2) return null;
|
|
75134
|
+
return {
|
|
75135
|
+
type: "AlexaInterface",
|
|
75136
|
+
interface: "Alexa.ModeController",
|
|
75137
|
+
version: "3",
|
|
75138
|
+
instance: INSTANCE_MODE,
|
|
75139
|
+
capabilityResources: { friendlyNames: [{
|
|
75140
|
+
"@type": "asset",
|
|
75141
|
+
value: { assetId: "Alexa.Setting.Mode" }
|
|
75142
|
+
}, ...textNames("Modalità", "Mode")] },
|
|
75143
|
+
properties: {
|
|
75144
|
+
supported: [{ name: "mode" }],
|
|
75145
|
+
proactivelyReported: true,
|
|
75146
|
+
retrievable: true
|
|
75147
|
+
},
|
|
75148
|
+
configuration: {
|
|
75149
|
+
ordered: false,
|
|
75150
|
+
supportedModes: values.map((value) => {
|
|
75151
|
+
const [it, en] = MODE_NAMES[value] ?? [value, value];
|
|
75152
|
+
return {
|
|
75153
|
+
value,
|
|
75154
|
+
modeResources: { friendlyNames: textNames(it, en) }
|
|
75155
|
+
};
|
|
75156
|
+
})
|
|
75157
|
+
}
|
|
75158
|
+
};
|
|
75159
|
+
}
|
|
75160
|
+
var FAN_ASSETS = {
|
|
75161
|
+
low: "Alexa.Value.Low",
|
|
75162
|
+
medium: "Alexa.Value.Medium",
|
|
75163
|
+
high: "Alexa.Value.High"
|
|
75164
|
+
};
|
|
75165
|
+
var FAN_TEXTS = {
|
|
75166
|
+
auto: ["Automatico", "Auto"],
|
|
75167
|
+
quiet: ["Silenzioso", "Quiet"],
|
|
75168
|
+
turbo: ["Turbo", "Turbo"],
|
|
75169
|
+
on: ["Acceso", "On"],
|
|
75170
|
+
off: ["Spento", "Off"],
|
|
75171
|
+
middle: ["Medio", "Medium"],
|
|
75172
|
+
mediumlow: ["Medio basso", "Medium low"],
|
|
75173
|
+
mediumhigh: ["Medio alto", "Medium high"],
|
|
75174
|
+
medium_low: ["Medio basso", "Medium low"],
|
|
75175
|
+
medium_high: ["Medio alto", "Medium high"]
|
|
75176
|
+
};
|
|
75177
|
+
function capitalize(word) {
|
|
75178
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
75179
|
+
}
|
|
75180
|
+
/** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
|
|
75181
|
+
function fanValueOf(fanMode) {
|
|
75182
|
+
const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
|
|
75183
|
+
return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
|
|
75184
|
+
}
|
|
75185
|
+
/** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
|
|
75186
|
+
function fanModesOf(slice) {
|
|
75187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75188
|
+
const out = [];
|
|
75189
|
+
for (const fanMode of slice.availableFanModes) {
|
|
75190
|
+
const value = fanValueOf(fanMode);
|
|
75191
|
+
if (value === null || seen.has(value)) continue;
|
|
75192
|
+
seen.add(value);
|
|
75193
|
+
out.push([value, fanMode]);
|
|
75194
|
+
}
|
|
75195
|
+
return out;
|
|
75196
|
+
}
|
|
75197
|
+
function hasFanModes(slice) {
|
|
75198
|
+
return slice !== null && fanModesOf(slice).length >= 2;
|
|
75199
|
+
}
|
|
75200
|
+
function fanFriendlyNames(fanMode) {
|
|
75201
|
+
const key = fanMode.toLowerCase();
|
|
75202
|
+
const asset = FAN_ASSETS[key];
|
|
75203
|
+
if (asset !== void 0) return [{
|
|
75204
|
+
"@type": "asset",
|
|
75205
|
+
value: { assetId: asset }
|
|
75206
|
+
}];
|
|
75207
|
+
const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
|
|
75208
|
+
return textNames(it, en);
|
|
75209
|
+
}
|
|
75210
|
+
function fanCapability(slice) {
|
|
75211
|
+
const modes = fanModesOf(slice);
|
|
75212
|
+
if (modes.length < 2) return null;
|
|
75213
|
+
return {
|
|
75214
|
+
type: "AlexaInterface",
|
|
75215
|
+
interface: "Alexa.ModeController",
|
|
75216
|
+
version: "3",
|
|
75217
|
+
instance: INSTANCE_FAN,
|
|
75218
|
+
capabilityResources: { friendlyNames: [{
|
|
75219
|
+
"@type": "asset",
|
|
75220
|
+
value: { assetId: "Alexa.Setting.FanSpeed" }
|
|
75221
|
+
}, ...textNames("Velocità ventola", "Fan speed")] },
|
|
75222
|
+
properties: {
|
|
75223
|
+
supported: [{ name: "mode" }],
|
|
75224
|
+
proactivelyReported: true,
|
|
75225
|
+
retrievable: true
|
|
75226
|
+
},
|
|
75227
|
+
configuration: {
|
|
75228
|
+
ordered: false,
|
|
75229
|
+
supportedModes: modes.map(([value, fanMode]) => ({
|
|
75230
|
+
value,
|
|
75231
|
+
modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
|
|
75232
|
+
}))
|
|
75233
|
+
}
|
|
75234
|
+
};
|
|
75235
|
+
}
|
|
75236
|
+
function propertiesBlock(names) {
|
|
75237
|
+
return {
|
|
75238
|
+
supported: names.map((name) => ({ name })),
|
|
75239
|
+
proactivelyReported: true,
|
|
75240
|
+
retrievable: true
|
|
75241
|
+
};
|
|
75242
|
+
}
|
|
75243
|
+
function thermostatCapability(slice, modes, features) {
|
|
75244
|
+
return {
|
|
75245
|
+
type: "AlexaInterface",
|
|
75246
|
+
interface: "Alexa.ThermostatController",
|
|
75247
|
+
version: "3.1",
|
|
75248
|
+
properties: propertiesBlock([
|
|
75249
|
+
...supportsSingle(slice) ? ["targetSetpoint"] : [],
|
|
75250
|
+
...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
|
|
75251
|
+
"thermostatMode"
|
|
75252
|
+
]),
|
|
75253
|
+
configuration: {
|
|
75254
|
+
supportedModes: modes,
|
|
75255
|
+
supportsScheduling: false
|
|
75256
|
+
}
|
|
75257
|
+
};
|
|
75258
|
+
}
|
|
75259
|
+
/** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
|
|
75260
|
+
function climateCategoryFor(deviceType) {
|
|
75261
|
+
return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
|
|
75262
|
+
}
|
|
75263
|
+
function describeClimate(device) {
|
|
75264
|
+
const slice = readClimateSlice(device.slices["climate-control"]);
|
|
75265
|
+
const modes = supportedThermostatModes(slice);
|
|
75266
|
+
if (modes.length === 0) return {
|
|
75267
|
+
kind: "skip",
|
|
75268
|
+
reason: "climate: the device offers no mode Alexa has a thermostat value for"
|
|
75269
|
+
};
|
|
75270
|
+
const extras = [];
|
|
75271
|
+
if (slice !== null) {
|
|
75272
|
+
const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
|
|
75273
|
+
const fan = hasFanModes(slice) ? fanCapability(slice) : null;
|
|
75274
|
+
if (mode !== null) extras.push(mode);
|
|
75275
|
+
if (fan !== null) extras.push(fan);
|
|
75276
|
+
}
|
|
75277
|
+
return {
|
|
75278
|
+
kind: "publish",
|
|
75279
|
+
capabilities: [
|
|
75280
|
+
thermostatCapability(slice, modes, device.features),
|
|
75281
|
+
{
|
|
75282
|
+
type: "AlexaInterface",
|
|
75283
|
+
interface: "Alexa.TemperatureSensor",
|
|
75284
|
+
version: "3",
|
|
75285
|
+
properties: propertiesBlock(["temperature"])
|
|
75286
|
+
},
|
|
75287
|
+
...canPowerOff(slice) ? [{
|
|
75288
|
+
type: "AlexaInterface",
|
|
75289
|
+
interface: "Alexa.PowerController",
|
|
75290
|
+
version: "3",
|
|
75291
|
+
properties: propertiesBlock(["powerState"])
|
|
75292
|
+
}] : [],
|
|
75293
|
+
...extras
|
|
75294
|
+
],
|
|
75295
|
+
category: climateCategoryFor(device.type)
|
|
75296
|
+
};
|
|
75297
|
+
}
|
|
75298
|
+
/** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
|
|
75299
|
+
function thermostatModeOf(slice) {
|
|
75300
|
+
if (slice.mode === "off") return "OFF";
|
|
75301
|
+
const eco = ecoPresetOf(slice);
|
|
75302
|
+
if (eco !== null && slice.preset === eco) return "ECO";
|
|
75303
|
+
switch (slice.mode) {
|
|
75304
|
+
case "heat": return "HEAT";
|
|
75305
|
+
case "cool": return "COOL";
|
|
75306
|
+
case "auto":
|
|
75307
|
+
case "heat_cool": return "AUTO";
|
|
75308
|
+
default: return null;
|
|
75309
|
+
}
|
|
75310
|
+
}
|
|
75311
|
+
function climateProperties(slice) {
|
|
75312
|
+
const s = readClimateSlice(slice);
|
|
75313
|
+
if (s === null) return [];
|
|
75314
|
+
const out = [];
|
|
75315
|
+
const thermostat = (name, value) => {
|
|
75316
|
+
out.push({
|
|
75317
|
+
namespace: "Alexa.ThermostatController",
|
|
75318
|
+
name,
|
|
75319
|
+
value
|
|
75320
|
+
});
|
|
75321
|
+
};
|
|
75322
|
+
if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
|
|
75323
|
+
thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
|
|
75324
|
+
thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
|
|
75325
|
+
} else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
|
|
75326
|
+
const mode = thermostatModeOf(s);
|
|
75327
|
+
if (mode !== null) thermostat("thermostatMode", mode);
|
|
75328
|
+
if (s.currentTemp !== null) out.push({
|
|
75329
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75330
|
+
name: "temperature",
|
|
75331
|
+
value: temperatureValue(s.currentTemp)
|
|
75332
|
+
});
|
|
75333
|
+
if (canPowerOff(s)) out.push({
|
|
75334
|
+
namespace: "Alexa.PowerController",
|
|
75335
|
+
name: "powerState",
|
|
75336
|
+
value: s.mode === "off" ? "OFF" : "ON"
|
|
75337
|
+
});
|
|
75338
|
+
if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
|
|
75339
|
+
namespace: "Alexa.ModeController",
|
|
75340
|
+
instance: INSTANCE_MODE,
|
|
75341
|
+
name: "mode",
|
|
75342
|
+
value: modeValueOf(s.mode)
|
|
75343
|
+
});
|
|
75344
|
+
const fan = fanValueOf(s.fanMode);
|
|
75345
|
+
if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
|
|
75346
|
+
namespace: "Alexa.ModeController",
|
|
75347
|
+
instance: INSTANCE_FAN,
|
|
75348
|
+
name: "mode",
|
|
75349
|
+
value: fan
|
|
75350
|
+
});
|
|
75351
|
+
return out;
|
|
75352
|
+
}
|
|
75353
|
+
//#endregion
|
|
75354
|
+
//#region src/handlers/climate-controller.ts
|
|
75355
|
+
var CAP_CLIMATE = "climate-control";
|
|
75356
|
+
var CAP_SWITCH$1 = "switch";
|
|
75357
|
+
var UNCERTAINTY_MS = 500;
|
|
75358
|
+
var PRESET_NONE = "none";
|
|
75359
|
+
var ScaleSchema = _enum([
|
|
75360
|
+
"CELSIUS",
|
|
75361
|
+
"FAHRENHEIT",
|
|
75362
|
+
"KELVIN"
|
|
75363
|
+
]);
|
|
75364
|
+
var TemperatureSchema = object({
|
|
75365
|
+
value: number(),
|
|
75366
|
+
scale: ScaleSchema
|
|
75367
|
+
});
|
|
75368
|
+
var SetTemperaturePayload = object({
|
|
75369
|
+
targetSetpoint: TemperatureSchema.optional(),
|
|
75370
|
+
lowerSetpoint: TemperatureSchema.optional(),
|
|
75371
|
+
upperSetpoint: TemperatureSchema.optional()
|
|
75372
|
+
});
|
|
75373
|
+
var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
|
|
75374
|
+
var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
|
|
75375
|
+
var ModePayload = object({ mode: string() });
|
|
75376
|
+
function isRefusal(outcome) {
|
|
75377
|
+
return "refusal" in outcome;
|
|
75378
|
+
}
|
|
75379
|
+
function toCelsius(t) {
|
|
75380
|
+
if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
|
|
75381
|
+
if (t.scale === "KELVIN") return t.value - 273.15;
|
|
75382
|
+
return t.value;
|
|
75383
|
+
}
|
|
75384
|
+
/** A DELTA converts without the scale's offset. */
|
|
75385
|
+
function deltaToCelsius(t) {
|
|
75386
|
+
return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
|
|
75387
|
+
}
|
|
75388
|
+
function withPayload(envelope, extra) {
|
|
75389
|
+
return { event: {
|
|
75390
|
+
...envelope.event,
|
|
75391
|
+
payload: {
|
|
75392
|
+
...envelope.event.payload,
|
|
75393
|
+
...extra
|
|
75394
|
+
}
|
|
75395
|
+
} };
|
|
75396
|
+
}
|
|
75397
|
+
function outOfRange$1(header, bounds) {
|
|
75398
|
+
return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
|
|
75399
|
+
minimumValue: temperatureValue(bounds.min),
|
|
75400
|
+
maximumValue: temperatureValue(bounds.max)
|
|
75401
|
+
} }) };
|
|
75402
|
+
}
|
|
75403
|
+
function tooClose(header, bounds) {
|
|
75404
|
+
return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
|
|
75405
|
+
}
|
|
75406
|
+
function refuse$1(header, type, message) {
|
|
75407
|
+
return { refusal: errorEnvelope(header, type, message) };
|
|
75408
|
+
}
|
|
75409
|
+
function inRange(celsius, bounds) {
|
|
75410
|
+
const v = round1(celsius);
|
|
75411
|
+
return v >= bounds.min && v <= bounds.max;
|
|
75412
|
+
}
|
|
75413
|
+
var THERMOSTAT_PROPERTIES = new Set([
|
|
75414
|
+
"targetSetpoint",
|
|
75415
|
+
"lowerSetpoint",
|
|
75416
|
+
"upperSetpoint",
|
|
75417
|
+
"thermostatMode"
|
|
75418
|
+
]);
|
|
75419
|
+
function thermostatProperty(property) {
|
|
75420
|
+
return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
|
|
75421
|
+
}
|
|
75422
|
+
function commandsFor(api, deviceId) {
|
|
75423
|
+
return {
|
|
75424
|
+
setMode: (mode) => api.climateControl.setMode.mutate({
|
|
75425
|
+
deviceId,
|
|
75426
|
+
mode
|
|
75427
|
+
}),
|
|
75428
|
+
setPreset: (preset) => api.climateControl.setPreset.mutate({
|
|
75429
|
+
deviceId,
|
|
75430
|
+
preset
|
|
75431
|
+
}),
|
|
75432
|
+
setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
|
|
75433
|
+
deviceId,
|
|
75434
|
+
fanMode
|
|
75435
|
+
}),
|
|
75436
|
+
setTarget: (target) => api.climateControl.setTarget.mutate({
|
|
75437
|
+
deviceId,
|
|
75438
|
+
target
|
|
75439
|
+
}),
|
|
75440
|
+
setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
|
|
75441
|
+
deviceId,
|
|
75442
|
+
targetLow,
|
|
75443
|
+
targetHigh
|
|
75444
|
+
})
|
|
75445
|
+
};
|
|
75446
|
+
}
|
|
75447
|
+
function planRange(header, slice, cmd, low, high) {
|
|
75448
|
+
const bounds = boundsOf(slice);
|
|
75449
|
+
if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
|
|
75450
|
+
if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
|
|
75451
|
+
const targetLow = snapToStep(low, bounds);
|
|
75452
|
+
const targetHigh = snapToStep(high, bounds);
|
|
75453
|
+
if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
|
|
75454
|
+
return {
|
|
75455
|
+
commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
|
|
75456
|
+
projected: {
|
|
75457
|
+
...slice,
|
|
75458
|
+
mode: "heat_cool",
|
|
75459
|
+
targetLow,
|
|
75460
|
+
targetHigh
|
|
75461
|
+
},
|
|
75462
|
+
keep: thermostatProperty
|
|
75463
|
+
};
|
|
75464
|
+
}
|
|
75465
|
+
function planSingle(header, slice, cmd, celsius) {
|
|
75466
|
+
const bounds = boundsOf(slice);
|
|
75467
|
+
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");
|
|
75468
|
+
if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
|
|
75469
|
+
const target = snapToStep(celsius, bounds);
|
|
75470
|
+
return {
|
|
75471
|
+
commands: [() => cmd.setTarget(target)],
|
|
75472
|
+
projected: {
|
|
75473
|
+
...slice,
|
|
75474
|
+
target
|
|
75475
|
+
},
|
|
75476
|
+
keep: thermostatProperty
|
|
75477
|
+
};
|
|
75478
|
+
}
|
|
75479
|
+
function planSetTemperature(c, slice, cmd) {
|
|
75480
|
+
const parsed = SetTemperaturePayload.safeParse(c.payload);
|
|
75481
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75482
|
+
const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
|
|
75483
|
+
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");
|
|
75484
|
+
if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
|
|
75485
|
+
if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
|
|
75486
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
|
|
75487
|
+
}
|
|
75488
|
+
function planAdjust(c, slice, cmd) {
|
|
75489
|
+
const parsed = AdjustTemperaturePayload.safeParse(c.payload);
|
|
75490
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75491
|
+
const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
|
|
75492
|
+
if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
|
|
75493
|
+
if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
|
|
75494
|
+
return planSingle(c.header, slice, cmd, slice.target + delta);
|
|
75495
|
+
}
|
|
75496
|
+
var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
|
|
75497
|
+
function hvacModeFor(requested, slice) {
|
|
75498
|
+
const has = (m) => slice.availableModes.includes(m);
|
|
75499
|
+
switch (requested) {
|
|
75500
|
+
case "HEAT": return has("heat") ? "heat" : null;
|
|
75501
|
+
case "COOL": return has("cool") ? "cool" : null;
|
|
75502
|
+
case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
|
|
75503
|
+
case "OFF": return has("off") ? "off" : null;
|
|
75504
|
+
default: return null;
|
|
75505
|
+
}
|
|
75506
|
+
}
|
|
75507
|
+
/** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
|
|
75508
|
+
function presetClearing(slice) {
|
|
75509
|
+
return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
|
|
75510
|
+
}
|
|
75511
|
+
function planEco(c, slice, cmd) {
|
|
75512
|
+
const eco = ecoPresetOf(slice);
|
|
75513
|
+
if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
|
|
75514
|
+
const wake = slice.mode === "off" ? onModeOf(slice) : null;
|
|
75515
|
+
if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
|
|
75516
|
+
return {
|
|
75517
|
+
commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
|
|
75518
|
+
projected: {
|
|
75519
|
+
...slice,
|
|
75520
|
+
mode: wake ?? slice.mode,
|
|
75521
|
+
preset: eco
|
|
75522
|
+
},
|
|
75523
|
+
keep: thermostatProperty
|
|
75524
|
+
};
|
|
75525
|
+
}
|
|
75526
|
+
function planSetThermostatMode(c, slice, cmd) {
|
|
75527
|
+
const parsed = ThermostatModePayload.safeParse(c.payload);
|
|
75528
|
+
if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
|
|
75529
|
+
const raw = parsed.data.thermostatMode;
|
|
75530
|
+
const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
|
|
75531
|
+
if (requested === "ECO") return planEco(c, slice, cmd);
|
|
75532
|
+
const mode = hvacModeFor(requested, slice);
|
|
75533
|
+
if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
|
|
75534
|
+
const eco = ecoPresetOf(slice);
|
|
75535
|
+
const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
|
|
75536
|
+
return {
|
|
75537
|
+
commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
|
|
75538
|
+
projected: {
|
|
75539
|
+
...slice,
|
|
75540
|
+
mode,
|
|
75541
|
+
preset: clear ?? slice.preset
|
|
75542
|
+
},
|
|
75543
|
+
keep: thermostatProperty
|
|
75544
|
+
};
|
|
75545
|
+
}
|
|
75546
|
+
function planPower(c, slice, cmd) {
|
|
75547
|
+
const turnOn = c.header.name === "TurnOn";
|
|
75548
|
+
const keep = (p) => p.namespace === "Alexa.PowerController";
|
|
75549
|
+
if (turnOn) {
|
|
75550
|
+
if (slice.mode !== "off") return {
|
|
75551
|
+
commands: [],
|
|
75552
|
+
projected: slice,
|
|
75553
|
+
keep
|
|
75554
|
+
};
|
|
75555
|
+
const mode = onModeOf(slice);
|
|
75556
|
+
if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
|
|
75557
|
+
return {
|
|
75558
|
+
commands: [() => cmd.setMode(mode)],
|
|
75559
|
+
projected: {
|
|
75560
|
+
...slice,
|
|
75561
|
+
mode
|
|
75562
|
+
},
|
|
75563
|
+
keep
|
|
75564
|
+
};
|
|
75565
|
+
}
|
|
75566
|
+
if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
|
|
75567
|
+
if (slice.mode === "off") return {
|
|
75568
|
+
commands: [],
|
|
75569
|
+
projected: slice,
|
|
75570
|
+
keep
|
|
75571
|
+
};
|
|
75572
|
+
return {
|
|
75573
|
+
commands: [() => cmd.setMode("off")],
|
|
75574
|
+
projected: {
|
|
75575
|
+
...slice,
|
|
75576
|
+
mode: "off"
|
|
75577
|
+
},
|
|
75578
|
+
keep
|
|
75579
|
+
};
|
|
75580
|
+
}
|
|
75581
|
+
function planModeController(c, slice, cmd) {
|
|
75582
|
+
const parsed = ModePayload.safeParse(c.payload);
|
|
75583
|
+
const instance = c.header.instance;
|
|
75584
|
+
if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
|
|
75585
|
+
const keep = (p) => p.instance === instance;
|
|
75586
|
+
if (instance === "Climate.Mode") {
|
|
75587
|
+
const mode = hvacModeForValue(parsed.data.mode, slice);
|
|
75588
|
+
if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
|
|
75589
|
+
return {
|
|
75590
|
+
commands: [() => cmd.setMode(mode)],
|
|
75591
|
+
projected: {
|
|
75592
|
+
...slice,
|
|
75593
|
+
mode
|
|
75594
|
+
},
|
|
75595
|
+
keep
|
|
75596
|
+
};
|
|
75597
|
+
}
|
|
75598
|
+
if (instance === "Climate.FanSpeed") {
|
|
75599
|
+
const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
|
|
75600
|
+
if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
|
|
75601
|
+
return {
|
|
75602
|
+
commands: [() => cmd.setFanMode(fan[1])],
|
|
75603
|
+
projected: {
|
|
75604
|
+
...slice,
|
|
75605
|
+
fanMode: fan[1]
|
|
75606
|
+
},
|
|
75607
|
+
keep
|
|
75608
|
+
};
|
|
75609
|
+
}
|
|
75610
|
+
return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
|
|
75611
|
+
}
|
|
75612
|
+
function plan(c, slice, cmd) {
|
|
75613
|
+
switch (`${c.header.namespace}/${c.header.name}`) {
|
|
75614
|
+
case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
|
|
75615
|
+
case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
|
|
75616
|
+
case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
|
|
75617
|
+
case "Alexa.PowerController/TurnOn":
|
|
75618
|
+
case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
|
|
75619
|
+
case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
|
|
75620
|
+
default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
|
|
75621
|
+
}
|
|
75622
|
+
}
|
|
75623
|
+
async function readSlice$1(c, deps) {
|
|
75624
|
+
try {
|
|
75625
|
+
return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
|
|
75626
|
+
deviceId: c.deviceId,
|
|
75627
|
+
capName: CAP_CLIMATE
|
|
75628
|
+
}));
|
|
75629
|
+
} catch (err) {
|
|
75630
|
+
deps.logger.warn("alexa climate-controller: the climate state could not be read", {
|
|
75631
|
+
tags: { deviceId: c.deviceId },
|
|
75632
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
75633
|
+
});
|
|
75634
|
+
return null;
|
|
75635
|
+
}
|
|
75636
|
+
}
|
|
75637
|
+
function successResponse$1(c, properties) {
|
|
75638
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
75639
|
+
return {
|
|
75640
|
+
event: {
|
|
75641
|
+
header: {
|
|
75642
|
+
namespace: "Alexa",
|
|
75643
|
+
name: "Response",
|
|
75644
|
+
messageId: c.header.messageId,
|
|
75645
|
+
...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
|
|
75646
|
+
payloadVersion: "3"
|
|
75647
|
+
},
|
|
75648
|
+
endpoint: { endpointId: c.endpoint.endpointId },
|
|
75649
|
+
payload: {}
|
|
75650
|
+
},
|
|
75651
|
+
context: { properties: properties.map((p) => ({
|
|
75652
|
+
...p,
|
|
75653
|
+
timeOfSample: now,
|
|
75654
|
+
uncertaintyInMilliseconds: UNCERTAINTY_MS
|
|
75655
|
+
})) }
|
|
75656
|
+
};
|
|
75657
|
+
}
|
|
75658
|
+
/** The one handler behind every climate directive. */
|
|
75659
|
+
async function handleClimateDirective(c) {
|
|
75660
|
+
const deps = c.deps.climateController;
|
|
75661
|
+
const slice = await readSlice$1(c, deps);
|
|
75662
|
+
if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
|
|
75663
|
+
const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
|
|
75664
|
+
if (isRefusal(outcome)) return outcome.refusal;
|
|
75665
|
+
try {
|
|
75666
|
+
for (const command of outcome.commands) await command();
|
|
75667
|
+
} catch (err) {
|
|
75668
|
+
deps.logger.warn("alexa climate-controller: the climate command failed", {
|
|
75669
|
+
tags: { deviceId: c.deviceId },
|
|
75670
|
+
meta: {
|
|
75671
|
+
directive: c.header.name,
|
|
75672
|
+
error: err instanceof Error ? err.message : String(err)
|
|
75673
|
+
}
|
|
75674
|
+
});
|
|
75675
|
+
return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
|
|
75676
|
+
}
|
|
75677
|
+
deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
|
|
75678
|
+
tags: { deviceId: c.deviceId },
|
|
75679
|
+
meta: { commands: outcome.commands.length }
|
|
75680
|
+
});
|
|
75681
|
+
return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
|
|
75682
|
+
}
|
|
75683
|
+
/**
|
|
75684
|
+
* Last answer per device, per deps instance: a failed bindings read changes
|
|
75685
|
+
* nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
|
|
75686
|
+
* switch handler because one `getBindings` call failed.
|
|
75687
|
+
*/
|
|
75688
|
+
var lastPowerClaim = /* @__PURE__ */ new WeakMap();
|
|
75689
|
+
/** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
|
|
75690
|
+
async function handlesPower(c) {
|
|
75691
|
+
const deps = c.deps.climateController;
|
|
75692
|
+
const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
|
|
75693
|
+
lastPowerClaim.set(deps, known);
|
|
75694
|
+
try {
|
|
75695
|
+
const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
|
|
75696
|
+
const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
|
|
75697
|
+
known.set(c.deviceId, claim);
|
|
75698
|
+
return claim;
|
|
75699
|
+
} catch (err) {
|
|
75700
|
+
const previous = known.get(c.deviceId);
|
|
75701
|
+
deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
|
|
75702
|
+
tags: { deviceId: c.deviceId },
|
|
75703
|
+
meta: {
|
|
75704
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75705
|
+
decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
|
|
75706
|
+
}
|
|
75707
|
+
});
|
|
75708
|
+
return previous ?? false;
|
|
75709
|
+
}
|
|
75710
|
+
}
|
|
75711
|
+
//#endregion
|
|
74030
75712
|
//#region src/endpoints/adapters/shared.ts
|
|
74031
75713
|
/** The base `Alexa` interface every endpoint declares. */
|
|
74032
75714
|
var BASE = {
|
|
@@ -74077,6 +75759,88 @@ function numericValueField(value) {
|
|
|
74077
75759
|
return typeof raw === "number" ? raw : null;
|
|
74078
75760
|
}
|
|
74079
75761
|
//#endregion
|
|
75762
|
+
//#region src/endpoints/adapters/temperature.ts
|
|
75763
|
+
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
75764
|
+
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
75765
|
+
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
75766
|
+
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
75767
|
+
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
75768
|
+
var temperatureAdapter = {
|
|
75769
|
+
cap: "temperature-sensor",
|
|
75770
|
+
stateCaps: ["temperature-sensor"],
|
|
75771
|
+
discoveryReads: [],
|
|
75772
|
+
describe: () => ({
|
|
75773
|
+
kind: "publish",
|
|
75774
|
+
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
75775
|
+
category: "TEMPERATURE_SENSOR"
|
|
75776
|
+
}),
|
|
75777
|
+
properties: (_cap, slice) => {
|
|
75778
|
+
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
75779
|
+
return p.success ? [{
|
|
75780
|
+
namespace: "Alexa.TemperatureSensor",
|
|
75781
|
+
name: "temperature",
|
|
75782
|
+
value: {
|
|
75783
|
+
value: Math.round(p.data.celsius * 10) / 10,
|
|
75784
|
+
scale: "CELSIUS"
|
|
75785
|
+
}
|
|
75786
|
+
}] : [];
|
|
75787
|
+
},
|
|
75788
|
+
directives: [],
|
|
75789
|
+
shaping: () => ({
|
|
75790
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75791
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75792
|
+
numericOf: numericValueField
|
|
75793
|
+
})
|
|
75794
|
+
};
|
|
75795
|
+
//#endregion
|
|
75796
|
+
//#region src/endpoints/adapters/climate.ts
|
|
75797
|
+
var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
|
|
75798
|
+
var THERMOSTAT_DIRECTIVES = [
|
|
75799
|
+
"Alexa.ThermostatController/SetTargetTemperature",
|
|
75800
|
+
"Alexa.ThermostatController/AdjustTargetTemperature",
|
|
75801
|
+
"Alexa.ThermostatController/SetThermostatMode"
|
|
75802
|
+
].map((key) => ({
|
|
75803
|
+
key,
|
|
75804
|
+
needsReachableDevice: true,
|
|
75805
|
+
handle: handleClimateDirective
|
|
75806
|
+
}));
|
|
75807
|
+
var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
|
|
75808
|
+
key,
|
|
75809
|
+
needsReachableDevice: true,
|
|
75810
|
+
handle: handleClimateDirective,
|
|
75811
|
+
claims: handlesPower
|
|
75812
|
+
}));
|
|
75813
|
+
var MODE_DIRECTIVE = {
|
|
75814
|
+
key: "Alexa.ModeController/SetMode",
|
|
75815
|
+
needsReachableDevice: true,
|
|
75816
|
+
handle: handleClimateDirective,
|
|
75817
|
+
claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
|
|
75818
|
+
};
|
|
75819
|
+
/**
|
|
75820
|
+
* `climate-control` -> a thermostat (Home Assistant) or an air conditioner
|
|
75821
|
+
* (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
|
|
75822
|
+
* power, and the modes Amazon's vocabulary lacks and the fan mode as
|
|
75823
|
+
* ModeControllers. The current temperature is shaped like any temperature
|
|
75824
|
+
* sensor; setpoints, mode and fan report on every change.
|
|
75825
|
+
*/
|
|
75826
|
+
var climateAdapter = {
|
|
75827
|
+
cap: "climate-control",
|
|
75828
|
+
stateCaps: ["climate-control"],
|
|
75829
|
+
discoveryReads: ["climate-control"],
|
|
75830
|
+
describe: describeClimate,
|
|
75831
|
+
properties: (_cap, slice) => climateProperties(slice),
|
|
75832
|
+
directives: [
|
|
75833
|
+
...THERMOSTAT_DIRECTIVES,
|
|
75834
|
+
...POWER_DIRECTIVES$1,
|
|
75835
|
+
MODE_DIRECTIVE
|
|
75836
|
+
],
|
|
75837
|
+
shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
|
|
75838
|
+
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
75839
|
+
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
75840
|
+
numericOf: numericValueField
|
|
75841
|
+
} : null
|
|
75842
|
+
};
|
|
75843
|
+
//#endregion
|
|
74080
75844
|
//#region src/endpoints/adapters/contact.ts
|
|
74081
75845
|
/** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
|
|
74082
75846
|
var contactAdapter = {
|
|
@@ -74741,7 +76505,8 @@ var coverAdapter = {
|
|
|
74741
76505
|
].map((key) => ({
|
|
74742
76506
|
key,
|
|
74743
76507
|
needsReachableDevice: true,
|
|
74744
|
-
handle: handleCoverDirective
|
|
76508
|
+
handle: handleCoverDirective,
|
|
76509
|
+
claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
|
|
74745
76510
|
})),
|
|
74746
76511
|
shaping: (property) => property.namespace === "Alexa.RangeController" ? {
|
|
74747
76512
|
deadband: 1,
|
|
@@ -74749,40 +76514,6 @@ var coverAdapter = {
|
|
|
74749
76514
|
numericOf: plainNumber
|
|
74750
76515
|
} : { debounceMs: COVER_DEBOUNCE_MS }
|
|
74751
76516
|
};
|
|
74752
|
-
//#endregion
|
|
74753
|
-
//#region src/endpoints/adapters/temperature.ts
|
|
74754
|
-
/** A temperature change under half a degree is not worth a ChangeReport. */
|
|
74755
|
-
var TEMPERATURE_DEADBAND_CELSIUS = .5;
|
|
74756
|
-
/** A numeric sensor reports at most once a minute per property (D869). */
|
|
74757
|
-
var SENSOR_MIN_INTERVAL_MS = 6e4;
|
|
74758
|
-
/** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
|
|
74759
|
-
var temperatureAdapter = {
|
|
74760
|
-
cap: "temperature-sensor",
|
|
74761
|
-
stateCaps: ["temperature-sensor"],
|
|
74762
|
-
discoveryReads: [],
|
|
74763
|
-
describe: () => ({
|
|
74764
|
-
kind: "publish",
|
|
74765
|
-
capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
|
|
74766
|
-
category: "TEMPERATURE_SENSOR"
|
|
74767
|
-
}),
|
|
74768
|
-
properties: (_cap, slice) => {
|
|
74769
|
-
const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
|
|
74770
|
-
return p.success ? [{
|
|
74771
|
-
namespace: "Alexa.TemperatureSensor",
|
|
74772
|
-
name: "temperature",
|
|
74773
|
-
value: {
|
|
74774
|
-
value: Math.round(p.data.celsius * 10) / 10,
|
|
74775
|
-
scale: "CELSIUS"
|
|
74776
|
-
}
|
|
74777
|
-
}] : [];
|
|
74778
|
-
},
|
|
74779
|
-
directives: [],
|
|
74780
|
-
shaping: () => ({
|
|
74781
|
-
deadband: TEMPERATURE_DEADBAND_CELSIUS,
|
|
74782
|
-
minIntervalMs: SENSOR_MIN_INTERVAL_MS,
|
|
74783
|
-
numericOf: numericValueField
|
|
74784
|
-
})
|
|
74785
|
-
};
|
|
74786
76517
|
/** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
|
|
74787
76518
|
var humidityAdapter = {
|
|
74788
76519
|
cap: "humidity-sensor",
|
|
@@ -75081,6 +76812,11 @@ var BRIGHTNESS_DIRECTIVE = {
|
|
|
75081
76812
|
needsReachableDevice: true,
|
|
75082
76813
|
handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
|
|
75083
76814
|
};
|
|
76815
|
+
/** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
|
|
76816
|
+
* smart plug; every other switch is a SWITCH. */
|
|
76817
|
+
function switchCategory(device) {
|
|
76818
|
+
return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
|
|
76819
|
+
}
|
|
75084
76820
|
//#endregion
|
|
75085
76821
|
//#region src/endpoints/adapters/index.ts
|
|
75086
76822
|
var GENERIC_ADAPTERS = [
|
|
@@ -75088,6 +76824,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75088
76824
|
contactAdapter,
|
|
75089
76825
|
motionAdapter,
|
|
75090
76826
|
presenceAdapter,
|
|
76827
|
+
climateAdapter,
|
|
75091
76828
|
temperatureAdapter,
|
|
75092
76829
|
humidityAdapter,
|
|
75093
76830
|
{
|
|
@@ -75099,7 +76836,7 @@ var GENERIC_ADAPTERS = [
|
|
|
75099
76836
|
return {
|
|
75100
76837
|
kind: "publish",
|
|
75101
76838
|
capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
|
|
75102
|
-
category: dimmable || device.type === "light" ? "LIGHT" :
|
|
76839
|
+
category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
|
|
75103
76840
|
};
|
|
75104
76841
|
},
|
|
75105
76842
|
properties: (capName, slice) => {
|
|
@@ -75145,12 +76882,19 @@ function shapingFor(capName, property) {
|
|
|
75145
76882
|
function discoveryReadCaps(capNames) {
|
|
75146
76883
|
return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
|
|
75147
76884
|
}
|
|
75148
|
-
/**
|
|
75149
|
-
function
|
|
75150
|
-
|
|
75151
|
-
|
|
75152
|
-
|
|
75153
|
-
|
|
76885
|
+
/** Every directive an adapter declares for `Namespace/Name`, in registry order. */
|
|
76886
|
+
function genericDirectives(key) {
|
|
76887
|
+
return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
|
|
76888
|
+
}
|
|
76889
|
+
/**
|
|
76890
|
+
* The directive that serves this endpoint: of the adapters declaring the key,
|
|
76891
|
+
* in registry order, the first that claims it (two caps may answer one key -
|
|
76892
|
+
* power on a switch and on a thermostat). Null = nobody claims it.
|
|
76893
|
+
*/
|
|
76894
|
+
async function resolveGenericDirective(context) {
|
|
76895
|
+
const key = `${context.header.namespace}/${context.header.name}`;
|
|
76896
|
+
for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
|
|
76897
|
+
return null;
|
|
75154
76898
|
}
|
|
75155
76899
|
/** Every directive key the registry handles. */
|
|
75156
76900
|
var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
|
|
@@ -75192,9 +76936,9 @@ function planGenericDevice(device) {
|
|
|
75192
76936
|
category
|
|
75193
76937
|
};
|
|
75194
76938
|
}
|
|
75195
|
-
/** Device types this export may publish as a generic endpoint.
|
|
75196
|
-
*
|
|
75197
|
-
*
|
|
76939
|
+
/** Device types this export may publish as a generic endpoint. `thermostat`
|
|
76940
|
+
* (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
|
|
76941
|
+
* the `climate-control` cap (D874). */
|
|
75198
76942
|
var GENERIC_DEVICE_TYPES = [
|
|
75199
76943
|
"light",
|
|
75200
76944
|
"switch",
|
|
@@ -75203,11 +76947,13 @@ var GENERIC_DEVICE_TYPES = [
|
|
|
75203
76947
|
"lock",
|
|
75204
76948
|
"presence",
|
|
75205
76949
|
"thermostat",
|
|
76950
|
+
"climate",
|
|
75206
76951
|
"cover",
|
|
75207
76952
|
"generic"
|
|
75208
76953
|
];
|
|
75209
76954
|
function toEndpoint(input, plan) {
|
|
75210
|
-
const
|
|
76955
|
+
const override = input.alexaName?.trim() ?? "";
|
|
76956
|
+
const name = override.length > 0 ? override : input.name.trim();
|
|
75211
76957
|
return {
|
|
75212
76958
|
endpointId: `${input.deviceId}__dev`,
|
|
75213
76959
|
manufacturerName: "camstack",
|
|
@@ -75218,13 +76964,7 @@ function toEndpoint(input, plan) {
|
|
|
75218
76964
|
cookie: { stableId: input.stableId }
|
|
75219
76965
|
};
|
|
75220
76966
|
}
|
|
75221
|
-
/** Why a climate device is withheld until its adapter exists. */
|
|
75222
|
-
var THERMOSTAT_SKIP_REASON = "thermostat: Alexa climate control (Alexa.ThermostatController) has no adapter yet - not published until it does (D869, P3)";
|
|
75223
76967
|
function planGenericEndpoint(input) {
|
|
75224
|
-
if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
|
|
75225
|
-
kind: "skip",
|
|
75226
|
-
reasons: [THERMOSTAT_SKIP_REASON]
|
|
75227
|
-
};
|
|
75228
76968
|
const plan = planGenericDevice({
|
|
75229
76969
|
deviceId: input.deviceId,
|
|
75230
76970
|
type: input.type,
|
|
@@ -75240,6 +76980,74 @@ function planGenericEndpoint(input) {
|
|
|
75240
76980
|
return plan;
|
|
75241
76981
|
}
|
|
75242
76982
|
//#endregion
|
|
76983
|
+
//#region src/endpoints/alexa-name.ts
|
|
76984
|
+
/**
|
|
76985
|
+
* The per-device "Alexa name" (D875): what Alexa calls the device, when it
|
|
76986
|
+
* should not be the device's own name.
|
|
76987
|
+
*
|
|
76988
|
+
* Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
|
|
76989
|
+
* (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
|
|
76990
|
+
* id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
|
|
76991
|
+
* AddOrUpdateReport instead of creating a duplicate.
|
|
76992
|
+
*
|
|
76993
|
+
* Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
|
|
76994
|
+
* to 256 alphanumeric characters and spaces. Don't include special characters
|
|
76995
|
+
* or punctuation." Letters of any script count as alphanumeric, so an Italian
|
|
76996
|
+
* name keeps its accents. The validator rejects what Amazon would reject, with
|
|
76997
|
+
* a message the operator can act on.
|
|
76998
|
+
*/
|
|
76999
|
+
/** Per-device store key of the saved Alexa name. */
|
|
77000
|
+
var ALEXA_NAME_KEY = "alexaName";
|
|
77001
|
+
/** The device-settings form field key, namespaced per exporter (see the addon). */
|
|
77002
|
+
function alexaNameKey(deviceId) {
|
|
77003
|
+
return `alexa:${deviceId}:alexaName`;
|
|
77004
|
+
}
|
|
77005
|
+
/** Letters (any script, with their combining marks), digits and spaces. */
|
|
77006
|
+
var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
|
|
77007
|
+
/**
|
|
77008
|
+
* Validate an operator-typed Alexa name. Blank clears the override
|
|
77009
|
+
* (`name: null`, the device name is used).
|
|
77010
|
+
*/
|
|
77011
|
+
function validateAlexaName(raw) {
|
|
77012
|
+
if (raw === null || raw === void 0) return {
|
|
77013
|
+
ok: true,
|
|
77014
|
+
name: null
|
|
77015
|
+
};
|
|
77016
|
+
if (typeof raw !== "string") return {
|
|
77017
|
+
ok: false,
|
|
77018
|
+
error: "The Alexa name must be text."
|
|
77019
|
+
};
|
|
77020
|
+
const name = raw.trim().replace(/\s+/g, " ");
|
|
77021
|
+
if (name.length === 0) return {
|
|
77022
|
+
ok: true,
|
|
77023
|
+
name: null
|
|
77024
|
+
};
|
|
77025
|
+
if (name.length > 256) return {
|
|
77026
|
+
ok: false,
|
|
77027
|
+
error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
|
|
77028
|
+
};
|
|
77029
|
+
if (!ALLOWED_PATTERN.test(name)) return {
|
|
77030
|
+
ok: false,
|
|
77031
|
+
error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
|
|
77032
|
+
};
|
|
77033
|
+
return {
|
|
77034
|
+
ok: true,
|
|
77035
|
+
name
|
|
77036
|
+
};
|
|
77037
|
+
}
|
|
77038
|
+
var StoredNameSchema = string();
|
|
77039
|
+
/**
|
|
77040
|
+
* The saved Alexa name in a per-device store blob, or null (never saved, or a
|
|
77041
|
+
* stored value that no longer validates: the device name is used rather than
|
|
77042
|
+
* pushing a name Amazon would refuse).
|
|
77043
|
+
*/
|
|
77044
|
+
function readAlexaName(store) {
|
|
77045
|
+
const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
|
|
77046
|
+
if (!parsed.success) return null;
|
|
77047
|
+
const check = validateAlexaName(parsed.data);
|
|
77048
|
+
return check.ok ? check.name : null;
|
|
77049
|
+
}
|
|
77050
|
+
//#endregion
|
|
75243
77051
|
//#region src/handlers/security-panel-arm-state.ts
|
|
75244
77052
|
/**
|
|
75245
77053
|
* The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
|
|
@@ -75913,6 +77721,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75913
77721
|
deviceId: parentInfo.id,
|
|
75914
77722
|
stableId: parentInfo.stableId,
|
|
75915
77723
|
name: parentInfo.name,
|
|
77724
|
+
alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
|
|
75916
77725
|
type: String(device.type),
|
|
75917
77726
|
capNames: parentInfo.capNames,
|
|
75918
77727
|
role: parentInfo.role,
|
|
@@ -75971,11 +77780,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75971
77780
|
return [];
|
|
75972
77781
|
});
|
|
75973
77782
|
const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
|
|
77783
|
+
const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
|
|
75974
77784
|
const out = [];
|
|
75975
77785
|
out.push(buildParentCameraEndpoint({
|
|
75976
77786
|
deviceId: parentInfo.id,
|
|
75977
77787
|
stableId: parentInfo.stableId,
|
|
75978
|
-
friendlyName:
|
|
77788
|
+
friendlyName: cameraName,
|
|
75979
77789
|
isDoorbell,
|
|
75980
77790
|
hasIntercom,
|
|
75981
77791
|
hasMotion,
|
|
@@ -75989,7 +77799,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
|
|
|
75989
77799
|
for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
|
|
75990
77800
|
deviceId: parentInfo.id,
|
|
75991
77801
|
stableId: parentInfo.stableId,
|
|
75992
|
-
cameraName
|
|
77802
|
+
cameraName,
|
|
75993
77803
|
detectionClass
|
|
75994
77804
|
}));
|
|
75995
77805
|
}
|
|
@@ -76483,6 +78293,36 @@ function validateInstanced(c, push) {
|
|
|
76483
78293
|
if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
|
|
76484
78294
|
}
|
|
76485
78295
|
//#endregion
|
|
78296
|
+
//#region src/endpoints/validate-thermostat.ts
|
|
78297
|
+
var PROPERTIES = new Set([
|
|
78298
|
+
"targetSetpoint",
|
|
78299
|
+
"lowerSetpoint",
|
|
78300
|
+
"upperSetpoint",
|
|
78301
|
+
"thermostatMode"
|
|
78302
|
+
]);
|
|
78303
|
+
var MODES = new Set([
|
|
78304
|
+
"AUTO",
|
|
78305
|
+
"COOL",
|
|
78306
|
+
"HEAT",
|
|
78307
|
+
"ECO",
|
|
78308
|
+
"OFF"
|
|
78309
|
+
]);
|
|
78310
|
+
function validateThermostat(c, push) {
|
|
78311
|
+
const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
|
|
78312
|
+
for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
|
|
78313
|
+
if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
|
|
78314
|
+
if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
|
|
78315
|
+
if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
|
|
78316
|
+
const modes = c.configuration?.["supportedModes"];
|
|
78317
|
+
if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
|
|
78318
|
+
else {
|
|
78319
|
+
for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
|
|
78320
|
+
if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
|
|
78321
|
+
}
|
|
78322
|
+
const scheduling = c.configuration?.["supportsScheduling"];
|
|
78323
|
+
if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
|
|
78324
|
+
}
|
|
78325
|
+
//#endregion
|
|
76486
78326
|
//#region src/endpoints/validate-endpoint.ts
|
|
76487
78327
|
/**
|
|
76488
78328
|
* Interfaces this skill may declare, with the version Amazon documents. An
|
|
@@ -76506,6 +78346,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
|
|
|
76506
78346
|
"Alexa.RangeController": "3",
|
|
76507
78347
|
"Alexa.SecurityPanelController": "3",
|
|
76508
78348
|
"Alexa.LockController": "3",
|
|
78349
|
+
"Alexa.ThermostatController": "3.1",
|
|
76509
78350
|
"Alexa.SmartVision.SnapshotProvider": "1.1",
|
|
76510
78351
|
"Alexa.SmartVision.ObjectDetectionSensor": "1.0",
|
|
76511
78352
|
"Alexa.DataController": "1.0"
|
|
@@ -76545,6 +78386,8 @@ var DISPLAY_CATEGORIES = new Set([
|
|
|
76545
78386
|
"SMARTLOCK",
|
|
76546
78387
|
"INTERIOR_BLIND",
|
|
76547
78388
|
"EXTERIOR_BLIND",
|
|
78389
|
+
"THERMOSTAT",
|
|
78390
|
+
"AIR_CONDITIONER",
|
|
76548
78391
|
"DOOR",
|
|
76549
78392
|
"GATE",
|
|
76550
78393
|
"GARAGE_DOOR",
|
|
@@ -76623,6 +78466,7 @@ function validateCapability(endpointId, c, out, declared) {
|
|
|
76623
78466
|
}
|
|
76624
78467
|
}
|
|
76625
78468
|
validateInstanced(c, push);
|
|
78469
|
+
if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
|
|
76626
78470
|
if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
|
|
76627
78471
|
if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
|
|
76628
78472
|
if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
|
|
@@ -78553,20 +80397,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
|
|
|
78553
80397
|
status: 200,
|
|
78554
80398
|
body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
|
|
78555
80399
|
};
|
|
78556
|
-
|
|
78557
|
-
if (adapterDirective !== void 0 && endpoint !== void 0) {
|
|
80400
|
+
if (genericDirectives(key).length > 0 && endpoint !== void 0) {
|
|
78558
80401
|
const target = parseCameraEndpointId(endpoint.endpointId);
|
|
78559
|
-
if (target.kind === "generic")
|
|
78560
|
-
|
|
78561
|
-
body: await adapterDirective.handle({
|
|
80402
|
+
if (target.kind === "generic") {
|
|
80403
|
+
const directiveContext = {
|
|
78562
80404
|
header,
|
|
78563
80405
|
endpoint,
|
|
78564
80406
|
payload,
|
|
78565
80407
|
deviceId: target.deviceId,
|
|
78566
80408
|
deps,
|
|
78567
80409
|
identity: callerIdentity
|
|
78568
|
-
}
|
|
78569
|
-
|
|
80410
|
+
};
|
|
80411
|
+
const adapterDirective = await resolveGenericDirective(directiveContext);
|
|
80412
|
+
if (adapterDirective === null) return {
|
|
80413
|
+
status: 200,
|
|
80414
|
+
body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
|
|
80415
|
+
};
|
|
80416
|
+
return {
|
|
80417
|
+
status: 200,
|
|
80418
|
+
body: await adapterDirective.handle(directiveContext)
|
|
80419
|
+
};
|
|
80420
|
+
}
|
|
78570
80421
|
}
|
|
78571
80422
|
if (key === "Alexa.ModeController/SetMode") return {
|
|
78572
80423
|
status: 200,
|
|
@@ -82709,6 +84560,7 @@ var SUPPORTED_KINDS = [
|
|
|
82709
84560
|
DeviceType.Lock,
|
|
82710
84561
|
DeviceType.Presence,
|
|
82711
84562
|
DeviceType.Thermostat,
|
|
84563
|
+
DeviceType.Climate,
|
|
82712
84564
|
DeviceType.Cover,
|
|
82713
84565
|
DeviceType.Generic
|
|
82714
84566
|
];
|
|
@@ -82847,6 +84699,10 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
82847
84699
|
ctx: this.ctx,
|
|
82848
84700
|
logger: this.ctx.logger
|
|
82849
84701
|
},
|
|
84702
|
+
climateController: {
|
|
84703
|
+
ctx: this.ctx,
|
|
84704
|
+
logger: this.ctx.logger
|
|
84705
|
+
},
|
|
82850
84706
|
coverController: {
|
|
82851
84707
|
ctx: this.ctx,
|
|
82852
84708
|
logger: this.ctx.logger,
|
|
@@ -83103,6 +84959,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83103
84959
|
logger: this.ctx.logger,
|
|
83104
84960
|
snapshots: this.config.snapshots,
|
|
83105
84961
|
...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
|
|
84962
|
+
alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
|
|
83106
84963
|
...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
|
|
83107
84964
|
});
|
|
83108
84965
|
}
|
|
@@ -83670,12 +85527,28 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83670
85527
|
value: enabled,
|
|
83671
85528
|
immediate: true
|
|
83672
85529
|
},
|
|
85530
|
+
await this.alexaNameField(deviceId, enabledKey),
|
|
83673
85531
|
...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
|
|
83674
85532
|
...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
|
|
83675
85533
|
]
|
|
83676
85534
|
}]
|
|
83677
85535
|
};
|
|
83678
85536
|
}
|
|
85537
|
+
/** D875: the "Alexa name" field, with the validation error of the saved value. */
|
|
85538
|
+
async alexaNameField(deviceId, enabledKey) {
|
|
85539
|
+
const saved = readAlexaName(await this.vision.readPrefs(deviceId));
|
|
85540
|
+
return {
|
|
85541
|
+
type: "text",
|
|
85542
|
+
key: alexaNameKey(deviceId),
|
|
85543
|
+
label: "Alexa name",
|
|
85544
|
+
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.`,
|
|
85545
|
+
value: saved ?? "",
|
|
85546
|
+
showWhen: {
|
|
85547
|
+
field: enabledKey,
|
|
85548
|
+
equals: true
|
|
85549
|
+
}
|
|
85550
|
+
};
|
|
85551
|
+
}
|
|
83679
85552
|
/**
|
|
83680
85553
|
* The camera's detection fields: which classes become Alexa sensors (D778;
|
|
83681
85554
|
* only the classes its pipeline produces are offered) and its object
|
|
@@ -83697,6 +85570,24 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83697
85570
|
})];
|
|
83698
85571
|
}
|
|
83699
85572
|
/**
|
|
85573
|
+
* D875: save the Alexa name from a device-settings patch. A name Amazon would
|
|
85574
|
+
* refuse throws BEFORE anything is written, so the form shows the reason and
|
|
85575
|
+
* the old name stays. True when the saved name changed.
|
|
85576
|
+
*/
|
|
85577
|
+
async applyAlexaNamePatch(deviceId, patch) {
|
|
85578
|
+
const key = alexaNameKey(deviceId);
|
|
85579
|
+
if (!(key in patch)) return false;
|
|
85580
|
+
const check = validateAlexaName(patch[key]);
|
|
85581
|
+
if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
|
|
85582
|
+
if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
|
|
85583
|
+
await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
|
|
85584
|
+
this.ctx.logger.info("export-alexa: Alexa name saved", {
|
|
85585
|
+
tags: { deviceId },
|
|
85586
|
+
meta: { name: check.name }
|
|
85587
|
+
});
|
|
85588
|
+
return true;
|
|
85589
|
+
}
|
|
85590
|
+
/**
|
|
83700
85591
|
* Persist the camera's detection preferences from a device-settings patch
|
|
83701
85592
|
* into this addon's per-device store. True when anything changed — the
|
|
83702
85593
|
* camera's Alexa shape then needs a reconcile.
|
|
@@ -83761,6 +85652,7 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83761
85652
|
const current = this.findEntry(deviceId);
|
|
83762
85653
|
const wasEnabled = current !== null;
|
|
83763
85654
|
const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
|
|
85655
|
+
const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
|
|
83764
85656
|
const enabledKey = `alexa:${deviceId}:enabled`;
|
|
83765
85657
|
const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
|
|
83766
85658
|
const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
|
|
@@ -83786,6 +85678,12 @@ var ExportAlexaAddon = class extends BaseAddon {
|
|
|
83786
85678
|
const currentAllow = current?.settings?.allowTranscode !== false;
|
|
83787
85679
|
await this.patchEntrySettings(deviceId, nextSettings);
|
|
83788
85680
|
const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
|
|
85681
|
+
if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
|
|
85682
|
+
this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
|
|
85683
|
+
tags: { deviceId },
|
|
85684
|
+
meta: { error: errMsg(err) }
|
|
85685
|
+
});
|
|
85686
|
+
});
|
|
83789
85687
|
if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
|
|
83790
85688
|
this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
|
|
83791
85689
|
tags: { deviceId },
|