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