@camstack/addon-provider-velux 0.2.156 → 0.2.158
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 +2399 -669
- package/dist/addon.mjs +2399 -669
- package/package.json +6 -2
package/dist/addon.mjs
CHANGED
|
@@ -821,7 +821,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
821
821
|
return EventCategory;
|
|
822
822
|
}({});
|
|
823
823
|
//#endregion
|
|
824
|
-
//#region ../types/dist/device-type-
|
|
824
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
825
825
|
/**
|
|
826
826
|
import { errMsg } from '@camstack/types'
|
|
827
827
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -1052,6 +1052,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
1052
1052
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
1053
1053
|
/** Media player exposes stop distinct from pause. */
|
|
1054
1054
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
1055
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
1056
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
1057
|
+
/** Media player exposes channel up / down in steps. */
|
|
1058
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
1059
|
+
/** Media player exposes power on / off. */
|
|
1060
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
1055
1061
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
1056
1062
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
1057
1063
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -4588,6 +4594,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4588
4594
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4589
4595
|
json.type = "boolean";
|
|
4590
4596
|
};
|
|
4597
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4598
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4599
|
+
};
|
|
4600
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
4601
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
4602
|
+
};
|
|
4591
4603
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4592
4604
|
if (ctx.target === "openapi-3.0") {
|
|
4593
4605
|
json.type = "string";
|
|
@@ -4595,12 +4607,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
4595
4607
|
json.enum = [null];
|
|
4596
4608
|
} else json.type = "null";
|
|
4597
4609
|
};
|
|
4610
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
4611
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
4612
|
+
};
|
|
4598
4613
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
4599
4614
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
4600
4615
|
};
|
|
4601
4616
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4602
4617
|
json.not = {};
|
|
4603
4618
|
};
|
|
4619
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4620
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4604
4621
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4605
4622
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
4606
4623
|
};
|
|
@@ -4632,12 +4649,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
4632
4649
|
json.enum = vals;
|
|
4633
4650
|
}
|
|
4634
4651
|
};
|
|
4652
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
4653
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
4654
|
+
};
|
|
4655
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
4656
|
+
const _json = json;
|
|
4657
|
+
const pattern = schema._zod.pattern;
|
|
4658
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
4659
|
+
_json.type = "string";
|
|
4660
|
+
_json.pattern = pattern.source;
|
|
4661
|
+
};
|
|
4662
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
4663
|
+
const _json = json;
|
|
4664
|
+
const file = {
|
|
4665
|
+
type: "string",
|
|
4666
|
+
format: "binary",
|
|
4667
|
+
contentEncoding: "binary"
|
|
4668
|
+
};
|
|
4669
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
4670
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
4671
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
4672
|
+
if (mime) if (mime.length === 1) {
|
|
4673
|
+
file.contentMediaType = mime[0];
|
|
4674
|
+
Object.assign(_json, file);
|
|
4675
|
+
} else {
|
|
4676
|
+
Object.assign(_json, file);
|
|
4677
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
4678
|
+
}
|
|
4679
|
+
else Object.assign(_json, file);
|
|
4680
|
+
};
|
|
4681
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
4682
|
+
json.type = "boolean";
|
|
4683
|
+
};
|
|
4635
4684
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
4636
4685
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
4637
4686
|
};
|
|
4687
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
4688
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
4689
|
+
};
|
|
4638
4690
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
4639
4691
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
4640
4692
|
};
|
|
4693
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
4694
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
4695
|
+
};
|
|
4696
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
4697
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
4698
|
+
};
|
|
4641
4699
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
4642
4700
|
const json = _json;
|
|
4643
4701
|
const def = schema._zod.def;
|
|
@@ -4842,6 +4900,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
4842
4900
|
seen.ref = def.innerType;
|
|
4843
4901
|
json.readOnly = true;
|
|
4844
4902
|
};
|
|
4903
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
4904
|
+
const def = schema._zod.def;
|
|
4905
|
+
process$1(def.innerType, ctx, params);
|
|
4906
|
+
const seen = ctx.seen.get(schema);
|
|
4907
|
+
seen.ref = def.innerType;
|
|
4908
|
+
};
|
|
4845
4909
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4846
4910
|
const def = schema._zod.def;
|
|
4847
4911
|
process$1(def.innerType, ctx, params);
|
|
@@ -4854,6 +4918,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
4854
4918
|
const seen = ctx.seen.get(schema);
|
|
4855
4919
|
seen.ref = innerType;
|
|
4856
4920
|
};
|
|
4921
|
+
var allProcessors = {
|
|
4922
|
+
string: stringProcessor,
|
|
4923
|
+
number: numberProcessor,
|
|
4924
|
+
boolean: booleanProcessor,
|
|
4925
|
+
bigint: bigintProcessor,
|
|
4926
|
+
symbol: symbolProcessor,
|
|
4927
|
+
null: nullProcessor,
|
|
4928
|
+
undefined: undefinedProcessor,
|
|
4929
|
+
void: voidProcessor,
|
|
4930
|
+
never: neverProcessor,
|
|
4931
|
+
any: anyProcessor,
|
|
4932
|
+
unknown: unknownProcessor,
|
|
4933
|
+
date: dateProcessor,
|
|
4934
|
+
enum: enumProcessor,
|
|
4935
|
+
literal: literalProcessor,
|
|
4936
|
+
nan: nanProcessor,
|
|
4937
|
+
template_literal: templateLiteralProcessor,
|
|
4938
|
+
file: fileProcessor,
|
|
4939
|
+
success: successProcessor,
|
|
4940
|
+
custom: customProcessor,
|
|
4941
|
+
function: functionProcessor,
|
|
4942
|
+
transform: transformProcessor,
|
|
4943
|
+
map: mapProcessor,
|
|
4944
|
+
set: setProcessor,
|
|
4945
|
+
array: arrayProcessor,
|
|
4946
|
+
object: objectProcessor,
|
|
4947
|
+
union: unionProcessor,
|
|
4948
|
+
intersection: intersectionProcessor,
|
|
4949
|
+
tuple: tupleProcessor,
|
|
4950
|
+
record: recordProcessor,
|
|
4951
|
+
nullable: nullableProcessor,
|
|
4952
|
+
nonoptional: nonoptionalProcessor,
|
|
4953
|
+
default: defaultProcessor,
|
|
4954
|
+
prefault: prefaultProcessor,
|
|
4955
|
+
catch: catchProcessor,
|
|
4956
|
+
pipe: pipeProcessor,
|
|
4957
|
+
readonly: readonlyProcessor,
|
|
4958
|
+
promise: promiseProcessor,
|
|
4959
|
+
optional: optionalProcessor,
|
|
4960
|
+
lazy: lazyProcessor
|
|
4961
|
+
};
|
|
4962
|
+
function toJSONSchema(input, params) {
|
|
4963
|
+
if ("_idmap" in input) {
|
|
4964
|
+
const registry = input;
|
|
4965
|
+
const ctx = initializeContext({
|
|
4966
|
+
...params,
|
|
4967
|
+
processors: allProcessors
|
|
4968
|
+
});
|
|
4969
|
+
const defs = {};
|
|
4970
|
+
for (const entry of registry._idmap.entries()) {
|
|
4971
|
+
const [_, schema] = entry;
|
|
4972
|
+
process$1(schema, ctx);
|
|
4973
|
+
}
|
|
4974
|
+
const schemas = {};
|
|
4975
|
+
ctx.external = {
|
|
4976
|
+
registry,
|
|
4977
|
+
uri: params?.uri,
|
|
4978
|
+
defs
|
|
4979
|
+
};
|
|
4980
|
+
for (const entry of registry._idmap.entries()) {
|
|
4981
|
+
const [key, schema] = entry;
|
|
4982
|
+
extractDefs(ctx, schema);
|
|
4983
|
+
schemas[key] = finalize(ctx, schema);
|
|
4984
|
+
}
|
|
4985
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
4986
|
+
return { schemas };
|
|
4987
|
+
}
|
|
4988
|
+
const ctx = initializeContext({
|
|
4989
|
+
...params,
|
|
4990
|
+
processors: allProcessors
|
|
4991
|
+
});
|
|
4992
|
+
process$1(input, ctx);
|
|
4993
|
+
extractDefs(ctx, input);
|
|
4994
|
+
return finalize(ctx, input);
|
|
4995
|
+
}
|
|
4857
4996
|
//#endregion
|
|
4858
4997
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
4859
4998
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -5878,7 +6017,7 @@ var ZodIssueCode = {
|
|
|
5878
6017
|
var ZodFirstPartyTypeKind;
|
|
5879
6018
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5880
6019
|
//#endregion
|
|
5881
|
-
//#region ../types/dist/sleep-
|
|
6020
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5882
6021
|
/**
|
|
5883
6022
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5884
6023
|
* window to float samples (D455).
|
|
@@ -5957,6 +6096,22 @@ function buildAlawTable() {
|
|
|
5957
6096
|
}
|
|
5958
6097
|
buildUlawTable();
|
|
5959
6098
|
buildAlawTable();
|
|
6099
|
+
/**
|
|
6100
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
6101
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
6102
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
6103
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
6104
|
+
*/
|
|
6105
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
6106
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
6107
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
6108
|
+
migrationIds;
|
|
6109
|
+
constructor(migrationIds) {
|
|
6110
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
6111
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
6112
|
+
this.migrationIds = migrationIds;
|
|
6113
|
+
}
|
|
6114
|
+
};
|
|
5960
6115
|
/** Is this field's stored value a credential? */
|
|
5961
6116
|
function isSecretConfigField(field) {
|
|
5962
6117
|
if (field.type === "password") return true;
|
|
@@ -6010,9 +6165,16 @@ function isSecretMarker(value) {
|
|
|
6010
6165
|
const ref = value["$secret"];
|
|
6011
6166
|
return typeof ref === "string" && ref.length > 0;
|
|
6012
6167
|
}
|
|
6168
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6169
|
+
function isSecretMarkerLike(value) {
|
|
6170
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6171
|
+
}
|
|
6013
6172
|
function isSecretClear(value) {
|
|
6014
6173
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6015
6174
|
}
|
|
6175
|
+
function isSecretFieldView(value) {
|
|
6176
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6177
|
+
}
|
|
6016
6178
|
/**
|
|
6017
6179
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6018
6180
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6394,6 +6556,53 @@ function emitReadiness(bus, params) {
|
|
|
6394
6556
|
ts
|
|
6395
6557
|
}));
|
|
6396
6558
|
}
|
|
6559
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
6560
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
6561
|
+
id;
|
|
6562
|
+
constructor(id, message) {
|
|
6563
|
+
super(`${id}: ${message}`);
|
|
6564
|
+
this.id = id;
|
|
6565
|
+
this.name = "AddonMigrationDefinitionError";
|
|
6566
|
+
}
|
|
6567
|
+
};
|
|
6568
|
+
/**
|
|
6569
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
6570
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
6571
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
6572
|
+
*/
|
|
6573
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
6574
|
+
return catalogue.map((entry) => {
|
|
6575
|
+
const { meta } = entry;
|
|
6576
|
+
if ("plan" in entry) {
|
|
6577
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
6578
|
+
return {
|
|
6579
|
+
meta,
|
|
6580
|
+
execute: () => guarded(async () => ({
|
|
6581
|
+
kind: "plan",
|
|
6582
|
+
plan: await entry.plan(ports)
|
|
6583
|
+
}))
|
|
6584
|
+
};
|
|
6585
|
+
}
|
|
6586
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
6587
|
+
return {
|
|
6588
|
+
meta,
|
|
6589
|
+
execute: () => guarded(async () => ({
|
|
6590
|
+
kind: "procedure",
|
|
6591
|
+
result: await entry.procedure(ports)
|
|
6592
|
+
}))
|
|
6593
|
+
};
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6596
|
+
async function guarded(work) {
|
|
6597
|
+
try {
|
|
6598
|
+
return await work();
|
|
6599
|
+
} catch (err) {
|
|
6600
|
+
return {
|
|
6601
|
+
kind: "failed",
|
|
6602
|
+
error: errMsg(err)
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6397
6606
|
var NOT_RUN = {
|
|
6398
6607
|
declared: false,
|
|
6399
6608
|
snapshot: null,
|
|
@@ -6408,7 +6617,7 @@ var NOT_RUN = {
|
|
|
6408
6617
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6409
6618
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6410
6619
|
*/
|
|
6411
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6620
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6412
6621
|
const first = migrations[0];
|
|
6413
6622
|
if (first === void 0) return NOT_RUN;
|
|
6414
6623
|
const owner = first.meta.owner;
|
|
@@ -6437,7 +6646,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6437
6646
|
else if (status === "blocked") blocked.push(id);
|
|
6438
6647
|
else skipped.push(id);
|
|
6439
6648
|
};
|
|
6649
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6440
6650
|
for (const step of declaration.steps) {
|
|
6651
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6441
6652
|
const bound = byId.get(step.id);
|
|
6442
6653
|
if (bound === void 0 || step.action === "skip") {
|
|
6443
6654
|
skipped.push(step.id);
|
|
@@ -6615,11 +6826,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6615
6826
|
}
|
|
6616
6827
|
return out;
|
|
6617
6828
|
}
|
|
6829
|
+
/**
|
|
6830
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6831
|
+
*
|
|
6832
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6833
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6834
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6835
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6836
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6837
|
+
* copied onto another ref does not decrypt.
|
|
6838
|
+
*
|
|
6839
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6840
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6841
|
+
*
|
|
6842
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6843
|
+
*/
|
|
6844
|
+
/** The URI scheme of every keychain ref. */
|
|
6845
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6618
6846
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6619
6847
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6620
6848
|
function globalSecretScope() {
|
|
6621
6849
|
return GLOBAL_SCOPE;
|
|
6622
6850
|
}
|
|
6851
|
+
/**
|
|
6852
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6853
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6854
|
+
* addon running forked.
|
|
6855
|
+
*/
|
|
6856
|
+
function normalizeSecretOwner(addonId) {
|
|
6857
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6858
|
+
}
|
|
6623
6859
|
function encodeSegment(value) {
|
|
6624
6860
|
return encodeURIComponent(value);
|
|
6625
6861
|
}
|
|
@@ -6630,6 +6866,16 @@ function decodeSegment(value) {
|
|
|
6630
6866
|
return null;
|
|
6631
6867
|
}
|
|
6632
6868
|
}
|
|
6869
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6870
|
+
function formatSecretScope(scope) {
|
|
6871
|
+
switch (scope.kind) {
|
|
6872
|
+
case "global": return "global";
|
|
6873
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6874
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6875
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6876
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6633
6879
|
/**
|
|
6634
6880
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6635
6881
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6638,6 +6884,13 @@ function decodeSegment(value) {
|
|
|
6638
6884
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6639
6885
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6640
6886
|
}
|
|
6887
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6888
|
+
function buildSecretRef(parts) {
|
|
6889
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6890
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6891
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6892
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6893
|
+
}
|
|
6641
6894
|
function parseScope(segments) {
|
|
6642
6895
|
switch (segments[0]) {
|
|
6643
6896
|
case "global": return {
|
|
@@ -7555,15 +7808,19 @@ var BaseAddon = class {
|
|
|
7555
7808
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7556
7809
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7557
7810
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7811
|
+
*
|
|
7812
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7813
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7814
|
+
* omitted applied ids "newer").
|
|
7558
7815
|
*/
|
|
7559
|
-
async runMigrations(migrations) {
|
|
7816
|
+
async runMigrations(migrations, options) {
|
|
7560
7817
|
const api = this.ctx.api.migrations;
|
|
7561
7818
|
const report = await runAddonMigrations({
|
|
7562
7819
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7563
7820
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7564
7821
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7565
|
-
}, migrations, this.ctx.logger);
|
|
7566
|
-
if (report.blockOwner.length > 0) throw new
|
|
7822
|
+
}, migrations, this.ctx.logger, options);
|
|
7823
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7567
7824
|
return report;
|
|
7568
7825
|
}
|
|
7569
7826
|
/**
|
|
@@ -7903,6 +8160,10 @@ var BaseAddon = class {
|
|
|
7903
8160
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
7904
8161
|
const patch = {};
|
|
7905
8162
|
for (const key of keys) {
|
|
8163
|
+
if (tree.get(key)?.kind === "array") {
|
|
8164
|
+
patch[key] = [];
|
|
8165
|
+
continue;
|
|
8166
|
+
}
|
|
7906
8167
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
7907
8168
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
7908
8169
|
}
|
|
@@ -8542,6 +8803,45 @@ function normalizeAddonInitResult(result) {
|
|
|
8542
8803
|
if (Array.isArray(result)) return { providers: result };
|
|
8543
8804
|
return result;
|
|
8544
8805
|
}
|
|
8806
|
+
var PREFIX = "records/";
|
|
8807
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8808
|
+
var RecordsError = class extends Error {
|
|
8809
|
+
code;
|
|
8810
|
+
detail;
|
|
8811
|
+
constructor(code, detail) {
|
|
8812
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
8813
|
+
this.name = "RecordsError";
|
|
8814
|
+
this.code = code;
|
|
8815
|
+
this.detail = detail;
|
|
8816
|
+
}
|
|
8817
|
+
};
|
|
8818
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8819
|
+
var RevConflictError = class extends RecordsError {
|
|
8820
|
+
constructor(detail) {
|
|
8821
|
+
super("rev-conflict", detail);
|
|
8822
|
+
this.name = "RevConflictError";
|
|
8823
|
+
}
|
|
8824
|
+
};
|
|
8825
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8826
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8827
|
+
constructor(detail) {
|
|
8828
|
+
super("secret-unavailable", detail);
|
|
8829
|
+
this.name = "RecordSecretUnavailableError";
|
|
8830
|
+
}
|
|
8831
|
+
};
|
|
8832
|
+
var InvalidRecordError = class extends RecordsError {
|
|
8833
|
+
constructor(detail) {
|
|
8834
|
+
super("invalid-record", detail);
|
|
8835
|
+
this.name = "InvalidRecordError";
|
|
8836
|
+
}
|
|
8837
|
+
};
|
|
8838
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
8839
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
8840
|
+
constructor(detail) {
|
|
8841
|
+
super("declaration-refused", detail);
|
|
8842
|
+
this.name = "DeclarationRefusedError";
|
|
8843
|
+
}
|
|
8844
|
+
};
|
|
8545
8845
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8546
8846
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8547
8847
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -8557,6 +8857,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
8557
8857
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8558
8858
|
var RECORD_BATCH_MAX = 1e3;
|
|
8559
8859
|
var RECORD_QUERY_MAX = 1e3;
|
|
8860
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8861
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8862
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8863
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8864
|
+
/**
|
|
8865
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8866
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8867
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
8868
|
+
*
|
|
8869
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8870
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8871
|
+
* key releases exactly that key's secret.
|
|
8872
|
+
*
|
|
8873
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8874
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8875
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8876
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
8877
|
+
*
|
|
8878
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8879
|
+
*/
|
|
8880
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8881
|
+
function parseSecretPath(path) {
|
|
8882
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8883
|
+
return path.split(".").map((raw) => {
|
|
8884
|
+
const each = raw.endsWith("[]");
|
|
8885
|
+
const map = raw.endsWith("{}");
|
|
8886
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8887
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8888
|
+
return {
|
|
8889
|
+
key,
|
|
8890
|
+
each,
|
|
8891
|
+
map
|
|
8892
|
+
};
|
|
8893
|
+
});
|
|
8894
|
+
}
|
|
8895
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8896
|
+
function secretArrayPrefixes(path) {
|
|
8897
|
+
const segments = parseSecretPath(path);
|
|
8898
|
+
const prefixes = [];
|
|
8899
|
+
const walked = [];
|
|
8900
|
+
for (const segment of segments) {
|
|
8901
|
+
walked.push(segment.key);
|
|
8902
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
8903
|
+
}
|
|
8904
|
+
return prefixes;
|
|
8905
|
+
}
|
|
8906
|
+
function isRecord$4(value) {
|
|
8907
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8908
|
+
}
|
|
8909
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8910
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8911
|
+
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`);
|
|
8912
|
+
return id;
|
|
8913
|
+
}
|
|
8914
|
+
/**
|
|
8915
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8916
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8917
|
+
* result is a new body, `data` is untouched.
|
|
8918
|
+
*/
|
|
8919
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8920
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8921
|
+
if (!isRecord$4(node)) return node;
|
|
8922
|
+
const segment = segments[index];
|
|
8923
|
+
if (segment === void 0) return node;
|
|
8924
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8925
|
+
const child = node[segment.key];
|
|
8926
|
+
const keys = [...walkedKeys, segment.key];
|
|
8927
|
+
const last = index === segments.length - 1;
|
|
8928
|
+
if (segment.each) {
|
|
8929
|
+
if (!Array.isArray(child)) return node;
|
|
8930
|
+
const prefix = keys.join(".");
|
|
8931
|
+
const idKey = arrayKeys[prefix];
|
|
8932
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8933
|
+
const mapped = child.map((element) => {
|
|
8934
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8935
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8936
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8937
|
+
});
|
|
8938
|
+
return {
|
|
8939
|
+
...node,
|
|
8940
|
+
[segment.key]: mapped
|
|
8941
|
+
};
|
|
8942
|
+
}
|
|
8943
|
+
if (segment.map) {
|
|
8944
|
+
if (!isRecord$4(child)) return node;
|
|
8945
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8946
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8947
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8948
|
+
return [mapKey, visit({
|
|
8949
|
+
declared,
|
|
8950
|
+
concrete,
|
|
8951
|
+
value
|
|
8952
|
+
})];
|
|
8953
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8954
|
+
return {
|
|
8955
|
+
...node,
|
|
8956
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8960
|
+
if (last) {
|
|
8961
|
+
const replacement = visit({
|
|
8962
|
+
declared,
|
|
8963
|
+
concrete,
|
|
8964
|
+
value: child
|
|
8965
|
+
});
|
|
8966
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8967
|
+
return {
|
|
8968
|
+
...node,
|
|
8969
|
+
[segment.key]: replacement
|
|
8970
|
+
};
|
|
8971
|
+
}
|
|
8972
|
+
return {
|
|
8973
|
+
...node,
|
|
8974
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8975
|
+
};
|
|
8976
|
+
}
|
|
8977
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8978
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8979
|
+
let current = data;
|
|
8980
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8981
|
+
return current;
|
|
8982
|
+
}
|
|
8983
|
+
/** Every present secret leaf, read-only. */
|
|
8984
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
8985
|
+
const found = [];
|
|
8986
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
8987
|
+
found.push(leaf);
|
|
8988
|
+
return leaf.value;
|
|
8989
|
+
});
|
|
8990
|
+
return found;
|
|
8991
|
+
}
|
|
8992
|
+
/**
|
|
8993
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8994
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8995
|
+
* already holds during an adoption.
|
|
8996
|
+
*/
|
|
8997
|
+
function recordSecretCollection(kind) {
|
|
8998
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8999
|
+
}
|
|
9000
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
9001
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
9002
|
+
return buildSecretRef({
|
|
9003
|
+
owner,
|
|
9004
|
+
scope: {
|
|
9005
|
+
kind: "row",
|
|
9006
|
+
collection: recordSecretCollection(kind),
|
|
9007
|
+
rowId
|
|
9008
|
+
},
|
|
9009
|
+
fieldPath: concretePath
|
|
9010
|
+
});
|
|
9011
|
+
}
|
|
9012
|
+
/**
|
|
9013
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
9014
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
9015
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
9016
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
9017
|
+
* stored secret (the records door reads a view as KEEP).
|
|
9018
|
+
*
|
|
9019
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
9020
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
9021
|
+
*/
|
|
9022
|
+
function isRecord$3(value) {
|
|
9023
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9024
|
+
}
|
|
9025
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
9026
|
+
if (decl.secretPaths.length === 0) return data;
|
|
9027
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
9028
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
9029
|
+
return {
|
|
9030
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
9031
|
+
v: 1,
|
|
9032
|
+
set: true
|
|
9033
|
+
};
|
|
9034
|
+
});
|
|
9035
|
+
return isRecord$3(shown) ? shown : data;
|
|
9036
|
+
}
|
|
8560
9037
|
/**
|
|
8561
9038
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8562
9039
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8629,7 +9106,8 @@ function isZodSecret(schema) {
|
|
|
8629
9106
|
}
|
|
8630
9107
|
/**
|
|
8631
9108
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
8632
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9109
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9110
|
+
* map" (`applianceSecrets{}`). A declared
|
|
8633
9111
|
* object or array is reported as one path and not descended into — everything
|
|
8634
9112
|
* under it is secret.
|
|
8635
9113
|
*/
|
|
@@ -8640,6 +9118,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
8640
9118
|
const def = defOf(layer);
|
|
8641
9119
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
8642
9120
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9121
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
8643
9122
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
8644
9123
|
else if (def.type === "intersection") {
|
|
8645
9124
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -8714,6 +9193,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
8714
9193
|
}).strict();
|
|
8715
9194
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
8716
9195
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9196
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9197
|
+
function assertValidDeclaration(name, decl) {
|
|
9198
|
+
const refuse = (why) => {
|
|
9199
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9200
|
+
};
|
|
9201
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9202
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9203
|
+
for (const index of decl.indexes) {
|
|
9204
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9205
|
+
seen.add(index.name);
|
|
9206
|
+
}
|
|
9207
|
+
const { retention } = decl;
|
|
9208
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
9209
|
+
if ("ttlMs" in retention) {
|
|
9210
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9211
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9212
|
+
}
|
|
9213
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9214
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9215
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9216
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9217
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9218
|
+
if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
|
|
9219
|
+
}
|
|
9220
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9221
|
+
for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
|
|
9222
|
+
}
|
|
9223
|
+
function bodyFieldsOf(schema) {
|
|
9224
|
+
return { ...toJSONSchema(schema, {
|
|
9225
|
+
unrepresentable: "any",
|
|
9226
|
+
io: "input"
|
|
9227
|
+
}) };
|
|
9228
|
+
}
|
|
9229
|
+
function isRecordLike(value) {
|
|
9230
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9231
|
+
}
|
|
9232
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9233
|
+
function hasNameField(json, field) {
|
|
9234
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9235
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9236
|
+
const properties = json["properties"];
|
|
9237
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9238
|
+
}
|
|
9239
|
+
/**
|
|
9240
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9241
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9242
|
+
* types its handle from.
|
|
9243
|
+
*/
|
|
9244
|
+
function defineRecordKinds(specs) {
|
|
9245
|
+
const out = {};
|
|
9246
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
9247
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9248
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
9249
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
9250
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9251
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
9252
|
+
database: spec.database,
|
|
9253
|
+
class: spec.class,
|
|
9254
|
+
writeTrigger: spec.writeTrigger,
|
|
9255
|
+
device: spec.device,
|
|
9256
|
+
at: spec.at,
|
|
9257
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9258
|
+
retention: spec.retention,
|
|
9259
|
+
deviceRemoval: spec.deviceRemoval,
|
|
9260
|
+
maxDataBytes: spec.maxDataBytes,
|
|
9261
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9262
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9263
|
+
transactFamily: spec.transactFamily,
|
|
9264
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
9265
|
+
title: spec.entry.title,
|
|
9266
|
+
nameField,
|
|
9267
|
+
fields
|
|
9268
|
+
}
|
|
9269
|
+
});
|
|
9270
|
+
assertValidDeclaration(name, parsed);
|
|
9271
|
+
out[name] = {
|
|
9272
|
+
name,
|
|
9273
|
+
schema: spec.schema,
|
|
9274
|
+
declaration: parsed
|
|
9275
|
+
};
|
|
9276
|
+
}
|
|
9277
|
+
return out;
|
|
9278
|
+
}
|
|
9279
|
+
/**
|
|
9280
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
9281
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9282
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9283
|
+
* through the `record-entries` capability with password fields derived from
|
|
9284
|
+
* the schema (`.meta({ secret: true })`).
|
|
9285
|
+
*/
|
|
9286
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9287
|
+
function entryBody(shape) {
|
|
9288
|
+
return object({
|
|
9289
|
+
name: string().min(1),
|
|
9290
|
+
...shape
|
|
9291
|
+
});
|
|
9292
|
+
}
|
|
9293
|
+
/**
|
|
9294
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9295
|
+
* engine, never aged, belonging to no device.
|
|
9296
|
+
*/
|
|
9297
|
+
function defineEntryKind(name, spec) {
|
|
9298
|
+
const defined = defineRecordKinds({ [name]: {
|
|
9299
|
+
schema: spec.schema,
|
|
9300
|
+
class: "config",
|
|
9301
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9302
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9303
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9304
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9305
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9306
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9307
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9308
|
+
entry: {
|
|
9309
|
+
title: spec.title,
|
|
9310
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9311
|
+
}
|
|
9312
|
+
} })[name];
|
|
9313
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9314
|
+
return defined;
|
|
9315
|
+
}
|
|
8717
9316
|
object({
|
|
8718
9317
|
field: string().min(1),
|
|
8719
9318
|
on: literal("advance")
|
|
@@ -9392,6 +9991,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9392
9991
|
"backup",
|
|
9393
9992
|
"battery",
|
|
9394
9993
|
"binary",
|
|
9994
|
+
"board-key-source",
|
|
9395
9995
|
"brightness",
|
|
9396
9996
|
"broker",
|
|
9397
9997
|
"button",
|
|
@@ -9513,6 +10113,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9513
10113
|
"privacy-mask",
|
|
9514
10114
|
"ptz",
|
|
9515
10115
|
"ptz-autotrack",
|
|
10116
|
+
"pump-cycle",
|
|
9516
10117
|
"reboot",
|
|
9517
10118
|
"record-entries",
|
|
9518
10119
|
"recording",
|
|
@@ -10805,18 +11406,6 @@ Object.freeze({
|
|
|
10805
11406
|
"path": "pumpRunning",
|
|
10806
11407
|
"valueType": "boolean"
|
|
10807
11408
|
},
|
|
10808
|
-
{
|
|
10809
|
-
"path": "pumpIntervalEnabled",
|
|
10810
|
-
"valueType": "boolean"
|
|
10811
|
-
},
|
|
10812
|
-
{
|
|
10813
|
-
"path": "pumpOnTime",
|
|
10814
|
-
"valueType": "number"
|
|
10815
|
-
},
|
|
10816
|
-
{
|
|
10817
|
-
"path": "pumpOffTime",
|
|
10818
|
-
"valueType": "number"
|
|
10819
|
-
},
|
|
10820
11409
|
{
|
|
10821
11410
|
"path": "lastDrink",
|
|
10822
11411
|
"valueType": "number"
|
|
@@ -10940,6 +11529,20 @@ Object.freeze({
|
|
|
10940
11529
|
"valueType": "number"
|
|
10941
11530
|
}
|
|
10942
11531
|
],
|
|
11532
|
+
"pump-cycle": [
|
|
11533
|
+
{
|
|
11534
|
+
"path": "enabled",
|
|
11535
|
+
"valueType": "boolean"
|
|
11536
|
+
},
|
|
11537
|
+
{
|
|
11538
|
+
"path": "onTime",
|
|
11539
|
+
"valueType": "number"
|
|
11540
|
+
},
|
|
11541
|
+
{
|
|
11542
|
+
"path": "offTime",
|
|
11543
|
+
"valueType": "number"
|
|
11544
|
+
}
|
|
11545
|
+
],
|
|
10943
11546
|
"recording-archive": [
|
|
10944
11547
|
{
|
|
10945
11548
|
"path": "deviceId",
|
|
@@ -11389,7 +11992,7 @@ object({
|
|
|
11389
11992
|
params: record(string(), union([string(), number()])).optional()
|
|
11390
11993
|
});
|
|
11391
11994
|
//#endregion
|
|
11392
|
-
//#region ../types/dist/composition-
|
|
11995
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11393
11996
|
/**
|
|
11394
11997
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11395
11998
|
*
|
|
@@ -12152,38 +12755,6 @@ function validateExpressionSource(src) {
|
|
|
12152
12755
|
return null;
|
|
12153
12756
|
}
|
|
12154
12757
|
/**
|
|
12155
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12156
|
-
*
|
|
12157
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12158
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12159
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12160
|
-
*
|
|
12161
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12162
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12163
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12164
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12165
|
-
* can move later without moving the others.
|
|
12166
|
-
*/
|
|
12167
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12168
|
-
var APPLIANCE_KINDS = [
|
|
12169
|
-
"washer",
|
|
12170
|
-
"dryer",
|
|
12171
|
-
"dishwasher"
|
|
12172
|
-
];
|
|
12173
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12174
|
-
/**
|
|
12175
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12176
|
-
*
|
|
12177
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12178
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12179
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12180
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12181
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12182
|
-
* the smallest radiator.
|
|
12183
|
-
*/
|
|
12184
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12185
|
-
DeviceType.Heater;
|
|
12186
|
-
/**
|
|
12187
12758
|
* Statistics and integrals over a history read (D944).
|
|
12188
12759
|
*
|
|
12189
12760
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12327,34 +12898,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12327
12898
|
/** Actuator groups only. Readings leave this unset. */
|
|
12328
12899
|
active: ActiveCriterionSchema.optional()
|
|
12329
12900
|
}),
|
|
12330
|
-
object({
|
|
12331
|
-
kind: literal("appliance"),
|
|
12332
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12333
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12334
|
-
thresholdWatts: number().positive(),
|
|
12335
|
-
startDwellMs: number().int().nonnegative(),
|
|
12336
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12337
|
-
member: HelperMemberSchema,
|
|
12338
|
-
/**
|
|
12339
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12340
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12341
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12342
|
-
*/
|
|
12343
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12344
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12345
|
-
offSeconds: number().optional()
|
|
12346
|
-
}),
|
|
12347
|
-
object({
|
|
12348
|
-
kind: literal("heater"),
|
|
12349
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12350
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12351
|
-
plug: HelperMemberSchema,
|
|
12352
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12353
|
-
meter: HelperMemberSchema.optional(),
|
|
12354
|
-
thresholdWatts: number().positive(),
|
|
12355
|
-
startDwellMs: number().int().nonnegative(),
|
|
12356
|
-
stopDwellMs: number().int().nonnegative()
|
|
12357
|
-
}),
|
|
12358
12901
|
object({
|
|
12359
12902
|
kind: literal("statistic"),
|
|
12360
12903
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12526,7 +13069,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
12526
13069
|
method: string().min(1),
|
|
12527
13070
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
12528
13071
|
});
|
|
12529
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
13072
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
13073
|
+
CompositionForwardCommandSchema,
|
|
13074
|
+
CompositionCodeSourceSchema,
|
|
13075
|
+
object({
|
|
13076
|
+
kind: literal("extension"),
|
|
13077
|
+
target: literal("remote")
|
|
13078
|
+
})
|
|
13079
|
+
]);
|
|
12530
13080
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
12531
13081
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
12532
13082
|
/**
|
|
@@ -12571,7 +13121,7 @@ var CompositionSchema = object({
|
|
|
12571
13121
|
kind: literal("existing"),
|
|
12572
13122
|
device: CompositionSourceRefSchema
|
|
12573
13123
|
})]),
|
|
12574
|
-
features: array(CompositionFeatureSchema).min(
|
|
13124
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12575
13125
|
/**
|
|
12576
13126
|
* Present when the block is a helper template. Absent on a free
|
|
12577
13127
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12603,6 +13153,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12603
13153
|
"node-g",
|
|
12604
13154
|
"node-h"
|
|
12605
13155
|
]);
|
|
13156
|
+
//#endregion
|
|
13157
|
+
//#region ../types/dist/index.mjs
|
|
13158
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
13159
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
13160
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
13161
|
+
/** The text excerpt a message carries on the bus. */
|
|
13162
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
13163
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
13164
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
13165
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
13166
|
+
object({
|
|
13167
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
13168
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
13169
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
13170
|
+
requireAuth: boolean().default(true),
|
|
13171
|
+
username: string().default(""),
|
|
13172
|
+
password: string().default("").meta({ secret: true }),
|
|
13173
|
+
starttls: boolean().default(false),
|
|
13174
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
13175
|
+
});
|
|
13176
|
+
object({
|
|
13177
|
+
host: string().min(1).nullable(),
|
|
13178
|
+
hostReason: string().nullable(),
|
|
13179
|
+
port: PortSchema,
|
|
13180
|
+
domain: string().min(1),
|
|
13181
|
+
requireAuth: boolean(),
|
|
13182
|
+
username: string(),
|
|
13183
|
+
password: string(),
|
|
13184
|
+
starttls: boolean()
|
|
13185
|
+
});
|
|
13186
|
+
object({
|
|
13187
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13188
|
+
/**
|
|
13189
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13190
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13191
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13192
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13193
|
+
*/
|
|
13194
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13195
|
+
});
|
|
13196
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13197
|
+
filename: string(),
|
|
13198
|
+
contentType: string(),
|
|
13199
|
+
bytes: number().int().nonnegative()
|
|
13200
|
+
});
|
|
13201
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13202
|
+
var SmtpInboundMessageSchema = object({
|
|
13203
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13204
|
+
seq: number().int().positive(),
|
|
13205
|
+
messageId: string(),
|
|
13206
|
+
receivedAt: number().int().nonnegative(),
|
|
13207
|
+
from: string(),
|
|
13208
|
+
to: string(),
|
|
13209
|
+
subject: string(),
|
|
13210
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13211
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13212
|
+
});
|
|
13213
|
+
object({
|
|
13214
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13215
|
+
lastSeq: number().int().nonnegative(),
|
|
13216
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13217
|
+
oldestSeq: number().int().nonnegative(),
|
|
13218
|
+
messages: array(object({
|
|
13219
|
+
key: string(),
|
|
13220
|
+
message: SmtpInboundMessageSchema
|
|
13221
|
+
}))
|
|
13222
|
+
});
|
|
12606
13223
|
new Set([
|
|
12607
13224
|
"track",
|
|
12608
13225
|
"face",
|
|
@@ -12722,6 +13339,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
12722
13339
|
]),
|
|
12723
13340
|
candidateStableId: string().optional()
|
|
12724
13341
|
});
|
|
13342
|
+
/**
|
|
13343
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13344
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13345
|
+
*
|
|
13346
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13347
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13348
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13349
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13350
|
+
*
|
|
13351
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13352
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13353
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13354
|
+
*/
|
|
13355
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13356
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13357
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13358
|
+
function topLevelSecretKeys(paths) {
|
|
13359
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13360
|
+
}
|
|
13361
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13362
|
+
function secretMapKeys(paths) {
|
|
13363
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13364
|
+
}
|
|
13365
|
+
/**
|
|
13366
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13367
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13368
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13369
|
+
*/
|
|
13370
|
+
function secretMapPatch(value) {
|
|
13371
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13372
|
+
const out = {};
|
|
13373
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13374
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13375
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13376
|
+
}
|
|
13377
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13378
|
+
function declaresEnabled(kind) {
|
|
13379
|
+
const json = toJSONSchema(kind.schema, {
|
|
13380
|
+
unrepresentable: "any",
|
|
13381
|
+
io: "input"
|
|
13382
|
+
});
|
|
13383
|
+
const properties = Reflect.get(json, "properties");
|
|
13384
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13385
|
+
}
|
|
13386
|
+
var RecordIntegrationInstances = class {
|
|
13387
|
+
records;
|
|
13388
|
+
kind;
|
|
13389
|
+
hooks;
|
|
13390
|
+
handle;
|
|
13391
|
+
secretKeys;
|
|
13392
|
+
secretMaps;
|
|
13393
|
+
nameField;
|
|
13394
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13395
|
+
registryOwnsEnabled;
|
|
13396
|
+
constructor(records, kind, hooks) {
|
|
13397
|
+
this.records = records;
|
|
13398
|
+
this.kind = kind;
|
|
13399
|
+
this.hooks = hooks;
|
|
13400
|
+
this.handle = records.kind(kind);
|
|
13401
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13402
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13403
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13404
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13405
|
+
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"`);
|
|
13406
|
+
}
|
|
13407
|
+
/**
|
|
13408
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13409
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13410
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13411
|
+
*/
|
|
13412
|
+
async readResolved(id) {
|
|
13413
|
+
try {
|
|
13414
|
+
const record = await this.handle.getResolved(id);
|
|
13415
|
+
return record === null ? null : {
|
|
13416
|
+
id,
|
|
13417
|
+
body: record.data,
|
|
13418
|
+
enabled: record.data.enabled !== false,
|
|
13419
|
+
secrets: "resolved"
|
|
13420
|
+
};
|
|
13421
|
+
} catch (err) {
|
|
13422
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13423
|
+
const stored = await this.handle.get(id);
|
|
13424
|
+
if (stored === null) return null;
|
|
13425
|
+
return {
|
|
13426
|
+
id,
|
|
13427
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13428
|
+
enabled: stored.data.enabled !== false,
|
|
13429
|
+
secrets: "unavailable"
|
|
13430
|
+
};
|
|
13431
|
+
}
|
|
13432
|
+
}
|
|
13433
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13434
|
+
async getResolved(id) {
|
|
13435
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13436
|
+
}
|
|
13437
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13438
|
+
async listResolved() {
|
|
13439
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13440
|
+
const out = [];
|
|
13441
|
+
for (const record of page.records) {
|
|
13442
|
+
const read = await this.readResolved(record.id);
|
|
13443
|
+
if (read !== null) out.push(read);
|
|
13444
|
+
}
|
|
13445
|
+
return out;
|
|
13446
|
+
}
|
|
13447
|
+
viewOf(record) {
|
|
13448
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13449
|
+
const rawName = record.data[this.nameField];
|
|
13450
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13451
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13452
|
+
return {
|
|
13453
|
+
id: record.id,
|
|
13454
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13455
|
+
configured: this.hooks.configured(record.data),
|
|
13456
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13457
|
+
formValues: shown,
|
|
13458
|
+
...kind !== void 0 ? { kind } : {},
|
|
13459
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13460
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13461
|
+
};
|
|
13462
|
+
}
|
|
13463
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13464
|
+
...this.hooks.description,
|
|
13465
|
+
mintsId: true
|
|
13466
|
+
};
|
|
13467
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13468
|
+
listInstances = async () => {
|
|
13469
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13470
|
+
};
|
|
13471
|
+
getInstance = async ({ id }) => {
|
|
13472
|
+
const record = await this.handle.get(id);
|
|
13473
|
+
return record === null ? null : this.viewOf(record);
|
|
13474
|
+
};
|
|
13475
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13476
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13477
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13478
|
+
name,
|
|
13479
|
+
...kind !== void 0 ? { kind } : {}
|
|
13480
|
+
});
|
|
13481
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13482
|
+
await this.handle.put({
|
|
13483
|
+
id: minted,
|
|
13484
|
+
data: this.hooks.initialBody(name, kind)
|
|
13485
|
+
}, { ifRev: 0 });
|
|
13486
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13487
|
+
return { id: minted };
|
|
13488
|
+
};
|
|
13489
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13490
|
+
const patch = {};
|
|
13491
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13492
|
+
if (value === void 0) continue;
|
|
13493
|
+
if (this.secretMaps.has(key)) {
|
|
13494
|
+
const entries = secretMapPatch(value);
|
|
13495
|
+
if (entries !== null) patch[key] = entries;
|
|
13496
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13497
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13498
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13499
|
+
}
|
|
13500
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13501
|
+
await this.handle.patch(id, patch);
|
|
13502
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13503
|
+
return { ok: true };
|
|
13504
|
+
};
|
|
13505
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13506
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13507
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13508
|
+
await this.handle.patch(id, { enabled });
|
|
13509
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13510
|
+
return { ok: true };
|
|
13511
|
+
};
|
|
13512
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13513
|
+
deleteInstance = async ({ id }) => {
|
|
13514
|
+
await this.handle.delete(id);
|
|
13515
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13516
|
+
return { ok: true };
|
|
13517
|
+
};
|
|
13518
|
+
testInstance = async (input) => {
|
|
13519
|
+
if (this.hooks.test === void 0) return {
|
|
13520
|
+
success: false,
|
|
13521
|
+
status: "unsupported",
|
|
13522
|
+
error: "This integration declares no connection test"
|
|
13523
|
+
};
|
|
13524
|
+
return this.hooks.test(input);
|
|
13525
|
+
};
|
|
13526
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13527
|
+
toProvider() {
|
|
13528
|
+
return {
|
|
13529
|
+
describe: this.describe,
|
|
13530
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13531
|
+
listInstances: this.listInstances,
|
|
13532
|
+
getInstance: this.getInstance,
|
|
13533
|
+
createInstance: this.createInstance,
|
|
13534
|
+
updateInstance: this.updateInstance,
|
|
13535
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13536
|
+
deleteInstance: this.deleteInstance,
|
|
13537
|
+
testInstance: this.testInstance
|
|
13538
|
+
};
|
|
13539
|
+
}
|
|
13540
|
+
};
|
|
12725
13541
|
var EncodeProfileSchema = object({
|
|
12726
13542
|
video: object({
|
|
12727
13543
|
codec: _enum([
|
|
@@ -13757,7 +14573,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
13757
14573
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
13758
14574
|
* catalogue lists the id (a newer database).
|
|
13759
14575
|
*/
|
|
13760
|
-
meta: MigrationMetaSchema.optional()
|
|
14576
|
+
meta: MigrationMetaSchema.optional(),
|
|
14577
|
+
/**
|
|
14578
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14579
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14580
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14581
|
+
*/
|
|
14582
|
+
ownerNotLoaded: boolean().optional()
|
|
13761
14583
|
});
|
|
13762
14584
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
13763
14585
|
var MigrationPendingEntrySchema = object({
|
|
@@ -13791,6 +14613,221 @@ var MigrationOwnerStepSchema = object({
|
|
|
13791
14613
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
13792
14614
|
});
|
|
13793
14615
|
/**
|
|
14616
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
14617
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
14618
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
14619
|
+
*
|
|
14620
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
14621
|
+
*
|
|
14622
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
14623
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
14624
|
+
* anything is written; `block-owner` then holds the addon.
|
|
14625
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
14626
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
14627
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
14628
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
14629
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
14630
|
+
*
|
|
14631
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
14632
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
14633
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
14634
|
+
*/
|
|
14635
|
+
function secretLeavesOf(body, spec) {
|
|
14636
|
+
const out = /* @__PURE__ */ new Map();
|
|
14637
|
+
for (const leaf of collectSecretLeaves(body, spec.secretPaths, spec.secretArrayKeys ?? {})) if (typeof leaf.value === "string" && leaf.value.length > 0) out.set(leaf.concrete, leaf.value);
|
|
14638
|
+
return out;
|
|
14639
|
+
}
|
|
14640
|
+
function why(err) {
|
|
14641
|
+
return err instanceof Error ? err.message : String(err);
|
|
14642
|
+
}
|
|
14643
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
14644
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
14645
|
+
if (integrations.length === 0) return {
|
|
14646
|
+
outcome: "already-done",
|
|
14647
|
+
summary: { integrations: 0 }
|
|
14648
|
+
};
|
|
14649
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
14650
|
+
for (const integration of integrations) try {
|
|
14651
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
14652
|
+
} catch (err) {
|
|
14653
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
14654
|
+
}
|
|
14655
|
+
let created = 0;
|
|
14656
|
+
let cleared = 0;
|
|
14657
|
+
let kept = 0;
|
|
14658
|
+
for (const integration of integrations) {
|
|
14659
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
14660
|
+
const hasRows = Object.keys(values).length > 0;
|
|
14661
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
14662
|
+
const put = await ports.putRecord(integration.id, body);
|
|
14663
|
+
if (put === "created") created += 1;
|
|
14664
|
+
if (!hasRows) continue;
|
|
14665
|
+
const wanted = secretLeavesOf(body, spec);
|
|
14666
|
+
const back = await ports.readRecord(integration.id);
|
|
14667
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
14668
|
+
const held = secretLeavesOf(back, spec);
|
|
14669
|
+
if (put === "created") {
|
|
14670
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
14671
|
+
if (wrong.length > 0) {
|
|
14672
|
+
await ports.deleteRecord(integration.id);
|
|
14673
|
+
throw new Error(`integration '${integration.id}': ${wrong.join(", ")} did not read back from the record - the record was removed and the legacy settings are kept`);
|
|
14674
|
+
}
|
|
14675
|
+
} else {
|
|
14676
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
14677
|
+
if (missing.length > 0) {
|
|
14678
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
14679
|
+
kept += 1;
|
|
14680
|
+
continue;
|
|
14681
|
+
}
|
|
14682
|
+
}
|
|
14683
|
+
await ports.clearRows(integration.id);
|
|
14684
|
+
cleared += 1;
|
|
14685
|
+
}
|
|
14686
|
+
return created === 0 && cleared === 0 ? {
|
|
14687
|
+
outcome: "already-done",
|
|
14688
|
+
summary: {
|
|
14689
|
+
integrations: integrations.length,
|
|
14690
|
+
kept
|
|
14691
|
+
}
|
|
14692
|
+
} : {
|
|
14693
|
+
outcome: "done",
|
|
14694
|
+
summary: {
|
|
14695
|
+
integrations: integrations.length,
|
|
14696
|
+
created,
|
|
14697
|
+
cleared,
|
|
14698
|
+
kept
|
|
14699
|
+
}
|
|
14700
|
+
};
|
|
14701
|
+
}
|
|
14702
|
+
/**
|
|
14703
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
14704
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
14705
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
14706
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
14707
|
+
* keychain values; the addon never touches the registry.
|
|
14708
|
+
*/
|
|
14709
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14710
|
+
return {
|
|
14711
|
+
warn: (message) => deps.log.warn(message),
|
|
14712
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14713
|
+
id: i.id,
|
|
14714
|
+
name: i.name
|
|
14715
|
+
})),
|
|
14716
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
14717
|
+
putRecord: async (id, body) => {
|
|
14718
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14719
|
+
try {
|
|
14720
|
+
await deps.records.put({
|
|
14721
|
+
id,
|
|
14722
|
+
data: body
|
|
14723
|
+
}, { ifRev: 0 });
|
|
14724
|
+
} catch (err) {
|
|
14725
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14726
|
+
throw err;
|
|
14727
|
+
}
|
|
14728
|
+
return "created";
|
|
14729
|
+
},
|
|
14730
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14731
|
+
deleteRecord: async (id) => {
|
|
14732
|
+
await deps.records.delete(id);
|
|
14733
|
+
},
|
|
14734
|
+
clearRows: async (integrationId) => {
|
|
14735
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
14736
|
+
}
|
|
14737
|
+
};
|
|
14738
|
+
}
|
|
14739
|
+
/**
|
|
14740
|
+
* The rows-to-record migration of a device addon whose integrations hold REAL settings
|
|
14741
|
+
* (planning#416, S-B): every integration of the addon becomes a record of the addon's own kind
|
|
14742
|
+
* under its registry id, the record body built from the legacy `integration_settings` rows, and
|
|
14743
|
+
* the rows and their keychain values are cleared once the record is read back
|
|
14744
|
+
* (`moveIntegrationRowsToRecords`). The addon declares ONE entry kind; this file turns that kind
|
|
14745
|
+
* into the body, the migration and its ports, so an addon states nothing but the kind.
|
|
14746
|
+
*
|
|
14747
|
+
* It runs in the addon process through the hub API, BEFORE the addon registers its
|
|
14748
|
+
* `integration-instances` provider, and nothing reads the old rows afterwards.
|
|
14749
|
+
*/
|
|
14750
|
+
/**
|
|
14751
|
+
* The record body of one integration from its resolved legacy rows. A key the kind has no field
|
|
14752
|
+
* for, or whose value the field refuses, is dropped and named in a warning (keys only, never a
|
|
14753
|
+
* value); a blank value is no value. A secret the keychain could not serve (a marker, not a
|
|
14754
|
+
* string) throws: moving it would lose it, and the rows are still there.
|
|
14755
|
+
*/
|
|
14756
|
+
function integrationBodyFromRows(kind, values, log, integration) {
|
|
14757
|
+
const fields = new Map(Object.entries(kind.schema.shape));
|
|
14758
|
+
const body = { name: integration.name };
|
|
14759
|
+
const dropped = [];
|
|
14760
|
+
for (const [key, value] of Object.entries(values)) {
|
|
14761
|
+
if (isSecretMarkerLike(value)) throw new Error(`integration '${integration.id}': ${key} could not be read from the keychain - nothing moved`);
|
|
14762
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
14763
|
+
const field = key === "name" ? void 0 : fields.get(key);
|
|
14764
|
+
if (field === void 0 || !field.safeParse(value).success) {
|
|
14765
|
+
dropped.push(key);
|
|
14766
|
+
continue;
|
|
14767
|
+
}
|
|
14768
|
+
body[key] = value;
|
|
14769
|
+
}
|
|
14770
|
+
if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings the ${kind.name} record has no valid field for are dropped: ${dropped.join(", ")}`);
|
|
14771
|
+
return kind.schema.parse(body);
|
|
14772
|
+
}
|
|
14773
|
+
function integrationRowsToRecordId(addonId, date) {
|
|
14774
|
+
return `${addonId}/${date}-integration-rows-to-record`;
|
|
14775
|
+
}
|
|
14776
|
+
function integrationRowsMigration(spec) {
|
|
14777
|
+
return {
|
|
14778
|
+
meta: {
|
|
14779
|
+
id: integrationRowsToRecordId(spec.addonId, spec.date),
|
|
14780
|
+
owner: spec.addonId,
|
|
14781
|
+
level: "addon-state",
|
|
14782
|
+
seq: 1,
|
|
14783
|
+
dependsOn: [],
|
|
14784
|
+
store: "settings",
|
|
14785
|
+
scope: "cluster",
|
|
14786
|
+
onFailure: "block-owner",
|
|
14787
|
+
bodyKind: "procedure",
|
|
14788
|
+
description: "Move every integration of the addon into a typed instance record keyed by its registry id, with its connection settings and secrets, then clear the legacy integration settings rows",
|
|
14789
|
+
fixtureMark: spec.fixtureMark
|
|
14790
|
+
},
|
|
14791
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
14792
|
+
addonId: spec.addonId,
|
|
14793
|
+
secretPaths: spec.kind.declaration.secretPaths,
|
|
14794
|
+
secretArrayKeys: spec.kind.declaration.secretArrayKeys,
|
|
14795
|
+
bodyFrom: (values, log, integration) => integrationBodyFromRows(spec.kind, values, log, integration)
|
|
14796
|
+
})
|
|
14797
|
+
};
|
|
14798
|
+
}
|
|
14799
|
+
/** The hub and records sides of {@link integrationRowsMigration}. */
|
|
14800
|
+
function integrationRowsPortsOver(deps) {
|
|
14801
|
+
return integrationRowsPortsOverApi({
|
|
14802
|
+
api: deps.api,
|
|
14803
|
+
records: deps.records.kind({
|
|
14804
|
+
name: deps.kind.name,
|
|
14805
|
+
declaration: deps.kind.declaration,
|
|
14806
|
+
schema: record(string(), unknown())
|
|
14807
|
+
}),
|
|
14808
|
+
log: { warn: deps.warn }
|
|
14809
|
+
});
|
|
14810
|
+
}
|
|
14811
|
+
function defaultIntegrationApiOver(api) {
|
|
14812
|
+
return {
|
|
14813
|
+
list: () => api.integrations.list.query(),
|
|
14814
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
14815
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
14816
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14817
|
+
addonId,
|
|
14818
|
+
projection: "slim"
|
|
14819
|
+
}),
|
|
14820
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14821
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14822
|
+
deviceId,
|
|
14823
|
+
integrationId
|
|
14824
|
+
});
|
|
14825
|
+
},
|
|
14826
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14827
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14828
|
+
};
|
|
14829
|
+
}
|
|
14830
|
+
/**
|
|
13794
14831
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
13795
14832
|
* gets back (cloud training, D805+).
|
|
13796
14833
|
*
|
|
@@ -18875,24 +19912,52 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18875
19912
|
args: { on: false }
|
|
18876
19913
|
},
|
|
18877
19914
|
{
|
|
18878
|
-
id: "petFountain.
|
|
18879
|
-
method: "
|
|
19915
|
+
id: "petFountain.resetDailyConsumption",
|
|
19916
|
+
method: "resetDailyConsumption",
|
|
19917
|
+
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19918
|
+
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19919
|
+
icon: "rotate-ccw"
|
|
19920
|
+
}
|
|
19921
|
+
],
|
|
19922
|
+
state: [{
|
|
19923
|
+
kind: "boolean",
|
|
19924
|
+
field: "empty",
|
|
19925
|
+
label: t("Tank empty", "Serbatoio vuoto"),
|
|
19926
|
+
whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
|
|
19927
|
+
whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
|
|
19928
|
+
icon: "droplet"
|
|
19929
|
+
}, {
|
|
19930
|
+
kind: "numeric",
|
|
19931
|
+
field: "waterLevel",
|
|
19932
|
+
quantity: t("the water level", "il livello dell’acqua"),
|
|
19933
|
+
label: t("Water level", "Livello dell’acqua"),
|
|
19934
|
+
unit: "%",
|
|
19935
|
+
icon: "droplets"
|
|
19936
|
+
}]
|
|
19937
|
+
},
|
|
19938
|
+
"pump-cycle": {
|
|
19939
|
+
scope: "device",
|
|
19940
|
+
noun: t("Pump cycle", "Ciclo della pompa"),
|
|
19941
|
+
actions: [
|
|
19942
|
+
{
|
|
19943
|
+
id: "pumpCycle.on",
|
|
19944
|
+
method: "setEnabled",
|
|
18880
19945
|
label: t("Pump cycle on", "Ciclo pompa attivo"),
|
|
18881
19946
|
phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
|
|
18882
19947
|
icon: "timer",
|
|
18883
19948
|
args: { on: true }
|
|
18884
19949
|
},
|
|
18885
19950
|
{
|
|
18886
|
-
id: "
|
|
18887
|
-
method: "
|
|
19951
|
+
id: "pumpCycle.off",
|
|
19952
|
+
method: "setEnabled",
|
|
18888
19953
|
label: t("Pump cycle off", "Ciclo pompa spento"),
|
|
18889
19954
|
phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
|
|
18890
19955
|
icon: "timer-off",
|
|
18891
19956
|
args: { on: false }
|
|
18892
19957
|
},
|
|
18893
19958
|
{
|
|
18894
|
-
id: "
|
|
18895
|
-
method: "
|
|
19959
|
+
id: "pumpCycle.onMinutes",
|
|
19960
|
+
method: "setOnMinutes",
|
|
18896
19961
|
label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
|
|
18897
19962
|
phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
|
|
18898
19963
|
icon: "timer",
|
|
@@ -18908,8 +19973,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18908
19973
|
}]
|
|
18909
19974
|
},
|
|
18910
19975
|
{
|
|
18911
|
-
id: "
|
|
18912
|
-
method: "
|
|
19976
|
+
id: "pumpCycle.offMinutes",
|
|
19977
|
+
method: "setOffMinutes",
|
|
18913
19978
|
label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
|
|
18914
19979
|
phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
|
|
18915
19980
|
icon: "timer-off",
|
|
@@ -18923,35 +19988,63 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18923
19988
|
unit: "min"
|
|
18924
19989
|
}
|
|
18925
19990
|
}]
|
|
18926
|
-
},
|
|
18927
|
-
{
|
|
18928
|
-
id: "petFountain.resetDailyConsumption",
|
|
18929
|
-
method: "resetDailyConsumption",
|
|
18930
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
18931
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
18932
|
-
icon: "rotate-ccw"
|
|
18933
19991
|
}
|
|
18934
19992
|
],
|
|
18935
19993
|
state: [{
|
|
18936
19994
|
kind: "boolean",
|
|
18937
|
-
field: "
|
|
18938
|
-
label: t("
|
|
18939
|
-
whenTrue: t("When
|
|
18940
|
-
whenFalse: t("When
|
|
18941
|
-
icon: "
|
|
18942
|
-
}, {
|
|
18943
|
-
kind: "numeric",
|
|
18944
|
-
field: "waterLevel",
|
|
18945
|
-
quantity: t("the water level", "il livello dell’acqua"),
|
|
18946
|
-
label: t("Water level", "Livello dell’acqua"),
|
|
18947
|
-
unit: "%",
|
|
18948
|
-
icon: "droplets"
|
|
19995
|
+
field: "enabled",
|
|
19996
|
+
label: t("Pump cycle", "Ciclo pompa"),
|
|
19997
|
+
whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
|
|
19998
|
+
whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
|
|
19999
|
+
icon: "timer"
|
|
18949
20000
|
}]
|
|
18950
20001
|
},
|
|
18951
20002
|
"media-player": {
|
|
18952
20003
|
scope: "device",
|
|
18953
20004
|
noun: t("Media player", "Lettore multimediale"),
|
|
18954
20005
|
actions: [
|
|
20006
|
+
{
|
|
20007
|
+
id: "media.turnOn",
|
|
20008
|
+
method: "turnOn",
|
|
20009
|
+
label: t("Turn on", "Accendi"),
|
|
20010
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
20011
|
+
icon: "power"
|
|
20012
|
+
},
|
|
20013
|
+
{
|
|
20014
|
+
id: "media.turnOff",
|
|
20015
|
+
method: "turnOff",
|
|
20016
|
+
label: t("Turn off", "Spegni"),
|
|
20017
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
20018
|
+
icon: "power"
|
|
20019
|
+
},
|
|
20020
|
+
{
|
|
20021
|
+
id: "media.volumeUp",
|
|
20022
|
+
method: "volumeUp",
|
|
20023
|
+
label: t("Volume up", "Alza il volume"),
|
|
20024
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
20025
|
+
icon: "volume-2"
|
|
20026
|
+
},
|
|
20027
|
+
{
|
|
20028
|
+
id: "media.volumeDown",
|
|
20029
|
+
method: "volumeDown",
|
|
20030
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
20031
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
20032
|
+
icon: "volume-2"
|
|
20033
|
+
},
|
|
20034
|
+
{
|
|
20035
|
+
id: "media.channelUp",
|
|
20036
|
+
method: "channelUp",
|
|
20037
|
+
label: t("Channel up", "Canale successivo"),
|
|
20038
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
20039
|
+
icon: "skip-forward"
|
|
20040
|
+
},
|
|
20041
|
+
{
|
|
20042
|
+
id: "media.channelDown",
|
|
20043
|
+
method: "channelDown",
|
|
20044
|
+
label: t("Channel down", "Canale precedente"),
|
|
20045
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
20046
|
+
icon: "skip-back"
|
|
20047
|
+
},
|
|
18955
20048
|
{
|
|
18956
20049
|
id: "media.play",
|
|
18957
20050
|
method: "play",
|
|
@@ -20838,6 +21931,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
20838
21931
|
"audio-analyzer": system(PIPELINE),
|
|
20839
21932
|
"audio-codec": system(INTERNAL),
|
|
20840
21933
|
"auth-provider": system(IDENTITY),
|
|
21934
|
+
"board-key-source": system(INTERNAL),
|
|
20841
21935
|
backup: system("system backups — run and restored from the Backups page"),
|
|
20842
21936
|
broker: system("message-broker connections — configured, not actuated"),
|
|
20843
21937
|
"calendar-source": system(INTERNAL),
|
|
@@ -45747,6 +46841,49 @@ var binaryCapability = {
|
|
|
45747
46841
|
volatileStateFields: ["lastChangedAt"]
|
|
45748
46842
|
};
|
|
45749
46843
|
/**
|
|
46844
|
+
* Why a board key could not be read. Typed so a caller reports the cause and
|
|
46845
|
+
* never treats "could not read" as "has no key" (D49).
|
|
46846
|
+
*/
|
|
46847
|
+
var BoardKeyFailureReasonSchema = _enum([
|
|
46848
|
+
"unknown-source",
|
|
46849
|
+
"not-admin",
|
|
46850
|
+
"no-builder",
|
|
46851
|
+
"no-ingress-url",
|
|
46852
|
+
"unreachable",
|
|
46853
|
+
"unknown-board",
|
|
46854
|
+
"no-encryption-key",
|
|
46855
|
+
"unreadable-config"
|
|
46856
|
+
]);
|
|
46857
|
+
var BoardKeySourceSchema = object({
|
|
46858
|
+
sourceId: string(),
|
|
46859
|
+
name: string()
|
|
46860
|
+
});
|
|
46861
|
+
var BoardKeyBoardSchema = object({
|
|
46862
|
+
/** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
|
|
46863
|
+
configuration: string(),
|
|
46864
|
+
/** The node name the builder gives the board. */
|
|
46865
|
+
name: string(),
|
|
46866
|
+
/** The address the builder knows the board by, when it has one. */
|
|
46867
|
+
address: string().nullable()
|
|
46868
|
+
});
|
|
46869
|
+
var BoardKeyFailureSchema = object({
|
|
46870
|
+
ok: literal(false),
|
|
46871
|
+
reason: BoardKeyFailureReasonSchema,
|
|
46872
|
+
detail: string().optional()
|
|
46873
|
+
});
|
|
46874
|
+
var ListBoardsResultSchema = discriminatedUnion("ok", [object({
|
|
46875
|
+
ok: literal(true),
|
|
46876
|
+
boards: array(BoardKeyBoardSchema).readonly()
|
|
46877
|
+
}), BoardKeyFailureSchema]);
|
|
46878
|
+
var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
|
|
46879
|
+
ok: literal(true),
|
|
46880
|
+
key: string()
|
|
46881
|
+
}), BoardKeyFailureSchema]);
|
|
46882
|
+
method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
|
|
46883
|
+
sourceId: string().min(1),
|
|
46884
|
+
configuration: string().min(1)
|
|
46885
|
+
}), GetBoardKeyResultSchema, { auth: "admin" });
|
|
46886
|
+
/**
|
|
45750
46887
|
* Dimmable-light brightness control. Co-exists with `switch` on the
|
|
45751
46888
|
* same device — the switch toggles on/off, this cap sets the level
|
|
45752
46889
|
* applied when the light is on. Drivers map their per-vendor dim
|
|
@@ -48490,6 +49627,244 @@ var controlCapability = {
|
|
|
48490
49627
|
*/
|
|
48491
49628
|
history: "none"
|
|
48492
49629
|
};
|
|
49630
|
+
/**
|
|
49631
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
49632
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
49633
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
49634
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
49635
|
+
*
|
|
49636
|
+
* The cap declares a wide method surface; not every backend supports
|
|
49637
|
+
* every method. Drivers signal sub-capability via the
|
|
49638
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
49639
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
49640
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
49641
|
+
*/
|
|
49642
|
+
var MediaPlayerStateSchema = _enum([
|
|
49643
|
+
"off",
|
|
49644
|
+
"on",
|
|
49645
|
+
"idle",
|
|
49646
|
+
"playing",
|
|
49647
|
+
"paused",
|
|
49648
|
+
"buffering",
|
|
49649
|
+
"standby"
|
|
49650
|
+
]);
|
|
49651
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
49652
|
+
"off",
|
|
49653
|
+
"all",
|
|
49654
|
+
"one"
|
|
49655
|
+
]);
|
|
49656
|
+
var MediaInfoSchema = object({
|
|
49657
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
49658
|
+
* `podcast`, …). */
|
|
49659
|
+
type: string(),
|
|
49660
|
+
/** Human-readable title. */
|
|
49661
|
+
title: string(),
|
|
49662
|
+
/** Optional artist / channel / station label. */
|
|
49663
|
+
artist: string().optional(),
|
|
49664
|
+
/** Optional album / season / show name. */
|
|
49665
|
+
album: string().optional(),
|
|
49666
|
+
/** Optional cover-art / thumbnail URL. */
|
|
49667
|
+
imageUrl: string().optional()
|
|
49668
|
+
});
|
|
49669
|
+
/**
|
|
49670
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
49671
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
49672
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
49673
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
49674
|
+
* guess, never as a reading.
|
|
49675
|
+
*/
|
|
49676
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
49677
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
49678
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
49679
|
+
"state",
|
|
49680
|
+
"volumeLevel",
|
|
49681
|
+
"isMuted",
|
|
49682
|
+
"source"
|
|
49683
|
+
]);
|
|
49684
|
+
var MediaPlayerStatusSchema = object({
|
|
49685
|
+
/** Playback lifecycle state. */
|
|
49686
|
+
state: MediaPlayerStateSchema,
|
|
49687
|
+
/**
|
|
49688
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
49689
|
+
* A slice written before this field existed parses as `reported`.
|
|
49690
|
+
*/
|
|
49691
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
49692
|
+
/**
|
|
49693
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
49694
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
49695
|
+
* from a running-state source, mute guessed from the last send).
|
|
49696
|
+
*/
|
|
49697
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
49698
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
49699
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
49700
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
49701
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
49702
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
49703
|
+
isMuted: boolean().nullable(),
|
|
49704
|
+
/** Active source / input. Empty when no source surface. */
|
|
49705
|
+
source: string(),
|
|
49706
|
+
/** Selectable sources. Empty when no source surface.
|
|
49707
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
49708
|
+
availableSources: array(string()),
|
|
49709
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
49710
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
49711
|
+
/** Current playback position in ms. Null when not seekable or
|
|
49712
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
49713
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
49714
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
49715
|
+
* (live stream) or nothing is playing. */
|
|
49716
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
49717
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
49718
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
49719
|
+
shuffle: boolean().nullable(),
|
|
49720
|
+
/** Repeat mode. Null when no repeat surface.
|
|
49721
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
49722
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
49723
|
+
/** Ms epoch when the slice was last updated. */
|
|
49724
|
+
lastChangedAt: number()
|
|
49725
|
+
});
|
|
49726
|
+
var stepInput = object({
|
|
49727
|
+
deviceId: number().int().nonnegative(),
|
|
49728
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
49729
|
+
steps: number().int().min(1).max(50).default(1)
|
|
49730
|
+
});
|
|
49731
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
49732
|
+
var mediaPlayerCapability = {
|
|
49733
|
+
name: "media-player",
|
|
49734
|
+
scope: "device",
|
|
49735
|
+
deviceNative: true,
|
|
49736
|
+
mode: "singleton",
|
|
49737
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
49738
|
+
methods: {
|
|
49739
|
+
turnOn: method(deviceOnly, _void(), {
|
|
49740
|
+
kind: "mutation",
|
|
49741
|
+
auth: "protected"
|
|
49742
|
+
}),
|
|
49743
|
+
turnOff: method(deviceOnly, _void(), {
|
|
49744
|
+
kind: "mutation",
|
|
49745
|
+
auth: "protected"
|
|
49746
|
+
}),
|
|
49747
|
+
volumeUp: method(stepInput, _void(), {
|
|
49748
|
+
kind: "mutation",
|
|
49749
|
+
auth: "protected"
|
|
49750
|
+
}),
|
|
49751
|
+
volumeDown: method(stepInput, _void(), {
|
|
49752
|
+
kind: "mutation",
|
|
49753
|
+
auth: "protected"
|
|
49754
|
+
}),
|
|
49755
|
+
channelUp: method(stepInput, _void(), {
|
|
49756
|
+
kind: "mutation",
|
|
49757
|
+
auth: "protected"
|
|
49758
|
+
}),
|
|
49759
|
+
channelDown: method(stepInput, _void(), {
|
|
49760
|
+
kind: "mutation",
|
|
49761
|
+
auth: "protected"
|
|
49762
|
+
}),
|
|
49763
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49764
|
+
kind: "mutation",
|
|
49765
|
+
auth: "protected"
|
|
49766
|
+
}),
|
|
49767
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49768
|
+
kind: "mutation",
|
|
49769
|
+
auth: "protected"
|
|
49770
|
+
}),
|
|
49771
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49772
|
+
kind: "mutation",
|
|
49773
|
+
auth: "protected"
|
|
49774
|
+
}),
|
|
49775
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49776
|
+
kind: "mutation",
|
|
49777
|
+
auth: "protected"
|
|
49778
|
+
}),
|
|
49779
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49780
|
+
kind: "mutation",
|
|
49781
|
+
auth: "protected"
|
|
49782
|
+
}),
|
|
49783
|
+
seek: method(object({
|
|
49784
|
+
deviceId: number().int().nonnegative(),
|
|
49785
|
+
position: number().int().nonnegative()
|
|
49786
|
+
}), _void(), {
|
|
49787
|
+
kind: "mutation",
|
|
49788
|
+
auth: "protected"
|
|
49789
|
+
}),
|
|
49790
|
+
setVolume: method(object({
|
|
49791
|
+
deviceId: number().int().nonnegative(),
|
|
49792
|
+
volumeLevel: number().min(0).max(100)
|
|
49793
|
+
}), _void(), {
|
|
49794
|
+
kind: "mutation",
|
|
49795
|
+
auth: "protected"
|
|
49796
|
+
}),
|
|
49797
|
+
setMute: method(object({
|
|
49798
|
+
deviceId: number().int().nonnegative(),
|
|
49799
|
+
muted: boolean()
|
|
49800
|
+
}), _void(), {
|
|
49801
|
+
kind: "mutation",
|
|
49802
|
+
auth: "protected"
|
|
49803
|
+
}),
|
|
49804
|
+
setShuffle: method(object({
|
|
49805
|
+
deviceId: number().int().nonnegative(),
|
|
49806
|
+
shuffle: boolean()
|
|
49807
|
+
}), _void(), {
|
|
49808
|
+
kind: "mutation",
|
|
49809
|
+
auth: "protected"
|
|
49810
|
+
}),
|
|
49811
|
+
setRepeat: method(object({
|
|
49812
|
+
deviceId: number().int().nonnegative(),
|
|
49813
|
+
repeat: MediaPlayerRepeatSchema
|
|
49814
|
+
}), _void(), {
|
|
49815
|
+
kind: "mutation",
|
|
49816
|
+
auth: "protected"
|
|
49817
|
+
}),
|
|
49818
|
+
selectSource: method(object({
|
|
49819
|
+
deviceId: number().int().nonnegative(),
|
|
49820
|
+
source: string().min(1)
|
|
49821
|
+
}), _void(), {
|
|
49822
|
+
kind: "mutation",
|
|
49823
|
+
auth: "protected"
|
|
49824
|
+
}),
|
|
49825
|
+
playMedia: method(object({
|
|
49826
|
+
deviceId: number().int().nonnegative(),
|
|
49827
|
+
/** Media identifier / URL. */
|
|
49828
|
+
mediaId: string().min(1),
|
|
49829
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
49830
|
+
* passes it through to the upstream service. */
|
|
49831
|
+
mediaType: string().min(1)
|
|
49832
|
+
}), _void(), {
|
|
49833
|
+
kind: "mutation",
|
|
49834
|
+
auth: "protected"
|
|
49835
|
+
})
|
|
49836
|
+
},
|
|
49837
|
+
status: {
|
|
49838
|
+
schema: MediaPlayerStatusSchema,
|
|
49839
|
+
kind: "push"
|
|
49840
|
+
},
|
|
49841
|
+
/**
|
|
49842
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
49843
|
+
* full slice for live now-playing, volume, and progress updates
|
|
49844
|
+
* without polling.
|
|
49845
|
+
*/
|
|
49846
|
+
runtimeState: MediaPlayerStatusSchema,
|
|
49847
|
+
/**
|
|
49848
|
+
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
49849
|
+
*
|
|
49850
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
49851
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
49852
|
+
*/
|
|
49853
|
+
durability: "session",
|
|
49854
|
+
/**
|
|
49855
|
+
* History (D850): volume (a level an operator or an automation sets; the
|
|
49856
|
+
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
49857
|
+
* `position` is not recorded: it moves continuously while playing.
|
|
49858
|
+
*/
|
|
49859
|
+
history: { fields: {
|
|
49860
|
+
volumeLevel: {
|
|
49861
|
+
kind: "gauge",
|
|
49862
|
+
unit: "%",
|
|
49863
|
+
deadband: 1
|
|
49864
|
+
},
|
|
49865
|
+
isMuted: { kind: "boolean" }
|
|
49866
|
+
} }
|
|
49867
|
+
};
|
|
48493
49868
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
|
|
48494
49869
|
var NUMBER = {
|
|
48495
49870
|
kind: "fixed",
|
|
@@ -48937,117 +50312,6 @@ function actuator(input) {
|
|
|
48937
50312
|
};
|
|
48938
50313
|
}
|
|
48939
50314
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
48940
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
48941
|
-
"no-power-source",
|
|
48942
|
-
"switch-missing",
|
|
48943
|
-
"switch-offline",
|
|
48944
|
-
"switch-unknown",
|
|
48945
|
-
"restart-in-progress",
|
|
48946
|
-
"switch-call-failed"
|
|
48947
|
-
]);
|
|
48948
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
48949
|
-
accepted: literal(false),
|
|
48950
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
48951
|
-
/** One line for the operator, naming the switch. */
|
|
48952
|
-
message: string()
|
|
48953
|
-
})]);
|
|
48954
|
-
var PowerSourceInfoSchema = object({
|
|
48955
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
48956
|
-
linked: boolean(),
|
|
48957
|
-
switchDeviceId: number().nullable(),
|
|
48958
|
-
switchName: string().nullable(),
|
|
48959
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
48960
|
-
switchOn: boolean().nullable(),
|
|
48961
|
-
switchOnline: boolean().nullable(),
|
|
48962
|
-
restartInProgress: boolean(),
|
|
48963
|
-
offSeconds: number().int()
|
|
48964
|
-
});
|
|
48965
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
48966
|
-
var PowerSourceStateSchema = object({
|
|
48967
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
48968
|
-
switchOn: boolean().nullable(),
|
|
48969
|
-
offSeconds: number().int(),
|
|
48970
|
-
lastFetchedAt: number()
|
|
48971
|
-
});
|
|
48972
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
48973
|
-
DeviceType.Gateway,
|
|
48974
|
-
DeviceType.Hub,
|
|
48975
|
-
DeviceType.Camera,
|
|
48976
|
-
DeviceType.Sensor,
|
|
48977
|
-
DeviceType.MediaPlayer,
|
|
48978
|
-
DeviceType.Vacuum,
|
|
48979
|
-
DeviceType.Generic,
|
|
48980
|
-
DeviceType.Washer,
|
|
48981
|
-
DeviceType.Dryer,
|
|
48982
|
-
DeviceType.Dishwasher,
|
|
48983
|
-
DeviceType.PetFountain,
|
|
48984
|
-
DeviceType.Fan,
|
|
48985
|
-
DeviceType.Humidifier,
|
|
48986
|
-
DeviceType.Climate,
|
|
48987
|
-
DeviceType.WaterHeater
|
|
48988
|
-
];
|
|
48989
|
-
var powerSourceCustomize = {
|
|
48990
|
-
version: 1,
|
|
48991
|
-
targets: [{
|
|
48992
|
-
id: "switch",
|
|
48993
|
-
cardinality: "scalar",
|
|
48994
|
-
vocabulary: {
|
|
48995
|
-
cap: "power-source",
|
|
48996
|
-
stateField: "switchOn"
|
|
48997
|
-
},
|
|
48998
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
48999
|
-
apply: "add",
|
|
49000
|
-
path: "switchOn",
|
|
49001
|
-
sources: [{
|
|
49002
|
-
kind: "switch",
|
|
49003
|
-
transforms: [{ kind: "identity" }]
|
|
49004
|
-
}],
|
|
49005
|
-
companions: [{
|
|
49006
|
-
kind: "constant",
|
|
49007
|
-
path: "offSeconds",
|
|
49008
|
-
value: 10,
|
|
49009
|
-
when: "add"
|
|
49010
|
-
}, {
|
|
49011
|
-
kind: "clock",
|
|
49012
|
-
path: "lastFetchedAt",
|
|
49013
|
-
when: "always"
|
|
49014
|
-
}],
|
|
49015
|
-
sourceReport: { status: "assignment-status" }
|
|
49016
|
-
}]
|
|
49017
|
-
};
|
|
49018
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
49019
|
-
var powerSourceCapability = {
|
|
49020
|
-
name: "power-source",
|
|
49021
|
-
scope: "device",
|
|
49022
|
-
mode: "singleton",
|
|
49023
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
49024
|
-
methods: {
|
|
49025
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
49026
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49027
|
-
kind: "mutation",
|
|
49028
|
-
auth: "admin"
|
|
49029
|
-
}),
|
|
49030
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49031
|
-
kind: "mutation",
|
|
49032
|
-
auth: "admin"
|
|
49033
|
-
}),
|
|
49034
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
49035
|
-
kind: "mutation",
|
|
49036
|
-
auth: "admin"
|
|
49037
|
-
})
|
|
49038
|
-
},
|
|
49039
|
-
runtimeState: PowerSourceStateSchema,
|
|
49040
|
-
customize: powerSourceCustomize,
|
|
49041
|
-
/**
|
|
49042
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
49043
|
-
* its next report; a restored value would outlive the reading it names.
|
|
49044
|
-
*
|
|
49045
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
49046
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
49047
|
-
*/
|
|
49048
|
-
durability: "session",
|
|
49049
|
-
history: "none"
|
|
49050
|
-
};
|
|
49051
50315
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
49052
50316
|
DeviceType.Camera;
|
|
49053
50317
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -49075,6 +50339,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
49075
50339
|
}).optional()
|
|
49076
50340
|
})) });
|
|
49077
50341
|
/**
|
|
50342
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
50343
|
+
* `z.infer`, they do not restate the shape.
|
|
50344
|
+
*/
|
|
50345
|
+
var OverrideSourceSchema = object({
|
|
50346
|
+
addonId: string().min(1),
|
|
50347
|
+
stableId: string().min(1),
|
|
50348
|
+
cap: string().min(1),
|
|
50349
|
+
fieldPath: string().min(1)
|
|
50350
|
+
});
|
|
50351
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
50352
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
50353
|
+
var ThresholdTransformDraftSchema = object({
|
|
50354
|
+
kind: literal("threshold"),
|
|
50355
|
+
comparison: _enum(["gte", "lte"]),
|
|
50356
|
+
threshold: number(),
|
|
50357
|
+
startDwellMs: number().nonnegative(),
|
|
50358
|
+
stopDwellMs: number().nonnegative()
|
|
50359
|
+
});
|
|
50360
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50361
|
+
IdentityTransformDraftSchema,
|
|
50362
|
+
InvertTransformDraftSchema,
|
|
50363
|
+
ThresholdTransformDraftSchema,
|
|
50364
|
+
object({
|
|
50365
|
+
kind: literal("linear"),
|
|
50366
|
+
scale: number(),
|
|
50367
|
+
offset: number(),
|
|
50368
|
+
clampMin: number().optional(),
|
|
50369
|
+
clampMax: number().optional()
|
|
50370
|
+
}),
|
|
50371
|
+
object({
|
|
50372
|
+
kind: literal("enum-map"),
|
|
50373
|
+
mapping: record(string(), string().nullable())
|
|
50374
|
+
}),
|
|
50375
|
+
object({ kind: literal("number") })
|
|
50376
|
+
]);
|
|
50377
|
+
var ScalarOverrideDraftSchema = object({
|
|
50378
|
+
kind: literal("scalar"),
|
|
50379
|
+
cap: string().min(1),
|
|
50380
|
+
targetId: string().min(1),
|
|
50381
|
+
source: OverrideSourceSchema,
|
|
50382
|
+
transform: CustomizeTransformDraftSchema
|
|
50383
|
+
});
|
|
50384
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
50385
|
+
kind: literal("keyedArray"),
|
|
50386
|
+
cap: string().min(1),
|
|
50387
|
+
targetId: string().min(1),
|
|
50388
|
+
key: string().min(1),
|
|
50389
|
+
label: string().min(1),
|
|
50390
|
+
fieldPath: string().min(1),
|
|
50391
|
+
source: OverrideSourceSchema,
|
|
50392
|
+
transform: CustomizeTransformDraftSchema
|
|
50393
|
+
});
|
|
50394
|
+
var PulseOverrideDraftSchema = object({
|
|
50395
|
+
kind: literal("pulse"),
|
|
50396
|
+
cap: string().min(1),
|
|
50397
|
+
targetId: string().min(1),
|
|
50398
|
+
enabled: boolean(),
|
|
50399
|
+
debounceMs: number().nonnegative(),
|
|
50400
|
+
edges: array(_enum([
|
|
50401
|
+
"rising",
|
|
50402
|
+
"button",
|
|
50403
|
+
"native-event"
|
|
50404
|
+
])).min(1).optional(),
|
|
50405
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
50406
|
+
});
|
|
50407
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
50408
|
+
"in-addition",
|
|
50409
|
+
"confirm",
|
|
50410
|
+
"replace"
|
|
50411
|
+
]).optional() });
|
|
50412
|
+
discriminatedUnion("kind", [
|
|
50413
|
+
ScalarOverrideDraftSchema,
|
|
50414
|
+
KeyedArrayOverrideDraftSchema,
|
|
50415
|
+
PulseOverrideDraftSchema,
|
|
50416
|
+
object({
|
|
50417
|
+
kind: literal("span"),
|
|
50418
|
+
cap: string().min(1),
|
|
50419
|
+
targetId: string().min(1),
|
|
50420
|
+
timeoutMs: number().nonnegative(),
|
|
50421
|
+
cooldownMs: number().nonnegative().optional(),
|
|
50422
|
+
sources: array(SpanSourceDraftSchema)
|
|
50423
|
+
})
|
|
50424
|
+
]);
|
|
50425
|
+
object({
|
|
50426
|
+
targetId: string().min(1),
|
|
50427
|
+
configured: boolean(),
|
|
50428
|
+
source: _enum(["native", "assigned"]),
|
|
50429
|
+
state: _enum([
|
|
50430
|
+
"active",
|
|
50431
|
+
"stale",
|
|
50432
|
+
"released",
|
|
50433
|
+
"unavailable"
|
|
50434
|
+
]),
|
|
50435
|
+
reason: string().nullable()
|
|
50436
|
+
});
|
|
50437
|
+
/**
|
|
50438
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
50439
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
50440
|
+
*
|
|
50441
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
50442
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
50443
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
50444
|
+
*/
|
|
50445
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
50446
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
50447
|
+
"high",
|
|
50448
|
+
"medium",
|
|
50449
|
+
"low"
|
|
50450
|
+
]);
|
|
50451
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
50452
|
+
"no-history",
|
|
50453
|
+
"span-too-short",
|
|
50454
|
+
"truncated",
|
|
50455
|
+
"no-idle-floor",
|
|
50456
|
+
"no-runs",
|
|
50457
|
+
"no-separation"
|
|
50458
|
+
]);
|
|
50459
|
+
var RunningProposalReplaySchema = object({
|
|
50460
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
50461
|
+
intervals: number().int().nonnegative(),
|
|
50462
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
50463
|
+
extra: number().int().nonnegative(),
|
|
50464
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
50465
|
+
missed: number().int().nonnegative()
|
|
50466
|
+
});
|
|
50467
|
+
var RunningProposalEvidenceSchema = object({
|
|
50468
|
+
tier: RunningProposalTierSchema,
|
|
50469
|
+
spanMs: number().nonnegative(),
|
|
50470
|
+
from: number(),
|
|
50471
|
+
to: number(),
|
|
50472
|
+
points: number().int().nonnegative(),
|
|
50473
|
+
runsFound: number().int().nonnegative(),
|
|
50474
|
+
pausesFound: number().int().nonnegative(),
|
|
50475
|
+
longestPauseMs: number().nonnegative(),
|
|
50476
|
+
idleFloorW: number(),
|
|
50477
|
+
noiseCeilingW: number(),
|
|
50478
|
+
plateauW: number(),
|
|
50479
|
+
replay: RunningProposalReplaySchema
|
|
50480
|
+
});
|
|
50481
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
50482
|
+
spanMs: number().nonnegative(),
|
|
50483
|
+
points: number().int().nonnegative(),
|
|
50484
|
+
runsFound: number().int().nonnegative()
|
|
50485
|
+
});
|
|
50486
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
50487
|
+
state: literal("proposal"),
|
|
50488
|
+
transform: ThresholdTransformDraftSchema,
|
|
50489
|
+
evidence: RunningProposalEvidenceSchema,
|
|
50490
|
+
confidence: RunningProposalConfidenceSchema,
|
|
50491
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
50492
|
+
notes: array(string())
|
|
50493
|
+
}), object({
|
|
50494
|
+
state: literal("insufficient"),
|
|
50495
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
50496
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
50497
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
50498
|
+
fallback: ThresholdTransformDraftSchema
|
|
50499
|
+
})]);
|
|
50500
|
+
/**
|
|
49078
50501
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
49079
50502
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
49080
50503
|
*
|
|
@@ -49364,6 +50787,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
49364
50787
|
kind: "mutation",
|
|
49365
50788
|
auth: "admin",
|
|
49366
50789
|
access: "view"
|
|
50790
|
+
}), method(object({
|
|
50791
|
+
deviceId: number().int(),
|
|
50792
|
+
cap: string().min(1),
|
|
50793
|
+
fieldPath: string().min(1),
|
|
50794
|
+
deviceType: _enum(DeviceType).optional()
|
|
50795
|
+
}), RunningThresholdProposalSchema, {
|
|
50796
|
+
auth: "admin",
|
|
50797
|
+
access: "view"
|
|
49367
50798
|
}), method(object({
|
|
49368
50799
|
blockId: string().min(1),
|
|
49369
50800
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -50340,7 +51771,27 @@ method(object({
|
|
|
50340
51771
|
* exposes only the COMMON surface: link state, supported device
|
|
50341
51772
|
* kinds (so the UI can filter the device picker), and per-device
|
|
50342
51773
|
* expose/unexpose.
|
|
50343
|
-
|
|
51774
|
+
*
|
|
51775
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
51776
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
51777
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
51778
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
51779
|
+
* default instance's); present = that instance; an id the provider does not
|
|
51780
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
51781
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
51782
|
+
* change always names the target it changes. A single-instance provider
|
|
51783
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
51784
|
+
*/
|
|
51785
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
51786
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
51787
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
51788
|
+
/** One independent export target of an addon. */
|
|
51789
|
+
var ExportInstanceSchema = object({
|
|
51790
|
+
instanceId: InstanceIdSchema$1,
|
|
51791
|
+
name: string(),
|
|
51792
|
+
enabled: boolean(),
|
|
51793
|
+
kind: string().optional()
|
|
51794
|
+
});
|
|
50344
51795
|
var LinkStateSchema = _enum([
|
|
50345
51796
|
"unlinked",
|
|
50346
51797
|
"linked",
|
|
@@ -50391,10 +51842,14 @@ var ExposedDeviceSchema = object({
|
|
|
50391
51842
|
capabilities: array(string()).optional()
|
|
50392
51843
|
});
|
|
50393
51844
|
var ExposeInputSchema = object({
|
|
51845
|
+
instanceId: InstanceIdSchema$1,
|
|
50394
51846
|
deviceId: string(),
|
|
50395
51847
|
capabilities: array(string()).optional()
|
|
50396
51848
|
});
|
|
50397
|
-
var UnexposeInputSchema = object({
|
|
51849
|
+
var UnexposeInputSchema = object({
|
|
51850
|
+
instanceId: InstanceIdSchema$1,
|
|
51851
|
+
deviceId: string()
|
|
51852
|
+
});
|
|
50398
51853
|
/**
|
|
50399
51854
|
* What an exporter may expose besides devices (D968). An action and a
|
|
50400
51855
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -50423,15 +51878,21 @@ var ExposedEntitySchema = object({
|
|
|
50423
51878
|
entity: ExportEntityRefSchema,
|
|
50424
51879
|
exposedAs: string().optional()
|
|
50425
51880
|
});
|
|
50426
|
-
var ExposeEntityInputSchema = object({
|
|
50427
|
-
|
|
50428
|
-
|
|
51881
|
+
var ExposeEntityInputSchema = object({
|
|
51882
|
+
instanceId: InstanceIdSchema$1,
|
|
51883
|
+
entity: ExportEntityRefSchema
|
|
51884
|
+
});
|
|
51885
|
+
var UnexposeEntityInputSchema = object({
|
|
51886
|
+
instanceId: InstanceIdSchema$1,
|
|
51887
|
+
entity: ExportEntityRefSchema
|
|
51888
|
+
});
|
|
51889
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
50429
51890
|
kind: "mutation",
|
|
50430
51891
|
auth: "admin"
|
|
50431
51892
|
}), method(UnexposeInputSchema, _void(), {
|
|
50432
51893
|
kind: "mutation",
|
|
50433
51894
|
auth: "admin"
|
|
50434
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
51895
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
50435
51896
|
kind: "mutation",
|
|
50436
51897
|
auth: "admin"
|
|
50437
51898
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -52433,7 +53894,7 @@ var DishwasherStatusSchema = object({
|
|
|
52433
53894
|
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
52434
53895
|
lastFetchedAt: number()
|
|
52435
53896
|
});
|
|
52436
|
-
var DeviceIdSchema$
|
|
53897
|
+
var DeviceIdSchema$2 = number().int().nonnegative();
|
|
52437
53898
|
/** A value a program option or setting can take. */
|
|
52438
53899
|
var DishwasherValueSchema = union([
|
|
52439
53900
|
boolean(),
|
|
@@ -52508,77 +53969,77 @@ var dishwasherCapability = {
|
|
|
52508
53969
|
* option feature keys; each is checked against the program's constraints.
|
|
52509
53970
|
*/
|
|
52510
53971
|
start: method(object({
|
|
52511
|
-
deviceId: DeviceIdSchema$
|
|
53972
|
+
deviceId: DeviceIdSchema$2,
|
|
52512
53973
|
program: string().min(1).optional(),
|
|
52513
53974
|
options: record(string(), DishwasherValueSchema).optional()
|
|
52514
53975
|
}), _void(), {
|
|
52515
53976
|
kind: "mutation",
|
|
52516
53977
|
auth: "protected"
|
|
52517
53978
|
}),
|
|
52518
|
-
pause: method(object({ deviceId: DeviceIdSchema$
|
|
53979
|
+
pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52519
53980
|
kind: "mutation",
|
|
52520
53981
|
auth: "protected"
|
|
52521
53982
|
}),
|
|
52522
|
-
resume: method(object({ deviceId: DeviceIdSchema$
|
|
53983
|
+
resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52523
53984
|
kind: "mutation",
|
|
52524
53985
|
auth: "protected"
|
|
52525
53986
|
}),
|
|
52526
|
-
stop: method(object({ deviceId: DeviceIdSchema$
|
|
53987
|
+
stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52527
53988
|
kind: "mutation",
|
|
52528
53989
|
auth: "protected"
|
|
52529
53990
|
}),
|
|
52530
53991
|
/** Select (not start) a program; refused when the appliance does not offer it. */
|
|
52531
53992
|
setProgram: method(object({
|
|
52532
|
-
deviceId: DeviceIdSchema$
|
|
53993
|
+
deviceId: DeviceIdSchema$2,
|
|
52533
53994
|
program: string().min(1)
|
|
52534
53995
|
}), _void(), {
|
|
52535
53996
|
kind: "mutation",
|
|
52536
53997
|
auth: "protected"
|
|
52537
53998
|
}),
|
|
52538
53999
|
setPower: method(object({
|
|
52539
|
-
deviceId: DeviceIdSchema$
|
|
54000
|
+
deviceId: DeviceIdSchema$2,
|
|
52540
54001
|
on: boolean()
|
|
52541
54002
|
}), _void(), {
|
|
52542
54003
|
kind: "mutation",
|
|
52543
54004
|
auth: "protected"
|
|
52544
54005
|
}),
|
|
52545
54006
|
setHalfLoad: method(object({
|
|
52546
|
-
deviceId: DeviceIdSchema$
|
|
54007
|
+
deviceId: DeviceIdSchema$2,
|
|
52547
54008
|
on: boolean()
|
|
52548
54009
|
}), _void(), {
|
|
52549
54010
|
kind: "mutation",
|
|
52550
54011
|
auth: "protected"
|
|
52551
54012
|
}),
|
|
52552
54013
|
setHygienePlus: method(object({
|
|
52553
|
-
deviceId: DeviceIdSchema$
|
|
54014
|
+
deviceId: DeviceIdSchema$2,
|
|
52554
54015
|
on: boolean()
|
|
52555
54016
|
}), _void(), {
|
|
52556
54017
|
kind: "mutation",
|
|
52557
54018
|
auth: "protected"
|
|
52558
54019
|
}),
|
|
52559
54020
|
setExtraDry: method(object({
|
|
52560
|
-
deviceId: DeviceIdSchema$
|
|
54021
|
+
deviceId: DeviceIdSchema$2,
|
|
52561
54022
|
on: boolean()
|
|
52562
54023
|
}), _void(), {
|
|
52563
54024
|
kind: "mutation",
|
|
52564
54025
|
auth: "protected"
|
|
52565
54026
|
}),
|
|
52566
54027
|
setIntensiveZone: method(object({
|
|
52567
|
-
deviceId: DeviceIdSchema$
|
|
54028
|
+
deviceId: DeviceIdSchema$2,
|
|
52568
54029
|
on: boolean()
|
|
52569
54030
|
}), _void(), {
|
|
52570
54031
|
kind: "mutation",
|
|
52571
54032
|
auth: "protected"
|
|
52572
54033
|
}),
|
|
52573
54034
|
setVarioSpeedPlus: method(object({
|
|
52574
|
-
deviceId: DeviceIdSchema$
|
|
54035
|
+
deviceId: DeviceIdSchema$2,
|
|
52575
54036
|
on: boolean()
|
|
52576
54037
|
}), _void(), {
|
|
52577
54038
|
kind: "mutation",
|
|
52578
54039
|
auth: "protected"
|
|
52579
54040
|
}),
|
|
52580
54041
|
setChildLock: method(object({
|
|
52581
|
-
deviceId: DeviceIdSchema$
|
|
54042
|
+
deviceId: DeviceIdSchema$2,
|
|
52582
54043
|
on: boolean()
|
|
52583
54044
|
}), _void(), {
|
|
52584
54045
|
kind: "mutation",
|
|
@@ -52590,7 +54051,7 @@ var dishwasherCapability = {
|
|
|
52590
54051
|
* its declared range / members.
|
|
52591
54052
|
*/
|
|
52592
54053
|
setOption: method(object({
|
|
52593
|
-
deviceId: DeviceIdSchema$
|
|
54054
|
+
deviceId: DeviceIdSchema$2,
|
|
52594
54055
|
key: string().min(1),
|
|
52595
54056
|
value: DishwasherValueSchema
|
|
52596
54057
|
}), _void(), {
|
|
@@ -52598,7 +54059,7 @@ var dishwasherCapability = {
|
|
|
52598
54059
|
auth: "protected"
|
|
52599
54060
|
}),
|
|
52600
54061
|
/** What this appliance can do, from its own description — the UI is derived from it. */
|
|
52601
|
-
getOptions: method(object({ deviceId: DeviceIdSchema$
|
|
54062
|
+
getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
|
|
52602
54063
|
},
|
|
52603
54064
|
status: {
|
|
52604
54065
|
schema: DishwasherStatusSchema,
|
|
@@ -54022,15 +55483,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
54022
55483
|
items: array(FanCaptureItemSchema),
|
|
54023
55484
|
omitted: array(FanCaptureOmissionSchema)
|
|
54024
55485
|
});
|
|
54025
|
-
var deviceInput$
|
|
55486
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
54026
55487
|
DeviceType.Fan;
|
|
54027
|
-
method(deviceInput$
|
|
55488
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
54028
55489
|
kind: "mutation",
|
|
54029
55490
|
auth: "admin"
|
|
54030
|
-
}), method(deviceInput$
|
|
55491
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
54031
55492
|
kind: "mutation",
|
|
54032
55493
|
auth: "protected"
|
|
54033
|
-
}), method(deviceInput$
|
|
55494
|
+
}), method(deviceInput$2.extend({
|
|
54034
55495
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
54035
55496
|
steps: array(ActionStepSchema).max(20).default([])
|
|
54036
55497
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -54913,6 +56374,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54913
56374
|
}), object({ success: boolean() }), {
|
|
54914
56375
|
kind: "mutation",
|
|
54915
56376
|
auth: "admin"
|
|
56377
|
+
}), method(object({ id: string() }), object({
|
|
56378
|
+
rows: number().int().nonnegative(),
|
|
56379
|
+
secrets: number().int().nonnegative()
|
|
56380
|
+
}), {
|
|
56381
|
+
kind: "mutation",
|
|
56382
|
+
auth: "admin"
|
|
54916
56383
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54917
56384
|
addonId: string(),
|
|
54918
56385
|
settings: record(string(), unknown())
|
|
@@ -54941,13 +56408,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54941
56408
|
*/
|
|
54942
56409
|
var ConfigSchemaPassthrough$3 = custom();
|
|
54943
56410
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
56411
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56412
|
+
/**
|
|
56413
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
56414
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
56415
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
56416
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
56417
|
+
* instance id.
|
|
56418
|
+
*/
|
|
56419
|
+
var IntegrationInstanceKindSchema = object({
|
|
56420
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
56421
|
+
id: string().min(1),
|
|
56422
|
+
label: string().min(1),
|
|
56423
|
+
/** The kind signs in through a server-held interactive session. */
|
|
56424
|
+
signIn: object({
|
|
56425
|
+
basePath: string().min(1),
|
|
56426
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
56427
|
+
}).optional(),
|
|
56428
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
56429
|
+
connectPath: string().min(1).optional(),
|
|
56430
|
+
/** Relative hub path to disconnect that account. */
|
|
56431
|
+
disconnectPath: string().min(1).optional()
|
|
56432
|
+
});
|
|
54944
56433
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56434
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56435
|
+
role: _enum([
|
|
56436
|
+
"devices",
|
|
56437
|
+
"services",
|
|
56438
|
+
"both"
|
|
56439
|
+
]),
|
|
56440
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56441
|
+
discovery: object({
|
|
56442
|
+
mode: _enum([
|
|
56443
|
+
"none",
|
|
56444
|
+
"lan",
|
|
56445
|
+
"account"
|
|
56446
|
+
]),
|
|
56447
|
+
scopedToInstance: boolean()
|
|
56448
|
+
}).optional(),
|
|
54945
56449
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
54946
|
-
instanceMode:
|
|
56450
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
54947
56451
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
54948
56452
|
kind: string(),
|
|
54949
56453
|
/** True when `testInstance` really probes the remote. */
|
|
54950
|
-
canTest: boolean()
|
|
56454
|
+
canTest: boolean(),
|
|
56455
|
+
/**
|
|
56456
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
56457
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
56458
|
+
*/
|
|
56459
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
56460
|
+
/**
|
|
56461
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
56462
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
56463
|
+
*/
|
|
56464
|
+
allowRestoreId: boolean().optional(),
|
|
56465
|
+
/**
|
|
56466
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
56467
|
+
* send an id (the UI then mints an opaque one).
|
|
56468
|
+
*/
|
|
56469
|
+
mintsId: boolean().optional()
|
|
54951
56470
|
});
|
|
54952
56471
|
var IntegrationInstanceStatusSchema = object({
|
|
54953
56472
|
state: _enum([
|
|
@@ -54956,16 +56475,28 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
54956
56475
|
"error",
|
|
54957
56476
|
"unknown"
|
|
54958
56477
|
]),
|
|
54959
|
-
detail: string().optional()
|
|
56478
|
+
detail: string().optional(),
|
|
56479
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
56480
|
+
label: string().optional()
|
|
54960
56481
|
});
|
|
54961
56482
|
var IntegrationInstanceSchema = object({
|
|
54962
56483
|
id: IntegrationInstanceIdSchema,
|
|
54963
56484
|
name: string(),
|
|
54964
56485
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
54965
56486
|
configured: boolean(),
|
|
56487
|
+
/**
|
|
56488
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56489
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56490
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56491
|
+
*/
|
|
56492
|
+
enabled: boolean().optional(),
|
|
56493
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56494
|
+
kind: string().optional(),
|
|
54966
56495
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
54967
56496
|
formValues: record(string(), unknown()),
|
|
54968
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
56497
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
56498
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
56499
|
+
lastCheckedAt: number().optional()
|
|
54969
56500
|
});
|
|
54970
56501
|
var IntegrationInstancePatchSchema = object({
|
|
54971
56502
|
id: IntegrationInstanceIdSchema,
|
|
@@ -54973,25 +56504,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
54973
56504
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
54974
56505
|
values: record(string(), unknown())
|
|
54975
56506
|
});
|
|
54976
|
-
|
|
54977
|
-
|
|
54978
|
-
|
|
54979
|
-
|
|
54980
|
-
|
|
54981
|
-
|
|
54982
|
-
|
|
54983
|
-
|
|
54984
|
-
|
|
54985
|
-
|
|
54986
|
-
|
|
54987
|
-
|
|
54988
|
-
|
|
54989
|
-
|
|
54990
|
-
|
|
54991
|
-
|
|
54992
|
-
|
|
54993
|
-
|
|
54994
|
-
})
|
|
56507
|
+
var integrationInstancesCapability = {
|
|
56508
|
+
name: "integration-instances",
|
|
56509
|
+
scope: "system",
|
|
56510
|
+
mode: "collection",
|
|
56511
|
+
methods: {
|
|
56512
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
56513
|
+
/** The form; `id: null` is the create form. */
|
|
56514
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
56515
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
56516
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
56517
|
+
/**
|
|
56518
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
56519
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
56520
|
+
*/
|
|
56521
|
+
createInstance: method(object({
|
|
56522
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
56523
|
+
name: string().min(1),
|
|
56524
|
+
kind: string().min(1).optional()
|
|
56525
|
+
}), object({ id: string() }), {
|
|
56526
|
+
kind: "mutation",
|
|
56527
|
+
auth: "admin"
|
|
56528
|
+
}),
|
|
56529
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
56530
|
+
kind: "mutation",
|
|
56531
|
+
auth: "admin"
|
|
56532
|
+
}),
|
|
56533
|
+
/**
|
|
56534
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
56535
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56536
|
+
*/
|
|
56537
|
+
setInstanceEnabled: method(object({
|
|
56538
|
+
id: IntegrationInstanceIdSchema,
|
|
56539
|
+
enabled: boolean()
|
|
56540
|
+
}), object({ ok: literal(true) }), {
|
|
56541
|
+
kind: "mutation",
|
|
56542
|
+
auth: "admin"
|
|
56543
|
+
}),
|
|
56544
|
+
/** Delete the record and its keychain refs together. */
|
|
56545
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
56546
|
+
kind: "mutation",
|
|
56547
|
+
auth: "admin"
|
|
56548
|
+
}),
|
|
56549
|
+
testInstance: optionalMethod(object({
|
|
56550
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
56551
|
+
values: record(string(), unknown())
|
|
56552
|
+
}), TestConnectionResultSchema$1, {
|
|
56553
|
+
kind: "mutation",
|
|
56554
|
+
auth: "admin"
|
|
56555
|
+
})
|
|
56556
|
+
}
|
|
56557
|
+
};
|
|
54995
56558
|
/**
|
|
54996
56559
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
54997
56560
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -57009,6 +58572,11 @@ var LogEntrySchema = object({
|
|
|
57009
58572
|
* flight, and not asking for the next slice ends the scan.
|
|
57010
58573
|
*/
|
|
57011
58574
|
var LogReadQuerySchema = object({
|
|
58575
|
+
/**
|
|
58576
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
58577
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
58578
|
+
*/
|
|
58579
|
+
instanceId: string().min(1).max(200).optional(),
|
|
57012
58580
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
57013
58581
|
requestId: string().min(1).max(64).optional(),
|
|
57014
58582
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -57071,6 +58639,8 @@ var LogReadResultSchema = object({
|
|
|
57071
58639
|
/** True when `cancelRead` ended this slice early. */
|
|
57072
58640
|
cancelled: boolean().optional()
|
|
57073
58641
|
});
|
|
58642
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
58643
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
57074
58644
|
var LogReadCapabilitiesSchema = object({
|
|
57075
58645
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
57076
58646
|
readable: boolean(),
|
|
@@ -57089,7 +58659,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
57089
58659
|
/** How far back the store reaches, when the provider knows (days). */
|
|
57090
58660
|
retentionDays: number().nullable()
|
|
57091
58661
|
});
|
|
57092
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
58662
|
+
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() }), {
|
|
57093
58663
|
kind: "mutation",
|
|
57094
58664
|
auth: "admin"
|
|
57095
58665
|
});
|
|
@@ -57193,214 +58763,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
57193
58763
|
]);
|
|
57194
58764
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
57195
58765
|
/**
|
|
57196
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
57197
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
57198
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
57199
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
57200
|
-
*
|
|
57201
|
-
* The cap declares a wide method surface; not every backend supports
|
|
57202
|
-
* every method. Drivers signal sub-capability via the
|
|
57203
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
57204
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
57205
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
57206
|
-
*/
|
|
57207
|
-
var MediaPlayerStateSchema = _enum([
|
|
57208
|
-
"off",
|
|
57209
|
-
"on",
|
|
57210
|
-
"idle",
|
|
57211
|
-
"playing",
|
|
57212
|
-
"paused",
|
|
57213
|
-
"buffering",
|
|
57214
|
-
"standby"
|
|
57215
|
-
]);
|
|
57216
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
57217
|
-
"off",
|
|
57218
|
-
"all",
|
|
57219
|
-
"one"
|
|
57220
|
-
]);
|
|
57221
|
-
var MediaInfoSchema = object({
|
|
57222
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
57223
|
-
* `podcast`, …). */
|
|
57224
|
-
type: string(),
|
|
57225
|
-
/** Human-readable title. */
|
|
57226
|
-
title: string(),
|
|
57227
|
-
/** Optional artist / channel / station label. */
|
|
57228
|
-
artist: string().optional(),
|
|
57229
|
-
/** Optional album / season / show name. */
|
|
57230
|
-
album: string().optional(),
|
|
57231
|
-
/** Optional cover-art / thumbnail URL. */
|
|
57232
|
-
imageUrl: string().optional()
|
|
57233
|
-
});
|
|
57234
|
-
/**
|
|
57235
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
57236
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
57237
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
57238
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
57239
|
-
* guess, never as a reading.
|
|
57240
|
-
*/
|
|
57241
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
57242
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
57243
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
57244
|
-
"state",
|
|
57245
|
-
"volumeLevel",
|
|
57246
|
-
"isMuted",
|
|
57247
|
-
"source"
|
|
57248
|
-
]);
|
|
57249
|
-
var MediaPlayerStatusSchema = object({
|
|
57250
|
-
/** Playback lifecycle state. */
|
|
57251
|
-
state: MediaPlayerStateSchema,
|
|
57252
|
-
/**
|
|
57253
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
57254
|
-
* A slice written before this field existed parses as `reported`.
|
|
57255
|
-
*/
|
|
57256
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
57257
|
-
/**
|
|
57258
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
57259
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
57260
|
-
* from a running-state source, mute guessed from the last send).
|
|
57261
|
-
*/
|
|
57262
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
57263
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
57264
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
57265
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
57266
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
57267
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
57268
|
-
isMuted: boolean().nullable(),
|
|
57269
|
-
/** Active source / input. Empty when no source surface. */
|
|
57270
|
-
source: string(),
|
|
57271
|
-
/** Selectable sources. Empty when no source surface.
|
|
57272
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
57273
|
-
availableSources: array(string()),
|
|
57274
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
57275
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
57276
|
-
/** Current playback position in ms. Null when not seekable or
|
|
57277
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
57278
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
57279
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
57280
|
-
* (live stream) or nothing is playing. */
|
|
57281
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
57282
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
57283
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
57284
|
-
shuffle: boolean().nullable(),
|
|
57285
|
-
/** Repeat mode. Null when no repeat surface.
|
|
57286
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
57287
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
57288
|
-
/** Ms epoch when the slice was last updated. */
|
|
57289
|
-
lastChangedAt: number()
|
|
57290
|
-
});
|
|
57291
|
-
var mediaPlayerCapability = {
|
|
57292
|
-
name: "media-player",
|
|
57293
|
-
scope: "device",
|
|
57294
|
-
deviceNative: true,
|
|
57295
|
-
mode: "singleton",
|
|
57296
|
-
deviceTypes: [DeviceType.MediaPlayer],
|
|
57297
|
-
methods: {
|
|
57298
|
-
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57299
|
-
kind: "mutation",
|
|
57300
|
-
auth: "protected"
|
|
57301
|
-
}),
|
|
57302
|
-
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57303
|
-
kind: "mutation",
|
|
57304
|
-
auth: "protected"
|
|
57305
|
-
}),
|
|
57306
|
-
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57307
|
-
kind: "mutation",
|
|
57308
|
-
auth: "protected"
|
|
57309
|
-
}),
|
|
57310
|
-
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57311
|
-
kind: "mutation",
|
|
57312
|
-
auth: "protected"
|
|
57313
|
-
}),
|
|
57314
|
-
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57315
|
-
kind: "mutation",
|
|
57316
|
-
auth: "protected"
|
|
57317
|
-
}),
|
|
57318
|
-
seek: method(object({
|
|
57319
|
-
deviceId: number().int().nonnegative(),
|
|
57320
|
-
position: number().int().nonnegative()
|
|
57321
|
-
}), _void(), {
|
|
57322
|
-
kind: "mutation",
|
|
57323
|
-
auth: "protected"
|
|
57324
|
-
}),
|
|
57325
|
-
setVolume: method(object({
|
|
57326
|
-
deviceId: number().int().nonnegative(),
|
|
57327
|
-
volumeLevel: number().min(0).max(100)
|
|
57328
|
-
}), _void(), {
|
|
57329
|
-
kind: "mutation",
|
|
57330
|
-
auth: "protected"
|
|
57331
|
-
}),
|
|
57332
|
-
setMute: method(object({
|
|
57333
|
-
deviceId: number().int().nonnegative(),
|
|
57334
|
-
muted: boolean()
|
|
57335
|
-
}), _void(), {
|
|
57336
|
-
kind: "mutation",
|
|
57337
|
-
auth: "protected"
|
|
57338
|
-
}),
|
|
57339
|
-
setShuffle: method(object({
|
|
57340
|
-
deviceId: number().int().nonnegative(),
|
|
57341
|
-
shuffle: boolean()
|
|
57342
|
-
}), _void(), {
|
|
57343
|
-
kind: "mutation",
|
|
57344
|
-
auth: "protected"
|
|
57345
|
-
}),
|
|
57346
|
-
setRepeat: method(object({
|
|
57347
|
-
deviceId: number().int().nonnegative(),
|
|
57348
|
-
repeat: MediaPlayerRepeatSchema
|
|
57349
|
-
}), _void(), {
|
|
57350
|
-
kind: "mutation",
|
|
57351
|
-
auth: "protected"
|
|
57352
|
-
}),
|
|
57353
|
-
selectSource: method(object({
|
|
57354
|
-
deviceId: number().int().nonnegative(),
|
|
57355
|
-
source: string().min(1)
|
|
57356
|
-
}), _void(), {
|
|
57357
|
-
kind: "mutation",
|
|
57358
|
-
auth: "protected"
|
|
57359
|
-
}),
|
|
57360
|
-
playMedia: method(object({
|
|
57361
|
-
deviceId: number().int().nonnegative(),
|
|
57362
|
-
/** Media identifier / URL. */
|
|
57363
|
-
mediaId: string().min(1),
|
|
57364
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
57365
|
-
* passes it through to the upstream service. */
|
|
57366
|
-
mediaType: string().min(1)
|
|
57367
|
-
}), _void(), {
|
|
57368
|
-
kind: "mutation",
|
|
57369
|
-
auth: "protected"
|
|
57370
|
-
})
|
|
57371
|
-
},
|
|
57372
|
-
status: {
|
|
57373
|
-
schema: MediaPlayerStatusSchema,
|
|
57374
|
-
kind: "push"
|
|
57375
|
-
},
|
|
57376
|
-
/**
|
|
57377
|
-
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
57378
|
-
* full slice for live now-playing, volume, and progress updates
|
|
57379
|
-
* without polling.
|
|
57380
|
-
*/
|
|
57381
|
-
runtimeState: MediaPlayerStatusSchema,
|
|
57382
|
-
/**
|
|
57383
|
-
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
57384
|
-
*
|
|
57385
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
57386
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
57387
|
-
*/
|
|
57388
|
-
durability: "session",
|
|
57389
|
-
/**
|
|
57390
|
-
* History (D850): volume (a level an operator or an automation sets; the
|
|
57391
|
-
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
57392
|
-
* `position` is not recorded: it moves continuously while playing.
|
|
57393
|
-
*/
|
|
57394
|
-
history: { fields: {
|
|
57395
|
-
volumeLevel: {
|
|
57396
|
-
kind: "gauge",
|
|
57397
|
-
unit: "%",
|
|
57398
|
-
deadband: 1
|
|
57399
|
-
},
|
|
57400
|
-
isMuted: { kind: "boolean" }
|
|
57401
|
-
} }
|
|
57402
|
-
};
|
|
57403
|
-
/**
|
|
57404
58766
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
57405
58767
|
*
|
|
57406
58768
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -66159,6 +67521,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
66159
67521
|
"sound",
|
|
66160
67522
|
"system"
|
|
66161
67523
|
]);
|
|
67524
|
+
/**
|
|
67525
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
67526
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
67527
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
67528
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
67529
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
67530
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
67531
|
+
* the hub's current language and with its overrides applied.
|
|
67532
|
+
*/
|
|
67533
|
+
var NcTemplateDefaultSchema = object({
|
|
67534
|
+
id: _enum(["appliance-cycle"]),
|
|
67535
|
+
kind: NcRuleKindSchema,
|
|
67536
|
+
title: string(),
|
|
67537
|
+
body: string()
|
|
67538
|
+
});
|
|
66162
67539
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
66163
67540
|
var NcTemplateFamilySchema = _enum([
|
|
66164
67541
|
"rule",
|
|
@@ -67014,7 +68391,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
67014
68391
|
}), method(object({}), object({
|
|
67015
68392
|
catalog: array(NcConditionDescriptorSchema),
|
|
67016
68393
|
taxonomy: NcTaxonomySchema.optional()
|
|
67017
|
-
})), method(object({}), object({
|
|
68394
|
+
})), method(object({}), object({
|
|
68395
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
68396
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
68397
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
68398
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
67018
68399
|
kind: "mutation",
|
|
67019
68400
|
access: "view"
|
|
67020
68401
|
}), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
|
|
@@ -67910,7 +69291,7 @@ var petFeederCapability = {
|
|
|
67910
69291
|
/**
|
|
67911
69292
|
* Pet water-fountain cap. Models the telemetry and the control surface of a
|
|
67912
69293
|
* circulating drinking fountain: how much water is in the tank, how much the
|
|
67913
|
-
* pet drank, whether the pump runs
|
|
69294
|
+
* pet drank, whether the pump runs.
|
|
67914
69295
|
*
|
|
67915
69296
|
* It has no native vendor provider of its own: a fountain reaches CamStack as
|
|
67916
69297
|
* a container of loose entities (a pump switch, two number boxes, a "filter
|
|
@@ -67926,8 +69307,10 @@ var petFeederCapability = {
|
|
|
67926
69307
|
* never here. Every reading may be `null`: a source that is offline or does
|
|
67927
69308
|
* not report is unknown, never zero.
|
|
67928
69309
|
*
|
|
67929
|
-
* The
|
|
67930
|
-
* cap
|
|
69310
|
+
* The pump's on/off duty cycle is model-specific and lives in the optional
|
|
69311
|
+
* `pump-cycle` cap. The filter, the cleaning and the water change are not
|
|
69312
|
+
* fields of this cap: they are items of the `consumables` cap on the same
|
|
69313
|
+
* device (kind `wear`), reset by the same forward mechanism.
|
|
67931
69314
|
*/
|
|
67932
69315
|
var PetFountainStatusSchema = object({
|
|
67933
69316
|
/** Water in the tank, 0..100 (%). Null while the source has not reported. */
|
|
@@ -67938,27 +69321,20 @@ var PetFountainStatusSchema = object({
|
|
|
67938
69321
|
empty: boolean().nullable(),
|
|
67939
69322
|
/** True while the pump is running. Null while unknown. */
|
|
67940
69323
|
pumpRunning: boolean().nullable(),
|
|
67941
|
-
/** True when the pump works on an on/off duty cycle rather than continuously. */
|
|
67942
|
-
pumpIntervalEnabled: boolean().nullable(),
|
|
67943
|
-
/** How long the pump runs per cycle, in seconds. */
|
|
67944
|
-
pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
67945
|
-
/** How long the pump rests per cycle, in seconds. */
|
|
67946
|
-
pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
67947
69324
|
/** Water drunk in the last drink, in litres. */
|
|
67948
69325
|
lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67949
69326
|
/** Water drunk in the current hour, in litres. */
|
|
67950
69327
|
currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67951
69328
|
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
67952
69329
|
last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67953
|
-
/** Water drunk since local midnight, in litres. */
|
|
69330
|
+
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
67954
69331
|
todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67955
69332
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
67956
69333
|
error: string().nullable(),
|
|
67957
69334
|
/** Ms epoch when the slice was last refreshed. */
|
|
67958
69335
|
lastFetchedAt: number()
|
|
67959
69336
|
});
|
|
67960
|
-
var DeviceIdSchema = number().int().nonnegative();
|
|
67961
|
-
var MinutesSchema = number().int().min(0);
|
|
69337
|
+
var DeviceIdSchema$1 = number().int().nonnegative();
|
|
67962
69338
|
var petFountainCapability = {
|
|
67963
69339
|
name: "pet-fountain",
|
|
67964
69340
|
scope: "device",
|
|
@@ -67968,38 +69344,14 @@ var petFountainCapability = {
|
|
|
67968
69344
|
methods: {
|
|
67969
69345
|
/** Start or stop the pump. */
|
|
67970
69346
|
setPump: method(object({
|
|
67971
|
-
deviceId: DeviceIdSchema,
|
|
67972
|
-
on: boolean()
|
|
67973
|
-
}), _void(), {
|
|
67974
|
-
kind: "mutation",
|
|
67975
|
-
auth: "protected"
|
|
67976
|
-
}),
|
|
67977
|
-
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
67978
|
-
setPumpInterval: method(object({
|
|
67979
|
-
deviceId: DeviceIdSchema,
|
|
69347
|
+
deviceId: DeviceIdSchema$1,
|
|
67980
69348
|
on: boolean()
|
|
67981
69349
|
}), _void(), {
|
|
67982
69350
|
kind: "mutation",
|
|
67983
69351
|
auth: "protected"
|
|
67984
69352
|
}),
|
|
67985
|
-
/** Set how many minutes the pump runs per cycle. */
|
|
67986
|
-
setPumpOnMinutes: method(object({
|
|
67987
|
-
deviceId: DeviceIdSchema,
|
|
67988
|
-
minutes: MinutesSchema
|
|
67989
|
-
}), _void(), {
|
|
67990
|
-
kind: "mutation",
|
|
67991
|
-
auth: "protected"
|
|
67992
|
-
}),
|
|
67993
|
-
/** Set how many minutes the pump rests per cycle. */
|
|
67994
|
-
setPumpOffMinutes: method(object({
|
|
67995
|
-
deviceId: DeviceIdSchema,
|
|
67996
|
-
minutes: MinutesSchema
|
|
67997
|
-
}), _void(), {
|
|
67998
|
-
kind: "mutation",
|
|
67999
|
-
auth: "protected"
|
|
68000
|
-
}),
|
|
68001
69353
|
/** Clear the rolling daily consumption counters on the device. */
|
|
68002
|
-
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
69354
|
+
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
68003
69355
|
kind: "mutation",
|
|
68004
69356
|
auth: "protected"
|
|
68005
69357
|
})
|
|
@@ -69634,6 +70986,14 @@ var powerMeterCapability = {
|
|
|
69634
70986
|
deviceNative: true,
|
|
69635
70987
|
mode: "singleton",
|
|
69636
70988
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
70989
|
+
deviceConfig: { ui: {
|
|
70990
|
+
kind: "widget",
|
|
70991
|
+
widgetId: "host/power-meter-line",
|
|
70992
|
+
tab: "power",
|
|
70993
|
+
placement: "hero",
|
|
70994
|
+
label: "Power",
|
|
70995
|
+
order: 4
|
|
70996
|
+
} },
|
|
69637
70997
|
methods: {},
|
|
69638
70998
|
status: {
|
|
69639
70999
|
schema: PowerMeterStatusSchema,
|
|
@@ -69693,6 +71053,117 @@ var powerMeterCapability = {
|
|
|
69693
71053
|
volatileStateFields: ["lastFetchedAt"],
|
|
69694
71054
|
customize: powerMeterCustomize
|
|
69695
71055
|
};
|
|
71056
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
71057
|
+
"no-power-source",
|
|
71058
|
+
"switch-missing",
|
|
71059
|
+
"switch-offline",
|
|
71060
|
+
"switch-unknown",
|
|
71061
|
+
"restart-in-progress",
|
|
71062
|
+
"switch-call-failed"
|
|
71063
|
+
]);
|
|
71064
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
71065
|
+
accepted: literal(false),
|
|
71066
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
71067
|
+
/** One line for the operator, naming the switch. */
|
|
71068
|
+
message: string()
|
|
71069
|
+
})]);
|
|
71070
|
+
var PowerSourceInfoSchema = object({
|
|
71071
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
71072
|
+
linked: boolean(),
|
|
71073
|
+
switchDeviceId: number().nullable(),
|
|
71074
|
+
switchName: string().nullable(),
|
|
71075
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
71076
|
+
switchOn: boolean().nullable(),
|
|
71077
|
+
switchOnline: boolean().nullable(),
|
|
71078
|
+
restartInProgress: boolean(),
|
|
71079
|
+
offSeconds: number().int()
|
|
71080
|
+
});
|
|
71081
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
71082
|
+
var PowerSourceStateSchema = object({
|
|
71083
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
71084
|
+
switchOn: boolean().nullable(),
|
|
71085
|
+
offSeconds: number().int(),
|
|
71086
|
+
lastFetchedAt: number()
|
|
71087
|
+
});
|
|
71088
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
71089
|
+
DeviceType.Gateway,
|
|
71090
|
+
DeviceType.Hub,
|
|
71091
|
+
DeviceType.Camera,
|
|
71092
|
+
DeviceType.Sensor,
|
|
71093
|
+
DeviceType.MediaPlayer,
|
|
71094
|
+
DeviceType.Vacuum,
|
|
71095
|
+
DeviceType.Generic,
|
|
71096
|
+
DeviceType.Washer,
|
|
71097
|
+
DeviceType.Dryer,
|
|
71098
|
+
DeviceType.Dishwasher,
|
|
71099
|
+
DeviceType.PetFountain,
|
|
71100
|
+
DeviceType.Fan,
|
|
71101
|
+
DeviceType.Humidifier,
|
|
71102
|
+
DeviceType.Climate,
|
|
71103
|
+
DeviceType.WaterHeater
|
|
71104
|
+
];
|
|
71105
|
+
var powerSourceCustomize = {
|
|
71106
|
+
version: 1,
|
|
71107
|
+
targets: [{
|
|
71108
|
+
id: "switch",
|
|
71109
|
+
cardinality: "scalar",
|
|
71110
|
+
vocabulary: {
|
|
71111
|
+
cap: "power-source",
|
|
71112
|
+
stateField: "switchOn"
|
|
71113
|
+
},
|
|
71114
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71115
|
+
apply: "add",
|
|
71116
|
+
path: "switchOn",
|
|
71117
|
+
sources: [{
|
|
71118
|
+
kind: "switch",
|
|
71119
|
+
transforms: [{ kind: "identity" }]
|
|
71120
|
+
}],
|
|
71121
|
+
companions: [{
|
|
71122
|
+
kind: "constant",
|
|
71123
|
+
path: "offSeconds",
|
|
71124
|
+
value: 10,
|
|
71125
|
+
when: "add"
|
|
71126
|
+
}, {
|
|
71127
|
+
kind: "clock",
|
|
71128
|
+
path: "lastFetchedAt",
|
|
71129
|
+
when: "always"
|
|
71130
|
+
}],
|
|
71131
|
+
sourceReport: { status: "assignment-status" }
|
|
71132
|
+
}]
|
|
71133
|
+
};
|
|
71134
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
71135
|
+
var powerSourceCapability = {
|
|
71136
|
+
name: "power-source",
|
|
71137
|
+
scope: "device",
|
|
71138
|
+
mode: "singleton",
|
|
71139
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
71140
|
+
methods: {
|
|
71141
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
71142
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71143
|
+
kind: "mutation",
|
|
71144
|
+
auth: "admin"
|
|
71145
|
+
}),
|
|
71146
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71147
|
+
kind: "mutation",
|
|
71148
|
+
auth: "admin"
|
|
71149
|
+
}),
|
|
71150
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
71151
|
+
kind: "mutation",
|
|
71152
|
+
auth: "admin"
|
|
71153
|
+
})
|
|
71154
|
+
},
|
|
71155
|
+
runtimeState: PowerSourceStateSchema,
|
|
71156
|
+
customize: powerSourceCustomize,
|
|
71157
|
+
/**
|
|
71158
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
71159
|
+
* its next report; a restored value would outlive the reading it names.
|
|
71160
|
+
*
|
|
71161
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71162
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71163
|
+
*/
|
|
71164
|
+
durability: "session",
|
|
71165
|
+
history: "none"
|
|
71166
|
+
};
|
|
69696
71167
|
/**
|
|
69697
71168
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
69698
71169
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -70333,6 +71804,79 @@ var ptzAutotrackCapability = {
|
|
|
70333
71804
|
history: "none"
|
|
70334
71805
|
};
|
|
70335
71806
|
/**
|
|
71807
|
+
* The on/off duty cycle of a pet fountain's pump. Some models run the pump
|
|
71808
|
+
* continuously and have nothing to configure; others can alternate a run time
|
|
71809
|
+
* and a rest time. That is model-specific, so it is its own optional cap on the
|
|
71810
|
+
* fountain device: a fountain without cycle entities simply never composes it
|
|
71811
|
+
* (planning#366), and the `pet-fountain` cap stays the model-independent core.
|
|
71812
|
+
*
|
|
71813
|
+
* Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
|
|
71814
|
+
* cycle from loose entities (a cycle switch and two number boxes) and every
|
|
71815
|
+
* method is a forward to one source entity (D659, D967). Durations are stored
|
|
71816
|
+
* in SECONDS (D944); a number box that cannot be read back leaves them `null`
|
|
71817
|
+
* — unknown, never zero (D393).
|
|
71818
|
+
*/
|
|
71819
|
+
var PumpCycleStatusSchema = object({
|
|
71820
|
+
/** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
|
|
71821
|
+
enabled: boolean().nullable(),
|
|
71822
|
+
/** How long the pump runs per cycle, in seconds. Null while unknown. */
|
|
71823
|
+
onTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
71824
|
+
/** How long the pump rests per cycle, in seconds. Null while unknown. */
|
|
71825
|
+
offTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
71826
|
+
/** Ms epoch when the slice was last refreshed. */
|
|
71827
|
+
lastFetchedAt: number()
|
|
71828
|
+
});
|
|
71829
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
71830
|
+
var MinutesSchema = number().int().min(0);
|
|
71831
|
+
var pumpCycleCapability = {
|
|
71832
|
+
name: "pump-cycle",
|
|
71833
|
+
scope: "device",
|
|
71834
|
+
deviceNative: true,
|
|
71835
|
+
mode: "singleton",
|
|
71836
|
+
deviceTypes: [DeviceType.PetFountain],
|
|
71837
|
+
methods: {
|
|
71838
|
+
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
71839
|
+
setEnabled: method(object({
|
|
71840
|
+
deviceId: DeviceIdSchema,
|
|
71841
|
+
on: boolean()
|
|
71842
|
+
}), _void(), {
|
|
71843
|
+
kind: "mutation",
|
|
71844
|
+
auth: "protected"
|
|
71845
|
+
}),
|
|
71846
|
+
/** Set how many minutes the pump runs per cycle. */
|
|
71847
|
+
setOnMinutes: method(object({
|
|
71848
|
+
deviceId: DeviceIdSchema,
|
|
71849
|
+
minutes: MinutesSchema
|
|
71850
|
+
}), _void(), {
|
|
71851
|
+
kind: "mutation",
|
|
71852
|
+
auth: "protected"
|
|
71853
|
+
}),
|
|
71854
|
+
/** Set how many minutes the pump rests per cycle. */
|
|
71855
|
+
setOffMinutes: method(object({
|
|
71856
|
+
deviceId: DeviceIdSchema,
|
|
71857
|
+
minutes: MinutesSchema
|
|
71858
|
+
}), _void(), {
|
|
71859
|
+
kind: "mutation",
|
|
71860
|
+
auth: "protected"
|
|
71861
|
+
})
|
|
71862
|
+
},
|
|
71863
|
+
status: {
|
|
71864
|
+
schema: PumpCycleStatusSchema,
|
|
71865
|
+
kind: "push"
|
|
71866
|
+
},
|
|
71867
|
+
/** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
|
|
71868
|
+
runtimeState: PumpCycleStatusSchema,
|
|
71869
|
+
/**
|
|
71870
|
+
* Runtime-state durability: **session** — live appliance state re-published on connect.
|
|
71871
|
+
*
|
|
71872
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71873
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71874
|
+
*/
|
|
71875
|
+
durability: "session",
|
|
71876
|
+
/** History (D850): a duty-cycle setting is not a series worth recording. */
|
|
71877
|
+
history: "none"
|
|
71878
|
+
};
|
|
71879
|
+
/**
|
|
70336
71880
|
* reboot — device-scoped capability for "soft" device reboots (firmware
|
|
70337
71881
|
* reboot via vendor protocol; cameras, NVRs, doorbells). Surfaces a
|
|
70338
71882
|
* single mutation so the UI can offer a confirm-and-reboot button for
|
|
@@ -73148,7 +74692,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
73148
74692
|
"next",
|
|
73149
74693
|
"previous",
|
|
73150
74694
|
"setMute",
|
|
73151
|
-
"selectSource"
|
|
74695
|
+
"selectSource",
|
|
74696
|
+
"turnOn",
|
|
74697
|
+
"turnOff",
|
|
74698
|
+
"volumeUp",
|
|
74699
|
+
"volumeDown",
|
|
74700
|
+
"channelUp",
|
|
74701
|
+
"channelDown"
|
|
73152
74702
|
]);
|
|
73153
74703
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
73154
74704
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -73169,7 +74719,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
73169
74719
|
}), RemoteSendResultSchema, {
|
|
73170
74720
|
kind: "mutation",
|
|
73171
74721
|
auth: "protected"
|
|
73172
|
-
}), method(deviceInput.extend({
|
|
74722
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
73173
74723
|
command: RemoteMediaCommandSchema,
|
|
73174
74724
|
muted: boolean().optional(),
|
|
73175
74725
|
source: string().optional(),
|
|
@@ -75415,7 +76965,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
|
|
|
75415
76965
|
* `getSettingsUISchema()` surface.
|
|
75416
76966
|
*/
|
|
75417
76967
|
var SwitchStatusSchema = object({
|
|
75418
|
-
|
|
76968
|
+
/**
|
|
76969
|
+
* `null` = the device has never reported its state (a measurement that
|
|
76970
|
+
* has not happened is unknown, never `false`).
|
|
76971
|
+
*/
|
|
76972
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
75419
76973
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
75420
76974
|
lastChangedAt: number()
|
|
75421
76975
|
});
|
|
@@ -75440,7 +76994,7 @@ var switchCapability = {
|
|
|
75440
76994
|
}) },
|
|
75441
76995
|
events: { onStateChanged: { data: object({
|
|
75442
76996
|
deviceId: number(),
|
|
75443
|
-
on: boolean(),
|
|
76997
|
+
on: boolean().nullable(),
|
|
75444
76998
|
lastChangedAt: number()
|
|
75445
76999
|
}) } },
|
|
75446
77000
|
status: {
|
|
@@ -79089,6 +80643,26 @@ function buildDescriptor(capName, kind) {
|
|
|
79089
80643
|
* contributes one entry per bound cap present in this map.
|
|
79090
80644
|
*/
|
|
79091
80645
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
80646
|
+
/**
|
|
80647
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
80648
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
80649
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
80650
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
80651
|
+
* server sends an id, a client maps it to its own icon set.
|
|
80652
|
+
*
|
|
80653
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
80654
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
80655
|
+
* to `icon`.
|
|
80656
|
+
*/
|
|
80657
|
+
function digit(n) {
|
|
80658
|
+
return {
|
|
80659
|
+
label: t(n, n),
|
|
80660
|
+
icon: "hash",
|
|
80661
|
+
legend: n
|
|
80662
|
+
};
|
|
80663
|
+
}
|
|
80664
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
80665
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
79092
80666
|
var btn = (idSuffix, row, col, init) => ({
|
|
79093
80667
|
idSuffix,
|
|
79094
80668
|
row,
|
|
@@ -79215,6 +80789,34 @@ discriminatedUnion("kind", [object({
|
|
|
79215
80789
|
kind: literal("pronto"),
|
|
79216
80790
|
code: string()
|
|
79217
80791
|
})]);
|
|
80792
|
+
/**
|
|
80793
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
80794
|
+
*
|
|
80795
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
80796
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
80797
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
80798
|
+
* count of samples.
|
|
80799
|
+
*
|
|
80800
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
80801
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
80802
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
80803
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
80804
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
80805
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
80806
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
80807
|
+
* covers the longest standby blip above T.
|
|
80808
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
80809
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
80810
|
+
*/
|
|
80811
|
+
var SECOND_MS = 1e3;
|
|
80812
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
80813
|
+
24 * (60 * MINUTE_MS$1);
|
|
80814
|
+
10 * MINUTE_MS$1;
|
|
80815
|
+
30 * SECOND_MS;
|
|
80816
|
+
60 * SECOND_MS;
|
|
80817
|
+
20 * MINUTE_MS$1;
|
|
80818
|
+
10 * SECOND_MS;
|
|
80819
|
+
10 * SECOND_MS;
|
|
79218
80820
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79219
80821
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79220
80822
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -79575,6 +81177,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
79575
81177
|
pressureSensor: pressureSensorCapability,
|
|
79576
81178
|
privacyMask: privacyMaskCapability,
|
|
79577
81179
|
ptzAutotrack: ptzAutotrackCapability,
|
|
81180
|
+
pumpCycle: pumpCycleCapability,
|
|
79578
81181
|
recordingOnboard: recordingOnboardCapability,
|
|
79579
81182
|
recordingSignal: recordingSignalCapability,
|
|
79580
81183
|
runningState: runningStateCapability,
|
|
@@ -81759,6 +83362,27 @@ Object.freeze({
|
|
|
81759
83362
|
access: "view",
|
|
81760
83363
|
role: "viewer"
|
|
81761
83364
|
},
|
|
83365
|
+
"boardKeySource.getBoardKey": {
|
|
83366
|
+
capName: "board-key-source",
|
|
83367
|
+
capScope: "system",
|
|
83368
|
+
addonId: null,
|
|
83369
|
+
access: "view",
|
|
83370
|
+
role: "admin"
|
|
83371
|
+
},
|
|
83372
|
+
"boardKeySource.listBoards": {
|
|
83373
|
+
capName: "board-key-source",
|
|
83374
|
+
capScope: "system",
|
|
83375
|
+
addonId: null,
|
|
83376
|
+
access: "view",
|
|
83377
|
+
role: "admin"
|
|
83378
|
+
},
|
|
83379
|
+
"boardKeySource.listSources": {
|
|
83380
|
+
capName: "board-key-source",
|
|
83381
|
+
capScope: "system",
|
|
83382
|
+
addonId: null,
|
|
83383
|
+
access: "view",
|
|
83384
|
+
role: "admin"
|
|
83385
|
+
},
|
|
81762
83386
|
"brightness.getStatus": {
|
|
81763
83387
|
capName: "brightness",
|
|
81764
83388
|
capScope: "device",
|
|
@@ -82298,6 +83922,13 @@ Object.freeze({
|
|
|
82298
83922
|
access: "create",
|
|
82299
83923
|
role: "admin"
|
|
82300
83924
|
},
|
|
83925
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
83926
|
+
capName: "core-blocks",
|
|
83927
|
+
capScope: "system",
|
|
83928
|
+
addonId: null,
|
|
83929
|
+
access: "view",
|
|
83930
|
+
role: "admin"
|
|
83931
|
+
},
|
|
82301
83932
|
"coreBlocks.reportAutomationRun": {
|
|
82302
83933
|
capName: "core-blocks",
|
|
82303
83934
|
capScope: "system",
|
|
@@ -82809,6 +84440,13 @@ Object.freeze({
|
|
|
82809
84440
|
access: "view",
|
|
82810
84441
|
role: "admin"
|
|
82811
84442
|
},
|
|
84443
|
+
"deviceExport.listInstances": {
|
|
84444
|
+
capName: "device-export",
|
|
84445
|
+
capScope: "system",
|
|
84446
|
+
addonId: null,
|
|
84447
|
+
access: "view",
|
|
84448
|
+
role: "admin"
|
|
84449
|
+
},
|
|
82812
84450
|
"deviceExport.listSupportedDeviceKinds": {
|
|
82813
84451
|
capName: "device-export",
|
|
82814
84452
|
capScope: "system",
|
|
@@ -84440,6 +86078,13 @@ Object.freeze({
|
|
|
84440
86078
|
access: "view",
|
|
84441
86079
|
role: "admin"
|
|
84442
86080
|
},
|
|
86081
|
+
"integrationInstances.setInstanceEnabled": {
|
|
86082
|
+
capName: "integration-instances",
|
|
86083
|
+
capScope: "system",
|
|
86084
|
+
addonId: null,
|
|
86085
|
+
access: "create",
|
|
86086
|
+
role: "admin"
|
|
86087
|
+
},
|
|
84443
86088
|
"integrationInstances.testInstance": {
|
|
84444
86089
|
capName: "integration-instances",
|
|
84445
86090
|
capScope: "system",
|
|
@@ -84454,6 +86099,13 @@ Object.freeze({
|
|
|
84454
86099
|
access: "create",
|
|
84455
86100
|
role: "admin"
|
|
84456
86101
|
},
|
|
86102
|
+
"integrations.clearSettings": {
|
|
86103
|
+
capName: "integrations",
|
|
86104
|
+
capScope: "system",
|
|
86105
|
+
addonId: null,
|
|
86106
|
+
access: "delete",
|
|
86107
|
+
role: "admin"
|
|
86108
|
+
},
|
|
84457
86109
|
"integrations.create": {
|
|
84458
86110
|
capName: "integrations",
|
|
84459
86111
|
capScope: "system",
|
|
@@ -85238,6 +86890,20 @@ Object.freeze({
|
|
|
85238
86890
|
access: "view",
|
|
85239
86891
|
role: "admin"
|
|
85240
86892
|
},
|
|
86893
|
+
"mediaPlayer.channelDown": {
|
|
86894
|
+
capName: "media-player",
|
|
86895
|
+
capScope: "device",
|
|
86896
|
+
addonId: null,
|
|
86897
|
+
access: "create",
|
|
86898
|
+
role: "viewer"
|
|
86899
|
+
},
|
|
86900
|
+
"mediaPlayer.channelUp": {
|
|
86901
|
+
capName: "media-player",
|
|
86902
|
+
capScope: "device",
|
|
86903
|
+
addonId: null,
|
|
86904
|
+
access: "create",
|
|
86905
|
+
role: "viewer"
|
|
86906
|
+
},
|
|
85241
86907
|
"mediaPlayer.getStatus": {
|
|
85242
86908
|
capName: "media-player",
|
|
85243
86909
|
capScope: "device",
|
|
@@ -85329,6 +86995,34 @@ Object.freeze({
|
|
|
85329
86995
|
access: "create",
|
|
85330
86996
|
role: "viewer"
|
|
85331
86997
|
},
|
|
86998
|
+
"mediaPlayer.turnOff": {
|
|
86999
|
+
capName: "media-player",
|
|
87000
|
+
capScope: "device",
|
|
87001
|
+
addonId: null,
|
|
87002
|
+
access: "create",
|
|
87003
|
+
role: "viewer"
|
|
87004
|
+
},
|
|
87005
|
+
"mediaPlayer.turnOn": {
|
|
87006
|
+
capName: "media-player",
|
|
87007
|
+
capScope: "device",
|
|
87008
|
+
addonId: null,
|
|
87009
|
+
access: "create",
|
|
87010
|
+
role: "viewer"
|
|
87011
|
+
},
|
|
87012
|
+
"mediaPlayer.volumeDown": {
|
|
87013
|
+
capName: "media-player",
|
|
87014
|
+
capScope: "device",
|
|
87015
|
+
addonId: null,
|
|
87016
|
+
access: "create",
|
|
87017
|
+
role: "viewer"
|
|
87018
|
+
},
|
|
87019
|
+
"mediaPlayer.volumeUp": {
|
|
87020
|
+
capName: "media-player",
|
|
87021
|
+
capScope: "device",
|
|
87022
|
+
addonId: null,
|
|
87023
|
+
access: "create",
|
|
87024
|
+
role: "viewer"
|
|
87025
|
+
},
|
|
85332
87026
|
"meshNetwork.getStatus": {
|
|
85333
87027
|
capName: "mesh-network",
|
|
85334
87028
|
capScope: "system",
|
|
@@ -86575,27 +88269,6 @@ Object.freeze({
|
|
|
86575
88269
|
access: "create",
|
|
86576
88270
|
role: "viewer"
|
|
86577
88271
|
},
|
|
86578
|
-
"petFountain.setPumpInterval": {
|
|
86579
|
-
capName: "pet-fountain",
|
|
86580
|
-
capScope: "device",
|
|
86581
|
-
addonId: null,
|
|
86582
|
-
access: "create",
|
|
86583
|
-
role: "viewer"
|
|
86584
|
-
},
|
|
86585
|
-
"petFountain.setPumpOffMinutes": {
|
|
86586
|
-
capName: "pet-fountain",
|
|
86587
|
-
capScope: "device",
|
|
86588
|
-
addonId: null,
|
|
86589
|
-
access: "create",
|
|
86590
|
-
role: "viewer"
|
|
86591
|
-
},
|
|
86592
|
-
"petFountain.setPumpOnMinutes": {
|
|
86593
|
-
capName: "pet-fountain",
|
|
86594
|
-
capScope: "device",
|
|
86595
|
-
addonId: null,
|
|
86596
|
-
access: "create",
|
|
86597
|
-
role: "viewer"
|
|
86598
|
-
},
|
|
86599
88272
|
"pipelineAnalytics.addRetrainFrameFromRecording": {
|
|
86600
88273
|
capName: "pipeline-analytics",
|
|
86601
88274
|
capScope: "device",
|
|
@@ -88374,6 +90047,34 @@ Object.freeze({
|
|
|
88374
90047
|
access: "create",
|
|
88375
90048
|
role: "admin"
|
|
88376
90049
|
},
|
|
90050
|
+
"pumpCycle.getStatus": {
|
|
90051
|
+
capName: "pump-cycle",
|
|
90052
|
+
capScope: "device",
|
|
90053
|
+
addonId: null,
|
|
90054
|
+
access: "view",
|
|
90055
|
+
role: "viewer"
|
|
90056
|
+
},
|
|
90057
|
+
"pumpCycle.setEnabled": {
|
|
90058
|
+
capName: "pump-cycle",
|
|
90059
|
+
capScope: "device",
|
|
90060
|
+
addonId: null,
|
|
90061
|
+
access: "create",
|
|
90062
|
+
role: "viewer"
|
|
90063
|
+
},
|
|
90064
|
+
"pumpCycle.setOffMinutes": {
|
|
90065
|
+
capName: "pump-cycle",
|
|
90066
|
+
capScope: "device",
|
|
90067
|
+
addonId: null,
|
|
90068
|
+
access: "create",
|
|
90069
|
+
role: "viewer"
|
|
90070
|
+
},
|
|
90071
|
+
"pumpCycle.setOnMinutes": {
|
|
90072
|
+
capName: "pump-cycle",
|
|
90073
|
+
capScope: "device",
|
|
90074
|
+
addonId: null,
|
|
90075
|
+
access: "create",
|
|
90076
|
+
role: "viewer"
|
|
90077
|
+
},
|
|
88377
90078
|
"reboot.reboot": {
|
|
88378
90079
|
capName: "reboot",
|
|
88379
90080
|
capScope: "device",
|
|
@@ -88878,6 +90579,13 @@ Object.freeze({
|
|
|
88878
90579
|
access: "create",
|
|
88879
90580
|
role: "admin"
|
|
88880
90581
|
},
|
|
90582
|
+
"remote.getAssumedStatus": {
|
|
90583
|
+
capName: "remote",
|
|
90584
|
+
capScope: "system",
|
|
90585
|
+
addonId: null,
|
|
90586
|
+
access: "view",
|
|
90587
|
+
role: "admin"
|
|
90588
|
+
},
|
|
88881
90589
|
"remote.getConfig": {
|
|
88882
90590
|
capName: "remote",
|
|
88883
90591
|
capScope: "system",
|
|
@@ -91693,6 +93401,11 @@ Object.freeze({
|
|
|
91693
93401
|
form: "single",
|
|
91694
93402
|
optional: false
|
|
91695
93403
|
}],
|
|
93404
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
93405
|
+
name: "deviceId",
|
|
93406
|
+
form: "single",
|
|
93407
|
+
optional: false
|
|
93408
|
+
}],
|
|
91696
93409
|
"cover.close": [{
|
|
91697
93410
|
name: "deviceId",
|
|
91698
93411
|
form: "single",
|
|
@@ -92544,6 +94257,16 @@ Object.freeze({
|
|
|
92544
94257
|
form: "single",
|
|
92545
94258
|
optional: true
|
|
92546
94259
|
}],
|
|
94260
|
+
"mediaPlayer.channelDown": [{
|
|
94261
|
+
name: "deviceId",
|
|
94262
|
+
form: "single",
|
|
94263
|
+
optional: false
|
|
94264
|
+
}],
|
|
94265
|
+
"mediaPlayer.channelUp": [{
|
|
94266
|
+
name: "deviceId",
|
|
94267
|
+
form: "single",
|
|
94268
|
+
optional: false
|
|
94269
|
+
}],
|
|
92547
94270
|
"mediaPlayer.getStatus": [{
|
|
92548
94271
|
name: "deviceId",
|
|
92549
94272
|
form: "single",
|
|
@@ -92609,6 +94332,26 @@ Object.freeze({
|
|
|
92609
94332
|
form: "single",
|
|
92610
94333
|
optional: false
|
|
92611
94334
|
}],
|
|
94335
|
+
"mediaPlayer.turnOff": [{
|
|
94336
|
+
name: "deviceId",
|
|
94337
|
+
form: "single",
|
|
94338
|
+
optional: false
|
|
94339
|
+
}],
|
|
94340
|
+
"mediaPlayer.turnOn": [{
|
|
94341
|
+
name: "deviceId",
|
|
94342
|
+
form: "single",
|
|
94343
|
+
optional: false
|
|
94344
|
+
}],
|
|
94345
|
+
"mediaPlayer.volumeDown": [{
|
|
94346
|
+
name: "deviceId",
|
|
94347
|
+
form: "single",
|
|
94348
|
+
optional: false
|
|
94349
|
+
}],
|
|
94350
|
+
"mediaPlayer.volumeUp": [{
|
|
94351
|
+
name: "deviceId",
|
|
94352
|
+
form: "single",
|
|
94353
|
+
optional: false
|
|
94354
|
+
}],
|
|
92612
94355
|
"meter.getStatus": [{
|
|
92613
94356
|
name: "deviceId",
|
|
92614
94357
|
form: "single",
|
|
@@ -92918,21 +94661,6 @@ Object.freeze({
|
|
|
92918
94661
|
form: "single",
|
|
92919
94662
|
optional: false
|
|
92920
94663
|
}],
|
|
92921
|
-
"petFountain.setPumpInterval": [{
|
|
92922
|
-
name: "deviceId",
|
|
92923
|
-
form: "single",
|
|
92924
|
-
optional: false
|
|
92925
|
-
}],
|
|
92926
|
-
"petFountain.setPumpOffMinutes": [{
|
|
92927
|
-
name: "deviceId",
|
|
92928
|
-
form: "single",
|
|
92929
|
-
optional: false
|
|
92930
|
-
}],
|
|
92931
|
-
"petFountain.setPumpOnMinutes": [{
|
|
92932
|
-
name: "deviceId",
|
|
92933
|
-
form: "single",
|
|
92934
|
-
optional: false
|
|
92935
|
-
}],
|
|
92936
94664
|
"pipelineAnalytics.addRetrainFrameFromRecording": [{
|
|
92937
94665
|
name: "deviceId",
|
|
92938
94666
|
form: "single",
|
|
@@ -93568,6 +95296,26 @@ Object.freeze({
|
|
|
93568
95296
|
form: "single",
|
|
93569
95297
|
optional: false
|
|
93570
95298
|
}],
|
|
95299
|
+
"pumpCycle.getStatus": [{
|
|
95300
|
+
name: "deviceId",
|
|
95301
|
+
form: "single",
|
|
95302
|
+
optional: false
|
|
95303
|
+
}],
|
|
95304
|
+
"pumpCycle.setEnabled": [{
|
|
95305
|
+
name: "deviceId",
|
|
95306
|
+
form: "single",
|
|
95307
|
+
optional: false
|
|
95308
|
+
}],
|
|
95309
|
+
"pumpCycle.setOffMinutes": [{
|
|
95310
|
+
name: "deviceId",
|
|
95311
|
+
form: "single",
|
|
95312
|
+
optional: false
|
|
95313
|
+
}],
|
|
95314
|
+
"pumpCycle.setOnMinutes": [{
|
|
95315
|
+
name: "deviceId",
|
|
95316
|
+
form: "single",
|
|
95317
|
+
optional: false
|
|
95318
|
+
}],
|
|
93571
95319
|
"reboot.reboot": [{
|
|
93572
95320
|
name: "deviceId",
|
|
93573
95321
|
form: "single",
|
|
@@ -93769,6 +95517,11 @@ Object.freeze({
|
|
|
93769
95517
|
form: "single",
|
|
93770
95518
|
optional: true
|
|
93771
95519
|
}],
|
|
95520
|
+
"remote.getAssumedStatus": [{
|
|
95521
|
+
name: "deviceId",
|
|
95522
|
+
form: "single",
|
|
95523
|
+
optional: false
|
|
95524
|
+
}],
|
|
93772
95525
|
"remote.getConfig": [{
|
|
93773
95526
|
name: "deviceId",
|
|
93774
95527
|
form: "single",
|
|
@@ -94316,99 +96069,6 @@ Object.freeze({
|
|
|
94316
96069
|
"network-access": "ingress",
|
|
94317
96070
|
"smtp-provider": "email"
|
|
94318
96071
|
});
|
|
94319
|
-
/**
|
|
94320
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
94321
|
-
* `z.infer`, they do not restate the shape.
|
|
94322
|
-
*/
|
|
94323
|
-
var OverrideSourceSchema = object({
|
|
94324
|
-
addonId: string().min(1),
|
|
94325
|
-
stableId: string().min(1),
|
|
94326
|
-
cap: string().min(1),
|
|
94327
|
-
fieldPath: string().min(1)
|
|
94328
|
-
});
|
|
94329
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
94330
|
-
object({ kind: literal("identity") }),
|
|
94331
|
-
object({ kind: literal("invert") }),
|
|
94332
|
-
object({
|
|
94333
|
-
kind: literal("threshold"),
|
|
94334
|
-
comparison: _enum(["gte", "lte"]),
|
|
94335
|
-
threshold: number(),
|
|
94336
|
-
startDwellMs: number().nonnegative(),
|
|
94337
|
-
stopDwellMs: number().nonnegative()
|
|
94338
|
-
}),
|
|
94339
|
-
object({
|
|
94340
|
-
kind: literal("linear"),
|
|
94341
|
-
scale: number(),
|
|
94342
|
-
offset: number(),
|
|
94343
|
-
clampMin: number().optional(),
|
|
94344
|
-
clampMax: number().optional()
|
|
94345
|
-
}),
|
|
94346
|
-
object({
|
|
94347
|
-
kind: literal("enum-map"),
|
|
94348
|
-
mapping: record(string(), string().nullable())
|
|
94349
|
-
}),
|
|
94350
|
-
object({ kind: literal("number") })
|
|
94351
|
-
]);
|
|
94352
|
-
var ScalarOverrideDraftSchema = object({
|
|
94353
|
-
kind: literal("scalar"),
|
|
94354
|
-
cap: string().min(1),
|
|
94355
|
-
targetId: string().min(1),
|
|
94356
|
-
source: OverrideSourceSchema,
|
|
94357
|
-
transform: CustomizeTransformDraftSchema
|
|
94358
|
-
});
|
|
94359
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
94360
|
-
kind: literal("keyedArray"),
|
|
94361
|
-
cap: string().min(1),
|
|
94362
|
-
targetId: string().min(1),
|
|
94363
|
-
key: string().min(1),
|
|
94364
|
-
label: string().min(1),
|
|
94365
|
-
fieldPath: string().min(1),
|
|
94366
|
-
source: OverrideSourceSchema,
|
|
94367
|
-
transform: CustomizeTransformDraftSchema
|
|
94368
|
-
});
|
|
94369
|
-
var PulseOverrideDraftSchema = object({
|
|
94370
|
-
kind: literal("pulse"),
|
|
94371
|
-
cap: string().min(1),
|
|
94372
|
-
targetId: string().min(1),
|
|
94373
|
-
enabled: boolean(),
|
|
94374
|
-
debounceMs: number().nonnegative(),
|
|
94375
|
-
edges: array(_enum([
|
|
94376
|
-
"rising",
|
|
94377
|
-
"button",
|
|
94378
|
-
"native-event"
|
|
94379
|
-
])).min(1).optional(),
|
|
94380
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
94381
|
-
});
|
|
94382
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
94383
|
-
"in-addition",
|
|
94384
|
-
"confirm",
|
|
94385
|
-
"replace"
|
|
94386
|
-
]).optional() });
|
|
94387
|
-
discriminatedUnion("kind", [
|
|
94388
|
-
ScalarOverrideDraftSchema,
|
|
94389
|
-
KeyedArrayOverrideDraftSchema,
|
|
94390
|
-
PulseOverrideDraftSchema,
|
|
94391
|
-
object({
|
|
94392
|
-
kind: literal("span"),
|
|
94393
|
-
cap: string().min(1),
|
|
94394
|
-
targetId: string().min(1),
|
|
94395
|
-
timeoutMs: number().nonnegative(),
|
|
94396
|
-
cooldownMs: number().nonnegative().optional(),
|
|
94397
|
-
sources: array(SpanSourceDraftSchema)
|
|
94398
|
-
})
|
|
94399
|
-
]);
|
|
94400
|
-
object({
|
|
94401
|
-
targetId: string().min(1),
|
|
94402
|
-
configured: boolean(),
|
|
94403
|
-
source: _enum(["native", "assigned"]),
|
|
94404
|
-
state: _enum([
|
|
94405
|
-
"active",
|
|
94406
|
-
"stale",
|
|
94407
|
-
"released",
|
|
94408
|
-
"unavailable"
|
|
94409
|
-
]),
|
|
94410
|
-
reason: string().nullable()
|
|
94411
|
-
});
|
|
94412
96072
|
var G711_SCALE_CORRECTION_DB = {
|
|
94413
96073
|
PCMU: 20 * Math.log10(4),
|
|
94414
96074
|
PCMA: 20 * Math.log10(8)
|
|
@@ -95024,6 +96684,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
95024
96684
|
[...RULE_TEXT],
|
|
95025
96685
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
95026
96686
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
96687
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
95027
96688
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
95028
96689
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
95029
96690
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -95912,6 +97573,20 @@ var VeluxReconciler = class {
|
|
|
95912
97573
|
}
|
|
95913
97574
|
};
|
|
95914
97575
|
//#endregion
|
|
97576
|
+
//#region src/custom-actions.ts
|
|
97577
|
+
/** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
|
|
97578
|
+
var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
|
|
97579
|
+
hosts: array(object({
|
|
97580
|
+
host: string(),
|
|
97581
|
+
name: string(),
|
|
97582
|
+
id: string()
|
|
97583
|
+
})),
|
|
97584
|
+
summary: string()
|
|
97585
|
+
}), {
|
|
97586
|
+
kind: "mutation",
|
|
97587
|
+
auth: "admin"
|
|
97588
|
+
}) });
|
|
97589
|
+
//#endregion
|
|
95915
97590
|
//#region src/config.ts
|
|
95916
97591
|
/**
|
|
95917
97592
|
* Operator-supplied connection to ONE Velux KLF-200 gateway on the local
|
|
@@ -96035,20 +97710,6 @@ function buildConnectionFormSchema() {
|
|
|
96035
97710
|
}] };
|
|
96036
97711
|
}
|
|
96037
97712
|
//#endregion
|
|
96038
|
-
//#region src/custom-actions.ts
|
|
96039
|
-
/** Custom actions of the Velux addon: the LAN scan behind the integration form's gateway picker. */
|
|
96040
|
-
var veluxActions = defineCustomActions({ discoverGateway: customAction(object({}), object({
|
|
96041
|
-
hosts: array(object({
|
|
96042
|
-
host: string(),
|
|
96043
|
-
name: string(),
|
|
96044
|
-
id: string()
|
|
96045
|
-
})),
|
|
96046
|
-
summary: string()
|
|
96047
|
-
}), {
|
|
96048
|
-
kind: "mutation",
|
|
96049
|
-
auth: "admin"
|
|
96050
|
-
}) });
|
|
96051
|
-
//#endregion
|
|
96052
97713
|
//#region src/velux-node-resolver.ts
|
|
96053
97714
|
/**
|
|
96054
97715
|
* Per-integration registry that device classes use to reach the live
|
|
@@ -96644,6 +98305,7 @@ var VeluxWindowDevice = class extends BaseDevice {
|
|
|
96644
98305
|
stableIdSuffix: "rain",
|
|
96645
98306
|
meta: {
|
|
96646
98307
|
type: DeviceType.Sensor,
|
|
98308
|
+
role: DeviceRole.LeakSensor,
|
|
96647
98309
|
name: `${this.name} rain`,
|
|
96648
98310
|
...link
|
|
96649
98311
|
},
|
|
@@ -105369,7 +107031,54 @@ function veluxDiscoverySummary(foundCount, proposedCount) {
|
|
|
105369
107031
|
return `Found ${proposedCount} ${proposedCount === 1 ? "gateway" : "gateways"}.`;
|
|
105370
107032
|
}
|
|
105371
107033
|
//#endregion
|
|
107034
|
+
//#region src/velux-instances.ts
|
|
107035
|
+
/**
|
|
107036
|
+
* Velux integrations as `integration-instances` (planning#416, D1007): one `connections` record
|
|
107037
|
+
* per integration under its registry id, holding the connection to ONE KLF-200 gateway. The
|
|
107038
|
+
* gateway password is declared a secret by type; the records door keeps it in the keychain.
|
|
107039
|
+
*/
|
|
107040
|
+
var veluxConnectionsEntryKind = defineEntryKind("connections", {
|
|
107041
|
+
title: "Velux gateways",
|
|
107042
|
+
schema: entryBody({
|
|
107043
|
+
host: string().optional(),
|
|
107044
|
+
password: string().optional().meta({ secret: true })
|
|
107045
|
+
}),
|
|
107046
|
+
writeTrigger: "an operator adds, edits or removes a Velux KLF-200 gateway integration"
|
|
107047
|
+
});
|
|
107048
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
107049
|
+
var veluxRecordKinds = { connections: veluxConnectionsEntryKind };
|
|
107050
|
+
function buildVeluxInstances(records, deps) {
|
|
107051
|
+
return new RecordIntegrationInstances(records, veluxConnectionsEntryKind, {
|
|
107052
|
+
description: {
|
|
107053
|
+
role: "devices",
|
|
107054
|
+
discovery: {
|
|
107055
|
+
mode: "account",
|
|
107056
|
+
scopedToInstance: true
|
|
107057
|
+
},
|
|
107058
|
+
instanceMode: "multiple",
|
|
107059
|
+
kind: "server",
|
|
107060
|
+
canTest: false
|
|
107061
|
+
},
|
|
107062
|
+
formSchema: () => buildConnectionFormSchema(),
|
|
107063
|
+
initialBody: (name) => ({ name }),
|
|
107064
|
+
configured: (body) => (body.host ?? "").length > 0 && body.password !== void 0,
|
|
107065
|
+
onChanged: () => deps.onChanged()
|
|
107066
|
+
});
|
|
107067
|
+
}
|
|
107068
|
+
/**
|
|
107069
|
+
* The gateway connection of one integration as the addon's runtime reads it: from the record,
|
|
107070
|
+
* never from the legacy rows. Null while the record is absent, incomplete, or its password is
|
|
107071
|
+
* unreadable (the runtime keeps what it had, D49).
|
|
107072
|
+
*/
|
|
107073
|
+
async function veluxConnectionOf(instances, id) {
|
|
107074
|
+
const read = await instances.readResolved(id);
|
|
107075
|
+
if (read === null || read.secrets === "unavailable") return null;
|
|
107076
|
+
return connectionFromSettings(read.body);
|
|
107077
|
+
}
|
|
107078
|
+
//#endregion
|
|
105372
107079
|
//#region src/addon.ts
|
|
107080
|
+
/** The date of the integration-rows-to-record migration (D1007). */
|
|
107081
|
+
var VELUX_MIGRATIONS_DATE = "2026-11-05";
|
|
105373
107082
|
/**
|
|
105374
107083
|
* Velux KLF-200 — `mode: account`, one integration per gateway.
|
|
105375
107084
|
*
|
|
@@ -105392,11 +107101,27 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105392
107101
|
};
|
|
105393
107102
|
integrationReconciler = null;
|
|
105394
107103
|
clients = null;
|
|
107104
|
+
instances = null;
|
|
105395
107105
|
constructor() {
|
|
105396
107106
|
super({});
|
|
105397
107107
|
}
|
|
105398
107108
|
async onInitialize() {
|
|
105399
107109
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
107110
|
+
const records = this.ctx.records;
|
|
107111
|
+
if (records === void 0) throw new Error("provider-velux: ctx.records is required (hub-only addon)");
|
|
107112
|
+
await records.declare(veluxRecordKinds);
|
|
107113
|
+
await this.runMigrations(defineAddonMigrations([integrationRowsMigration({
|
|
107114
|
+
addonId: this.addonId,
|
|
107115
|
+
date: VELUX_MIGRATIONS_DATE,
|
|
107116
|
+
kind: veluxConnectionsEntryKind,
|
|
107117
|
+
fixtureMark: "host"
|
|
107118
|
+
})], integrationRowsPortsOver({
|
|
107119
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
107120
|
+
records,
|
|
107121
|
+
kind: veluxConnectionsEntryKind,
|
|
107122
|
+
warn: (message) => this.ctx.logger.warn(message)
|
|
107123
|
+
})));
|
|
107124
|
+
this.instances = buildVeluxInstances(records, { onChanged: () => this.reconcileIntegrations() });
|
|
105400
107125
|
setKlfLibraryLogSink((level, message) => {
|
|
105401
107126
|
this.ctx.logger.debug(`klf-200-api ${level}: ${message}`);
|
|
105402
107127
|
});
|
|
@@ -105422,6 +107147,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105422
107147
|
{
|
|
105423
107148
|
capability: connectionTestCapability,
|
|
105424
107149
|
provider: this.buildConnectionTestProvider()
|
|
107150
|
+
},
|
|
107151
|
+
{
|
|
107152
|
+
capability: integrationInstancesCapability,
|
|
107153
|
+
provider: this.requireInstances().toProvider()
|
|
105425
107154
|
}
|
|
105426
107155
|
],
|
|
105427
107156
|
customActions: veluxActions,
|
|
@@ -105456,9 +107185,6 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105456
107185
|
setKlfLibraryLogSink(null);
|
|
105457
107186
|
await super.onShutdown();
|
|
105458
107187
|
}
|
|
105459
|
-
globalSettingsSchema() {
|
|
105460
|
-
return buildConnectionFormSchema();
|
|
105461
|
-
}
|
|
105462
107188
|
async supportsManualCreation() {
|
|
105463
107189
|
return false;
|
|
105464
107190
|
}
|
|
@@ -105500,7 +107226,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105500
107226
|
for (const integration of all) {
|
|
105501
107227
|
if (integration.addonId !== this.ctx.id) continue;
|
|
105502
107228
|
try {
|
|
105503
|
-
const connection =
|
|
107229
|
+
const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
|
|
105504
107230
|
if (connection) hosts.push(connection.host);
|
|
105505
107231
|
} catch (err) {
|
|
105506
107232
|
this.ctx.logger.warn("Velux discovery: settings read failed", {
|
|
@@ -105515,6 +107241,10 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105515
107241
|
return [];
|
|
105516
107242
|
}
|
|
105517
107243
|
}
|
|
107244
|
+
requireInstances() {
|
|
107245
|
+
if (!this.instances) throw new Error("Velux provider not initialised");
|
|
107246
|
+
return this.instances;
|
|
107247
|
+
}
|
|
105518
107248
|
requireClients() {
|
|
105519
107249
|
if (!this.clients) throw new Error("Velux provider not initialised");
|
|
105520
107250
|
return this.clients;
|
|
@@ -105531,7 +107261,7 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
105531
107261
|
},
|
|
105532
107262
|
listIntegrations: () => this.ctx.api.integrations.list.query(),
|
|
105533
107263
|
applySettings: async (integration) => {
|
|
105534
|
-
const connection =
|
|
107264
|
+
const connection = await veluxConnectionOf(this.requireInstances(), integration.id);
|
|
105535
107265
|
if (!connection) return false;
|
|
105536
107266
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
105537
107267
|
return true;
|