@camstack/addon-provider-rademacher 0.2.157 → 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 +6 -0
- package/dist/addon.js +2397 -654
- package/dist/addon.mjs +2397 -654
- package/package.json +6 -2
package/dist/addon.js
CHANGED
|
@@ -788,7 +788,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
788
788
|
return EventCategory;
|
|
789
789
|
}({});
|
|
790
790
|
//#endregion
|
|
791
|
-
//#region ../types/dist/device-type-
|
|
791
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
792
792
|
/**
|
|
793
793
|
import { errMsg } from '@camstack/types'
|
|
794
794
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -1019,6 +1019,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
1019
1019
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
1020
1020
|
/** Media player exposes stop distinct from pause. */
|
|
1021
1021
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
1022
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
1023
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
1024
|
+
/** Media player exposes channel up / down in steps. */
|
|
1025
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
1026
|
+
/** Media player exposes power on / off. */
|
|
1027
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
1022
1028
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
1023
1029
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
1024
1030
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -4555,6 +4561,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4555
4561
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4556
4562
|
json.type = "boolean";
|
|
4557
4563
|
};
|
|
4564
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4565
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4566
|
+
};
|
|
4567
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
4568
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
4569
|
+
};
|
|
4558
4570
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4559
4571
|
if (ctx.target === "openapi-3.0") {
|
|
4560
4572
|
json.type = "string";
|
|
@@ -4562,12 +4574,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
4562
4574
|
json.enum = [null];
|
|
4563
4575
|
} else json.type = "null";
|
|
4564
4576
|
};
|
|
4577
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
4578
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
4579
|
+
};
|
|
4565
4580
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
4566
4581
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
4567
4582
|
};
|
|
4568
4583
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4569
4584
|
json.not = {};
|
|
4570
4585
|
};
|
|
4586
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4587
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4571
4588
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4572
4589
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
4573
4590
|
};
|
|
@@ -4599,12 +4616,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
4599
4616
|
json.enum = vals;
|
|
4600
4617
|
}
|
|
4601
4618
|
};
|
|
4619
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
4620
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
4621
|
+
};
|
|
4622
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
4623
|
+
const _json = json;
|
|
4624
|
+
const pattern = schema._zod.pattern;
|
|
4625
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
4626
|
+
_json.type = "string";
|
|
4627
|
+
_json.pattern = pattern.source;
|
|
4628
|
+
};
|
|
4629
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
4630
|
+
const _json = json;
|
|
4631
|
+
const file = {
|
|
4632
|
+
type: "string",
|
|
4633
|
+
format: "binary",
|
|
4634
|
+
contentEncoding: "binary"
|
|
4635
|
+
};
|
|
4636
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
4637
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
4638
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
4639
|
+
if (mime) if (mime.length === 1) {
|
|
4640
|
+
file.contentMediaType = mime[0];
|
|
4641
|
+
Object.assign(_json, file);
|
|
4642
|
+
} else {
|
|
4643
|
+
Object.assign(_json, file);
|
|
4644
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
4645
|
+
}
|
|
4646
|
+
else Object.assign(_json, file);
|
|
4647
|
+
};
|
|
4648
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
4649
|
+
json.type = "boolean";
|
|
4650
|
+
};
|
|
4602
4651
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
4603
4652
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
4604
4653
|
};
|
|
4654
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
4655
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
4656
|
+
};
|
|
4605
4657
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
4606
4658
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
4607
4659
|
};
|
|
4660
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
4661
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
4662
|
+
};
|
|
4663
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
4664
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
4665
|
+
};
|
|
4608
4666
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
4609
4667
|
const json = _json;
|
|
4610
4668
|
const def = schema._zod.def;
|
|
@@ -4809,6 +4867,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
4809
4867
|
seen.ref = def.innerType;
|
|
4810
4868
|
json.readOnly = true;
|
|
4811
4869
|
};
|
|
4870
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
4871
|
+
const def = schema._zod.def;
|
|
4872
|
+
process(def.innerType, ctx, params);
|
|
4873
|
+
const seen = ctx.seen.get(schema);
|
|
4874
|
+
seen.ref = def.innerType;
|
|
4875
|
+
};
|
|
4812
4876
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
4813
4877
|
const def = schema._zod.def;
|
|
4814
4878
|
process(def.innerType, ctx, params);
|
|
@@ -4821,6 +4885,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
4821
4885
|
const seen = ctx.seen.get(schema);
|
|
4822
4886
|
seen.ref = innerType;
|
|
4823
4887
|
};
|
|
4888
|
+
var allProcessors = {
|
|
4889
|
+
string: stringProcessor,
|
|
4890
|
+
number: numberProcessor,
|
|
4891
|
+
boolean: booleanProcessor,
|
|
4892
|
+
bigint: bigintProcessor,
|
|
4893
|
+
symbol: symbolProcessor,
|
|
4894
|
+
null: nullProcessor,
|
|
4895
|
+
undefined: undefinedProcessor,
|
|
4896
|
+
void: voidProcessor,
|
|
4897
|
+
never: neverProcessor,
|
|
4898
|
+
any: anyProcessor,
|
|
4899
|
+
unknown: unknownProcessor,
|
|
4900
|
+
date: dateProcessor,
|
|
4901
|
+
enum: enumProcessor,
|
|
4902
|
+
literal: literalProcessor,
|
|
4903
|
+
nan: nanProcessor,
|
|
4904
|
+
template_literal: templateLiteralProcessor,
|
|
4905
|
+
file: fileProcessor,
|
|
4906
|
+
success: successProcessor,
|
|
4907
|
+
custom: customProcessor,
|
|
4908
|
+
function: functionProcessor,
|
|
4909
|
+
transform: transformProcessor,
|
|
4910
|
+
map: mapProcessor,
|
|
4911
|
+
set: setProcessor,
|
|
4912
|
+
array: arrayProcessor,
|
|
4913
|
+
object: objectProcessor,
|
|
4914
|
+
union: unionProcessor,
|
|
4915
|
+
intersection: intersectionProcessor,
|
|
4916
|
+
tuple: tupleProcessor,
|
|
4917
|
+
record: recordProcessor,
|
|
4918
|
+
nullable: nullableProcessor,
|
|
4919
|
+
nonoptional: nonoptionalProcessor,
|
|
4920
|
+
default: defaultProcessor,
|
|
4921
|
+
prefault: prefaultProcessor,
|
|
4922
|
+
catch: catchProcessor,
|
|
4923
|
+
pipe: pipeProcessor,
|
|
4924
|
+
readonly: readonlyProcessor,
|
|
4925
|
+
promise: promiseProcessor,
|
|
4926
|
+
optional: optionalProcessor,
|
|
4927
|
+
lazy: lazyProcessor
|
|
4928
|
+
};
|
|
4929
|
+
function toJSONSchema(input, params) {
|
|
4930
|
+
if ("_idmap" in input) {
|
|
4931
|
+
const registry = input;
|
|
4932
|
+
const ctx = initializeContext({
|
|
4933
|
+
...params,
|
|
4934
|
+
processors: allProcessors
|
|
4935
|
+
});
|
|
4936
|
+
const defs = {};
|
|
4937
|
+
for (const entry of registry._idmap.entries()) {
|
|
4938
|
+
const [_, schema] = entry;
|
|
4939
|
+
process(schema, ctx);
|
|
4940
|
+
}
|
|
4941
|
+
const schemas = {};
|
|
4942
|
+
ctx.external = {
|
|
4943
|
+
registry,
|
|
4944
|
+
uri: params?.uri,
|
|
4945
|
+
defs
|
|
4946
|
+
};
|
|
4947
|
+
for (const entry of registry._idmap.entries()) {
|
|
4948
|
+
const [key, schema] = entry;
|
|
4949
|
+
extractDefs(ctx, schema);
|
|
4950
|
+
schemas[key] = finalize(ctx, schema);
|
|
4951
|
+
}
|
|
4952
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
4953
|
+
return { schemas };
|
|
4954
|
+
}
|
|
4955
|
+
const ctx = initializeContext({
|
|
4956
|
+
...params,
|
|
4957
|
+
processors: allProcessors
|
|
4958
|
+
});
|
|
4959
|
+
process(input, ctx);
|
|
4960
|
+
extractDefs(ctx, input);
|
|
4961
|
+
return finalize(ctx, input);
|
|
4962
|
+
}
|
|
4824
4963
|
//#endregion
|
|
4825
4964
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
4826
4965
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -5845,7 +5984,7 @@ var ZodIssueCode = {
|
|
|
5845
5984
|
var ZodFirstPartyTypeKind;
|
|
5846
5985
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5847
5986
|
//#endregion
|
|
5848
|
-
//#region ../types/dist/sleep-
|
|
5987
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5849
5988
|
/**
|
|
5850
5989
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5851
5990
|
* window to float samples (D455).
|
|
@@ -5924,6 +6063,22 @@ function buildAlawTable() {
|
|
|
5924
6063
|
}
|
|
5925
6064
|
buildUlawTable();
|
|
5926
6065
|
buildAlawTable();
|
|
6066
|
+
/**
|
|
6067
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
6068
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
6069
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
6070
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
6071
|
+
*/
|
|
6072
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
6073
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
6074
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
6075
|
+
migrationIds;
|
|
6076
|
+
constructor(migrationIds) {
|
|
6077
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
6078
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
6079
|
+
this.migrationIds = migrationIds;
|
|
6080
|
+
}
|
|
6081
|
+
};
|
|
5927
6082
|
/** Is this field's stored value a credential? */
|
|
5928
6083
|
function isSecretConfigField(field) {
|
|
5929
6084
|
if (field.type === "password") return true;
|
|
@@ -5977,9 +6132,16 @@ function isSecretMarker(value) {
|
|
|
5977
6132
|
const ref = value["$secret"];
|
|
5978
6133
|
return typeof ref === "string" && ref.length > 0;
|
|
5979
6134
|
}
|
|
6135
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6136
|
+
function isSecretMarkerLike(value) {
|
|
6137
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6138
|
+
}
|
|
5980
6139
|
function isSecretClear(value) {
|
|
5981
6140
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
5982
6141
|
}
|
|
6142
|
+
function isSecretFieldView(value) {
|
|
6143
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6144
|
+
}
|
|
5983
6145
|
/**
|
|
5984
6146
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
5985
6147
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6361,6 +6523,53 @@ function emitReadiness(bus, params) {
|
|
|
6361
6523
|
ts
|
|
6362
6524
|
}));
|
|
6363
6525
|
}
|
|
6526
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
6527
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
6528
|
+
id;
|
|
6529
|
+
constructor(id, message) {
|
|
6530
|
+
super(`${id}: ${message}`);
|
|
6531
|
+
this.id = id;
|
|
6532
|
+
this.name = "AddonMigrationDefinitionError";
|
|
6533
|
+
}
|
|
6534
|
+
};
|
|
6535
|
+
/**
|
|
6536
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
6537
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
6538
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
6539
|
+
*/
|
|
6540
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
6541
|
+
return catalogue.map((entry) => {
|
|
6542
|
+
const { meta } = entry;
|
|
6543
|
+
if ("plan" in entry) {
|
|
6544
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
6545
|
+
return {
|
|
6546
|
+
meta,
|
|
6547
|
+
execute: () => guarded(async () => ({
|
|
6548
|
+
kind: "plan",
|
|
6549
|
+
plan: await entry.plan(ports)
|
|
6550
|
+
}))
|
|
6551
|
+
};
|
|
6552
|
+
}
|
|
6553
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
6554
|
+
return {
|
|
6555
|
+
meta,
|
|
6556
|
+
execute: () => guarded(async () => ({
|
|
6557
|
+
kind: "procedure",
|
|
6558
|
+
result: await entry.procedure(ports)
|
|
6559
|
+
}))
|
|
6560
|
+
};
|
|
6561
|
+
});
|
|
6562
|
+
}
|
|
6563
|
+
async function guarded(work) {
|
|
6564
|
+
try {
|
|
6565
|
+
return await work();
|
|
6566
|
+
} catch (err) {
|
|
6567
|
+
return {
|
|
6568
|
+
kind: "failed",
|
|
6569
|
+
error: errMsg(err)
|
|
6570
|
+
};
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6364
6573
|
var NOT_RUN = {
|
|
6365
6574
|
declared: false,
|
|
6366
6575
|
snapshot: null,
|
|
@@ -6375,7 +6584,7 @@ var NOT_RUN = {
|
|
|
6375
6584
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6376
6585
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6377
6586
|
*/
|
|
6378
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6587
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6379
6588
|
const first = migrations[0];
|
|
6380
6589
|
if (first === void 0) return NOT_RUN;
|
|
6381
6590
|
const owner = first.meta.owner;
|
|
@@ -6404,7 +6613,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6404
6613
|
else if (status === "blocked") blocked.push(id);
|
|
6405
6614
|
else skipped.push(id);
|
|
6406
6615
|
};
|
|
6616
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6407
6617
|
for (const step of declaration.steps) {
|
|
6618
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6408
6619
|
const bound = byId.get(step.id);
|
|
6409
6620
|
if (bound === void 0 || step.action === "skip") {
|
|
6410
6621
|
skipped.push(step.id);
|
|
@@ -6582,11 +6793,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6582
6793
|
}
|
|
6583
6794
|
return out;
|
|
6584
6795
|
}
|
|
6796
|
+
/**
|
|
6797
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6798
|
+
*
|
|
6799
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6800
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6801
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6802
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6803
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6804
|
+
* copied onto another ref does not decrypt.
|
|
6805
|
+
*
|
|
6806
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6807
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6808
|
+
*
|
|
6809
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6810
|
+
*/
|
|
6811
|
+
/** The URI scheme of every keychain ref. */
|
|
6812
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6585
6813
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6586
6814
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6587
6815
|
function globalSecretScope() {
|
|
6588
6816
|
return GLOBAL_SCOPE;
|
|
6589
6817
|
}
|
|
6818
|
+
/**
|
|
6819
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6820
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6821
|
+
* addon running forked.
|
|
6822
|
+
*/
|
|
6823
|
+
function normalizeSecretOwner(addonId) {
|
|
6824
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6825
|
+
}
|
|
6590
6826
|
function encodeSegment(value) {
|
|
6591
6827
|
return encodeURIComponent(value);
|
|
6592
6828
|
}
|
|
@@ -6597,6 +6833,16 @@ function decodeSegment(value) {
|
|
|
6597
6833
|
return null;
|
|
6598
6834
|
}
|
|
6599
6835
|
}
|
|
6836
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6837
|
+
function formatSecretScope(scope) {
|
|
6838
|
+
switch (scope.kind) {
|
|
6839
|
+
case "global": return "global";
|
|
6840
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6841
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6842
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6843
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6600
6846
|
/**
|
|
6601
6847
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6602
6848
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6605,6 +6851,13 @@ function decodeSegment(value) {
|
|
|
6605
6851
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6606
6852
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6607
6853
|
}
|
|
6854
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6855
|
+
function buildSecretRef(parts) {
|
|
6856
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6857
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6858
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6859
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6860
|
+
}
|
|
6608
6861
|
function parseScope(segments) {
|
|
6609
6862
|
switch (segments[0]) {
|
|
6610
6863
|
case "global": return {
|
|
@@ -7522,15 +7775,19 @@ var BaseAddon = class {
|
|
|
7522
7775
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7523
7776
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7524
7777
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7778
|
+
*
|
|
7779
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7780
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7781
|
+
* omitted applied ids "newer").
|
|
7525
7782
|
*/
|
|
7526
|
-
async runMigrations(migrations) {
|
|
7783
|
+
async runMigrations(migrations, options) {
|
|
7527
7784
|
const api = this.ctx.api.migrations;
|
|
7528
7785
|
const report = await runAddonMigrations({
|
|
7529
7786
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7530
7787
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7531
7788
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7532
|
-
}, migrations, this.ctx.logger);
|
|
7533
|
-
if (report.blockOwner.length > 0) throw new
|
|
7789
|
+
}, migrations, this.ctx.logger, options);
|
|
7790
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7534
7791
|
return report;
|
|
7535
7792
|
}
|
|
7536
7793
|
/**
|
|
@@ -7870,6 +8127,10 @@ var BaseAddon = class {
|
|
|
7870
8127
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
7871
8128
|
const patch = {};
|
|
7872
8129
|
for (const key of keys) {
|
|
8130
|
+
if (tree.get(key)?.kind === "array") {
|
|
8131
|
+
patch[key] = [];
|
|
8132
|
+
continue;
|
|
8133
|
+
}
|
|
7873
8134
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
7874
8135
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
7875
8136
|
}
|
|
@@ -8509,6 +8770,45 @@ function normalizeAddonInitResult(result) {
|
|
|
8509
8770
|
if (Array.isArray(result)) return { providers: result };
|
|
8510
8771
|
return result;
|
|
8511
8772
|
}
|
|
8773
|
+
var PREFIX = "records/";
|
|
8774
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8775
|
+
var RecordsError = class extends Error {
|
|
8776
|
+
code;
|
|
8777
|
+
detail;
|
|
8778
|
+
constructor(code, detail) {
|
|
8779
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
8780
|
+
this.name = "RecordsError";
|
|
8781
|
+
this.code = code;
|
|
8782
|
+
this.detail = detail;
|
|
8783
|
+
}
|
|
8784
|
+
};
|
|
8785
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8786
|
+
var RevConflictError = class extends RecordsError {
|
|
8787
|
+
constructor(detail) {
|
|
8788
|
+
super("rev-conflict", detail);
|
|
8789
|
+
this.name = "RevConflictError";
|
|
8790
|
+
}
|
|
8791
|
+
};
|
|
8792
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8793
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8794
|
+
constructor(detail) {
|
|
8795
|
+
super("secret-unavailable", detail);
|
|
8796
|
+
this.name = "RecordSecretUnavailableError";
|
|
8797
|
+
}
|
|
8798
|
+
};
|
|
8799
|
+
var InvalidRecordError = class extends RecordsError {
|
|
8800
|
+
constructor(detail) {
|
|
8801
|
+
super("invalid-record", detail);
|
|
8802
|
+
this.name = "InvalidRecordError";
|
|
8803
|
+
}
|
|
8804
|
+
};
|
|
8805
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
8806
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
8807
|
+
constructor(detail) {
|
|
8808
|
+
super("declaration-refused", detail);
|
|
8809
|
+
this.name = "DeclarationRefusedError";
|
|
8810
|
+
}
|
|
8811
|
+
};
|
|
8512
8812
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8513
8813
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8514
8814
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -8524,6 +8824,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
8524
8824
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
8525
8825
|
var RECORD_BATCH_MAX = 1e3;
|
|
8526
8826
|
var RECORD_QUERY_MAX = 1e3;
|
|
8827
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8828
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8829
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8830
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8831
|
+
/**
|
|
8832
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8833
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8834
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
8835
|
+
*
|
|
8836
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8837
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8838
|
+
* key releases exactly that key's secret.
|
|
8839
|
+
*
|
|
8840
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8841
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8842
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
8843
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
8844
|
+
*
|
|
8845
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
8846
|
+
*/
|
|
8847
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
8848
|
+
function parseSecretPath(path) {
|
|
8849
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8850
|
+
return path.split(".").map((raw) => {
|
|
8851
|
+
const each = raw.endsWith("[]");
|
|
8852
|
+
const map = raw.endsWith("{}");
|
|
8853
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8854
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8855
|
+
return {
|
|
8856
|
+
key,
|
|
8857
|
+
each,
|
|
8858
|
+
map
|
|
8859
|
+
};
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
8863
|
+
function secretArrayPrefixes(path) {
|
|
8864
|
+
const segments = parseSecretPath(path);
|
|
8865
|
+
const prefixes = [];
|
|
8866
|
+
const walked = [];
|
|
8867
|
+
for (const segment of segments) {
|
|
8868
|
+
walked.push(segment.key);
|
|
8869
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
8870
|
+
}
|
|
8871
|
+
return prefixes;
|
|
8872
|
+
}
|
|
8873
|
+
function isRecord$4(value) {
|
|
8874
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8875
|
+
}
|
|
8876
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8877
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8878
|
+
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`);
|
|
8879
|
+
return id;
|
|
8880
|
+
}
|
|
8881
|
+
/**
|
|
8882
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8883
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8884
|
+
* result is a new body, `data` is untouched.
|
|
8885
|
+
*/
|
|
8886
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8887
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8888
|
+
if (!isRecord$4(node)) return node;
|
|
8889
|
+
const segment = segments[index];
|
|
8890
|
+
if (segment === void 0) return node;
|
|
8891
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8892
|
+
const child = node[segment.key];
|
|
8893
|
+
const keys = [...walkedKeys, segment.key];
|
|
8894
|
+
const last = index === segments.length - 1;
|
|
8895
|
+
if (segment.each) {
|
|
8896
|
+
if (!Array.isArray(child)) return node;
|
|
8897
|
+
const prefix = keys.join(".");
|
|
8898
|
+
const idKey = arrayKeys[prefix];
|
|
8899
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8900
|
+
const mapped = child.map((element) => {
|
|
8901
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8902
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8903
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8904
|
+
});
|
|
8905
|
+
return {
|
|
8906
|
+
...node,
|
|
8907
|
+
[segment.key]: mapped
|
|
8908
|
+
};
|
|
8909
|
+
}
|
|
8910
|
+
if (segment.map) {
|
|
8911
|
+
if (!isRecord$4(child)) return node;
|
|
8912
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8913
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8914
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8915
|
+
return [mapKey, visit({
|
|
8916
|
+
declared,
|
|
8917
|
+
concrete,
|
|
8918
|
+
value
|
|
8919
|
+
})];
|
|
8920
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8921
|
+
return {
|
|
8922
|
+
...node,
|
|
8923
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8924
|
+
};
|
|
8925
|
+
}
|
|
8926
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8927
|
+
if (last) {
|
|
8928
|
+
const replacement = visit({
|
|
8929
|
+
declared,
|
|
8930
|
+
concrete,
|
|
8931
|
+
value: child
|
|
8932
|
+
});
|
|
8933
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8934
|
+
return {
|
|
8935
|
+
...node,
|
|
8936
|
+
[segment.key]: replacement
|
|
8937
|
+
};
|
|
8938
|
+
}
|
|
8939
|
+
return {
|
|
8940
|
+
...node,
|
|
8941
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8942
|
+
};
|
|
8943
|
+
}
|
|
8944
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8945
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8946
|
+
let current = data;
|
|
8947
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8948
|
+
return current;
|
|
8949
|
+
}
|
|
8950
|
+
/** Every present secret leaf, read-only. */
|
|
8951
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
8952
|
+
const found = [];
|
|
8953
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
8954
|
+
found.push(leaf);
|
|
8955
|
+
return leaf.value;
|
|
8956
|
+
});
|
|
8957
|
+
return found;
|
|
8958
|
+
}
|
|
8959
|
+
/**
|
|
8960
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8961
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8962
|
+
* already holds during an adoption.
|
|
8963
|
+
*/
|
|
8964
|
+
function recordSecretCollection(kind) {
|
|
8965
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8966
|
+
}
|
|
8967
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
8968
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
8969
|
+
return buildSecretRef({
|
|
8970
|
+
owner,
|
|
8971
|
+
scope: {
|
|
8972
|
+
kind: "row",
|
|
8973
|
+
collection: recordSecretCollection(kind),
|
|
8974
|
+
rowId
|
|
8975
|
+
},
|
|
8976
|
+
fieldPath: concretePath
|
|
8977
|
+
});
|
|
8978
|
+
}
|
|
8979
|
+
/**
|
|
8980
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
8981
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
8982
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
8983
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
8984
|
+
* stored secret (the records door reads a view as KEEP).
|
|
8985
|
+
*
|
|
8986
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
8987
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
8988
|
+
*/
|
|
8989
|
+
function isRecord$3(value) {
|
|
8990
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8991
|
+
}
|
|
8992
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
8993
|
+
if (decl.secretPaths.length === 0) return data;
|
|
8994
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
8995
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
8996
|
+
return {
|
|
8997
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
8998
|
+
v: 1,
|
|
8999
|
+
set: true
|
|
9000
|
+
};
|
|
9001
|
+
});
|
|
9002
|
+
return isRecord$3(shown) ? shown : data;
|
|
9003
|
+
}
|
|
8527
9004
|
/**
|
|
8528
9005
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8529
9006
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8596,7 +9073,8 @@ function isZodSecret(schema) {
|
|
|
8596
9073
|
}
|
|
8597
9074
|
/**
|
|
8598
9075
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
8599
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9076
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9077
|
+
* map" (`applianceSecrets{}`). A declared
|
|
8600
9078
|
* object or array is reported as one path and not descended into — everything
|
|
8601
9079
|
* under it is secret.
|
|
8602
9080
|
*/
|
|
@@ -8607,6 +9085,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
8607
9085
|
const def = defOf(layer);
|
|
8608
9086
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
8609
9087
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9088
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
8610
9089
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
8611
9090
|
else if (def.type === "intersection") {
|
|
8612
9091
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -8681,6 +9160,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
8681
9160
|
}).strict();
|
|
8682
9161
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
8683
9162
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
9163
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
9164
|
+
function assertValidDeclaration(name, decl) {
|
|
9165
|
+
const refuse = (why) => {
|
|
9166
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
9167
|
+
};
|
|
9168
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
9169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9170
|
+
for (const index of decl.indexes) {
|
|
9171
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
9172
|
+
seen.add(index.name);
|
|
9173
|
+
}
|
|
9174
|
+
const { retention } = decl;
|
|
9175
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
9176
|
+
if ("ttlMs" in retention) {
|
|
9177
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
9178
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
9179
|
+
}
|
|
9180
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
9181
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
9182
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
9183
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
9184
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
9185
|
+
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)`);
|
|
9186
|
+
}
|
|
9187
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
9188
|
+
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`);
|
|
9189
|
+
}
|
|
9190
|
+
function bodyFieldsOf(schema) {
|
|
9191
|
+
return { ...toJSONSchema(schema, {
|
|
9192
|
+
unrepresentable: "any",
|
|
9193
|
+
io: "input"
|
|
9194
|
+
}) };
|
|
9195
|
+
}
|
|
9196
|
+
function isRecordLike(value) {
|
|
9197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9198
|
+
}
|
|
9199
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
9200
|
+
function hasNameField(json, field) {
|
|
9201
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
9202
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
9203
|
+
const properties = json["properties"];
|
|
9204
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
9205
|
+
}
|
|
9206
|
+
/**
|
|
9207
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
9208
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
9209
|
+
* types its handle from.
|
|
9210
|
+
*/
|
|
9211
|
+
function defineRecordKinds(specs) {
|
|
9212
|
+
const out = {};
|
|
9213
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
9214
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
9215
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
9216
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
9217
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
9218
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
9219
|
+
database: spec.database,
|
|
9220
|
+
class: spec.class,
|
|
9221
|
+
writeTrigger: spec.writeTrigger,
|
|
9222
|
+
device: spec.device,
|
|
9223
|
+
at: spec.at,
|
|
9224
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
9225
|
+
retention: spec.retention,
|
|
9226
|
+
deviceRemoval: spec.deviceRemoval,
|
|
9227
|
+
maxDataBytes: spec.maxDataBytes,
|
|
9228
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
9229
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
9230
|
+
transactFamily: spec.transactFamily,
|
|
9231
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
9232
|
+
title: spec.entry.title,
|
|
9233
|
+
nameField,
|
|
9234
|
+
fields
|
|
9235
|
+
}
|
|
9236
|
+
});
|
|
9237
|
+
assertValidDeclaration(name, parsed);
|
|
9238
|
+
out[name] = {
|
|
9239
|
+
name,
|
|
9240
|
+
schema: spec.schema,
|
|
9241
|
+
declaration: parsed
|
|
9242
|
+
};
|
|
9243
|
+
}
|
|
9244
|
+
return out;
|
|
9245
|
+
}
|
|
9246
|
+
/**
|
|
9247
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
9248
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
9249
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
9250
|
+
* through the `record-entries` capability with password fields derived from
|
|
9251
|
+
* the schema (`.meta({ secret: true })`).
|
|
9252
|
+
*/
|
|
9253
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
9254
|
+
function entryBody(shape) {
|
|
9255
|
+
return object({
|
|
9256
|
+
name: string().min(1),
|
|
9257
|
+
...shape
|
|
9258
|
+
});
|
|
9259
|
+
}
|
|
9260
|
+
/**
|
|
9261
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
9262
|
+
* engine, never aged, belonging to no device.
|
|
9263
|
+
*/
|
|
9264
|
+
function defineEntryKind(name, spec) {
|
|
9265
|
+
const defined = defineRecordKinds({ [name]: {
|
|
9266
|
+
schema: spec.schema,
|
|
9267
|
+
class: "config",
|
|
9268
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9269
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9270
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9271
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9272
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9273
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9274
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
9275
|
+
entry: {
|
|
9276
|
+
title: spec.title,
|
|
9277
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
9278
|
+
}
|
|
9279
|
+
} })[name];
|
|
9280
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
9281
|
+
return defined;
|
|
9282
|
+
}
|
|
8684
9283
|
object({
|
|
8685
9284
|
field: string().min(1),
|
|
8686
9285
|
on: literal("advance")
|
|
@@ -9337,6 +9936,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9337
9936
|
"backup",
|
|
9338
9937
|
"battery",
|
|
9339
9938
|
"binary",
|
|
9939
|
+
"board-key-source",
|
|
9340
9940
|
"brightness",
|
|
9341
9941
|
"broker",
|
|
9342
9942
|
"button",
|
|
@@ -9458,6 +10058,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9458
10058
|
"privacy-mask",
|
|
9459
10059
|
"ptz",
|
|
9460
10060
|
"ptz-autotrack",
|
|
10061
|
+
"pump-cycle",
|
|
9461
10062
|
"reboot",
|
|
9462
10063
|
"record-entries",
|
|
9463
10064
|
"recording",
|
|
@@ -10750,18 +11351,6 @@ Object.freeze({
|
|
|
10750
11351
|
"path": "pumpRunning",
|
|
10751
11352
|
"valueType": "boolean"
|
|
10752
11353
|
},
|
|
10753
|
-
{
|
|
10754
|
-
"path": "pumpIntervalEnabled",
|
|
10755
|
-
"valueType": "boolean"
|
|
10756
|
-
},
|
|
10757
|
-
{
|
|
10758
|
-
"path": "pumpOnTime",
|
|
10759
|
-
"valueType": "number"
|
|
10760
|
-
},
|
|
10761
|
-
{
|
|
10762
|
-
"path": "pumpOffTime",
|
|
10763
|
-
"valueType": "number"
|
|
10764
|
-
},
|
|
10765
11354
|
{
|
|
10766
11355
|
"path": "lastDrink",
|
|
10767
11356
|
"valueType": "number"
|
|
@@ -10885,6 +11474,20 @@ Object.freeze({
|
|
|
10885
11474
|
"valueType": "number"
|
|
10886
11475
|
}
|
|
10887
11476
|
],
|
|
11477
|
+
"pump-cycle": [
|
|
11478
|
+
{
|
|
11479
|
+
"path": "enabled",
|
|
11480
|
+
"valueType": "boolean"
|
|
11481
|
+
},
|
|
11482
|
+
{
|
|
11483
|
+
"path": "onTime",
|
|
11484
|
+
"valueType": "number"
|
|
11485
|
+
},
|
|
11486
|
+
{
|
|
11487
|
+
"path": "offTime",
|
|
11488
|
+
"valueType": "number"
|
|
11489
|
+
}
|
|
11490
|
+
],
|
|
10888
11491
|
"recording-archive": [
|
|
10889
11492
|
{
|
|
10890
11493
|
"path": "deviceId",
|
|
@@ -11334,7 +11937,7 @@ object({
|
|
|
11334
11937
|
params: record(string(), union([string(), number()])).optional()
|
|
11335
11938
|
});
|
|
11336
11939
|
//#endregion
|
|
11337
|
-
//#region ../types/dist/composition-
|
|
11940
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11338
11941
|
/**
|
|
11339
11942
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11340
11943
|
*
|
|
@@ -12097,38 +12700,6 @@ function validateExpressionSource(src) {
|
|
|
12097
12700
|
return null;
|
|
12098
12701
|
}
|
|
12099
12702
|
/**
|
|
12100
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12101
|
-
*
|
|
12102
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12103
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12104
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12105
|
-
*
|
|
12106
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12107
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12108
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12109
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12110
|
-
* can move later without moving the others.
|
|
12111
|
-
*/
|
|
12112
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12113
|
-
var APPLIANCE_KINDS = [
|
|
12114
|
-
"washer",
|
|
12115
|
-
"dryer",
|
|
12116
|
-
"dishwasher"
|
|
12117
|
-
];
|
|
12118
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12119
|
-
/**
|
|
12120
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12121
|
-
*
|
|
12122
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12123
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12124
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12125
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12126
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12127
|
-
* the smallest radiator.
|
|
12128
|
-
*/
|
|
12129
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12130
|
-
DeviceType.Heater;
|
|
12131
|
-
/**
|
|
12132
12703
|
* Statistics and integrals over a history read (D944).
|
|
12133
12704
|
*
|
|
12134
12705
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12272,34 +12843,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12272
12843
|
/** Actuator groups only. Readings leave this unset. */
|
|
12273
12844
|
active: ActiveCriterionSchema.optional()
|
|
12274
12845
|
}),
|
|
12275
|
-
object({
|
|
12276
|
-
kind: literal("appliance"),
|
|
12277
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12278
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12279
|
-
thresholdWatts: number().positive(),
|
|
12280
|
-
startDwellMs: number().int().nonnegative(),
|
|
12281
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12282
|
-
member: HelperMemberSchema,
|
|
12283
|
-
/**
|
|
12284
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12285
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12286
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12287
|
-
*/
|
|
12288
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12289
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12290
|
-
offSeconds: number().optional()
|
|
12291
|
-
}),
|
|
12292
|
-
object({
|
|
12293
|
-
kind: literal("heater"),
|
|
12294
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12295
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12296
|
-
plug: HelperMemberSchema,
|
|
12297
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12298
|
-
meter: HelperMemberSchema.optional(),
|
|
12299
|
-
thresholdWatts: number().positive(),
|
|
12300
|
-
startDwellMs: number().int().nonnegative(),
|
|
12301
|
-
stopDwellMs: number().int().nonnegative()
|
|
12302
|
-
}),
|
|
12303
12846
|
object({
|
|
12304
12847
|
kind: literal("statistic"),
|
|
12305
12848
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12471,7 +13014,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
12471
13014
|
method: string().min(1),
|
|
12472
13015
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
12473
13016
|
});
|
|
12474
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
13017
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
13018
|
+
CompositionForwardCommandSchema,
|
|
13019
|
+
CompositionCodeSourceSchema,
|
|
13020
|
+
object({
|
|
13021
|
+
kind: literal("extension"),
|
|
13022
|
+
target: literal("remote")
|
|
13023
|
+
})
|
|
13024
|
+
]);
|
|
12475
13025
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
12476
13026
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
12477
13027
|
/**
|
|
@@ -12516,7 +13066,7 @@ var CompositionSchema = object({
|
|
|
12516
13066
|
kind: literal("existing"),
|
|
12517
13067
|
device: CompositionSourceRefSchema
|
|
12518
13068
|
})]),
|
|
12519
|
-
features: array(CompositionFeatureSchema).min(
|
|
13069
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12520
13070
|
/**
|
|
12521
13071
|
* Present when the block is a helper template. Absent on a free
|
|
12522
13072
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12548,6 +13098,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12548
13098
|
"node-g",
|
|
12549
13099
|
"node-h"
|
|
12550
13100
|
]);
|
|
13101
|
+
//#endregion
|
|
13102
|
+
//#region ../types/dist/index.mjs
|
|
13103
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
13104
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
13105
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
13106
|
+
/** The text excerpt a message carries on the bus. */
|
|
13107
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
13108
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
13109
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
13110
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
13111
|
+
object({
|
|
13112
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
13113
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
13114
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
13115
|
+
requireAuth: boolean().default(true),
|
|
13116
|
+
username: string().default(""),
|
|
13117
|
+
password: string().default("").meta({ secret: true }),
|
|
13118
|
+
starttls: boolean().default(false),
|
|
13119
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
13120
|
+
});
|
|
13121
|
+
object({
|
|
13122
|
+
host: string().min(1).nullable(),
|
|
13123
|
+
hostReason: string().nullable(),
|
|
13124
|
+
port: PortSchema,
|
|
13125
|
+
domain: string().min(1),
|
|
13126
|
+
requireAuth: boolean(),
|
|
13127
|
+
username: string(),
|
|
13128
|
+
password: string(),
|
|
13129
|
+
starttls: boolean()
|
|
13130
|
+
});
|
|
13131
|
+
object({
|
|
13132
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13133
|
+
/**
|
|
13134
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13135
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13136
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13137
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13138
|
+
*/
|
|
13139
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13140
|
+
});
|
|
13141
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13142
|
+
filename: string(),
|
|
13143
|
+
contentType: string(),
|
|
13144
|
+
bytes: number().int().nonnegative()
|
|
13145
|
+
});
|
|
13146
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13147
|
+
var SmtpInboundMessageSchema = object({
|
|
13148
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13149
|
+
seq: number().int().positive(),
|
|
13150
|
+
messageId: string(),
|
|
13151
|
+
receivedAt: number().int().nonnegative(),
|
|
13152
|
+
from: string(),
|
|
13153
|
+
to: string(),
|
|
13154
|
+
subject: string(),
|
|
13155
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13156
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13157
|
+
});
|
|
13158
|
+
object({
|
|
13159
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13160
|
+
lastSeq: number().int().nonnegative(),
|
|
13161
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13162
|
+
oldestSeq: number().int().nonnegative(),
|
|
13163
|
+
messages: array(object({
|
|
13164
|
+
key: string(),
|
|
13165
|
+
message: SmtpInboundMessageSchema
|
|
13166
|
+
}))
|
|
13167
|
+
});
|
|
12551
13168
|
new Set([
|
|
12552
13169
|
"track",
|
|
12553
13170
|
"face",
|
|
@@ -12667,6 +13284,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
12667
13284
|
]),
|
|
12668
13285
|
candidateStableId: string().optional()
|
|
12669
13286
|
});
|
|
13287
|
+
/**
|
|
13288
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13289
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13290
|
+
*
|
|
13291
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13292
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13293
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13294
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13295
|
+
*
|
|
13296
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13297
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13298
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13299
|
+
*/
|
|
13300
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13301
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13302
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13303
|
+
function topLevelSecretKeys(paths) {
|
|
13304
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13305
|
+
}
|
|
13306
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13307
|
+
function secretMapKeys(paths) {
|
|
13308
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13309
|
+
}
|
|
13310
|
+
/**
|
|
13311
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13312
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13313
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13314
|
+
*/
|
|
13315
|
+
function secretMapPatch(value) {
|
|
13316
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13317
|
+
const out = {};
|
|
13318
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13319
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13320
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13321
|
+
}
|
|
13322
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13323
|
+
function declaresEnabled(kind) {
|
|
13324
|
+
const json = toJSONSchema(kind.schema, {
|
|
13325
|
+
unrepresentable: "any",
|
|
13326
|
+
io: "input"
|
|
13327
|
+
});
|
|
13328
|
+
const properties = Reflect.get(json, "properties");
|
|
13329
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13330
|
+
}
|
|
13331
|
+
var RecordIntegrationInstances = class {
|
|
13332
|
+
records;
|
|
13333
|
+
kind;
|
|
13334
|
+
hooks;
|
|
13335
|
+
handle;
|
|
13336
|
+
secretKeys;
|
|
13337
|
+
secretMaps;
|
|
13338
|
+
nameField;
|
|
13339
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13340
|
+
registryOwnsEnabled;
|
|
13341
|
+
constructor(records, kind, hooks) {
|
|
13342
|
+
this.records = records;
|
|
13343
|
+
this.kind = kind;
|
|
13344
|
+
this.hooks = hooks;
|
|
13345
|
+
this.handle = records.kind(kind);
|
|
13346
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13347
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13348
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13349
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13350
|
+
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"`);
|
|
13351
|
+
}
|
|
13352
|
+
/**
|
|
13353
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13354
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13355
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13356
|
+
*/
|
|
13357
|
+
async readResolved(id) {
|
|
13358
|
+
try {
|
|
13359
|
+
const record = await this.handle.getResolved(id);
|
|
13360
|
+
return record === null ? null : {
|
|
13361
|
+
id,
|
|
13362
|
+
body: record.data,
|
|
13363
|
+
enabled: record.data.enabled !== false,
|
|
13364
|
+
secrets: "resolved"
|
|
13365
|
+
};
|
|
13366
|
+
} catch (err) {
|
|
13367
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13368
|
+
const stored = await this.handle.get(id);
|
|
13369
|
+
if (stored === null) return null;
|
|
13370
|
+
return {
|
|
13371
|
+
id,
|
|
13372
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13373
|
+
enabled: stored.data.enabled !== false,
|
|
13374
|
+
secrets: "unavailable"
|
|
13375
|
+
};
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13379
|
+
async getResolved(id) {
|
|
13380
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13381
|
+
}
|
|
13382
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13383
|
+
async listResolved() {
|
|
13384
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13385
|
+
const out = [];
|
|
13386
|
+
for (const record of page.records) {
|
|
13387
|
+
const read = await this.readResolved(record.id);
|
|
13388
|
+
if (read !== null) out.push(read);
|
|
13389
|
+
}
|
|
13390
|
+
return out;
|
|
13391
|
+
}
|
|
13392
|
+
viewOf(record) {
|
|
13393
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13394
|
+
const rawName = record.data[this.nameField];
|
|
13395
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13396
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13397
|
+
return {
|
|
13398
|
+
id: record.id,
|
|
13399
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13400
|
+
configured: this.hooks.configured(record.data),
|
|
13401
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13402
|
+
formValues: shown,
|
|
13403
|
+
...kind !== void 0 ? { kind } : {},
|
|
13404
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13405
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13406
|
+
};
|
|
13407
|
+
}
|
|
13408
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13409
|
+
...this.hooks.description,
|
|
13410
|
+
mintsId: true
|
|
13411
|
+
};
|
|
13412
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13413
|
+
listInstances = async () => {
|
|
13414
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13415
|
+
};
|
|
13416
|
+
getInstance = async ({ id }) => {
|
|
13417
|
+
const record = await this.handle.get(id);
|
|
13418
|
+
return record === null ? null : this.viewOf(record);
|
|
13419
|
+
};
|
|
13420
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13421
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13422
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13423
|
+
name,
|
|
13424
|
+
...kind !== void 0 ? { kind } : {}
|
|
13425
|
+
});
|
|
13426
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13427
|
+
await this.handle.put({
|
|
13428
|
+
id: minted,
|
|
13429
|
+
data: this.hooks.initialBody(name, kind)
|
|
13430
|
+
}, { ifRev: 0 });
|
|
13431
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13432
|
+
return { id: minted };
|
|
13433
|
+
};
|
|
13434
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13435
|
+
const patch = {};
|
|
13436
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13437
|
+
if (value === void 0) continue;
|
|
13438
|
+
if (this.secretMaps.has(key)) {
|
|
13439
|
+
const entries = secretMapPatch(value);
|
|
13440
|
+
if (entries !== null) patch[key] = entries;
|
|
13441
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13442
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13443
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13444
|
+
}
|
|
13445
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13446
|
+
await this.handle.patch(id, patch);
|
|
13447
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13448
|
+
return { ok: true };
|
|
13449
|
+
};
|
|
13450
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13451
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13452
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13453
|
+
await this.handle.patch(id, { enabled });
|
|
13454
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13455
|
+
return { ok: true };
|
|
13456
|
+
};
|
|
13457
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13458
|
+
deleteInstance = async ({ id }) => {
|
|
13459
|
+
await this.handle.delete(id);
|
|
13460
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13461
|
+
return { ok: true };
|
|
13462
|
+
};
|
|
13463
|
+
testInstance = async (input) => {
|
|
13464
|
+
if (this.hooks.test === void 0) return {
|
|
13465
|
+
success: false,
|
|
13466
|
+
status: "unsupported",
|
|
13467
|
+
error: "This integration declares no connection test"
|
|
13468
|
+
};
|
|
13469
|
+
return this.hooks.test(input);
|
|
13470
|
+
};
|
|
13471
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13472
|
+
toProvider() {
|
|
13473
|
+
return {
|
|
13474
|
+
describe: this.describe,
|
|
13475
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13476
|
+
listInstances: this.listInstances,
|
|
13477
|
+
getInstance: this.getInstance,
|
|
13478
|
+
createInstance: this.createInstance,
|
|
13479
|
+
updateInstance: this.updateInstance,
|
|
13480
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13481
|
+
deleteInstance: this.deleteInstance,
|
|
13482
|
+
testInstance: this.testInstance
|
|
13483
|
+
};
|
|
13484
|
+
}
|
|
13485
|
+
};
|
|
12670
13486
|
var EncodeProfileSchema = object({
|
|
12671
13487
|
video: object({
|
|
12672
13488
|
codec: _enum([
|
|
@@ -13702,7 +14518,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
13702
14518
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
13703
14519
|
* catalogue lists the id (a newer database).
|
|
13704
14520
|
*/
|
|
13705
|
-
meta: MigrationMetaSchema.optional()
|
|
14521
|
+
meta: MigrationMetaSchema.optional(),
|
|
14522
|
+
/**
|
|
14523
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14524
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14525
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14526
|
+
*/
|
|
14527
|
+
ownerNotLoaded: boolean().optional()
|
|
13706
14528
|
});
|
|
13707
14529
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
13708
14530
|
var MigrationPendingEntrySchema = object({
|
|
@@ -13736,6 +14558,149 @@ var MigrationOwnerStepSchema = object({
|
|
|
13736
14558
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
13737
14559
|
});
|
|
13738
14560
|
/**
|
|
14561
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
14562
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
14563
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
14564
|
+
*
|
|
14565
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
14566
|
+
*
|
|
14567
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
14568
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
14569
|
+
* anything is written; `block-owner` then holds the addon.
|
|
14570
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
14571
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
14572
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
14573
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
14574
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
14575
|
+
*
|
|
14576
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
14577
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
14578
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
14579
|
+
*/
|
|
14580
|
+
function secretLeavesOf(body, spec) {
|
|
14581
|
+
const out = /* @__PURE__ */ new Map();
|
|
14582
|
+
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);
|
|
14583
|
+
return out;
|
|
14584
|
+
}
|
|
14585
|
+
function why(err) {
|
|
14586
|
+
return err instanceof Error ? err.message : String(err);
|
|
14587
|
+
}
|
|
14588
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
14589
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
14590
|
+
if (integrations.length === 0) return {
|
|
14591
|
+
outcome: "already-done",
|
|
14592
|
+
summary: { integrations: 0 }
|
|
14593
|
+
};
|
|
14594
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
14595
|
+
for (const integration of integrations) try {
|
|
14596
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
14597
|
+
} catch (err) {
|
|
14598
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
14599
|
+
}
|
|
14600
|
+
let created = 0;
|
|
14601
|
+
let cleared = 0;
|
|
14602
|
+
let kept = 0;
|
|
14603
|
+
for (const integration of integrations) {
|
|
14604
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
14605
|
+
const hasRows = Object.keys(values).length > 0;
|
|
14606
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
14607
|
+
const put = await ports.putRecord(integration.id, body);
|
|
14608
|
+
if (put === "created") created += 1;
|
|
14609
|
+
if (!hasRows) continue;
|
|
14610
|
+
const wanted = secretLeavesOf(body, spec);
|
|
14611
|
+
const back = await ports.readRecord(integration.id);
|
|
14612
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
14613
|
+
const held = secretLeavesOf(back, spec);
|
|
14614
|
+
if (put === "created") {
|
|
14615
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
14616
|
+
if (wrong.length > 0) {
|
|
14617
|
+
await ports.deleteRecord(integration.id);
|
|
14618
|
+
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`);
|
|
14619
|
+
}
|
|
14620
|
+
} else {
|
|
14621
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
14622
|
+
if (missing.length > 0) {
|
|
14623
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
14624
|
+
kept += 1;
|
|
14625
|
+
continue;
|
|
14626
|
+
}
|
|
14627
|
+
}
|
|
14628
|
+
await ports.clearRows(integration.id);
|
|
14629
|
+
cleared += 1;
|
|
14630
|
+
}
|
|
14631
|
+
return created === 0 && cleared === 0 ? {
|
|
14632
|
+
outcome: "already-done",
|
|
14633
|
+
summary: {
|
|
14634
|
+
integrations: integrations.length,
|
|
14635
|
+
kept
|
|
14636
|
+
}
|
|
14637
|
+
} : {
|
|
14638
|
+
outcome: "done",
|
|
14639
|
+
summary: {
|
|
14640
|
+
integrations: integrations.length,
|
|
14641
|
+
created,
|
|
14642
|
+
cleared,
|
|
14643
|
+
kept
|
|
14644
|
+
}
|
|
14645
|
+
};
|
|
14646
|
+
}
|
|
14647
|
+
/**
|
|
14648
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
14649
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
14650
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
14651
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
14652
|
+
* keychain values; the addon never touches the registry.
|
|
14653
|
+
*/
|
|
14654
|
+
function integrationRowsPortsOverApi(deps) {
|
|
14655
|
+
return {
|
|
14656
|
+
warn: (message) => deps.log.warn(message),
|
|
14657
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
14658
|
+
id: i.id,
|
|
14659
|
+
name: i.name
|
|
14660
|
+
})),
|
|
14661
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
14662
|
+
putRecord: async (id, body) => {
|
|
14663
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
14664
|
+
try {
|
|
14665
|
+
await deps.records.put({
|
|
14666
|
+
id,
|
|
14667
|
+
data: body
|
|
14668
|
+
}, { ifRev: 0 });
|
|
14669
|
+
} catch (err) {
|
|
14670
|
+
if (err instanceof RevConflictError) return "exists";
|
|
14671
|
+
throw err;
|
|
14672
|
+
}
|
|
14673
|
+
return "created";
|
|
14674
|
+
},
|
|
14675
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
14676
|
+
deleteRecord: async (id) => {
|
|
14677
|
+
await deps.records.delete(id);
|
|
14678
|
+
},
|
|
14679
|
+
clearRows: async (integrationId) => {
|
|
14680
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
14681
|
+
}
|
|
14682
|
+
};
|
|
14683
|
+
}
|
|
14684
|
+
function defaultIntegrationApiOver(api) {
|
|
14685
|
+
return {
|
|
14686
|
+
list: () => api.integrations.list.query(),
|
|
14687
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
14688
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
14689
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
14690
|
+
addonId,
|
|
14691
|
+
projection: "slim"
|
|
14692
|
+
}),
|
|
14693
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
14694
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
14695
|
+
deviceId,
|
|
14696
|
+
integrationId
|
|
14697
|
+
});
|
|
14698
|
+
},
|
|
14699
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
14700
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
14701
|
+
};
|
|
14702
|
+
}
|
|
14703
|
+
/**
|
|
13739
14704
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
13740
14705
|
* gets back (cloud training, D805+).
|
|
13741
14706
|
*
|
|
@@ -18820,24 +19785,52 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18820
19785
|
args: { on: false }
|
|
18821
19786
|
},
|
|
18822
19787
|
{
|
|
18823
|
-
id: "petFountain.
|
|
18824
|
-
method: "
|
|
19788
|
+
id: "petFountain.resetDailyConsumption",
|
|
19789
|
+
method: "resetDailyConsumption",
|
|
19790
|
+
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19791
|
+
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19792
|
+
icon: "rotate-ccw"
|
|
19793
|
+
}
|
|
19794
|
+
],
|
|
19795
|
+
state: [{
|
|
19796
|
+
kind: "boolean",
|
|
19797
|
+
field: "empty",
|
|
19798
|
+
label: t("Tank empty", "Serbatoio vuoto"),
|
|
19799
|
+
whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
|
|
19800
|
+
whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
|
|
19801
|
+
icon: "droplet"
|
|
19802
|
+
}, {
|
|
19803
|
+
kind: "numeric",
|
|
19804
|
+
field: "waterLevel",
|
|
19805
|
+
quantity: t("the water level", "il livello dell’acqua"),
|
|
19806
|
+
label: t("Water level", "Livello dell’acqua"),
|
|
19807
|
+
unit: "%",
|
|
19808
|
+
icon: "droplets"
|
|
19809
|
+
}]
|
|
19810
|
+
},
|
|
19811
|
+
"pump-cycle": {
|
|
19812
|
+
scope: "device",
|
|
19813
|
+
noun: t("Pump cycle", "Ciclo della pompa"),
|
|
19814
|
+
actions: [
|
|
19815
|
+
{
|
|
19816
|
+
id: "pumpCycle.on",
|
|
19817
|
+
method: "setEnabled",
|
|
18825
19818
|
label: t("Pump cycle on", "Ciclo pompa attivo"),
|
|
18826
19819
|
phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
|
|
18827
19820
|
icon: "timer",
|
|
18828
19821
|
args: { on: true }
|
|
18829
19822
|
},
|
|
18830
19823
|
{
|
|
18831
|
-
id: "
|
|
18832
|
-
method: "
|
|
19824
|
+
id: "pumpCycle.off",
|
|
19825
|
+
method: "setEnabled",
|
|
18833
19826
|
label: t("Pump cycle off", "Ciclo pompa spento"),
|
|
18834
19827
|
phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
|
|
18835
19828
|
icon: "timer-off",
|
|
18836
19829
|
args: { on: false }
|
|
18837
19830
|
},
|
|
18838
19831
|
{
|
|
18839
|
-
id: "
|
|
18840
|
-
method: "
|
|
19832
|
+
id: "pumpCycle.onMinutes",
|
|
19833
|
+
method: "setOnMinutes",
|
|
18841
19834
|
label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
|
|
18842
19835
|
phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
|
|
18843
19836
|
icon: "timer",
|
|
@@ -18853,8 +19846,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18853
19846
|
}]
|
|
18854
19847
|
},
|
|
18855
19848
|
{
|
|
18856
|
-
id: "
|
|
18857
|
-
method: "
|
|
19849
|
+
id: "pumpCycle.offMinutes",
|
|
19850
|
+
method: "setOffMinutes",
|
|
18858
19851
|
label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
|
|
18859
19852
|
phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
|
|
18860
19853
|
icon: "timer-off",
|
|
@@ -18868,35 +19861,63 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
18868
19861
|
unit: "min"
|
|
18869
19862
|
}
|
|
18870
19863
|
}]
|
|
18871
|
-
},
|
|
18872
|
-
{
|
|
18873
|
-
id: "petFountain.resetDailyConsumption",
|
|
18874
|
-
method: "resetDailyConsumption",
|
|
18875
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
18876
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
18877
|
-
icon: "rotate-ccw"
|
|
18878
19864
|
}
|
|
18879
19865
|
],
|
|
18880
19866
|
state: [{
|
|
18881
19867
|
kind: "boolean",
|
|
18882
|
-
field: "
|
|
18883
|
-
label: t("
|
|
18884
|
-
whenTrue: t("When
|
|
18885
|
-
whenFalse: t("When
|
|
18886
|
-
icon: "
|
|
18887
|
-
}, {
|
|
18888
|
-
kind: "numeric",
|
|
18889
|
-
field: "waterLevel",
|
|
18890
|
-
quantity: t("the water level", "il livello dell’acqua"),
|
|
18891
|
-
label: t("Water level", "Livello dell’acqua"),
|
|
18892
|
-
unit: "%",
|
|
18893
|
-
icon: "droplets"
|
|
19868
|
+
field: "enabled",
|
|
19869
|
+
label: t("Pump cycle", "Ciclo pompa"),
|
|
19870
|
+
whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
|
|
19871
|
+
whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
|
|
19872
|
+
icon: "timer"
|
|
18894
19873
|
}]
|
|
18895
19874
|
},
|
|
18896
19875
|
"media-player": {
|
|
18897
19876
|
scope: "device",
|
|
18898
19877
|
noun: t("Media player", "Lettore multimediale"),
|
|
18899
19878
|
actions: [
|
|
19879
|
+
{
|
|
19880
|
+
id: "media.turnOn",
|
|
19881
|
+
method: "turnOn",
|
|
19882
|
+
label: t("Turn on", "Accendi"),
|
|
19883
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
19884
|
+
icon: "power"
|
|
19885
|
+
},
|
|
19886
|
+
{
|
|
19887
|
+
id: "media.turnOff",
|
|
19888
|
+
method: "turnOff",
|
|
19889
|
+
label: t("Turn off", "Spegni"),
|
|
19890
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
19891
|
+
icon: "power"
|
|
19892
|
+
},
|
|
19893
|
+
{
|
|
19894
|
+
id: "media.volumeUp",
|
|
19895
|
+
method: "volumeUp",
|
|
19896
|
+
label: t("Volume up", "Alza il volume"),
|
|
19897
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
19898
|
+
icon: "volume-2"
|
|
19899
|
+
},
|
|
19900
|
+
{
|
|
19901
|
+
id: "media.volumeDown",
|
|
19902
|
+
method: "volumeDown",
|
|
19903
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
19904
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
19905
|
+
icon: "volume-2"
|
|
19906
|
+
},
|
|
19907
|
+
{
|
|
19908
|
+
id: "media.channelUp",
|
|
19909
|
+
method: "channelUp",
|
|
19910
|
+
label: t("Channel up", "Canale successivo"),
|
|
19911
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
19912
|
+
icon: "skip-forward"
|
|
19913
|
+
},
|
|
19914
|
+
{
|
|
19915
|
+
id: "media.channelDown",
|
|
19916
|
+
method: "channelDown",
|
|
19917
|
+
label: t("Channel down", "Canale precedente"),
|
|
19918
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
19919
|
+
icon: "skip-back"
|
|
19920
|
+
},
|
|
18900
19921
|
{
|
|
18901
19922
|
id: "media.play",
|
|
18902
19923
|
method: "play",
|
|
@@ -20783,6 +21804,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
20783
21804
|
"audio-analyzer": system(PIPELINE),
|
|
20784
21805
|
"audio-codec": system(INTERNAL),
|
|
20785
21806
|
"auth-provider": system(IDENTITY),
|
|
21807
|
+
"board-key-source": system(INTERNAL),
|
|
20786
21808
|
backup: system("system backups — run and restored from the Backups page"),
|
|
20787
21809
|
broker: system("message-broker connections — configured, not actuated"),
|
|
20788
21810
|
"calendar-source": system(INTERNAL),
|
|
@@ -45692,6 +46714,49 @@ var binaryCapability = {
|
|
|
45692
46714
|
volatileStateFields: ["lastChangedAt"]
|
|
45693
46715
|
};
|
|
45694
46716
|
/**
|
|
46717
|
+
* Why a board key could not be read. Typed so a caller reports the cause and
|
|
46718
|
+
* never treats "could not read" as "has no key" (D49).
|
|
46719
|
+
*/
|
|
46720
|
+
var BoardKeyFailureReasonSchema = _enum([
|
|
46721
|
+
"unknown-source",
|
|
46722
|
+
"not-admin",
|
|
46723
|
+
"no-builder",
|
|
46724
|
+
"no-ingress-url",
|
|
46725
|
+
"unreachable",
|
|
46726
|
+
"unknown-board",
|
|
46727
|
+
"no-encryption-key",
|
|
46728
|
+
"unreadable-config"
|
|
46729
|
+
]);
|
|
46730
|
+
var BoardKeySourceSchema = object({
|
|
46731
|
+
sourceId: string(),
|
|
46732
|
+
name: string()
|
|
46733
|
+
});
|
|
46734
|
+
var BoardKeyBoardSchema = object({
|
|
46735
|
+
/** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
|
|
46736
|
+
configuration: string(),
|
|
46737
|
+
/** The node name the builder gives the board. */
|
|
46738
|
+
name: string(),
|
|
46739
|
+
/** The address the builder knows the board by, when it has one. */
|
|
46740
|
+
address: string().nullable()
|
|
46741
|
+
});
|
|
46742
|
+
var BoardKeyFailureSchema = object({
|
|
46743
|
+
ok: literal(false),
|
|
46744
|
+
reason: BoardKeyFailureReasonSchema,
|
|
46745
|
+
detail: string().optional()
|
|
46746
|
+
});
|
|
46747
|
+
var ListBoardsResultSchema = discriminatedUnion("ok", [object({
|
|
46748
|
+
ok: literal(true),
|
|
46749
|
+
boards: array(BoardKeyBoardSchema).readonly()
|
|
46750
|
+
}), BoardKeyFailureSchema]);
|
|
46751
|
+
var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
|
|
46752
|
+
ok: literal(true),
|
|
46753
|
+
key: string()
|
|
46754
|
+
}), BoardKeyFailureSchema]);
|
|
46755
|
+
method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
|
|
46756
|
+
sourceId: string().min(1),
|
|
46757
|
+
configuration: string().min(1)
|
|
46758
|
+
}), GetBoardKeyResultSchema, { auth: "admin" });
|
|
46759
|
+
/**
|
|
45695
46760
|
* Dimmable-light brightness control. Co-exists with `switch` on the
|
|
45696
46761
|
* same device — the switch toggles on/off, this cap sets the level
|
|
45697
46762
|
* applied when the light is on. Drivers map their per-vendor dim
|
|
@@ -48381,6 +49446,244 @@ var controlCapability = {
|
|
|
48381
49446
|
*/
|
|
48382
49447
|
history: "none"
|
|
48383
49448
|
};
|
|
49449
|
+
/**
|
|
49450
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
49451
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
49452
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
49453
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
49454
|
+
*
|
|
49455
|
+
* The cap declares a wide method surface; not every backend supports
|
|
49456
|
+
* every method. Drivers signal sub-capability via the
|
|
49457
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
49458
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
49459
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
49460
|
+
*/
|
|
49461
|
+
var MediaPlayerStateSchema = _enum([
|
|
49462
|
+
"off",
|
|
49463
|
+
"on",
|
|
49464
|
+
"idle",
|
|
49465
|
+
"playing",
|
|
49466
|
+
"paused",
|
|
49467
|
+
"buffering",
|
|
49468
|
+
"standby"
|
|
49469
|
+
]);
|
|
49470
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
49471
|
+
"off",
|
|
49472
|
+
"all",
|
|
49473
|
+
"one"
|
|
49474
|
+
]);
|
|
49475
|
+
var MediaInfoSchema = object({
|
|
49476
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
49477
|
+
* `podcast`, …). */
|
|
49478
|
+
type: string(),
|
|
49479
|
+
/** Human-readable title. */
|
|
49480
|
+
title: string(),
|
|
49481
|
+
/** Optional artist / channel / station label. */
|
|
49482
|
+
artist: string().optional(),
|
|
49483
|
+
/** Optional album / season / show name. */
|
|
49484
|
+
album: string().optional(),
|
|
49485
|
+
/** Optional cover-art / thumbnail URL. */
|
|
49486
|
+
imageUrl: string().optional()
|
|
49487
|
+
});
|
|
49488
|
+
/**
|
|
49489
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
49490
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
49491
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
49492
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
49493
|
+
* guess, never as a reading.
|
|
49494
|
+
*/
|
|
49495
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
49496
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
49497
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
49498
|
+
"state",
|
|
49499
|
+
"volumeLevel",
|
|
49500
|
+
"isMuted",
|
|
49501
|
+
"source"
|
|
49502
|
+
]);
|
|
49503
|
+
var MediaPlayerStatusSchema = object({
|
|
49504
|
+
/** Playback lifecycle state. */
|
|
49505
|
+
state: MediaPlayerStateSchema,
|
|
49506
|
+
/**
|
|
49507
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
49508
|
+
* A slice written before this field existed parses as `reported`.
|
|
49509
|
+
*/
|
|
49510
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
49511
|
+
/**
|
|
49512
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
49513
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
49514
|
+
* from a running-state source, mute guessed from the last send).
|
|
49515
|
+
*/
|
|
49516
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
49517
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
49518
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
49519
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
49520
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
49521
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
49522
|
+
isMuted: boolean().nullable(),
|
|
49523
|
+
/** Active source / input. Empty when no source surface. */
|
|
49524
|
+
source: string(),
|
|
49525
|
+
/** Selectable sources. Empty when no source surface.
|
|
49526
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
49527
|
+
availableSources: array(string()),
|
|
49528
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
49529
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
49530
|
+
/** Current playback position in ms. Null when not seekable or
|
|
49531
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
49532
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
49533
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
49534
|
+
* (live stream) or nothing is playing. */
|
|
49535
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
49536
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
49537
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
49538
|
+
shuffle: boolean().nullable(),
|
|
49539
|
+
/** Repeat mode. Null when no repeat surface.
|
|
49540
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
49541
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
49542
|
+
/** Ms epoch when the slice was last updated. */
|
|
49543
|
+
lastChangedAt: number()
|
|
49544
|
+
});
|
|
49545
|
+
var stepInput = object({
|
|
49546
|
+
deviceId: number().int().nonnegative(),
|
|
49547
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
49548
|
+
steps: number().int().min(1).max(50).default(1)
|
|
49549
|
+
});
|
|
49550
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
49551
|
+
var mediaPlayerCapability = {
|
|
49552
|
+
name: "media-player",
|
|
49553
|
+
scope: "device",
|
|
49554
|
+
deviceNative: true,
|
|
49555
|
+
mode: "singleton",
|
|
49556
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
49557
|
+
methods: {
|
|
49558
|
+
turnOn: method(deviceOnly, _void(), {
|
|
49559
|
+
kind: "mutation",
|
|
49560
|
+
auth: "protected"
|
|
49561
|
+
}),
|
|
49562
|
+
turnOff: method(deviceOnly, _void(), {
|
|
49563
|
+
kind: "mutation",
|
|
49564
|
+
auth: "protected"
|
|
49565
|
+
}),
|
|
49566
|
+
volumeUp: method(stepInput, _void(), {
|
|
49567
|
+
kind: "mutation",
|
|
49568
|
+
auth: "protected"
|
|
49569
|
+
}),
|
|
49570
|
+
volumeDown: method(stepInput, _void(), {
|
|
49571
|
+
kind: "mutation",
|
|
49572
|
+
auth: "protected"
|
|
49573
|
+
}),
|
|
49574
|
+
channelUp: method(stepInput, _void(), {
|
|
49575
|
+
kind: "mutation",
|
|
49576
|
+
auth: "protected"
|
|
49577
|
+
}),
|
|
49578
|
+
channelDown: method(stepInput, _void(), {
|
|
49579
|
+
kind: "mutation",
|
|
49580
|
+
auth: "protected"
|
|
49581
|
+
}),
|
|
49582
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49583
|
+
kind: "mutation",
|
|
49584
|
+
auth: "protected"
|
|
49585
|
+
}),
|
|
49586
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49587
|
+
kind: "mutation",
|
|
49588
|
+
auth: "protected"
|
|
49589
|
+
}),
|
|
49590
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49591
|
+
kind: "mutation",
|
|
49592
|
+
auth: "protected"
|
|
49593
|
+
}),
|
|
49594
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49595
|
+
kind: "mutation",
|
|
49596
|
+
auth: "protected"
|
|
49597
|
+
}),
|
|
49598
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
49599
|
+
kind: "mutation",
|
|
49600
|
+
auth: "protected"
|
|
49601
|
+
}),
|
|
49602
|
+
seek: method(object({
|
|
49603
|
+
deviceId: number().int().nonnegative(),
|
|
49604
|
+
position: number().int().nonnegative()
|
|
49605
|
+
}), _void(), {
|
|
49606
|
+
kind: "mutation",
|
|
49607
|
+
auth: "protected"
|
|
49608
|
+
}),
|
|
49609
|
+
setVolume: method(object({
|
|
49610
|
+
deviceId: number().int().nonnegative(),
|
|
49611
|
+
volumeLevel: number().min(0).max(100)
|
|
49612
|
+
}), _void(), {
|
|
49613
|
+
kind: "mutation",
|
|
49614
|
+
auth: "protected"
|
|
49615
|
+
}),
|
|
49616
|
+
setMute: method(object({
|
|
49617
|
+
deviceId: number().int().nonnegative(),
|
|
49618
|
+
muted: boolean()
|
|
49619
|
+
}), _void(), {
|
|
49620
|
+
kind: "mutation",
|
|
49621
|
+
auth: "protected"
|
|
49622
|
+
}),
|
|
49623
|
+
setShuffle: method(object({
|
|
49624
|
+
deviceId: number().int().nonnegative(),
|
|
49625
|
+
shuffle: boolean()
|
|
49626
|
+
}), _void(), {
|
|
49627
|
+
kind: "mutation",
|
|
49628
|
+
auth: "protected"
|
|
49629
|
+
}),
|
|
49630
|
+
setRepeat: method(object({
|
|
49631
|
+
deviceId: number().int().nonnegative(),
|
|
49632
|
+
repeat: MediaPlayerRepeatSchema
|
|
49633
|
+
}), _void(), {
|
|
49634
|
+
kind: "mutation",
|
|
49635
|
+
auth: "protected"
|
|
49636
|
+
}),
|
|
49637
|
+
selectSource: method(object({
|
|
49638
|
+
deviceId: number().int().nonnegative(),
|
|
49639
|
+
source: string().min(1)
|
|
49640
|
+
}), _void(), {
|
|
49641
|
+
kind: "mutation",
|
|
49642
|
+
auth: "protected"
|
|
49643
|
+
}),
|
|
49644
|
+
playMedia: method(object({
|
|
49645
|
+
deviceId: number().int().nonnegative(),
|
|
49646
|
+
/** Media identifier / URL. */
|
|
49647
|
+
mediaId: string().min(1),
|
|
49648
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
49649
|
+
* passes it through to the upstream service. */
|
|
49650
|
+
mediaType: string().min(1)
|
|
49651
|
+
}), _void(), {
|
|
49652
|
+
kind: "mutation",
|
|
49653
|
+
auth: "protected"
|
|
49654
|
+
})
|
|
49655
|
+
},
|
|
49656
|
+
status: {
|
|
49657
|
+
schema: MediaPlayerStatusSchema,
|
|
49658
|
+
kind: "push"
|
|
49659
|
+
},
|
|
49660
|
+
/**
|
|
49661
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
49662
|
+
* full slice for live now-playing, volume, and progress updates
|
|
49663
|
+
* without polling.
|
|
49664
|
+
*/
|
|
49665
|
+
runtimeState: MediaPlayerStatusSchema,
|
|
49666
|
+
/**
|
|
49667
|
+
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
49668
|
+
*
|
|
49669
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
49670
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
49671
|
+
*/
|
|
49672
|
+
durability: "session",
|
|
49673
|
+
/**
|
|
49674
|
+
* History (D850): volume (a level an operator or an automation sets; the
|
|
49675
|
+
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
49676
|
+
* `position` is not recorded: it moves continuously while playing.
|
|
49677
|
+
*/
|
|
49678
|
+
history: { fields: {
|
|
49679
|
+
volumeLevel: {
|
|
49680
|
+
kind: "gauge",
|
|
49681
|
+
unit: "%",
|
|
49682
|
+
deadband: 1
|
|
49683
|
+
},
|
|
49684
|
+
isMuted: { kind: "boolean" }
|
|
49685
|
+
} }
|
|
49686
|
+
};
|
|
48384
49687
|
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;
|
|
48385
49688
|
var NUMBER = {
|
|
48386
49689
|
kind: "fixed",
|
|
@@ -48828,117 +50131,6 @@ function actuator(input) {
|
|
|
48828
50131
|
};
|
|
48829
50132
|
}
|
|
48830
50133
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
48831
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
48832
|
-
"no-power-source",
|
|
48833
|
-
"switch-missing",
|
|
48834
|
-
"switch-offline",
|
|
48835
|
-
"switch-unknown",
|
|
48836
|
-
"restart-in-progress",
|
|
48837
|
-
"switch-call-failed"
|
|
48838
|
-
]);
|
|
48839
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
48840
|
-
accepted: literal(false),
|
|
48841
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
48842
|
-
/** One line for the operator, naming the switch. */
|
|
48843
|
-
message: string()
|
|
48844
|
-
})]);
|
|
48845
|
-
var PowerSourceInfoSchema = object({
|
|
48846
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
48847
|
-
linked: boolean(),
|
|
48848
|
-
switchDeviceId: number().nullable(),
|
|
48849
|
-
switchName: string().nullable(),
|
|
48850
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
48851
|
-
switchOn: boolean().nullable(),
|
|
48852
|
-
switchOnline: boolean().nullable(),
|
|
48853
|
-
restartInProgress: boolean(),
|
|
48854
|
-
offSeconds: number().int()
|
|
48855
|
-
});
|
|
48856
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
48857
|
-
var PowerSourceStateSchema = object({
|
|
48858
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
48859
|
-
switchOn: boolean().nullable(),
|
|
48860
|
-
offSeconds: number().int(),
|
|
48861
|
-
lastFetchedAt: number()
|
|
48862
|
-
});
|
|
48863
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
48864
|
-
DeviceType.Gateway,
|
|
48865
|
-
DeviceType.Hub,
|
|
48866
|
-
DeviceType.Camera,
|
|
48867
|
-
DeviceType.Sensor,
|
|
48868
|
-
DeviceType.MediaPlayer,
|
|
48869
|
-
DeviceType.Vacuum,
|
|
48870
|
-
DeviceType.Generic,
|
|
48871
|
-
DeviceType.Washer,
|
|
48872
|
-
DeviceType.Dryer,
|
|
48873
|
-
DeviceType.Dishwasher,
|
|
48874
|
-
DeviceType.PetFountain,
|
|
48875
|
-
DeviceType.Fan,
|
|
48876
|
-
DeviceType.Humidifier,
|
|
48877
|
-
DeviceType.Climate,
|
|
48878
|
-
DeviceType.WaterHeater
|
|
48879
|
-
];
|
|
48880
|
-
var powerSourceCustomize = {
|
|
48881
|
-
version: 1,
|
|
48882
|
-
targets: [{
|
|
48883
|
-
id: "switch",
|
|
48884
|
-
cardinality: "scalar",
|
|
48885
|
-
vocabulary: {
|
|
48886
|
-
cap: "power-source",
|
|
48887
|
-
stateField: "switchOn"
|
|
48888
|
-
},
|
|
48889
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
48890
|
-
apply: "add",
|
|
48891
|
-
path: "switchOn",
|
|
48892
|
-
sources: [{
|
|
48893
|
-
kind: "switch",
|
|
48894
|
-
transforms: [{ kind: "identity" }]
|
|
48895
|
-
}],
|
|
48896
|
-
companions: [{
|
|
48897
|
-
kind: "constant",
|
|
48898
|
-
path: "offSeconds",
|
|
48899
|
-
value: 10,
|
|
48900
|
-
when: "add"
|
|
48901
|
-
}, {
|
|
48902
|
-
kind: "clock",
|
|
48903
|
-
path: "lastFetchedAt",
|
|
48904
|
-
when: "always"
|
|
48905
|
-
}],
|
|
48906
|
-
sourceReport: { status: "assignment-status" }
|
|
48907
|
-
}]
|
|
48908
|
-
};
|
|
48909
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
48910
|
-
var powerSourceCapability = {
|
|
48911
|
-
name: "power-source",
|
|
48912
|
-
scope: "device",
|
|
48913
|
-
mode: "singleton",
|
|
48914
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
48915
|
-
methods: {
|
|
48916
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
48917
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48918
|
-
kind: "mutation",
|
|
48919
|
-
auth: "admin"
|
|
48920
|
-
}),
|
|
48921
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48922
|
-
kind: "mutation",
|
|
48923
|
-
auth: "admin"
|
|
48924
|
-
}),
|
|
48925
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48926
|
-
kind: "mutation",
|
|
48927
|
-
auth: "admin"
|
|
48928
|
-
})
|
|
48929
|
-
},
|
|
48930
|
-
runtimeState: PowerSourceStateSchema,
|
|
48931
|
-
customize: powerSourceCustomize,
|
|
48932
|
-
/**
|
|
48933
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
48934
|
-
* its next report; a restored value would outlive the reading it names.
|
|
48935
|
-
*
|
|
48936
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
48937
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
48938
|
-
*/
|
|
48939
|
-
durability: "session",
|
|
48940
|
-
history: "none"
|
|
48941
|
-
};
|
|
48942
50134
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
48943
50135
|
DeviceType.Camera;
|
|
48944
50136
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -48966,6 +50158,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
48966
50158
|
}).optional()
|
|
48967
50159
|
})) });
|
|
48968
50160
|
/**
|
|
50161
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
50162
|
+
* `z.infer`, they do not restate the shape.
|
|
50163
|
+
*/
|
|
50164
|
+
var OverrideSourceSchema = object({
|
|
50165
|
+
addonId: string().min(1),
|
|
50166
|
+
stableId: string().min(1),
|
|
50167
|
+
cap: string().min(1),
|
|
50168
|
+
fieldPath: string().min(1)
|
|
50169
|
+
});
|
|
50170
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
50171
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
50172
|
+
var ThresholdTransformDraftSchema = object({
|
|
50173
|
+
kind: literal("threshold"),
|
|
50174
|
+
comparison: _enum(["gte", "lte"]),
|
|
50175
|
+
threshold: number(),
|
|
50176
|
+
startDwellMs: number().nonnegative(),
|
|
50177
|
+
stopDwellMs: number().nonnegative()
|
|
50178
|
+
});
|
|
50179
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50180
|
+
IdentityTransformDraftSchema,
|
|
50181
|
+
InvertTransformDraftSchema,
|
|
50182
|
+
ThresholdTransformDraftSchema,
|
|
50183
|
+
object({
|
|
50184
|
+
kind: literal("linear"),
|
|
50185
|
+
scale: number(),
|
|
50186
|
+
offset: number(),
|
|
50187
|
+
clampMin: number().optional(),
|
|
50188
|
+
clampMax: number().optional()
|
|
50189
|
+
}),
|
|
50190
|
+
object({
|
|
50191
|
+
kind: literal("enum-map"),
|
|
50192
|
+
mapping: record(string(), string().nullable())
|
|
50193
|
+
}),
|
|
50194
|
+
object({ kind: literal("number") })
|
|
50195
|
+
]);
|
|
50196
|
+
var ScalarOverrideDraftSchema = object({
|
|
50197
|
+
kind: literal("scalar"),
|
|
50198
|
+
cap: string().min(1),
|
|
50199
|
+
targetId: string().min(1),
|
|
50200
|
+
source: OverrideSourceSchema,
|
|
50201
|
+
transform: CustomizeTransformDraftSchema
|
|
50202
|
+
});
|
|
50203
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
50204
|
+
kind: literal("keyedArray"),
|
|
50205
|
+
cap: string().min(1),
|
|
50206
|
+
targetId: string().min(1),
|
|
50207
|
+
key: string().min(1),
|
|
50208
|
+
label: string().min(1),
|
|
50209
|
+
fieldPath: string().min(1),
|
|
50210
|
+
source: OverrideSourceSchema,
|
|
50211
|
+
transform: CustomizeTransformDraftSchema
|
|
50212
|
+
});
|
|
50213
|
+
var PulseOverrideDraftSchema = object({
|
|
50214
|
+
kind: literal("pulse"),
|
|
50215
|
+
cap: string().min(1),
|
|
50216
|
+
targetId: string().min(1),
|
|
50217
|
+
enabled: boolean(),
|
|
50218
|
+
debounceMs: number().nonnegative(),
|
|
50219
|
+
edges: array(_enum([
|
|
50220
|
+
"rising",
|
|
50221
|
+
"button",
|
|
50222
|
+
"native-event"
|
|
50223
|
+
])).min(1).optional(),
|
|
50224
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
50225
|
+
});
|
|
50226
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
50227
|
+
"in-addition",
|
|
50228
|
+
"confirm",
|
|
50229
|
+
"replace"
|
|
50230
|
+
]).optional() });
|
|
50231
|
+
discriminatedUnion("kind", [
|
|
50232
|
+
ScalarOverrideDraftSchema,
|
|
50233
|
+
KeyedArrayOverrideDraftSchema,
|
|
50234
|
+
PulseOverrideDraftSchema,
|
|
50235
|
+
object({
|
|
50236
|
+
kind: literal("span"),
|
|
50237
|
+
cap: string().min(1),
|
|
50238
|
+
targetId: string().min(1),
|
|
50239
|
+
timeoutMs: number().nonnegative(),
|
|
50240
|
+
cooldownMs: number().nonnegative().optional(),
|
|
50241
|
+
sources: array(SpanSourceDraftSchema)
|
|
50242
|
+
})
|
|
50243
|
+
]);
|
|
50244
|
+
object({
|
|
50245
|
+
targetId: string().min(1),
|
|
50246
|
+
configured: boolean(),
|
|
50247
|
+
source: _enum(["native", "assigned"]),
|
|
50248
|
+
state: _enum([
|
|
50249
|
+
"active",
|
|
50250
|
+
"stale",
|
|
50251
|
+
"released",
|
|
50252
|
+
"unavailable"
|
|
50253
|
+
]),
|
|
50254
|
+
reason: string().nullable()
|
|
50255
|
+
});
|
|
50256
|
+
/**
|
|
50257
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
50258
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
50259
|
+
*
|
|
50260
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
50261
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
50262
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
50263
|
+
*/
|
|
50264
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
50265
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
50266
|
+
"high",
|
|
50267
|
+
"medium",
|
|
50268
|
+
"low"
|
|
50269
|
+
]);
|
|
50270
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
50271
|
+
"no-history",
|
|
50272
|
+
"span-too-short",
|
|
50273
|
+
"truncated",
|
|
50274
|
+
"no-idle-floor",
|
|
50275
|
+
"no-runs",
|
|
50276
|
+
"no-separation"
|
|
50277
|
+
]);
|
|
50278
|
+
var RunningProposalReplaySchema = object({
|
|
50279
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
50280
|
+
intervals: number().int().nonnegative(),
|
|
50281
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
50282
|
+
extra: number().int().nonnegative(),
|
|
50283
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
50284
|
+
missed: number().int().nonnegative()
|
|
50285
|
+
});
|
|
50286
|
+
var RunningProposalEvidenceSchema = object({
|
|
50287
|
+
tier: RunningProposalTierSchema,
|
|
50288
|
+
spanMs: number().nonnegative(),
|
|
50289
|
+
from: number(),
|
|
50290
|
+
to: number(),
|
|
50291
|
+
points: number().int().nonnegative(),
|
|
50292
|
+
runsFound: number().int().nonnegative(),
|
|
50293
|
+
pausesFound: number().int().nonnegative(),
|
|
50294
|
+
longestPauseMs: number().nonnegative(),
|
|
50295
|
+
idleFloorW: number(),
|
|
50296
|
+
noiseCeilingW: number(),
|
|
50297
|
+
plateauW: number(),
|
|
50298
|
+
replay: RunningProposalReplaySchema
|
|
50299
|
+
});
|
|
50300
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
50301
|
+
spanMs: number().nonnegative(),
|
|
50302
|
+
points: number().int().nonnegative(),
|
|
50303
|
+
runsFound: number().int().nonnegative()
|
|
50304
|
+
});
|
|
50305
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
50306
|
+
state: literal("proposal"),
|
|
50307
|
+
transform: ThresholdTransformDraftSchema,
|
|
50308
|
+
evidence: RunningProposalEvidenceSchema,
|
|
50309
|
+
confidence: RunningProposalConfidenceSchema,
|
|
50310
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
50311
|
+
notes: array(string())
|
|
50312
|
+
}), object({
|
|
50313
|
+
state: literal("insufficient"),
|
|
50314
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
50315
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
50316
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
50317
|
+
fallback: ThresholdTransformDraftSchema
|
|
50318
|
+
})]);
|
|
50319
|
+
/**
|
|
48969
50320
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
48970
50321
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
48971
50322
|
*
|
|
@@ -49255,6 +50606,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
49255
50606
|
kind: "mutation",
|
|
49256
50607
|
auth: "admin",
|
|
49257
50608
|
access: "view"
|
|
50609
|
+
}), method(object({
|
|
50610
|
+
deviceId: number().int(),
|
|
50611
|
+
cap: string().min(1),
|
|
50612
|
+
fieldPath: string().min(1),
|
|
50613
|
+
deviceType: _enum(DeviceType).optional()
|
|
50614
|
+
}), RunningThresholdProposalSchema, {
|
|
50615
|
+
auth: "admin",
|
|
50616
|
+
access: "view"
|
|
49258
50617
|
}), method(object({
|
|
49259
50618
|
blockId: string().min(1),
|
|
49260
50619
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -50231,7 +51590,27 @@ method(object({
|
|
|
50231
51590
|
* exposes only the COMMON surface: link state, supported device
|
|
50232
51591
|
* kinds (so the UI can filter the device picker), and per-device
|
|
50233
51592
|
* expose/unexpose.
|
|
50234
|
-
|
|
51593
|
+
*
|
|
51594
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
51595
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
51596
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
51597
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
51598
|
+
* default instance's); present = that instance; an id the provider does not
|
|
51599
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
51600
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
51601
|
+
* change always names the target it changes. A single-instance provider
|
|
51602
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
51603
|
+
*/
|
|
51604
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
51605
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
51606
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
51607
|
+
/** One independent export target of an addon. */
|
|
51608
|
+
var ExportInstanceSchema = object({
|
|
51609
|
+
instanceId: InstanceIdSchema$1,
|
|
51610
|
+
name: string(),
|
|
51611
|
+
enabled: boolean(),
|
|
51612
|
+
kind: string().optional()
|
|
51613
|
+
});
|
|
50235
51614
|
var LinkStateSchema = _enum([
|
|
50236
51615
|
"unlinked",
|
|
50237
51616
|
"linked",
|
|
@@ -50282,10 +51661,14 @@ var ExposedDeviceSchema = object({
|
|
|
50282
51661
|
capabilities: array(string()).optional()
|
|
50283
51662
|
});
|
|
50284
51663
|
var ExposeInputSchema = object({
|
|
51664
|
+
instanceId: InstanceIdSchema$1,
|
|
50285
51665
|
deviceId: string(),
|
|
50286
51666
|
capabilities: array(string()).optional()
|
|
50287
51667
|
});
|
|
50288
|
-
var UnexposeInputSchema = object({
|
|
51668
|
+
var UnexposeInputSchema = object({
|
|
51669
|
+
instanceId: InstanceIdSchema$1,
|
|
51670
|
+
deviceId: string()
|
|
51671
|
+
});
|
|
50289
51672
|
/**
|
|
50290
51673
|
* What an exporter may expose besides devices (D968). An action and a
|
|
50291
51674
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -50314,15 +51697,21 @@ var ExposedEntitySchema = object({
|
|
|
50314
51697
|
entity: ExportEntityRefSchema,
|
|
50315
51698
|
exposedAs: string().optional()
|
|
50316
51699
|
});
|
|
50317
|
-
var ExposeEntityInputSchema = object({
|
|
50318
|
-
|
|
50319
|
-
|
|
51700
|
+
var ExposeEntityInputSchema = object({
|
|
51701
|
+
instanceId: InstanceIdSchema$1,
|
|
51702
|
+
entity: ExportEntityRefSchema
|
|
51703
|
+
});
|
|
51704
|
+
var UnexposeEntityInputSchema = object({
|
|
51705
|
+
instanceId: InstanceIdSchema$1,
|
|
51706
|
+
entity: ExportEntityRefSchema
|
|
51707
|
+
});
|
|
51708
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
50320
51709
|
kind: "mutation",
|
|
50321
51710
|
auth: "admin"
|
|
50322
51711
|
}), method(UnexposeInputSchema, _void(), {
|
|
50323
51712
|
kind: "mutation",
|
|
50324
51713
|
auth: "admin"
|
|
50325
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
51714
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
50326
51715
|
kind: "mutation",
|
|
50327
51716
|
auth: "admin"
|
|
50328
51717
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -52324,7 +53713,7 @@ var DishwasherStatusSchema = object({
|
|
|
52324
53713
|
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
52325
53714
|
lastFetchedAt: number()
|
|
52326
53715
|
});
|
|
52327
|
-
var DeviceIdSchema$
|
|
53716
|
+
var DeviceIdSchema$2 = number().int().nonnegative();
|
|
52328
53717
|
/** A value a program option or setting can take. */
|
|
52329
53718
|
var DishwasherValueSchema = union([
|
|
52330
53719
|
boolean(),
|
|
@@ -52399,77 +53788,77 @@ var dishwasherCapability = {
|
|
|
52399
53788
|
* option feature keys; each is checked against the program's constraints.
|
|
52400
53789
|
*/
|
|
52401
53790
|
start: method(object({
|
|
52402
|
-
deviceId: DeviceIdSchema$
|
|
53791
|
+
deviceId: DeviceIdSchema$2,
|
|
52403
53792
|
program: string().min(1).optional(),
|
|
52404
53793
|
options: record(string(), DishwasherValueSchema).optional()
|
|
52405
53794
|
}), _void(), {
|
|
52406
53795
|
kind: "mutation",
|
|
52407
53796
|
auth: "protected"
|
|
52408
53797
|
}),
|
|
52409
|
-
pause: method(object({ deviceId: DeviceIdSchema$
|
|
53798
|
+
pause: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52410
53799
|
kind: "mutation",
|
|
52411
53800
|
auth: "protected"
|
|
52412
53801
|
}),
|
|
52413
|
-
resume: method(object({ deviceId: DeviceIdSchema$
|
|
53802
|
+
resume: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52414
53803
|
kind: "mutation",
|
|
52415
53804
|
auth: "protected"
|
|
52416
53805
|
}),
|
|
52417
|
-
stop: method(object({ deviceId: DeviceIdSchema$
|
|
53806
|
+
stop: method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
52418
53807
|
kind: "mutation",
|
|
52419
53808
|
auth: "protected"
|
|
52420
53809
|
}),
|
|
52421
53810
|
/** Select (not start) a program; refused when the appliance does not offer it. */
|
|
52422
53811
|
setProgram: method(object({
|
|
52423
|
-
deviceId: DeviceIdSchema$
|
|
53812
|
+
deviceId: DeviceIdSchema$2,
|
|
52424
53813
|
program: string().min(1)
|
|
52425
53814
|
}), _void(), {
|
|
52426
53815
|
kind: "mutation",
|
|
52427
53816
|
auth: "protected"
|
|
52428
53817
|
}),
|
|
52429
53818
|
setPower: method(object({
|
|
52430
|
-
deviceId: DeviceIdSchema$
|
|
53819
|
+
deviceId: DeviceIdSchema$2,
|
|
52431
53820
|
on: boolean()
|
|
52432
53821
|
}), _void(), {
|
|
52433
53822
|
kind: "mutation",
|
|
52434
53823
|
auth: "protected"
|
|
52435
53824
|
}),
|
|
52436
53825
|
setHalfLoad: method(object({
|
|
52437
|
-
deviceId: DeviceIdSchema$
|
|
53826
|
+
deviceId: DeviceIdSchema$2,
|
|
52438
53827
|
on: boolean()
|
|
52439
53828
|
}), _void(), {
|
|
52440
53829
|
kind: "mutation",
|
|
52441
53830
|
auth: "protected"
|
|
52442
53831
|
}),
|
|
52443
53832
|
setHygienePlus: method(object({
|
|
52444
|
-
deviceId: DeviceIdSchema$
|
|
53833
|
+
deviceId: DeviceIdSchema$2,
|
|
52445
53834
|
on: boolean()
|
|
52446
53835
|
}), _void(), {
|
|
52447
53836
|
kind: "mutation",
|
|
52448
53837
|
auth: "protected"
|
|
52449
53838
|
}),
|
|
52450
53839
|
setExtraDry: method(object({
|
|
52451
|
-
deviceId: DeviceIdSchema$
|
|
53840
|
+
deviceId: DeviceIdSchema$2,
|
|
52452
53841
|
on: boolean()
|
|
52453
53842
|
}), _void(), {
|
|
52454
53843
|
kind: "mutation",
|
|
52455
53844
|
auth: "protected"
|
|
52456
53845
|
}),
|
|
52457
53846
|
setIntensiveZone: method(object({
|
|
52458
|
-
deviceId: DeviceIdSchema$
|
|
53847
|
+
deviceId: DeviceIdSchema$2,
|
|
52459
53848
|
on: boolean()
|
|
52460
53849
|
}), _void(), {
|
|
52461
53850
|
kind: "mutation",
|
|
52462
53851
|
auth: "protected"
|
|
52463
53852
|
}),
|
|
52464
53853
|
setVarioSpeedPlus: method(object({
|
|
52465
|
-
deviceId: DeviceIdSchema$
|
|
53854
|
+
deviceId: DeviceIdSchema$2,
|
|
52466
53855
|
on: boolean()
|
|
52467
53856
|
}), _void(), {
|
|
52468
53857
|
kind: "mutation",
|
|
52469
53858
|
auth: "protected"
|
|
52470
53859
|
}),
|
|
52471
53860
|
setChildLock: method(object({
|
|
52472
|
-
deviceId: DeviceIdSchema$
|
|
53861
|
+
deviceId: DeviceIdSchema$2,
|
|
52473
53862
|
on: boolean()
|
|
52474
53863
|
}), _void(), {
|
|
52475
53864
|
kind: "mutation",
|
|
@@ -52481,7 +53870,7 @@ var dishwasherCapability = {
|
|
|
52481
53870
|
* its declared range / members.
|
|
52482
53871
|
*/
|
|
52483
53872
|
setOption: method(object({
|
|
52484
|
-
deviceId: DeviceIdSchema$
|
|
53873
|
+
deviceId: DeviceIdSchema$2,
|
|
52485
53874
|
key: string().min(1),
|
|
52486
53875
|
value: DishwasherValueSchema
|
|
52487
53876
|
}), _void(), {
|
|
@@ -52489,7 +53878,7 @@ var dishwasherCapability = {
|
|
|
52489
53878
|
auth: "protected"
|
|
52490
53879
|
}),
|
|
52491
53880
|
/** What this appliance can do, from its own description — the UI is derived from it. */
|
|
52492
|
-
getOptions: method(object({ deviceId: DeviceIdSchema$
|
|
53881
|
+
getOptions: method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" })
|
|
52493
53882
|
},
|
|
52494
53883
|
status: {
|
|
52495
53884
|
schema: DishwasherStatusSchema,
|
|
@@ -53913,15 +55302,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
53913
55302
|
items: array(FanCaptureItemSchema),
|
|
53914
55303
|
omitted: array(FanCaptureOmissionSchema)
|
|
53915
55304
|
});
|
|
53916
|
-
var deviceInput$
|
|
55305
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
53917
55306
|
DeviceType.Fan;
|
|
53918
|
-
method(deviceInput$
|
|
55307
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
53919
55308
|
kind: "mutation",
|
|
53920
55309
|
auth: "admin"
|
|
53921
|
-
}), method(deviceInput$
|
|
55310
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
53922
55311
|
kind: "mutation",
|
|
53923
55312
|
auth: "protected"
|
|
53924
|
-
}), method(deviceInput$
|
|
55313
|
+
}), method(deviceInput$2.extend({
|
|
53925
55314
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
53926
55315
|
steps: array(ActionStepSchema).max(20).default([])
|
|
53927
55316
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -54804,6 +56193,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54804
56193
|
}), object({ success: boolean() }), {
|
|
54805
56194
|
kind: "mutation",
|
|
54806
56195
|
auth: "admin"
|
|
56196
|
+
}), method(object({ id: string() }), object({
|
|
56197
|
+
rows: number().int().nonnegative(),
|
|
56198
|
+
secrets: number().int().nonnegative()
|
|
56199
|
+
}), {
|
|
56200
|
+
kind: "mutation",
|
|
56201
|
+
auth: "admin"
|
|
54807
56202
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54808
56203
|
addonId: string(),
|
|
54809
56204
|
settings: record(string(), unknown())
|
|
@@ -54832,13 +56227,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54832
56227
|
*/
|
|
54833
56228
|
var ConfigSchemaPassthrough$3 = custom();
|
|
54834
56229
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
56230
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56231
|
+
/**
|
|
56232
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
56233
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
56234
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
56235
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
56236
|
+
* instance id.
|
|
56237
|
+
*/
|
|
56238
|
+
var IntegrationInstanceKindSchema = object({
|
|
56239
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
56240
|
+
id: string().min(1),
|
|
56241
|
+
label: string().min(1),
|
|
56242
|
+
/** The kind signs in through a server-held interactive session. */
|
|
56243
|
+
signIn: object({
|
|
56244
|
+
basePath: string().min(1),
|
|
56245
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
56246
|
+
}).optional(),
|
|
56247
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
56248
|
+
connectPath: string().min(1).optional(),
|
|
56249
|
+
/** Relative hub path to disconnect that account. */
|
|
56250
|
+
disconnectPath: string().min(1).optional()
|
|
56251
|
+
});
|
|
54835
56252
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56253
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56254
|
+
role: _enum([
|
|
56255
|
+
"devices",
|
|
56256
|
+
"services",
|
|
56257
|
+
"both"
|
|
56258
|
+
]),
|
|
56259
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56260
|
+
discovery: object({
|
|
56261
|
+
mode: _enum([
|
|
56262
|
+
"none",
|
|
56263
|
+
"lan",
|
|
56264
|
+
"account"
|
|
56265
|
+
]),
|
|
56266
|
+
scopedToInstance: boolean()
|
|
56267
|
+
}).optional(),
|
|
54836
56268
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
54837
|
-
instanceMode:
|
|
56269
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
54838
56270
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
54839
56271
|
kind: string(),
|
|
54840
56272
|
/** True when `testInstance` really probes the remote. */
|
|
54841
|
-
canTest: boolean()
|
|
56273
|
+
canTest: boolean(),
|
|
56274
|
+
/**
|
|
56275
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
56276
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
56277
|
+
*/
|
|
56278
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
56279
|
+
/**
|
|
56280
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
56281
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
56282
|
+
*/
|
|
56283
|
+
allowRestoreId: boolean().optional(),
|
|
56284
|
+
/**
|
|
56285
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
56286
|
+
* send an id (the UI then mints an opaque one).
|
|
56287
|
+
*/
|
|
56288
|
+
mintsId: boolean().optional()
|
|
54842
56289
|
});
|
|
54843
56290
|
var IntegrationInstanceStatusSchema = object({
|
|
54844
56291
|
state: _enum([
|
|
@@ -54847,16 +56294,28 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
54847
56294
|
"error",
|
|
54848
56295
|
"unknown"
|
|
54849
56296
|
]),
|
|
54850
|
-
detail: string().optional()
|
|
56297
|
+
detail: string().optional(),
|
|
56298
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
56299
|
+
label: string().optional()
|
|
54851
56300
|
});
|
|
54852
56301
|
var IntegrationInstanceSchema = object({
|
|
54853
56302
|
id: IntegrationInstanceIdSchema,
|
|
54854
56303
|
name: string(),
|
|
54855
56304
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
54856
56305
|
configured: boolean(),
|
|
56306
|
+
/**
|
|
56307
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56308
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56309
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56310
|
+
*/
|
|
56311
|
+
enabled: boolean().optional(),
|
|
56312
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56313
|
+
kind: string().optional(),
|
|
54857
56314
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
54858
56315
|
formValues: record(string(), unknown()),
|
|
54859
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
56316
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
56317
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
56318
|
+
lastCheckedAt: number().optional()
|
|
54860
56319
|
});
|
|
54861
56320
|
var IntegrationInstancePatchSchema = object({
|
|
54862
56321
|
id: IntegrationInstanceIdSchema,
|
|
@@ -54864,25 +56323,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
54864
56323
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
54865
56324
|
values: record(string(), unknown())
|
|
54866
56325
|
});
|
|
54867
|
-
|
|
54868
|
-
|
|
54869
|
-
|
|
54870
|
-
|
|
54871
|
-
|
|
54872
|
-
|
|
54873
|
-
|
|
54874
|
-
|
|
54875
|
-
|
|
54876
|
-
|
|
54877
|
-
|
|
54878
|
-
|
|
54879
|
-
|
|
54880
|
-
|
|
54881
|
-
|
|
54882
|
-
|
|
54883
|
-
|
|
54884
|
-
|
|
54885
|
-
})
|
|
56326
|
+
var integrationInstancesCapability = {
|
|
56327
|
+
name: "integration-instances",
|
|
56328
|
+
scope: "system",
|
|
56329
|
+
mode: "collection",
|
|
56330
|
+
methods: {
|
|
56331
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
56332
|
+
/** The form; `id: null` is the create form. */
|
|
56333
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
56334
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
56335
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
56336
|
+
/**
|
|
56337
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
56338
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
56339
|
+
*/
|
|
56340
|
+
createInstance: method(object({
|
|
56341
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
56342
|
+
name: string().min(1),
|
|
56343
|
+
kind: string().min(1).optional()
|
|
56344
|
+
}), object({ id: string() }), {
|
|
56345
|
+
kind: "mutation",
|
|
56346
|
+
auth: "admin"
|
|
56347
|
+
}),
|
|
56348
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
56349
|
+
kind: "mutation",
|
|
56350
|
+
auth: "admin"
|
|
56351
|
+
}),
|
|
56352
|
+
/**
|
|
56353
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
56354
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56355
|
+
*/
|
|
56356
|
+
setInstanceEnabled: method(object({
|
|
56357
|
+
id: IntegrationInstanceIdSchema,
|
|
56358
|
+
enabled: boolean()
|
|
56359
|
+
}), object({ ok: literal(true) }), {
|
|
56360
|
+
kind: "mutation",
|
|
56361
|
+
auth: "admin"
|
|
56362
|
+
}),
|
|
56363
|
+
/** Delete the record and its keychain refs together. */
|
|
56364
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
56365
|
+
kind: "mutation",
|
|
56366
|
+
auth: "admin"
|
|
56367
|
+
}),
|
|
56368
|
+
testInstance: optionalMethod(object({
|
|
56369
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
56370
|
+
values: record(string(), unknown())
|
|
56371
|
+
}), TestConnectionResultSchema$1, {
|
|
56372
|
+
kind: "mutation",
|
|
56373
|
+
auth: "admin"
|
|
56374
|
+
})
|
|
56375
|
+
}
|
|
56376
|
+
};
|
|
54886
56377
|
/**
|
|
54887
56378
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
54888
56379
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -56900,6 +58391,11 @@ var LogEntrySchema = object({
|
|
|
56900
58391
|
* flight, and not asking for the next slice ends the scan.
|
|
56901
58392
|
*/
|
|
56902
58393
|
var LogReadQuerySchema = object({
|
|
58394
|
+
/**
|
|
58395
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
58396
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
58397
|
+
*/
|
|
58398
|
+
instanceId: string().min(1).max(200).optional(),
|
|
56903
58399
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
56904
58400
|
requestId: string().min(1).max(64).optional(),
|
|
56905
58401
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -56962,6 +58458,8 @@ var LogReadResultSchema = object({
|
|
|
56962
58458
|
/** True when `cancelRead` ended this slice early. */
|
|
56963
58459
|
cancelled: boolean().optional()
|
|
56964
58460
|
});
|
|
58461
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
58462
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
56965
58463
|
var LogReadCapabilitiesSchema = object({
|
|
56966
58464
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
56967
58465
|
readable: boolean(),
|
|
@@ -56980,7 +58478,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
56980
58478
|
/** How far back the store reaches, when the provider knows (days). */
|
|
56981
58479
|
retentionDays: number().nullable()
|
|
56982
58480
|
});
|
|
56983
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
58481
|
+
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() }), {
|
|
56984
58482
|
kind: "mutation",
|
|
56985
58483
|
auth: "admin"
|
|
56986
58484
|
});
|
|
@@ -57084,214 +58582,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
57084
58582
|
]);
|
|
57085
58583
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
57086
58584
|
/**
|
|
57087
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
57088
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
57089
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
57090
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
57091
|
-
*
|
|
57092
|
-
* The cap declares a wide method surface; not every backend supports
|
|
57093
|
-
* every method. Drivers signal sub-capability via the
|
|
57094
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
57095
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
57096
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
57097
|
-
*/
|
|
57098
|
-
var MediaPlayerStateSchema = _enum([
|
|
57099
|
-
"off",
|
|
57100
|
-
"on",
|
|
57101
|
-
"idle",
|
|
57102
|
-
"playing",
|
|
57103
|
-
"paused",
|
|
57104
|
-
"buffering",
|
|
57105
|
-
"standby"
|
|
57106
|
-
]);
|
|
57107
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
57108
|
-
"off",
|
|
57109
|
-
"all",
|
|
57110
|
-
"one"
|
|
57111
|
-
]);
|
|
57112
|
-
var MediaInfoSchema = object({
|
|
57113
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
57114
|
-
* `podcast`, …). */
|
|
57115
|
-
type: string(),
|
|
57116
|
-
/** Human-readable title. */
|
|
57117
|
-
title: string(),
|
|
57118
|
-
/** Optional artist / channel / station label. */
|
|
57119
|
-
artist: string().optional(),
|
|
57120
|
-
/** Optional album / season / show name. */
|
|
57121
|
-
album: string().optional(),
|
|
57122
|
-
/** Optional cover-art / thumbnail URL. */
|
|
57123
|
-
imageUrl: string().optional()
|
|
57124
|
-
});
|
|
57125
|
-
/**
|
|
57126
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
57127
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
57128
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
57129
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
57130
|
-
* guess, never as a reading.
|
|
57131
|
-
*/
|
|
57132
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
57133
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
57134
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
57135
|
-
"state",
|
|
57136
|
-
"volumeLevel",
|
|
57137
|
-
"isMuted",
|
|
57138
|
-
"source"
|
|
57139
|
-
]);
|
|
57140
|
-
var MediaPlayerStatusSchema = object({
|
|
57141
|
-
/** Playback lifecycle state. */
|
|
57142
|
-
state: MediaPlayerStateSchema,
|
|
57143
|
-
/**
|
|
57144
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
57145
|
-
* A slice written before this field existed parses as `reported`.
|
|
57146
|
-
*/
|
|
57147
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
57148
|
-
/**
|
|
57149
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
57150
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
57151
|
-
* from a running-state source, mute guessed from the last send).
|
|
57152
|
-
*/
|
|
57153
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
57154
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
57155
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
57156
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
57157
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
57158
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
57159
|
-
isMuted: boolean().nullable(),
|
|
57160
|
-
/** Active source / input. Empty when no source surface. */
|
|
57161
|
-
source: string(),
|
|
57162
|
-
/** Selectable sources. Empty when no source surface.
|
|
57163
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
57164
|
-
availableSources: array(string()),
|
|
57165
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
57166
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
57167
|
-
/** Current playback position in ms. Null when not seekable or
|
|
57168
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
57169
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
57170
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
57171
|
-
* (live stream) or nothing is playing. */
|
|
57172
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
57173
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
57174
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
57175
|
-
shuffle: boolean().nullable(),
|
|
57176
|
-
/** Repeat mode. Null when no repeat surface.
|
|
57177
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
57178
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
57179
|
-
/** Ms epoch when the slice was last updated. */
|
|
57180
|
-
lastChangedAt: number()
|
|
57181
|
-
});
|
|
57182
|
-
var mediaPlayerCapability = {
|
|
57183
|
-
name: "media-player",
|
|
57184
|
-
scope: "device",
|
|
57185
|
-
deviceNative: true,
|
|
57186
|
-
mode: "singleton",
|
|
57187
|
-
deviceTypes: [DeviceType.MediaPlayer],
|
|
57188
|
-
methods: {
|
|
57189
|
-
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57190
|
-
kind: "mutation",
|
|
57191
|
-
auth: "protected"
|
|
57192
|
-
}),
|
|
57193
|
-
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57194
|
-
kind: "mutation",
|
|
57195
|
-
auth: "protected"
|
|
57196
|
-
}),
|
|
57197
|
-
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57198
|
-
kind: "mutation",
|
|
57199
|
-
auth: "protected"
|
|
57200
|
-
}),
|
|
57201
|
-
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57202
|
-
kind: "mutation",
|
|
57203
|
-
auth: "protected"
|
|
57204
|
-
}),
|
|
57205
|
-
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
57206
|
-
kind: "mutation",
|
|
57207
|
-
auth: "protected"
|
|
57208
|
-
}),
|
|
57209
|
-
seek: method(object({
|
|
57210
|
-
deviceId: number().int().nonnegative(),
|
|
57211
|
-
position: number().int().nonnegative()
|
|
57212
|
-
}), _void(), {
|
|
57213
|
-
kind: "mutation",
|
|
57214
|
-
auth: "protected"
|
|
57215
|
-
}),
|
|
57216
|
-
setVolume: method(object({
|
|
57217
|
-
deviceId: number().int().nonnegative(),
|
|
57218
|
-
volumeLevel: number().min(0).max(100)
|
|
57219
|
-
}), _void(), {
|
|
57220
|
-
kind: "mutation",
|
|
57221
|
-
auth: "protected"
|
|
57222
|
-
}),
|
|
57223
|
-
setMute: method(object({
|
|
57224
|
-
deviceId: number().int().nonnegative(),
|
|
57225
|
-
muted: boolean()
|
|
57226
|
-
}), _void(), {
|
|
57227
|
-
kind: "mutation",
|
|
57228
|
-
auth: "protected"
|
|
57229
|
-
}),
|
|
57230
|
-
setShuffle: method(object({
|
|
57231
|
-
deviceId: number().int().nonnegative(),
|
|
57232
|
-
shuffle: boolean()
|
|
57233
|
-
}), _void(), {
|
|
57234
|
-
kind: "mutation",
|
|
57235
|
-
auth: "protected"
|
|
57236
|
-
}),
|
|
57237
|
-
setRepeat: method(object({
|
|
57238
|
-
deviceId: number().int().nonnegative(),
|
|
57239
|
-
repeat: MediaPlayerRepeatSchema
|
|
57240
|
-
}), _void(), {
|
|
57241
|
-
kind: "mutation",
|
|
57242
|
-
auth: "protected"
|
|
57243
|
-
}),
|
|
57244
|
-
selectSource: method(object({
|
|
57245
|
-
deviceId: number().int().nonnegative(),
|
|
57246
|
-
source: string().min(1)
|
|
57247
|
-
}), _void(), {
|
|
57248
|
-
kind: "mutation",
|
|
57249
|
-
auth: "protected"
|
|
57250
|
-
}),
|
|
57251
|
-
playMedia: method(object({
|
|
57252
|
-
deviceId: number().int().nonnegative(),
|
|
57253
|
-
/** Media identifier / URL. */
|
|
57254
|
-
mediaId: string().min(1),
|
|
57255
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
57256
|
-
* passes it through to the upstream service. */
|
|
57257
|
-
mediaType: string().min(1)
|
|
57258
|
-
}), _void(), {
|
|
57259
|
-
kind: "mutation",
|
|
57260
|
-
auth: "protected"
|
|
57261
|
-
})
|
|
57262
|
-
},
|
|
57263
|
-
status: {
|
|
57264
|
-
schema: MediaPlayerStatusSchema,
|
|
57265
|
-
kind: "push"
|
|
57266
|
-
},
|
|
57267
|
-
/**
|
|
57268
|
-
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
57269
|
-
* full slice for live now-playing, volume, and progress updates
|
|
57270
|
-
* without polling.
|
|
57271
|
-
*/
|
|
57272
|
-
runtimeState: MediaPlayerStatusSchema,
|
|
57273
|
-
/**
|
|
57274
|
-
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
57275
|
-
*
|
|
57276
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
57277
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
57278
|
-
*/
|
|
57279
|
-
durability: "session",
|
|
57280
|
-
/**
|
|
57281
|
-
* History (D850): volume (a level an operator or an automation sets; the
|
|
57282
|
-
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
57283
|
-
* `position` is not recorded: it moves continuously while playing.
|
|
57284
|
-
*/
|
|
57285
|
-
history: { fields: {
|
|
57286
|
-
volumeLevel: {
|
|
57287
|
-
kind: "gauge",
|
|
57288
|
-
unit: "%",
|
|
57289
|
-
deadband: 1
|
|
57290
|
-
},
|
|
57291
|
-
isMuted: { kind: "boolean" }
|
|
57292
|
-
} }
|
|
57293
|
-
};
|
|
57294
|
-
/**
|
|
57295
58585
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
57296
58586
|
*
|
|
57297
58587
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -66050,6 +67340,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
66050
67340
|
"sound",
|
|
66051
67341
|
"system"
|
|
66052
67342
|
]);
|
|
67343
|
+
/**
|
|
67344
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
67345
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
67346
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
67347
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
67348
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
67349
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
67350
|
+
* the hub's current language and with its overrides applied.
|
|
67351
|
+
*/
|
|
67352
|
+
var NcTemplateDefaultSchema = object({
|
|
67353
|
+
id: _enum(["appliance-cycle"]),
|
|
67354
|
+
kind: NcRuleKindSchema,
|
|
67355
|
+
title: string(),
|
|
67356
|
+
body: string()
|
|
67357
|
+
});
|
|
66053
67358
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
66054
67359
|
var NcTemplateFamilySchema = _enum([
|
|
66055
67360
|
"rule",
|
|
@@ -66905,7 +68210,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
66905
68210
|
}), method(object({}), object({
|
|
66906
68211
|
catalog: array(NcConditionDescriptorSchema),
|
|
66907
68212
|
taxonomy: NcTaxonomySchema.optional()
|
|
66908
|
-
})), method(object({}), object({
|
|
68213
|
+
})), method(object({}), object({
|
|
68214
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
68215
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
68216
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
68217
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
66909
68218
|
kind: "mutation",
|
|
66910
68219
|
access: "view"
|
|
66911
68220
|
}), 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 }), {
|
|
@@ -67801,7 +69110,7 @@ var petFeederCapability = {
|
|
|
67801
69110
|
/**
|
|
67802
69111
|
* Pet water-fountain cap. Models the telemetry and the control surface of a
|
|
67803
69112
|
* circulating drinking fountain: how much water is in the tank, how much the
|
|
67804
|
-
* pet drank, whether the pump runs
|
|
69113
|
+
* pet drank, whether the pump runs.
|
|
67805
69114
|
*
|
|
67806
69115
|
* It has no native vendor provider of its own: a fountain reaches CamStack as
|
|
67807
69116
|
* a container of loose entities (a pump switch, two number boxes, a "filter
|
|
@@ -67817,8 +69126,10 @@ var petFeederCapability = {
|
|
|
67817
69126
|
* never here. Every reading may be `null`: a source that is offline or does
|
|
67818
69127
|
* not report is unknown, never zero.
|
|
67819
69128
|
*
|
|
67820
|
-
* The
|
|
67821
|
-
* cap
|
|
69129
|
+
* The pump's on/off duty cycle is model-specific and lives in the optional
|
|
69130
|
+
* `pump-cycle` cap. The filter, the cleaning and the water change are not
|
|
69131
|
+
* fields of this cap: they are items of the `consumables` cap on the same
|
|
69132
|
+
* device (kind `wear`), reset by the same forward mechanism.
|
|
67822
69133
|
*/
|
|
67823
69134
|
var PetFountainStatusSchema = object({
|
|
67824
69135
|
/** Water in the tank, 0..100 (%). Null while the source has not reported. */
|
|
@@ -67829,27 +69140,20 @@ var PetFountainStatusSchema = object({
|
|
|
67829
69140
|
empty: boolean().nullable(),
|
|
67830
69141
|
/** True while the pump is running. Null while unknown. */
|
|
67831
69142
|
pumpRunning: boolean().nullable(),
|
|
67832
|
-
/** True when the pump works on an on/off duty cycle rather than continuously. */
|
|
67833
|
-
pumpIntervalEnabled: boolean().nullable(),
|
|
67834
|
-
/** How long the pump runs per cycle, in seconds. */
|
|
67835
|
-
pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
67836
|
-
/** How long the pump rests per cycle, in seconds. */
|
|
67837
|
-
pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
67838
69143
|
/** Water drunk in the last drink, in litres. */
|
|
67839
69144
|
lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67840
69145
|
/** Water drunk in the current hour, in litres. */
|
|
67841
69146
|
currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67842
69147
|
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
67843
69148
|
last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67844
|
-
/** Water drunk since local midnight, in litres. */
|
|
69149
|
+
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
67845
69150
|
todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
67846
69151
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
67847
69152
|
error: string().nullable(),
|
|
67848
69153
|
/** Ms epoch when the slice was last refreshed. */
|
|
67849
69154
|
lastFetchedAt: number()
|
|
67850
69155
|
});
|
|
67851
|
-
var DeviceIdSchema = number().int().nonnegative();
|
|
67852
|
-
var MinutesSchema = number().int().min(0);
|
|
69156
|
+
var DeviceIdSchema$1 = number().int().nonnegative();
|
|
67853
69157
|
var petFountainCapability = {
|
|
67854
69158
|
name: "pet-fountain",
|
|
67855
69159
|
scope: "device",
|
|
@@ -67859,38 +69163,14 @@ var petFountainCapability = {
|
|
|
67859
69163
|
methods: {
|
|
67860
69164
|
/** Start or stop the pump. */
|
|
67861
69165
|
setPump: method(object({
|
|
67862
|
-
deviceId: DeviceIdSchema,
|
|
67863
|
-
on: boolean()
|
|
67864
|
-
}), _void(), {
|
|
67865
|
-
kind: "mutation",
|
|
67866
|
-
auth: "protected"
|
|
67867
|
-
}),
|
|
67868
|
-
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
67869
|
-
setPumpInterval: method(object({
|
|
67870
|
-
deviceId: DeviceIdSchema,
|
|
69166
|
+
deviceId: DeviceIdSchema$1,
|
|
67871
69167
|
on: boolean()
|
|
67872
69168
|
}), _void(), {
|
|
67873
69169
|
kind: "mutation",
|
|
67874
69170
|
auth: "protected"
|
|
67875
69171
|
}),
|
|
67876
|
-
/** Set how many minutes the pump runs per cycle. */
|
|
67877
|
-
setPumpOnMinutes: method(object({
|
|
67878
|
-
deviceId: DeviceIdSchema,
|
|
67879
|
-
minutes: MinutesSchema
|
|
67880
|
-
}), _void(), {
|
|
67881
|
-
kind: "mutation",
|
|
67882
|
-
auth: "protected"
|
|
67883
|
-
}),
|
|
67884
|
-
/** Set how many minutes the pump rests per cycle. */
|
|
67885
|
-
setPumpOffMinutes: method(object({
|
|
67886
|
-
deviceId: DeviceIdSchema,
|
|
67887
|
-
minutes: MinutesSchema
|
|
67888
|
-
}), _void(), {
|
|
67889
|
-
kind: "mutation",
|
|
67890
|
-
auth: "protected"
|
|
67891
|
-
}),
|
|
67892
69172
|
/** Clear the rolling daily consumption counters on the device. */
|
|
67893
|
-
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
69173
|
+
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
67894
69174
|
kind: "mutation",
|
|
67895
69175
|
auth: "protected"
|
|
67896
69176
|
})
|
|
@@ -69525,6 +70805,14 @@ var powerMeterCapability = {
|
|
|
69525
70805
|
deviceNative: true,
|
|
69526
70806
|
mode: "singleton",
|
|
69527
70807
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
70808
|
+
deviceConfig: { ui: {
|
|
70809
|
+
kind: "widget",
|
|
70810
|
+
widgetId: "host/power-meter-line",
|
|
70811
|
+
tab: "power",
|
|
70812
|
+
placement: "hero",
|
|
70813
|
+
label: "Power",
|
|
70814
|
+
order: 4
|
|
70815
|
+
} },
|
|
69528
70816
|
methods: {},
|
|
69529
70817
|
status: {
|
|
69530
70818
|
schema: PowerMeterStatusSchema,
|
|
@@ -69584,6 +70872,117 @@ var powerMeterCapability = {
|
|
|
69584
70872
|
volatileStateFields: ["lastFetchedAt"],
|
|
69585
70873
|
customize: powerMeterCustomize
|
|
69586
70874
|
};
|
|
70875
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
70876
|
+
"no-power-source",
|
|
70877
|
+
"switch-missing",
|
|
70878
|
+
"switch-offline",
|
|
70879
|
+
"switch-unknown",
|
|
70880
|
+
"restart-in-progress",
|
|
70881
|
+
"switch-call-failed"
|
|
70882
|
+
]);
|
|
70883
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
70884
|
+
accepted: literal(false),
|
|
70885
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
70886
|
+
/** One line for the operator, naming the switch. */
|
|
70887
|
+
message: string()
|
|
70888
|
+
})]);
|
|
70889
|
+
var PowerSourceInfoSchema = object({
|
|
70890
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
70891
|
+
linked: boolean(),
|
|
70892
|
+
switchDeviceId: number().nullable(),
|
|
70893
|
+
switchName: string().nullable(),
|
|
70894
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
70895
|
+
switchOn: boolean().nullable(),
|
|
70896
|
+
switchOnline: boolean().nullable(),
|
|
70897
|
+
restartInProgress: boolean(),
|
|
70898
|
+
offSeconds: number().int()
|
|
70899
|
+
});
|
|
70900
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
70901
|
+
var PowerSourceStateSchema = object({
|
|
70902
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
70903
|
+
switchOn: boolean().nullable(),
|
|
70904
|
+
offSeconds: number().int(),
|
|
70905
|
+
lastFetchedAt: number()
|
|
70906
|
+
});
|
|
70907
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
70908
|
+
DeviceType.Gateway,
|
|
70909
|
+
DeviceType.Hub,
|
|
70910
|
+
DeviceType.Camera,
|
|
70911
|
+
DeviceType.Sensor,
|
|
70912
|
+
DeviceType.MediaPlayer,
|
|
70913
|
+
DeviceType.Vacuum,
|
|
70914
|
+
DeviceType.Generic,
|
|
70915
|
+
DeviceType.Washer,
|
|
70916
|
+
DeviceType.Dryer,
|
|
70917
|
+
DeviceType.Dishwasher,
|
|
70918
|
+
DeviceType.PetFountain,
|
|
70919
|
+
DeviceType.Fan,
|
|
70920
|
+
DeviceType.Humidifier,
|
|
70921
|
+
DeviceType.Climate,
|
|
70922
|
+
DeviceType.WaterHeater
|
|
70923
|
+
];
|
|
70924
|
+
var powerSourceCustomize = {
|
|
70925
|
+
version: 1,
|
|
70926
|
+
targets: [{
|
|
70927
|
+
id: "switch",
|
|
70928
|
+
cardinality: "scalar",
|
|
70929
|
+
vocabulary: {
|
|
70930
|
+
cap: "power-source",
|
|
70931
|
+
stateField: "switchOn"
|
|
70932
|
+
},
|
|
70933
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
70934
|
+
apply: "add",
|
|
70935
|
+
path: "switchOn",
|
|
70936
|
+
sources: [{
|
|
70937
|
+
kind: "switch",
|
|
70938
|
+
transforms: [{ kind: "identity" }]
|
|
70939
|
+
}],
|
|
70940
|
+
companions: [{
|
|
70941
|
+
kind: "constant",
|
|
70942
|
+
path: "offSeconds",
|
|
70943
|
+
value: 10,
|
|
70944
|
+
when: "add"
|
|
70945
|
+
}, {
|
|
70946
|
+
kind: "clock",
|
|
70947
|
+
path: "lastFetchedAt",
|
|
70948
|
+
when: "always"
|
|
70949
|
+
}],
|
|
70950
|
+
sourceReport: { status: "assignment-status" }
|
|
70951
|
+
}]
|
|
70952
|
+
};
|
|
70953
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
70954
|
+
var powerSourceCapability = {
|
|
70955
|
+
name: "power-source",
|
|
70956
|
+
scope: "device",
|
|
70957
|
+
mode: "singleton",
|
|
70958
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
70959
|
+
methods: {
|
|
70960
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
70961
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
70962
|
+
kind: "mutation",
|
|
70963
|
+
auth: "admin"
|
|
70964
|
+
}),
|
|
70965
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
70966
|
+
kind: "mutation",
|
|
70967
|
+
auth: "admin"
|
|
70968
|
+
}),
|
|
70969
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
70970
|
+
kind: "mutation",
|
|
70971
|
+
auth: "admin"
|
|
70972
|
+
})
|
|
70973
|
+
},
|
|
70974
|
+
runtimeState: PowerSourceStateSchema,
|
|
70975
|
+
customize: powerSourceCustomize,
|
|
70976
|
+
/**
|
|
70977
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
70978
|
+
* its next report; a restored value would outlive the reading it names.
|
|
70979
|
+
*
|
|
70980
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
70981
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
70982
|
+
*/
|
|
70983
|
+
durability: "session",
|
|
70984
|
+
history: "none"
|
|
70985
|
+
};
|
|
69587
70986
|
/**
|
|
69588
70987
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
69589
70988
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -70223,6 +71622,79 @@ var ptzAutotrackCapability = {
|
|
|
70223
71622
|
*/
|
|
70224
71623
|
history: "none"
|
|
70225
71624
|
};
|
|
71625
|
+
/**
|
|
71626
|
+
* The on/off duty cycle of a pet fountain's pump. Some models run the pump
|
|
71627
|
+
* continuously and have nothing to configure; others can alternate a run time
|
|
71628
|
+
* and a rest time. That is model-specific, so it is its own optional cap on the
|
|
71629
|
+
* fountain device: a fountain without cycle entities simply never composes it
|
|
71630
|
+
* (planning#366), and the `pet-fountain` cap stays the model-independent core.
|
|
71631
|
+
*
|
|
71632
|
+
* Like `pet-fountain` it has no native vendor provider: an operator BUILDS the
|
|
71633
|
+
* cycle from loose entities (a cycle switch and two number boxes) and every
|
|
71634
|
+
* method is a forward to one source entity (D659, D967). Durations are stored
|
|
71635
|
+
* in SECONDS (D944); a number box that cannot be read back leaves them `null`
|
|
71636
|
+
* — unknown, never zero (D393).
|
|
71637
|
+
*/
|
|
71638
|
+
var PumpCycleStatusSchema = object({
|
|
71639
|
+
/** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
|
|
71640
|
+
enabled: boolean().nullable(),
|
|
71641
|
+
/** How long the pump runs per cycle, in seconds. Null while unknown. */
|
|
71642
|
+
onTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
71643
|
+
/** How long the pump rests per cycle, in seconds. Null while unknown. */
|
|
71644
|
+
offTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
71645
|
+
/** Ms epoch when the slice was last refreshed. */
|
|
71646
|
+
lastFetchedAt: number()
|
|
71647
|
+
});
|
|
71648
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
71649
|
+
var MinutesSchema = number().int().min(0);
|
|
71650
|
+
var pumpCycleCapability = {
|
|
71651
|
+
name: "pump-cycle",
|
|
71652
|
+
scope: "device",
|
|
71653
|
+
deviceNative: true,
|
|
71654
|
+
mode: "singleton",
|
|
71655
|
+
deviceTypes: [DeviceType.PetFountain],
|
|
71656
|
+
methods: {
|
|
71657
|
+
/** Switch the on/off duty cycle on or off (off = the pump runs continuously). */
|
|
71658
|
+
setEnabled: method(object({
|
|
71659
|
+
deviceId: DeviceIdSchema,
|
|
71660
|
+
on: boolean()
|
|
71661
|
+
}), _void(), {
|
|
71662
|
+
kind: "mutation",
|
|
71663
|
+
auth: "protected"
|
|
71664
|
+
}),
|
|
71665
|
+
/** Set how many minutes the pump runs per cycle. */
|
|
71666
|
+
setOnMinutes: method(object({
|
|
71667
|
+
deviceId: DeviceIdSchema,
|
|
71668
|
+
minutes: MinutesSchema
|
|
71669
|
+
}), _void(), {
|
|
71670
|
+
kind: "mutation",
|
|
71671
|
+
auth: "protected"
|
|
71672
|
+
}),
|
|
71673
|
+
/** Set how many minutes the pump rests per cycle. */
|
|
71674
|
+
setOffMinutes: method(object({
|
|
71675
|
+
deviceId: DeviceIdSchema,
|
|
71676
|
+
minutes: MinutesSchema
|
|
71677
|
+
}), _void(), {
|
|
71678
|
+
kind: "mutation",
|
|
71679
|
+
auth: "protected"
|
|
71680
|
+
})
|
|
71681
|
+
},
|
|
71682
|
+
status: {
|
|
71683
|
+
schema: PumpCycleStatusSchema,
|
|
71684
|
+
kind: "push"
|
|
71685
|
+
},
|
|
71686
|
+
/** Runtime-state slice — mirrored by the kernel; the hero card reads it whole. */
|
|
71687
|
+
runtimeState: PumpCycleStatusSchema,
|
|
71688
|
+
/**
|
|
71689
|
+
* Runtime-state durability: **session** — live appliance state re-published on connect.
|
|
71690
|
+
*
|
|
71691
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
71692
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
71693
|
+
*/
|
|
71694
|
+
durability: "session",
|
|
71695
|
+
/** History (D850): a duty-cycle setting is not a series worth recording. */
|
|
71696
|
+
history: "none"
|
|
71697
|
+
};
|
|
70226
71698
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
70227
71699
|
kind: "mutation",
|
|
70228
71700
|
auth: "admin"
|
|
@@ -73013,7 +74485,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
73013
74485
|
"next",
|
|
73014
74486
|
"previous",
|
|
73015
74487
|
"setMute",
|
|
73016
|
-
"selectSource"
|
|
74488
|
+
"selectSource",
|
|
74489
|
+
"turnOn",
|
|
74490
|
+
"turnOff",
|
|
74491
|
+
"volumeUp",
|
|
74492
|
+
"volumeDown",
|
|
74493
|
+
"channelUp",
|
|
74494
|
+
"channelDown"
|
|
73017
74495
|
]);
|
|
73018
74496
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
73019
74497
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -73034,7 +74512,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
73034
74512
|
}), RemoteSendResultSchema, {
|
|
73035
74513
|
kind: "mutation",
|
|
73036
74514
|
auth: "protected"
|
|
73037
|
-
}), method(deviceInput.extend({
|
|
74515
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
73038
74516
|
command: RemoteMediaCommandSchema,
|
|
73039
74517
|
muted: boolean().optional(),
|
|
73040
74518
|
source: string().optional(),
|
|
@@ -75280,7 +76758,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
|
|
|
75280
76758
|
* `getSettingsUISchema()` surface.
|
|
75281
76759
|
*/
|
|
75282
76760
|
var SwitchStatusSchema = object({
|
|
75283
|
-
|
|
76761
|
+
/**
|
|
76762
|
+
* `null` = the device has never reported its state (a measurement that
|
|
76763
|
+
* has not happened is unknown, never `false`).
|
|
76764
|
+
*/
|
|
76765
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
75284
76766
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
75285
76767
|
lastChangedAt: number()
|
|
75286
76768
|
});
|
|
@@ -75305,7 +76787,7 @@ var switchCapability = {
|
|
|
75305
76787
|
}) },
|
|
75306
76788
|
events: { onStateChanged: { data: object({
|
|
75307
76789
|
deviceId: number(),
|
|
75308
|
-
on: boolean(),
|
|
76790
|
+
on: boolean().nullable(),
|
|
75309
76791
|
lastChangedAt: number()
|
|
75310
76792
|
}) } },
|
|
75311
76793
|
status: {
|
|
@@ -78954,6 +80436,26 @@ function buildDescriptor(capName, kind) {
|
|
|
78954
80436
|
* contributes one entry per bound cap present in this map.
|
|
78955
80437
|
*/
|
|
78956
80438
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
80439
|
+
/**
|
|
80440
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
80441
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
80442
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
80443
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
80444
|
+
* server sends an id, a client maps it to its own icon set.
|
|
80445
|
+
*
|
|
80446
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
80447
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
80448
|
+
* to `icon`.
|
|
80449
|
+
*/
|
|
80450
|
+
function digit(n) {
|
|
80451
|
+
return {
|
|
80452
|
+
label: t(n, n),
|
|
80453
|
+
icon: "hash",
|
|
80454
|
+
legend: n
|
|
80455
|
+
};
|
|
80456
|
+
}
|
|
80457
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
80458
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
78957
80459
|
var btn = (idSuffix, row, col, init) => ({
|
|
78958
80460
|
idSuffix,
|
|
78959
80461
|
row,
|
|
@@ -79080,6 +80582,34 @@ discriminatedUnion("kind", [object({
|
|
|
79080
80582
|
kind: literal("pronto"),
|
|
79081
80583
|
code: string()
|
|
79082
80584
|
})]);
|
|
80585
|
+
/**
|
|
80586
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
80587
|
+
*
|
|
80588
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
80589
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
80590
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
80591
|
+
* count of samples.
|
|
80592
|
+
*
|
|
80593
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
80594
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
80595
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
80596
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
80597
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
80598
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
80599
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
80600
|
+
* covers the longest standby blip above T.
|
|
80601
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
80602
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
80603
|
+
*/
|
|
80604
|
+
var SECOND_MS = 1e3;
|
|
80605
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
80606
|
+
24 * (60 * MINUTE_MS$1);
|
|
80607
|
+
10 * MINUTE_MS$1;
|
|
80608
|
+
30 * SECOND_MS;
|
|
80609
|
+
60 * SECOND_MS;
|
|
80610
|
+
20 * MINUTE_MS$1;
|
|
80611
|
+
10 * SECOND_MS;
|
|
80612
|
+
10 * SECOND_MS;
|
|
79083
80613
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
79084
80614
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
79085
80615
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
@@ -79440,6 +80970,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
79440
80970
|
pressureSensor: pressureSensorCapability,
|
|
79441
80971
|
privacyMask: privacyMaskCapability,
|
|
79442
80972
|
ptzAutotrack: ptzAutotrackCapability,
|
|
80973
|
+
pumpCycle: pumpCycleCapability,
|
|
79443
80974
|
recordingOnboard: recordingOnboardCapability,
|
|
79444
80975
|
recordingSignal: recordingSignalCapability,
|
|
79445
80976
|
runningState: runningStateCapability,
|
|
@@ -81624,6 +83155,27 @@ Object.freeze({
|
|
|
81624
83155
|
access: "view",
|
|
81625
83156
|
role: "viewer"
|
|
81626
83157
|
},
|
|
83158
|
+
"boardKeySource.getBoardKey": {
|
|
83159
|
+
capName: "board-key-source",
|
|
83160
|
+
capScope: "system",
|
|
83161
|
+
addonId: null,
|
|
83162
|
+
access: "view",
|
|
83163
|
+
role: "admin"
|
|
83164
|
+
},
|
|
83165
|
+
"boardKeySource.listBoards": {
|
|
83166
|
+
capName: "board-key-source",
|
|
83167
|
+
capScope: "system",
|
|
83168
|
+
addonId: null,
|
|
83169
|
+
access: "view",
|
|
83170
|
+
role: "admin"
|
|
83171
|
+
},
|
|
83172
|
+
"boardKeySource.listSources": {
|
|
83173
|
+
capName: "board-key-source",
|
|
83174
|
+
capScope: "system",
|
|
83175
|
+
addonId: null,
|
|
83176
|
+
access: "view",
|
|
83177
|
+
role: "admin"
|
|
83178
|
+
},
|
|
81627
83179
|
"brightness.getStatus": {
|
|
81628
83180
|
capName: "brightness",
|
|
81629
83181
|
capScope: "device",
|
|
@@ -82163,6 +83715,13 @@ Object.freeze({
|
|
|
82163
83715
|
access: "create",
|
|
82164
83716
|
role: "admin"
|
|
82165
83717
|
},
|
|
83718
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
83719
|
+
capName: "core-blocks",
|
|
83720
|
+
capScope: "system",
|
|
83721
|
+
addonId: null,
|
|
83722
|
+
access: "view",
|
|
83723
|
+
role: "admin"
|
|
83724
|
+
},
|
|
82166
83725
|
"coreBlocks.reportAutomationRun": {
|
|
82167
83726
|
capName: "core-blocks",
|
|
82168
83727
|
capScope: "system",
|
|
@@ -82674,6 +84233,13 @@ Object.freeze({
|
|
|
82674
84233
|
access: "view",
|
|
82675
84234
|
role: "admin"
|
|
82676
84235
|
},
|
|
84236
|
+
"deviceExport.listInstances": {
|
|
84237
|
+
capName: "device-export",
|
|
84238
|
+
capScope: "system",
|
|
84239
|
+
addonId: null,
|
|
84240
|
+
access: "view",
|
|
84241
|
+
role: "admin"
|
|
84242
|
+
},
|
|
82677
84243
|
"deviceExport.listSupportedDeviceKinds": {
|
|
82678
84244
|
capName: "device-export",
|
|
82679
84245
|
capScope: "system",
|
|
@@ -84305,6 +85871,13 @@ Object.freeze({
|
|
|
84305
85871
|
access: "view",
|
|
84306
85872
|
role: "admin"
|
|
84307
85873
|
},
|
|
85874
|
+
"integrationInstances.setInstanceEnabled": {
|
|
85875
|
+
capName: "integration-instances",
|
|
85876
|
+
capScope: "system",
|
|
85877
|
+
addonId: null,
|
|
85878
|
+
access: "create",
|
|
85879
|
+
role: "admin"
|
|
85880
|
+
},
|
|
84308
85881
|
"integrationInstances.testInstance": {
|
|
84309
85882
|
capName: "integration-instances",
|
|
84310
85883
|
capScope: "system",
|
|
@@ -84319,6 +85892,13 @@ Object.freeze({
|
|
|
84319
85892
|
access: "create",
|
|
84320
85893
|
role: "admin"
|
|
84321
85894
|
},
|
|
85895
|
+
"integrations.clearSettings": {
|
|
85896
|
+
capName: "integrations",
|
|
85897
|
+
capScope: "system",
|
|
85898
|
+
addonId: null,
|
|
85899
|
+
access: "delete",
|
|
85900
|
+
role: "admin"
|
|
85901
|
+
},
|
|
84322
85902
|
"integrations.create": {
|
|
84323
85903
|
capName: "integrations",
|
|
84324
85904
|
capScope: "system",
|
|
@@ -85103,6 +86683,20 @@ Object.freeze({
|
|
|
85103
86683
|
access: "view",
|
|
85104
86684
|
role: "admin"
|
|
85105
86685
|
},
|
|
86686
|
+
"mediaPlayer.channelDown": {
|
|
86687
|
+
capName: "media-player",
|
|
86688
|
+
capScope: "device",
|
|
86689
|
+
addonId: null,
|
|
86690
|
+
access: "create",
|
|
86691
|
+
role: "viewer"
|
|
86692
|
+
},
|
|
86693
|
+
"mediaPlayer.channelUp": {
|
|
86694
|
+
capName: "media-player",
|
|
86695
|
+
capScope: "device",
|
|
86696
|
+
addonId: null,
|
|
86697
|
+
access: "create",
|
|
86698
|
+
role: "viewer"
|
|
86699
|
+
},
|
|
85106
86700
|
"mediaPlayer.getStatus": {
|
|
85107
86701
|
capName: "media-player",
|
|
85108
86702
|
capScope: "device",
|
|
@@ -85194,6 +86788,34 @@ Object.freeze({
|
|
|
85194
86788
|
access: "create",
|
|
85195
86789
|
role: "viewer"
|
|
85196
86790
|
},
|
|
86791
|
+
"mediaPlayer.turnOff": {
|
|
86792
|
+
capName: "media-player",
|
|
86793
|
+
capScope: "device",
|
|
86794
|
+
addonId: null,
|
|
86795
|
+
access: "create",
|
|
86796
|
+
role: "viewer"
|
|
86797
|
+
},
|
|
86798
|
+
"mediaPlayer.turnOn": {
|
|
86799
|
+
capName: "media-player",
|
|
86800
|
+
capScope: "device",
|
|
86801
|
+
addonId: null,
|
|
86802
|
+
access: "create",
|
|
86803
|
+
role: "viewer"
|
|
86804
|
+
},
|
|
86805
|
+
"mediaPlayer.volumeDown": {
|
|
86806
|
+
capName: "media-player",
|
|
86807
|
+
capScope: "device",
|
|
86808
|
+
addonId: null,
|
|
86809
|
+
access: "create",
|
|
86810
|
+
role: "viewer"
|
|
86811
|
+
},
|
|
86812
|
+
"mediaPlayer.volumeUp": {
|
|
86813
|
+
capName: "media-player",
|
|
86814
|
+
capScope: "device",
|
|
86815
|
+
addonId: null,
|
|
86816
|
+
access: "create",
|
|
86817
|
+
role: "viewer"
|
|
86818
|
+
},
|
|
85197
86819
|
"meshNetwork.getStatus": {
|
|
85198
86820
|
capName: "mesh-network",
|
|
85199
86821
|
capScope: "system",
|
|
@@ -86440,27 +88062,6 @@ Object.freeze({
|
|
|
86440
88062
|
access: "create",
|
|
86441
88063
|
role: "viewer"
|
|
86442
88064
|
},
|
|
86443
|
-
"petFountain.setPumpInterval": {
|
|
86444
|
-
capName: "pet-fountain",
|
|
86445
|
-
capScope: "device",
|
|
86446
|
-
addonId: null,
|
|
86447
|
-
access: "create",
|
|
86448
|
-
role: "viewer"
|
|
86449
|
-
},
|
|
86450
|
-
"petFountain.setPumpOffMinutes": {
|
|
86451
|
-
capName: "pet-fountain",
|
|
86452
|
-
capScope: "device",
|
|
86453
|
-
addonId: null,
|
|
86454
|
-
access: "create",
|
|
86455
|
-
role: "viewer"
|
|
86456
|
-
},
|
|
86457
|
-
"petFountain.setPumpOnMinutes": {
|
|
86458
|
-
capName: "pet-fountain",
|
|
86459
|
-
capScope: "device",
|
|
86460
|
-
addonId: null,
|
|
86461
|
-
access: "create",
|
|
86462
|
-
role: "viewer"
|
|
86463
|
-
},
|
|
86464
88065
|
"pipelineAnalytics.addRetrainFrameFromRecording": {
|
|
86465
88066
|
capName: "pipeline-analytics",
|
|
86466
88067
|
capScope: "device",
|
|
@@ -88239,6 +89840,34 @@ Object.freeze({
|
|
|
88239
89840
|
access: "create",
|
|
88240
89841
|
role: "admin"
|
|
88241
89842
|
},
|
|
89843
|
+
"pumpCycle.getStatus": {
|
|
89844
|
+
capName: "pump-cycle",
|
|
89845
|
+
capScope: "device",
|
|
89846
|
+
addonId: null,
|
|
89847
|
+
access: "view",
|
|
89848
|
+
role: "viewer"
|
|
89849
|
+
},
|
|
89850
|
+
"pumpCycle.setEnabled": {
|
|
89851
|
+
capName: "pump-cycle",
|
|
89852
|
+
capScope: "device",
|
|
89853
|
+
addonId: null,
|
|
89854
|
+
access: "create",
|
|
89855
|
+
role: "viewer"
|
|
89856
|
+
},
|
|
89857
|
+
"pumpCycle.setOffMinutes": {
|
|
89858
|
+
capName: "pump-cycle",
|
|
89859
|
+
capScope: "device",
|
|
89860
|
+
addonId: null,
|
|
89861
|
+
access: "create",
|
|
89862
|
+
role: "viewer"
|
|
89863
|
+
},
|
|
89864
|
+
"pumpCycle.setOnMinutes": {
|
|
89865
|
+
capName: "pump-cycle",
|
|
89866
|
+
capScope: "device",
|
|
89867
|
+
addonId: null,
|
|
89868
|
+
access: "create",
|
|
89869
|
+
role: "viewer"
|
|
89870
|
+
},
|
|
88242
89871
|
"reboot.reboot": {
|
|
88243
89872
|
capName: "reboot",
|
|
88244
89873
|
capScope: "device",
|
|
@@ -88743,6 +90372,13 @@ Object.freeze({
|
|
|
88743
90372
|
access: "create",
|
|
88744
90373
|
role: "admin"
|
|
88745
90374
|
},
|
|
90375
|
+
"remote.getAssumedStatus": {
|
|
90376
|
+
capName: "remote",
|
|
90377
|
+
capScope: "system",
|
|
90378
|
+
addonId: null,
|
|
90379
|
+
access: "view",
|
|
90380
|
+
role: "admin"
|
|
90381
|
+
},
|
|
88746
90382
|
"remote.getConfig": {
|
|
88747
90383
|
capName: "remote",
|
|
88748
90384
|
capScope: "system",
|
|
@@ -91558,6 +93194,11 @@ Object.freeze({
|
|
|
91558
93194
|
form: "single",
|
|
91559
93195
|
optional: false
|
|
91560
93196
|
}],
|
|
93197
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
93198
|
+
name: "deviceId",
|
|
93199
|
+
form: "single",
|
|
93200
|
+
optional: false
|
|
93201
|
+
}],
|
|
91561
93202
|
"cover.close": [{
|
|
91562
93203
|
name: "deviceId",
|
|
91563
93204
|
form: "single",
|
|
@@ -92409,6 +94050,16 @@ Object.freeze({
|
|
|
92409
94050
|
form: "single",
|
|
92410
94051
|
optional: true
|
|
92411
94052
|
}],
|
|
94053
|
+
"mediaPlayer.channelDown": [{
|
|
94054
|
+
name: "deviceId",
|
|
94055
|
+
form: "single",
|
|
94056
|
+
optional: false
|
|
94057
|
+
}],
|
|
94058
|
+
"mediaPlayer.channelUp": [{
|
|
94059
|
+
name: "deviceId",
|
|
94060
|
+
form: "single",
|
|
94061
|
+
optional: false
|
|
94062
|
+
}],
|
|
92412
94063
|
"mediaPlayer.getStatus": [{
|
|
92413
94064
|
name: "deviceId",
|
|
92414
94065
|
form: "single",
|
|
@@ -92474,6 +94125,26 @@ Object.freeze({
|
|
|
92474
94125
|
form: "single",
|
|
92475
94126
|
optional: false
|
|
92476
94127
|
}],
|
|
94128
|
+
"mediaPlayer.turnOff": [{
|
|
94129
|
+
name: "deviceId",
|
|
94130
|
+
form: "single",
|
|
94131
|
+
optional: false
|
|
94132
|
+
}],
|
|
94133
|
+
"mediaPlayer.turnOn": [{
|
|
94134
|
+
name: "deviceId",
|
|
94135
|
+
form: "single",
|
|
94136
|
+
optional: false
|
|
94137
|
+
}],
|
|
94138
|
+
"mediaPlayer.volumeDown": [{
|
|
94139
|
+
name: "deviceId",
|
|
94140
|
+
form: "single",
|
|
94141
|
+
optional: false
|
|
94142
|
+
}],
|
|
94143
|
+
"mediaPlayer.volumeUp": [{
|
|
94144
|
+
name: "deviceId",
|
|
94145
|
+
form: "single",
|
|
94146
|
+
optional: false
|
|
94147
|
+
}],
|
|
92477
94148
|
"meter.getStatus": [{
|
|
92478
94149
|
name: "deviceId",
|
|
92479
94150
|
form: "single",
|
|
@@ -92783,21 +94454,6 @@ Object.freeze({
|
|
|
92783
94454
|
form: "single",
|
|
92784
94455
|
optional: false
|
|
92785
94456
|
}],
|
|
92786
|
-
"petFountain.setPumpInterval": [{
|
|
92787
|
-
name: "deviceId",
|
|
92788
|
-
form: "single",
|
|
92789
|
-
optional: false
|
|
92790
|
-
}],
|
|
92791
|
-
"petFountain.setPumpOffMinutes": [{
|
|
92792
|
-
name: "deviceId",
|
|
92793
|
-
form: "single",
|
|
92794
|
-
optional: false
|
|
92795
|
-
}],
|
|
92796
|
-
"petFountain.setPumpOnMinutes": [{
|
|
92797
|
-
name: "deviceId",
|
|
92798
|
-
form: "single",
|
|
92799
|
-
optional: false
|
|
92800
|
-
}],
|
|
92801
94457
|
"pipelineAnalytics.addRetrainFrameFromRecording": [{
|
|
92802
94458
|
name: "deviceId",
|
|
92803
94459
|
form: "single",
|
|
@@ -93433,6 +95089,26 @@ Object.freeze({
|
|
|
93433
95089
|
form: "single",
|
|
93434
95090
|
optional: false
|
|
93435
95091
|
}],
|
|
95092
|
+
"pumpCycle.getStatus": [{
|
|
95093
|
+
name: "deviceId",
|
|
95094
|
+
form: "single",
|
|
95095
|
+
optional: false
|
|
95096
|
+
}],
|
|
95097
|
+
"pumpCycle.setEnabled": [{
|
|
95098
|
+
name: "deviceId",
|
|
95099
|
+
form: "single",
|
|
95100
|
+
optional: false
|
|
95101
|
+
}],
|
|
95102
|
+
"pumpCycle.setOffMinutes": [{
|
|
95103
|
+
name: "deviceId",
|
|
95104
|
+
form: "single",
|
|
95105
|
+
optional: false
|
|
95106
|
+
}],
|
|
95107
|
+
"pumpCycle.setOnMinutes": [{
|
|
95108
|
+
name: "deviceId",
|
|
95109
|
+
form: "single",
|
|
95110
|
+
optional: false
|
|
95111
|
+
}],
|
|
93436
95112
|
"reboot.reboot": [{
|
|
93437
95113
|
name: "deviceId",
|
|
93438
95114
|
form: "single",
|
|
@@ -93634,6 +95310,11 @@ Object.freeze({
|
|
|
93634
95310
|
form: "single",
|
|
93635
95311
|
optional: true
|
|
93636
95312
|
}],
|
|
95313
|
+
"remote.getAssumedStatus": [{
|
|
95314
|
+
name: "deviceId",
|
|
95315
|
+
form: "single",
|
|
95316
|
+
optional: false
|
|
95317
|
+
}],
|
|
93637
95318
|
"remote.getConfig": [{
|
|
93638
95319
|
name: "deviceId",
|
|
93639
95320
|
form: "single",
|
|
@@ -94181,99 +95862,6 @@ Object.freeze({
|
|
|
94181
95862
|
"network-access": "ingress",
|
|
94182
95863
|
"smtp-provider": "email"
|
|
94183
95864
|
});
|
|
94184
|
-
/**
|
|
94185
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
94186
|
-
* `z.infer`, they do not restate the shape.
|
|
94187
|
-
*/
|
|
94188
|
-
var OverrideSourceSchema = object({
|
|
94189
|
-
addonId: string().min(1),
|
|
94190
|
-
stableId: string().min(1),
|
|
94191
|
-
cap: string().min(1),
|
|
94192
|
-
fieldPath: string().min(1)
|
|
94193
|
-
});
|
|
94194
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
94195
|
-
object({ kind: literal("identity") }),
|
|
94196
|
-
object({ kind: literal("invert") }),
|
|
94197
|
-
object({
|
|
94198
|
-
kind: literal("threshold"),
|
|
94199
|
-
comparison: _enum(["gte", "lte"]),
|
|
94200
|
-
threshold: number(),
|
|
94201
|
-
startDwellMs: number().nonnegative(),
|
|
94202
|
-
stopDwellMs: number().nonnegative()
|
|
94203
|
-
}),
|
|
94204
|
-
object({
|
|
94205
|
-
kind: literal("linear"),
|
|
94206
|
-
scale: number(),
|
|
94207
|
-
offset: number(),
|
|
94208
|
-
clampMin: number().optional(),
|
|
94209
|
-
clampMax: number().optional()
|
|
94210
|
-
}),
|
|
94211
|
-
object({
|
|
94212
|
-
kind: literal("enum-map"),
|
|
94213
|
-
mapping: record(string(), string().nullable())
|
|
94214
|
-
}),
|
|
94215
|
-
object({ kind: literal("number") })
|
|
94216
|
-
]);
|
|
94217
|
-
var ScalarOverrideDraftSchema = object({
|
|
94218
|
-
kind: literal("scalar"),
|
|
94219
|
-
cap: string().min(1),
|
|
94220
|
-
targetId: string().min(1),
|
|
94221
|
-
source: OverrideSourceSchema,
|
|
94222
|
-
transform: CustomizeTransformDraftSchema
|
|
94223
|
-
});
|
|
94224
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
94225
|
-
kind: literal("keyedArray"),
|
|
94226
|
-
cap: string().min(1),
|
|
94227
|
-
targetId: string().min(1),
|
|
94228
|
-
key: string().min(1),
|
|
94229
|
-
label: string().min(1),
|
|
94230
|
-
fieldPath: string().min(1),
|
|
94231
|
-
source: OverrideSourceSchema,
|
|
94232
|
-
transform: CustomizeTransformDraftSchema
|
|
94233
|
-
});
|
|
94234
|
-
var PulseOverrideDraftSchema = object({
|
|
94235
|
-
kind: literal("pulse"),
|
|
94236
|
-
cap: string().min(1),
|
|
94237
|
-
targetId: string().min(1),
|
|
94238
|
-
enabled: boolean(),
|
|
94239
|
-
debounceMs: number().nonnegative(),
|
|
94240
|
-
edges: array(_enum([
|
|
94241
|
-
"rising",
|
|
94242
|
-
"button",
|
|
94243
|
-
"native-event"
|
|
94244
|
-
])).min(1).optional(),
|
|
94245
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
94246
|
-
});
|
|
94247
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
94248
|
-
"in-addition",
|
|
94249
|
-
"confirm",
|
|
94250
|
-
"replace"
|
|
94251
|
-
]).optional() });
|
|
94252
|
-
discriminatedUnion("kind", [
|
|
94253
|
-
ScalarOverrideDraftSchema,
|
|
94254
|
-
KeyedArrayOverrideDraftSchema,
|
|
94255
|
-
PulseOverrideDraftSchema,
|
|
94256
|
-
object({
|
|
94257
|
-
kind: literal("span"),
|
|
94258
|
-
cap: string().min(1),
|
|
94259
|
-
targetId: string().min(1),
|
|
94260
|
-
timeoutMs: number().nonnegative(),
|
|
94261
|
-
cooldownMs: number().nonnegative().optional(),
|
|
94262
|
-
sources: array(SpanSourceDraftSchema)
|
|
94263
|
-
})
|
|
94264
|
-
]);
|
|
94265
|
-
object({
|
|
94266
|
-
targetId: string().min(1),
|
|
94267
|
-
configured: boolean(),
|
|
94268
|
-
source: _enum(["native", "assigned"]),
|
|
94269
|
-
state: _enum([
|
|
94270
|
-
"active",
|
|
94271
|
-
"stale",
|
|
94272
|
-
"released",
|
|
94273
|
-
"unavailable"
|
|
94274
|
-
]),
|
|
94275
|
-
reason: string().nullable()
|
|
94276
|
-
});
|
|
94277
95865
|
var G711_SCALE_CORRECTION_DB = {
|
|
94278
95866
|
PCMU: 20 * Math.log10(4),
|
|
94279
95867
|
PCMA: 20 * Math.log10(8)
|
|
@@ -94889,6 +96477,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
94889
96477
|
[...RULE_TEXT],
|
|
94890
96478
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
94891
96479
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
96480
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
94892
96481
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
94893
96482
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
94894
96483
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -95876,6 +97465,140 @@ function buildConnectionFormSchema() {
|
|
|
95876
97465
|
}] };
|
|
95877
97466
|
}
|
|
95878
97467
|
//#endregion
|
|
97468
|
+
//#region src/rademacher-instances.ts
|
|
97469
|
+
/**
|
|
97470
|
+
* Rademacher integrations as `integration-instances` (planning#416, D985, D1007): one
|
|
97471
|
+
* `connections` record per HomePilot / Start2Smart hub under the `integrations` registry id. The
|
|
97472
|
+
* body IS the connection (host, password in the keychain, base path, poll interval), so the form
|
|
97473
|
+
* the instances page edits is the stored shape and the addon reads nothing else.
|
|
97474
|
+
*/
|
|
97475
|
+
var rademacherConnectionsEntryKind = defineEntryKind("connections", {
|
|
97476
|
+
title: "Rademacher hubs",
|
|
97477
|
+
schema: entryBody({
|
|
97478
|
+
host: string().optional(),
|
|
97479
|
+
password: string().optional().meta({ secret: true }),
|
|
97480
|
+
basePath: string().optional(),
|
|
97481
|
+
pollIntervalMs: number().optional()
|
|
97482
|
+
}),
|
|
97483
|
+
writeTrigger: "an operator adds, edits or removes a Rademacher hub connection"
|
|
97484
|
+
});
|
|
97485
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
97486
|
+
var rademacherRecordKinds = { connections: rademacherConnectionsEntryKind };
|
|
97487
|
+
/** The legacy row keys the record carries over; any other key is dropped and named. */
|
|
97488
|
+
var LEGACY_KEYS = [
|
|
97489
|
+
"host",
|
|
97490
|
+
"password",
|
|
97491
|
+
"basePath",
|
|
97492
|
+
"pollIntervalMs"
|
|
97493
|
+
];
|
|
97494
|
+
function text(value) {
|
|
97495
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
97496
|
+
}
|
|
97497
|
+
function whole(value) {
|
|
97498
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
97499
|
+
const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
97500
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
97501
|
+
}
|
|
97502
|
+
/** The record body of one integration, from its legacy rows (secrets resolved). */
|
|
97503
|
+
function rademacherBodyFromRows(values, log, integration) {
|
|
97504
|
+
const dropped = Object.keys(values).filter((key) => !LEGACY_KEYS.includes(key));
|
|
97505
|
+
if (dropped.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${dropped.join(", ")}`);
|
|
97506
|
+
const host = text(values["host"]);
|
|
97507
|
+
const password = text(values["password"]);
|
|
97508
|
+
const basePath = text(values["basePath"]);
|
|
97509
|
+
const pollIntervalMs = whole(values["pollIntervalMs"]);
|
|
97510
|
+
return {
|
|
97511
|
+
name: integration.name,
|
|
97512
|
+
...host !== void 0 ? { host } : {},
|
|
97513
|
+
...password !== void 0 ? { password } : {},
|
|
97514
|
+
...basePath !== void 0 ? { basePath } : {},
|
|
97515
|
+
...pollIntervalMs !== void 0 ? { pollIntervalMs } : {}
|
|
97516
|
+
};
|
|
97517
|
+
}
|
|
97518
|
+
/** The whole `provider-rademacher` catalogue (D987): offered to the engine at `onInitialize`. */
|
|
97519
|
+
var RADEMACHER_MIGRATIONS = [{
|
|
97520
|
+
meta: {
|
|
97521
|
+
id: "provider-rademacher/2026-11-05-integration-rows-to-record",
|
|
97522
|
+
owner: "provider-rademacher",
|
|
97523
|
+
level: "addon-state",
|
|
97524
|
+
seq: 1,
|
|
97525
|
+
dependsOn: [],
|
|
97526
|
+
store: "settings",
|
|
97527
|
+
scope: "cluster",
|
|
97528
|
+
onFailure: "block-owner",
|
|
97529
|
+
bodyKind: "procedure",
|
|
97530
|
+
description: "Move every Rademacher hub connection into a typed instance record keyed by its registry id, then clear the legacy integration settings rows and their keychain values",
|
|
97531
|
+
fixtureMark: "a legacy setting key the migration does not know"
|
|
97532
|
+
},
|
|
97533
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
97534
|
+
addonId: "provider-rademacher",
|
|
97535
|
+
secretPaths: rademacherConnectionsEntryKind.declaration.secretPaths,
|
|
97536
|
+
bodyFrom: rademacherBodyFromRows
|
|
97537
|
+
})
|
|
97538
|
+
}];
|
|
97539
|
+
/** The hub side and the records side of the move, over `ctx.api` and `ctx.records`. */
|
|
97540
|
+
function rademacherRowsPorts(deps) {
|
|
97541
|
+
return integrationRowsPortsOverApi({
|
|
97542
|
+
api: deps.api,
|
|
97543
|
+
records: deps.records.kind({
|
|
97544
|
+
name: rademacherConnectionsEntryKind.name,
|
|
97545
|
+
declaration: rademacherConnectionsEntryKind.declaration,
|
|
97546
|
+
schema: record(string(), unknown())
|
|
97547
|
+
}),
|
|
97548
|
+
log: { warn: deps.warn }
|
|
97549
|
+
});
|
|
97550
|
+
}
|
|
97551
|
+
/**
|
|
97552
|
+
* The live connection of one instance, read from its record. null while the host is missing; a
|
|
97553
|
+
* locked keychain throws so the caller keeps the client it has instead of reconfiguring it with an
|
|
97554
|
+
* empty password (D49).
|
|
97555
|
+
*/
|
|
97556
|
+
async function rademacherConnectionOf(instances, id) {
|
|
97557
|
+
const read = await instances.readResolved(id);
|
|
97558
|
+
if (read === null) return null;
|
|
97559
|
+
if (read.secrets === "unavailable") throw new Error("the keychain is locked, the saved password cannot be read");
|
|
97560
|
+
return connectionFromSettings(read.body);
|
|
97561
|
+
}
|
|
97562
|
+
function typedPassword(value) {
|
|
97563
|
+
return typeof value === "string" && value !== "" && value !== "__camstack_redacted__" ? value : void 0;
|
|
97564
|
+
}
|
|
97565
|
+
function buildRademacherInstances(records, deps) {
|
|
97566
|
+
const handle = records.kind(rademacherConnectionsEntryKind);
|
|
97567
|
+
return new RecordIntegrationInstances(records, rademacherConnectionsEntryKind, {
|
|
97568
|
+
description: {
|
|
97569
|
+
role: "devices",
|
|
97570
|
+
instanceMode: "multiple",
|
|
97571
|
+
kind: "connection",
|
|
97572
|
+
canTest: true
|
|
97573
|
+
},
|
|
97574
|
+
formSchema: () => buildConnectionFormSchema(),
|
|
97575
|
+
initialBody: (name) => ({ name }),
|
|
97576
|
+
configured: (body) => connectionFromSettings(body) !== null,
|
|
97577
|
+
onChanged: async (_id, change) => {
|
|
97578
|
+
if (change !== "deleted") await deps.onChanged();
|
|
97579
|
+
},
|
|
97580
|
+
test: async ({ id, values }) => {
|
|
97581
|
+
const stored = id === null ? null : (await handle.getResolved(id))?.data.password ?? null;
|
|
97582
|
+
const password = typedPassword(values["password"]) ?? stored ?? "";
|
|
97583
|
+
const outcome = await deps.tester.testSettings({ settings: {
|
|
97584
|
+
...values,
|
|
97585
|
+
password
|
|
97586
|
+
} });
|
|
97587
|
+
if (outcome.outcome === "validated") return {
|
|
97588
|
+
success: true,
|
|
97589
|
+
status: "validated",
|
|
97590
|
+
...outcome.latencyMs !== void 0 ? { latencyMs: outcome.latencyMs } : {},
|
|
97591
|
+
...outcome.detail !== void 0 ? { detail: outcome.detail } : {}
|
|
97592
|
+
};
|
|
97593
|
+
return {
|
|
97594
|
+
success: false,
|
|
97595
|
+
status: outcome.outcome,
|
|
97596
|
+
error: outcome.error
|
|
97597
|
+
};
|
|
97598
|
+
}
|
|
97599
|
+
});
|
|
97600
|
+
}
|
|
97601
|
+
//#endregion
|
|
95879
97602
|
//#region src/cover-mapping.ts
|
|
95880
97603
|
/** Clamp a raw percentage to the cap's `0..100` integer domain. */
|
|
95881
97604
|
function clampPercent(value) {
|
|
@@ -97930,11 +99653,25 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
97930
99653
|
};
|
|
97931
99654
|
integrationReconciler = null;
|
|
97932
99655
|
clients = null;
|
|
99656
|
+
instances = null;
|
|
97933
99657
|
constructor() {
|
|
97934
99658
|
super({});
|
|
97935
99659
|
}
|
|
97936
99660
|
async onInitialize() {
|
|
97937
99661
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
99662
|
+
const records = this.ctx.records;
|
|
99663
|
+
if (records === void 0) throw new Error("provider-rademacher: ctx.records is required (hub-only addon)");
|
|
99664
|
+
await records.declare(rademacherRecordKinds);
|
|
99665
|
+
await this.runMigrations(defineAddonMigrations(RADEMACHER_MIGRATIONS, rademacherRowsPorts({
|
|
99666
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
99667
|
+
records,
|
|
99668
|
+
warn: (message) => this.ctx.logger.warn(message)
|
|
99669
|
+
})));
|
|
99670
|
+
const instances = buildRademacherInstances(records, {
|
|
99671
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
99672
|
+
tester: this.buildConnectionTestProvider()
|
|
99673
|
+
});
|
|
99674
|
+
this.instances = instances;
|
|
97938
99675
|
this.clients = new RademacherClientRegistry(this.ctx.logger);
|
|
97939
99676
|
this.clients.setOnConnected((integrationId) => {
|
|
97940
99677
|
this.setIntegrationDevicesOnline(integrationId, true);
|
|
@@ -97953,6 +99690,10 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
97953
99690
|
{
|
|
97954
99691
|
capability: connectionTestCapability,
|
|
97955
99692
|
provider: this.buildConnectionTestProvider()
|
|
99693
|
+
},
|
|
99694
|
+
{
|
|
99695
|
+
capability: integrationInstancesCapability,
|
|
99696
|
+
provider: instances.toProvider()
|
|
97956
99697
|
}
|
|
97957
99698
|
] };
|
|
97958
99699
|
}
|
|
@@ -97965,11 +99706,9 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
97965
99706
|
this.clients = null;
|
|
97966
99707
|
this.integrationReconciler?.dispose();
|
|
97967
99708
|
this.integrationReconciler = null;
|
|
99709
|
+
this.instances = null;
|
|
97968
99710
|
await super.onShutdown();
|
|
97969
99711
|
}
|
|
97970
|
-
globalSettingsSchema() {
|
|
97971
|
-
return buildConnectionFormSchema();
|
|
97972
|
-
}
|
|
97973
99712
|
async supportsManualCreation() {
|
|
97974
99713
|
return false;
|
|
97975
99714
|
}
|
|
@@ -97985,6 +99724,10 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
97985
99724
|
if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("provider-rademacher generateStableId: nativeId required");
|
|
97986
99725
|
return `rademacher:${nativeId}`;
|
|
97987
99726
|
}
|
|
99727
|
+
requireInstances() {
|
|
99728
|
+
if (!this.instances) throw new Error("Rademacher provider not initialised");
|
|
99729
|
+
return this.instances;
|
|
99730
|
+
}
|
|
97988
99731
|
requireClients() {
|
|
97989
99732
|
if (!this.clients) throw new Error("Rademacher provider not initialised");
|
|
97990
99733
|
return this.clients;
|
|
@@ -98001,7 +99744,7 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
98001
99744
|
},
|
|
98002
99745
|
listIntegrations: () => this.ctx.api.integrations.list.query(),
|
|
98003
99746
|
applySettings: async (integration) => {
|
|
98004
|
-
const connection =
|
|
99747
|
+
const connection = await rademacherConnectionOf(this.requireInstances(), integration.id);
|
|
98005
99748
|
if (!connection) return false;
|
|
98006
99749
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
98007
99750
|
return true;
|