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