@camstack/addon-import-alexa 0.2.182 → 0.2.184
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 +1200 -349
- package/dist/addon.mjs +1200 -349
- package/package.json +6 -2
package/dist/addon.mjs
CHANGED
|
@@ -6042,7 +6042,7 @@ function number(params) {
|
|
|
6042
6042
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6043
6043
|
}
|
|
6044
6044
|
//#endregion
|
|
6045
|
-
//#region ../types/dist/sleep-
|
|
6045
|
+
//#region ../types/dist/sleep-BaWhXZFm.mjs
|
|
6046
6046
|
/**
|
|
6047
6047
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6048
6048
|
* window to float samples (D455).
|
|
@@ -6190,9 +6190,16 @@ function isSecretMarker(value) {
|
|
|
6190
6190
|
const ref = value["$secret"];
|
|
6191
6191
|
return typeof ref === "string" && ref.length > 0;
|
|
6192
6192
|
}
|
|
6193
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6194
|
+
function isSecretMarkerLike(value) {
|
|
6195
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6196
|
+
}
|
|
6193
6197
|
function isSecretClear(value) {
|
|
6194
6198
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6195
6199
|
}
|
|
6200
|
+
function isSecretFieldView(value) {
|
|
6201
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6202
|
+
}
|
|
6196
6203
|
/**
|
|
6197
6204
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6198
6205
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6842,11 +6849,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6842
6849
|
}
|
|
6843
6850
|
return out;
|
|
6844
6851
|
}
|
|
6852
|
+
/**
|
|
6853
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6854
|
+
*
|
|
6855
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6856
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6857
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6858
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6859
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6860
|
+
* copied onto another ref does not decrypt.
|
|
6861
|
+
*
|
|
6862
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6863
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6864
|
+
*
|
|
6865
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6866
|
+
*/
|
|
6867
|
+
/** The URI scheme of every keychain ref. */
|
|
6868
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6845
6869
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6846
6870
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6847
6871
|
function globalSecretScope() {
|
|
6848
6872
|
return GLOBAL_SCOPE;
|
|
6849
6873
|
}
|
|
6874
|
+
/**
|
|
6875
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6876
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6877
|
+
* addon running forked.
|
|
6878
|
+
*/
|
|
6879
|
+
function normalizeSecretOwner(addonId) {
|
|
6880
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6881
|
+
}
|
|
6850
6882
|
function encodeSegment(value) {
|
|
6851
6883
|
return encodeURIComponent(value);
|
|
6852
6884
|
}
|
|
@@ -6857,6 +6889,16 @@ function decodeSegment(value) {
|
|
|
6857
6889
|
return null;
|
|
6858
6890
|
}
|
|
6859
6891
|
}
|
|
6892
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6893
|
+
function formatSecretScope(scope) {
|
|
6894
|
+
switch (scope.kind) {
|
|
6895
|
+
case "global": return "global";
|
|
6896
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6897
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6898
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6899
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6860
6902
|
/**
|
|
6861
6903
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6862
6904
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6865,6 +6907,13 @@ function decodeSegment(value) {
|
|
|
6865
6907
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6866
6908
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6867
6909
|
}
|
|
6910
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6911
|
+
function buildSecretRef(parts) {
|
|
6912
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6913
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6914
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6915
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6916
|
+
}
|
|
6868
6917
|
function parseScope(segments) {
|
|
6869
6918
|
switch (segments[0]) {
|
|
6870
6919
|
case "global": return {
|
|
@@ -8822,11 +8871,17 @@ var RECORD_BATCH_MAX = 1e3;
|
|
|
8822
8871
|
var RECORD_QUERY_MAX = 1e3;
|
|
8823
8872
|
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8824
8873
|
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8874
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8875
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8825
8876
|
/**
|
|
8826
8877
|
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8827
8878
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8828
8879
|
* (`auth.accessToken`, `headers[].value`).
|
|
8829
8880
|
*
|
|
8881
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8882
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8883
|
+
* key releases exactly that key's secret.
|
|
8884
|
+
*
|
|
8830
8885
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8831
8886
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8832
8887
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8839,11 +8894,13 @@ function parseSecretPath(path) {
|
|
|
8839
8894
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8840
8895
|
return path.split(".").map((raw) => {
|
|
8841
8896
|
const each = raw.endsWith("[]");
|
|
8842
|
-
const
|
|
8897
|
+
const map = raw.endsWith("{}");
|
|
8898
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8843
8899
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8844
8900
|
return {
|
|
8845
8901
|
key,
|
|
8846
|
-
each
|
|
8902
|
+
each,
|
|
8903
|
+
map
|
|
8847
8904
|
};
|
|
8848
8905
|
});
|
|
8849
8906
|
}
|
|
@@ -8858,6 +8915,128 @@ function secretArrayPrefixes(path) {
|
|
|
8858
8915
|
}
|
|
8859
8916
|
return prefixes;
|
|
8860
8917
|
}
|
|
8918
|
+
function isRecord$4$1(value) {
|
|
8919
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8920
|
+
}
|
|
8921
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8922
|
+
const id = isRecord$4$1(element) ? element[idKey] : void 0;
|
|
8923
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
8924
|
+
return id;
|
|
8925
|
+
}
|
|
8926
|
+
/**
|
|
8927
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8928
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8929
|
+
* result is a new body, `data` is untouched.
|
|
8930
|
+
*/
|
|
8931
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8932
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8933
|
+
if (!isRecord$4$1(node)) return node;
|
|
8934
|
+
const segment = segments[index];
|
|
8935
|
+
if (segment === void 0) return node;
|
|
8936
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8937
|
+
const child = node[segment.key];
|
|
8938
|
+
const keys = [...walkedKeys, segment.key];
|
|
8939
|
+
const last = index === segments.length - 1;
|
|
8940
|
+
if (segment.each) {
|
|
8941
|
+
if (!Array.isArray(child)) return node;
|
|
8942
|
+
const prefix = keys.join(".");
|
|
8943
|
+
const idKey = arrayKeys[prefix];
|
|
8944
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8945
|
+
const mapped = child.map((element) => {
|
|
8946
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8947
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8948
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8949
|
+
});
|
|
8950
|
+
return {
|
|
8951
|
+
...node,
|
|
8952
|
+
[segment.key]: mapped
|
|
8953
|
+
};
|
|
8954
|
+
}
|
|
8955
|
+
if (segment.map) {
|
|
8956
|
+
if (!isRecord$4$1(child)) return node;
|
|
8957
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8958
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8959
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8960
|
+
return [mapKey, visit({
|
|
8961
|
+
declared,
|
|
8962
|
+
concrete,
|
|
8963
|
+
value
|
|
8964
|
+
})];
|
|
8965
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8966
|
+
return {
|
|
8967
|
+
...node,
|
|
8968
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8971
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8972
|
+
if (last) {
|
|
8973
|
+
const replacement = visit({
|
|
8974
|
+
declared,
|
|
8975
|
+
concrete,
|
|
8976
|
+
value: child
|
|
8977
|
+
});
|
|
8978
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8979
|
+
return {
|
|
8980
|
+
...node,
|
|
8981
|
+
[segment.key]: replacement
|
|
8982
|
+
};
|
|
8983
|
+
}
|
|
8984
|
+
return {
|
|
8985
|
+
...node,
|
|
8986
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8987
|
+
};
|
|
8988
|
+
}
|
|
8989
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8990
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8991
|
+
let current = data;
|
|
8992
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8993
|
+
return current;
|
|
8994
|
+
}
|
|
8995
|
+
/**
|
|
8996
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8997
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8998
|
+
* already holds during an adoption.
|
|
8999
|
+
*/
|
|
9000
|
+
function recordSecretCollection(kind) {
|
|
9001
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
9002
|
+
}
|
|
9003
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
9004
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
9005
|
+
return buildSecretRef({
|
|
9006
|
+
owner,
|
|
9007
|
+
scope: {
|
|
9008
|
+
kind: "row",
|
|
9009
|
+
collection: recordSecretCollection(kind),
|
|
9010
|
+
rowId
|
|
9011
|
+
},
|
|
9012
|
+
fieldPath: concretePath
|
|
9013
|
+
});
|
|
9014
|
+
}
|
|
9015
|
+
/**
|
|
9016
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
9017
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
9018
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
9019
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
9020
|
+
* stored secret (the records door reads a view as KEEP).
|
|
9021
|
+
*
|
|
9022
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
9023
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
9024
|
+
*/
|
|
9025
|
+
function isRecord$3$1(value) {
|
|
9026
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9027
|
+
}
|
|
9028
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
9029
|
+
if (decl.secretPaths.length === 0) return data;
|
|
9030
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
9031
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
9032
|
+
return {
|
|
9033
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
9034
|
+
v: 1,
|
|
9035
|
+
set: true
|
|
9036
|
+
};
|
|
9037
|
+
});
|
|
9038
|
+
return isRecord$3$1(shown) ? shown : data;
|
|
9039
|
+
}
|
|
8861
9040
|
/**
|
|
8862
9041
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8863
9042
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8930,7 +9109,8 @@ function isZodSecret(schema) {
|
|
|
8930
9109
|
}
|
|
8931
9110
|
/**
|
|
8932
9111
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
8933
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9112
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9113
|
+
* map" (`applianceSecrets{}`). A declared
|
|
8934
9114
|
* object or array is reported as one path and not descended into — everything
|
|
8935
9115
|
* under it is secret.
|
|
8936
9116
|
*/
|
|
@@ -8941,6 +9121,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
8941
9121
|
const def = defOf(layer);
|
|
8942
9122
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
8943
9123
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9124
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
8944
9125
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
8945
9126
|
else if (def.type === "intersection") {
|
|
8946
9127
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -9080,7 +9261,7 @@ function defineRecordKinds(specs) {
|
|
|
9080
9261
|
retention: spec.retention,
|
|
9081
9262
|
deviceRemoval: spec.deviceRemoval,
|
|
9082
9263
|
maxDataBytes: spec.maxDataBytes,
|
|
9083
|
-
secretPaths: [...secretPaths],
|
|
9264
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9084
9265
|
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9085
9266
|
transactFamily: spec.transactFamily,
|
|
9086
9267
|
entry: spec.entry === void 0 ? void 0 : {
|
|
@@ -9125,6 +9306,7 @@ function defineEntryKind(name, spec) {
|
|
|
9125
9306
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9126
9307
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9127
9308
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9309
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9128
9310
|
entry: {
|
|
9129
9311
|
title: spec.title,
|
|
9130
9312
|
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
@@ -11790,7 +11972,7 @@ object({
|
|
|
11790
11972
|
params: record(string(), union([string(), number$1()])).optional()
|
|
11791
11973
|
});
|
|
11792
11974
|
//#endregion
|
|
11793
|
-
//#region ../types/dist/composition-
|
|
11975
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11794
11976
|
/**
|
|
11795
11977
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11796
11978
|
*
|
|
@@ -12553,38 +12735,6 @@ function validateExpressionSource(src) {
|
|
|
12553
12735
|
return null;
|
|
12554
12736
|
}
|
|
12555
12737
|
/**
|
|
12556
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12557
|
-
*
|
|
12558
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12559
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12560
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12561
|
-
*
|
|
12562
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12563
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12564
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12565
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12566
|
-
* can move later without moving the others.
|
|
12567
|
-
*/
|
|
12568
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12569
|
-
var APPLIANCE_KINDS = [
|
|
12570
|
-
"washer",
|
|
12571
|
-
"dryer",
|
|
12572
|
-
"dishwasher"
|
|
12573
|
-
];
|
|
12574
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12575
|
-
/**
|
|
12576
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12577
|
-
*
|
|
12578
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12579
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12580
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12581
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12582
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12583
|
-
* the smallest radiator.
|
|
12584
|
-
*/
|
|
12585
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12586
|
-
DeviceType.Heater;
|
|
12587
|
-
/**
|
|
12588
12738
|
* Statistics and integrals over a history read (D944).
|
|
12589
12739
|
*
|
|
12590
12740
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12728,34 +12878,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12728
12878
|
/** Actuator groups only. Readings leave this unset. */
|
|
12729
12879
|
active: ActiveCriterionSchema.optional()
|
|
12730
12880
|
}),
|
|
12731
|
-
object({
|
|
12732
|
-
kind: literal("appliance"),
|
|
12733
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12734
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12735
|
-
thresholdWatts: number$1().positive(),
|
|
12736
|
-
startDwellMs: number$1().int().nonnegative(),
|
|
12737
|
-
stopDwellMs: number$1().int().nonnegative(),
|
|
12738
|
-
member: HelperMemberSchema,
|
|
12739
|
-
/**
|
|
12740
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12741
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12742
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12743
|
-
*/
|
|
12744
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12745
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12746
|
-
offSeconds: number$1().optional()
|
|
12747
|
-
}),
|
|
12748
|
-
object({
|
|
12749
|
-
kind: literal("heater"),
|
|
12750
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12751
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12752
|
-
plug: HelperMemberSchema,
|
|
12753
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12754
|
-
meter: HelperMemberSchema.optional(),
|
|
12755
|
-
thresholdWatts: number$1().positive(),
|
|
12756
|
-
startDwellMs: number$1().int().nonnegative(),
|
|
12757
|
-
stopDwellMs: number$1().int().nonnegative()
|
|
12758
|
-
}),
|
|
12759
12881
|
object({
|
|
12760
12882
|
kind: literal("statistic"),
|
|
12761
12883
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -13314,6 +13436,205 @@ function buildCapturingReply(envelope) {
|
|
|
13314
13436
|
};
|
|
13315
13437
|
return wrapper;
|
|
13316
13438
|
}
|
|
13439
|
+
/**
|
|
13440
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13441
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13442
|
+
*
|
|
13443
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13444
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13445
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13446
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13447
|
+
*
|
|
13448
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13449
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13450
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13451
|
+
*/
|
|
13452
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13453
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13454
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13455
|
+
function topLevelSecretKeys(paths) {
|
|
13456
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13457
|
+
}
|
|
13458
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13459
|
+
function secretMapKeys(paths) {
|
|
13460
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13461
|
+
}
|
|
13462
|
+
/**
|
|
13463
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13464
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13465
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13466
|
+
*/
|
|
13467
|
+
function secretMapPatch(value) {
|
|
13468
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13469
|
+
const out = {};
|
|
13470
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13471
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13472
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13473
|
+
}
|
|
13474
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13475
|
+
function declaresEnabled(kind) {
|
|
13476
|
+
const json = toJSONSchema(kind.schema, {
|
|
13477
|
+
unrepresentable: "any",
|
|
13478
|
+
io: "input"
|
|
13479
|
+
});
|
|
13480
|
+
const properties = Reflect.get(json, "properties");
|
|
13481
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13482
|
+
}
|
|
13483
|
+
var RecordIntegrationInstances = class {
|
|
13484
|
+
records;
|
|
13485
|
+
kind;
|
|
13486
|
+
hooks;
|
|
13487
|
+
handle;
|
|
13488
|
+
secretKeys;
|
|
13489
|
+
secretMaps;
|
|
13490
|
+
nameField;
|
|
13491
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13492
|
+
registryOwnsEnabled;
|
|
13493
|
+
constructor(records, kind, hooks) {
|
|
13494
|
+
this.records = records;
|
|
13495
|
+
this.kind = kind;
|
|
13496
|
+
this.hooks = hooks;
|
|
13497
|
+
this.handle = records.kind(kind);
|
|
13498
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13499
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13500
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13501
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13502
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13503
|
+
}
|
|
13504
|
+
/**
|
|
13505
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13506
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13507
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13508
|
+
*/
|
|
13509
|
+
async readResolved(id) {
|
|
13510
|
+
try {
|
|
13511
|
+
const record = await this.handle.getResolved(id);
|
|
13512
|
+
return record === null ? null : {
|
|
13513
|
+
id,
|
|
13514
|
+
body: record.data,
|
|
13515
|
+
enabled: record.data.enabled !== false,
|
|
13516
|
+
secrets: "resolved"
|
|
13517
|
+
};
|
|
13518
|
+
} catch (err) {
|
|
13519
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13520
|
+
const stored = await this.handle.get(id);
|
|
13521
|
+
if (stored === null) return null;
|
|
13522
|
+
return {
|
|
13523
|
+
id,
|
|
13524
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13525
|
+
enabled: stored.data.enabled !== false,
|
|
13526
|
+
secrets: "unavailable"
|
|
13527
|
+
};
|
|
13528
|
+
}
|
|
13529
|
+
}
|
|
13530
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13531
|
+
async getResolved(id) {
|
|
13532
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13533
|
+
}
|
|
13534
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13535
|
+
async listResolved() {
|
|
13536
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13537
|
+
const out = [];
|
|
13538
|
+
for (const record of page.records) {
|
|
13539
|
+
const read = await this.readResolved(record.id);
|
|
13540
|
+
if (read !== null) out.push(read);
|
|
13541
|
+
}
|
|
13542
|
+
return out;
|
|
13543
|
+
}
|
|
13544
|
+
viewOf(record) {
|
|
13545
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13546
|
+
const rawName = record.data[this.nameField];
|
|
13547
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13548
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13549
|
+
return {
|
|
13550
|
+
id: record.id,
|
|
13551
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13552
|
+
configured: this.hooks.configured(record.data),
|
|
13553
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13554
|
+
formValues: shown,
|
|
13555
|
+
...kind !== void 0 ? { kind } : {},
|
|
13556
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13557
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13558
|
+
};
|
|
13559
|
+
}
|
|
13560
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13561
|
+
...this.hooks.description,
|
|
13562
|
+
mintsId: true
|
|
13563
|
+
};
|
|
13564
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13565
|
+
listInstances = async () => {
|
|
13566
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13567
|
+
};
|
|
13568
|
+
getInstance = async ({ id }) => {
|
|
13569
|
+
const record = await this.handle.get(id);
|
|
13570
|
+
return record === null ? null : this.viewOf(record);
|
|
13571
|
+
};
|
|
13572
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13573
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13574
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13575
|
+
name,
|
|
13576
|
+
...kind !== void 0 ? { kind } : {}
|
|
13577
|
+
});
|
|
13578
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13579
|
+
await this.handle.put({
|
|
13580
|
+
id: minted,
|
|
13581
|
+
data: this.hooks.initialBody(name, kind)
|
|
13582
|
+
}, { ifRev: 0 });
|
|
13583
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13584
|
+
return { id: minted };
|
|
13585
|
+
};
|
|
13586
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13587
|
+
const patch = {};
|
|
13588
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13589
|
+
if (value === void 0) continue;
|
|
13590
|
+
if (this.secretMaps.has(key)) {
|
|
13591
|
+
const entries = secretMapPatch(value);
|
|
13592
|
+
if (entries !== null) patch[key] = entries;
|
|
13593
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13594
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13595
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13596
|
+
}
|
|
13597
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13598
|
+
await this.handle.patch(id, patch);
|
|
13599
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13600
|
+
return { ok: true };
|
|
13601
|
+
};
|
|
13602
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13603
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13604
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13605
|
+
await this.handle.patch(id, { enabled });
|
|
13606
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13607
|
+
return { ok: true };
|
|
13608
|
+
};
|
|
13609
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13610
|
+
deleteInstance = async ({ id }) => {
|
|
13611
|
+
await this.handle.delete(id);
|
|
13612
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13613
|
+
return { ok: true };
|
|
13614
|
+
};
|
|
13615
|
+
testInstance = async (input) => {
|
|
13616
|
+
if (this.hooks.test === void 0) return {
|
|
13617
|
+
success: false,
|
|
13618
|
+
status: "unsupported",
|
|
13619
|
+
error: "This integration declares no connection test"
|
|
13620
|
+
};
|
|
13621
|
+
return this.hooks.test(input);
|
|
13622
|
+
};
|
|
13623
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13624
|
+
toProvider() {
|
|
13625
|
+
return {
|
|
13626
|
+
describe: this.describe,
|
|
13627
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13628
|
+
listInstances: this.listInstances,
|
|
13629
|
+
getInstance: this.getInstance,
|
|
13630
|
+
createInstance: this.createInstance,
|
|
13631
|
+
updateInstance: this.updateInstance,
|
|
13632
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13633
|
+
deleteInstance: this.deleteInstance,
|
|
13634
|
+
testInstance: this.testInstance
|
|
13635
|
+
};
|
|
13636
|
+
}
|
|
13637
|
+
};
|
|
13317
13638
|
var EncodeProfileSchema = object({
|
|
13318
13639
|
video: object({
|
|
13319
13640
|
codec: _enum([
|
|
@@ -14382,6 +14703,58 @@ var MigrationOwnerStepSchema = object({
|
|
|
14382
14703
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
14383
14704
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
14384
14705
|
});
|
|
14706
|
+
function isPlain(value) {
|
|
14707
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14708
|
+
}
|
|
14709
|
+
async function flattenNestedRecords(ports, spec) {
|
|
14710
|
+
const ids = await ports.listNested(spec.legacyKey);
|
|
14711
|
+
if (ids.length === 0) return {
|
|
14712
|
+
outcome: "already-done",
|
|
14713
|
+
summary: { converted: 0 }
|
|
14714
|
+
};
|
|
14715
|
+
const rows = /* @__PURE__ */ new Map();
|
|
14716
|
+
for (const id of ids) try {
|
|
14717
|
+
rows.set(id, await ports.readResolved(id));
|
|
14718
|
+
} catch (err) {
|
|
14719
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
14720
|
+
throw new Error(`record '${id}': cannot be read with its secrets (${why}) - nothing moved`);
|
|
14721
|
+
}
|
|
14722
|
+
for (const [id, row] of rows) {
|
|
14723
|
+
const legacy = row[spec.legacyKey];
|
|
14724
|
+
if (!isPlain(legacy)) throw new Error(`record '${id}': '${spec.legacyKey}' is not an object - nothing moved`);
|
|
14725
|
+
const flat = spec.toFlat(legacy, row);
|
|
14726
|
+
await ports.patch(id, flat);
|
|
14727
|
+
const back = await ports.readResolved(id);
|
|
14728
|
+
const wrong = spec.flatSecretKeys.filter((key) => {
|
|
14729
|
+
const sent = flat[key];
|
|
14730
|
+
const got = back[key];
|
|
14731
|
+
return (typeof sent === "string" ? sent : "") !== (typeof got === "string" ? got : "");
|
|
14732
|
+
});
|
|
14733
|
+
if (wrong.length > 0) {
|
|
14734
|
+
await ports.patch(id, Object.fromEntries(Object.keys(flat).map((key) => [key, null])));
|
|
14735
|
+
throw new Error(`record '${id}': ${wrong.join(", ")} did not read back from its new home - the row is unchanged`);
|
|
14736
|
+
}
|
|
14737
|
+
await ports.patch(id, { [spec.legacyKey]: null });
|
|
14738
|
+
}
|
|
14739
|
+
return {
|
|
14740
|
+
outcome: "done",
|
|
14741
|
+
summary: { converted: rows.size }
|
|
14742
|
+
};
|
|
14743
|
+
}
|
|
14744
|
+
/** The ports over one kind's records handle. */
|
|
14745
|
+
function flattenPortsOver(handle) {
|
|
14746
|
+
return {
|
|
14747
|
+
listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
|
|
14748
|
+
readResolved: async (id) => {
|
|
14749
|
+
const row = await handle.getResolved(id);
|
|
14750
|
+
if (row === null) throw new Error("the record is gone");
|
|
14751
|
+
return row.data;
|
|
14752
|
+
},
|
|
14753
|
+
patch: async (id, patch) => {
|
|
14754
|
+
await handle.patch(id, patch);
|
|
14755
|
+
}
|
|
14756
|
+
};
|
|
14757
|
+
}
|
|
14385
14758
|
/**
|
|
14386
14759
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
14387
14760
|
* gets back (cloud training, D805+).
|
|
@@ -49912,117 +50285,6 @@ function actuator(input) {
|
|
|
49912
50285
|
};
|
|
49913
50286
|
}
|
|
49914
50287
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
49915
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
49916
|
-
"no-power-source",
|
|
49917
|
-
"switch-missing",
|
|
49918
|
-
"switch-offline",
|
|
49919
|
-
"switch-unknown",
|
|
49920
|
-
"restart-in-progress",
|
|
49921
|
-
"switch-call-failed"
|
|
49922
|
-
]);
|
|
49923
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
49924
|
-
accepted: literal(false),
|
|
49925
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
49926
|
-
/** One line for the operator, naming the switch. */
|
|
49927
|
-
message: string()
|
|
49928
|
-
})]);
|
|
49929
|
-
var PowerSourceInfoSchema = object({
|
|
49930
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
49931
|
-
linked: boolean(),
|
|
49932
|
-
switchDeviceId: number$1().nullable(),
|
|
49933
|
-
switchName: string().nullable(),
|
|
49934
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
49935
|
-
switchOn: boolean().nullable(),
|
|
49936
|
-
switchOnline: boolean().nullable(),
|
|
49937
|
-
restartInProgress: boolean(),
|
|
49938
|
-
offSeconds: number$1().int()
|
|
49939
|
-
});
|
|
49940
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
49941
|
-
var PowerSourceStateSchema = object({
|
|
49942
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
49943
|
-
switchOn: boolean().nullable(),
|
|
49944
|
-
offSeconds: number$1().int(),
|
|
49945
|
-
lastFetchedAt: number$1()
|
|
49946
|
-
});
|
|
49947
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
49948
|
-
DeviceType.Gateway,
|
|
49949
|
-
DeviceType.Hub,
|
|
49950
|
-
DeviceType.Camera,
|
|
49951
|
-
DeviceType.Sensor,
|
|
49952
|
-
DeviceType.MediaPlayer,
|
|
49953
|
-
DeviceType.Vacuum,
|
|
49954
|
-
DeviceType.Generic,
|
|
49955
|
-
DeviceType.Washer,
|
|
49956
|
-
DeviceType.Dryer,
|
|
49957
|
-
DeviceType.Dishwasher,
|
|
49958
|
-
DeviceType.PetFountain,
|
|
49959
|
-
DeviceType.Fan,
|
|
49960
|
-
DeviceType.Humidifier,
|
|
49961
|
-
DeviceType.Climate,
|
|
49962
|
-
DeviceType.WaterHeater
|
|
49963
|
-
];
|
|
49964
|
-
var powerSourceCustomize = {
|
|
49965
|
-
version: 1,
|
|
49966
|
-
targets: [{
|
|
49967
|
-
id: "switch",
|
|
49968
|
-
cardinality: "scalar",
|
|
49969
|
-
vocabulary: {
|
|
49970
|
-
cap: "power-source",
|
|
49971
|
-
stateField: "switchOn"
|
|
49972
|
-
},
|
|
49973
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49974
|
-
apply: "add",
|
|
49975
|
-
path: "switchOn",
|
|
49976
|
-
sources: [{
|
|
49977
|
-
kind: "switch",
|
|
49978
|
-
transforms: [{ kind: "identity" }]
|
|
49979
|
-
}],
|
|
49980
|
-
companions: [{
|
|
49981
|
-
kind: "constant",
|
|
49982
|
-
path: "offSeconds",
|
|
49983
|
-
value: 10,
|
|
49984
|
-
when: "add"
|
|
49985
|
-
}, {
|
|
49986
|
-
kind: "clock",
|
|
49987
|
-
path: "lastFetchedAt",
|
|
49988
|
-
when: "always"
|
|
49989
|
-
}],
|
|
49990
|
-
sourceReport: { status: "assignment-status" }
|
|
49991
|
-
}]
|
|
49992
|
-
};
|
|
49993
|
-
var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
|
|
49994
|
-
var powerSourceCapability = {
|
|
49995
|
-
name: "power-source",
|
|
49996
|
-
scope: "device",
|
|
49997
|
-
mode: "singleton",
|
|
49998
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49999
|
-
methods: {
|
|
50000
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
50001
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50002
|
-
kind: "mutation",
|
|
50003
|
-
auth: "admin"
|
|
50004
|
-
}),
|
|
50005
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50006
|
-
kind: "mutation",
|
|
50007
|
-
auth: "admin"
|
|
50008
|
-
}),
|
|
50009
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
50010
|
-
kind: "mutation",
|
|
50011
|
-
auth: "admin"
|
|
50012
|
-
})
|
|
50013
|
-
},
|
|
50014
|
-
runtimeState: PowerSourceStateSchema,
|
|
50015
|
-
customize: powerSourceCustomize,
|
|
50016
|
-
/**
|
|
50017
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
50018
|
-
* its next report; a restored value would outlive the reading it names.
|
|
50019
|
-
*
|
|
50020
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
50021
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
50022
|
-
*/
|
|
50023
|
-
durability: "session",
|
|
50024
|
-
history: "none"
|
|
50025
|
-
};
|
|
50026
50288
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
50027
50289
|
DeviceType.Camera;
|
|
50028
50290
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -50050,6 +50312,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
50050
50312
|
}).optional()
|
|
50051
50313
|
})) });
|
|
50052
50314
|
/**
|
|
50315
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
50316
|
+
* `z.infer`, they do not restate the shape.
|
|
50317
|
+
*/
|
|
50318
|
+
var OverrideSourceSchema = object({
|
|
50319
|
+
addonId: string().min(1),
|
|
50320
|
+
stableId: string().min(1),
|
|
50321
|
+
cap: string().min(1),
|
|
50322
|
+
fieldPath: string().min(1)
|
|
50323
|
+
});
|
|
50324
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
50325
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
50326
|
+
var ThresholdTransformDraftSchema = object({
|
|
50327
|
+
kind: literal("threshold"),
|
|
50328
|
+
comparison: _enum(["gte", "lte"]),
|
|
50329
|
+
threshold: number$1(),
|
|
50330
|
+
startDwellMs: number$1().nonnegative(),
|
|
50331
|
+
stopDwellMs: number$1().nonnegative()
|
|
50332
|
+
});
|
|
50333
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50334
|
+
IdentityTransformDraftSchema,
|
|
50335
|
+
InvertTransformDraftSchema,
|
|
50336
|
+
ThresholdTransformDraftSchema,
|
|
50337
|
+
object({
|
|
50338
|
+
kind: literal("linear"),
|
|
50339
|
+
scale: number$1(),
|
|
50340
|
+
offset: number$1(),
|
|
50341
|
+
clampMin: number$1().optional(),
|
|
50342
|
+
clampMax: number$1().optional()
|
|
50343
|
+
}),
|
|
50344
|
+
object({
|
|
50345
|
+
kind: literal("enum-map"),
|
|
50346
|
+
mapping: record(string(), string().nullable())
|
|
50347
|
+
}),
|
|
50348
|
+
object({ kind: literal("number") })
|
|
50349
|
+
]);
|
|
50350
|
+
var ScalarOverrideDraftSchema = object({
|
|
50351
|
+
kind: literal("scalar"),
|
|
50352
|
+
cap: string().min(1),
|
|
50353
|
+
targetId: string().min(1),
|
|
50354
|
+
source: OverrideSourceSchema,
|
|
50355
|
+
transform: CustomizeTransformDraftSchema
|
|
50356
|
+
});
|
|
50357
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
50358
|
+
kind: literal("keyedArray"),
|
|
50359
|
+
cap: string().min(1),
|
|
50360
|
+
targetId: string().min(1),
|
|
50361
|
+
key: string().min(1),
|
|
50362
|
+
label: string().min(1),
|
|
50363
|
+
fieldPath: string().min(1),
|
|
50364
|
+
source: OverrideSourceSchema,
|
|
50365
|
+
transform: CustomizeTransformDraftSchema
|
|
50366
|
+
});
|
|
50367
|
+
var PulseOverrideDraftSchema = object({
|
|
50368
|
+
kind: literal("pulse"),
|
|
50369
|
+
cap: string().min(1),
|
|
50370
|
+
targetId: string().min(1),
|
|
50371
|
+
enabled: boolean(),
|
|
50372
|
+
debounceMs: number$1().nonnegative(),
|
|
50373
|
+
edges: array(_enum([
|
|
50374
|
+
"rising",
|
|
50375
|
+
"button",
|
|
50376
|
+
"native-event"
|
|
50377
|
+
])).min(1).optional(),
|
|
50378
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
50379
|
+
});
|
|
50380
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
50381
|
+
"in-addition",
|
|
50382
|
+
"confirm",
|
|
50383
|
+
"replace"
|
|
50384
|
+
]).optional() });
|
|
50385
|
+
discriminatedUnion("kind", [
|
|
50386
|
+
ScalarOverrideDraftSchema,
|
|
50387
|
+
KeyedArrayOverrideDraftSchema,
|
|
50388
|
+
PulseOverrideDraftSchema,
|
|
50389
|
+
object({
|
|
50390
|
+
kind: literal("span"),
|
|
50391
|
+
cap: string().min(1),
|
|
50392
|
+
targetId: string().min(1),
|
|
50393
|
+
timeoutMs: number$1().nonnegative(),
|
|
50394
|
+
cooldownMs: number$1().nonnegative().optional(),
|
|
50395
|
+
sources: array(SpanSourceDraftSchema)
|
|
50396
|
+
})
|
|
50397
|
+
]);
|
|
50398
|
+
object({
|
|
50399
|
+
targetId: string().min(1),
|
|
50400
|
+
configured: boolean(),
|
|
50401
|
+
source: _enum(["native", "assigned"]),
|
|
50402
|
+
state: _enum([
|
|
50403
|
+
"active",
|
|
50404
|
+
"stale",
|
|
50405
|
+
"released",
|
|
50406
|
+
"unavailable"
|
|
50407
|
+
]),
|
|
50408
|
+
reason: string().nullable()
|
|
50409
|
+
});
|
|
50410
|
+
/**
|
|
50411
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
50412
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
50413
|
+
*
|
|
50414
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
50415
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
50416
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
50417
|
+
*/
|
|
50418
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
50419
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
50420
|
+
"high",
|
|
50421
|
+
"medium",
|
|
50422
|
+
"low"
|
|
50423
|
+
]);
|
|
50424
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
50425
|
+
"no-history",
|
|
50426
|
+
"span-too-short",
|
|
50427
|
+
"truncated",
|
|
50428
|
+
"no-idle-floor",
|
|
50429
|
+
"no-runs",
|
|
50430
|
+
"no-separation"
|
|
50431
|
+
]);
|
|
50432
|
+
var RunningProposalReplaySchema = object({
|
|
50433
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
50434
|
+
intervals: number$1().int().nonnegative(),
|
|
50435
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
50436
|
+
extra: number$1().int().nonnegative(),
|
|
50437
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
50438
|
+
missed: number$1().int().nonnegative()
|
|
50439
|
+
});
|
|
50440
|
+
var RunningProposalEvidenceSchema = object({
|
|
50441
|
+
tier: RunningProposalTierSchema,
|
|
50442
|
+
spanMs: number$1().nonnegative(),
|
|
50443
|
+
from: number$1(),
|
|
50444
|
+
to: number$1(),
|
|
50445
|
+
points: number$1().int().nonnegative(),
|
|
50446
|
+
runsFound: number$1().int().nonnegative(),
|
|
50447
|
+
pausesFound: number$1().int().nonnegative(),
|
|
50448
|
+
longestPauseMs: number$1().nonnegative(),
|
|
50449
|
+
idleFloorW: number$1(),
|
|
50450
|
+
noiseCeilingW: number$1(),
|
|
50451
|
+
plateauW: number$1(),
|
|
50452
|
+
replay: RunningProposalReplaySchema
|
|
50453
|
+
});
|
|
50454
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
50455
|
+
spanMs: number$1().nonnegative(),
|
|
50456
|
+
points: number$1().int().nonnegative(),
|
|
50457
|
+
runsFound: number$1().int().nonnegative()
|
|
50458
|
+
});
|
|
50459
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
50460
|
+
state: literal("proposal"),
|
|
50461
|
+
transform: ThresholdTransformDraftSchema,
|
|
50462
|
+
evidence: RunningProposalEvidenceSchema,
|
|
50463
|
+
confidence: RunningProposalConfidenceSchema,
|
|
50464
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
50465
|
+
notes: array(string())
|
|
50466
|
+
}), object({
|
|
50467
|
+
state: literal("insufficient"),
|
|
50468
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
50469
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
50470
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
50471
|
+
fallback: ThresholdTransformDraftSchema
|
|
50472
|
+
})]);
|
|
50473
|
+
/**
|
|
50053
50474
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
50054
50475
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
50055
50476
|
*
|
|
@@ -50339,6 +50760,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
50339
50760
|
kind: "mutation",
|
|
50340
50761
|
auth: "admin",
|
|
50341
50762
|
access: "view"
|
|
50763
|
+
}), method(object({
|
|
50764
|
+
deviceId: number$1().int(),
|
|
50765
|
+
cap: string().min(1),
|
|
50766
|
+
fieldPath: string().min(1),
|
|
50767
|
+
deviceType: _enum(DeviceType).optional()
|
|
50768
|
+
}), RunningThresholdProposalSchema, {
|
|
50769
|
+
auth: "admin",
|
|
50770
|
+
access: "view"
|
|
50342
50771
|
}), method(object({
|
|
50343
50772
|
blockId: string().min(1),
|
|
50344
50773
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -51294,7 +51723,27 @@ method(object({
|
|
|
51294
51723
|
* exposes only the COMMON surface: link state, supported device
|
|
51295
51724
|
* kinds (so the UI can filter the device picker), and per-device
|
|
51296
51725
|
* expose/unexpose.
|
|
51297
|
-
|
|
51726
|
+
*
|
|
51727
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
51728
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
51729
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
51730
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
51731
|
+
* default instance's); present = that instance; an id the provider does not
|
|
51732
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
51733
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
51734
|
+
* change always names the target it changes. A single-instance provider
|
|
51735
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
51736
|
+
*/
|
|
51737
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
51738
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
51739
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
51740
|
+
/** One independent export target of an addon. */
|
|
51741
|
+
var ExportInstanceSchema = object({
|
|
51742
|
+
instanceId: InstanceIdSchema$1,
|
|
51743
|
+
name: string(),
|
|
51744
|
+
enabled: boolean(),
|
|
51745
|
+
kind: string().optional()
|
|
51746
|
+
});
|
|
51298
51747
|
var LinkStateSchema = _enum([
|
|
51299
51748
|
"unlinked",
|
|
51300
51749
|
"linked",
|
|
@@ -51345,10 +51794,14 @@ var ExposedDeviceSchema = object({
|
|
|
51345
51794
|
capabilities: array(string()).optional()
|
|
51346
51795
|
});
|
|
51347
51796
|
var ExposeInputSchema = object({
|
|
51797
|
+
instanceId: InstanceIdSchema$1,
|
|
51348
51798
|
deviceId: string(),
|
|
51349
51799
|
capabilities: array(string()).optional()
|
|
51350
51800
|
});
|
|
51351
|
-
var UnexposeInputSchema = object({
|
|
51801
|
+
var UnexposeInputSchema = object({
|
|
51802
|
+
instanceId: InstanceIdSchema$1,
|
|
51803
|
+
deviceId: string()
|
|
51804
|
+
});
|
|
51352
51805
|
/**
|
|
51353
51806
|
* What an exporter may expose besides devices (D968). An action and a
|
|
51354
51807
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -51377,15 +51830,21 @@ var ExposedEntitySchema = object({
|
|
|
51377
51830
|
entity: ExportEntityRefSchema,
|
|
51378
51831
|
exposedAs: string().optional()
|
|
51379
51832
|
});
|
|
51380
|
-
var ExposeEntityInputSchema = object({
|
|
51381
|
-
|
|
51382
|
-
|
|
51833
|
+
var ExposeEntityInputSchema = object({
|
|
51834
|
+
instanceId: InstanceIdSchema$1,
|
|
51835
|
+
entity: ExportEntityRefSchema
|
|
51836
|
+
});
|
|
51837
|
+
var UnexposeEntityInputSchema = object({
|
|
51838
|
+
instanceId: InstanceIdSchema$1,
|
|
51839
|
+
entity: ExportEntityRefSchema
|
|
51840
|
+
});
|
|
51841
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
51383
51842
|
kind: "mutation",
|
|
51384
51843
|
auth: "admin"
|
|
51385
51844
|
}), method(UnexposeInputSchema, _void(), {
|
|
51386
51845
|
kind: "mutation",
|
|
51387
51846
|
auth: "admin"
|
|
51388
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
51847
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
51389
51848
|
kind: "mutation",
|
|
51390
51849
|
auth: "admin"
|
|
51391
51850
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -54976,15 +55435,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
54976
55435
|
items: array(FanCaptureItemSchema),
|
|
54977
55436
|
omitted: array(FanCaptureOmissionSchema)
|
|
54978
55437
|
});
|
|
54979
|
-
var deviceInput$
|
|
55438
|
+
var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
|
|
54980
55439
|
DeviceType.Fan;
|
|
54981
|
-
method(deviceInput$
|
|
55440
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
54982
55441
|
kind: "mutation",
|
|
54983
55442
|
auth: "admin"
|
|
54984
|
-
}), method(deviceInput$
|
|
55443
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
54985
55444
|
kind: "mutation",
|
|
54986
55445
|
auth: "protected"
|
|
54987
|
-
}), method(deviceInput$
|
|
55446
|
+
}), method(deviceInput$2.extend({
|
|
54988
55447
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
54989
55448
|
steps: array(ActionStepSchema).max(20).default([])
|
|
54990
55449
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -55895,13 +56354,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
55895
56354
|
*/
|
|
55896
56355
|
var ConfigSchemaPassthrough$3 = custom();
|
|
55897
56356
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
56357
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56358
|
+
/**
|
|
56359
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
56360
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
56361
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
56362
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
56363
|
+
* instance id.
|
|
56364
|
+
*/
|
|
56365
|
+
var IntegrationInstanceKindSchema = object({
|
|
56366
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
56367
|
+
id: string().min(1),
|
|
56368
|
+
label: string().min(1),
|
|
56369
|
+
/** The kind signs in through a server-held interactive session. */
|
|
56370
|
+
signIn: object({
|
|
56371
|
+
basePath: string().min(1),
|
|
56372
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
56373
|
+
}).optional(),
|
|
56374
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
56375
|
+
connectPath: string().min(1).optional(),
|
|
56376
|
+
/** Relative hub path to disconnect that account. */
|
|
56377
|
+
disconnectPath: string().min(1).optional()
|
|
56378
|
+
});
|
|
55898
56379
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56380
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56381
|
+
role: _enum([
|
|
56382
|
+
"devices",
|
|
56383
|
+
"services",
|
|
56384
|
+
"both"
|
|
56385
|
+
]),
|
|
56386
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56387
|
+
discovery: object({
|
|
56388
|
+
mode: _enum([
|
|
56389
|
+
"none",
|
|
56390
|
+
"lan",
|
|
56391
|
+
"account"
|
|
56392
|
+
]),
|
|
56393
|
+
scopedToInstance: boolean()
|
|
56394
|
+
}).optional(),
|
|
55899
56395
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
55900
|
-
instanceMode:
|
|
56396
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
55901
56397
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
55902
56398
|
kind: string(),
|
|
55903
56399
|
/** True when `testInstance` really probes the remote. */
|
|
55904
|
-
canTest: boolean()
|
|
56400
|
+
canTest: boolean(),
|
|
56401
|
+
/**
|
|
56402
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
56403
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
56404
|
+
*/
|
|
56405
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
56406
|
+
/**
|
|
56407
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
56408
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
56409
|
+
*/
|
|
56410
|
+
allowRestoreId: boolean().optional(),
|
|
56411
|
+
/**
|
|
56412
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
56413
|
+
* send an id (the UI then mints an opaque one).
|
|
56414
|
+
*/
|
|
56415
|
+
mintsId: boolean().optional()
|
|
55905
56416
|
});
|
|
55906
56417
|
var IntegrationInstanceStatusSchema = object({
|
|
55907
56418
|
state: _enum([
|
|
@@ -55910,7 +56421,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
55910
56421
|
"error",
|
|
55911
56422
|
"unknown"
|
|
55912
56423
|
]),
|
|
55913
|
-
detail: string().optional()
|
|
56424
|
+
detail: string().optional(),
|
|
56425
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
56426
|
+
label: string().optional()
|
|
55914
56427
|
});
|
|
55915
56428
|
var IntegrationInstanceSchema = object({
|
|
55916
56429
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55918,13 +56431,18 @@ var IntegrationInstanceSchema = object({
|
|
|
55918
56431
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
55919
56432
|
configured: boolean(),
|
|
55920
56433
|
/**
|
|
55921
|
-
* The instance's own on/off switch (D996)
|
|
55922
|
-
*
|
|
56434
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56435
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56436
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
55923
56437
|
*/
|
|
55924
|
-
enabled: boolean(),
|
|
56438
|
+
enabled: boolean().optional(),
|
|
56439
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56440
|
+
kind: string().optional(),
|
|
55925
56441
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55926
56442
|
formValues: record(string(), unknown()),
|
|
55927
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
56443
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
56444
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
56445
|
+
lastCheckedAt: number$1().optional()
|
|
55928
56446
|
});
|
|
55929
56447
|
var IntegrationInstancePatchSchema = object({
|
|
55930
56448
|
id: IntegrationInstanceIdSchema,
|
|
@@ -55932,31 +56450,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
55932
56450
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
55933
56451
|
values: record(string(), unknown())
|
|
55934
56452
|
});
|
|
55935
|
-
|
|
55936
|
-
|
|
55937
|
-
|
|
55938
|
-
|
|
55939
|
-
|
|
55940
|
-
|
|
55941
|
-
|
|
55942
|
-
|
|
55943
|
-
|
|
55944
|
-
|
|
55945
|
-
|
|
55946
|
-
|
|
55947
|
-
|
|
55948
|
-
|
|
55949
|
-
|
|
55950
|
-
|
|
55951
|
-
|
|
55952
|
-
|
|
55953
|
-
}),
|
|
55954
|
-
|
|
55955
|
-
|
|
55956
|
-
}),
|
|
55957
|
-
|
|
55958
|
-
|
|
55959
|
-
|
|
56453
|
+
var integrationInstancesCapability = {
|
|
56454
|
+
name: "integration-instances",
|
|
56455
|
+
scope: "system",
|
|
56456
|
+
mode: "collection",
|
|
56457
|
+
methods: {
|
|
56458
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
56459
|
+
/** The form; `id: null` is the create form. */
|
|
56460
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
56461
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
56462
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
56463
|
+
/**
|
|
56464
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
56465
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
56466
|
+
*/
|
|
56467
|
+
createInstance: method(object({
|
|
56468
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
56469
|
+
name: string().min(1),
|
|
56470
|
+
kind: string().min(1).optional()
|
|
56471
|
+
}), object({ id: string() }), {
|
|
56472
|
+
kind: "mutation",
|
|
56473
|
+
auth: "admin"
|
|
56474
|
+
}),
|
|
56475
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
56476
|
+
kind: "mutation",
|
|
56477
|
+
auth: "admin"
|
|
56478
|
+
}),
|
|
56479
|
+
/**
|
|
56480
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
56481
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56482
|
+
*/
|
|
56483
|
+
setInstanceEnabled: method(object({
|
|
56484
|
+
id: IntegrationInstanceIdSchema,
|
|
56485
|
+
enabled: boolean()
|
|
56486
|
+
}), object({ ok: literal(true) }), {
|
|
56487
|
+
kind: "mutation",
|
|
56488
|
+
auth: "admin"
|
|
56489
|
+
}),
|
|
56490
|
+
/** Delete the record and its keychain refs together. */
|
|
56491
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
56492
|
+
kind: "mutation",
|
|
56493
|
+
auth: "admin"
|
|
56494
|
+
}),
|
|
56495
|
+
testInstance: optionalMethod(object({
|
|
56496
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
56497
|
+
values: record(string(), unknown())
|
|
56498
|
+
}), TestConnectionResultSchema$1, {
|
|
56499
|
+
kind: "mutation",
|
|
56500
|
+
auth: "admin"
|
|
56501
|
+
})
|
|
56502
|
+
}
|
|
56503
|
+
};
|
|
55960
56504
|
/**
|
|
55961
56505
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
55962
56506
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -57974,6 +58518,11 @@ var LogEntrySchema = object({
|
|
|
57974
58518
|
* flight, and not asking for the next slice ends the scan.
|
|
57975
58519
|
*/
|
|
57976
58520
|
var LogReadQuerySchema = object({
|
|
58521
|
+
/**
|
|
58522
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
58523
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
58524
|
+
*/
|
|
58525
|
+
instanceId: string().min(1).max(200).optional(),
|
|
57977
58526
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
57978
58527
|
requestId: string().min(1).max(64).optional(),
|
|
57979
58528
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -58036,6 +58585,8 @@ var LogReadResultSchema = object({
|
|
|
58036
58585
|
/** True when `cancelRead` ended this slice early. */
|
|
58037
58586
|
cancelled: boolean().optional()
|
|
58038
58587
|
});
|
|
58588
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
58589
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
58039
58590
|
var LogReadCapabilitiesSchema = object({
|
|
58040
58591
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
58041
58592
|
readable: boolean(),
|
|
@@ -58054,7 +58605,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
58054
58605
|
/** How far back the store reaches, when the provider knows (days). */
|
|
58055
58606
|
retentionDays: number$1().nullable()
|
|
58056
58607
|
});
|
|
58057
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
58608
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
58058
58609
|
kind: "mutation",
|
|
58059
58610
|
auth: "admin"
|
|
58060
58611
|
});
|
|
@@ -70448,6 +70999,117 @@ var powerMeterCapability = {
|
|
|
70448
70999
|
volatileStateFields: ["lastFetchedAt"],
|
|
70449
71000
|
customize: powerMeterCustomize
|
|
70450
71001
|
};
|
|
71002
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
71003
|
+
"no-power-source",
|
|
71004
|
+
"switch-missing",
|
|
71005
|
+
"switch-offline",
|
|
71006
|
+
"switch-unknown",
|
|
71007
|
+
"restart-in-progress",
|
|
71008
|
+
"switch-call-failed"
|
|
71009
|
+
]);
|
|
71010
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
71011
|
+
accepted: literal(false),
|
|
71012
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
71013
|
+
/** One line for the operator, naming the switch. */
|
|
71014
|
+
message: string()
|
|
71015
|
+
})]);
|
|
71016
|
+
var PowerSourceInfoSchema = object({
|
|
71017
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
71018
|
+
linked: boolean(),
|
|
71019
|
+
switchDeviceId: number$1().nullable(),
|
|
71020
|
+
switchName: string().nullable(),
|
|
71021
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
71022
|
+
switchOn: boolean().nullable(),
|
|
71023
|
+
switchOnline: boolean().nullable(),
|
|
71024
|
+
restartInProgress: boolean(),
|
|
71025
|
+
offSeconds: number$1().int()
|
|
71026
|
+
});
|
|
71027
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
71028
|
+
var PowerSourceStateSchema = object({
|
|
71029
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
71030
|
+
switchOn: boolean().nullable(),
|
|
71031
|
+
offSeconds: number$1().int(),
|
|
71032
|
+
lastFetchedAt: number$1()
|
|
71033
|
+
});
|
|
71034
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
71035
|
+
DeviceType.Gateway,
|
|
71036
|
+
DeviceType.Hub,
|
|
71037
|
+
DeviceType.Camera,
|
|
71038
|
+
DeviceType.Sensor,
|
|
71039
|
+
DeviceType.MediaPlayer,
|
|
71040
|
+
DeviceType.Vacuum,
|
|
71041
|
+
DeviceType.Generic,
|
|
71042
|
+
DeviceType.Washer,
|
|
71043
|
+
DeviceType.Dryer,
|
|
71044
|
+
DeviceType.Dishwasher,
|
|
71045
|
+
DeviceType.PetFountain,
|
|
71046
|
+
DeviceType.Fan,
|
|
71047
|
+
DeviceType.Humidifier,
|
|
71048
|
+
DeviceType.Climate,
|
|
71049
|
+
DeviceType.WaterHeater
|
|
71050
|
+
];
|
|
71051
|
+
var powerSourceCustomize = {
|
|
71052
|
+
version: 1,
|
|
71053
|
+
targets: [{
|
|
71054
|
+
id: "switch",
|
|
71055
|
+
cardinality: "scalar",
|
|
71056
|
+
vocabulary: {
|
|
71057
|
+
cap: "power-source",
|
|
71058
|
+
stateField: "switchOn"
|
|
71059
|
+
},
|
|
71060
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71061
|
+
apply: "add",
|
|
71062
|
+
path: "switchOn",
|
|
71063
|
+
sources: [{
|
|
71064
|
+
kind: "switch",
|
|
71065
|
+
transforms: [{ kind: "identity" }]
|
|
71066
|
+
}],
|
|
71067
|
+
companions: [{
|
|
71068
|
+
kind: "constant",
|
|
71069
|
+
path: "offSeconds",
|
|
71070
|
+
value: 10,
|
|
71071
|
+
when: "add"
|
|
71072
|
+
}, {
|
|
71073
|
+
kind: "clock",
|
|
71074
|
+
path: "lastFetchedAt",
|
|
71075
|
+
when: "always"
|
|
71076
|
+
}],
|
|
71077
|
+
sourceReport: { status: "assignment-status" }
|
|
71078
|
+
}]
|
|
71079
|
+
};
|
|
71080
|
+
var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
|
|
71081
|
+
var powerSourceCapability = {
|
|
71082
|
+
name: "power-source",
|
|
71083
|
+
scope: "device",
|
|
71084
|
+
mode: "singleton",
|
|
71085
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71086
|
+
methods: {
|
|
71087
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
71088
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71089
|
+
kind: "mutation",
|
|
71090
|
+
auth: "admin"
|
|
71091
|
+
}),
|
|
71092
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71093
|
+
kind: "mutation",
|
|
71094
|
+
auth: "admin"
|
|
71095
|
+
}),
|
|
71096
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71097
|
+
kind: "mutation",
|
|
71098
|
+
auth: "admin"
|
|
71099
|
+
})
|
|
71100
|
+
},
|
|
71101
|
+
runtimeState: PowerSourceStateSchema,
|
|
71102
|
+
customize: powerSourceCustomize,
|
|
71103
|
+
/**
|
|
71104
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
71105
|
+
* its next report; a restored value would outlive the reading it names.
|
|
71106
|
+
*
|
|
71107
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71108
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71109
|
+
*/
|
|
71110
|
+
durability: "session",
|
|
71111
|
+
history: "none"
|
|
71112
|
+
};
|
|
70451
71113
|
/**
|
|
70452
71114
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
70453
71115
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -79809,6 +80471,25 @@ function buildDescriptor(capName, kind) {
|
|
|
79809
80471
|
* contributes one entry per bound cap present in this map.
|
|
79810
80472
|
*/
|
|
79811
80473
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
80474
|
+
/**
|
|
80475
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
80476
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
80477
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
80478
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
80479
|
+
* server sends an id, a client maps it to its own icon set.
|
|
80480
|
+
*
|
|
80481
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
80482
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
80483
|
+
* to `icon`.
|
|
80484
|
+
*/
|
|
80485
|
+
function digit(n) {
|
|
80486
|
+
return {
|
|
80487
|
+
label: t(n, n),
|
|
80488
|
+
icon: "hash",
|
|
80489
|
+
legend: n
|
|
80490
|
+
};
|
|
80491
|
+
}
|
|
80492
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
79812
80493
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
79813
80494
|
var btn = (idSuffix, row, col, init) => ({
|
|
79814
80495
|
idSuffix,
|
|
@@ -79936,6 +80617,34 @@ discriminatedUnion("kind", [object({
|
|
|
79936
80617
|
kind: literal("pronto"),
|
|
79937
80618
|
code: string()
|
|
79938
80619
|
})]);
|
|
80620
|
+
/**
|
|
80621
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
80622
|
+
*
|
|
80623
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
80624
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
80625
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
80626
|
+
* count of samples.
|
|
80627
|
+
*
|
|
80628
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
80629
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
80630
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
80631
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
80632
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
80633
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
80634
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
80635
|
+
* covers the longest standby blip above T.
|
|
80636
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
80637
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
80638
|
+
*/
|
|
80639
|
+
var SECOND_MS = 1e3;
|
|
80640
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
80641
|
+
24 * (60 * MINUTE_MS$1);
|
|
80642
|
+
10 * MINUTE_MS$1;
|
|
80643
|
+
30 * SECOND_MS;
|
|
80644
|
+
60 * SECOND_MS;
|
|
80645
|
+
20 * MINUTE_MS$1;
|
|
80646
|
+
10 * SECOND_MS;
|
|
80647
|
+
10 * SECOND_MS;
|
|
79939
80648
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79940
80649
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79941
80650
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -83041,6 +83750,13 @@ Object.freeze({
|
|
|
83041
83750
|
access: "create",
|
|
83042
83751
|
role: "admin"
|
|
83043
83752
|
},
|
|
83753
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
83754
|
+
capName: "core-blocks",
|
|
83755
|
+
capScope: "system",
|
|
83756
|
+
addonId: null,
|
|
83757
|
+
access: "view",
|
|
83758
|
+
role: "admin"
|
|
83759
|
+
},
|
|
83044
83760
|
"coreBlocks.reportAutomationRun": {
|
|
83045
83761
|
capName: "core-blocks",
|
|
83046
83762
|
capScope: "system",
|
|
@@ -83552,6 +84268,13 @@ Object.freeze({
|
|
|
83552
84268
|
access: "view",
|
|
83553
84269
|
role: "admin"
|
|
83554
84270
|
},
|
|
84271
|
+
"deviceExport.listInstances": {
|
|
84272
|
+
capName: "device-export",
|
|
84273
|
+
capScope: "system",
|
|
84274
|
+
addonId: null,
|
|
84275
|
+
access: "view",
|
|
84276
|
+
role: "admin"
|
|
84277
|
+
},
|
|
83555
84278
|
"deviceExport.listSupportedDeviceKinds": {
|
|
83556
84279
|
capName: "device-export",
|
|
83557
84280
|
capScope: "system",
|
|
@@ -92499,6 +93222,11 @@ Object.freeze({
|
|
|
92499
93222
|
form: "single",
|
|
92500
93223
|
optional: false
|
|
92501
93224
|
}],
|
|
93225
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
93226
|
+
name: "deviceId",
|
|
93227
|
+
form: "single",
|
|
93228
|
+
optional: false
|
|
93229
|
+
}],
|
|
92502
93230
|
"cover.close": [{
|
|
92503
93231
|
name: "deviceId",
|
|
92504
93232
|
form: "single",
|
|
@@ -95162,99 +95890,6 @@ Object.freeze({
|
|
|
95162
95890
|
"network-access": "ingress",
|
|
95163
95891
|
"smtp-provider": "email"
|
|
95164
95892
|
});
|
|
95165
|
-
/**
|
|
95166
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
95167
|
-
* `z.infer`, they do not restate the shape.
|
|
95168
|
-
*/
|
|
95169
|
-
var OverrideSourceSchema = object({
|
|
95170
|
-
addonId: string().min(1),
|
|
95171
|
-
stableId: string().min(1),
|
|
95172
|
-
cap: string().min(1),
|
|
95173
|
-
fieldPath: string().min(1)
|
|
95174
|
-
});
|
|
95175
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
95176
|
-
object({ kind: literal("identity") }),
|
|
95177
|
-
object({ kind: literal("invert") }),
|
|
95178
|
-
object({
|
|
95179
|
-
kind: literal("threshold"),
|
|
95180
|
-
comparison: _enum(["gte", "lte"]),
|
|
95181
|
-
threshold: number$1(),
|
|
95182
|
-
startDwellMs: number$1().nonnegative(),
|
|
95183
|
-
stopDwellMs: number$1().nonnegative()
|
|
95184
|
-
}),
|
|
95185
|
-
object({
|
|
95186
|
-
kind: literal("linear"),
|
|
95187
|
-
scale: number$1(),
|
|
95188
|
-
offset: number$1(),
|
|
95189
|
-
clampMin: number$1().optional(),
|
|
95190
|
-
clampMax: number$1().optional()
|
|
95191
|
-
}),
|
|
95192
|
-
object({
|
|
95193
|
-
kind: literal("enum-map"),
|
|
95194
|
-
mapping: record(string(), string().nullable())
|
|
95195
|
-
}),
|
|
95196
|
-
object({ kind: literal("number") })
|
|
95197
|
-
]);
|
|
95198
|
-
var ScalarOverrideDraftSchema = object({
|
|
95199
|
-
kind: literal("scalar"),
|
|
95200
|
-
cap: string().min(1),
|
|
95201
|
-
targetId: string().min(1),
|
|
95202
|
-
source: OverrideSourceSchema,
|
|
95203
|
-
transform: CustomizeTransformDraftSchema
|
|
95204
|
-
});
|
|
95205
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
95206
|
-
kind: literal("keyedArray"),
|
|
95207
|
-
cap: string().min(1),
|
|
95208
|
-
targetId: string().min(1),
|
|
95209
|
-
key: string().min(1),
|
|
95210
|
-
label: string().min(1),
|
|
95211
|
-
fieldPath: string().min(1),
|
|
95212
|
-
source: OverrideSourceSchema,
|
|
95213
|
-
transform: CustomizeTransformDraftSchema
|
|
95214
|
-
});
|
|
95215
|
-
var PulseOverrideDraftSchema = object({
|
|
95216
|
-
kind: literal("pulse"),
|
|
95217
|
-
cap: string().min(1),
|
|
95218
|
-
targetId: string().min(1),
|
|
95219
|
-
enabled: boolean(),
|
|
95220
|
-
debounceMs: number$1().nonnegative(),
|
|
95221
|
-
edges: array(_enum([
|
|
95222
|
-
"rising",
|
|
95223
|
-
"button",
|
|
95224
|
-
"native-event"
|
|
95225
|
-
])).min(1).optional(),
|
|
95226
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
95227
|
-
});
|
|
95228
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
95229
|
-
"in-addition",
|
|
95230
|
-
"confirm",
|
|
95231
|
-
"replace"
|
|
95232
|
-
]).optional() });
|
|
95233
|
-
discriminatedUnion("kind", [
|
|
95234
|
-
ScalarOverrideDraftSchema,
|
|
95235
|
-
KeyedArrayOverrideDraftSchema,
|
|
95236
|
-
PulseOverrideDraftSchema,
|
|
95237
|
-
object({
|
|
95238
|
-
kind: literal("span"),
|
|
95239
|
-
cap: string().min(1),
|
|
95240
|
-
targetId: string().min(1),
|
|
95241
|
-
timeoutMs: number$1().nonnegative(),
|
|
95242
|
-
cooldownMs: number$1().nonnegative().optional(),
|
|
95243
|
-
sources: array(SpanSourceDraftSchema)
|
|
95244
|
-
})
|
|
95245
|
-
]);
|
|
95246
|
-
object({
|
|
95247
|
-
targetId: string().min(1),
|
|
95248
|
-
configured: boolean(),
|
|
95249
|
-
source: _enum(["native", "assigned"]),
|
|
95250
|
-
state: _enum([
|
|
95251
|
-
"active",
|
|
95252
|
-
"stale",
|
|
95253
|
-
"released",
|
|
95254
|
-
"unavailable"
|
|
95255
|
-
]),
|
|
95256
|
-
reason: string().nullable()
|
|
95257
|
-
});
|
|
95258
95893
|
var G711_SCALE_CORRECTION_DB = {
|
|
95259
95894
|
PCMU: 20 * Math.log10(4),
|
|
95260
95895
|
PCMA: 20 * Math.log10(8)
|
|
@@ -97560,7 +98195,7 @@ var alexaConnectionSchema = object({
|
|
|
97560
98195
|
*/
|
|
97561
98196
|
refreshToken: string().default("").describe("Alexa refresh token (must be inside cookie blob)").meta({ secret: true }),
|
|
97562
98197
|
/** Device-registration secret some flows need. */
|
|
97563
|
-
macDms: string().default("").describe("macDms device-registration secret (optional)"),
|
|
98198
|
+
macDms: string().default("").describe("macDms device-registration secret (optional)").meta({ secret: true }),
|
|
97564
98199
|
/** Poll interval for state sync (Alexa has no clean 3rd-party push). */
|
|
97565
98200
|
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number$1().int().min(15e3).max(36e5).default(6e4)).describe("State poll interval (ms) — conservative to avoid Amazon throttling")
|
|
97566
98201
|
});
|
|
@@ -97578,7 +98213,9 @@ var AlexaBrokerEntrySchema = object({
|
|
|
97578
98213
|
id: string().min(1),
|
|
97579
98214
|
name: string().min(1),
|
|
97580
98215
|
connection: alexaConnectionSchema,
|
|
97581
|
-
integrationId: string().optional()
|
|
98216
|
+
integrationId: string().optional(),
|
|
98217
|
+
/** Absent means on; the instances page switches a connection off without deleting it. */
|
|
98218
|
+
enabled: boolean().optional()
|
|
97582
98219
|
});
|
|
97583
98220
|
/**
|
|
97584
98221
|
* Interactive proxy-login settings (addon-global, not per-broker). alexa-remote2
|
|
@@ -97615,7 +98252,6 @@ function readEchoTargets(config) {
|
|
|
97615
98252
|
}
|
|
97616
98253
|
/** Top-level addon config — broker entries + the proxy-login settings. */
|
|
97617
98254
|
var alexaAddonConfigSchema = object({
|
|
97618
|
-
brokers: array(AlexaBrokerEntrySchema).default([]),
|
|
97619
98255
|
proxyLogin: alexaProxyLoginSchema.default({ proxyPort: 3456 }),
|
|
97620
98256
|
echoTargets: array(alexaEchoTargetSchema).default([])
|
|
97621
98257
|
});
|
|
@@ -141160,13 +141796,16 @@ var AlexaBrokerRegistry = class {
|
|
|
141160
141796
|
}
|
|
141161
141797
|
async restore(entries) {
|
|
141162
141798
|
for (const entry of entries) this.#seedCounter(entry.id);
|
|
141163
|
-
for (const entry of entries)
|
|
141164
|
-
|
|
141165
|
-
|
|
141166
|
-
|
|
141167
|
-
|
|
141168
|
-
|
|
141169
|
-
|
|
141799
|
+
for (const entry of entries) {
|
|
141800
|
+
if (entry.enabled === false) continue;
|
|
141801
|
+
try {
|
|
141802
|
+
await this.#startManager(entry);
|
|
141803
|
+
} catch (err) {
|
|
141804
|
+
this.#logger.warn("AlexaBrokerRegistry: failed to restore manager", {
|
|
141805
|
+
tags: { brokerId: entry.id },
|
|
141806
|
+
meta: { error: errMsg(err) }
|
|
141807
|
+
});
|
|
141808
|
+
}
|
|
141170
141809
|
}
|
|
141171
141810
|
}
|
|
141172
141811
|
async shutdown() {
|
|
@@ -141200,7 +141839,8 @@ var AlexaBrokerRegistry = class {
|
|
|
141200
141839
|
this.#onStatesPolled = cb;
|
|
141201
141840
|
}
|
|
141202
141841
|
async createEntry(name, connection, opts = {}) {
|
|
141203
|
-
const id = this.#allocateId();
|
|
141842
|
+
const id = opts.id ?? this.#allocateId();
|
|
141843
|
+
if (opts.id !== void 0) this.#seedCounter(opts.id);
|
|
141204
141844
|
const entry = {
|
|
141205
141845
|
id,
|
|
141206
141846
|
name,
|
|
@@ -141213,7 +141853,7 @@ var AlexaBrokerRegistry = class {
|
|
|
141213
141853
|
}
|
|
141214
141854
|
async removeEntry(id) {
|
|
141215
141855
|
const mgr = this.#managers.get(id);
|
|
141216
|
-
if (!mgr)
|
|
141856
|
+
if (!mgr) return;
|
|
141217
141857
|
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
141218
141858
|
this.#managers.delete(id);
|
|
141219
141859
|
try {
|
|
@@ -141280,6 +141920,10 @@ var AlexaBrokerRegistry = class {
|
|
|
141280
141920
|
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
141281
141921
|
await mgr.start();
|
|
141282
141922
|
}
|
|
141923
|
+
/** The next free broker id, for a record minted before its manager exists. */
|
|
141924
|
+
allocateId() {
|
|
141925
|
+
return this.#allocateId();
|
|
141926
|
+
}
|
|
141283
141927
|
#allocateId() {
|
|
141284
141928
|
const id = `alexa_${String(this.#nextId).padStart(3, "0")}`;
|
|
141285
141929
|
this.#nextId++;
|
|
@@ -142701,10 +143345,12 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
142701
143345
|
var alexaBrokersEntryKind = defineEntryKind("brokers", {
|
|
142702
143346
|
title: "Alexa import brokers",
|
|
142703
143347
|
schema: entryBody({
|
|
142704
|
-
|
|
142705
|
-
integrationId: string().optional()
|
|
143348
|
+
...alexaConnectionSchema.shape,
|
|
143349
|
+
integrationId: string().optional(),
|
|
143350
|
+
enabled: boolean().optional()
|
|
142706
143351
|
}),
|
|
142707
|
-
writeTrigger: "an operator links or removes an Alexa account broker"
|
|
143352
|
+
writeTrigger: "an operator links or removes an Alexa account broker",
|
|
143353
|
+
retiredSecretPaths: ["connection.cookie", "connection.refreshToken"]
|
|
142708
143354
|
});
|
|
142709
143355
|
var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
|
|
142710
143356
|
title: "Alexa Echo speech sessions",
|
|
@@ -142717,13 +143363,119 @@ var alexaRecordKinds = {
|
|
|
142717
143363
|
"speak-sessions": alexaSpeakSessionsEntryKind
|
|
142718
143364
|
};
|
|
142719
143365
|
//#endregion
|
|
143366
|
+
//#region src/alexa-instances.ts
|
|
143367
|
+
/**
|
|
143368
|
+
* The Alexa accounts as `integration-instances` (planning#408 P3): one instance per `brokers`
|
|
143369
|
+
* record, over the SAME records and ids (`alexa_NNN`) the `broker` cap serves. The body is flat, so
|
|
143370
|
+
* the form the instances page edits is the stored shape. Signing in (device code or the Amazon
|
|
143371
|
+
* login proxy) goes through the routes the addon already serves under `/addon/import-alexa/login`.
|
|
143372
|
+
*/
|
|
143373
|
+
var ALEXA_INSTANCE_KIND = "alexa";
|
|
143374
|
+
var ALEXA_SIGN_IN_PATH = "/addon/import-alexa/login";
|
|
143375
|
+
function isAlexaBodyConfigured(body) {
|
|
143376
|
+
return body.cookie.length > 0 || body.refreshToken.length > 0;
|
|
143377
|
+
}
|
|
143378
|
+
function statusOf(info, configured) {
|
|
143379
|
+
if (!configured) return {
|
|
143380
|
+
state: "unknown",
|
|
143381
|
+
label: "Not signed in"
|
|
143382
|
+
};
|
|
143383
|
+
if (info === null) return {
|
|
143384
|
+
state: "unknown",
|
|
143385
|
+
label: "Not connected"
|
|
143386
|
+
};
|
|
143387
|
+
if (info.status === "connected") return {
|
|
143388
|
+
state: "ok",
|
|
143389
|
+
label: "Connected"
|
|
143390
|
+
};
|
|
143391
|
+
if (info.status === "connecting") return {
|
|
143392
|
+
state: "unknown",
|
|
143393
|
+
label: "Connecting"
|
|
143394
|
+
};
|
|
143395
|
+
return {
|
|
143396
|
+
state: "error",
|
|
143397
|
+
label: info.status,
|
|
143398
|
+
...info.error !== null ? { detail: info.error } : {}
|
|
143399
|
+
};
|
|
143400
|
+
}
|
|
143401
|
+
function buildAlexaInstances(records, deps) {
|
|
143402
|
+
return new RecordIntegrationInstances(records, alexaBrokersEntryKind, {
|
|
143403
|
+
description: {
|
|
143404
|
+
role: "both",
|
|
143405
|
+
instanceMode: "multiple",
|
|
143406
|
+
kind: "account",
|
|
143407
|
+
canTest: true,
|
|
143408
|
+
kinds: [{
|
|
143409
|
+
id: ALEXA_INSTANCE_KIND,
|
|
143410
|
+
label: "Alexa",
|
|
143411
|
+
signIn: {
|
|
143412
|
+
basePath: ALEXA_SIGN_IN_PATH,
|
|
143413
|
+
methods: ["device-code", "proxy"]
|
|
143414
|
+
}
|
|
143415
|
+
}],
|
|
143416
|
+
allowRestoreId: true
|
|
143417
|
+
},
|
|
143418
|
+
formSchema: () => buildConnectionFormSchema(),
|
|
143419
|
+
initialBody: (name) => ({
|
|
143420
|
+
name,
|
|
143421
|
+
amazonDomain: "amazon.com",
|
|
143422
|
+
cookie: "",
|
|
143423
|
+
refreshToken: "",
|
|
143424
|
+
macDms: "",
|
|
143425
|
+
pollIntervalMs: 6e4
|
|
143426
|
+
}),
|
|
143427
|
+
mintId: () => deps.mintId(),
|
|
143428
|
+
kindOf: () => ALEXA_INSTANCE_KIND,
|
|
143429
|
+
health: (body, id) => {
|
|
143430
|
+
const info = deps.infoOf(id);
|
|
143431
|
+
return {
|
|
143432
|
+
status: statusOf(info, isAlexaBodyConfigured(body)),
|
|
143433
|
+
...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
|
|
143434
|
+
};
|
|
143435
|
+
},
|
|
143436
|
+
configured: isAlexaBodyConfigured,
|
|
143437
|
+
onChanged: deps.onChanged,
|
|
143438
|
+
test: async ({ id, values }) => {
|
|
143439
|
+
if (["cookie", "refreshToken"].some((key) => {
|
|
143440
|
+
const v = values[key];
|
|
143441
|
+
return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
|
|
143442
|
+
})) return {
|
|
143443
|
+
success: true,
|
|
143444
|
+
status: "inconclusive",
|
|
143445
|
+
error: "Save the account to check the sign-in"
|
|
143446
|
+
};
|
|
143447
|
+
const info = id === null ? null : deps.infoOf(id);
|
|
143448
|
+
if (info === null) return {
|
|
143449
|
+
success: false,
|
|
143450
|
+
status: "inconclusive",
|
|
143451
|
+
error: "Not connected: sign in first"
|
|
143452
|
+
};
|
|
143453
|
+
return info.status === "connected" ? {
|
|
143454
|
+
success: true,
|
|
143455
|
+
status: "validated"
|
|
143456
|
+
} : {
|
|
143457
|
+
success: false,
|
|
143458
|
+
status: "rejected",
|
|
143459
|
+
error: info.error ?? `status: ${info.status}`
|
|
143460
|
+
};
|
|
143461
|
+
}
|
|
143462
|
+
});
|
|
143463
|
+
}
|
|
143464
|
+
//#endregion
|
|
142720
143465
|
//#region src/alexa-broker-store.ts
|
|
142721
143466
|
function rowToEntry(id, data) {
|
|
142722
143467
|
const parsed = AlexaBrokerEntrySchema.safeParse({
|
|
142723
143468
|
id,
|
|
142724
143469
|
name: data.name,
|
|
142725
|
-
connection:
|
|
142726
|
-
|
|
143470
|
+
connection: {
|
|
143471
|
+
amazonDomain: data.amazonDomain,
|
|
143472
|
+
cookie: data.cookie,
|
|
143473
|
+
refreshToken: data.refreshToken,
|
|
143474
|
+
macDms: data.macDms,
|
|
143475
|
+
pollIntervalMs: data.pollIntervalMs
|
|
143476
|
+
},
|
|
143477
|
+
integrationId: data.integrationId,
|
|
143478
|
+
enabled: data.enabled
|
|
142727
143479
|
});
|
|
142728
143480
|
return parsed.success ? parsed.data : null;
|
|
142729
143481
|
}
|
|
@@ -142751,16 +143503,20 @@ var AlexaBrokerStore = class AlexaBrokerStore {
|
|
|
142751
143503
|
}
|
|
142752
143504
|
return out;
|
|
142753
143505
|
}
|
|
143506
|
+
async getById(id) {
|
|
143507
|
+
return (await this.listResolved()).find((entry) => entry.id === id) ?? null;
|
|
143508
|
+
}
|
|
142754
143509
|
async replaceAll(entries) {
|
|
142755
143510
|
const page = await this.kind.query({ limit: 1e3 });
|
|
142756
143511
|
const nextIds = new Set(entries.map((e) => e.id));
|
|
142757
143512
|
for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
|
|
142758
143513
|
for (const entry of entries) {
|
|
142759
|
-
const { id, name, connection, integrationId } = entry;
|
|
143514
|
+
const { id, name, connection, integrationId, enabled } = entry;
|
|
142760
143515
|
const body = {
|
|
142761
143516
|
name,
|
|
142762
|
-
connection,
|
|
142763
|
-
...integrationId === void 0 ? {} : { integrationId }
|
|
143517
|
+
...connection,
|
|
143518
|
+
...integrationId === void 0 ? {} : { integrationId },
|
|
143519
|
+
...enabled === void 0 ? {} : { enabled }
|
|
142764
143520
|
};
|
|
142765
143521
|
const parsed = alexaBrokersEntryKind.schema.safeParse(body);
|
|
142766
143522
|
if (!parsed.success) throw new Error(`invalid Alexa broker '${id}': ${parsed.error.message}`);
|
|
@@ -144148,6 +144904,34 @@ var AlexaSpeakController = class {
|
|
|
144148
144904
|
}
|
|
144149
144905
|
};
|
|
144150
144906
|
//#endregion
|
|
144907
|
+
//#region src/migrations/flatten-broker-records.ts
|
|
144908
|
+
/**
|
|
144909
|
+
* The Alexa broker records keep their account flat (planning#408 P3): the nested `connection`
|
|
144910
|
+
* object (`amazonDomain`, `cookie`, `refreshToken`, `macDms`, `pollIntervalMs`) becomes top-level
|
|
144911
|
+
* fields under the SAME record id (`alexa_NNN`). `macDms`, which used to be stored in plain text,
|
|
144912
|
+
* becomes a keychain secret. The work is `flattenNestedRecords`: every secret is read before
|
|
144913
|
+
* anything is written, read back from its new ref before the nested object goes, and the old refs
|
|
144914
|
+
* are deleted last.
|
|
144915
|
+
*/
|
|
144916
|
+
var FLATTEN_BROKER_RECORDS_ID = "import-alexa/2026-11-04-flatten-broker-records";
|
|
144917
|
+
async function flattenBrokerRecords(ports) {
|
|
144918
|
+
return flattenNestedRecords(ports, {
|
|
144919
|
+
legacyKey: "connection",
|
|
144920
|
+
flatSecretKeys: [
|
|
144921
|
+
"cookie",
|
|
144922
|
+
"refreshToken",
|
|
144923
|
+
"macDms"
|
|
144924
|
+
],
|
|
144925
|
+
toFlat: (legacy) => Object.fromEntries([
|
|
144926
|
+
"amazonDomain",
|
|
144927
|
+
"cookie",
|
|
144928
|
+
"refreshToken",
|
|
144929
|
+
"macDms",
|
|
144930
|
+
"pollIntervalMs"
|
|
144931
|
+
].filter((key) => legacy[key] !== void 0).map((key) => [key, legacy[key]]))
|
|
144932
|
+
});
|
|
144933
|
+
}
|
|
144934
|
+
//#endregion
|
|
144151
144935
|
//#region src/migrations/strip-echo-target-volume-and-mode.ts
|
|
144152
144936
|
var RETIRED_KEYS = ["volume", "mode"];
|
|
144153
144937
|
function isRecord(value) {
|
|
@@ -144197,6 +144981,21 @@ var ADDON_MIGRATIONS = [{
|
|
|
144197
144981
|
fixtureMark: "the Echo id and the name the migration does not know"
|
|
144198
144982
|
},
|
|
144199
144983
|
procedure: stripEchoTargetVolumeAndMode
|
|
144984
|
+
}, {
|
|
144985
|
+
meta: {
|
|
144986
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
144987
|
+
owner: "import-alexa",
|
|
144988
|
+
level: "addon-state",
|
|
144989
|
+
seq: 2,
|
|
144990
|
+
dependsOn: [],
|
|
144991
|
+
store: "settings",
|
|
144992
|
+
scope: "cluster",
|
|
144993
|
+
onFailure: "block-owner",
|
|
144994
|
+
bodyKind: "procedure",
|
|
144995
|
+
description: "Move each Alexa account from a nested connection object to flat top-level fields under the same id, reading every secret back first",
|
|
144996
|
+
fixtureMark: "the integration link and the amazon domain of each account"
|
|
144997
|
+
},
|
|
144998
|
+
procedure: flattenBrokerRecords
|
|
144200
144999
|
}];
|
|
144201
145000
|
//#endregion
|
|
144202
145001
|
//#region src/speak/icon-routes.ts
|
|
@@ -144251,7 +145050,6 @@ var RecordsSpeakSessionStore = class RecordsSpeakSessionStore {
|
|
|
144251
145050
|
//#region src/addon.ts
|
|
144252
145051
|
/** Default config — a fresh install starts with no accounts. */
|
|
144253
145052
|
var DEFAULTS = {
|
|
144254
|
-
brokers: [],
|
|
144255
145053
|
proxyLogin: { proxyPort: 3456 },
|
|
144256
145054
|
echoTargets: []
|
|
144257
145055
|
};
|
|
@@ -144299,6 +145097,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144299
145097
|
loginPlane = null;
|
|
144300
145098
|
loginProxy = null;
|
|
144301
145099
|
brokerStore = null;
|
|
145100
|
+
instances = null;
|
|
144302
145101
|
activeBrokers = [];
|
|
144303
145102
|
speak = null;
|
|
144304
145103
|
constructor() {
|
|
@@ -144310,6 +145109,12 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144310
145109
|
if (records === void 0) throw new Error("import-alexa: ctx.records is required (hub-only addon)");
|
|
144311
145110
|
await records.declare(alexaRecordKinds);
|
|
144312
145111
|
this.brokerStore = AlexaBrokerStore.of(records);
|
|
145112
|
+
this.instances = buildAlexaInstances(records, {
|
|
145113
|
+
mintId: () => this.requireRegistry().allocateId(),
|
|
145114
|
+
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
145115
|
+
onChanged: (id) => this.syncBrokerRuntime(id)
|
|
145116
|
+
});
|
|
145117
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
|
|
144313
145118
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
144314
145119
|
this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
|
|
144315
145120
|
dataDir: this.ctx.dataDir,
|
|
@@ -144354,12 +145159,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144354
145159
|
deviceCode: this.deviceCodeLogin,
|
|
144355
145160
|
commitCookie: (input) => this.commitCapturedCookie(input)
|
|
144356
145161
|
}), ...speakIconRoutes()];
|
|
144357
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS,
|
|
144358
|
-
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
144359
|
-
writeRows: async (rows) => {
|
|
144360
|
-
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
144361
|
-
}
|
|
144362
|
-
}));
|
|
145162
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.migrationPorts(records)));
|
|
144363
145163
|
const speak = new AlexaSpeakController({
|
|
144364
145164
|
logger: this.ctx.logger.child("speak"),
|
|
144365
145165
|
readConfig: () => this.config,
|
|
@@ -144389,6 +145189,10 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144389
145189
|
capability: brokerCapability,
|
|
144390
145190
|
provider: this.buildBrokerProvider()
|
|
144391
145191
|
},
|
|
145192
|
+
{
|
|
145193
|
+
capability: integrationInstancesCapability,
|
|
145194
|
+
provider: this.requireInstances().toProvider()
|
|
145195
|
+
},
|
|
144392
145196
|
{
|
|
144393
145197
|
capability: deviceAdoptionCapability,
|
|
144394
145198
|
provider: this.buildAdoptionProvider()
|
|
@@ -144403,19 +145207,65 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144403
145207
|
}
|
|
144404
145208
|
] };
|
|
144405
145209
|
}
|
|
144406
|
-
|
|
144407
|
-
|
|
144408
|
-
|
|
144409
|
-
|
|
144410
|
-
|
|
145210
|
+
migrationPorts(records) {
|
|
145211
|
+
if (this.ctx.secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
|
|
145212
|
+
return {
|
|
145213
|
+
readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
|
|
145214
|
+
writeRows: async (rows) => {
|
|
145215
|
+
await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
|
|
145216
|
+
},
|
|
145217
|
+
...flattenPortsOver(records.kind(alexaBrokersEntryKind))
|
|
145218
|
+
};
|
|
145219
|
+
}
|
|
145220
|
+
/**
|
|
145221
|
+
* Bring the runtime in line with one record after the instances page changed it: a removed
|
|
145222
|
+
* record stops its manager and cascades its devices, an off record only stops the manager, a
|
|
145223
|
+
* live one re-applies its connection or starts under the record's own id.
|
|
145224
|
+
*/
|
|
145225
|
+
async syncBrokerRuntime(id) {
|
|
145226
|
+
const reg = this.requireRegistry();
|
|
145227
|
+
const entry = await this.requireBrokerStore().getById(id);
|
|
145228
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
145229
|
+
if (entry === null) {
|
|
145230
|
+
await reg.removeEntry(id);
|
|
145231
|
+
await this.cascadeRemoveDevicesForBroker(id);
|
|
145232
|
+
this.speak?.brokersChanged();
|
|
145233
|
+
return;
|
|
144411
145234
|
}
|
|
144412
|
-
|
|
145235
|
+
const configured = entry.connection.cookie.length > 0 || entry.connection.refreshToken.length > 0;
|
|
145236
|
+
if (entry.enabled === false || !configured) {
|
|
145237
|
+
await reg.removeEntry(id);
|
|
145238
|
+
this.speak?.brokersChanged();
|
|
145239
|
+
return;
|
|
145240
|
+
}
|
|
145241
|
+
try {
|
|
145242
|
+
if (reg.get(id) !== null) await reg.updateEntry(id, entry.connection);
|
|
145243
|
+
else await reg.createEntry(entry.name, entry.connection, {
|
|
145244
|
+
id,
|
|
145245
|
+
...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
|
|
145246
|
+
});
|
|
145247
|
+
} catch (err) {
|
|
145248
|
+
this.ctx.logger.warn("alexa instance: could not start the account", { meta: {
|
|
145249
|
+
brokerId: id,
|
|
145250
|
+
error: errMsg(err)
|
|
145251
|
+
} });
|
|
145252
|
+
}
|
|
145253
|
+
this.speak?.brokersChanged();
|
|
145254
|
+
}
|
|
145255
|
+
requireBrokerStore() {
|
|
145256
|
+
if (this.brokerStore === null) throw new Error("import-alexa: broker store not ready");
|
|
145257
|
+
return this.brokerStore;
|
|
145258
|
+
}
|
|
145259
|
+
requireInstances() {
|
|
145260
|
+
if (this.instances === null) throw new Error("import-alexa: instances not ready");
|
|
145261
|
+
return this.instances;
|
|
145262
|
+
}
|
|
145263
|
+
async loadPersistedBrokers() {
|
|
145264
|
+
return this.requireBrokerStore().listResolved();
|
|
144413
145265
|
}
|
|
144414
145266
|
async persistBrokerEntries(entries) {
|
|
144415
145267
|
this.activeBrokers = [...entries];
|
|
144416
|
-
|
|
144417
|
-
if (store !== null) await store.replaceAll(entries);
|
|
144418
|
-
else await this.updateGlobalSettings({ brokers: [...entries] });
|
|
145268
|
+
await this.requireBrokerStore().replaceAll(entries);
|
|
144419
145269
|
this.speak?.brokersChanged();
|
|
144420
145270
|
}
|
|
144421
145271
|
async onConfigChanged() {
|
|
@@ -144434,6 +145284,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
144434
145284
|
} });
|
|
144435
145285
|
}
|
|
144436
145286
|
for (const entry of persisted) {
|
|
145287
|
+
if (entry.enabled === false) continue;
|
|
144437
145288
|
if (entry.id && liveIds.has(entry.id)) {
|
|
144438
145289
|
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
144439
145290
|
try {
|