@camstack/addon-import-alexa 0.2.177 → 0.2.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/addon.js +1550 -315
- package/dist/addon.mjs +1550 -315
- package/package.json +2 -2
- package/python/alexa_gateway.py +123 -1
- package/python/alexa_protocol.py +43 -1
- package/python/alexa_sequences.py +15 -0
- package/python/test_alexa_gateway_echo.py +149 -0
- package/python/test_alexa_gateway_send.py +1 -1
package/dist/addon.js
CHANGED
|
@@ -834,7 +834,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
834
834
|
return EventCategory;
|
|
835
835
|
}({});
|
|
836
836
|
//#endregion
|
|
837
|
-
//#region ../types/dist/device-type-
|
|
837
|
+
//#region ../types/dist/device-type-CfHw_quR.mjs
|
|
838
838
|
/**
|
|
839
839
|
import { errMsg } from '@camstack/types'
|
|
840
840
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -1023,6 +1023,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
1023
1023
|
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
1024
1024
|
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
1025
1025
|
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
1026
|
+
/** Cover has a real stop: `stop` halts the travel in place. Gates the stop
|
|
1027
|
+
* control in exporters; a provider whose `stop` is a no-op never declares it. */
|
|
1028
|
+
DeviceFeature["CoverStoppable"] = "cover-stoppable";
|
|
1026
1029
|
/** Valve exposes intermediate position control (0..100). Gates the
|
|
1027
1030
|
* position slider / drag surface UI. */
|
|
1028
1031
|
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
@@ -6035,7 +6038,7 @@ function number(params) {
|
|
|
6035
6038
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6036
6039
|
}
|
|
6037
6040
|
//#endregion
|
|
6038
|
-
//#region ../types/dist/sleep-
|
|
6041
|
+
//#region ../types/dist/sleep-hmSP-eJt.mjs
|
|
6039
6042
|
/**
|
|
6040
6043
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6041
6044
|
* window to float samples (D455).
|
|
@@ -6152,7 +6155,7 @@ function isKeychainStoredField(field) {
|
|
|
6152
6155
|
var SECRET_CLEAR = { $secretClear: true };
|
|
6153
6156
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
6154
6157
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
6155
|
-
function isRecord$
|
|
6158
|
+
function isRecord$7(value) {
|
|
6156
6159
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6157
6160
|
}
|
|
6158
6161
|
function makeSecretMarker(ref) {
|
|
@@ -6163,12 +6166,12 @@ function makeSecretMarker(ref) {
|
|
|
6163
6166
|
}
|
|
6164
6167
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
6165
6168
|
function isSecretMarker(value) {
|
|
6166
|
-
if (!isRecord$
|
|
6169
|
+
if (!isRecord$7(value)) return false;
|
|
6167
6170
|
const ref = value["$secret"];
|
|
6168
6171
|
return typeof ref === "string" && ref.length > 0;
|
|
6169
6172
|
}
|
|
6170
6173
|
function isSecretClear(value) {
|
|
6171
|
-
return isRecord$
|
|
6174
|
+
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6172
6175
|
}
|
|
6173
6176
|
/**
|
|
6174
6177
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -6921,7 +6924,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
6921
6924
|
}
|
|
6922
6925
|
};
|
|
6923
6926
|
var KEEP = { kind: "keep" };
|
|
6924
|
-
function isRecord$
|
|
6927
|
+
function isRecord$6(value) {
|
|
6925
6928
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6926
6929
|
}
|
|
6927
6930
|
function defaultMintId() {
|
|
@@ -6934,7 +6937,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
6934
6937
|
return out;
|
|
6935
6938
|
}
|
|
6936
6939
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
6937
|
-
else if (isRecord$
|
|
6940
|
+
else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
6938
6941
|
return out;
|
|
6939
6942
|
}
|
|
6940
6943
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -6989,7 +6992,7 @@ function rowId(row, idKey) {
|
|
|
6989
6992
|
return null;
|
|
6990
6993
|
}
|
|
6991
6994
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
6992
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
6995
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
|
|
6993
6996
|
if (!Array.isArray(incoming)) {
|
|
6994
6997
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
6995
6998
|
return {
|
|
@@ -7005,7 +7008,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
7005
7008
|
const kept = /* @__PURE__ */ new Set();
|
|
7006
7009
|
const rows = [];
|
|
7007
7010
|
for (const row of incoming) {
|
|
7008
|
-
if (!isRecord$
|
|
7011
|
+
if (!isRecord$6(row)) {
|
|
7009
7012
|
rows.push(row);
|
|
7010
7013
|
continue;
|
|
7011
7014
|
}
|
|
@@ -7030,7 +7033,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
7030
7033
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
7031
7034
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
7032
7035
|
case "object": {
|
|
7033
|
-
if (!isRecord$
|
|
7036
|
+
if (!isRecord$6(incoming)) {
|
|
7034
7037
|
if (incoming === void 0) return KEEP;
|
|
7035
7038
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
7036
7039
|
return {
|
|
@@ -7038,7 +7041,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
7038
7041
|
value: incoming
|
|
7039
7042
|
};
|
|
7040
7043
|
}
|
|
7041
|
-
const storedObj = isRecord$
|
|
7044
|
+
const storedObj = isRecord$6(stored) ? stored : {};
|
|
7042
7045
|
return {
|
|
7043
7046
|
kind: "value",
|
|
7044
7047
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -7161,7 +7164,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
7161
7164
|
value: items
|
|
7162
7165
|
};
|
|
7163
7166
|
}
|
|
7164
|
-
if (isRecord$
|
|
7167
|
+
if (isRecord$6(v)) {
|
|
7165
7168
|
const out = {};
|
|
7166
7169
|
for (const [k, child] of Object.entries(v)) {
|
|
7167
7170
|
const s = substitute(child);
|
|
@@ -7193,7 +7196,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
7193
7196
|
this.name = "SecretOverwriteRefusedError";
|
|
7194
7197
|
}
|
|
7195
7198
|
};
|
|
7196
|
-
function isRecord$
|
|
7199
|
+
function isRecord$5(value) {
|
|
7197
7200
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7198
7201
|
}
|
|
7199
7202
|
function resolvedValue(pass, marker) {
|
|
@@ -7222,7 +7225,7 @@ function inArray(pass, incoming, stored) {
|
|
|
7222
7225
|
return incoming.map((item, index) => {
|
|
7223
7226
|
const twin = stored[index];
|
|
7224
7227
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
7225
|
-
if (isRecord$
|
|
7228
|
+
if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
|
|
7226
7229
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
7227
7230
|
return item;
|
|
7228
7231
|
});
|
|
@@ -7233,7 +7236,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
7233
7236
|
const twin = stored[key];
|
|
7234
7237
|
if (isSecretMarker(twin)) {
|
|
7235
7238
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
7236
|
-
} else if (isRecord$
|
|
7239
|
+
} else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
7237
7240
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
7238
7241
|
}
|
|
7239
7242
|
return out;
|
|
@@ -7241,7 +7244,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
7241
7244
|
async function walk(pass, incoming, stored) {
|
|
7242
7245
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
7243
7246
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
7244
|
-
if (!isRecord$
|
|
7247
|
+
if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
|
|
7245
7248
|
const out = { ...incoming };
|
|
7246
7249
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
7247
7250
|
return out;
|
|
@@ -7272,7 +7275,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
7272
7275
|
function isJsonEqual(a, b) {
|
|
7273
7276
|
if (Object.is(a, b)) return true;
|
|
7274
7277
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
7275
|
-
if (isRecord$
|
|
7278
|
+
if (isRecord$5(a) && isRecord$5(b)) {
|
|
7276
7279
|
const keys = Object.keys(a);
|
|
7277
7280
|
if (keys.length !== Object.keys(b).length) return false;
|
|
7278
7281
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -7296,7 +7299,7 @@ function markerKeysOf(blob, owner) {
|
|
|
7296
7299
|
}
|
|
7297
7300
|
function nodeFromStored(value, owner) {
|
|
7298
7301
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
7299
|
-
if (!isRecord$
|
|
7302
|
+
if (!isRecord$5(value)) return null;
|
|
7300
7303
|
const children = /* @__PURE__ */ new Map();
|
|
7301
7304
|
for (const [key, child] of Object.entries(value)) {
|
|
7302
7305
|
const node = nodeFromStored(child, owner);
|
|
@@ -7915,6 +7918,44 @@ var BaseAddon = class {
|
|
|
7915
7918
|
return null;
|
|
7916
7919
|
}
|
|
7917
7920
|
/**
|
|
7921
|
+
* D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
|
|
7922
|
+
* provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
|
|
7923
|
+
* declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
|
|
7924
|
+
* {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
|
|
7925
|
+
* contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
|
|
7926
|
+
*/
|
|
7927
|
+
async clusterSettingsSchema(_cap) {
|
|
7928
|
+
return null;
|
|
7929
|
+
}
|
|
7930
|
+
/**
|
|
7931
|
+
* D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
|
|
7932
|
+
* `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
|
|
7933
|
+
* forgotten node's rows go in one central delete.
|
|
7934
|
+
*/
|
|
7935
|
+
async nodeSettingsSchema(_cap) {
|
|
7936
|
+
return null;
|
|
7937
|
+
}
|
|
7938
|
+
/**
|
|
7939
|
+
* D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
|
|
7940
|
+
* or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
|
|
7941
|
+
* as device row, then this default, then the schema default (`resolveDeviceSettings`).
|
|
7942
|
+
*/
|
|
7943
|
+
async deviceDefaultsSchema(_cap) {
|
|
7944
|
+
return null;
|
|
7945
|
+
}
|
|
7946
|
+
/**
|
|
7947
|
+
* The host's read of the scoped declarations: the schema one capability declares for `scope`,
|
|
7948
|
+
* or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
|
|
7949
|
+
* host asks this method.
|
|
7950
|
+
*/
|
|
7951
|
+
async getScopedSettingsSchema(scope, cap) {
|
|
7952
|
+
switch (scope) {
|
|
7953
|
+
case "cluster": return this.clusterSettingsSchema(cap);
|
|
7954
|
+
case "node": return this.nodeSettingsSchema(cap);
|
|
7955
|
+
case "device-defaults": return this.deviceDefaultsSchema(cap);
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
/**
|
|
7918
7959
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
7919
7960
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
7920
7961
|
*/
|
|
@@ -8708,47 +8749,6 @@ function normalizeAddonInitResult(result) {
|
|
|
8708
8749
|
if (Array.isArray(result)) return { providers: result };
|
|
8709
8750
|
return result;
|
|
8710
8751
|
}
|
|
8711
|
-
/**
|
|
8712
|
-
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8713
|
-
* and validates them (`ShareViewGrantSchema` builds its
|
|
8714
|
-
* enum from this list); a data plane names the kinds it ACCEPTS in `share`.
|
|
8715
|
-
*/
|
|
8716
|
-
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
8717
|
-
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
8718
|
-
var PeerBytesTicketSchema = object({
|
|
8719
|
-
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
8720
|
-
url: string().min(1),
|
|
8721
|
-
/**
|
|
8722
|
-
* The HOST node this URL means something on — the hub or a named agent,
|
|
8723
|
-
* never a runner. {@link AddonPeerBytes.open} compares it to its own and
|
|
8724
|
-
* refuses `cross-node` by name when they differ, without dialling.
|
|
8725
|
-
*/
|
|
8726
|
-
hostNodeId: string().min(1),
|
|
8727
|
-
expiresAtMs: number$1().int().nonnegative(),
|
|
8728
|
-
/**
|
|
8729
|
-
* What the producer DECLARED the body to be, when it knows — `null` when it
|
|
8730
|
-
* does not. Never `0` for unknown (D393): a consumer sizing a bound off this
|
|
8731
|
-
* must be able to tell "the producer did not say" from "the body is empty".
|
|
8732
|
-
*/
|
|
8733
|
-
declaredBytes: number$1().int().nonnegative().nullable()
|
|
8734
|
-
});
|
|
8735
|
-
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8736
|
-
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8737
|
-
var RECORD_DATABASES = ["settings", "events"];
|
|
8738
|
-
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
8739
|
-
var RECORD_CLASSES = [
|
|
8740
|
-
"config",
|
|
8741
|
-
"ledger",
|
|
8742
|
-
"audit",
|
|
8743
|
-
"mirror",
|
|
8744
|
-
"ephemeral"
|
|
8745
|
-
];
|
|
8746
|
-
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
8747
|
-
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8748
|
-
var RECORD_BATCH_MAX = 1e3;
|
|
8749
|
-
var RECORD_QUERY_MAX = 1e3;
|
|
8750
|
-
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8751
|
-
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8752
8752
|
var PREFIX = "records/";
|
|
8753
8753
|
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8754
8754
|
var RecordsError = class extends Error {
|
|
@@ -8781,6 +8781,83 @@ var DeclarationRefusedError = class extends RecordsError {
|
|
|
8781
8781
|
this.name = "DeclarationRefusedError";
|
|
8782
8782
|
}
|
|
8783
8783
|
};
|
|
8784
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8785
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8786
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
8787
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
8788
|
+
var RECORD_CLASSES = [
|
|
8789
|
+
"config",
|
|
8790
|
+
"ledger",
|
|
8791
|
+
"audit",
|
|
8792
|
+
"mirror",
|
|
8793
|
+
"ephemeral"
|
|
8794
|
+
];
|
|
8795
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
8796
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8797
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
8798
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
8799
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8800
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8801
|
+
/**
|
|
8802
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8803
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8804
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
8805
|
+
*
|
|
8806
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8807
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8808
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8809
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
8810
|
+
*
|
|
8811
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8812
|
+
*/
|
|
8813
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8814
|
+
function parseSecretPath(path) {
|
|
8815
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8816
|
+
return path.split(".").map((raw) => {
|
|
8817
|
+
const each = raw.endsWith("[]");
|
|
8818
|
+
const key = each ? raw.slice(0, -2) : raw;
|
|
8819
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8820
|
+
return {
|
|
8821
|
+
key,
|
|
8822
|
+
each
|
|
8823
|
+
};
|
|
8824
|
+
});
|
|
8825
|
+
}
|
|
8826
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8827
|
+
function secretArrayPrefixes(path) {
|
|
8828
|
+
const segments = parseSecretPath(path);
|
|
8829
|
+
const prefixes = [];
|
|
8830
|
+
const walked = [];
|
|
8831
|
+
for (const segment of segments) {
|
|
8832
|
+
walked.push(segment.key);
|
|
8833
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
8834
|
+
}
|
|
8835
|
+
return prefixes;
|
|
8836
|
+
}
|
|
8837
|
+
/**
|
|
8838
|
+
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8839
|
+
* and validates them (`ShareViewGrantSchema` builds its
|
|
8840
|
+
* enum from this list); a data plane names the kinds it ACCEPTS in `share`.
|
|
8841
|
+
*/
|
|
8842
|
+
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
8843
|
+
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
8844
|
+
var PeerBytesTicketSchema = object({
|
|
8845
|
+
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
8846
|
+
url: string().min(1),
|
|
8847
|
+
/**
|
|
8848
|
+
* The HOST node this URL means something on — the hub or a named agent,
|
|
8849
|
+
* never a runner. {@link AddonPeerBytes.open} compares it to its own and
|
|
8850
|
+
* refuses `cross-node` by name when they differ, without dialling.
|
|
8851
|
+
*/
|
|
8852
|
+
hostNodeId: string().min(1),
|
|
8853
|
+
expiresAtMs: number$1().int().nonnegative(),
|
|
8854
|
+
/**
|
|
8855
|
+
* What the producer DECLARED the body to be, when it knows — `null` when it
|
|
8856
|
+
* does not. Never `0` for unknown (D393): a consumer sizing a bound off this
|
|
8857
|
+
* must be able to tell "the producer did not say" from "the body is empty".
|
|
8858
|
+
*/
|
|
8859
|
+
declaredBytes: number$1().int().nonnegative().nullable()
|
|
8860
|
+
});
|
|
8784
8861
|
function defOf(schema) {
|
|
8785
8862
|
const zodBag = Reflect.get(schema, "_zod");
|
|
8786
8863
|
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
@@ -8849,42 +8926,6 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
8849
8926
|
return [...new Set(paths)];
|
|
8850
8927
|
}
|
|
8851
8928
|
/**
|
|
8852
|
-
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8853
|
-
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8854
|
-
* (`auth.accessToken`, `headers[].value`).
|
|
8855
|
-
*
|
|
8856
|
-
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8857
|
-
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8858
|
-
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8859
|
-
* leaf names its element: `headers[X-Api].value`.
|
|
8860
|
-
*
|
|
8861
|
-
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8862
|
-
*/
|
|
8863
|
-
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8864
|
-
function parseSecretPath(path) {
|
|
8865
|
-
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8866
|
-
return path.split(".").map((raw) => {
|
|
8867
|
-
const each = raw.endsWith("[]");
|
|
8868
|
-
const key = each ? raw.slice(0, -2) : raw;
|
|
8869
|
-
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8870
|
-
return {
|
|
8871
|
-
key,
|
|
8872
|
-
each
|
|
8873
|
-
};
|
|
8874
|
-
});
|
|
8875
|
-
}
|
|
8876
|
-
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8877
|
-
function secretArrayPrefixes(path) {
|
|
8878
|
-
const segments = parseSecretPath(path);
|
|
8879
|
-
const prefixes = [];
|
|
8880
|
-
const walked = [];
|
|
8881
|
-
for (const segment of segments) {
|
|
8882
|
-
walked.push(segment.key);
|
|
8883
|
-
if (segment.each) prefixes.push(walked.join("."));
|
|
8884
|
-
}
|
|
8885
|
-
return prefixes;
|
|
8886
|
-
}
|
|
8887
|
-
/**
|
|
8888
8929
|
* Record kinds (D881): the declaration an addon makes of one kind of fact, the
|
|
8889
8930
|
* Zod row schema that describes it, and the helper that turns the two into the
|
|
8890
8931
|
* JSON the engine stores.
|
|
@@ -9724,6 +9765,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9724
9765
|
"backup",
|
|
9725
9766
|
"battery",
|
|
9726
9767
|
"binary",
|
|
9768
|
+
"board-key-source",
|
|
9727
9769
|
"brightness",
|
|
9728
9770
|
"broker",
|
|
9729
9771
|
"button",
|
|
@@ -9766,6 +9808,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9766
9808
|
"device-status",
|
|
9767
9809
|
"dishwasher",
|
|
9768
9810
|
"display-palette",
|
|
9811
|
+
"do-not-disturb",
|
|
9769
9812
|
"doorbell",
|
|
9770
9813
|
"embedding-encoder",
|
|
9771
9814
|
"enum-sensor",
|
|
@@ -9786,6 +9829,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9786
9829
|
"humidity-sensor",
|
|
9787
9830
|
"image",
|
|
9788
9831
|
"image-settings",
|
|
9832
|
+
"integration-instances",
|
|
9789
9833
|
"integrations",
|
|
9790
9834
|
"intercom",
|
|
9791
9835
|
"ir-transmitter",
|
|
@@ -9843,6 +9887,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9843
9887
|
"privacy-mask",
|
|
9844
9888
|
"ptz",
|
|
9845
9889
|
"ptz-autotrack",
|
|
9890
|
+
"pump-cycle",
|
|
9846
9891
|
"reboot",
|
|
9847
9892
|
"record-entries",
|
|
9848
9893
|
"recording",
|
|
@@ -10525,6 +10570,10 @@ Object.freeze({
|
|
|
10525
10570
|
"valueType": "string"
|
|
10526
10571
|
}
|
|
10527
10572
|
],
|
|
10573
|
+
"do-not-disturb": [{
|
|
10574
|
+
"path": "enabled",
|
|
10575
|
+
"valueType": "boolean"
|
|
10576
|
+
}],
|
|
10528
10577
|
"doorbell": [{
|
|
10529
10578
|
"path": "lastPressedAt",
|
|
10530
10579
|
"valueType": "number"
|
|
@@ -11131,18 +11180,6 @@ Object.freeze({
|
|
|
11131
11180
|
"path": "pumpRunning",
|
|
11132
11181
|
"valueType": "boolean"
|
|
11133
11182
|
},
|
|
11134
|
-
{
|
|
11135
|
-
"path": "pumpIntervalEnabled",
|
|
11136
|
-
"valueType": "boolean"
|
|
11137
|
-
},
|
|
11138
|
-
{
|
|
11139
|
-
"path": "pumpOnTime",
|
|
11140
|
-
"valueType": "number"
|
|
11141
|
-
},
|
|
11142
|
-
{
|
|
11143
|
-
"path": "pumpOffTime",
|
|
11144
|
-
"valueType": "number"
|
|
11145
|
-
},
|
|
11146
11183
|
{
|
|
11147
11184
|
"path": "lastDrink",
|
|
11148
11185
|
"valueType": "number"
|
|
@@ -11266,6 +11303,20 @@ Object.freeze({
|
|
|
11266
11303
|
"valueType": "number"
|
|
11267
11304
|
}
|
|
11268
11305
|
],
|
|
11306
|
+
"pump-cycle": [
|
|
11307
|
+
{
|
|
11308
|
+
"path": "enabled",
|
|
11309
|
+
"valueType": "boolean"
|
|
11310
|
+
},
|
|
11311
|
+
{
|
|
11312
|
+
"path": "onTime",
|
|
11313
|
+
"valueType": "number"
|
|
11314
|
+
},
|
|
11315
|
+
{
|
|
11316
|
+
"path": "offTime",
|
|
11317
|
+
"valueType": "number"
|
|
11318
|
+
}
|
|
11319
|
+
],
|
|
11269
11320
|
"recording-archive": [
|
|
11270
11321
|
{
|
|
11271
11322
|
"path": "deviceId",
|
|
@@ -11715,7 +11766,7 @@ object({
|
|
|
11715
11766
|
params: record(string(), union([string(), number$1()])).optional()
|
|
11716
11767
|
});
|
|
11717
11768
|
//#endregion
|
|
11718
|
-
//#region ../types/dist/composition-
|
|
11769
|
+
//#region ../types/dist/composition-Dh3RfMx5.mjs
|
|
11719
11770
|
/**
|
|
11720
11771
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11721
11772
|
*
|
|
@@ -12929,6 +12980,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12929
12980
|
"node-g",
|
|
12930
12981
|
"node-h"
|
|
12931
12982
|
]);
|
|
12983
|
+
//#endregion
|
|
12984
|
+
//#region ../types/dist/index.mjs
|
|
12985
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
12986
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
12987
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
12988
|
+
/** The text excerpt a message carries on the bus. */
|
|
12989
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
12990
|
+
var PortSchema = number$1().int().min(1).max(65535);
|
|
12991
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
12992
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
12993
|
+
object({
|
|
12994
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
12995
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
12996
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
12997
|
+
requireAuth: boolean().default(true),
|
|
12998
|
+
username: string().default(""),
|
|
12999
|
+
password: string().default("").meta({ secret: true }),
|
|
13000
|
+
starttls: boolean().default(false),
|
|
13001
|
+
maxMessageBytes: number$1().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
13002
|
+
});
|
|
13003
|
+
object({
|
|
13004
|
+
host: string().min(1).nullable(),
|
|
13005
|
+
hostReason: string().nullable(),
|
|
13006
|
+
port: PortSchema,
|
|
13007
|
+
domain: string().min(1),
|
|
13008
|
+
requireAuth: boolean(),
|
|
13009
|
+
username: string(),
|
|
13010
|
+
password: string(),
|
|
13011
|
+
starttls: boolean()
|
|
13012
|
+
});
|
|
13013
|
+
object({
|
|
13014
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13015
|
+
/**
|
|
13016
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13017
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13018
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13019
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13020
|
+
*/
|
|
13021
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13022
|
+
});
|
|
13023
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13024
|
+
filename: string(),
|
|
13025
|
+
contentType: string(),
|
|
13026
|
+
bytes: number$1().int().nonnegative()
|
|
13027
|
+
});
|
|
13028
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13029
|
+
var SmtpInboundMessageSchema = object({
|
|
13030
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13031
|
+
seq: number$1().int().positive(),
|
|
13032
|
+
messageId: string(),
|
|
13033
|
+
receivedAt: number$1().int().nonnegative(),
|
|
13034
|
+
from: string(),
|
|
13035
|
+
to: string(),
|
|
13036
|
+
subject: string(),
|
|
13037
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13038
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13039
|
+
});
|
|
13040
|
+
object({
|
|
13041
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13042
|
+
lastSeq: number$1().int().nonnegative(),
|
|
13043
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13044
|
+
oldestSeq: number$1().int().nonnegative(),
|
|
13045
|
+
messages: array(object({
|
|
13046
|
+
key: string(),
|
|
13047
|
+
message: SmtpInboundMessageSchema
|
|
13048
|
+
}))
|
|
13049
|
+
});
|
|
12932
13050
|
new Set([
|
|
12933
13051
|
"track",
|
|
12934
13052
|
"face",
|
|
@@ -16088,8 +16206,18 @@ var LogChannelDescriptorSchema = object({
|
|
|
16088
16206
|
* the operator narrows to one camera, sees nothing, and concludes the code
|
|
16089
16207
|
* path was never taken.
|
|
16090
16208
|
*/
|
|
16091
|
-
perDevice: boolean()
|
|
16092
|
-
|
|
16209
|
+
perDevice: boolean(),
|
|
16210
|
+
/**
|
|
16211
|
+
* What the channel is FOR, so the UI can list the channels an addon offers.
|
|
16212
|
+
*
|
|
16213
|
+
* - `diagnostic` (the default): a per-component diagnostic armed from the
|
|
16214
|
+
* Levels page, emitted at its declared level.
|
|
16215
|
+
* - `debug`: a `debug.<channel>` dump watched live for a short window
|
|
16216
|
+
* (`ctx.debug`). Always per-device, and its lines are denied by the log
|
|
16217
|
+
* ring unless somebody is watching that device.
|
|
16218
|
+
*/
|
|
16219
|
+
kind: _enum(["diagnostic", "debug"]).default("diagnostic")
|
|
16220
|
+
}).refine((d) => d.kind !== "debug" || d.name.startsWith("debug.") && d.perDevice, { message: "a debug channel is named debug.<channel> and is per-device" });
|
|
16093
16221
|
/**
|
|
16094
16222
|
* An armed window over one channel, as the document hands it to a mirror.
|
|
16095
16223
|
*
|
|
@@ -19308,24 +19436,52 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19308
19436
|
args: { on: false }
|
|
19309
19437
|
},
|
|
19310
19438
|
{
|
|
19311
|
-
id: "petFountain.
|
|
19312
|
-
method: "
|
|
19439
|
+
id: "petFountain.resetDailyConsumption",
|
|
19440
|
+
method: "resetDailyConsumption",
|
|
19441
|
+
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19442
|
+
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19443
|
+
icon: "rotate-ccw"
|
|
19444
|
+
}
|
|
19445
|
+
],
|
|
19446
|
+
state: [{
|
|
19447
|
+
kind: "boolean",
|
|
19448
|
+
field: "empty",
|
|
19449
|
+
label: t("Tank empty", "Serbatoio vuoto"),
|
|
19450
|
+
whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
|
|
19451
|
+
whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
|
|
19452
|
+
icon: "droplet"
|
|
19453
|
+
}, {
|
|
19454
|
+
kind: "numeric",
|
|
19455
|
+
field: "waterLevel",
|
|
19456
|
+
quantity: t("the water level", "il livello dell’acqua"),
|
|
19457
|
+
label: t("Water level", "Livello dell’acqua"),
|
|
19458
|
+
unit: "%",
|
|
19459
|
+
icon: "droplets"
|
|
19460
|
+
}]
|
|
19461
|
+
},
|
|
19462
|
+
"pump-cycle": {
|
|
19463
|
+
scope: "device",
|
|
19464
|
+
noun: t("Pump cycle", "Ciclo della pompa"),
|
|
19465
|
+
actions: [
|
|
19466
|
+
{
|
|
19467
|
+
id: "pumpCycle.on",
|
|
19468
|
+
method: "setEnabled",
|
|
19313
19469
|
label: t("Pump cycle on", "Ciclo pompa attivo"),
|
|
19314
19470
|
phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
|
|
19315
19471
|
icon: "timer",
|
|
19316
19472
|
args: { on: true }
|
|
19317
19473
|
},
|
|
19318
19474
|
{
|
|
19319
|
-
id: "
|
|
19320
|
-
method: "
|
|
19475
|
+
id: "pumpCycle.off",
|
|
19476
|
+
method: "setEnabled",
|
|
19321
19477
|
label: t("Pump cycle off", "Ciclo pompa spento"),
|
|
19322
19478
|
phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
|
|
19323
19479
|
icon: "timer-off",
|
|
19324
19480
|
args: { on: false }
|
|
19325
19481
|
},
|
|
19326
19482
|
{
|
|
19327
|
-
id: "
|
|
19328
|
-
method: "
|
|
19483
|
+
id: "pumpCycle.onMinutes",
|
|
19484
|
+
method: "setOnMinutes",
|
|
19329
19485
|
label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
|
|
19330
19486
|
phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
|
|
19331
19487
|
icon: "timer",
|
|
@@ -19341,8 +19497,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19341
19497
|
}]
|
|
19342
19498
|
},
|
|
19343
19499
|
{
|
|
19344
|
-
id: "
|
|
19345
|
-
method: "
|
|
19500
|
+
id: "pumpCycle.offMinutes",
|
|
19501
|
+
method: "setOffMinutes",
|
|
19346
19502
|
label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
|
|
19347
19503
|
phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
|
|
19348
19504
|
icon: "timer-off",
|
|
@@ -19356,29 +19512,15 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19356
19512
|
unit: "min"
|
|
19357
19513
|
}
|
|
19358
19514
|
}]
|
|
19359
|
-
},
|
|
19360
|
-
{
|
|
19361
|
-
id: "petFountain.resetDailyConsumption",
|
|
19362
|
-
method: "resetDailyConsumption",
|
|
19363
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19364
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19365
|
-
icon: "rotate-ccw"
|
|
19366
19515
|
}
|
|
19367
19516
|
],
|
|
19368
19517
|
state: [{
|
|
19369
19518
|
kind: "boolean",
|
|
19370
|
-
field: "
|
|
19371
|
-
label: t("
|
|
19372
|
-
whenTrue: t("When
|
|
19373
|
-
whenFalse: t("When
|
|
19374
|
-
icon: "
|
|
19375
|
-
}, {
|
|
19376
|
-
kind: "numeric",
|
|
19377
|
-
field: "waterLevel",
|
|
19378
|
-
quantity: t("the water level", "il livello dell’acqua"),
|
|
19379
|
-
label: t("Water level", "Livello dell’acqua"),
|
|
19380
|
-
unit: "%",
|
|
19381
|
-
icon: "droplets"
|
|
19519
|
+
field: "enabled",
|
|
19520
|
+
label: t("Pump cycle", "Ciclo pompa"),
|
|
19521
|
+
whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
|
|
19522
|
+
whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
|
|
19523
|
+
icon: "timer"
|
|
19382
19524
|
}]
|
|
19383
19525
|
},
|
|
19384
19526
|
"media-player": {
|
|
@@ -20064,6 +20206,33 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
20064
20206
|
}
|
|
20065
20207
|
]
|
|
20066
20208
|
},
|
|
20209
|
+
"do-not-disturb": {
|
|
20210
|
+
scope: "device",
|
|
20211
|
+
noun: t("Do not disturb", "Non disturbare"),
|
|
20212
|
+
actions: [{
|
|
20213
|
+
id: "dnd.on",
|
|
20214
|
+
method: "setEnabled",
|
|
20215
|
+
label: t("Do not disturb on", "Attiva Non disturbare"),
|
|
20216
|
+
phrase: t("Turn on Do not disturb on {device}", "Attiva Non disturbare su {device}"),
|
|
20217
|
+
icon: "bell-off",
|
|
20218
|
+
args: { enabled: true }
|
|
20219
|
+
}, {
|
|
20220
|
+
id: "dnd.off",
|
|
20221
|
+
method: "setEnabled",
|
|
20222
|
+
label: t("Do not disturb off", "Disattiva Non disturbare"),
|
|
20223
|
+
phrase: t("Turn off Do not disturb on {device}", "Disattiva Non disturbare su {device}"),
|
|
20224
|
+
icon: "bell",
|
|
20225
|
+
args: { enabled: false }
|
|
20226
|
+
}],
|
|
20227
|
+
state: [{
|
|
20228
|
+
kind: "boolean",
|
|
20229
|
+
field: "enabled",
|
|
20230
|
+
label: t("Do not disturb", "Non disturbare"),
|
|
20231
|
+
whenTrue: t("When Do not disturb turns on on {device}", "Quando {device} attiva Non disturbare"),
|
|
20232
|
+
whenFalse: t("When Do not disturb turns off on {device}", "Quando {device} disattiva Non disturbare"),
|
|
20233
|
+
icon: "bell-off"
|
|
20234
|
+
}]
|
|
20235
|
+
},
|
|
20067
20236
|
update: {
|
|
20068
20237
|
scope: "device",
|
|
20069
20238
|
noun: t("Firmware update", "Aggiornamento firmware"),
|
|
@@ -21244,6 +21413,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
21244
21413
|
"audio-analyzer": system(PIPELINE),
|
|
21245
21414
|
"audio-codec": system(INTERNAL),
|
|
21246
21415
|
"auth-provider": system(IDENTITY),
|
|
21416
|
+
"board-key-source": system(INTERNAL),
|
|
21247
21417
|
backup: system("system backups — run and restored from the Backups page"),
|
|
21248
21418
|
broker: system("message-broker connections — configured, not actuated"),
|
|
21249
21419
|
"calendar-source": system(INTERNAL),
|
|
@@ -21278,6 +21448,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
21278
21448
|
"load-contribution": system(INTERNAL),
|
|
21279
21449
|
"local-network": system(NETWORK),
|
|
21280
21450
|
"log-channels": system(INTERNAL),
|
|
21451
|
+
"integration-instances": system(INTERNAL),
|
|
21281
21452
|
"log-destination": system(INTERNAL),
|
|
21282
21453
|
"login-method": system(IDENTITY),
|
|
21283
21454
|
"mesh-network": system(NETWORK),
|
|
@@ -42180,14 +42351,16 @@ var DeviceIsLeafSchema = object({
|
|
|
42180
42351
|
});
|
|
42181
42352
|
/**
|
|
42182
42353
|
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
42183
|
-
* least one of `zones
|
|
42354
|
+
* least one of `zones` (`minCount` of them, default one). Reads the zone-analytics snapshot through the same
|
|
42184
42355
|
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
42185
42356
|
*/
|
|
42186
42357
|
var StationaryPresentLeafSchema = object({
|
|
42187
42358
|
kind: literal("stationary-present"),
|
|
42188
42359
|
deviceId: number$1().int().nonnegative(),
|
|
42189
42360
|
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
42190
|
-
zones: array(string().min(1)).min(1).optional()
|
|
42361
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
42362
|
+
/** How many matching parked objects must be there; absent means one. */
|
|
42363
|
+
minCount: number$1().int().positive().optional()
|
|
42191
42364
|
});
|
|
42192
42365
|
/**
|
|
42193
42366
|
* The escape hatch: the bounded expression engine over named device bindings.
|
|
@@ -42699,6 +42872,28 @@ function convertLegacyRecipe(legacy) {
|
|
|
42699
42872
|
* it. The output type is always the new one.
|
|
42700
42873
|
*/
|
|
42701
42874
|
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
42875
|
+
var DETECTION_DESCRIPTIONS = {
|
|
42876
|
+
deviceId: "the camera that saw it",
|
|
42877
|
+
className: "what it saw",
|
|
42878
|
+
trackId: "the track",
|
|
42879
|
+
score: "how sure the detector was",
|
|
42880
|
+
label: "the label it matched (a name, a plate), empty when none"
|
|
42881
|
+
};
|
|
42882
|
+
var STATIONARY_DESCRIPTIONS = {
|
|
42883
|
+
deviceId: "the camera that saw it",
|
|
42884
|
+
className: "what it saw",
|
|
42885
|
+
zones: "the zones it is in",
|
|
42886
|
+
phase: "appeared or departed",
|
|
42887
|
+
entryId: "the stopped object"
|
|
42888
|
+
};
|
|
42889
|
+
function describe$1(descriptions) {
|
|
42890
|
+
return Object.entries(descriptions).map(([name, description]) => ({
|
|
42891
|
+
name,
|
|
42892
|
+
description
|
|
42893
|
+
}));
|
|
42894
|
+
}
|
|
42895
|
+
describe$1(DETECTION_DESCRIPTIONS);
|
|
42896
|
+
describe$1(STATIONARY_DESCRIPTIONS);
|
|
42702
42897
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
42703
42898
|
/**
|
|
42704
42899
|
* Schedulers (D983) — an entity that performs actions over the day.
|
|
@@ -43460,7 +43655,7 @@ var TargetKindCapsSchema = object({
|
|
|
43460
43655
|
* the union is large and not meant for runtime validation here; the exported
|
|
43461
43656
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
43462
43657
|
*/
|
|
43463
|
-
var ConfigSchemaPassthrough$
|
|
43658
|
+
var ConfigSchemaPassthrough$4 = unknown();
|
|
43464
43659
|
var TargetKindSchema = object({
|
|
43465
43660
|
kind: string(),
|
|
43466
43661
|
label: string(),
|
|
@@ -43497,7 +43692,7 @@ var TargetKindSchema = object({
|
|
|
43497
43692
|
* its raster path, which is the safe default for an unknown image.
|
|
43498
43693
|
*/
|
|
43499
43694
|
iconMediaType: string().optional(),
|
|
43500
|
-
configSchema: ConfigSchemaPassthrough$
|
|
43695
|
+
configSchema: ConfigSchemaPassthrough$4,
|
|
43501
43696
|
/**
|
|
43502
43697
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
43503
43698
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -46169,6 +46364,49 @@ var binaryCapability = {
|
|
|
46169
46364
|
volatileStateFields: ["lastChangedAt"]
|
|
46170
46365
|
};
|
|
46171
46366
|
/**
|
|
46367
|
+
* Why a board key could not be read. Typed so a caller reports the cause and
|
|
46368
|
+
* never treats "could not read" as "has no key" (D49).
|
|
46369
|
+
*/
|
|
46370
|
+
var BoardKeyFailureReasonSchema = _enum([
|
|
46371
|
+
"unknown-source",
|
|
46372
|
+
"not-admin",
|
|
46373
|
+
"no-builder",
|
|
46374
|
+
"no-ingress-url",
|
|
46375
|
+
"unreachable",
|
|
46376
|
+
"unknown-board",
|
|
46377
|
+
"no-encryption-key",
|
|
46378
|
+
"unreadable-config"
|
|
46379
|
+
]);
|
|
46380
|
+
var BoardKeySourceSchema = object({
|
|
46381
|
+
sourceId: string(),
|
|
46382
|
+
name: string()
|
|
46383
|
+
});
|
|
46384
|
+
var BoardKeyBoardSchema = object({
|
|
46385
|
+
/** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
|
|
46386
|
+
configuration: string(),
|
|
46387
|
+
/** The node name the builder gives the board. */
|
|
46388
|
+
name: string(),
|
|
46389
|
+
/** The address the builder knows the board by, when it has one. */
|
|
46390
|
+
address: string().nullable()
|
|
46391
|
+
});
|
|
46392
|
+
var BoardKeyFailureSchema = object({
|
|
46393
|
+
ok: literal(false),
|
|
46394
|
+
reason: BoardKeyFailureReasonSchema,
|
|
46395
|
+
detail: string().optional()
|
|
46396
|
+
});
|
|
46397
|
+
var ListBoardsResultSchema = discriminatedUnion("ok", [object({
|
|
46398
|
+
ok: literal(true),
|
|
46399
|
+
boards: array(BoardKeyBoardSchema).readonly()
|
|
46400
|
+
}), BoardKeyFailureSchema]);
|
|
46401
|
+
var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
|
|
46402
|
+
ok: literal(true),
|
|
46403
|
+
key: string()
|
|
46404
|
+
}), BoardKeyFailureSchema]);
|
|
46405
|
+
method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
|
|
46406
|
+
sourceId: string().min(1),
|
|
46407
|
+
configuration: string().min(1)
|
|
46408
|
+
}), GetBoardKeyResultSchema, { auth: "admin" });
|
|
46409
|
+
/**
|
|
46172
46410
|
* Dimmable-light brightness control. Co-exists with `switch` on the
|
|
46173
46411
|
* same device — the switch toggles on/off, this cap sets the level
|
|
46174
46412
|
* applied when the light is on. Drivers map their per-vendor dim
|
|
@@ -51202,6 +51440,45 @@ var deviceProviderCapability = {
|
|
|
51202
51440
|
}
|
|
51203
51441
|
};
|
|
51204
51442
|
/**
|
|
51443
|
+
* What the passive listener of `local-network` saw on the wire, before any
|
|
51444
|
+
* provider classified it. Device-manager turns an observation into the hits
|
|
51445
|
+
* of the providers whose declared discovery modes it matches.
|
|
51446
|
+
*/
|
|
51447
|
+
var DiscoveryObservationSchema = discriminatedUnion("kind", [object({
|
|
51448
|
+
kind: literal("mdns"),
|
|
51449
|
+
/** Normalised DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
51450
|
+
serviceType: string(),
|
|
51451
|
+
instance: string(),
|
|
51452
|
+
host: string(),
|
|
51453
|
+
/** The A record address when the announcement carried one. */
|
|
51454
|
+
address: string().nullable(),
|
|
51455
|
+
port: number$1().int(),
|
|
51456
|
+
txt: record(string(), string())
|
|
51457
|
+
}), object({
|
|
51458
|
+
kind: literal("ssdp"),
|
|
51459
|
+
/** Sender address of the datagram. */
|
|
51460
|
+
host: string(),
|
|
51461
|
+
port: number$1().int(),
|
|
51462
|
+
/** `NT` of a NOTIFY, `ST` of a search response. */
|
|
51463
|
+
target: string(),
|
|
51464
|
+
headers: record(string(), string())
|
|
51465
|
+
})]);
|
|
51466
|
+
object({
|
|
51467
|
+
addonId: string(),
|
|
51468
|
+
candidate: DiscoveryCandidateSchema,
|
|
51469
|
+
firstSeenAt: number$1(),
|
|
51470
|
+
lastSeenAt: number$1(),
|
|
51471
|
+
dismissed: boolean()
|
|
51472
|
+
});
|
|
51473
|
+
var DiscoveryProposalSchema = object({
|
|
51474
|
+
id: string(),
|
|
51475
|
+
addonId: string(),
|
|
51476
|
+
candidate: DiscoveryCandidateSchema,
|
|
51477
|
+
firstSeenAt: number$1(),
|
|
51478
|
+
lastSeenAt: number$1(),
|
|
51479
|
+
dismissed: boolean()
|
|
51480
|
+
});
|
|
51481
|
+
/**
|
|
51205
51482
|
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
51206
51483
|
* live registry access, and all management operations into a single tRPC surface.
|
|
51207
51484
|
*
|
|
@@ -51930,6 +52207,19 @@ method(object({
|
|
|
51930
52207
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
51931
52208
|
kind: "mutation",
|
|
51932
52209
|
auth: "admin"
|
|
52210
|
+
}), method(object({}), object({ proposals: array(DiscoveryProposalSchema).readonly() }), { auth: "admin" }), method(object({ id: string() }), object({ dismissed: boolean() }), {
|
|
52211
|
+
kind: "mutation",
|
|
52212
|
+
auth: "admin"
|
|
52213
|
+
}), method(object({ observations: array(DiscoveryObservationSchema).max(512) }), object({ proposed: number$1().int() }), {
|
|
52214
|
+
kind: "mutation",
|
|
52215
|
+
auth: "admin"
|
|
52216
|
+
}), method(object({}), object({
|
|
52217
|
+
providers: number$1().int(),
|
|
52218
|
+
proposed: number$1().int()
|
|
52219
|
+
}), {
|
|
52220
|
+
kind: "mutation",
|
|
52221
|
+
auth: "admin",
|
|
52222
|
+
timeoutMs: 12e4
|
|
51933
52223
|
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
51934
52224
|
deviceId: number$1(),
|
|
51935
52225
|
key: string(),
|
|
@@ -52786,7 +53076,7 @@ var DishwasherStatusSchema = object({
|
|
|
52786
53076
|
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
52787
53077
|
lastFetchedAt: number$1()
|
|
52788
53078
|
});
|
|
52789
|
-
var DeviceIdSchema$
|
|
53079
|
+
var DeviceIdSchema$2 = number$1().int().nonnegative();
|
|
52790
53080
|
/** A value a program option or setting can take. */
|
|
52791
53081
|
var DishwasherValueSchema = union([
|
|
52792
53082
|
boolean(),
|
|
@@ -52861,77 +53151,77 @@ var dishwasherCapability = {
|
|
|
52861
53151
|
* option feature keys; each is checked against the program's constraints.
|
|
52862
53152
|
*/
|
|
52863
53153
|
start: method(object({
|
|
52864
|
-
deviceId: DeviceIdSchema$
|
|
53154
|
+
deviceId: DeviceIdSchema$2,
|
|
52865
53155
|
program: string().min(1).optional(),
|
|
52866
53156
|
options: record(string(), DishwasherValueSchema).optional()
|
|
52867
53157
|
}), _void(), {
|
|
52868
53158
|
kind: "mutation",
|
|
52869
53159
|
auth: "protected"
|
|
52870
53160
|
}),
|
|
52871
|
-
pause: method(object({ deviceId: DeviceIdSchema$
|
|
53161
|
+
pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52872
53162
|
kind: "mutation",
|
|
52873
53163
|
auth: "protected"
|
|
52874
53164
|
}),
|
|
52875
|
-
resume: method(object({ deviceId: DeviceIdSchema$
|
|
53165
|
+
resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52876
53166
|
kind: "mutation",
|
|
52877
53167
|
auth: "protected"
|
|
52878
53168
|
}),
|
|
52879
|
-
stop: method(object({ deviceId: DeviceIdSchema$
|
|
53169
|
+
stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52880
53170
|
kind: "mutation",
|
|
52881
53171
|
auth: "protected"
|
|
52882
53172
|
}),
|
|
52883
53173
|
/** Select (not start) a program; refused when the appliance does not offer it. */
|
|
52884
53174
|
setProgram: method(object({
|
|
52885
|
-
deviceId: DeviceIdSchema$
|
|
53175
|
+
deviceId: DeviceIdSchema$2,
|
|
52886
53176
|
program: string().min(1)
|
|
52887
53177
|
}), _void(), {
|
|
52888
53178
|
kind: "mutation",
|
|
52889
53179
|
auth: "protected"
|
|
52890
53180
|
}),
|
|
52891
53181
|
setPower: method(object({
|
|
52892
|
-
deviceId: DeviceIdSchema$
|
|
53182
|
+
deviceId: DeviceIdSchema$2,
|
|
52893
53183
|
on: boolean()
|
|
52894
53184
|
}), _void(), {
|
|
52895
53185
|
kind: "mutation",
|
|
52896
53186
|
auth: "protected"
|
|
52897
53187
|
}),
|
|
52898
53188
|
setHalfLoad: method(object({
|
|
52899
|
-
deviceId: DeviceIdSchema$
|
|
53189
|
+
deviceId: DeviceIdSchema$2,
|
|
52900
53190
|
on: boolean()
|
|
52901
53191
|
}), _void(), {
|
|
52902
53192
|
kind: "mutation",
|
|
52903
53193
|
auth: "protected"
|
|
52904
53194
|
}),
|
|
52905
53195
|
setHygienePlus: method(object({
|
|
52906
|
-
deviceId: DeviceIdSchema$
|
|
53196
|
+
deviceId: DeviceIdSchema$2,
|
|
52907
53197
|
on: boolean()
|
|
52908
53198
|
}), _void(), {
|
|
52909
53199
|
kind: "mutation",
|
|
52910
53200
|
auth: "protected"
|
|
52911
53201
|
}),
|
|
52912
53202
|
setExtraDry: method(object({
|
|
52913
|
-
deviceId: DeviceIdSchema$
|
|
53203
|
+
deviceId: DeviceIdSchema$2,
|
|
52914
53204
|
on: boolean()
|
|
52915
53205
|
}), _void(), {
|
|
52916
53206
|
kind: "mutation",
|
|
52917
53207
|
auth: "protected"
|
|
52918
53208
|
}),
|
|
52919
53209
|
setIntensiveZone: method(object({
|
|
52920
|
-
deviceId: DeviceIdSchema$
|
|
53210
|
+
deviceId: DeviceIdSchema$2,
|
|
52921
53211
|
on: boolean()
|
|
52922
53212
|
}), _void(), {
|
|
52923
53213
|
kind: "mutation",
|
|
52924
53214
|
auth: "protected"
|
|
52925
53215
|
}),
|
|
52926
53216
|
setVarioSpeedPlus: method(object({
|
|
52927
|
-
deviceId: DeviceIdSchema$
|
|
53217
|
+
deviceId: DeviceIdSchema$2,
|
|
52928
53218
|
on: boolean()
|
|
52929
53219
|
}), _void(), {
|
|
52930
53220
|
kind: "mutation",
|
|
52931
53221
|
auth: "protected"
|
|
52932
53222
|
}),
|
|
52933
53223
|
setChildLock: method(object({
|
|
52934
|
-
deviceId: DeviceIdSchema$
|
|
53224
|
+
deviceId: DeviceIdSchema$2,
|
|
52935
53225
|
on: boolean()
|
|
52936
53226
|
}), _void(), {
|
|
52937
53227
|
kind: "mutation",
|
|
@@ -52943,7 +53233,7 @@ var dishwasherCapability = {
|
|
|
52943
53233
|
* its declared range / members.
|
|
52944
53234
|
*/
|
|
52945
53235
|
setOption: method(object({
|
|
52946
|
-
deviceId: DeviceIdSchema$
|
|
53236
|
+
deviceId: DeviceIdSchema$2,
|
|
52947
53237
|
key: string().min(1),
|
|
52948
53238
|
value: DishwasherValueSchema
|
|
52949
53239
|
}), _void(), {
|
|
@@ -52951,7 +53241,7 @@ var dishwasherCapability = {
|
|
|
52951
53241
|
auth: "protected"
|
|
52952
53242
|
}),
|
|
52953
53243
|
/** What this appliance can do, from its own description — the UI is derived from it. */
|
|
52954
|
-
getOptions: method(object({ deviceId: DeviceIdSchema$
|
|
53244
|
+
getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
|
|
52955
53245
|
},
|
|
52956
53246
|
status: {
|
|
52957
53247
|
schema: DishwasherStatusSchema,
|
|
@@ -53379,6 +53669,49 @@ method(_void(), paletteResultSchema, {
|
|
|
53379
53669
|
kind: "mutation",
|
|
53380
53670
|
auth: "admin"
|
|
53381
53671
|
});
|
|
53672
|
+
/**
|
|
53673
|
+
* do-not-disturb - whether a speaker or display holds back its own sounds
|
|
53674
|
+
* (announcements, notifications, alarms) until the operator lets it speak again.
|
|
53675
|
+
*
|
|
53676
|
+
* It is NOT part of `media-player`: a player is about what it plays and how
|
|
53677
|
+
* loud, while Do Not Disturb is a household switch the vendor keeps per device.
|
|
53678
|
+
* `enabled` is null when the vendor has not said (never a guess of `false`).
|
|
53679
|
+
*/
|
|
53680
|
+
var DoNotDisturbStatusSchema = object({
|
|
53681
|
+
/** True while Do Not Disturb is on; null when unknown. */
|
|
53682
|
+
enabled: boolean().nullable().meta({ label: "Do not disturb" }),
|
|
53683
|
+
/** Ms epoch of the last read or write that set `enabled`. */
|
|
53684
|
+
lastChangedAt: number$1()
|
|
53685
|
+
});
|
|
53686
|
+
var doNotDisturbCapability = {
|
|
53687
|
+
name: "do-not-disturb",
|
|
53688
|
+
scope: "device",
|
|
53689
|
+
deviceNative: true,
|
|
53690
|
+
mode: "singleton",
|
|
53691
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
53692
|
+
methods: { setEnabled: method(object({
|
|
53693
|
+
deviceId: number$1().int(),
|
|
53694
|
+
enabled: boolean()
|
|
53695
|
+
}), _void(), {
|
|
53696
|
+
kind: "mutation",
|
|
53697
|
+
auth: "protected"
|
|
53698
|
+
}) },
|
|
53699
|
+
status: {
|
|
53700
|
+
schema: DoNotDisturbStatusSchema,
|
|
53701
|
+
kind: "push"
|
|
53702
|
+
},
|
|
53703
|
+
runtimeState: DoNotDisturbStatusSchema,
|
|
53704
|
+
/**
|
|
53705
|
+
* Runtime-state durability: **session** - the vendor owns the switch and the
|
|
53706
|
+
* device re-reads it on the next poll, so a restored value would only be stale.
|
|
53707
|
+
*
|
|
53708
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
53709
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
53710
|
+
*/
|
|
53711
|
+
durability: "session",
|
|
53712
|
+
/** History (D850): when Do Not Disturb was switched on and off. */
|
|
53713
|
+
history: { fields: { enabled: { kind: "boolean" } } }
|
|
53714
|
+
};
|
|
53382
53715
|
/** Pulse override: a rising edge advances `lastPressedAt` and the press count. */
|
|
53383
53716
|
var doorbellCustomize = {
|
|
53384
53717
|
version: 1,
|
|
@@ -55231,6 +55564,89 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
55231
55564
|
auth: "admin"
|
|
55232
55565
|
});
|
|
55233
55566
|
/**
|
|
55567
|
+
* `integration-instances` - the connection of an addon that holds ONE or SEVERAL
|
|
55568
|
+
* (an account, a relay, a destination) as typed records (D985, D986). The addon's
|
|
55569
|
+
* own function page reads and writes them (Logging for Loki); the Integrations
|
|
55570
|
+
* page lists only the integrations that bring devices (D996).
|
|
55571
|
+
*
|
|
55572
|
+
* A collection of providers: each addon that holds connections registers one and a
|
|
55573
|
+
* caller reaches a provider with the pinned `addonId`.
|
|
55574
|
+
*
|
|
55575
|
+
* Identity: the instance id is immutable and opaque to the caller. A record the
|
|
55576
|
+
* Loki migration made keeps the `integrations` registry id it was created under
|
|
55577
|
+
* (`int_NNNN`); a single instance made by its function page takes the addon id.
|
|
55578
|
+
* `name` is the editable label.
|
|
55579
|
+
*
|
|
55580
|
+
* Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
|
|
55581
|
+
* the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
|
|
55582
|
+
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
55583
|
+
* clears it.
|
|
55584
|
+
*/
|
|
55585
|
+
var ConfigSchemaPassthrough$3 = custom();
|
|
55586
|
+
var IntegrationInstanceIdSchema = string().min(1);
|
|
55587
|
+
var IntegrationInstanceDescriptionSchema = object({
|
|
55588
|
+
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
55589
|
+
instanceMode: _enum(["single", "multiple"]),
|
|
55590
|
+
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
55591
|
+
kind: string(),
|
|
55592
|
+
/** True when `testInstance` really probes the remote. */
|
|
55593
|
+
canTest: boolean()
|
|
55594
|
+
});
|
|
55595
|
+
var IntegrationInstanceStatusSchema = object({
|
|
55596
|
+
state: _enum([
|
|
55597
|
+
"ok",
|
|
55598
|
+
"off",
|
|
55599
|
+
"error",
|
|
55600
|
+
"unknown"
|
|
55601
|
+
]),
|
|
55602
|
+
detail: string().optional()
|
|
55603
|
+
});
|
|
55604
|
+
var IntegrationInstanceSchema = object({
|
|
55605
|
+
id: IntegrationInstanceIdSchema,
|
|
55606
|
+
name: string(),
|
|
55607
|
+
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
55608
|
+
configured: boolean(),
|
|
55609
|
+
/**
|
|
55610
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
55611
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
55612
|
+
*/
|
|
55613
|
+
enabled: boolean(),
|
|
55614
|
+
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55615
|
+
formValues: record(string(), unknown()),
|
|
55616
|
+
status: IntegrationInstanceStatusSchema.optional()
|
|
55617
|
+
});
|
|
55618
|
+
var IntegrationInstancePatchSchema = object({
|
|
55619
|
+
id: IntegrationInstanceIdSchema,
|
|
55620
|
+
name: string().min(1).optional(),
|
|
55621
|
+
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
55622
|
+
values: record(string(), unknown())
|
|
55623
|
+
});
|
|
55624
|
+
method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
|
|
55625
|
+
id: IntegrationInstanceIdSchema,
|
|
55626
|
+
name: string().min(1)
|
|
55627
|
+
}), object({ id: string() }), {
|
|
55628
|
+
kind: "mutation",
|
|
55629
|
+
auth: "admin"
|
|
55630
|
+
}), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
55631
|
+
kind: "mutation",
|
|
55632
|
+
auth: "admin"
|
|
55633
|
+
}), method(object({
|
|
55634
|
+
id: IntegrationInstanceIdSchema,
|
|
55635
|
+
enabled: boolean()
|
|
55636
|
+
}), object({ ok: literal(true) }), {
|
|
55637
|
+
kind: "mutation",
|
|
55638
|
+
auth: "admin"
|
|
55639
|
+
}), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
55640
|
+
kind: "mutation",
|
|
55641
|
+
auth: "admin"
|
|
55642
|
+
}), optionalMethod(object({
|
|
55643
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
55644
|
+
values: record(string(), unknown())
|
|
55645
|
+
}), TestConnectionResultSchema$1, {
|
|
55646
|
+
kind: "mutation",
|
|
55647
|
+
auth: "admin"
|
|
55648
|
+
});
|
|
55649
|
+
/**
|
|
55234
55650
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
55235
55651
|
* session (server-initiated offer pattern, identical to the main
|
|
55236
55652
|
* video WebRTC flow) and the browser captures the mic; the server
|
|
@@ -55970,6 +56386,14 @@ var LlmGenerateBaseInputSchema = object({
|
|
|
55970
56386
|
topP: number$1().min(0).max(1).optional(),
|
|
55971
56387
|
/** Per-call override of the profile default (top-k sampling). */
|
|
55972
56388
|
topK: number$1().int().positive().optional(),
|
|
56389
|
+
/** Per-call override of `profile.reasoningEffort`. Sent only to a kind whose
|
|
56390
|
+
* descriptor offers the field; a kind that does not never sends it. */
|
|
56391
|
+
reasoningEffort: _enum([
|
|
56392
|
+
"off",
|
|
56393
|
+
"low",
|
|
56394
|
+
"medium",
|
|
56395
|
+
"high"
|
|
56396
|
+
]).optional(),
|
|
55973
56397
|
/** Per-call override of `profile.timeoutMs` — the total generation bound. */
|
|
55974
56398
|
timeoutMs: number$1().int().positive().optional(),
|
|
55975
56399
|
/** Per-call override; beats both the consumer table and the profile. */
|
|
@@ -56261,7 +56685,10 @@ var LlmProfileKindSchema = string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "a kind i
|
|
|
56261
56685
|
var LlmModelFeaturesSchema = object({
|
|
56262
56686
|
vision: boolean(),
|
|
56263
56687
|
tools: boolean(),
|
|
56264
|
-
streaming: boolean()
|
|
56688
|
+
streaming: boolean(),
|
|
56689
|
+
/** Whether the model thinks before it answers. `null` = the endpoint does not
|
|
56690
|
+
* say; an editor then still offers the switch (unknown is not "no"). */
|
|
56691
|
+
reasoning: boolean().nullable().optional()
|
|
56265
56692
|
});
|
|
56266
56693
|
/** One model a provider offers. `features` overrides the kind's defaults, per field. */
|
|
56267
56694
|
var LlmModelInfoSchema = object({
|
|
@@ -65987,7 +66414,14 @@ var NcMediaPolicySchema = object({
|
|
|
65987
66414
|
*/
|
|
65988
66415
|
profile: CamProfileSchema.optional(),
|
|
65989
66416
|
/** See {@link NcLinkedCameraModeSchema}. */
|
|
65990
|
-
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
66417
|
+
linkedCameras: NcLinkedCameraModeSchema.optional(),
|
|
66418
|
+
/**
|
|
66419
|
+
* Sensor rules only: may this rule wake a SLEEPING battery camera to take
|
|
66420
|
+
* the linked still it attaches (D998)? Absent means yes, an explicit `false`
|
|
66421
|
+
* opts out. Deliberately not a Zod default: a rule that never chose keeps
|
|
66422
|
+
* following the hub's. One bounded wake per event; nothing is attached later.
|
|
66423
|
+
*/
|
|
66424
|
+
wakeSleepingCamera: boolean().optional()
|
|
65991
66425
|
});
|
|
65992
66426
|
/**
|
|
65993
66427
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -68130,7 +68564,7 @@ var petFeederCapability = {
|
|
|
68130
68564
|
/**
|
|
68131
68565
|
* Pet water-fountain cap. Models the telemetry and the control surface of a
|
|
68132
68566
|
* circulating drinking fountain: how much water is in the tank, how much the
|
|
68133
|
-
* pet drank, whether the pump runs
|
|
68567
|
+
* pet drank, whether the pump runs.
|
|
68134
68568
|
*
|
|
68135
68569
|
* It has no native vendor provider of its own: a fountain reaches CamStack as
|
|
68136
68570
|
* a container of loose entities (a pump switch, two number boxes, a "filter
|
|
@@ -68146,8 +68580,10 @@ var petFeederCapability = {
|
|
|
68146
68580
|
* never here. Every reading may be `null`: a source that is offline or does
|
|
68147
68581
|
* not report is unknown, never zero.
|
|
68148
68582
|
*
|
|
68149
|
-
* The
|
|
68150
|
-
* cap
|
|
68583
|
+
* The pump's on/off duty cycle is model-specific and lives in the optional
|
|
68584
|
+
* `pump-cycle` cap. The filter, the cleaning and the water change are not
|
|
68585
|
+
* fields of this cap: they are items of the `consumables` cap on the same
|
|
68586
|
+
* device (kind `wear`), reset by the same forward mechanism.
|
|
68151
68587
|
*/
|
|
68152
68588
|
var PetFountainStatusSchema = object({
|
|
68153
68589
|
/** Water in the tank, 0..100 (%). Null while the source has not reported. */
|
|
@@ -68158,27 +68594,20 @@ var PetFountainStatusSchema = object({
|
|
|
68158
68594
|
empty: boolean().nullable(),
|
|
68159
68595
|
/** True while the pump is running. Null while unknown. */
|
|
68160
68596
|
pumpRunning: boolean().nullable(),
|
|
68161
|
-
/** True when the pump works on an on/off duty cycle rather than continuously. */
|
|
68162
|
-
pumpIntervalEnabled: boolean().nullable(),
|
|
68163
|
-
/** How long the pump runs per cycle, in seconds. */
|
|
68164
|
-
pumpOnTime: number$1().min(0).nullable().meta(readingMeta("duration")),
|
|
68165
|
-
/** How long the pump rests per cycle, in seconds. */
|
|
68166
|
-
pumpOffTime: number$1().min(0).nullable().meta(readingMeta("duration")),
|
|
68167
68597
|
/** Water drunk in the last drink, in litres. */
|
|
68168
68598
|
lastDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
68169
68599
|
/** Water drunk in the current hour, in litres. */
|
|
68170
68600
|
currentHourDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
68171
68601
|
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
68172
68602
|
last24hDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
68173
|
-
/** Water drunk since local midnight, in litres. */
|
|
68603
|
+
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
68174
68604
|
todayDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
68175
68605
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
68176
68606
|
error: string().nullable(),
|
|
68177
68607
|
/** Ms epoch when the slice was last refreshed. */
|
|
68178
68608
|
lastFetchedAt: number$1()
|
|
68179
68609
|
});
|
|
68180
|
-
var DeviceIdSchema = number$1().int().nonnegative();
|
|
68181
|
-
var MinutesSchema = number$1().int().min(0);
|
|
68610
|
+
var DeviceIdSchema$1 = number$1().int().nonnegative();
|
|
68182
68611
|
var petFountainCapability = {
|
|
68183
68612
|
name: "pet-fountain",
|
|
68184
68613
|
scope: "device",
|
|
@@ -68188,38 +68617,14 @@ var petFountainCapability = {
|
|
|
68188
68617
|
methods: {
|
|
68189
68618
|
/** Start or stop the pump. */
|
|
68190
68619
|
setPump: method(object({
|
|
68191
|
-
deviceId: DeviceIdSchema,
|
|
68192
|
-
on: boolean()
|
|
68193
|
-
}), _void(), {
|
|
68194
|
-
kind: "mutation",
|
|
68195
|
-
auth: "protected"
|
|
68196
|
-
}),
|
|
68197
|
-
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
68198
|
-
setPumpInterval: method(object({
|
|
68199
|
-
deviceId: DeviceIdSchema,
|
|
68620
|
+
deviceId: DeviceIdSchema$1,
|
|
68200
68621
|
on: boolean()
|
|
68201
68622
|
}), _void(), {
|
|
68202
68623
|
kind: "mutation",
|
|
68203
68624
|
auth: "protected"
|
|
68204
68625
|
}),
|
|
68205
|
-
/** Set how many minutes the pump runs per cycle. */
|
|
68206
|
-
setPumpOnMinutes: method(object({
|
|
68207
|
-
deviceId: DeviceIdSchema,
|
|
68208
|
-
minutes: MinutesSchema
|
|
68209
|
-
}), _void(), {
|
|
68210
|
-
kind: "mutation",
|
|
68211
|
-
auth: "protected"
|
|
68212
|
-
}),
|
|
68213
|
-
/** Set how many minutes the pump rests per cycle. */
|
|
68214
|
-
setPumpOffMinutes: method(object({
|
|
68215
|
-
deviceId: DeviceIdSchema,
|
|
68216
|
-
minutes: MinutesSchema
|
|
68217
|
-
}), _void(), {
|
|
68218
|
-
kind: "mutation",
|
|
68219
|
-
auth: "protected"
|
|
68220
|
-
}),
|
|
68221
68626
|
/** Clear the rolling daily consumption counters on the device. */
|
|
68222
|
-
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
68627
|
+
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
68223
68628
|
kind: "mutation",
|
|
68224
68629
|
auth: "protected"
|
|
68225
68630
|
})
|
|
@@ -70552,6 +70957,79 @@ var ptzAutotrackCapability = {
|
|
|
70552
70957
|
*/
|
|
70553
70958
|
history: "none"
|
|
70554
70959
|
};
|
|
70960
|
+
/**
|
|
70961
|
+
* The on/off duty cycle of a pet fountain's pump. Some models run the pump
|
|
70962
|
+
* continuously and have nothing to configure; others can alternate a run time
|
|
70963
|
+
* and a rest time. That is model-specific, so it is its own optional cap on the
|
|
70964
|
+
* fountain device: a fountain without cycle entities simply never composes it
|
|
70965
|
+
* (planning#366), and the `pet-fountain` cap stays the model-independent core.
|
|
70966
|
+
*
|
|
70967
|
+
* Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
|
|
70968
|
+
* cycle from loose entities (a cycle switch and two number boxes) and every
|
|
70969
|
+
* method is a forward to one source entity (D659, D967). Durations are stored
|
|
70970
|
+
* in SECONDS (D944); a number box that cannot be read back leaves them `null`
|
|
70971
|
+
* — unknown, never zero (D393).
|
|
70972
|
+
*/
|
|
70973
|
+
var PumpCycleStatusSchema = object({
|
|
70974
|
+
/** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
|
|
70975
|
+
enabled: boolean().nullable(),
|
|
70976
|
+
/** How long the pump runs per cycle, in seconds. Null while unknown. */
|
|
70977
|
+
onTime: number$1().min(0).nullable().meta(readingMeta("duration")),
|
|
70978
|
+
/** How long the pump rests per cycle, in seconds. Null while unknown. */
|
|
70979
|
+
offTime: number$1().min(0).nullable().meta(readingMeta("duration")),
|
|
70980
|
+
/** Ms epoch when the slice was last refreshed. */
|
|
70981
|
+
lastFetchedAt: number$1()
|
|
70982
|
+
});
|
|
70983
|
+
var DeviceIdSchema = number$1().int().nonnegative();
|
|
70984
|
+
var MinutesSchema = number$1().int().min(0);
|
|
70985
|
+
var pumpCycleCapability = {
|
|
70986
|
+
name: "pump-cycle",
|
|
70987
|
+
scope: "device",
|
|
70988
|
+
deviceNative: true,
|
|
70989
|
+
mode: "singleton",
|
|
70990
|
+
deviceTypes: [DeviceType.PetFountain],
|
|
70991
|
+
methods: {
|
|
70992
|
+
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
70993
|
+
setEnabled: method(object({
|
|
70994
|
+
deviceId: DeviceIdSchema,
|
|
70995
|
+
on: boolean()
|
|
70996
|
+
}), _void(), {
|
|
70997
|
+
kind: "mutation",
|
|
70998
|
+
auth: "protected"
|
|
70999
|
+
}),
|
|
71000
|
+
/** Set how many minutes the pump runs per cycle. */
|
|
71001
|
+
setOnMinutes: method(object({
|
|
71002
|
+
deviceId: DeviceIdSchema,
|
|
71003
|
+
minutes: MinutesSchema
|
|
71004
|
+
}), _void(), {
|
|
71005
|
+
kind: "mutation",
|
|
71006
|
+
auth: "protected"
|
|
71007
|
+
}),
|
|
71008
|
+
/** Set how many minutes the pump rests per cycle. */
|
|
71009
|
+
setOffMinutes: method(object({
|
|
71010
|
+
deviceId: DeviceIdSchema,
|
|
71011
|
+
minutes: MinutesSchema
|
|
71012
|
+
}), _void(), {
|
|
71013
|
+
kind: "mutation",
|
|
71014
|
+
auth: "protected"
|
|
71015
|
+
})
|
|
71016
|
+
},
|
|
71017
|
+
status: {
|
|
71018
|
+
schema: PumpCycleStatusSchema,
|
|
71019
|
+
kind: "push"
|
|
71020
|
+
},
|
|
71021
|
+
/** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
|
|
71022
|
+
runtimeState: PumpCycleStatusSchema,
|
|
71023
|
+
/**
|
|
71024
|
+
* Runtime-state durability: **session** — live appliance state re-published on connect.
|
|
71025
|
+
*
|
|
71026
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71027
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71028
|
+
*/
|
|
71029
|
+
durability: "session",
|
|
71030
|
+
/** History (D850): a duty-cycle setting is not a series worth recording. */
|
|
71031
|
+
history: "none"
|
|
71032
|
+
};
|
|
70555
71033
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number$1().int().nonnegative() }), object({ success: literal(true) }), {
|
|
70556
71034
|
kind: "mutation",
|
|
70557
71035
|
auth: "admin"
|
|
@@ -74447,9 +74925,17 @@ var ServerUpdateActionResultSchema = object({
|
|
|
74447
74925
|
*/
|
|
74448
74926
|
requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
|
|
74449
74927
|
});
|
|
74450
|
-
var LaunchOverridesSchema = object({
|
|
74451
|
-
/** The persisted heap ceiling in MB, or `null` when none is set. */
|
|
74452
|
-
heapMb: number$1().int().nullable()
|
|
74928
|
+
var LaunchOverridesSchema = object({
|
|
74929
|
+
/** The persisted heap ceiling in MB, or `null` when none is set. */
|
|
74930
|
+
heapMb: number$1().int().nullable(),
|
|
74931
|
+
/** The persisted hub-main RSS budget in MB, or `null` when none is set. */
|
|
74932
|
+
rssBudgetMb: number$1().int().nullable()
|
|
74933
|
+
});
|
|
74934
|
+
var SetLaunchOverridesInputSchema = object({
|
|
74935
|
+
heapMb: number$1().int().min(512).max(65536).nullable(),
|
|
74936
|
+
/** Omitted keeps the stored budget; `null` clears it. */
|
|
74937
|
+
rssBudgetMb: number$1().int().min(512).max(65536).nullable().optional()
|
|
74938
|
+
});
|
|
74453
74939
|
var SetLaunchOverridesResultSchema = LaunchOverridesSchema.extend({
|
|
74454
74940
|
/** Always true: the override is read at launch, not by the running process. */
|
|
74455
74941
|
restartRequired: boolean() });
|
|
@@ -74478,7 +74964,7 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
|
|
|
74478
74964
|
}), method(_void(), ServerUpdateActionResultSchema, {
|
|
74479
74965
|
kind: "mutation",
|
|
74480
74966
|
auth: "admin"
|
|
74481
|
-
}), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(
|
|
74967
|
+
}), method(_void(), LaunchOverridesSchema, { auth: "admin" }), method(SetLaunchOverridesInputSchema, SetLaunchOverridesResultSchema, {
|
|
74482
74968
|
kind: "mutation",
|
|
74483
74969
|
auth: "admin"
|
|
74484
74970
|
});
|
|
@@ -74536,7 +75022,16 @@ var smokeCapability = {
|
|
|
74536
75022
|
* future fields without breaking the existing call sites.
|
|
74537
75023
|
*/
|
|
74538
75024
|
var EmailAddressSchema = email();
|
|
75025
|
+
/**
|
|
75026
|
+
* Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
|
|
75027
|
+
* relay, so a caller written before relays existed keeps working unchanged. A provider with one
|
|
75028
|
+
* fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
|
|
75029
|
+
* collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
|
|
75030
|
+
*/
|
|
75031
|
+
var InstanceIdSchema = string().min(1).optional();
|
|
75032
|
+
var RelayPinSchema = object({ instanceId: InstanceIdSchema });
|
|
74539
75033
|
var SendEmailInputSchema = object({
|
|
75034
|
+
instanceId: InstanceIdSchema,
|
|
74540
75035
|
to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
|
|
74541
75036
|
cc: array(EmailAddressSchema).optional(),
|
|
74542
75037
|
bcc: array(EmailAddressSchema).optional(),
|
|
@@ -74572,11 +75067,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
74572
75067
|
kind: "mutation",
|
|
74573
75068
|
auth: "admin",
|
|
74574
75069
|
access: "create"
|
|
74575
|
-
}), method(
|
|
75070
|
+
}), method(RelayPinSchema, SmtpStatusSchema, {
|
|
74576
75071
|
kind: "mutation",
|
|
74577
75072
|
auth: "admin",
|
|
74578
75073
|
access: "view"
|
|
74579
|
-
}), method(
|
|
75074
|
+
}), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
|
|
74580
75075
|
/**
|
|
74581
75076
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
74582
75077
|
*
|
|
@@ -74608,10 +75103,12 @@ DeviceType.Camera, method(object({
|
|
|
74608
75103
|
* even when the cache is well within the device's
|
|
74609
75104
|
* `snapshotMaxAgeS` window.
|
|
74610
75105
|
*
|
|
74611
|
-
* **`force` is
|
|
75106
|
+
* **`force` is a demand with a named principal, not a freshness
|
|
75107
|
+
* preference** — an operator button, the scheduled refresh (D847), or a
|
|
75108
|
+
* notification rule that left the camera's wake allowed (D998). On a
|
|
74612
75109
|
* battery camera it is the one thing that walks past the wrapper's
|
|
74613
|
-
* sleep gate and wakes the camera, so a
|
|
74614
|
-
*
|
|
75110
|
+
* sleep gate and wakes the camera, so a poller, a thumbnail or a
|
|
75111
|
+
* prestream must NEVER set it. Every such caller
|
|
74615
75112
|
* gets the cached frame, which on a sleeping battery camera is the
|
|
74616
75113
|
* correct answer: stale but honest beats woken.
|
|
74617
75114
|
*/
|
|
@@ -76181,6 +76678,40 @@ var LoggingSettingsStateSchema = object({
|
|
|
76181
76678
|
opsLogRetentionDays: number$1().int().min(0),
|
|
76182
76679
|
persisted: boolean()
|
|
76183
76680
|
});
|
|
76681
|
+
/**
|
|
76682
|
+
* Debug WATCH — a short, per-device window over `debug.<channel>` lines.
|
|
76683
|
+
*
|
|
76684
|
+
* `start` opens (or re-arms) a 10 minute window; `rearm` extends a LIVE one
|
|
76685
|
+
* and never revives a lapsed one; `stop` closes it; `list` only reads. No
|
|
76686
|
+
* chain of re-arms outlives 60 minutes from the first start. The hub holds the
|
|
76687
|
+
* watches in memory and pushes the interest to every node, so a watch lapses
|
|
76688
|
+
* by itself and survives nothing — a restart is a stop.
|
|
76689
|
+
*/
|
|
76690
|
+
var DebugWatchInputSchema = object({
|
|
76691
|
+
action: _enum([
|
|
76692
|
+
"list",
|
|
76693
|
+
"start",
|
|
76694
|
+
"rearm",
|
|
76695
|
+
"stop"
|
|
76696
|
+
]),
|
|
76697
|
+
/** Full channel names, `debug.<channel>`, each declared with `kind: 'debug'`. */
|
|
76698
|
+
channels: array(string().min(1)).readonly().default([]),
|
|
76699
|
+
/** Numeric device ids. Required (non-empty) for `start`. */
|
|
76700
|
+
deviceIds: array(number$1().int()).readonly().default([])
|
|
76701
|
+
});
|
|
76702
|
+
var DebugWatchStateSchema = object({
|
|
76703
|
+
channel: string(),
|
|
76704
|
+
deviceIds: array(number$1().int()).readonly(),
|
|
76705
|
+
startedAtMs: number$1(),
|
|
76706
|
+
untilMs: number$1(),
|
|
76707
|
+
capAtMs: number$1()
|
|
76708
|
+
});
|
|
76709
|
+
var DebugWatchResultSchema = object({
|
|
76710
|
+
nowMs: number$1(),
|
|
76711
|
+
watches: array(DebugWatchStateSchema).readonly(),
|
|
76712
|
+
/** Channels a `rearm` found already lapsed: nothing was revived. */
|
|
76713
|
+
lapsed: array(string()).readonly()
|
|
76714
|
+
});
|
|
76184
76715
|
method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
|
|
76185
76716
|
kind: "mutation",
|
|
76186
76717
|
auth: "admin"
|
|
@@ -76196,6 +76727,9 @@ method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_
|
|
|
76196
76727
|
}), method(_void(), RequestCensusStatusSchema, { auth: "admin" }), method(_void(), array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }), method(_void(), array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }), method(GetLoggingSettingsInputSchema, LoggingSettingsStateSchema, { auth: "admin" }), method(SetLoggingSettingsInputSchema, LoggingSettingsStateSchema, {
|
|
76197
76728
|
kind: "mutation",
|
|
76198
76729
|
auth: "admin"
|
|
76730
|
+
}), method(DebugWatchInputSchema, DebugWatchResultSchema, {
|
|
76731
|
+
kind: "mutation",
|
|
76732
|
+
auth: "admin"
|
|
76199
76733
|
});
|
|
76200
76734
|
/**
|
|
76201
76735
|
* Tamper / case-open detection sensor. Drives Home Assistant
|
|
@@ -79261,7 +79795,7 @@ discriminatedUnion("kind", [object({
|
|
|
79261
79795
|
kind: literal("pronto"),
|
|
79262
79796
|
code: string()
|
|
79263
79797
|
})]);
|
|
79264
|
-
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79798
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79265
79799
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79266
79800
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
79267
79801
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
@@ -79584,6 +80118,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
79584
80118
|
deviceDiscovery: deviceDiscoveryCapability,
|
|
79585
80119
|
deviceStatus: deviceStatusCapability,
|
|
79586
80120
|
dishwasher: dishwasherCapability,
|
|
80121
|
+
doNotDisturb: doNotDisturbCapability,
|
|
79587
80122
|
doorbell: doorbellCapability,
|
|
79588
80123
|
enumSensor: enumSensorCapability,
|
|
79589
80124
|
eventEmitter: eventEmitterCapability,
|
|
@@ -79620,6 +80155,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
79620
80155
|
pressureSensor: pressureSensorCapability,
|
|
79621
80156
|
privacyMask: privacyMaskCapability,
|
|
79622
80157
|
ptzAutotrack: ptzAutotrackCapability,
|
|
80158
|
+
pumpCycle: pumpCycleCapability,
|
|
79623
80159
|
recordingOnboard: recordingOnboardCapability,
|
|
79624
80160
|
recordingSignal: recordingSignalCapability,
|
|
79625
80161
|
runningState: runningStateCapability,
|
|
@@ -81804,6 +82340,27 @@ Object.freeze({
|
|
|
81804
82340
|
access: "view",
|
|
81805
82341
|
role: "viewer"
|
|
81806
82342
|
},
|
|
82343
|
+
"boardKeySource.getBoardKey": {
|
|
82344
|
+
capName: "board-key-source",
|
|
82345
|
+
capScope: "system",
|
|
82346
|
+
addonId: null,
|
|
82347
|
+
access: "view",
|
|
82348
|
+
role: "admin"
|
|
82349
|
+
},
|
|
82350
|
+
"boardKeySource.listBoards": {
|
|
82351
|
+
capName: "board-key-source",
|
|
82352
|
+
capScope: "system",
|
|
82353
|
+
addonId: null,
|
|
82354
|
+
access: "view",
|
|
82355
|
+
role: "admin"
|
|
82356
|
+
},
|
|
82357
|
+
"boardKeySource.listSources": {
|
|
82358
|
+
capName: "board-key-source",
|
|
82359
|
+
capScope: "system",
|
|
82360
|
+
addonId: null,
|
|
82361
|
+
access: "view",
|
|
82362
|
+
role: "admin"
|
|
82363
|
+
},
|
|
81807
82364
|
"brightness.getStatus": {
|
|
81808
82365
|
capName: "brightness",
|
|
81809
82366
|
capScope: "device",
|
|
@@ -83141,6 +83698,13 @@ Object.freeze({
|
|
|
83141
83698
|
access: "view",
|
|
83142
83699
|
role: "admin"
|
|
83143
83700
|
},
|
|
83701
|
+
"deviceManager.dismissDiscoveryProposal": {
|
|
83702
|
+
capName: "device-manager",
|
|
83703
|
+
capScope: "system",
|
|
83704
|
+
addonId: null,
|
|
83705
|
+
access: "create",
|
|
83706
|
+
role: "admin"
|
|
83707
|
+
},
|
|
83144
83708
|
"deviceManager.enable": {
|
|
83145
83709
|
capName: "device-manager",
|
|
83146
83710
|
capScope: "system",
|
|
@@ -83323,6 +83887,13 @@ Object.freeze({
|
|
|
83323
83887
|
access: "view",
|
|
83324
83888
|
role: "viewer"
|
|
83325
83889
|
},
|
|
83890
|
+
"deviceManager.listDiscoveryProposals": {
|
|
83891
|
+
capName: "device-manager",
|
|
83892
|
+
capScope: "system",
|
|
83893
|
+
addonId: null,
|
|
83894
|
+
access: "view",
|
|
83895
|
+
role: "admin"
|
|
83896
|
+
},
|
|
83326
83897
|
"deviceManager.listLocationDescriptions": {
|
|
83327
83898
|
capName: "device-manager",
|
|
83328
83899
|
capScope: "system",
|
|
@@ -83477,6 +84048,13 @@ Object.freeze({
|
|
|
83477
84048
|
access: "create",
|
|
83478
84049
|
role: "admin"
|
|
83479
84050
|
},
|
|
84051
|
+
"deviceManager.reportDiscoveryObservations": {
|
|
84052
|
+
capName: "device-manager",
|
|
84053
|
+
capScope: "system",
|
|
84054
|
+
addonId: null,
|
|
84055
|
+
access: "create",
|
|
84056
|
+
role: "admin"
|
|
84057
|
+
},
|
|
83480
84058
|
"deviceManager.runDeviceAction": {
|
|
83481
84059
|
capName: "device-manager",
|
|
83482
84060
|
capScope: "system",
|
|
@@ -83484,6 +84062,13 @@ Object.freeze({
|
|
|
83484
84062
|
access: "create",
|
|
83485
84063
|
role: "admin"
|
|
83486
84064
|
},
|
|
84065
|
+
"deviceManager.runDiscoverySweep": {
|
|
84066
|
+
capName: "device-manager",
|
|
84067
|
+
capScope: "system",
|
|
84068
|
+
addonId: null,
|
|
84069
|
+
access: "create",
|
|
84070
|
+
role: "admin"
|
|
84071
|
+
},
|
|
83487
84072
|
"deviceManager.setChildLayout": {
|
|
83488
84073
|
capName: "device-manager",
|
|
83489
84074
|
capScope: "system",
|
|
@@ -84037,6 +84622,20 @@ Object.freeze({
|
|
|
84037
84622
|
access: "create",
|
|
84038
84623
|
role: "admin"
|
|
84039
84624
|
},
|
|
84625
|
+
"doNotDisturb.getStatus": {
|
|
84626
|
+
capName: "do-not-disturb",
|
|
84627
|
+
capScope: "device",
|
|
84628
|
+
addonId: null,
|
|
84629
|
+
access: "view",
|
|
84630
|
+
role: "viewer"
|
|
84631
|
+
},
|
|
84632
|
+
"doNotDisturb.setEnabled": {
|
|
84633
|
+
capName: "do-not-disturb",
|
|
84634
|
+
capScope: "device",
|
|
84635
|
+
addonId: null,
|
|
84636
|
+
access: "create",
|
|
84637
|
+
role: "viewer"
|
|
84638
|
+
},
|
|
84040
84639
|
"doorbell.getStatus": {
|
|
84041
84640
|
capName: "doorbell",
|
|
84042
84641
|
capScope: "device",
|
|
@@ -84401,6 +85000,69 @@ Object.freeze({
|
|
|
84401
85000
|
access: "create",
|
|
84402
85001
|
role: "admin"
|
|
84403
85002
|
},
|
|
85003
|
+
"integrationInstances.createInstance": {
|
|
85004
|
+
capName: "integration-instances",
|
|
85005
|
+
capScope: "system",
|
|
85006
|
+
addonId: null,
|
|
85007
|
+
access: "create",
|
|
85008
|
+
role: "admin"
|
|
85009
|
+
},
|
|
85010
|
+
"integrationInstances.deleteInstance": {
|
|
85011
|
+
capName: "integration-instances",
|
|
85012
|
+
capScope: "system",
|
|
85013
|
+
addonId: null,
|
|
85014
|
+
access: "delete",
|
|
85015
|
+
role: "admin"
|
|
85016
|
+
},
|
|
85017
|
+
"integrationInstances.describe": {
|
|
85018
|
+
capName: "integration-instances",
|
|
85019
|
+
capScope: "system",
|
|
85020
|
+
addonId: null,
|
|
85021
|
+
access: "view",
|
|
85022
|
+
role: "admin"
|
|
85023
|
+
},
|
|
85024
|
+
"integrationInstances.getInstance": {
|
|
85025
|
+
capName: "integration-instances",
|
|
85026
|
+
capScope: "system",
|
|
85027
|
+
addonId: null,
|
|
85028
|
+
access: "view",
|
|
85029
|
+
role: "admin"
|
|
85030
|
+
},
|
|
85031
|
+
"integrationInstances.instanceFormSchema": {
|
|
85032
|
+
capName: "integration-instances",
|
|
85033
|
+
capScope: "system",
|
|
85034
|
+
addonId: null,
|
|
85035
|
+
access: "view",
|
|
85036
|
+
role: "admin"
|
|
85037
|
+
},
|
|
85038
|
+
"integrationInstances.listInstances": {
|
|
85039
|
+
capName: "integration-instances",
|
|
85040
|
+
capScope: "system",
|
|
85041
|
+
addonId: null,
|
|
85042
|
+
access: "view",
|
|
85043
|
+
role: "admin"
|
|
85044
|
+
},
|
|
85045
|
+
"integrationInstances.setInstanceEnabled": {
|
|
85046
|
+
capName: "integration-instances",
|
|
85047
|
+
capScope: "system",
|
|
85048
|
+
addonId: null,
|
|
85049
|
+
access: "create",
|
|
85050
|
+
role: "admin"
|
|
85051
|
+
},
|
|
85052
|
+
"integrationInstances.testInstance": {
|
|
85053
|
+
capName: "integration-instances",
|
|
85054
|
+
capScope: "system",
|
|
85055
|
+
addonId: null,
|
|
85056
|
+
access: "create",
|
|
85057
|
+
role: "admin"
|
|
85058
|
+
},
|
|
85059
|
+
"integrationInstances.updateInstance": {
|
|
85060
|
+
capName: "integration-instances",
|
|
85061
|
+
capScope: "system",
|
|
85062
|
+
addonId: null,
|
|
85063
|
+
access: "create",
|
|
85064
|
+
role: "admin"
|
|
85065
|
+
},
|
|
84404
85066
|
"integrations.create": {
|
|
84405
85067
|
capName: "integrations",
|
|
84406
85068
|
capScope: "system",
|
|
@@ -86522,27 +87184,6 @@ Object.freeze({
|
|
|
86522
87184
|
access: "create",
|
|
86523
87185
|
role: "viewer"
|
|
86524
87186
|
},
|
|
86525
|
-
"petFountain.setPumpInterval": {
|
|
86526
|
-
capName: "pet-fountain",
|
|
86527
|
-
capScope: "device",
|
|
86528
|
-
addonId: null,
|
|
86529
|
-
access: "create",
|
|
86530
|
-
role: "viewer"
|
|
86531
|
-
},
|
|
86532
|
-
"petFountain.setPumpOffMinutes": {
|
|
86533
|
-
capName: "pet-fountain",
|
|
86534
|
-
capScope: "device",
|
|
86535
|
-
addonId: null,
|
|
86536
|
-
access: "create",
|
|
86537
|
-
role: "viewer"
|
|
86538
|
-
},
|
|
86539
|
-
"petFountain.setPumpOnMinutes": {
|
|
86540
|
-
capName: "pet-fountain",
|
|
86541
|
-
capScope: "device",
|
|
86542
|
-
addonId: null,
|
|
86543
|
-
access: "create",
|
|
86544
|
-
role: "viewer"
|
|
86545
|
-
},
|
|
86546
87187
|
"pipelineAnalytics.addRetrainFrameFromRecording": {
|
|
86547
87188
|
capName: "pipeline-analytics",
|
|
86548
87189
|
capScope: "device",
|
|
@@ -88321,6 +88962,34 @@ Object.freeze({
|
|
|
88321
88962
|
access: "create",
|
|
88322
88963
|
role: "admin"
|
|
88323
88964
|
},
|
|
88965
|
+
"pumpCycle.getStatus": {
|
|
88966
|
+
capName: "pump-cycle",
|
|
88967
|
+
capScope: "device",
|
|
88968
|
+
addonId: null,
|
|
88969
|
+
access: "view",
|
|
88970
|
+
role: "viewer"
|
|
88971
|
+
},
|
|
88972
|
+
"pumpCycle.setEnabled": {
|
|
88973
|
+
capName: "pump-cycle",
|
|
88974
|
+
capScope: "device",
|
|
88975
|
+
addonId: null,
|
|
88976
|
+
access: "create",
|
|
88977
|
+
role: "viewer"
|
|
88978
|
+
},
|
|
88979
|
+
"pumpCycle.setOffMinutes": {
|
|
88980
|
+
capName: "pump-cycle",
|
|
88981
|
+
capScope: "device",
|
|
88982
|
+
addonId: null,
|
|
88983
|
+
access: "create",
|
|
88984
|
+
role: "viewer"
|
|
88985
|
+
},
|
|
88986
|
+
"pumpCycle.setOnMinutes": {
|
|
88987
|
+
capName: "pump-cycle",
|
|
88988
|
+
capScope: "device",
|
|
88989
|
+
addonId: null,
|
|
88990
|
+
access: "create",
|
|
88991
|
+
role: "viewer"
|
|
88992
|
+
},
|
|
88324
88993
|
"reboot.reboot": {
|
|
88325
88994
|
capName: "reboot",
|
|
88326
88995
|
capScope: "device",
|
|
@@ -90057,6 +90726,13 @@ Object.freeze({
|
|
|
90057
90726
|
access: "create",
|
|
90058
90727
|
role: "viewer"
|
|
90059
90728
|
},
|
|
90729
|
+
"system.debugWatch": {
|
|
90730
|
+
capName: "system",
|
|
90731
|
+
capScope: "system",
|
|
90732
|
+
addonId: null,
|
|
90733
|
+
access: "create",
|
|
90734
|
+
role: "admin"
|
|
90735
|
+
},
|
|
90060
90736
|
"system.detectSiteLocation": {
|
|
90061
90737
|
capName: "system",
|
|
90062
90738
|
capScope: "system",
|
|
@@ -92225,6 +92901,16 @@ Object.freeze({
|
|
|
92225
92901
|
form: "single",
|
|
92226
92902
|
optional: false
|
|
92227
92903
|
}],
|
|
92904
|
+
"doNotDisturb.getStatus": [{
|
|
92905
|
+
name: "deviceId",
|
|
92906
|
+
form: "single",
|
|
92907
|
+
optional: false
|
|
92908
|
+
}],
|
|
92909
|
+
"doNotDisturb.setEnabled": [{
|
|
92910
|
+
name: "deviceId",
|
|
92911
|
+
form: "single",
|
|
92912
|
+
optional: false
|
|
92913
|
+
}],
|
|
92228
92914
|
"doorbell.getStatus": [{
|
|
92229
92915
|
name: "deviceId",
|
|
92230
92916
|
form: "single",
|
|
@@ -92848,21 +93534,6 @@ Object.freeze({
|
|
|
92848
93534
|
form: "single",
|
|
92849
93535
|
optional: false
|
|
92850
93536
|
}],
|
|
92851
|
-
"petFountain.setPumpInterval": [{
|
|
92852
|
-
name: "deviceId",
|
|
92853
|
-
form: "single",
|
|
92854
|
-
optional: false
|
|
92855
|
-
}],
|
|
92856
|
-
"petFountain.setPumpOffMinutes": [{
|
|
92857
|
-
name: "deviceId",
|
|
92858
|
-
form: "single",
|
|
92859
|
-
optional: false
|
|
92860
|
-
}],
|
|
92861
|
-
"petFountain.setPumpOnMinutes": [{
|
|
92862
|
-
name: "deviceId",
|
|
92863
|
-
form: "single",
|
|
92864
|
-
optional: false
|
|
92865
|
-
}],
|
|
92866
93537
|
"pipelineAnalytics.addRetrainFrameFromRecording": [{
|
|
92867
93538
|
name: "deviceId",
|
|
92868
93539
|
form: "single",
|
|
@@ -93498,6 +94169,26 @@ Object.freeze({
|
|
|
93498
94169
|
form: "single",
|
|
93499
94170
|
optional: false
|
|
93500
94171
|
}],
|
|
94172
|
+
"pumpCycle.getStatus": [{
|
|
94173
|
+
name: "deviceId",
|
|
94174
|
+
form: "single",
|
|
94175
|
+
optional: false
|
|
94176
|
+
}],
|
|
94177
|
+
"pumpCycle.setEnabled": [{
|
|
94178
|
+
name: "deviceId",
|
|
94179
|
+
form: "single",
|
|
94180
|
+
optional: false
|
|
94181
|
+
}],
|
|
94182
|
+
"pumpCycle.setOffMinutes": [{
|
|
94183
|
+
name: "deviceId",
|
|
94184
|
+
form: "single",
|
|
94185
|
+
optional: false
|
|
94186
|
+
}],
|
|
94187
|
+
"pumpCycle.setOnMinutes": [{
|
|
94188
|
+
name: "deviceId",
|
|
94189
|
+
form: "single",
|
|
94190
|
+
optional: false
|
|
94191
|
+
}],
|
|
93501
94192
|
"reboot.reboot": [{
|
|
93502
94193
|
name: "deviceId",
|
|
93503
94194
|
form: "single",
|
|
@@ -93965,6 +94656,11 @@ Object.freeze({
|
|
|
93965
94656
|
form: "single",
|
|
93966
94657
|
optional: false
|
|
93967
94658
|
}],
|
|
94659
|
+
"system.debugWatch": [{
|
|
94660
|
+
name: "deviceIds",
|
|
94661
|
+
form: "array",
|
|
94662
|
+
optional: true
|
|
94663
|
+
}],
|
|
93968
94664
|
"tamper.getStatus": [{
|
|
93969
94665
|
name: "deviceId",
|
|
93970
94666
|
form: "single",
|
|
@@ -140380,6 +141076,13 @@ var ALEXA_BROKER_KIND = "alexa";
|
|
|
140380
141076
|
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
140381
141077
|
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
140382
141078
|
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
141079
|
+
/** How often an adopted Echo's player state is read (a push channel is a later phase). */
|
|
141080
|
+
var ECHO_POLL_MS = 3e4;
|
|
141081
|
+
/** One more read after a command, once Amazon has had time to apply it. */
|
|
141082
|
+
var ECHO_REREAD_MS = 1500;
|
|
141083
|
+
/** After a 429 the account is left alone for this long, doubling up to the cap. */
|
|
141084
|
+
var ECHO_RATE_BACKOFF_BASE_MS = 3e4;
|
|
141085
|
+
var ECHO_RATE_BACKOFF_MAX_MS = 5 * 6e4;
|
|
140383
141086
|
//#endregion
|
|
140384
141087
|
//#region src/alexa-broker-provider.ts
|
|
140385
141088
|
/**
|
|
@@ -140419,7 +141122,7 @@ function buildAlexaBrokerProvider(deps) {
|
|
|
140419
141122
|
label: "Alexa",
|
|
140420
141123
|
signIn: {
|
|
140421
141124
|
basePath: "/addon/import-alexa/login",
|
|
140422
|
-
methods: ["device-code"]
|
|
141125
|
+
methods: ["device-code", "proxy"]
|
|
140423
141126
|
}
|
|
140424
141127
|
}]
|
|
140425
141128
|
}],
|
|
@@ -140609,6 +141312,37 @@ function buildAlexaApplianceCandidate(input) {
|
|
|
140609
141312
|
function mappableAppliance(appliance) {
|
|
140610
141313
|
return mapAlexaCapabilitiesToCamStack(appliance.interfaces).capabilities.length > 0;
|
|
140611
141314
|
}
|
|
141315
|
+
/** `echo:<serial>`: how an Echo is named among an account's adoption candidates. */
|
|
141316
|
+
var ECHO_NATIVE_ID_PREFIX = "echo:";
|
|
141317
|
+
function echoNativeId(serial) {
|
|
141318
|
+
return `${ECHO_NATIVE_ID_PREFIX}${serial}`;
|
|
141319
|
+
}
|
|
141320
|
+
/** The serial an Echo candidate id names, or null when the id is not an Echo's. */
|
|
141321
|
+
function echoSerialOf(childNativeId) {
|
|
141322
|
+
if (!childNativeId.startsWith("echo:")) return null;
|
|
141323
|
+
const serial = childNativeId.slice(5);
|
|
141324
|
+
return serial === "" ? null : serial;
|
|
141325
|
+
}
|
|
141326
|
+
/** An Echo the adoption list offers: a media player, a flat sibling under the Alexa integration. */
|
|
141327
|
+
function isAdoptableEcho(device) {
|
|
141328
|
+
return device.kind === "device";
|
|
141329
|
+
}
|
|
141330
|
+
function buildAlexaEchoCandidate(input) {
|
|
141331
|
+
const { device, adoptedDeviceId } = input;
|
|
141332
|
+
return {
|
|
141333
|
+
childNativeId: echoNativeId(device.id),
|
|
141334
|
+
name: device.name.length > 0 ? device.name : device.id,
|
|
141335
|
+
type: DeviceType.MediaPlayer,
|
|
141336
|
+
status: device.online ? "online" : "offline",
|
|
141337
|
+
metadata: {
|
|
141338
|
+
manufacturer: "Amazon",
|
|
141339
|
+
...device.room !== void 0 ? { externalLocation: device.room } : {}
|
|
141340
|
+
},
|
|
141341
|
+
alreadyAdopted: adoptedDeviceId !== null,
|
|
141342
|
+
adoptedDeviceId,
|
|
141343
|
+
capabilities: ["media-player", "do-not-disturb"]
|
|
141344
|
+
};
|
|
141345
|
+
}
|
|
140612
141346
|
//#endregion
|
|
140613
141347
|
//#region src/alexa-adoption-provider.ts
|
|
140614
141348
|
/** Single granularity: each Alexa appliance adopts as one device. */
|
|
@@ -140622,6 +141356,11 @@ async function adoptedApplianceId(brokerId, applianceId, listAdoptedAlexa) {
|
|
|
140622
141356
|
for (const d of await listAdoptedAlexa()) if (d.config["system"] === "alexa" && d.config["brokerId"] === brokerId && d.config["applianceId"] === applianceId) return d.id;
|
|
140623
141357
|
return null;
|
|
140624
141358
|
}
|
|
141359
|
+
/** Resolve the adopted CamStack device id for a (broker, Echo serial), or null. */
|
|
141360
|
+
async function adoptedEchoId(brokerId, serial, listAdoptedAlexa) {
|
|
141361
|
+
for (const d of await listAdoptedAlexa()) if (d.config["system"] === "alexa" && d.config["brokerId"] === brokerId && d.config["serial"] === serial) return d.id;
|
|
141362
|
+
return null;
|
|
141363
|
+
}
|
|
140625
141364
|
/**
|
|
140626
141365
|
* Construct the `device-adoption` cap provider for the Alexa import addon. Pure
|
|
140627
141366
|
* builder. One broker = one Amazon account; each mappable appliance is a flat
|
|
@@ -140629,7 +141368,7 @@ async function adoptedApplianceId(brokerId, applianceId, listAdoptedAlexa) {
|
|
|
140629
141368
|
* Container fan-out — Alexa appliances are standalone devices).
|
|
140630
141369
|
*/
|
|
140631
141370
|
function buildAlexaAdoptionProvider(deps) {
|
|
140632
|
-
const { registry, getBrokerIdForIntegration, listAdoptedAlexa, adoptAppliance, removeDevice, findDeviceConfig, logger } = deps;
|
|
141371
|
+
const { registry, getBrokerIdForIntegration, listAdoptedAlexa, adoptAppliance, adoptEcho, removeDevice, findDeviceConfig, logger } = deps;
|
|
140633
141372
|
async function candidatesForBroker(brokerId) {
|
|
140634
141373
|
const appliances = registry.getAppliancesForBroker(brokerId).filter(mappableAppliance);
|
|
140635
141374
|
const out = [];
|
|
@@ -140640,6 +141379,13 @@ function buildAlexaAdoptionProvider(deps) {
|
|
|
140640
141379
|
adoptedDeviceId
|
|
140641
141380
|
}));
|
|
140642
141381
|
}
|
|
141382
|
+
for (const device of registry.getEchoesForBroker(brokerId).filter(isAdoptableEcho)) {
|
|
141383
|
+
const adoptedDeviceId = await adoptedEchoId(brokerId, device.id, listAdoptedAlexa);
|
|
141384
|
+
out.push(buildAlexaEchoCandidate({
|
|
141385
|
+
device,
|
|
141386
|
+
adoptedDeviceId
|
|
141387
|
+
}));
|
|
141388
|
+
}
|
|
140643
141389
|
return out;
|
|
140644
141390
|
}
|
|
140645
141391
|
return {
|
|
@@ -140694,7 +141440,41 @@ function buildAlexaAdoptionProvider(deps) {
|
|
|
140694
141440
|
const appliances = registry.getAppliancesForBroker(brokerId);
|
|
140695
141441
|
const adopted = [];
|
|
140696
141442
|
let failures = 0;
|
|
141443
|
+
const echoes = registry.getEchoesForBroker(brokerId);
|
|
140697
141444
|
for (const nativeId of childNativeIds) {
|
|
141445
|
+
const echoSerial = echoSerialOf(nativeId);
|
|
141446
|
+
if (echoSerial !== null) {
|
|
141447
|
+
const echo = echoes.find((device) => device.id === echoSerial && isAdoptableEcho(device));
|
|
141448
|
+
if (echo === void 0) {
|
|
141449
|
+
logger.warn("alexa adopt: Echo not found in the account — skipping", { meta: {
|
|
141450
|
+
nativeId,
|
|
141451
|
+
brokerId
|
|
141452
|
+
} });
|
|
141453
|
+
failures++;
|
|
141454
|
+
continue;
|
|
141455
|
+
}
|
|
141456
|
+
try {
|
|
141457
|
+
const { deviceId } = await adoptEcho({
|
|
141458
|
+
brokerId,
|
|
141459
|
+
echo,
|
|
141460
|
+
integrationId,
|
|
141461
|
+
name: perCandidate?.[nativeId]?.name ?? echo.name
|
|
141462
|
+
});
|
|
141463
|
+
adopted.push({
|
|
141464
|
+
childNativeId: nativeId,
|
|
141465
|
+
parentDeviceId: deviceId,
|
|
141466
|
+
accessoryDeviceIds: []
|
|
141467
|
+
});
|
|
141468
|
+
} catch (err) {
|
|
141469
|
+
logger.warn("alexa adopt: failed to adopt Echo", { meta: {
|
|
141470
|
+
brokerId,
|
|
141471
|
+
serial: echoSerial,
|
|
141472
|
+
error: errMsg(err)
|
|
141473
|
+
} });
|
|
141474
|
+
failures++;
|
|
141475
|
+
}
|
|
141476
|
+
continue;
|
|
141477
|
+
}
|
|
140698
141478
|
const appliance = appliances.find((a) => a.applianceId === nativeId);
|
|
140699
141479
|
if (appliance === void 0) {
|
|
140700
141480
|
logger.warn("alexa adopt: appliance not found in inventory — skipping", { meta: {
|
|
@@ -140787,6 +141567,7 @@ function setBrokerDevicesOnline(devices, brokerId, online) {
|
|
|
140787
141567
|
for (const dev of devices) {
|
|
140788
141568
|
if (dev.config.get("system") !== "alexa") continue;
|
|
140789
141569
|
if (dev.config.get("brokerId") !== brokerId) continue;
|
|
141570
|
+
if (typeof dev.config.get("serial") === "string") continue;
|
|
140790
141571
|
if (dev.online === online) continue;
|
|
140791
141572
|
dev.markOnline(online);
|
|
140792
141573
|
count += 1;
|
|
@@ -141194,6 +141975,421 @@ var AlexaDevice = class extends BaseDevice {
|
|
|
141194
141975
|
function isKnownCap(cap) {
|
|
141195
141976
|
return KNOWN_CAPS.has(cap);
|
|
141196
141977
|
}
|
|
141978
|
+
var EchoSessionRegistry = class {
|
|
141979
|
+
#resolve = null;
|
|
141980
|
+
#dnd = /* @__PURE__ */ new Map();
|
|
141981
|
+
setResolver(fn) {
|
|
141982
|
+
this.#resolve = fn;
|
|
141983
|
+
}
|
|
141984
|
+
get(brokerId) {
|
|
141985
|
+
return this.#resolve === null ? null : this.#resolve(brokerId);
|
|
141986
|
+
}
|
|
141987
|
+
/** Do Not Disturb of every device of the account. A failed read is not kept for the next device. */
|
|
141988
|
+
dnd(brokerId, now) {
|
|
141989
|
+
const shared = this.#dnd.get(brokerId);
|
|
141990
|
+
if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
|
|
141991
|
+
const session = this.get(brokerId);
|
|
141992
|
+
if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
|
|
141993
|
+
const pending = session.dndState();
|
|
141994
|
+
this.#dnd.set(brokerId, {
|
|
141995
|
+
at: now,
|
|
141996
|
+
pending
|
|
141997
|
+
});
|
|
141998
|
+
pending.catch(() => {
|
|
141999
|
+
if (this.#dnd.get(brokerId)?.pending === pending) this.#dnd.delete(brokerId);
|
|
142000
|
+
});
|
|
142001
|
+
return pending;
|
|
142002
|
+
}
|
|
142003
|
+
clear() {
|
|
142004
|
+
this.#resolve = null;
|
|
142005
|
+
this.#dnd.clear();
|
|
142006
|
+
}
|
|
142007
|
+
};
|
|
142008
|
+
/** The single in-process registry shared across the addon. */
|
|
142009
|
+
var alexaEchoSessions = new EchoSessionRegistry();
|
|
142010
|
+
//#endregion
|
|
142011
|
+
//#region src/alexa-echo-state-to-cap.ts
|
|
142012
|
+
/** The slice of an Echo that cannot be read: every reading null, the state as given. */
|
|
142013
|
+
function emptyEchoStatus(state, now) {
|
|
142014
|
+
return {
|
|
142015
|
+
state,
|
|
142016
|
+
stateSource: "reported",
|
|
142017
|
+
assumedFields: [],
|
|
142018
|
+
volumeLevel: null,
|
|
142019
|
+
isMuted: null,
|
|
142020
|
+
source: "",
|
|
142021
|
+
availableSources: [],
|
|
142022
|
+
currentMedia: null,
|
|
142023
|
+
position: null,
|
|
142024
|
+
duration: null,
|
|
142025
|
+
shuffle: null,
|
|
142026
|
+
repeat: null,
|
|
142027
|
+
lastChangedAt: now
|
|
142028
|
+
};
|
|
142029
|
+
}
|
|
142030
|
+
function mapEchoPlaybackState(value) {
|
|
142031
|
+
switch (value) {
|
|
142032
|
+
case "PLAYING": return "playing";
|
|
142033
|
+
case "PAUSED": return "paused";
|
|
142034
|
+
case "BUFFERING": return "buffering";
|
|
142035
|
+
default: return "idle";
|
|
142036
|
+
}
|
|
142037
|
+
}
|
|
142038
|
+
function percent(value) {
|
|
142039
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
142040
|
+
return Math.round(Math.max(0, Math.min(100, value)));
|
|
142041
|
+
}
|
|
142042
|
+
function secondsToMs(value) {
|
|
142043
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
142044
|
+
return Math.round(value * 1e3);
|
|
142045
|
+
}
|
|
142046
|
+
function nonEmpty(value) {
|
|
142047
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
142048
|
+
}
|
|
142049
|
+
/** `SELECTED` is on, `UNSELECTED` is off; `HIDDEN` and `DISABLED` mean the surface does not exist. */
|
|
142050
|
+
function shuffleOf(value) {
|
|
142051
|
+
if (value === "SELECTED") return true;
|
|
142052
|
+
if (value === "UNSELECTED") return false;
|
|
142053
|
+
return null;
|
|
142054
|
+
}
|
|
142055
|
+
function repeatOf(value) {
|
|
142056
|
+
if (value === "SELECTED") return "all";
|
|
142057
|
+
if (value === "UNSELECTED") return "off";
|
|
142058
|
+
return null;
|
|
142059
|
+
}
|
|
142060
|
+
function currentMediaOf(info) {
|
|
142061
|
+
const title = nonEmpty(info.infoText?.title);
|
|
142062
|
+
if (title === null) return null;
|
|
142063
|
+
const artist = nonEmpty(info.infoText?.subText1);
|
|
142064
|
+
const album = nonEmpty(info.infoText?.subText2);
|
|
142065
|
+
const imageUrl = nonEmpty(info.mainArt?.url);
|
|
142066
|
+
return {
|
|
142067
|
+
type: "unknown",
|
|
142068
|
+
title,
|
|
142069
|
+
...artist !== null ? { artist } : {},
|
|
142070
|
+
...album !== null ? { album } : {},
|
|
142071
|
+
...imageUrl !== null ? { imageUrl } : {}
|
|
142072
|
+
};
|
|
142073
|
+
}
|
|
142074
|
+
function mapPlayerInfoToStatus(info, input) {
|
|
142075
|
+
if (!input.online) return emptyEchoStatus("off", input.now);
|
|
142076
|
+
const length = secondsToMs(info.progress?.mediaLength);
|
|
142077
|
+
return {
|
|
142078
|
+
...emptyEchoStatus(mapEchoPlaybackState(info.state), input.now),
|
|
142079
|
+
volumeLevel: percent(info.volume?.volume),
|
|
142080
|
+
isMuted: typeof info.volume?.muted === "boolean" ? info.volume.muted : null,
|
|
142081
|
+
currentMedia: currentMediaOf(info),
|
|
142082
|
+
position: secondsToMs(info.progress?.mediaProgress),
|
|
142083
|
+
duration: length === 0 ? null : length,
|
|
142084
|
+
shuffle: shuffleOf(info.transport?.shuffle),
|
|
142085
|
+
repeat: repeatOf(info.transport?.repeat)
|
|
142086
|
+
};
|
|
142087
|
+
}
|
|
142088
|
+
//#endregion
|
|
142089
|
+
//#region src/speak/worker-protocol.ts
|
|
142090
|
+
/** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
|
|
142091
|
+
var EchoDeviceSchema = object({
|
|
142092
|
+
id: string().min(1),
|
|
142093
|
+
name: string(),
|
|
142094
|
+
family: string(),
|
|
142095
|
+
online: boolean(),
|
|
142096
|
+
kind: _enum(["device", "group"]),
|
|
142097
|
+
dnd: boolean(),
|
|
142098
|
+
deviceType: string(),
|
|
142099
|
+
/** The room the account files the device under, when Amazon names one. */
|
|
142100
|
+
room: string().optional()
|
|
142101
|
+
});
|
|
142102
|
+
var LoginStatusSchema = _enum(["ok", "failed"]);
|
|
142103
|
+
var SessionStatusSchema = object({
|
|
142104
|
+
loggedIn: boolean(),
|
|
142105
|
+
ageSec: number$1().int().nonnegative(),
|
|
142106
|
+
lastSuccessAt: number$1().int().nullable()
|
|
142107
|
+
});
|
|
142108
|
+
var TargetDeliverySchema = object({
|
|
142109
|
+
targetId: string(),
|
|
142110
|
+
ok: boolean(),
|
|
142111
|
+
error: string().optional()
|
|
142112
|
+
});
|
|
142113
|
+
var WorkerLineSchema = union([object({
|
|
142114
|
+
id: string(),
|
|
142115
|
+
ok: literal(true),
|
|
142116
|
+
result: unknown().optional()
|
|
142117
|
+
}), object({
|
|
142118
|
+
id: string(),
|
|
142119
|
+
ok: literal(false),
|
|
142120
|
+
error: string(),
|
|
142121
|
+
/** A typed reason the worker names; `rate-limited` is Amazon's 429. */
|
|
142122
|
+
code: string().optional()
|
|
142123
|
+
})]);
|
|
142124
|
+
var RATE_LIMITED_CODE = "rate-limited";
|
|
142125
|
+
/** Amazon answered 429. Never an empty answer: the caller backs off and keeps what it had. */
|
|
142126
|
+
var RateLimitedError = class extends Error {
|
|
142127
|
+
code = RATE_LIMITED_CODE;
|
|
142128
|
+
constructor(message) {
|
|
142129
|
+
super(message);
|
|
142130
|
+
this.name = "RateLimitedError";
|
|
142131
|
+
}
|
|
142132
|
+
};
|
|
142133
|
+
function requestLine(id, request) {
|
|
142134
|
+
return `${JSON.stringify({
|
|
142135
|
+
id,
|
|
142136
|
+
...request
|
|
142137
|
+
})}\n`;
|
|
142138
|
+
}
|
|
142139
|
+
function parseLoginResult(result) {
|
|
142140
|
+
const parsed = object({
|
|
142141
|
+
status: LoginStatusSchema,
|
|
142142
|
+
error: string().optional()
|
|
142143
|
+
}).safeParse(result);
|
|
142144
|
+
if (!parsed.success) return {
|
|
142145
|
+
status: "failed",
|
|
142146
|
+
error: "worker returned an unexpected login result"
|
|
142147
|
+
};
|
|
142148
|
+
return parsed.data;
|
|
142149
|
+
}
|
|
142150
|
+
function parseDevices(result) {
|
|
142151
|
+
const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
|
|
142152
|
+
if (!parsed.success) throw new Error("worker returned an unexpected device list");
|
|
142153
|
+
return parsed.data.devices;
|
|
142154
|
+
}
|
|
142155
|
+
function parseDeliveries(result) {
|
|
142156
|
+
const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
|
|
142157
|
+
if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
|
|
142158
|
+
return parsed.data.outcomes;
|
|
142159
|
+
}
|
|
142160
|
+
function parseSessionStatus(result) {
|
|
142161
|
+
const parsed = SessionStatusSchema.safeParse(result);
|
|
142162
|
+
if (!parsed.success) throw new Error("worker returned an unexpected session status");
|
|
142163
|
+
return parsed.data;
|
|
142164
|
+
}
|
|
142165
|
+
/**
|
|
142166
|
+
* The raw `playerInfo` of `/api/np/player`. Amazon omits what it does not know,
|
|
142167
|
+
* so every field is nullable; the mapping to the media-player cap turns a missing
|
|
142168
|
+
* value into null, never into 0 (D393).
|
|
142169
|
+
*/
|
|
142170
|
+
var PlayerInfoSchema = object({
|
|
142171
|
+
state: string().nullish(),
|
|
142172
|
+
volume: object({
|
|
142173
|
+
volume: number$1().nullish(),
|
|
142174
|
+
muted: boolean().nullish()
|
|
142175
|
+
}).nullish(),
|
|
142176
|
+
infoText: object({
|
|
142177
|
+
title: string().nullish(),
|
|
142178
|
+
subText1: string().nullish(),
|
|
142179
|
+
subText2: string().nullish()
|
|
142180
|
+
}).nullish(),
|
|
142181
|
+
mainArt: object({ url: string().nullish() }).nullish(),
|
|
142182
|
+
progress: object({
|
|
142183
|
+
mediaLength: number$1().nullish(),
|
|
142184
|
+
mediaProgress: number$1().nullish()
|
|
142185
|
+
}).nullish(),
|
|
142186
|
+
transport: object({
|
|
142187
|
+
shuffle: string().nullish(),
|
|
142188
|
+
repeat: string().nullish()
|
|
142189
|
+
}).nullish()
|
|
142190
|
+
});
|
|
142191
|
+
function parsePlayerState(result) {
|
|
142192
|
+
const parsed = object({ playerInfo: PlayerInfoSchema.nullish() }).safeParse(result);
|
|
142193
|
+
if (!parsed.success) throw new Error("worker returned an unexpected player state");
|
|
142194
|
+
return parsed.data.playerInfo ?? {};
|
|
142195
|
+
}
|
|
142196
|
+
var DndEntrySchema = object({
|
|
142197
|
+
serial: string().min(1),
|
|
142198
|
+
enabled: boolean()
|
|
142199
|
+
});
|
|
142200
|
+
function parseDndState(result) {
|
|
142201
|
+
const parsed = object({ devices: array(DndEntrySchema) }).safeParse(result);
|
|
142202
|
+
if (!parsed.success) throw new Error("worker returned an unexpected Do Not Disturb list");
|
|
142203
|
+
return new Map(parsed.data.devices.map((entry) => [entry.serial, entry.enabled]));
|
|
142204
|
+
}
|
|
142205
|
+
//#endregion
|
|
142206
|
+
//#region src/devices/alexa-echo-device.ts
|
|
142207
|
+
/**
|
|
142208
|
+
* An Echo is a media-player device. Identity only: the account (`brokerId`) and
|
|
142209
|
+
* the Echo's serial. The Echo's notification target is a separate record linked
|
|
142210
|
+
* by the same (brokerId, serial); nothing here sets or reads a notification.
|
|
142211
|
+
*/
|
|
142212
|
+
var alexaEchoDeviceSchema = object({
|
|
142213
|
+
brokerId: string().min(1).describe("Registry broker id"),
|
|
142214
|
+
serial: string().min(1).describe("The Echo serial number"),
|
|
142215
|
+
deviceType: string().default("").describe("Amazon device type id"),
|
|
142216
|
+
system: literal("alexa").optional(),
|
|
142217
|
+
integrationId: string().optional()
|
|
142218
|
+
});
|
|
142219
|
+
var COLD_DND = {
|
|
142220
|
+
enabled: null,
|
|
142221
|
+
lastChangedAt: 0
|
|
142222
|
+
};
|
|
142223
|
+
function unsupported(what) {
|
|
142224
|
+
throw new Error(`${what} is not available on an Echo`);
|
|
142225
|
+
}
|
|
142226
|
+
var AlexaEchoDevice = class extends BaseDevice {
|
|
142227
|
+
features = [
|
|
142228
|
+
DeviceFeature.MediaPlayerVolume,
|
|
142229
|
+
DeviceFeature.MediaPlayerNext,
|
|
142230
|
+
DeviceFeature.MediaPlayerPrevious
|
|
142231
|
+
];
|
|
142232
|
+
brokerId;
|
|
142233
|
+
serial;
|
|
142234
|
+
amazonDeviceType;
|
|
142235
|
+
timer = null;
|
|
142236
|
+
rereadTimer = null;
|
|
142237
|
+
polling = false;
|
|
142238
|
+
failureLogged = false;
|
|
142239
|
+
health = "ok";
|
|
142240
|
+
constructor(ctx) {
|
|
142241
|
+
const persisted = alexaEchoDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
142242
|
+
super(ctx, alexaEchoDeviceSchema, { type: DeviceType.MediaPlayer });
|
|
142243
|
+
this.brokerId = persisted.brokerId;
|
|
142244
|
+
this.serial = persisted.serial;
|
|
142245
|
+
this.amazonDeviceType = persisted.deviceType;
|
|
142246
|
+
}
|
|
142247
|
+
/** The outcome of the last read, for the broker status and the specs. */
|
|
142248
|
+
get pollHealth() {
|
|
142249
|
+
return this.health;
|
|
142250
|
+
}
|
|
142251
|
+
async onActivate() {
|
|
142252
|
+
await super.onActivate();
|
|
142253
|
+
this.registerMediaPlayer();
|
|
142254
|
+
this.registerDoNotDisturb();
|
|
142255
|
+
this.runtimeState.setCapState("media-player", emptyEchoStatus("off", 0));
|
|
142256
|
+
this.runtimeState.setCapState("do-not-disturb", COLD_DND);
|
|
142257
|
+
this.poll();
|
|
142258
|
+
this.timer = setInterval(() => void this.poll(), ECHO_POLL_MS);
|
|
142259
|
+
this.timer.unref();
|
|
142260
|
+
}
|
|
142261
|
+
async removeDevice() {
|
|
142262
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
142263
|
+
if (this.rereadTimer !== null) clearTimeout(this.rereadTimer);
|
|
142264
|
+
this.timer = null;
|
|
142265
|
+
this.rereadTimer = null;
|
|
142266
|
+
await super.removeDevice();
|
|
142267
|
+
}
|
|
142268
|
+
session() {
|
|
142269
|
+
const session = alexaEchoSessions.get(this.brokerId);
|
|
142270
|
+
if (session === null) throw new Error("the Amazon account of this Echo is not connected");
|
|
142271
|
+
return session;
|
|
142272
|
+
}
|
|
142273
|
+
/** One re-read shortly after a command, once Amazon has applied it. */
|
|
142274
|
+
rereadSoon() {
|
|
142275
|
+
if (this.rereadTimer !== null) clearTimeout(this.rereadTimer);
|
|
142276
|
+
this.rereadTimer = setTimeout(() => {
|
|
142277
|
+
this.rereadTimer = null;
|
|
142278
|
+
this.poll();
|
|
142279
|
+
}, ECHO_REREAD_MS);
|
|
142280
|
+
this.rereadTimer.unref();
|
|
142281
|
+
}
|
|
142282
|
+
/**
|
|
142283
|
+
* Read the player and Do Not Disturb. A failed read leaves both slices as they
|
|
142284
|
+
* were and marks the device degraded; a 429 is a pause, not a fault. Never throws.
|
|
142285
|
+
*/
|
|
142286
|
+
async poll() {
|
|
142287
|
+
if (this.polling) return;
|
|
142288
|
+
this.polling = true;
|
|
142289
|
+
try {
|
|
142290
|
+
await this.readOnce();
|
|
142291
|
+
} finally {
|
|
142292
|
+
this.polling = false;
|
|
142293
|
+
}
|
|
142294
|
+
}
|
|
142295
|
+
async readOnce() {
|
|
142296
|
+
const session = alexaEchoSessions.get(this.brokerId);
|
|
142297
|
+
if (session === null || session.state !== "ready") return;
|
|
142298
|
+
if (session.rateLimitedUntil !== null) {
|
|
142299
|
+
this.health = "rate-limited";
|
|
142300
|
+
return;
|
|
142301
|
+
}
|
|
142302
|
+
const listed = session.devices.find((device) => device.id === this.serial);
|
|
142303
|
+
if (listed !== void 0 && !listed.online) {
|
|
142304
|
+
this.runtimeState.setCapState("media-player", emptyEchoStatus("off", Date.now()));
|
|
142305
|
+
this.markOnline(false);
|
|
142306
|
+
this.health = "ok";
|
|
142307
|
+
return;
|
|
142308
|
+
}
|
|
142309
|
+
const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
|
|
142310
|
+
try {
|
|
142311
|
+
const info = await session.playerState(this.serial, deviceType);
|
|
142312
|
+
const now = Date.now();
|
|
142313
|
+
this.runtimeState.setCapState("media-player", mapPlayerInfoToStatus(info, {
|
|
142314
|
+
online: true,
|
|
142315
|
+
now
|
|
142316
|
+
}));
|
|
142317
|
+
this.markOnline(true);
|
|
142318
|
+
this.noteRecovered();
|
|
142319
|
+
} catch (err) {
|
|
142320
|
+
this.noteReadFailure(err);
|
|
142321
|
+
return;
|
|
142322
|
+
}
|
|
142323
|
+
await this.readDoNotDisturb();
|
|
142324
|
+
}
|
|
142325
|
+
async readDoNotDisturb() {
|
|
142326
|
+
try {
|
|
142327
|
+
const enabled = (await alexaEchoSessions.dnd(this.brokerId, Date.now())).get(this.serial);
|
|
142328
|
+
if (enabled === void 0) return;
|
|
142329
|
+
this.runtimeState.setCapState("do-not-disturb", {
|
|
142330
|
+
enabled,
|
|
142331
|
+
lastChangedAt: Date.now()
|
|
142332
|
+
});
|
|
142333
|
+
} catch (err) {
|
|
142334
|
+
this.noteReadFailure(err);
|
|
142335
|
+
}
|
|
142336
|
+
}
|
|
142337
|
+
noteRecovered() {
|
|
142338
|
+
if (this.failureLogged) this.ctx.logger.info("alexa echo: reads are working again", { tags: { deviceId: this.id } });
|
|
142339
|
+
this.failureLogged = false;
|
|
142340
|
+
this.health = "ok";
|
|
142341
|
+
}
|
|
142342
|
+
noteReadFailure(err) {
|
|
142343
|
+
if (err instanceof RateLimitedError) {
|
|
142344
|
+
this.health = "rate-limited";
|
|
142345
|
+
this.ctx.logger.info("alexa echo: amazon asked for a pause, keeping the last state", { tags: { deviceId: this.id } });
|
|
142346
|
+
return;
|
|
142347
|
+
}
|
|
142348
|
+
this.health = "degraded";
|
|
142349
|
+
if (this.failureLogged) return;
|
|
142350
|
+
this.failureLogged = true;
|
|
142351
|
+
this.ctx.logger.warn("alexa echo: could not read the player, keeping the last state", {
|
|
142352
|
+
tags: { deviceId: this.id },
|
|
142353
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
142354
|
+
});
|
|
142355
|
+
}
|
|
142356
|
+
async command(command) {
|
|
142357
|
+
await this.session().npCommand(this.serial, command);
|
|
142358
|
+
this.rereadSoon();
|
|
142359
|
+
}
|
|
142360
|
+
registerMediaPlayer() {
|
|
142361
|
+
this.ctx.registerNativeCap(mediaPlayerCapability, {
|
|
142362
|
+
getStatus: async () => this.runtimeState.getCapState("media-player") ?? emptyEchoStatus("off", 0),
|
|
142363
|
+
play: () => this.command("play"),
|
|
142364
|
+
pause: () => this.command("pause"),
|
|
142365
|
+
next: () => this.command("next"),
|
|
142366
|
+
previous: () => this.command("previous"),
|
|
142367
|
+
setVolume: async ({ volumeLevel }) => {
|
|
142368
|
+
await this.session().setVolume(this.serial, Math.round(volumeLevel));
|
|
142369
|
+
this.rereadSoon();
|
|
142370
|
+
},
|
|
142371
|
+
stop: async () => unsupported("stop"),
|
|
142372
|
+
seek: async () => unsupported("seek"),
|
|
142373
|
+
setMute: async () => unsupported("mute"),
|
|
142374
|
+
setShuffle: async () => unsupported("shuffle"),
|
|
142375
|
+
setRepeat: async () => unsupported("repeat"),
|
|
142376
|
+
selectSource: async () => unsupported("source selection"),
|
|
142377
|
+
playMedia: async () => unsupported("playing media")
|
|
142378
|
+
});
|
|
142379
|
+
}
|
|
142380
|
+
registerDoNotDisturb() {
|
|
142381
|
+
this.ctx.registerNativeCap(doNotDisturbCapability, {
|
|
142382
|
+
getStatus: async () => this.runtimeState.getCapState("do-not-disturb") ?? COLD_DND,
|
|
142383
|
+
setEnabled: async ({ enabled }) => {
|
|
142384
|
+
await this.session().setDnd(this.serial, enabled);
|
|
142385
|
+
this.runtimeState.setCapState("do-not-disturb", {
|
|
142386
|
+
enabled,
|
|
142387
|
+
lastChangedAt: Date.now()
|
|
142388
|
+
});
|
|
142389
|
+
}
|
|
142390
|
+
});
|
|
142391
|
+
}
|
|
142392
|
+
};
|
|
141197
142393
|
var alexaBrokersEntryKind = defineEntryKind("brokers", {
|
|
141198
142394
|
title: "Alexa import brokers",
|
|
141199
142395
|
schema: entryBody({
|
|
@@ -141502,78 +142698,14 @@ function scrubSecrets(text, secrets) {
|
|
|
141502
142698
|
return out;
|
|
141503
142699
|
}
|
|
141504
142700
|
//#endregion
|
|
141505
|
-
//#region src/speak/worker-protocol.ts
|
|
141506
|
-
/** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
|
|
141507
|
-
var EchoDeviceSchema = object({
|
|
141508
|
-
id: string().min(1),
|
|
141509
|
-
name: string(),
|
|
141510
|
-
family: string(),
|
|
141511
|
-
online: boolean(),
|
|
141512
|
-
kind: _enum(["device", "group"]),
|
|
141513
|
-
dnd: boolean(),
|
|
141514
|
-
deviceType: string(),
|
|
141515
|
-
/** The room the account files the device under, when Amazon names one. */
|
|
141516
|
-
room: string().optional()
|
|
141517
|
-
});
|
|
141518
|
-
var LoginStatusSchema = _enum(["ok", "failed"]);
|
|
141519
|
-
var SessionStatusSchema = object({
|
|
141520
|
-
loggedIn: boolean(),
|
|
141521
|
-
ageSec: number$1().int().nonnegative(),
|
|
141522
|
-
lastSuccessAt: number$1().int().nullable()
|
|
141523
|
-
});
|
|
141524
|
-
var TargetDeliverySchema = object({
|
|
141525
|
-
targetId: string(),
|
|
141526
|
-
ok: boolean(),
|
|
141527
|
-
error: string().optional()
|
|
141528
|
-
});
|
|
141529
|
-
var WorkerLineSchema = union([object({
|
|
141530
|
-
id: string(),
|
|
141531
|
-
ok: literal(true),
|
|
141532
|
-
result: unknown().optional()
|
|
141533
|
-
}), object({
|
|
141534
|
-
id: string(),
|
|
141535
|
-
ok: literal(false),
|
|
141536
|
-
error: string()
|
|
141537
|
-
})]);
|
|
141538
|
-
function requestLine(id, request) {
|
|
141539
|
-
return `${JSON.stringify({
|
|
141540
|
-
id,
|
|
141541
|
-
...request
|
|
141542
|
-
})}\n`;
|
|
141543
|
-
}
|
|
141544
|
-
function parseLoginResult(result) {
|
|
141545
|
-
const parsed = object({
|
|
141546
|
-
status: LoginStatusSchema,
|
|
141547
|
-
error: string().optional()
|
|
141548
|
-
}).safeParse(result);
|
|
141549
|
-
if (!parsed.success) return {
|
|
141550
|
-
status: "failed",
|
|
141551
|
-
error: "worker returned an unexpected login result"
|
|
141552
|
-
};
|
|
141553
|
-
return parsed.data;
|
|
141554
|
-
}
|
|
141555
|
-
function parseDevices(result) {
|
|
141556
|
-
const parsed = object({ devices: array(EchoDeviceSchema) }).safeParse(result);
|
|
141557
|
-
if (!parsed.success) throw new Error("worker returned an unexpected device list");
|
|
141558
|
-
return parsed.data.devices;
|
|
141559
|
-
}
|
|
141560
|
-
function parseDeliveries(result) {
|
|
141561
|
-
const parsed = object({ outcomes: array(TargetDeliverySchema) }).safeParse(result);
|
|
141562
|
-
if (!parsed.success) throw new Error("worker returned an unexpected delivery list");
|
|
141563
|
-
return parsed.data.outcomes;
|
|
141564
|
-
}
|
|
141565
|
-
function parseSessionStatus(result) {
|
|
141566
|
-
const parsed = SessionStatusSchema.safeParse(result);
|
|
141567
|
-
if (!parsed.success) throw new Error("worker returned an unexpected session status");
|
|
141568
|
-
return parsed.data;
|
|
141569
|
-
}
|
|
141570
|
-
//#endregion
|
|
141571
142701
|
//#region src/speak/broker-echo.ts
|
|
141572
142702
|
var BrokerEcho = class {
|
|
141573
142703
|
deps;
|
|
141574
142704
|
currentState = "starting";
|
|
141575
142705
|
currentError = null;
|
|
141576
142706
|
currentDevices = [];
|
|
142707
|
+
backoffMs = 0;
|
|
142708
|
+
limitedUntil = 0;
|
|
141577
142709
|
constructor(deps) {
|
|
141578
142710
|
this.deps = deps;
|
|
141579
142711
|
}
|
|
@@ -141592,6 +142724,13 @@ var BrokerEcho = class {
|
|
|
141592
142724
|
get devices() {
|
|
141593
142725
|
return this.currentDevices;
|
|
141594
142726
|
}
|
|
142727
|
+
/** Ms epoch until which Amazon asked us to wait (429), or null when the account is free. */
|
|
142728
|
+
get rateLimitedUntil() {
|
|
142729
|
+
return this.limitedUntil > this.nowMs() ? this.limitedUntil : null;
|
|
142730
|
+
}
|
|
142731
|
+
nowMs() {
|
|
142732
|
+
return (this.deps.now ?? Date.now)();
|
|
142733
|
+
}
|
|
141595
142734
|
stop() {
|
|
141596
142735
|
this.deps.worker.stop();
|
|
141597
142736
|
}
|
|
@@ -141682,6 +142821,69 @@ var BrokerEcho = class {
|
|
|
141682
142821
|
text
|
|
141683
142822
|
}));
|
|
141684
142823
|
}
|
|
142824
|
+
/**
|
|
142825
|
+
* Run one worker call. A 429 starts (or doubles) the account's back-off and is
|
|
142826
|
+
* rethrown as is; any answer ends it. Rate limiting is a status, not a failure.
|
|
142827
|
+
*/
|
|
142828
|
+
async limited(call) {
|
|
142829
|
+
try {
|
|
142830
|
+
const result = await call();
|
|
142831
|
+
this.backoffMs = 0;
|
|
142832
|
+
this.limitedUntil = 0;
|
|
142833
|
+
return result;
|
|
142834
|
+
} catch (err) {
|
|
142835
|
+
if (err instanceof RateLimitedError) {
|
|
142836
|
+
this.backoffMs = this.backoffMs === 0 ? ECHO_RATE_BACKOFF_BASE_MS : Math.min(this.backoffMs * 2, ECHO_RATE_BACKOFF_MAX_MS);
|
|
142837
|
+
this.limitedUntil = this.nowMs() + this.backoffMs;
|
|
142838
|
+
this.deps.logger.info("alexa echo: amazon asked for a pause (429)", {
|
|
142839
|
+
tags: { brokerId: this.deps.brokerId },
|
|
142840
|
+
meta: { backoffMs: this.backoffMs }
|
|
142841
|
+
});
|
|
142842
|
+
}
|
|
142843
|
+
throw err;
|
|
142844
|
+
}
|
|
142845
|
+
}
|
|
142846
|
+
/** The raw player state of one Echo. Throws on any failure, a 429 included: never an empty answer. */
|
|
142847
|
+
async playerState(serial, deviceType) {
|
|
142848
|
+
await this.requireReady();
|
|
142849
|
+
return this.limited(async () => parsePlayerState(await this.deps.worker.request({
|
|
142850
|
+
op: "player_state",
|
|
142851
|
+
serial,
|
|
142852
|
+
deviceType
|
|
142853
|
+
})));
|
|
142854
|
+
}
|
|
142855
|
+
async npCommand(serial, command) {
|
|
142856
|
+
await this.requireReady();
|
|
142857
|
+
await this.limited(() => this.deps.worker.request({
|
|
142858
|
+
op: "np_command",
|
|
142859
|
+
serial,
|
|
142860
|
+
command
|
|
142861
|
+
}));
|
|
142862
|
+
}
|
|
142863
|
+
async setVolume(serial, level) {
|
|
142864
|
+
await this.requireReady();
|
|
142865
|
+
await this.limited(() => this.deps.worker.request({
|
|
142866
|
+
op: "set_volume",
|
|
142867
|
+
serial,
|
|
142868
|
+
level
|
|
142869
|
+
}));
|
|
142870
|
+
}
|
|
142871
|
+
/** Do Not Disturb of every device of the account, by serial, in one call. */
|
|
142872
|
+
async dndState() {
|
|
142873
|
+
await this.requireReady();
|
|
142874
|
+
return this.limited(async () => parseDndState(await this.deps.worker.request({ op: "dnd_state" })));
|
|
142875
|
+
}
|
|
142876
|
+
async setDnd(serial, enabled) {
|
|
142877
|
+
await this.requireReady();
|
|
142878
|
+
await this.limited(() => this.deps.worker.request({
|
|
142879
|
+
op: "set_dnd",
|
|
142880
|
+
serial,
|
|
142881
|
+
enabled
|
|
142882
|
+
}));
|
|
142883
|
+
}
|
|
142884
|
+
async requireReady() {
|
|
142885
|
+
if (!await this.ensureReady()) throw new Error(this.notReadyReason());
|
|
142886
|
+
}
|
|
141685
142887
|
async session() {
|
|
141686
142888
|
if (this.deps.worker.failed) return {
|
|
141687
142889
|
loggedIn: false,
|
|
@@ -142404,6 +143606,7 @@ function startAlexaWorker(deps) {
|
|
|
142404
143606
|
const current = pending;
|
|
142405
143607
|
pending = null;
|
|
142406
143608
|
if (message.ok) current.resolve(message.result);
|
|
143609
|
+
else if (message.code === "rate-limited") current.reject(new RateLimitedError(scrub(message.error)));
|
|
142407
143610
|
else current.reject(new Error(scrub(message.error)));
|
|
142408
143611
|
pump();
|
|
142409
143612
|
}
|
|
@@ -142557,6 +143760,10 @@ var AlexaSpeakController = class {
|
|
|
142557
143760
|
this.queue = this.queue.then(() => this.reconcile()).catch(() => void 0);
|
|
142558
143761
|
return this.queue;
|
|
142559
143762
|
}
|
|
143763
|
+
/** The live session of one account, for the Echo devices that poll it. */
|
|
143764
|
+
echoFor(brokerId) {
|
|
143765
|
+
return this.echoes.get(brokerId) ?? null;
|
|
143766
|
+
}
|
|
142560
143767
|
brokerViews() {
|
|
142561
143768
|
return this.host.listBrokers().map((ref) => {
|
|
142562
143769
|
const echo = this.echoes.get(ref.id);
|
|
@@ -142564,7 +143771,8 @@ var AlexaSpeakController = class {
|
|
|
142564
143771
|
id: ref.id,
|
|
142565
143772
|
name: ref.name,
|
|
142566
143773
|
ready: echo?.state === "ready",
|
|
142567
|
-
devices: echo?.devices ?? []
|
|
143774
|
+
devices: echo?.devices ?? [],
|
|
143775
|
+
rateLimitedUntil: echo?.rateLimitedUntil ?? null
|
|
142568
143776
|
};
|
|
142569
143777
|
});
|
|
142570
143778
|
}
|
|
@@ -142608,7 +143816,8 @@ var AlexaSpeakController = class {
|
|
|
142608
143816
|
worker: this.makeWorker(ref.id, pythonPath),
|
|
142609
143817
|
readSeed: () => this.host.readSeed(ref.id),
|
|
142610
143818
|
sessions: this.host.sessions,
|
|
142611
|
-
logger: this.host.logger
|
|
143819
|
+
logger: this.host.logger,
|
|
143820
|
+
now: this.now
|
|
142612
143821
|
}));
|
|
142613
143822
|
await Promise.all(refs.map(async (ref) => {
|
|
142614
143823
|
const echo = this.echoes.get(ref.id);
|
|
@@ -142767,7 +143976,10 @@ var ALEXA_DEVICE_TYPES = [
|
|
|
142767
143976
|
var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
142768
143977
|
addonId = "import-alexa";
|
|
142769
143978
|
providerName = "Alexa";
|
|
142770
|
-
deviceClasses =
|
|
143979
|
+
deviceClasses = {
|
|
143980
|
+
...Object.fromEntries(ALEXA_DEVICE_TYPES.map((t) => [t, AlexaDevice])),
|
|
143981
|
+
[DeviceType.MediaPlayer]: AlexaEchoDevice
|
|
143982
|
+
};
|
|
142771
143983
|
registry = null;
|
|
142772
143984
|
proxyLogin = null;
|
|
142773
143985
|
deviceCodeLogin = null;
|
|
@@ -142854,6 +144066,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
142854
144066
|
ensurePython: () => this.ctx.deps.ensurePython()
|
|
142855
144067
|
});
|
|
142856
144068
|
this.speak = speak;
|
|
144069
|
+
alexaEchoSessions.setResolver((brokerId) => speak.echoFor(brokerId));
|
|
142857
144070
|
speak.start().catch((err) => {
|
|
142858
144071
|
this.ctx.logger.warn("alexa speak: worker did not start", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
142859
144072
|
});
|
|
@@ -142958,6 +144171,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
142958
144171
|
async onShutdown() {
|
|
142959
144172
|
this.speak?.stop();
|
|
142960
144173
|
this.speak = null;
|
|
144174
|
+
alexaEchoSessions.clear();
|
|
142961
144175
|
alexaCommands.clear();
|
|
142962
144176
|
try {
|
|
142963
144177
|
await this.proxyLogin?.dispose();
|
|
@@ -142981,7 +144195,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
142981
144195
|
}
|
|
142982
144196
|
/** One broker = one account; the appliance id distinguishes devices within it. */
|
|
142983
144197
|
generateStableId(_type, config) {
|
|
142984
|
-
|
|
144198
|
+
const brokerId = String(config?.["brokerId"] ?? "unknown");
|
|
144199
|
+
const serial = config?.["serial"];
|
|
144200
|
+
if (typeof serial === "string") return `alexa:${brokerId}:echo:${serial}`;
|
|
144201
|
+
return `alexa:${brokerId}:${String(config?.["applianceId"] ?? "unknown")}`;
|
|
142985
144202
|
}
|
|
142986
144203
|
/**
|
|
142987
144204
|
* Re-derive every adopted Alexa device's `capabilities[]` from the live
|
|
@@ -143144,7 +144361,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
143144
144361
|
id: b.id,
|
|
143145
144362
|
name: b.name
|
|
143146
144363
|
})),
|
|
143147
|
-
has: (id) => this.requireRegistry().get(id) !== null
|
|
144364
|
+
has: (id) => this.requireRegistry().get(id) !== null,
|
|
144365
|
+
getEchoesForBroker: (id) => this.speak?.echoFor(id)?.devices ?? []
|
|
143148
144366
|
},
|
|
143149
144367
|
logger: this.ctx.logger,
|
|
143150
144368
|
getBrokerIdForIntegration: async (id) => {
|
|
@@ -143186,6 +144404,23 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
143186
144404
|
integrationId
|
|
143187
144405
|
})).id };
|
|
143188
144406
|
},
|
|
144407
|
+
adoptEcho: async ({ brokerId, echo, integrationId, name }) => {
|
|
144408
|
+
const devices = this.ctx.kernel.devices;
|
|
144409
|
+
if (!devices) throw new Error("alexa adopt: kernel.devices unavailable");
|
|
144410
|
+
const config = {
|
|
144411
|
+
brokerId,
|
|
144412
|
+
serial: echo.id,
|
|
144413
|
+
deviceType: echo.deviceType,
|
|
144414
|
+
system: "alexa",
|
|
144415
|
+
integrationId,
|
|
144416
|
+
name
|
|
144417
|
+
};
|
|
144418
|
+
return { deviceId: (await devices.create(this.generateStableId(DeviceType.MediaPlayer, config), AlexaEchoDevice, config, null, {
|
|
144419
|
+
type: DeviceType.MediaPlayer,
|
|
144420
|
+
name,
|
|
144421
|
+
integrationId
|
|
144422
|
+
})).id };
|
|
144423
|
+
},
|
|
143189
144424
|
removeDevice: async (id) => {
|
|
143190
144425
|
await this.ctx.kernel.devices?.remove(id);
|
|
143191
144426
|
},
|