@camstack/addon-provider-reolink 1.2.221 → 1.2.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/addon.js +2091 -615
- package/dist/addon.mjs +2091 -615
- package/package.json +6 -2
package/dist/addon.mjs
CHANGED
|
@@ -105246,6 +105246,12 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
105246
105246
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
105247
105247
|
json.type = "boolean";
|
|
105248
105248
|
};
|
|
105249
|
+
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
105250
|
+
if (ctx.unrepresentable === "throw") throw new Error("BigInt cannot be represented in JSON Schema");
|
|
105251
|
+
};
|
|
105252
|
+
var symbolProcessor = (_schema, ctx, _json, _params) => {
|
|
105253
|
+
if (ctx.unrepresentable === "throw") throw new Error("Symbols cannot be represented in JSON Schema");
|
|
105254
|
+
};
|
|
105249
105255
|
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
105250
105256
|
if (ctx.target === "openapi-3.0") {
|
|
105251
105257
|
json.type = "string";
|
|
@@ -105253,12 +105259,17 @@ var nullProcessor = (_schema, ctx, json, _params) => {
|
|
|
105253
105259
|
json.enum = [null];
|
|
105254
105260
|
} else json.type = "null";
|
|
105255
105261
|
};
|
|
105262
|
+
var undefinedProcessor = (_schema, ctx, _json, _params) => {
|
|
105263
|
+
if (ctx.unrepresentable === "throw") throw new Error("Undefined cannot be represented in JSON Schema");
|
|
105264
|
+
};
|
|
105256
105265
|
var voidProcessor = (_schema, ctx, _json, _params) => {
|
|
105257
105266
|
if (ctx.unrepresentable === "throw") throw new Error("Void cannot be represented in JSON Schema");
|
|
105258
105267
|
};
|
|
105259
105268
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
105260
105269
|
json.not = {};
|
|
105261
105270
|
};
|
|
105271
|
+
var anyProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105272
|
+
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
105262
105273
|
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
105263
105274
|
if (ctx.unrepresentable === "throw") throw new Error("Date cannot be represented in JSON Schema");
|
|
105264
105275
|
};
|
|
@@ -105290,12 +105301,53 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
105290
105301
|
json.enum = vals;
|
|
105291
105302
|
}
|
|
105292
105303
|
};
|
|
105304
|
+
var nanProcessor = (_schema, ctx, _json, _params) => {
|
|
105305
|
+
if (ctx.unrepresentable === "throw") throw new Error("NaN cannot be represented in JSON Schema");
|
|
105306
|
+
};
|
|
105307
|
+
var templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
105308
|
+
const _json = json;
|
|
105309
|
+
const pattern = schema._zod.pattern;
|
|
105310
|
+
if (!pattern) throw new Error("Pattern not found in template literal");
|
|
105311
|
+
_json.type = "string";
|
|
105312
|
+
_json.pattern = pattern.source;
|
|
105313
|
+
};
|
|
105314
|
+
var fileProcessor = (schema, _ctx, json, _params) => {
|
|
105315
|
+
const _json = json;
|
|
105316
|
+
const file = {
|
|
105317
|
+
type: "string",
|
|
105318
|
+
format: "binary",
|
|
105319
|
+
contentEncoding: "binary"
|
|
105320
|
+
};
|
|
105321
|
+
const { minimum, maximum, mime } = schema._zod.bag;
|
|
105322
|
+
if (minimum !== void 0) file.minLength = minimum;
|
|
105323
|
+
if (maximum !== void 0) file.maxLength = maximum;
|
|
105324
|
+
if (mime) if (mime.length === 1) {
|
|
105325
|
+
file.contentMediaType = mime[0];
|
|
105326
|
+
Object.assign(_json, file);
|
|
105327
|
+
} else {
|
|
105328
|
+
Object.assign(_json, file);
|
|
105329
|
+
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
105330
|
+
}
|
|
105331
|
+
else Object.assign(_json, file);
|
|
105332
|
+
};
|
|
105333
|
+
var successProcessor = (_schema, _ctx, json, _params) => {
|
|
105334
|
+
json.type = "boolean";
|
|
105335
|
+
};
|
|
105293
105336
|
var customProcessor = (_schema, ctx, _json, _params) => {
|
|
105294
105337
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
105295
105338
|
};
|
|
105339
|
+
var functionProcessor = (_schema, ctx, _json, _params) => {
|
|
105340
|
+
if (ctx.unrepresentable === "throw") throw new Error("Function types cannot be represented in JSON Schema");
|
|
105341
|
+
};
|
|
105296
105342
|
var transformProcessor = (_schema, ctx, _json, _params) => {
|
|
105297
105343
|
if (ctx.unrepresentable === "throw") throw new Error("Transforms cannot be represented in JSON Schema");
|
|
105298
105344
|
};
|
|
105345
|
+
var mapProcessor = (_schema, ctx, _json, _params) => {
|
|
105346
|
+
if (ctx.unrepresentable === "throw") throw new Error("Map cannot be represented in JSON Schema");
|
|
105347
|
+
};
|
|
105348
|
+
var setProcessor = (_schema, ctx, _json, _params) => {
|
|
105349
|
+
if (ctx.unrepresentable === "throw") throw new Error("Set cannot be represented in JSON Schema");
|
|
105350
|
+
};
|
|
105299
105351
|
var arrayProcessor = (schema, ctx, _json, params) => {
|
|
105300
105352
|
const json = _json;
|
|
105301
105353
|
const def = schema._zod.def;
|
|
@@ -105500,6 +105552,12 @@ var readonlyProcessor = (schema, ctx, json, params) => {
|
|
|
105500
105552
|
seen.ref = def.innerType;
|
|
105501
105553
|
json.readOnly = true;
|
|
105502
105554
|
};
|
|
105555
|
+
var promiseProcessor = (schema, ctx, _json, params) => {
|
|
105556
|
+
const def = schema._zod.def;
|
|
105557
|
+
process$1(def.innerType, ctx, params);
|
|
105558
|
+
const seen = ctx.seen.get(schema);
|
|
105559
|
+
seen.ref = def.innerType;
|
|
105560
|
+
};
|
|
105503
105561
|
var optionalProcessor = (schema, ctx, _json, params) => {
|
|
105504
105562
|
const def = schema._zod.def;
|
|
105505
105563
|
process$1(def.innerType, ctx, params);
|
|
@@ -105512,6 +105570,81 @@ var lazyProcessor = (schema, ctx, _json, params) => {
|
|
|
105512
105570
|
const seen = ctx.seen.get(schema);
|
|
105513
105571
|
seen.ref = innerType;
|
|
105514
105572
|
};
|
|
105573
|
+
var allProcessors = {
|
|
105574
|
+
string: stringProcessor,
|
|
105575
|
+
number: numberProcessor,
|
|
105576
|
+
boolean: booleanProcessor,
|
|
105577
|
+
bigint: bigintProcessor,
|
|
105578
|
+
symbol: symbolProcessor,
|
|
105579
|
+
null: nullProcessor,
|
|
105580
|
+
undefined: undefinedProcessor,
|
|
105581
|
+
void: voidProcessor,
|
|
105582
|
+
never: neverProcessor,
|
|
105583
|
+
any: anyProcessor,
|
|
105584
|
+
unknown: unknownProcessor,
|
|
105585
|
+
date: dateProcessor,
|
|
105586
|
+
enum: enumProcessor,
|
|
105587
|
+
literal: literalProcessor,
|
|
105588
|
+
nan: nanProcessor,
|
|
105589
|
+
template_literal: templateLiteralProcessor,
|
|
105590
|
+
file: fileProcessor,
|
|
105591
|
+
success: successProcessor,
|
|
105592
|
+
custom: customProcessor,
|
|
105593
|
+
function: functionProcessor,
|
|
105594
|
+
transform: transformProcessor,
|
|
105595
|
+
map: mapProcessor,
|
|
105596
|
+
set: setProcessor,
|
|
105597
|
+
array: arrayProcessor,
|
|
105598
|
+
object: objectProcessor,
|
|
105599
|
+
union: unionProcessor,
|
|
105600
|
+
intersection: intersectionProcessor,
|
|
105601
|
+
tuple: tupleProcessor,
|
|
105602
|
+
record: recordProcessor,
|
|
105603
|
+
nullable: nullableProcessor,
|
|
105604
|
+
nonoptional: nonoptionalProcessor,
|
|
105605
|
+
default: defaultProcessor,
|
|
105606
|
+
prefault: prefaultProcessor,
|
|
105607
|
+
catch: catchProcessor,
|
|
105608
|
+
pipe: pipeProcessor,
|
|
105609
|
+
readonly: readonlyProcessor,
|
|
105610
|
+
promise: promiseProcessor,
|
|
105611
|
+
optional: optionalProcessor,
|
|
105612
|
+
lazy: lazyProcessor
|
|
105613
|
+
};
|
|
105614
|
+
function toJSONSchema(input, params) {
|
|
105615
|
+
if ("_idmap" in input) {
|
|
105616
|
+
const registry = input;
|
|
105617
|
+
const ctx = initializeContext({
|
|
105618
|
+
...params,
|
|
105619
|
+
processors: allProcessors
|
|
105620
|
+
});
|
|
105621
|
+
const defs = {};
|
|
105622
|
+
for (const entry of registry._idmap.entries()) {
|
|
105623
|
+
const [_, schema] = entry;
|
|
105624
|
+
process$1(schema, ctx);
|
|
105625
|
+
}
|
|
105626
|
+
const schemas = {};
|
|
105627
|
+
ctx.external = {
|
|
105628
|
+
registry,
|
|
105629
|
+
uri: params?.uri,
|
|
105630
|
+
defs
|
|
105631
|
+
};
|
|
105632
|
+
for (const entry of registry._idmap.entries()) {
|
|
105633
|
+
const [key, schema] = entry;
|
|
105634
|
+
extractDefs(ctx, schema);
|
|
105635
|
+
schemas[key] = finalize(ctx, schema);
|
|
105636
|
+
}
|
|
105637
|
+
if (Object.keys(defs).length > 0) schemas.__shared = { [ctx.target === "draft-2020-12" ? "$defs" : "definitions"]: defs };
|
|
105638
|
+
return { schemas };
|
|
105639
|
+
}
|
|
105640
|
+
const ctx = initializeContext({
|
|
105641
|
+
...params,
|
|
105642
|
+
processors: allProcessors
|
|
105643
|
+
});
|
|
105644
|
+
process$1(input, ctx);
|
|
105645
|
+
extractDefs(ctx, input);
|
|
105646
|
+
return finalize(ctx, input);
|
|
105647
|
+
}
|
|
105515
105648
|
//#endregion
|
|
105516
105649
|
//#region ../../node_modules/zod/v4/classic/iso.js
|
|
105517
105650
|
var ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
|
|
@@ -106536,7 +106669,7 @@ var ZodIssueCode = {
|
|
|
106536
106669
|
var ZodFirstPartyTypeKind;
|
|
106537
106670
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106538
106671
|
//#endregion
|
|
106539
|
-
//#region ../types/dist/sleep-
|
|
106672
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
106540
106673
|
/**
|
|
106541
106674
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106542
106675
|
* window to float samples (D455).
|
|
@@ -106691,6 +106824,9 @@ function isSecretMarkerLike(value) {
|
|
|
106691
106824
|
function isSecretClear(value) {
|
|
106692
106825
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
106693
106826
|
}
|
|
106827
|
+
function isSecretFieldView(value) {
|
|
106828
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
106829
|
+
}
|
|
106694
106830
|
/**
|
|
106695
106831
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
106696
106832
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -107133,7 +107269,7 @@ var NOT_RUN = {
|
|
|
107133
107269
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
107134
107270
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
107135
107271
|
*/
|
|
107136
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
107272
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
107137
107273
|
const first = migrations[0];
|
|
107138
107274
|
if (first === void 0) return NOT_RUN;
|
|
107139
107275
|
const owner = first.meta.owner;
|
|
@@ -107162,7 +107298,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107162
107298
|
else if (status === "blocked") blocked.push(id);
|
|
107163
107299
|
else skipped.push(id);
|
|
107164
107300
|
};
|
|
107301
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
107165
107302
|
for (const step of declaration.steps) {
|
|
107303
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
107166
107304
|
const bound = byId.get(step.id);
|
|
107167
107305
|
if (bound === void 0 || step.action === "skip") {
|
|
107168
107306
|
skipped.push(step.id);
|
|
@@ -107340,11 +107478,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
107340
107478
|
}
|
|
107341
107479
|
return out;
|
|
107342
107480
|
}
|
|
107481
|
+
/**
|
|
107482
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
107483
|
+
*
|
|
107484
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
107485
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
107486
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
107487
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
107488
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
107489
|
+
* copied onto another ref does not decrypt.
|
|
107490
|
+
*
|
|
107491
|
+
* Array items are addressed by ITEM ID, never by index
|
|
107492
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
107493
|
+
*
|
|
107494
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
107495
|
+
*/
|
|
107496
|
+
/** The URI scheme of every keychain ref. */
|
|
107497
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
107343
107498
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
107344
107499
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
107345
107500
|
function globalSecretScope() {
|
|
107346
107501
|
return GLOBAL_SCOPE;
|
|
107347
107502
|
}
|
|
107503
|
+
/**
|
|
107504
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
107505
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
107506
|
+
* addon running forked.
|
|
107507
|
+
*/
|
|
107508
|
+
function normalizeSecretOwner(addonId) {
|
|
107509
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
107510
|
+
}
|
|
107348
107511
|
function encodeSegment(value) {
|
|
107349
107512
|
return encodeURIComponent(value);
|
|
107350
107513
|
}
|
|
@@ -107355,6 +107518,16 @@ function decodeSegment(value) {
|
|
|
107355
107518
|
return null;
|
|
107356
107519
|
}
|
|
107357
107520
|
}
|
|
107521
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
107522
|
+
function formatSecretScope(scope) {
|
|
107523
|
+
switch (scope.kind) {
|
|
107524
|
+
case "global": return "global";
|
|
107525
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
107526
|
+
case "device": return `device/${scope.deviceId}`;
|
|
107527
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
107528
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
107529
|
+
}
|
|
107530
|
+
}
|
|
107358
107531
|
/**
|
|
107359
107532
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
107360
107533
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -107363,6 +107536,13 @@ function decodeSegment(value) {
|
|
|
107363
107536
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
107364
107537
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
107365
107538
|
}
|
|
107539
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
107540
|
+
function buildSecretRef(parts) {
|
|
107541
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
107542
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
107543
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
107544
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
107545
|
+
}
|
|
107366
107546
|
function parseScope(segments) {
|
|
107367
107547
|
switch (segments[0]) {
|
|
107368
107548
|
case "global": return {
|
|
@@ -108280,14 +108460,18 @@ var BaseAddon = class {
|
|
|
108280
108460
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
108281
108461
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
108282
108462
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
108463
|
+
*
|
|
108464
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
108465
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
108466
|
+
* omitted applied ids "newer").
|
|
108283
108467
|
*/
|
|
108284
|
-
async runMigrations(migrations) {
|
|
108468
|
+
async runMigrations(migrations, options) {
|
|
108285
108469
|
const api = this.ctx.api.migrations;
|
|
108286
108470
|
const report = await runAddonMigrations({
|
|
108287
108471
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
108288
108472
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
108289
108473
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
108290
|
-
}, migrations, this.ctx.logger);
|
|
108474
|
+
}, migrations, this.ctx.logger, options);
|
|
108291
108475
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
108292
108476
|
return report;
|
|
108293
108477
|
}
|
|
@@ -108628,6 +108812,10 @@ var BaseAddon = class {
|
|
|
108628
108812
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
108629
108813
|
const patch = {};
|
|
108630
108814
|
for (const key of keys) {
|
|
108815
|
+
if (tree.get(key)?.kind === "array") {
|
|
108816
|
+
patch[key] = [];
|
|
108817
|
+
continue;
|
|
108818
|
+
}
|
|
108631
108819
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
108632
108820
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
108633
108821
|
}
|
|
@@ -109267,6 +109455,45 @@ function normalizeAddonInitResult(result) {
|
|
|
109267
109455
|
if (Array.isArray(result)) return { providers: result };
|
|
109268
109456
|
return result;
|
|
109269
109457
|
}
|
|
109458
|
+
var PREFIX = "records/";
|
|
109459
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
109460
|
+
var RecordsError = class extends Error {
|
|
109461
|
+
code;
|
|
109462
|
+
detail;
|
|
109463
|
+
constructor(code, detail) {
|
|
109464
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
109465
|
+
this.name = "RecordsError";
|
|
109466
|
+
this.code = code;
|
|
109467
|
+
this.detail = detail;
|
|
109468
|
+
}
|
|
109469
|
+
};
|
|
109470
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
109471
|
+
var RevConflictError = class extends RecordsError {
|
|
109472
|
+
constructor(detail) {
|
|
109473
|
+
super("rev-conflict", detail);
|
|
109474
|
+
this.name = "RevConflictError";
|
|
109475
|
+
}
|
|
109476
|
+
};
|
|
109477
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
109478
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
109479
|
+
constructor(detail) {
|
|
109480
|
+
super("secret-unavailable", detail);
|
|
109481
|
+
this.name = "RecordSecretUnavailableError";
|
|
109482
|
+
}
|
|
109483
|
+
};
|
|
109484
|
+
var InvalidRecordError = class extends RecordsError {
|
|
109485
|
+
constructor(detail) {
|
|
109486
|
+
super("invalid-record", detail);
|
|
109487
|
+
this.name = "InvalidRecordError";
|
|
109488
|
+
}
|
|
109489
|
+
};
|
|
109490
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
109491
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
109492
|
+
constructor(detail) {
|
|
109493
|
+
super("declaration-refused", detail);
|
|
109494
|
+
this.name = "DeclarationRefusedError";
|
|
109495
|
+
}
|
|
109496
|
+
};
|
|
109270
109497
|
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109271
109498
|
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109272
109499
|
var RECORD_DATABASES = ["settings", "events"];
|
|
@@ -109282,6 +109509,183 @@ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
|
109282
109509
|
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109283
109510
|
var RECORD_BATCH_MAX = 1e3;
|
|
109284
109511
|
var RECORD_QUERY_MAX = 1e3;
|
|
109512
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
109513
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
109514
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
109515
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
109516
|
+
/**
|
|
109517
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
109518
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
109519
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
109520
|
+
*
|
|
109521
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
109522
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
109523
|
+
* key releases exactly that key's secret.
|
|
109524
|
+
*
|
|
109525
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
109526
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
109527
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
109528
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
109529
|
+
*
|
|
109530
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
109531
|
+
*/
|
|
109532
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
109533
|
+
function parseSecretPath(path) {
|
|
109534
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
109535
|
+
return path.split(".").map((raw) => {
|
|
109536
|
+
const each = raw.endsWith("[]");
|
|
109537
|
+
const map = raw.endsWith("{}");
|
|
109538
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
109539
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
109540
|
+
return {
|
|
109541
|
+
key,
|
|
109542
|
+
each,
|
|
109543
|
+
map
|
|
109544
|
+
};
|
|
109545
|
+
});
|
|
109546
|
+
}
|
|
109547
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
109548
|
+
function secretArrayPrefixes(path) {
|
|
109549
|
+
const segments = parseSecretPath(path);
|
|
109550
|
+
const prefixes = [];
|
|
109551
|
+
const walked = [];
|
|
109552
|
+
for (const segment of segments) {
|
|
109553
|
+
walked.push(segment.key);
|
|
109554
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
109555
|
+
}
|
|
109556
|
+
return prefixes;
|
|
109557
|
+
}
|
|
109558
|
+
function isRecord$4(value) {
|
|
109559
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109560
|
+
}
|
|
109561
|
+
function elementId(element, idKey, declared, prefix) {
|
|
109562
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
109563
|
+
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`);
|
|
109564
|
+
return id;
|
|
109565
|
+
}
|
|
109566
|
+
/**
|
|
109567
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
109568
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
109569
|
+
* result is a new body, `data` is untouched.
|
|
109570
|
+
*/
|
|
109571
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
109572
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
109573
|
+
if (!isRecord$4(node)) return node;
|
|
109574
|
+
const segment = segments[index];
|
|
109575
|
+
if (segment === void 0) return node;
|
|
109576
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
109577
|
+
const child = node[segment.key];
|
|
109578
|
+
const keys = [...walkedKeys, segment.key];
|
|
109579
|
+
const last = index === segments.length - 1;
|
|
109580
|
+
if (segment.each) {
|
|
109581
|
+
if (!Array.isArray(child)) return node;
|
|
109582
|
+
const prefix = keys.join(".");
|
|
109583
|
+
const idKey = arrayKeys[prefix];
|
|
109584
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
109585
|
+
const mapped = child.map((element) => {
|
|
109586
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
109587
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
109588
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
109589
|
+
});
|
|
109590
|
+
return {
|
|
109591
|
+
...node,
|
|
109592
|
+
[segment.key]: mapped
|
|
109593
|
+
};
|
|
109594
|
+
}
|
|
109595
|
+
if (segment.map) {
|
|
109596
|
+
if (!isRecord$4(child)) return node;
|
|
109597
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
109598
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
109599
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
109600
|
+
return [mapKey, visit({
|
|
109601
|
+
declared,
|
|
109602
|
+
concrete,
|
|
109603
|
+
value
|
|
109604
|
+
})];
|
|
109605
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
109606
|
+
return {
|
|
109607
|
+
...node,
|
|
109608
|
+
[segment.key]: Object.fromEntries(kept)
|
|
109609
|
+
};
|
|
109610
|
+
}
|
|
109611
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
109612
|
+
if (last) {
|
|
109613
|
+
const replacement = visit({
|
|
109614
|
+
declared,
|
|
109615
|
+
concrete,
|
|
109616
|
+
value: child
|
|
109617
|
+
});
|
|
109618
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
109619
|
+
return {
|
|
109620
|
+
...node,
|
|
109621
|
+
[segment.key]: replacement
|
|
109622
|
+
};
|
|
109623
|
+
}
|
|
109624
|
+
return {
|
|
109625
|
+
...node,
|
|
109626
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
109627
|
+
};
|
|
109628
|
+
}
|
|
109629
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
109630
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
109631
|
+
let current = data;
|
|
109632
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
109633
|
+
return current;
|
|
109634
|
+
}
|
|
109635
|
+
/** Every present secret leaf, read-only. */
|
|
109636
|
+
function collectSecretLeaves(data, paths, arrayKeys) {
|
|
109637
|
+
const found = [];
|
|
109638
|
+
mapSecretLeaves(data, paths, arrayKeys, (leaf) => {
|
|
109639
|
+
found.push(leaf);
|
|
109640
|
+
return leaf.value;
|
|
109641
|
+
});
|
|
109642
|
+
return found;
|
|
109643
|
+
}
|
|
109644
|
+
/**
|
|
109645
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
109646
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
109647
|
+
* already holds during an adoption.
|
|
109648
|
+
*/
|
|
109649
|
+
function recordSecretCollection(kind) {
|
|
109650
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
109651
|
+
}
|
|
109652
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
109653
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
109654
|
+
return buildSecretRef({
|
|
109655
|
+
owner,
|
|
109656
|
+
scope: {
|
|
109657
|
+
kind: "row",
|
|
109658
|
+
collection: recordSecretCollection(kind),
|
|
109659
|
+
rowId
|
|
109660
|
+
},
|
|
109661
|
+
fieldPath: concretePath
|
|
109662
|
+
});
|
|
109663
|
+
}
|
|
109664
|
+
/**
|
|
109665
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
109666
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
109667
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
109668
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
109669
|
+
* stored secret (the records door reads a view as KEEP).
|
|
109670
|
+
*
|
|
109671
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
109672
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
109673
|
+
*/
|
|
109674
|
+
function isRecord$3(value) {
|
|
109675
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109676
|
+
}
|
|
109677
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
109678
|
+
if (decl.secretPaths.length === 0) return data;
|
|
109679
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
109680
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
109681
|
+
return {
|
|
109682
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
109683
|
+
v: 1,
|
|
109684
|
+
set: true
|
|
109685
|
+
};
|
|
109686
|
+
});
|
|
109687
|
+
return isRecord$3(shown) ? shown : data;
|
|
109688
|
+
}
|
|
109285
109689
|
/**
|
|
109286
109690
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
109287
109691
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -109354,7 +109758,8 @@ function isZodSecret(schema) {
|
|
|
109354
109758
|
}
|
|
109355
109759
|
/**
|
|
109356
109760
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
109357
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
109761
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
109762
|
+
* map" (`applianceSecrets{}`). A declared
|
|
109358
109763
|
* object or array is reported as one path and not descended into — everything
|
|
109359
109764
|
* under it is secret.
|
|
109360
109765
|
*/
|
|
@@ -109365,6 +109770,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
109365
109770
|
const def = defOf(layer);
|
|
109366
109771
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
109367
109772
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
109773
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
109368
109774
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
109369
109775
|
else if (def.type === "intersection") {
|
|
109370
109776
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -109439,6 +109845,126 @@ var RecordKindDeclarationSchema = object({
|
|
|
109439
109845
|
}).strict();
|
|
109440
109846
|
/** The kind names: lower-case, dash separated, short enough for an index name. */
|
|
109441
109847
|
var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
|
|
109848
|
+
/** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
|
|
109849
|
+
function assertValidDeclaration(name, decl) {
|
|
109850
|
+
const refuse = (why) => {
|
|
109851
|
+
throw new DeclarationRefusedError(`kind "${name}": ${why}`);
|
|
109852
|
+
};
|
|
109853
|
+
if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
|
|
109854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109855
|
+
for (const index of decl.indexes) {
|
|
109856
|
+
if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
|
|
109857
|
+
seen.add(index.name);
|
|
109858
|
+
}
|
|
109859
|
+
const { retention } = decl;
|
|
109860
|
+
const hasSecrets = decl.secretPaths.length > 0;
|
|
109861
|
+
if ("ttlMs" in retention) {
|
|
109862
|
+
if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
|
|
109863
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
|
|
109864
|
+
}
|
|
109865
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
|
|
109866
|
+
if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
|
|
109867
|
+
if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
|
|
109868
|
+
if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
|
|
109869
|
+
const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
|
|
109870
|
+
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)`);
|
|
109871
|
+
}
|
|
109872
|
+
if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
|
|
109873
|
+
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`);
|
|
109874
|
+
}
|
|
109875
|
+
function bodyFieldsOf(schema) {
|
|
109876
|
+
return { ...toJSONSchema(schema, {
|
|
109877
|
+
unrepresentable: "any",
|
|
109878
|
+
io: "input"
|
|
109879
|
+
}) };
|
|
109880
|
+
}
|
|
109881
|
+
function isRecordLike(value) {
|
|
109882
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109883
|
+
}
|
|
109884
|
+
/** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
|
|
109885
|
+
function hasNameField(json, field) {
|
|
109886
|
+
const branches = json["oneOf"] ?? json["anyOf"];
|
|
109887
|
+
if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
|
|
109888
|
+
const properties = json["properties"];
|
|
109889
|
+
return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
|
|
109890
|
+
}
|
|
109891
|
+
/**
|
|
109892
|
+
* Declare the kinds of a package. The keys are the kind names. The result is
|
|
109893
|
+
* what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
|
|
109894
|
+
* types its handle from.
|
|
109895
|
+
*/
|
|
109896
|
+
function defineRecordKinds(specs) {
|
|
109897
|
+
const out = {};
|
|
109898
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
109899
|
+
const secretPaths = collectZodSecretPaths(spec.schema);
|
|
109900
|
+
const nameField = spec.entry?.nameField ?? "name";
|
|
109901
|
+
const fields = bodyFieldsOf(spec.schema);
|
|
109902
|
+
if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
|
|
109903
|
+
const parsed = RecordKindDeclarationSchema.parse({
|
|
109904
|
+
database: spec.database,
|
|
109905
|
+
class: spec.class,
|
|
109906
|
+
writeTrigger: spec.writeTrigger,
|
|
109907
|
+
device: spec.device,
|
|
109908
|
+
at: spec.at,
|
|
109909
|
+
indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
|
|
109910
|
+
retention: spec.retention,
|
|
109911
|
+
deviceRemoval: spec.deviceRemoval,
|
|
109912
|
+
maxDataBytes: spec.maxDataBytes,
|
|
109913
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
109914
|
+
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
109915
|
+
transactFamily: spec.transactFamily,
|
|
109916
|
+
entry: spec.entry === void 0 ? void 0 : {
|
|
109917
|
+
title: spec.entry.title,
|
|
109918
|
+
nameField,
|
|
109919
|
+
fields
|
|
109920
|
+
}
|
|
109921
|
+
});
|
|
109922
|
+
assertValidDeclaration(name, parsed);
|
|
109923
|
+
out[name] = {
|
|
109924
|
+
name,
|
|
109925
|
+
schema: spec.schema,
|
|
109926
|
+
declaration: parsed
|
|
109927
|
+
};
|
|
109928
|
+
}
|
|
109929
|
+
return out;
|
|
109930
|
+
}
|
|
109931
|
+
/**
|
|
109932
|
+
* Entry kinds (D881): every list of user-created entries (notification
|
|
109933
|
+
* targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
|
|
109934
|
+
* kind-specific body, and the admin UI lists/adds/edits/deletes all of them
|
|
109935
|
+
* through the `record-entries` capability with password fields derived from
|
|
109936
|
+
* the schema (`.meta({ secret: true })`).
|
|
109937
|
+
*/
|
|
109938
|
+
/** The body of a plain entry: a name and the fields the kind adds. */
|
|
109939
|
+
function entryBody(shape) {
|
|
109940
|
+
return object({
|
|
109941
|
+
name: string().min(1),
|
|
109942
|
+
...shape
|
|
109943
|
+
});
|
|
109944
|
+
}
|
|
109945
|
+
/**
|
|
109946
|
+
* Declare one entry kind. An entry is configuration: never trimmed by the
|
|
109947
|
+
* engine, never aged, belonging to no device.
|
|
109948
|
+
*/
|
|
109949
|
+
function defineEntryKind(name, spec) {
|
|
109950
|
+
const defined = defineRecordKinds({ [name]: {
|
|
109951
|
+
schema: spec.schema,
|
|
109952
|
+
class: "config",
|
|
109953
|
+
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
109954
|
+
retention: { none: "an entry goes only when the operator deletes it" },
|
|
109955
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
109956
|
+
...spec.database === void 0 ? {} : { database: spec.database },
|
|
109957
|
+
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
109958
|
+
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
109959
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
109960
|
+
entry: {
|
|
109961
|
+
title: spec.title,
|
|
109962
|
+
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
109963
|
+
}
|
|
109964
|
+
} })[name];
|
|
109965
|
+
if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
|
|
109966
|
+
return defined;
|
|
109967
|
+
}
|
|
109442
109968
|
object({
|
|
109443
109969
|
field: string().min(1),
|
|
109444
109970
|
on: literal("advance")
|
|
@@ -112243,7 +112769,7 @@ function sleep(ms) {
|
|
|
112243
112769
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112244
112770
|
}
|
|
112245
112771
|
//#endregion
|
|
112246
|
-
//#region ../types/dist/composition-
|
|
112772
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
112247
112773
|
/**
|
|
112248
112774
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112249
112775
|
*
|
|
@@ -113006,38 +113532,6 @@ function validateExpressionSource(src) {
|
|
|
113006
113532
|
return null;
|
|
113007
113533
|
}
|
|
113008
113534
|
/**
|
|
113009
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
113010
|
-
*
|
|
113011
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
113012
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
113013
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
113014
|
-
*
|
|
113015
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
113016
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
113017
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
113018
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
113019
|
-
* can move later without moving the others.
|
|
113020
|
-
*/
|
|
113021
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
113022
|
-
var APPLIANCE_KINDS = [
|
|
113023
|
-
"washer",
|
|
113024
|
-
"dryer",
|
|
113025
|
-
"dishwasher"
|
|
113026
|
-
];
|
|
113027
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
113028
|
-
/**
|
|
113029
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
113030
|
-
*
|
|
113031
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
113032
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
113033
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
113034
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
113035
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
113036
|
-
* the smallest radiator.
|
|
113037
|
-
*/
|
|
113038
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
113039
|
-
DeviceType.Heater;
|
|
113040
|
-
/**
|
|
113041
113535
|
* Statistics and integrals over a history read (D944).
|
|
113042
113536
|
*
|
|
113043
113537
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -113181,34 +113675,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
113181
113675
|
/** Actuator groups only. Readings leave this unset. */
|
|
113182
113676
|
active: ActiveCriterionSchema.optional()
|
|
113183
113677
|
}),
|
|
113184
|
-
object({
|
|
113185
|
-
kind: literal("appliance"),
|
|
113186
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
113187
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
113188
|
-
thresholdWatts: number().positive(),
|
|
113189
|
-
startDwellMs: number().int().nonnegative(),
|
|
113190
|
-
stopDwellMs: number().int().nonnegative(),
|
|
113191
|
-
member: HelperMemberSchema,
|
|
113192
|
-
/**
|
|
113193
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
113194
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
113195
|
-
* that plug (power-source link of a NEW-target composition).
|
|
113196
|
-
*/
|
|
113197
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
113198
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
113199
|
-
offSeconds: number().optional()
|
|
113200
|
-
}),
|
|
113201
|
-
object({
|
|
113202
|
-
kind: literal("heater"),
|
|
113203
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
113204
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
113205
|
-
plug: HelperMemberSchema,
|
|
113206
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
113207
|
-
meter: HelperMemberSchema.optional(),
|
|
113208
|
-
thresholdWatts: number().positive(),
|
|
113209
|
-
startDwellMs: number().int().nonnegative(),
|
|
113210
|
-
stopDwellMs: number().int().nonnegative()
|
|
113211
|
-
}),
|
|
113212
113678
|
object({
|
|
113213
113679
|
kind: literal("statistic"),
|
|
113214
113680
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -113432,7 +113898,7 @@ var CompositionSchema = object({
|
|
|
113432
113898
|
kind: literal("existing"),
|
|
113433
113899
|
device: CompositionSourceRefSchema
|
|
113434
113900
|
})]),
|
|
113435
|
-
features: array(CompositionFeatureSchema).min(
|
|
113901
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
113436
113902
|
/**
|
|
113437
113903
|
* Present when the block is a helper template. Absent on a free
|
|
113438
113904
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -113671,6 +114137,205 @@ var IntegrationInstanceRefSchema = object({
|
|
|
113671
114137
|
]),
|
|
113672
114138
|
candidateStableId: string().optional()
|
|
113673
114139
|
});
|
|
114140
|
+
/**
|
|
114141
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
114142
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
114143
|
+
*
|
|
114144
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
114145
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
114146
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
114147
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
114148
|
+
*
|
|
114149
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
114150
|
+
* token or the view the form was given KEEP the stored secret;
|
|
114151
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
114152
|
+
*/
|
|
114153
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
114154
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
114155
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
114156
|
+
function topLevelSecretKeys(paths) {
|
|
114157
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
114158
|
+
}
|
|
114159
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
114160
|
+
function secretMapKeys(paths) {
|
|
114161
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
114162
|
+
}
|
|
114163
|
+
/**
|
|
114164
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
114165
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
114166
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
114167
|
+
*/
|
|
114168
|
+
function secretMapPatch(value) {
|
|
114169
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
114170
|
+
const out = {};
|
|
114171
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
114172
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
114173
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
114174
|
+
}
|
|
114175
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
114176
|
+
function declaresEnabled(kind) {
|
|
114177
|
+
const json = toJSONSchema(kind.schema, {
|
|
114178
|
+
unrepresentable: "any",
|
|
114179
|
+
io: "input"
|
|
114180
|
+
});
|
|
114181
|
+
const properties = Reflect.get(json, "properties");
|
|
114182
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
114183
|
+
}
|
|
114184
|
+
var RecordIntegrationInstances = class {
|
|
114185
|
+
records;
|
|
114186
|
+
kind;
|
|
114187
|
+
hooks;
|
|
114188
|
+
handle;
|
|
114189
|
+
secretKeys;
|
|
114190
|
+
secretMaps;
|
|
114191
|
+
nameField;
|
|
114192
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
114193
|
+
registryOwnsEnabled;
|
|
114194
|
+
constructor(records, kind, hooks) {
|
|
114195
|
+
this.records = records;
|
|
114196
|
+
this.kind = kind;
|
|
114197
|
+
this.hooks = hooks;
|
|
114198
|
+
this.handle = records.kind(kind);
|
|
114199
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
114200
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
114201
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
114202
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
114203
|
+
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"`);
|
|
114204
|
+
}
|
|
114205
|
+
/**
|
|
114206
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
114207
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
114208
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
114209
|
+
*/
|
|
114210
|
+
async readResolved(id) {
|
|
114211
|
+
try {
|
|
114212
|
+
const record = await this.handle.getResolved(id);
|
|
114213
|
+
return record === null ? null : {
|
|
114214
|
+
id,
|
|
114215
|
+
body: record.data,
|
|
114216
|
+
enabled: record.data.enabled !== false,
|
|
114217
|
+
secrets: "resolved"
|
|
114218
|
+
};
|
|
114219
|
+
} catch (err) {
|
|
114220
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
114221
|
+
const stored = await this.handle.get(id);
|
|
114222
|
+
if (stored === null) return null;
|
|
114223
|
+
return {
|
|
114224
|
+
id,
|
|
114225
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
114226
|
+
enabled: stored.data.enabled !== false,
|
|
114227
|
+
secrets: "unavailable"
|
|
114228
|
+
};
|
|
114229
|
+
}
|
|
114230
|
+
}
|
|
114231
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
114232
|
+
async getResolved(id) {
|
|
114233
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
114234
|
+
}
|
|
114235
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
114236
|
+
async listResolved() {
|
|
114237
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
114238
|
+
const out = [];
|
|
114239
|
+
for (const record of page.records) {
|
|
114240
|
+
const read = await this.readResolved(record.id);
|
|
114241
|
+
if (read !== null) out.push(read);
|
|
114242
|
+
}
|
|
114243
|
+
return out;
|
|
114244
|
+
}
|
|
114245
|
+
viewOf(record) {
|
|
114246
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
114247
|
+
const rawName = record.data[this.nameField];
|
|
114248
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
114249
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
114250
|
+
return {
|
|
114251
|
+
id: record.id,
|
|
114252
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
114253
|
+
configured: this.hooks.configured(record.data),
|
|
114254
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
114255
|
+
formValues: shown,
|
|
114256
|
+
...kind !== void 0 ? { kind } : {},
|
|
114257
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
114258
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
114259
|
+
};
|
|
114260
|
+
}
|
|
114261
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
114262
|
+
...this.hooks.description,
|
|
114263
|
+
mintsId: true
|
|
114264
|
+
};
|
|
114265
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
114266
|
+
listInstances = async () => {
|
|
114267
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
114268
|
+
};
|
|
114269
|
+
getInstance = async ({ id }) => {
|
|
114270
|
+
const record = await this.handle.get(id);
|
|
114271
|
+
return record === null ? null : this.viewOf(record);
|
|
114272
|
+
};
|
|
114273
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
114274
|
+
createInstance = async ({ id, name, kind }) => {
|
|
114275
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
114276
|
+
name,
|
|
114277
|
+
...kind !== void 0 ? { kind } : {}
|
|
114278
|
+
});
|
|
114279
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
114280
|
+
await this.handle.put({
|
|
114281
|
+
id: minted,
|
|
114282
|
+
data: this.hooks.initialBody(name, kind)
|
|
114283
|
+
}, { ifRev: 0 });
|
|
114284
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
114285
|
+
return { id: minted };
|
|
114286
|
+
};
|
|
114287
|
+
updateInstance = async ({ id, name, values }) => {
|
|
114288
|
+
const patch = {};
|
|
114289
|
+
for (const [key, value] of Object.entries(values)) {
|
|
114290
|
+
if (value === void 0) continue;
|
|
114291
|
+
if (this.secretMaps.has(key)) {
|
|
114292
|
+
const entries = secretMapPatch(value);
|
|
114293
|
+
if (entries !== null) patch[key] = entries;
|
|
114294
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
114295
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
114296
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
114297
|
+
}
|
|
114298
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
114299
|
+
await this.handle.patch(id, patch);
|
|
114300
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
114301
|
+
return { ok: true };
|
|
114302
|
+
};
|
|
114303
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
114304
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
114305
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
114306
|
+
await this.handle.patch(id, { enabled });
|
|
114307
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
114308
|
+
return { ok: true };
|
|
114309
|
+
};
|
|
114310
|
+
/** The row and its keychain refs go together (the records door). */
|
|
114311
|
+
deleteInstance = async ({ id }) => {
|
|
114312
|
+
await this.handle.delete(id);
|
|
114313
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
114314
|
+
return { ok: true };
|
|
114315
|
+
};
|
|
114316
|
+
testInstance = async (input) => {
|
|
114317
|
+
if (this.hooks.test === void 0) return {
|
|
114318
|
+
success: false,
|
|
114319
|
+
status: "unsupported",
|
|
114320
|
+
error: "This integration declares no connection test"
|
|
114321
|
+
};
|
|
114322
|
+
return this.hooks.test(input);
|
|
114323
|
+
};
|
|
114324
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
114325
|
+
toProvider() {
|
|
114326
|
+
return {
|
|
114327
|
+
describe: this.describe,
|
|
114328
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
114329
|
+
listInstances: this.listInstances,
|
|
114330
|
+
getInstance: this.getInstance,
|
|
114331
|
+
createInstance: this.createInstance,
|
|
114332
|
+
updateInstance: this.updateInstance,
|
|
114333
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
114334
|
+
deleteInstance: this.deleteInstance,
|
|
114335
|
+
testInstance: this.testInstance
|
|
114336
|
+
};
|
|
114337
|
+
}
|
|
114338
|
+
};
|
|
113674
114339
|
var EncodeProfileSchema = object({
|
|
113675
114340
|
video: object({
|
|
113676
114341
|
codec: _enum([
|
|
@@ -114736,7 +115401,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
114736
115401
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
114737
115402
|
* catalogue lists the id (a newer database).
|
|
114738
115403
|
*/
|
|
114739
|
-
meta: MigrationMetaSchema.optional()
|
|
115404
|
+
meta: MigrationMetaSchema.optional(),
|
|
115405
|
+
/**
|
|
115406
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
115407
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
115408
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
115409
|
+
*/
|
|
115410
|
+
ownerNotLoaded: boolean().optional()
|
|
114740
115411
|
});
|
|
114741
115412
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
114742
115413
|
var MigrationPendingEntrySchema = object({
|
|
@@ -114770,6 +115441,304 @@ var MigrationOwnerStepSchema = object({
|
|
|
114770
115441
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114771
115442
|
});
|
|
114772
115443
|
/**
|
|
115444
|
+
* Move an integration's legacy `integration_settings` rows into a typed record of the addon's own
|
|
115445
|
+
* kind (planning#416, the one store). The record is keyed by the `integrations` registry id, so
|
|
115446
|
+
* the integration id, the devices stamped with it and the automations that name it do not change.
|
|
115447
|
+
*
|
|
115448
|
+
* Order, so a crash or a locked keychain is safe at every point:
|
|
115449
|
+
*
|
|
115450
|
+
* 1. Phase A, read only: every row of every integration of the addon is read with its secrets
|
|
115451
|
+
* resolved. One that cannot be read (a locked keychain) throws, naming the integration, before
|
|
115452
|
+
* anything is written; `block-owner` then holds the addon.
|
|
115453
|
+
* 2. Phase B, per integration: the addon's `bodyFrom` builds the body; the record is put with
|
|
115454
|
+
* `ifRev: 0` (an existing record is never overwritten). A record this pass CREATED is read back
|
|
115455
|
+
* and every secret leaf must equal what the rows held, else the record is deleted and the pass
|
|
115456
|
+
* throws; an EXISTING record must hold every secret the rows carry, else the rows are kept and
|
|
115457
|
+
* a warning names the path. Only then are the rows (and their keychain values) cleared.
|
|
115458
|
+
*
|
|
115459
|
+
* Idempotent: an integration whose rows are gone and whose record exists is skipped, so a rerun is
|
|
115460
|
+
* a no-op. A default integration with no rows still gets its record (an empty body, D985).
|
|
115461
|
+
* Never logs or throws a secret value; messages name integration ids and secret PATHS only.
|
|
115462
|
+
*/
|
|
115463
|
+
function secretLeavesOf(body, spec) {
|
|
115464
|
+
const out = /* @__PURE__ */ new Map();
|
|
115465
|
+
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);
|
|
115466
|
+
return out;
|
|
115467
|
+
}
|
|
115468
|
+
function why(err) {
|
|
115469
|
+
return err instanceof Error ? err.message : String(err);
|
|
115470
|
+
}
|
|
115471
|
+
async function moveIntegrationRowsToRecords(ports, spec) {
|
|
115472
|
+
const integrations = await ports.listIntegrations(spec.addonId);
|
|
115473
|
+
if (integrations.length === 0) return {
|
|
115474
|
+
outcome: "already-done",
|
|
115475
|
+
summary: { integrations: 0 }
|
|
115476
|
+
};
|
|
115477
|
+
const rowsOf = /* @__PURE__ */ new Map();
|
|
115478
|
+
for (const integration of integrations) try {
|
|
115479
|
+
rowsOf.set(integration.id, await ports.readRows(integration.id));
|
|
115480
|
+
} catch (err) {
|
|
115481
|
+
throw new Error(`integration '${integration.id}': the legacy settings cannot be read with their secrets (${why(err)}) - nothing moved`, { cause: err });
|
|
115482
|
+
}
|
|
115483
|
+
let created = 0;
|
|
115484
|
+
let cleared = 0;
|
|
115485
|
+
let kept = 0;
|
|
115486
|
+
for (const integration of integrations) {
|
|
115487
|
+
const values = rowsOf.get(integration.id) ?? {};
|
|
115488
|
+
const hasRows = Object.keys(values).length > 0;
|
|
115489
|
+
const body = spec.bodyFrom(values, ports, integration);
|
|
115490
|
+
const put = await ports.putRecord(integration.id, body);
|
|
115491
|
+
if (put === "created") created += 1;
|
|
115492
|
+
if (!hasRows) continue;
|
|
115493
|
+
const wanted = secretLeavesOf(body, spec);
|
|
115494
|
+
const back = await ports.readRecord(integration.id);
|
|
115495
|
+
if (back === null) throw new Error(`integration '${integration.id}': the record is gone after it was put`);
|
|
115496
|
+
const held = secretLeavesOf(back, spec);
|
|
115497
|
+
if (put === "created") {
|
|
115498
|
+
const wrong = [...wanted].filter(([path, value]) => held.get(path) !== value).map(([p]) => p);
|
|
115499
|
+
if (wrong.length > 0) {
|
|
115500
|
+
await ports.deleteRecord(integration.id);
|
|
115501
|
+
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`);
|
|
115502
|
+
}
|
|
115503
|
+
} else {
|
|
115504
|
+
const missing = [...wanted].filter(([path]) => !held.has(path)).map(([p]) => p);
|
|
115505
|
+
if (missing.length > 0) {
|
|
115506
|
+
ports.warn(`integration '${integration.id}': the existing record holds no ${missing.join(", ")}; the legacy settings are kept, not cleared`);
|
|
115507
|
+
kept += 1;
|
|
115508
|
+
continue;
|
|
115509
|
+
}
|
|
115510
|
+
}
|
|
115511
|
+
await ports.clearRows(integration.id);
|
|
115512
|
+
cleared += 1;
|
|
115513
|
+
}
|
|
115514
|
+
return created === 0 && cleared === 0 ? {
|
|
115515
|
+
outcome: "already-done",
|
|
115516
|
+
summary: {
|
|
115517
|
+
integrations: integrations.length,
|
|
115518
|
+
kept
|
|
115519
|
+
}
|
|
115520
|
+
} : {
|
|
115521
|
+
outcome: "done",
|
|
115522
|
+
summary: {
|
|
115523
|
+
integrations: integrations.length,
|
|
115524
|
+
created,
|
|
115525
|
+
cleared,
|
|
115526
|
+
kept
|
|
115527
|
+
}
|
|
115528
|
+
};
|
|
115529
|
+
}
|
|
115530
|
+
/**
|
|
115531
|
+
* The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
|
|
115532
|
+
* from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
|
|
115533
|
+
* `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
|
|
115534
|
+
* the new record through the addon's own records handle. The hub deletes the rows and their
|
|
115535
|
+
* keychain values; the addon never touches the registry.
|
|
115536
|
+
*/
|
|
115537
|
+
function integrationRowsPortsOverApi(deps) {
|
|
115538
|
+
return {
|
|
115539
|
+
warn: (message) => deps.log.warn(message),
|
|
115540
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
115541
|
+
id: i.id,
|
|
115542
|
+
name: i.name
|
|
115543
|
+
})),
|
|
115544
|
+
readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
|
|
115545
|
+
putRecord: async (id, body) => {
|
|
115546
|
+
if (await deps.records.get(id) !== null) return "exists";
|
|
115547
|
+
try {
|
|
115548
|
+
await deps.records.put({
|
|
115549
|
+
id,
|
|
115550
|
+
data: body
|
|
115551
|
+
}, { ifRev: 0 });
|
|
115552
|
+
} catch (err) {
|
|
115553
|
+
if (err instanceof RevConflictError) return "exists";
|
|
115554
|
+
throw err;
|
|
115555
|
+
}
|
|
115556
|
+
return "created";
|
|
115557
|
+
},
|
|
115558
|
+
readRecord: async (id) => (await deps.records.getResolved(id))?.data ?? null,
|
|
115559
|
+
deleteRecord: async (id) => {
|
|
115560
|
+
await deps.records.delete(id);
|
|
115561
|
+
},
|
|
115562
|
+
clearRows: async (integrationId) => {
|
|
115563
|
+
await deps.api.clearSettings({ id: integrationId });
|
|
115564
|
+
}
|
|
115565
|
+
};
|
|
115566
|
+
}
|
|
115567
|
+
function ordinalOf(id) {
|
|
115568
|
+
const digits = id.replace(/\D+/g, "");
|
|
115569
|
+
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
115570
|
+
}
|
|
115571
|
+
/** The lowest-numbered eligible integration, or null. */
|
|
115572
|
+
function defaultIntegrationOf(integrations, eligible) {
|
|
115573
|
+
return [...eligible === void 0 ? integrations : integrations.filter(eligible)].sort((a, b) => ordinalOf(a.id) - ordinalOf(b.id) || (a.id < b.id ? -1 : 1))[0] ?? null;
|
|
115574
|
+
}
|
|
115575
|
+
function unstamped(device) {
|
|
115576
|
+
return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
|
|
115577
|
+
}
|
|
115578
|
+
/**
|
|
115579
|
+
* An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
|
|
115580
|
+
* `generic`, named by its stable id, that is not registered and holds no configuration. The hub
|
|
115581
|
+
* listing does not carry the `registered` bit, so the shape picks the candidates and the missing
|
|
115582
|
+
* configuration decides.
|
|
115583
|
+
*/
|
|
115584
|
+
async function isReservation(ports, device) {
|
|
115585
|
+
if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
|
|
115586
|
+
return !await ports.hasConfig(device.id);
|
|
115587
|
+
}
|
|
115588
|
+
async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
115589
|
+
const all = await ports.listDevices(spec.addonId);
|
|
115590
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
115591
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
115592
|
+
for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
|
|
115593
|
+
const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
|
|
115594
|
+
const summaryBase = { reservations: reservations.size };
|
|
115595
|
+
if (orphans.length === 0) return {
|
|
115596
|
+
outcome: "already-done",
|
|
115597
|
+
summary: {
|
|
115598
|
+
claimed: 0,
|
|
115599
|
+
unresolved: 0,
|
|
115600
|
+
...summaryBase
|
|
115601
|
+
}
|
|
115602
|
+
};
|
|
115603
|
+
const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
|
|
115604
|
+
const plan = /* @__PURE__ */ new Map();
|
|
115605
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
115606
|
+
const failed = /* @__PURE__ */ new Set();
|
|
115607
|
+
const resolve = (device, seen) => {
|
|
115608
|
+
if (!unstamped(device)) return device.integrationId ?? null;
|
|
115609
|
+
const planned = plan.get(device.id);
|
|
115610
|
+
if (planned !== void 0) return planned;
|
|
115611
|
+
if (failed.has(device.id)) return null;
|
|
115612
|
+
let target = null;
|
|
115613
|
+
const parentId = device.parentDeviceId ?? null;
|
|
115614
|
+
if (parentId === null) target = fallback?.id ?? null;
|
|
115615
|
+
else {
|
|
115616
|
+
const parent = byId.get(parentId);
|
|
115617
|
+
if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
|
|
115618
|
+
}
|
|
115619
|
+
if (target === null) failed.add(device.id);
|
|
115620
|
+
else plan.set(device.id, target);
|
|
115621
|
+
return target;
|
|
115622
|
+
};
|
|
115623
|
+
if (orphans.some((d) => (d.parentDeviceId ?? null) === null) && fallback === null) throw new Error(`${orphans.length} device(s) of '${spec.addonId}' carry no integration and the addon has no integration to claim them to`);
|
|
115624
|
+
for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
|
|
115625
|
+
for (const id of unresolved) ports.warnDevice(`device ${id} of '${spec.addonId}' carries no integration and its owner chain does not resolve to one: left unstamped`, id);
|
|
115626
|
+
for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
|
|
115627
|
+
const wrong = [];
|
|
115628
|
+
for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
|
|
115629
|
+
if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
|
|
115630
|
+
const claimed = plan.size;
|
|
115631
|
+
ports.info(`${claimed} device(s) of '${spec.addonId}' claimed to their owner's integration or the default; ${unresolved.size} unresolved, ${reservations.size} reservation(s) skipped`);
|
|
115632
|
+
return {
|
|
115633
|
+
outcome: claimed > 0 ? "done" : "already-done",
|
|
115634
|
+
summary: {
|
|
115635
|
+
claimed,
|
|
115636
|
+
unresolved: unresolved.size,
|
|
115637
|
+
...summaryBase
|
|
115638
|
+
}
|
|
115639
|
+
};
|
|
115640
|
+
}
|
|
115641
|
+
/**
|
|
115642
|
+
* The two migrations of a device addon whose integrations hold NO settings of their own
|
|
115643
|
+
* (planning#416, S-A): every integration becomes a record of the addon's own kind with a name and
|
|
115644
|
+
* nothing else (D985, the default integration is an instance with an empty body), then the devices
|
|
115645
|
+
* that carry no `integrationId` are claimed to the addon's default integration.
|
|
115646
|
+
*
|
|
115647
|
+
* 1. `rows-to-record` (block-owner): `moveIntegrationRowsToRecords` over the hub API. Rows an
|
|
115648
|
+
* addon of this shape does not own (a key nothing reads) are named in a warning, by key, and
|
|
115649
|
+
* dropped with the rows once the record is read back.
|
|
115650
|
+
* 2. `claim-devices` (tolerate): `claimDevicesToDefaultIntegration`; with no integration to claim
|
|
115651
|
+
* to it fails and is retried on the next boot.
|
|
115652
|
+
*
|
|
115653
|
+
* Both run in the addon process through the hub API (`ctx.api`), BEFORE the addon registers its
|
|
115654
|
+
* `integration-instances` provider: while a provider is registered the hub answers `getSettings`
|
|
115655
|
+
* from the record, not from the rows.
|
|
115656
|
+
*/
|
|
115657
|
+
function defaultIntegrationPortsOver(deps) {
|
|
115658
|
+
return {
|
|
115659
|
+
...integrationRowsPortsOverApi({
|
|
115660
|
+
api: deps.api,
|
|
115661
|
+
records: deps.records.kind({
|
|
115662
|
+
name: deps.kind.name,
|
|
115663
|
+
declaration: deps.kind.declaration,
|
|
115664
|
+
schema: record(string(), unknown())
|
|
115665
|
+
}),
|
|
115666
|
+
log: { warn: deps.warn }
|
|
115667
|
+
}),
|
|
115668
|
+
...claimDevicesPortsOver({
|
|
115669
|
+
api: deps.api,
|
|
115670
|
+
info: deps.info,
|
|
115671
|
+
warnDevice: deps.warnDevice
|
|
115672
|
+
})
|
|
115673
|
+
};
|
|
115674
|
+
}
|
|
115675
|
+
/** The hub side of the device claim, over `ctx.api`. */
|
|
115676
|
+
function claimDevicesPortsOver(deps) {
|
|
115677
|
+
return {
|
|
115678
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
115679
|
+
id: i.id,
|
|
115680
|
+
name: i.name,
|
|
115681
|
+
info: i.info ?? {}
|
|
115682
|
+
})),
|
|
115683
|
+
listDevices: (addonId) => deps.api.listDevices(addonId),
|
|
115684
|
+
hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
|
|
115685
|
+
setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
|
|
115686
|
+
readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
|
|
115687
|
+
info: deps.info,
|
|
115688
|
+
warnDevice: deps.warnDevice
|
|
115689
|
+
};
|
|
115690
|
+
}
|
|
115691
|
+
function defaultIntegrationMigrationIds(addonId, date) {
|
|
115692
|
+
return {
|
|
115693
|
+
rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
|
|
115694
|
+
claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
|
|
115695
|
+
};
|
|
115696
|
+
}
|
|
115697
|
+
/**
|
|
115698
|
+
* The device claim alone, for an addon that keeps its integration settings where they are for now
|
|
115699
|
+
* (its own wave moves them) but whose persisted devices must name their default integration.
|
|
115700
|
+
*/
|
|
115701
|
+
function claimDevicesMigration(spec) {
|
|
115702
|
+
return {
|
|
115703
|
+
meta: {
|
|
115704
|
+
id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
|
|
115705
|
+
owner: spec.addonId,
|
|
115706
|
+
level: "addon-state",
|
|
115707
|
+
seq: spec.seq,
|
|
115708
|
+
dependsOn: [...spec.dependsOn],
|
|
115709
|
+
store: "settings",
|
|
115710
|
+
scope: "cluster",
|
|
115711
|
+
onFailure: "tolerate",
|
|
115712
|
+
bodyKind: "procedure",
|
|
115713
|
+
description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
|
|
115714
|
+
fixtureMark: "a device config field the claim does not know"
|
|
115715
|
+
},
|
|
115716
|
+
procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
|
|
115717
|
+
addonId: spec.addonId,
|
|
115718
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
115719
|
+
})
|
|
115720
|
+
};
|
|
115721
|
+
}
|
|
115722
|
+
function defaultIntegrationApiOver(api) {
|
|
115723
|
+
return {
|
|
115724
|
+
list: () => api.integrations.list.query(),
|
|
115725
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
115726
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
115727
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
115728
|
+
addonId,
|
|
115729
|
+
projection: "slim"
|
|
115730
|
+
}),
|
|
115731
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
115732
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
115733
|
+
deviceId,
|
|
115734
|
+
integrationId
|
|
115735
|
+
});
|
|
115736
|
+
},
|
|
115737
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
115738
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
115739
|
+
};
|
|
115740
|
+
}
|
|
115741
|
+
/**
|
|
114773
115742
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
114774
115743
|
* gets back (cloud training, D805+).
|
|
114775
115744
|
*
|
|
@@ -150744,117 +151713,6 @@ function actuator(input) {
|
|
|
150744
151713
|
};
|
|
150745
151714
|
}
|
|
150746
151715
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
150747
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
150748
|
-
"no-power-source",
|
|
150749
|
-
"switch-missing",
|
|
150750
|
-
"switch-offline",
|
|
150751
|
-
"switch-unknown",
|
|
150752
|
-
"restart-in-progress",
|
|
150753
|
-
"switch-call-failed"
|
|
150754
|
-
]);
|
|
150755
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
150756
|
-
accepted: literal(false),
|
|
150757
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
150758
|
-
/** One line for the operator, naming the switch. */
|
|
150759
|
-
message: string()
|
|
150760
|
-
})]);
|
|
150761
|
-
var PowerSourceInfoSchema = object({
|
|
150762
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
150763
|
-
linked: boolean(),
|
|
150764
|
-
switchDeviceId: number().nullable(),
|
|
150765
|
-
switchName: string().nullable(),
|
|
150766
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
150767
|
-
switchOn: boolean().nullable(),
|
|
150768
|
-
switchOnline: boolean().nullable(),
|
|
150769
|
-
restartInProgress: boolean(),
|
|
150770
|
-
offSeconds: number().int()
|
|
150771
|
-
});
|
|
150772
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
150773
|
-
var PowerSourceStateSchema = object({
|
|
150774
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
150775
|
-
switchOn: boolean().nullable(),
|
|
150776
|
-
offSeconds: number().int(),
|
|
150777
|
-
lastFetchedAt: number()
|
|
150778
|
-
});
|
|
150779
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
150780
|
-
DeviceType.Gateway,
|
|
150781
|
-
DeviceType.Hub,
|
|
150782
|
-
DeviceType.Camera,
|
|
150783
|
-
DeviceType.Sensor,
|
|
150784
|
-
DeviceType.MediaPlayer,
|
|
150785
|
-
DeviceType.Vacuum,
|
|
150786
|
-
DeviceType.Generic,
|
|
150787
|
-
DeviceType.Washer,
|
|
150788
|
-
DeviceType.Dryer,
|
|
150789
|
-
DeviceType.Dishwasher,
|
|
150790
|
-
DeviceType.PetFountain,
|
|
150791
|
-
DeviceType.Fan,
|
|
150792
|
-
DeviceType.Humidifier,
|
|
150793
|
-
DeviceType.Climate,
|
|
150794
|
-
DeviceType.WaterHeater
|
|
150795
|
-
];
|
|
150796
|
-
var powerSourceCustomize = {
|
|
150797
|
-
version: 1,
|
|
150798
|
-
targets: [{
|
|
150799
|
-
id: "switch",
|
|
150800
|
-
cardinality: "scalar",
|
|
150801
|
-
vocabulary: {
|
|
150802
|
-
cap: "power-source",
|
|
150803
|
-
stateField: "switchOn"
|
|
150804
|
-
},
|
|
150805
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150806
|
-
apply: "add",
|
|
150807
|
-
path: "switchOn",
|
|
150808
|
-
sources: [{
|
|
150809
|
-
kind: "switch",
|
|
150810
|
-
transforms: [{ kind: "identity" }]
|
|
150811
|
-
}],
|
|
150812
|
-
companions: [{
|
|
150813
|
-
kind: "constant",
|
|
150814
|
-
path: "offSeconds",
|
|
150815
|
-
value: 10,
|
|
150816
|
-
when: "add"
|
|
150817
|
-
}, {
|
|
150818
|
-
kind: "clock",
|
|
150819
|
-
path: "lastFetchedAt",
|
|
150820
|
-
when: "always"
|
|
150821
|
-
}],
|
|
150822
|
-
sourceReport: { status: "assignment-status" }
|
|
150823
|
-
}]
|
|
150824
|
-
};
|
|
150825
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
150826
|
-
var powerSourceCapability = {
|
|
150827
|
-
name: "power-source",
|
|
150828
|
-
scope: "device",
|
|
150829
|
-
mode: "singleton",
|
|
150830
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150831
|
-
methods: {
|
|
150832
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
150833
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150834
|
-
kind: "mutation",
|
|
150835
|
-
auth: "admin"
|
|
150836
|
-
}),
|
|
150837
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150838
|
-
kind: "mutation",
|
|
150839
|
-
auth: "admin"
|
|
150840
|
-
}),
|
|
150841
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150842
|
-
kind: "mutation",
|
|
150843
|
-
auth: "admin"
|
|
150844
|
-
})
|
|
150845
|
-
},
|
|
150846
|
-
runtimeState: PowerSourceStateSchema,
|
|
150847
|
-
customize: powerSourceCustomize,
|
|
150848
|
-
/**
|
|
150849
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
150850
|
-
* its next report; a restored value would outlive the reading it names.
|
|
150851
|
-
*
|
|
150852
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
150853
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
150854
|
-
*/
|
|
150855
|
-
durability: "session",
|
|
150856
|
-
history: "none"
|
|
150857
|
-
};
|
|
150858
151716
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
150859
151717
|
DeviceType.Camera;
|
|
150860
151718
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -150882,6 +151740,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
150882
151740
|
}).optional()
|
|
150883
151741
|
})) });
|
|
150884
151742
|
/**
|
|
151743
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
151744
|
+
* `z.infer`, they do not restate the shape.
|
|
151745
|
+
*/
|
|
151746
|
+
var OverrideSourceSchema = object({
|
|
151747
|
+
addonId: string().min(1),
|
|
151748
|
+
stableId: string().min(1),
|
|
151749
|
+
cap: string().min(1),
|
|
151750
|
+
fieldPath: string().min(1)
|
|
151751
|
+
});
|
|
151752
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
151753
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
151754
|
+
var ThresholdTransformDraftSchema = object({
|
|
151755
|
+
kind: literal("threshold"),
|
|
151756
|
+
comparison: _enum(["gte", "lte"]),
|
|
151757
|
+
threshold: number(),
|
|
151758
|
+
startDwellMs: number().nonnegative(),
|
|
151759
|
+
stopDwellMs: number().nonnegative()
|
|
151760
|
+
});
|
|
151761
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
151762
|
+
IdentityTransformDraftSchema,
|
|
151763
|
+
InvertTransformDraftSchema,
|
|
151764
|
+
ThresholdTransformDraftSchema,
|
|
151765
|
+
object({
|
|
151766
|
+
kind: literal("linear"),
|
|
151767
|
+
scale: number(),
|
|
151768
|
+
offset: number(),
|
|
151769
|
+
clampMin: number().optional(),
|
|
151770
|
+
clampMax: number().optional()
|
|
151771
|
+
}),
|
|
151772
|
+
object({
|
|
151773
|
+
kind: literal("enum-map"),
|
|
151774
|
+
mapping: record(string(), string().nullable())
|
|
151775
|
+
}),
|
|
151776
|
+
object({ kind: literal("number") })
|
|
151777
|
+
]);
|
|
151778
|
+
var ScalarOverrideDraftSchema = object({
|
|
151779
|
+
kind: literal("scalar"),
|
|
151780
|
+
cap: string().min(1),
|
|
151781
|
+
targetId: string().min(1),
|
|
151782
|
+
source: OverrideSourceSchema,
|
|
151783
|
+
transform: CustomizeTransformDraftSchema
|
|
151784
|
+
});
|
|
151785
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
151786
|
+
kind: literal("keyedArray"),
|
|
151787
|
+
cap: string().min(1),
|
|
151788
|
+
targetId: string().min(1),
|
|
151789
|
+
key: string().min(1),
|
|
151790
|
+
label: string().min(1),
|
|
151791
|
+
fieldPath: string().min(1),
|
|
151792
|
+
source: OverrideSourceSchema,
|
|
151793
|
+
transform: CustomizeTransformDraftSchema
|
|
151794
|
+
});
|
|
151795
|
+
var PulseOverrideDraftSchema = object({
|
|
151796
|
+
kind: literal("pulse"),
|
|
151797
|
+
cap: string().min(1),
|
|
151798
|
+
targetId: string().min(1),
|
|
151799
|
+
enabled: boolean(),
|
|
151800
|
+
debounceMs: number().nonnegative(),
|
|
151801
|
+
edges: array(_enum([
|
|
151802
|
+
"rising",
|
|
151803
|
+
"button",
|
|
151804
|
+
"native-event"
|
|
151805
|
+
])).min(1).optional(),
|
|
151806
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
151807
|
+
});
|
|
151808
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
151809
|
+
"in-addition",
|
|
151810
|
+
"confirm",
|
|
151811
|
+
"replace"
|
|
151812
|
+
]).optional() });
|
|
151813
|
+
discriminatedUnion("kind", [
|
|
151814
|
+
ScalarOverrideDraftSchema,
|
|
151815
|
+
KeyedArrayOverrideDraftSchema,
|
|
151816
|
+
PulseOverrideDraftSchema,
|
|
151817
|
+
object({
|
|
151818
|
+
kind: literal("span"),
|
|
151819
|
+
cap: string().min(1),
|
|
151820
|
+
targetId: string().min(1),
|
|
151821
|
+
timeoutMs: number().nonnegative(),
|
|
151822
|
+
cooldownMs: number().nonnegative().optional(),
|
|
151823
|
+
sources: array(SpanSourceDraftSchema)
|
|
151824
|
+
})
|
|
151825
|
+
]);
|
|
151826
|
+
object({
|
|
151827
|
+
targetId: string().min(1),
|
|
151828
|
+
configured: boolean(),
|
|
151829
|
+
source: _enum(["native", "assigned"]),
|
|
151830
|
+
state: _enum([
|
|
151831
|
+
"active",
|
|
151832
|
+
"stale",
|
|
151833
|
+
"released",
|
|
151834
|
+
"unavailable"
|
|
151835
|
+
]),
|
|
151836
|
+
reason: string().nullable()
|
|
151837
|
+
});
|
|
151838
|
+
/**
|
|
151839
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
151840
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
151841
|
+
*
|
|
151842
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
151843
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
151844
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
151845
|
+
*/
|
|
151846
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
151847
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
151848
|
+
"high",
|
|
151849
|
+
"medium",
|
|
151850
|
+
"low"
|
|
151851
|
+
]);
|
|
151852
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
151853
|
+
"no-history",
|
|
151854
|
+
"span-too-short",
|
|
151855
|
+
"truncated",
|
|
151856
|
+
"no-idle-floor",
|
|
151857
|
+
"no-runs",
|
|
151858
|
+
"no-separation"
|
|
151859
|
+
]);
|
|
151860
|
+
var RunningProposalReplaySchema = object({
|
|
151861
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
151862
|
+
intervals: number().int().nonnegative(),
|
|
151863
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
151864
|
+
extra: number().int().nonnegative(),
|
|
151865
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
151866
|
+
missed: number().int().nonnegative()
|
|
151867
|
+
});
|
|
151868
|
+
var RunningProposalEvidenceSchema = object({
|
|
151869
|
+
tier: RunningProposalTierSchema,
|
|
151870
|
+
spanMs: number().nonnegative(),
|
|
151871
|
+
from: number(),
|
|
151872
|
+
to: number(),
|
|
151873
|
+
points: number().int().nonnegative(),
|
|
151874
|
+
runsFound: number().int().nonnegative(),
|
|
151875
|
+
pausesFound: number().int().nonnegative(),
|
|
151876
|
+
longestPauseMs: number().nonnegative(),
|
|
151877
|
+
idleFloorW: number(),
|
|
151878
|
+
noiseCeilingW: number(),
|
|
151879
|
+
plateauW: number(),
|
|
151880
|
+
replay: RunningProposalReplaySchema
|
|
151881
|
+
});
|
|
151882
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
151883
|
+
spanMs: number().nonnegative(),
|
|
151884
|
+
points: number().int().nonnegative(),
|
|
151885
|
+
runsFound: number().int().nonnegative()
|
|
151886
|
+
});
|
|
151887
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
151888
|
+
state: literal("proposal"),
|
|
151889
|
+
transform: ThresholdTransformDraftSchema,
|
|
151890
|
+
evidence: RunningProposalEvidenceSchema,
|
|
151891
|
+
confidence: RunningProposalConfidenceSchema,
|
|
151892
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
151893
|
+
notes: array(string())
|
|
151894
|
+
}), object({
|
|
151895
|
+
state: literal("insufficient"),
|
|
151896
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
151897
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
151898
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
151899
|
+
fallback: ThresholdTransformDraftSchema
|
|
151900
|
+
})]);
|
|
151901
|
+
/**
|
|
150885
151902
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
150886
151903
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
150887
151904
|
*
|
|
@@ -151171,6 +152188,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
151171
152188
|
kind: "mutation",
|
|
151172
152189
|
auth: "admin",
|
|
151173
152190
|
access: "view"
|
|
152191
|
+
}), method(object({
|
|
152192
|
+
deviceId: number().int(),
|
|
152193
|
+
cap: string().min(1),
|
|
152194
|
+
fieldPath: string().min(1),
|
|
152195
|
+
deviceType: _enum(DeviceType).optional()
|
|
152196
|
+
}), RunningThresholdProposalSchema, {
|
|
152197
|
+
auth: "admin",
|
|
152198
|
+
access: "view"
|
|
151174
152199
|
}), method(object({
|
|
151175
152200
|
blockId: string().min(1),
|
|
151176
152201
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -152147,7 +153172,27 @@ method(object({
|
|
|
152147
153172
|
* exposes only the COMMON surface: link state, supported device
|
|
152148
153173
|
* kinds (so the UI can filter the device picker), and per-device
|
|
152149
153174
|
* expose/unexpose.
|
|
152150
|
-
|
|
153175
|
+
*
|
|
153176
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
153177
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
153178
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
153179
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
153180
|
+
* default instance's); present = that instance; an id the provider does not
|
|
153181
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
153182
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
153183
|
+
* change always names the target it changes. A single-instance provider
|
|
153184
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
153185
|
+
*/
|
|
153186
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
153187
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
153188
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
153189
|
+
/** One independent export target of an addon. */
|
|
153190
|
+
var ExportInstanceSchema = object({
|
|
153191
|
+
instanceId: InstanceIdSchema$1,
|
|
153192
|
+
name: string(),
|
|
153193
|
+
enabled: boolean(),
|
|
153194
|
+
kind: string().optional()
|
|
153195
|
+
});
|
|
152151
153196
|
var LinkStateSchema = _enum([
|
|
152152
153197
|
"unlinked",
|
|
152153
153198
|
"linked",
|
|
@@ -152198,10 +153243,14 @@ var ExposedDeviceSchema = object({
|
|
|
152198
153243
|
capabilities: array(string()).optional()
|
|
152199
153244
|
});
|
|
152200
153245
|
var ExposeInputSchema = object({
|
|
153246
|
+
instanceId: InstanceIdSchema$1,
|
|
152201
153247
|
deviceId: string(),
|
|
152202
153248
|
capabilities: array(string()).optional()
|
|
152203
153249
|
});
|
|
152204
|
-
var UnexposeInputSchema = object({
|
|
153250
|
+
var UnexposeInputSchema = object({
|
|
153251
|
+
instanceId: InstanceIdSchema$1,
|
|
153252
|
+
deviceId: string()
|
|
153253
|
+
});
|
|
152205
153254
|
/**
|
|
152206
153255
|
* What an exporter may expose besides devices (D968). An action and a
|
|
152207
153256
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -152230,15 +153279,21 @@ var ExposedEntitySchema = object({
|
|
|
152230
153279
|
entity: ExportEntityRefSchema,
|
|
152231
153280
|
exposedAs: string().optional()
|
|
152232
153281
|
});
|
|
152233
|
-
var ExposeEntityInputSchema = object({
|
|
152234
|
-
|
|
152235
|
-
|
|
153282
|
+
var ExposeEntityInputSchema = object({
|
|
153283
|
+
instanceId: InstanceIdSchema$1,
|
|
153284
|
+
entity: ExportEntityRefSchema
|
|
153285
|
+
});
|
|
153286
|
+
var UnexposeEntityInputSchema = object({
|
|
153287
|
+
instanceId: InstanceIdSchema$1,
|
|
153288
|
+
entity: ExportEntityRefSchema
|
|
153289
|
+
});
|
|
153290
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
152236
153291
|
kind: "mutation",
|
|
152237
153292
|
auth: "admin"
|
|
152238
153293
|
}), method(UnexposeInputSchema, _void(), {
|
|
152239
153294
|
kind: "mutation",
|
|
152240
153295
|
auth: "admin"
|
|
152241
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
153296
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
152242
153297
|
kind: "mutation",
|
|
152243
153298
|
auth: "admin"
|
|
152244
153299
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -155861,15 +156916,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
155861
156916
|
items: array(FanCaptureItemSchema),
|
|
155862
156917
|
omitted: array(FanCaptureOmissionSchema)
|
|
155863
156918
|
});
|
|
155864
|
-
var deviceInput$
|
|
156919
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
155865
156920
|
DeviceType.Fan;
|
|
155866
|
-
method(deviceInput$
|
|
156921
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
155867
156922
|
kind: "mutation",
|
|
155868
156923
|
auth: "admin"
|
|
155869
|
-
}), method(deviceInput$
|
|
156924
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
155870
156925
|
kind: "mutation",
|
|
155871
156926
|
auth: "protected"
|
|
155872
|
-
}), method(deviceInput$
|
|
156927
|
+
}), method(deviceInput$2.extend({
|
|
155873
156928
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
155874
156929
|
steps: array(ActionStepSchema).max(20).default([])
|
|
155875
156930
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -156752,6 +157807,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156752
157807
|
}), object({ success: boolean() }), {
|
|
156753
157808
|
kind: "mutation",
|
|
156754
157809
|
auth: "admin"
|
|
157810
|
+
}), method(object({ id: string() }), object({
|
|
157811
|
+
rows: number().int().nonnegative(),
|
|
157812
|
+
secrets: number().int().nonnegative()
|
|
157813
|
+
}), {
|
|
157814
|
+
kind: "mutation",
|
|
157815
|
+
auth: "admin"
|
|
156755
157816
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
156756
157817
|
addonId: string(),
|
|
156757
157818
|
settings: record(string(), unknown())
|
|
@@ -156780,13 +157841,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156780
157841
|
*/
|
|
156781
157842
|
var ConfigSchemaPassthrough$3 = custom();
|
|
156782
157843
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
157844
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
157845
|
+
/**
|
|
157846
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
157847
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
157848
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
157849
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
157850
|
+
* instance id.
|
|
157851
|
+
*/
|
|
157852
|
+
var IntegrationInstanceKindSchema = object({
|
|
157853
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
157854
|
+
id: string().min(1),
|
|
157855
|
+
label: string().min(1),
|
|
157856
|
+
/** The kind signs in through a server-held interactive session. */
|
|
157857
|
+
signIn: object({
|
|
157858
|
+
basePath: string().min(1),
|
|
157859
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
157860
|
+
}).optional(),
|
|
157861
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
157862
|
+
connectPath: string().min(1).optional(),
|
|
157863
|
+
/** Relative hub path to disconnect that account. */
|
|
157864
|
+
disconnectPath: string().min(1).optional()
|
|
157865
|
+
});
|
|
156783
157866
|
var IntegrationInstanceDescriptionSchema = object({
|
|
157867
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
157868
|
+
role: _enum([
|
|
157869
|
+
"devices",
|
|
157870
|
+
"services",
|
|
157871
|
+
"both"
|
|
157872
|
+
]),
|
|
157873
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
157874
|
+
discovery: object({
|
|
157875
|
+
mode: _enum([
|
|
157876
|
+
"none",
|
|
157877
|
+
"lan",
|
|
157878
|
+
"account"
|
|
157879
|
+
]),
|
|
157880
|
+
scopedToInstance: boolean()
|
|
157881
|
+
}).optional(),
|
|
156784
157882
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156785
|
-
instanceMode:
|
|
157883
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
156786
157884
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156787
157885
|
kind: string(),
|
|
156788
157886
|
/** True when `testInstance` really probes the remote. */
|
|
156789
|
-
canTest: boolean()
|
|
157887
|
+
canTest: boolean(),
|
|
157888
|
+
/**
|
|
157889
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
157890
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
157891
|
+
*/
|
|
157892
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
157893
|
+
/**
|
|
157894
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
157895
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
157896
|
+
*/
|
|
157897
|
+
allowRestoreId: boolean().optional(),
|
|
157898
|
+
/**
|
|
157899
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
157900
|
+
* send an id (the UI then mints an opaque one).
|
|
157901
|
+
*/
|
|
157902
|
+
mintsId: boolean().optional()
|
|
156790
157903
|
});
|
|
156791
157904
|
var IntegrationInstanceStatusSchema = object({
|
|
156792
157905
|
state: _enum([
|
|
@@ -156795,7 +157908,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
156795
157908
|
"error",
|
|
156796
157909
|
"unknown"
|
|
156797
157910
|
]),
|
|
156798
|
-
detail: string().optional()
|
|
157911
|
+
detail: string().optional(),
|
|
157912
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
157913
|
+
label: string().optional()
|
|
156799
157914
|
});
|
|
156800
157915
|
var IntegrationInstanceSchema = object({
|
|
156801
157916
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156803,13 +157918,18 @@ var IntegrationInstanceSchema = object({
|
|
|
156803
157918
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156804
157919
|
configured: boolean(),
|
|
156805
157920
|
/**
|
|
156806
|
-
* The instance's own on/off switch (D996)
|
|
156807
|
-
*
|
|
157921
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
157922
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
157923
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
156808
157924
|
*/
|
|
156809
|
-
enabled: boolean(),
|
|
157925
|
+
enabled: boolean().optional(),
|
|
157926
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
157927
|
+
kind: string().optional(),
|
|
156810
157928
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156811
157929
|
formValues: record(string(), unknown()),
|
|
156812
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
157930
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
157931
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
157932
|
+
lastCheckedAt: number().optional()
|
|
156813
157933
|
});
|
|
156814
157934
|
var IntegrationInstancePatchSchema = object({
|
|
156815
157935
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156817,31 +157937,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
156817
157937
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
156818
157938
|
values: record(string(), unknown())
|
|
156819
157939
|
});
|
|
156820
|
-
|
|
156821
|
-
|
|
156822
|
-
|
|
156823
|
-
|
|
156824
|
-
|
|
156825
|
-
|
|
156826
|
-
|
|
156827
|
-
|
|
156828
|
-
|
|
156829
|
-
|
|
156830
|
-
|
|
156831
|
-
|
|
156832
|
-
|
|
156833
|
-
|
|
156834
|
-
|
|
156835
|
-
|
|
156836
|
-
|
|
156837
|
-
|
|
156838
|
-
}),
|
|
156839
|
-
|
|
156840
|
-
|
|
156841
|
-
}),
|
|
156842
|
-
|
|
156843
|
-
|
|
156844
|
-
|
|
157940
|
+
var integrationInstancesCapability = {
|
|
157941
|
+
name: "integration-instances",
|
|
157942
|
+
scope: "system",
|
|
157943
|
+
mode: "collection",
|
|
157944
|
+
methods: {
|
|
157945
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
157946
|
+
/** The form; `id: null` is the create form. */
|
|
157947
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
157948
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
157949
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
157950
|
+
/**
|
|
157951
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
157952
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
157953
|
+
*/
|
|
157954
|
+
createInstance: method(object({
|
|
157955
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157956
|
+
name: string().min(1),
|
|
157957
|
+
kind: string().min(1).optional()
|
|
157958
|
+
}), object({ id: string() }), {
|
|
157959
|
+
kind: "mutation",
|
|
157960
|
+
auth: "admin"
|
|
157961
|
+
}),
|
|
157962
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
157963
|
+
kind: "mutation",
|
|
157964
|
+
auth: "admin"
|
|
157965
|
+
}),
|
|
157966
|
+
/**
|
|
157967
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
157968
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
157969
|
+
*/
|
|
157970
|
+
setInstanceEnabled: method(object({
|
|
157971
|
+
id: IntegrationInstanceIdSchema,
|
|
157972
|
+
enabled: boolean()
|
|
157973
|
+
}), object({ ok: literal(true) }), {
|
|
157974
|
+
kind: "mutation",
|
|
157975
|
+
auth: "admin"
|
|
157976
|
+
}),
|
|
157977
|
+
/** Delete the record and its keychain refs together. */
|
|
157978
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
157979
|
+
kind: "mutation",
|
|
157980
|
+
auth: "admin"
|
|
157981
|
+
}),
|
|
157982
|
+
testInstance: optionalMethod(object({
|
|
157983
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
157984
|
+
values: record(string(), unknown())
|
|
157985
|
+
}), TestConnectionResultSchema$1, {
|
|
157986
|
+
kind: "mutation",
|
|
157987
|
+
auth: "admin"
|
|
157988
|
+
})
|
|
157989
|
+
}
|
|
157990
|
+
};
|
|
156845
157991
|
/**
|
|
156846
157992
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
156847
157993
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -158880,6 +160026,11 @@ var LogEntrySchema = object({
|
|
|
158880
160026
|
* flight, and not asking for the next slice ends the scan.
|
|
158881
160027
|
*/
|
|
158882
160028
|
var LogReadQuerySchema = object({
|
|
160029
|
+
/**
|
|
160030
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
160031
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
160032
|
+
*/
|
|
160033
|
+
instanceId: string().min(1).max(200).optional(),
|
|
158883
160034
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
158884
160035
|
requestId: string().min(1).max(64).optional(),
|
|
158885
160036
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -158942,6 +160093,8 @@ var LogReadResultSchema = object({
|
|
|
158942
160093
|
/** True when `cancelRead` ended this slice early. */
|
|
158943
160094
|
cancelled: boolean().optional()
|
|
158944
160095
|
});
|
|
160096
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
160097
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
158945
160098
|
var LogReadCapabilitiesSchema = object({
|
|
158946
160099
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
158947
160100
|
readable: boolean(),
|
|
@@ -158960,7 +160113,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
158960
160113
|
/** How far back the store reaches, when the provider knows (days). */
|
|
158961
160114
|
retentionDays: number().nullable()
|
|
158962
160115
|
});
|
|
158963
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
160116
|
+
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() }), {
|
|
158964
160117
|
kind: "mutation",
|
|
158965
160118
|
auth: "admin"
|
|
158966
160119
|
});
|
|
@@ -171353,6 +172506,14 @@ var powerMeterCapability = {
|
|
|
171353
172506
|
deviceNative: true,
|
|
171354
172507
|
mode: "singleton",
|
|
171355
172508
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
172509
|
+
deviceConfig: { ui: {
|
|
172510
|
+
kind: "widget",
|
|
172511
|
+
widgetId: "host/power-meter-line",
|
|
172512
|
+
tab: "power",
|
|
172513
|
+
placement: "hero",
|
|
172514
|
+
label: "Power",
|
|
172515
|
+
order: 4
|
|
172516
|
+
} },
|
|
171356
172517
|
methods: {},
|
|
171357
172518
|
status: {
|
|
171358
172519
|
schema: PowerMeterStatusSchema,
|
|
@@ -171412,6 +172573,117 @@ var powerMeterCapability = {
|
|
|
171412
172573
|
volatileStateFields: ["lastFetchedAt"],
|
|
171413
172574
|
customize: powerMeterCustomize
|
|
171414
172575
|
};
|
|
172576
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
172577
|
+
"no-power-source",
|
|
172578
|
+
"switch-missing",
|
|
172579
|
+
"switch-offline",
|
|
172580
|
+
"switch-unknown",
|
|
172581
|
+
"restart-in-progress",
|
|
172582
|
+
"switch-call-failed"
|
|
172583
|
+
]);
|
|
172584
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
172585
|
+
accepted: literal(false),
|
|
172586
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
172587
|
+
/** One line for the operator, naming the switch. */
|
|
172588
|
+
message: string()
|
|
172589
|
+
})]);
|
|
172590
|
+
var PowerSourceInfoSchema = object({
|
|
172591
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
172592
|
+
linked: boolean(),
|
|
172593
|
+
switchDeviceId: number().nullable(),
|
|
172594
|
+
switchName: string().nullable(),
|
|
172595
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
172596
|
+
switchOn: boolean().nullable(),
|
|
172597
|
+
switchOnline: boolean().nullable(),
|
|
172598
|
+
restartInProgress: boolean(),
|
|
172599
|
+
offSeconds: number().int()
|
|
172600
|
+
});
|
|
172601
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
172602
|
+
var PowerSourceStateSchema = object({
|
|
172603
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
172604
|
+
switchOn: boolean().nullable(),
|
|
172605
|
+
offSeconds: number().int(),
|
|
172606
|
+
lastFetchedAt: number()
|
|
172607
|
+
});
|
|
172608
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
172609
|
+
DeviceType.Gateway,
|
|
172610
|
+
DeviceType.Hub,
|
|
172611
|
+
DeviceType.Camera,
|
|
172612
|
+
DeviceType.Sensor,
|
|
172613
|
+
DeviceType.MediaPlayer,
|
|
172614
|
+
DeviceType.Vacuum,
|
|
172615
|
+
DeviceType.Generic,
|
|
172616
|
+
DeviceType.Washer,
|
|
172617
|
+
DeviceType.Dryer,
|
|
172618
|
+
DeviceType.Dishwasher,
|
|
172619
|
+
DeviceType.PetFountain,
|
|
172620
|
+
DeviceType.Fan,
|
|
172621
|
+
DeviceType.Humidifier,
|
|
172622
|
+
DeviceType.Climate,
|
|
172623
|
+
DeviceType.WaterHeater
|
|
172624
|
+
];
|
|
172625
|
+
var powerSourceCustomize = {
|
|
172626
|
+
version: 1,
|
|
172627
|
+
targets: [{
|
|
172628
|
+
id: "switch",
|
|
172629
|
+
cardinality: "scalar",
|
|
172630
|
+
vocabulary: {
|
|
172631
|
+
cap: "power-source",
|
|
172632
|
+
stateField: "switchOn"
|
|
172633
|
+
},
|
|
172634
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
172635
|
+
apply: "add",
|
|
172636
|
+
path: "switchOn",
|
|
172637
|
+
sources: [{
|
|
172638
|
+
kind: "switch",
|
|
172639
|
+
transforms: [{ kind: "identity" }]
|
|
172640
|
+
}],
|
|
172641
|
+
companions: [{
|
|
172642
|
+
kind: "constant",
|
|
172643
|
+
path: "offSeconds",
|
|
172644
|
+
value: 10,
|
|
172645
|
+
when: "add"
|
|
172646
|
+
}, {
|
|
172647
|
+
kind: "clock",
|
|
172648
|
+
path: "lastFetchedAt",
|
|
172649
|
+
when: "always"
|
|
172650
|
+
}],
|
|
172651
|
+
sourceReport: { status: "assignment-status" }
|
|
172652
|
+
}]
|
|
172653
|
+
};
|
|
172654
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
172655
|
+
var powerSourceCapability = {
|
|
172656
|
+
name: "power-source",
|
|
172657
|
+
scope: "device",
|
|
172658
|
+
mode: "singleton",
|
|
172659
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
172660
|
+
methods: {
|
|
172661
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
172662
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
172663
|
+
kind: "mutation",
|
|
172664
|
+
auth: "admin"
|
|
172665
|
+
}),
|
|
172666
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
172667
|
+
kind: "mutation",
|
|
172668
|
+
auth: "admin"
|
|
172669
|
+
}),
|
|
172670
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
172671
|
+
kind: "mutation",
|
|
172672
|
+
auth: "admin"
|
|
172673
|
+
})
|
|
172674
|
+
},
|
|
172675
|
+
runtimeState: PowerSourceStateSchema,
|
|
172676
|
+
customize: powerSourceCustomize,
|
|
172677
|
+
/**
|
|
172678
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
172679
|
+
* its next report; a restored value would outlive the reading it names.
|
|
172680
|
+
*
|
|
172681
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
172682
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
172683
|
+
*/
|
|
172684
|
+
durability: "session",
|
|
172685
|
+
history: "none"
|
|
172686
|
+
};
|
|
171415
172687
|
/**
|
|
171416
172688
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
171417
172689
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -181892,6 +183164,25 @@ function buildStreamParamsConfigSchema(options, status) {
|
|
|
181892
183164
|
if (sections.length === 0) return null;
|
|
181893
183165
|
return { sections };
|
|
181894
183166
|
}
|
|
183167
|
+
/**
|
|
183168
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
183169
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
183170
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
183171
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
183172
|
+
* server sends an id, a client maps it to its own icon set.
|
|
183173
|
+
*
|
|
183174
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
183175
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
183176
|
+
* to `icon`.
|
|
183177
|
+
*/
|
|
183178
|
+
function digit(n) {
|
|
183179
|
+
return {
|
|
183180
|
+
label: t(n, n),
|
|
183181
|
+
icon: "hash",
|
|
183182
|
+
legend: n
|
|
183183
|
+
};
|
|
183184
|
+
}
|
|
183185
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
181895
183186
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
181896
183187
|
var btn = (idSuffix, row, col, init) => ({
|
|
181897
183188
|
idSuffix,
|
|
@@ -182020,6 +183311,34 @@ discriminatedUnion("kind", [object({
|
|
|
182020
183311
|
code: string()
|
|
182021
183312
|
})]);
|
|
182022
183313
|
/**
|
|
183314
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
183315
|
+
*
|
|
183316
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
183317
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
183318
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
183319
|
+
* count of samples.
|
|
183320
|
+
*
|
|
183321
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
183322
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
183323
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
183324
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
183325
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
183326
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
183327
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
183328
|
+
* covers the longest standby blip above T.
|
|
183329
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
183330
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
183331
|
+
*/
|
|
183332
|
+
var SECOND_MS = 1e3;
|
|
183333
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
183334
|
+
24 * (60 * MINUTE_MS$1);
|
|
183335
|
+
10 * MINUTE_MS$1;
|
|
183336
|
+
30 * SECOND_MS;
|
|
183337
|
+
60 * SECOND_MS;
|
|
183338
|
+
20 * MINUTE_MS$1;
|
|
183339
|
+
10 * SECOND_MS;
|
|
183340
|
+
10 * SECOND_MS;
|
|
183341
|
+
/**
|
|
182023
183342
|
* Which of OUR object detections a clip window holds — the ONE overlap rule
|
|
182024
183343
|
* and the ONE label ranking, for every clip source (D923).
|
|
182025
183344
|
*
|
|
@@ -185272,6 +186591,13 @@ Object.freeze({
|
|
|
185272
186591
|
access: "create",
|
|
185273
186592
|
role: "admin"
|
|
185274
186593
|
},
|
|
186594
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
186595
|
+
capName: "core-blocks",
|
|
186596
|
+
capScope: "system",
|
|
186597
|
+
addonId: null,
|
|
186598
|
+
access: "view",
|
|
186599
|
+
role: "admin"
|
|
186600
|
+
},
|
|
185275
186601
|
"coreBlocks.reportAutomationRun": {
|
|
185276
186602
|
capName: "core-blocks",
|
|
185277
186603
|
capScope: "system",
|
|
@@ -185783,6 +187109,13 @@ Object.freeze({
|
|
|
185783
187109
|
access: "view",
|
|
185784
187110
|
role: "admin"
|
|
185785
187111
|
},
|
|
187112
|
+
"deviceExport.listInstances": {
|
|
187113
|
+
capName: "device-export",
|
|
187114
|
+
capScope: "system",
|
|
187115
|
+
addonId: null,
|
|
187116
|
+
access: "view",
|
|
187117
|
+
role: "admin"
|
|
187118
|
+
},
|
|
185786
187119
|
"deviceExport.listSupportedDeviceKinds": {
|
|
185787
187120
|
capName: "device-export",
|
|
185788
187121
|
capScope: "system",
|
|
@@ -187435,6 +188768,13 @@ Object.freeze({
|
|
|
187435
188768
|
access: "create",
|
|
187436
188769
|
role: "admin"
|
|
187437
188770
|
},
|
|
188771
|
+
"integrations.clearSettings": {
|
|
188772
|
+
capName: "integrations",
|
|
188773
|
+
capScope: "system",
|
|
188774
|
+
addonId: null,
|
|
188775
|
+
access: "delete",
|
|
188776
|
+
role: "admin"
|
|
188777
|
+
},
|
|
187438
188778
|
"integrations.create": {
|
|
187439
188779
|
capName: "integrations",
|
|
187440
188780
|
capScope: "system",
|
|
@@ -194882,6 +196222,11 @@ Object.freeze({
|
|
|
194882
196222
|
form: "single",
|
|
194883
196223
|
optional: false
|
|
194884
196224
|
}],
|
|
196225
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
196226
|
+
name: "deviceId",
|
|
196227
|
+
form: "single",
|
|
196228
|
+
optional: false
|
|
196229
|
+
}],
|
|
194885
196230
|
"cover.close": [{
|
|
194886
196231
|
name: "deviceId",
|
|
194887
196232
|
form: "single",
|
|
@@ -197545,99 +198890,6 @@ Object.freeze({
|
|
|
197545
198890
|
"network-access": "ingress",
|
|
197546
198891
|
"smtp-provider": "email"
|
|
197547
198892
|
});
|
|
197548
|
-
/**
|
|
197549
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
197550
|
-
* `z.infer`, they do not restate the shape.
|
|
197551
|
-
*/
|
|
197552
|
-
var OverrideSourceSchema = object({
|
|
197553
|
-
addonId: string().min(1),
|
|
197554
|
-
stableId: string().min(1),
|
|
197555
|
-
cap: string().min(1),
|
|
197556
|
-
fieldPath: string().min(1)
|
|
197557
|
-
});
|
|
197558
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
197559
|
-
object({ kind: literal("identity") }),
|
|
197560
|
-
object({ kind: literal("invert") }),
|
|
197561
|
-
object({
|
|
197562
|
-
kind: literal("threshold"),
|
|
197563
|
-
comparison: _enum(["gte", "lte"]),
|
|
197564
|
-
threshold: number(),
|
|
197565
|
-
startDwellMs: number().nonnegative(),
|
|
197566
|
-
stopDwellMs: number().nonnegative()
|
|
197567
|
-
}),
|
|
197568
|
-
object({
|
|
197569
|
-
kind: literal("linear"),
|
|
197570
|
-
scale: number(),
|
|
197571
|
-
offset: number(),
|
|
197572
|
-
clampMin: number().optional(),
|
|
197573
|
-
clampMax: number().optional()
|
|
197574
|
-
}),
|
|
197575
|
-
object({
|
|
197576
|
-
kind: literal("enum-map"),
|
|
197577
|
-
mapping: record(string(), string().nullable())
|
|
197578
|
-
}),
|
|
197579
|
-
object({ kind: literal("number") })
|
|
197580
|
-
]);
|
|
197581
|
-
var ScalarOverrideDraftSchema = object({
|
|
197582
|
-
kind: literal("scalar"),
|
|
197583
|
-
cap: string().min(1),
|
|
197584
|
-
targetId: string().min(1),
|
|
197585
|
-
source: OverrideSourceSchema,
|
|
197586
|
-
transform: CustomizeTransformDraftSchema
|
|
197587
|
-
});
|
|
197588
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
197589
|
-
kind: literal("keyedArray"),
|
|
197590
|
-
cap: string().min(1),
|
|
197591
|
-
targetId: string().min(1),
|
|
197592
|
-
key: string().min(1),
|
|
197593
|
-
label: string().min(1),
|
|
197594
|
-
fieldPath: string().min(1),
|
|
197595
|
-
source: OverrideSourceSchema,
|
|
197596
|
-
transform: CustomizeTransformDraftSchema
|
|
197597
|
-
});
|
|
197598
|
-
var PulseOverrideDraftSchema = object({
|
|
197599
|
-
kind: literal("pulse"),
|
|
197600
|
-
cap: string().min(1),
|
|
197601
|
-
targetId: string().min(1),
|
|
197602
|
-
enabled: boolean(),
|
|
197603
|
-
debounceMs: number().nonnegative(),
|
|
197604
|
-
edges: array(_enum([
|
|
197605
|
-
"rising",
|
|
197606
|
-
"button",
|
|
197607
|
-
"native-event"
|
|
197608
|
-
])).min(1).optional(),
|
|
197609
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
197610
|
-
});
|
|
197611
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
197612
|
-
"in-addition",
|
|
197613
|
-
"confirm",
|
|
197614
|
-
"replace"
|
|
197615
|
-
]).optional() });
|
|
197616
|
-
discriminatedUnion("kind", [
|
|
197617
|
-
ScalarOverrideDraftSchema,
|
|
197618
|
-
KeyedArrayOverrideDraftSchema,
|
|
197619
|
-
PulseOverrideDraftSchema,
|
|
197620
|
-
object({
|
|
197621
|
-
kind: literal("span"),
|
|
197622
|
-
cap: string().min(1),
|
|
197623
|
-
targetId: string().min(1),
|
|
197624
|
-
timeoutMs: number().nonnegative(),
|
|
197625
|
-
cooldownMs: number().nonnegative().optional(),
|
|
197626
|
-
sources: array(SpanSourceDraftSchema)
|
|
197627
|
-
})
|
|
197628
|
-
]);
|
|
197629
|
-
object({
|
|
197630
|
-
targetId: string().min(1),
|
|
197631
|
-
configured: boolean(),
|
|
197632
|
-
source: _enum(["native", "assigned"]),
|
|
197633
|
-
state: _enum([
|
|
197634
|
-
"active",
|
|
197635
|
-
"stale",
|
|
197636
|
-
"released",
|
|
197637
|
-
"unavailable"
|
|
197638
|
-
]),
|
|
197639
|
-
reason: string().nullable()
|
|
197640
|
-
});
|
|
197641
198893
|
var G711_SCALE_CORRECTION_DB = {
|
|
197642
198894
|
PCMU: 20 * Math.log10(4),
|
|
197643
198895
|
PCMA: 20 * Math.log10(8)
|
|
@@ -219649,6 +220901,285 @@ async function startLegacyWebhookIntake(deps) {
|
|
|
219649
220901
|
};
|
|
219650
220902
|
}
|
|
219651
220903
|
//#endregion
|
|
220904
|
+
//#region src/reolink-discovery-map.ts
|
|
220905
|
+
/**
|
|
220906
|
+
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
220907
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
220908
|
+
*/
|
|
220909
|
+
function slugifyReolinkHost(host) {
|
|
220910
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
220911
|
+
}
|
|
220912
|
+
/**
|
|
220913
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
220914
|
+
*
|
|
220915
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
220916
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
220917
|
+
*/
|
|
220918
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
220919
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
220920
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
220921
|
+
if (byUid !== void 0) return byUid;
|
|
220922
|
+
}
|
|
220923
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
220924
|
+
}
|
|
220925
|
+
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
220926
|
+
var HUB_BAICHUAN_PORT = 9e3;
|
|
220927
|
+
/**
|
|
220928
|
+
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
220929
|
+
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
220930
|
+
* as a camera to add.
|
|
220931
|
+
*/
|
|
220932
|
+
function looksLikeReolinkHub(model) {
|
|
220933
|
+
if (model === void 0) return false;
|
|
220934
|
+
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
220935
|
+
}
|
|
220936
|
+
/**
|
|
220937
|
+
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
220938
|
+
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
220939
|
+
* creates hides this candidate through `originStableId`.
|
|
220940
|
+
*/
|
|
220941
|
+
function reolinkHubStableId(identity) {
|
|
220942
|
+
const uid = identity.uid?.trim() ?? "";
|
|
220943
|
+
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
220944
|
+
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
220945
|
+
}
|
|
220946
|
+
function isKnownHub(d, known) {
|
|
220947
|
+
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
220948
|
+
}
|
|
220949
|
+
/**
|
|
220950
|
+
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
220951
|
+
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
220952
|
+
* first responder for a host wins. Pure: no I/O.
|
|
220953
|
+
*
|
|
220954
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
220955
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
220956
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
220957
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
220958
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
220959
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
220960
|
+
* never a scan result.
|
|
220961
|
+
*
|
|
220962
|
+
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220963
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220964
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220965
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
220966
|
+
*/
|
|
220967
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220968
|
+
const username = credentials.username?.trim() ?? "";
|
|
220969
|
+
const password = credentials.password ?? "";
|
|
220970
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
220971
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220972
|
+
return [...byHost.values()].flatMap((d) => {
|
|
220973
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
220974
|
+
if (looksLikeReolinkHub(d.model)) {
|
|
220975
|
+
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220976
|
+
return [{
|
|
220977
|
+
stableId: reolinkHubStableId(d),
|
|
220978
|
+
type: DeviceType.Gateway,
|
|
220979
|
+
suggestedName: displayName,
|
|
220980
|
+
prefilledConfig: {
|
|
220981
|
+
host: d.host,
|
|
220982
|
+
port: HUB_BAICHUAN_PORT,
|
|
220983
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220984
|
+
...username ? { username } : {}
|
|
220985
|
+
},
|
|
220986
|
+
setupIntegration: true
|
|
220987
|
+
}];
|
|
220988
|
+
}
|
|
220989
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
220990
|
+
return [{
|
|
220991
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220992
|
+
type: DeviceType.Camera,
|
|
220993
|
+
suggestedName: displayName,
|
|
220994
|
+
prefilledConfig: {
|
|
220995
|
+
name: displayName,
|
|
220996
|
+
host: d.host,
|
|
220997
|
+
transport: "auto",
|
|
220998
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220999
|
+
...username ? { username } : {},
|
|
221000
|
+
...password ? { password } : {}
|
|
221001
|
+
},
|
|
221002
|
+
...match !== void 0 ? {
|
|
221003
|
+
alreadyOnboarded: true,
|
|
221004
|
+
onboardedDeviceId: match.deviceId,
|
|
221005
|
+
onboardedName: match.name
|
|
221006
|
+
} : {}
|
|
221007
|
+
}];
|
|
221008
|
+
});
|
|
221009
|
+
}
|
|
221010
|
+
//#endregion
|
|
221011
|
+
//#region src/reolink-hub-migration.ts
|
|
221012
|
+
/**
|
|
221013
|
+
* D985 — boot migration of the old Reolink Hub DEVICE into an INTEGRATION.
|
|
221014
|
+
*
|
|
221015
|
+
* A Hub used to be a device with the NVR's credentials in its config and its channels as
|
|
221016
|
+
* parented child cameras. Now the NVR is an integration (credentials in its keychain-backed
|
|
221017
|
+
* settings) and the channels are top-level cameras stamped with its id. For every Hub row:
|
|
221018
|
+
*
|
|
221019
|
+
* 1. create (or reuse) an integration named after it, config + password moved as settings;
|
|
221020
|
+
* 2. keep every child's deviceId and stableId, stamp it with the integrationId, un-parent it
|
|
221021
|
+
* and make sure its persisted blob can be restored on its own;
|
|
221022
|
+
* 3. only AFTER the children restored and the hub has none left, retire the hub by tombstone.
|
|
221023
|
+
*
|
|
221024
|
+
* Idempotent (a second run finds no Hub rows) and all-or-nothing per hub: a step that throws
|
|
221025
|
+
* leaves the hub device and its children's parentage exactly as they were, and the next boot
|
|
221026
|
+
* starts over. A locked secret is never read as an empty password and no replacement is minted.
|
|
221027
|
+
*/
|
|
221028
|
+
/** The marker an integration carries so a re-run reuses it instead of minting a second one. */
|
|
221029
|
+
var MIGRATED_FROM_HUB_KEY = "migratedFromHubDeviceId";
|
|
221030
|
+
function positivePort(value) {
|
|
221031
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
221032
|
+
}
|
|
221033
|
+
var CONNECTION_FIELDS = [
|
|
221034
|
+
"host",
|
|
221035
|
+
"port",
|
|
221036
|
+
"username",
|
|
221037
|
+
"transport"
|
|
221038
|
+
];
|
|
221039
|
+
function str(value) {
|
|
221040
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
221041
|
+
}
|
|
221042
|
+
function migratedChildConfig(child, hub, integrationId) {
|
|
221043
|
+
const filled = {};
|
|
221044
|
+
for (const key of CONNECTION_FIELDS) {
|
|
221045
|
+
const own = child[key];
|
|
221046
|
+
const inherited = hub[key];
|
|
221047
|
+
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
221048
|
+
}
|
|
221049
|
+
return {
|
|
221050
|
+
...child,
|
|
221051
|
+
...filled,
|
|
221052
|
+
integrationId
|
|
221053
|
+
};
|
|
221054
|
+
}
|
|
221055
|
+
async function migrateOneHub(hub, children, integrations, ports) {
|
|
221056
|
+
const read = readHubConnection(hub.config);
|
|
221057
|
+
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
221058
|
+
const { connection } = read;
|
|
221059
|
+
const uid = str(hub.config["uid"]);
|
|
221060
|
+
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
221061
|
+
addonId: ports.addonId,
|
|
221062
|
+
name: hub.name,
|
|
221063
|
+
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
221064
|
+
originStableId: reolinkHubStableId({
|
|
221065
|
+
...uid !== void 0 ? { uid } : {},
|
|
221066
|
+
host: connection.host
|
|
221067
|
+
}),
|
|
221068
|
+
settings: {
|
|
221069
|
+
host: connection.host,
|
|
221070
|
+
port: connection.port,
|
|
221071
|
+
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
221072
|
+
username: connection.username,
|
|
221073
|
+
password: connection.password,
|
|
221074
|
+
transport: connection.transport,
|
|
221075
|
+
...uid !== void 0 ? { uid } : {},
|
|
221076
|
+
debugGeneral: connection.debugGeneral,
|
|
221077
|
+
debugSocketLogs: connection.debugSocketLogs
|
|
221078
|
+
},
|
|
221079
|
+
secretKeys: ["password"]
|
|
221080
|
+
})).id;
|
|
221081
|
+
const rewritten = [];
|
|
221082
|
+
for (const child of children) {
|
|
221083
|
+
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
221084
|
+
await ports.persistConfig(child.stableId, config);
|
|
221085
|
+
await ports.setIntegrationId(child.id, integrationId);
|
|
221086
|
+
rewritten.push({
|
|
221087
|
+
...child,
|
|
221088
|
+
config,
|
|
221089
|
+
parentDeviceId: null
|
|
221090
|
+
});
|
|
221091
|
+
}
|
|
221092
|
+
return {
|
|
221093
|
+
rows: rewritten,
|
|
221094
|
+
retirement: {
|
|
221095
|
+
hubDeviceId: hub.id,
|
|
221096
|
+
hubName: hub.name,
|
|
221097
|
+
integrationId
|
|
221098
|
+
}
|
|
221099
|
+
};
|
|
221100
|
+
}
|
|
221101
|
+
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
221102
|
+
async function planHubMigration(saved, ports) {
|
|
221103
|
+
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
221104
|
+
if (hubs.length === 0) return {
|
|
221105
|
+
rows: saved,
|
|
221106
|
+
retirements: []
|
|
221107
|
+
};
|
|
221108
|
+
let integrations;
|
|
221109
|
+
try {
|
|
221110
|
+
integrations = await ports.listIntegrations();
|
|
221111
|
+
} catch (err) {
|
|
221112
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
221113
|
+
return {
|
|
221114
|
+
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
221115
|
+
retirements: []
|
|
221116
|
+
};
|
|
221117
|
+
}
|
|
221118
|
+
const hubIds = new Set(hubs.map((h) => h.id));
|
|
221119
|
+
const rewrittenById = /* @__PURE__ */ new Map();
|
|
221120
|
+
const retirements = [];
|
|
221121
|
+
for (const hub of hubs) {
|
|
221122
|
+
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
221123
|
+
try {
|
|
221124
|
+
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
221125
|
+
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
221126
|
+
retirements.push(done.retirement);
|
|
221127
|
+
ports.logger.info("Reolink hub migrated to an integration", {
|
|
221128
|
+
tags: { deviceId: hub.id },
|
|
221129
|
+
meta: {
|
|
221130
|
+
integrationId: done.retirement.integrationId,
|
|
221131
|
+
children: children.length,
|
|
221132
|
+
name: hub.name
|
|
221133
|
+
}
|
|
221134
|
+
});
|
|
221135
|
+
} catch (err) {
|
|
221136
|
+
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
221137
|
+
tags: { deviceId: hub.id },
|
|
221138
|
+
meta: {
|
|
221139
|
+
error: errMsg$3(err),
|
|
221140
|
+
name: hub.name
|
|
221141
|
+
}
|
|
221142
|
+
});
|
|
221143
|
+
}
|
|
221144
|
+
}
|
|
221145
|
+
return {
|
|
221146
|
+
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
221147
|
+
retirements
|
|
221148
|
+
};
|
|
221149
|
+
}
|
|
221150
|
+
/**
|
|
221151
|
+
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
221152
|
+
* and named: removing it would cascade the camera away with it.
|
|
221153
|
+
*/
|
|
221154
|
+
async function retireMigratedHubs(retirements, ports) {
|
|
221155
|
+
let retired = 0;
|
|
221156
|
+
for (const r of retirements) try {
|
|
221157
|
+
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
221158
|
+
if (remaining.length > 0) {
|
|
221159
|
+
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
221160
|
+
tags: { deviceId: r.hubDeviceId },
|
|
221161
|
+
meta: {
|
|
221162
|
+
remaining: [...remaining],
|
|
221163
|
+
integrationId: r.integrationId
|
|
221164
|
+
}
|
|
221165
|
+
});
|
|
221166
|
+
continue;
|
|
221167
|
+
}
|
|
221168
|
+
await ports.removeDevice(r.hubDeviceId);
|
|
221169
|
+
retired++;
|
|
221170
|
+
ports.logger.info("Reolink hub device retired", {
|
|
221171
|
+
tags: { deviceId: r.hubDeviceId },
|
|
221172
|
+
meta: { integrationId: r.integrationId }
|
|
221173
|
+
});
|
|
221174
|
+
} catch (err) {
|
|
221175
|
+
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
221176
|
+
tags: { deviceId: r.hubDeviceId },
|
|
221177
|
+
meta: { error: errMsg$3(err) }
|
|
221178
|
+
});
|
|
221179
|
+
}
|
|
221180
|
+
return retired;
|
|
221181
|
+
}
|
|
221182
|
+
//#endregion
|
|
219652
221183
|
//#region src/migrations/drop-webhook-intake-settings.ts
|
|
219653
221184
|
/**
|
|
219654
221185
|
* The camera webhook is a plane of the addon's HTTP data plane, not a private listener
|
|
@@ -219707,7 +221238,7 @@ function text(stored, key) {
|
|
|
219707
221238
|
const value = stored[key];
|
|
219708
221239
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
219709
221240
|
}
|
|
219710
|
-
function portOf(stored, ports) {
|
|
221241
|
+
function portOf$1(stored, ports) {
|
|
219711
221242
|
const value = stored["emailPushPort"];
|
|
219712
221243
|
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
219713
221244
|
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
|
|
@@ -219729,7 +221260,7 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
219729
221260
|
};
|
|
219730
221261
|
const bindHost = text(stored, "emailPushBindHost");
|
|
219731
221262
|
await ports.stashPending({
|
|
219732
|
-
port: portOf(stored, ports),
|
|
221263
|
+
port: portOf$1(stored, ports),
|
|
219733
221264
|
...bindHost === null ? {} : { bindHost },
|
|
219734
221265
|
domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
|
|
219735
221266
|
requireAuth: stored["emailPushRequireAuth"] !== false,
|
|
@@ -219741,6 +221272,188 @@ async function moveEmailPushToMailbox(ports) {
|
|
|
219741
221272
|
summary: { moved: true }
|
|
219742
221273
|
};
|
|
219743
221274
|
}
|
|
221275
|
+
var reolinkConnectionsEntryKind = defineEntryKind("connections", {
|
|
221276
|
+
title: "Reolink integrations",
|
|
221277
|
+
schema: entryBody({
|
|
221278
|
+
host: string().nullish(),
|
|
221279
|
+
port: number().int().min(1).max(65535).nullish(),
|
|
221280
|
+
rtspPort: number().int().min(1).max(65535).nullish(),
|
|
221281
|
+
username: string().nullish(),
|
|
221282
|
+
password: string().optional().meta({ secret: true }),
|
|
221283
|
+
transport: _enum(["tcp", "udp"]).nullish(),
|
|
221284
|
+
uid: string().nullish(),
|
|
221285
|
+
debugGeneral: boolean().nullish(),
|
|
221286
|
+
debugSocketLogs: array(ReolinkSocketDebugFlagSchema).nullish()
|
|
221287
|
+
}),
|
|
221288
|
+
writeTrigger: "an operator adds, edits or removes a Reolink integration, or a Hub device is migrated into one"
|
|
221289
|
+
});
|
|
221290
|
+
/** Every kind this addon declares, keyed by its name. */
|
|
221291
|
+
var reolinkRecordKinds = { connections: reolinkConnectionsEntryKind };
|
|
221292
|
+
/** The record body as the connection form's flat values: no name, no unset keys. */
|
|
221293
|
+
function hubSettingsOf(body) {
|
|
221294
|
+
const out = {};
|
|
221295
|
+
for (const [key, value] of Object.entries(body)) {
|
|
221296
|
+
if (key === "name" || value === null || value === void 0) continue;
|
|
221297
|
+
out[key] = value;
|
|
221298
|
+
}
|
|
221299
|
+
return out;
|
|
221300
|
+
}
|
|
221301
|
+
/**
|
|
221302
|
+
* What a resolved record says about its hub. A keychain that is locked yields a record WITHOUT
|
|
221303
|
+
* its password: that is `password-locked`, never an empty password (D49).
|
|
221304
|
+
*/
|
|
221305
|
+
function hubConnectionOf(resolved) {
|
|
221306
|
+
const read = readHubConnection(hubSettingsOf(resolved.body));
|
|
221307
|
+
return read.kind === "no-password" && resolved.secrets === "unavailable" ? { kind: "password-locked" } : read;
|
|
221308
|
+
}
|
|
221309
|
+
/**
|
|
221310
|
+
* What each enabled integration's RECORD says about its hub: the one reader of the connection.
|
|
221311
|
+
* A record that is absent reads as `no-host` (no session), exactly as an empty settings blob did.
|
|
221312
|
+
*/
|
|
221313
|
+
async function readHubConnections(instances, integrations) {
|
|
221314
|
+
const out = [];
|
|
221315
|
+
for (const integration of integrations) {
|
|
221316
|
+
const resolved = await instances.readResolved(integration.id);
|
|
221317
|
+
out.push({
|
|
221318
|
+
id: integration.id,
|
|
221319
|
+
name: integration.name,
|
|
221320
|
+
read: resolved === null ? { kind: "no-host" } : hubConnectionOf(resolved)
|
|
221321
|
+
});
|
|
221322
|
+
}
|
|
221323
|
+
return out;
|
|
221324
|
+
}
|
|
221325
|
+
function buildReolinkInstances(records, deps) {
|
|
221326
|
+
return new RecordIntegrationInstances(records, reolinkConnectionsEntryKind, {
|
|
221327
|
+
description: {
|
|
221328
|
+
role: "devices",
|
|
221329
|
+
discovery: {
|
|
221330
|
+
mode: "lan",
|
|
221331
|
+
scopedToInstance: false
|
|
221332
|
+
},
|
|
221333
|
+
instanceMode: "multiple",
|
|
221334
|
+
kind: "connection",
|
|
221335
|
+
canTest: false
|
|
221336
|
+
},
|
|
221337
|
+
formSchema: () => buildHubInstanceSchema(),
|
|
221338
|
+
initialBody: (name) => ({ name }),
|
|
221339
|
+
configured: (body) => (body.host ?? "").length === 0 || (body.password ?? "").length > 0,
|
|
221340
|
+
onChanged: () => deps.onChanged()
|
|
221341
|
+
});
|
|
221342
|
+
}
|
|
221343
|
+
/**
|
|
221344
|
+
* The provider handed to the hub: the default integration (no connection) shows no hub form, an
|
|
221345
|
+
* NVR / Home Hub one does, and a create wizard (no id yet) sets up a hub.
|
|
221346
|
+
*/
|
|
221347
|
+
function reolinkInstancesProvider(instances, deps) {
|
|
221348
|
+
return {
|
|
221349
|
+
...instances.toProvider(),
|
|
221350
|
+
instanceFormSchema: async ({ id }) => {
|
|
221351
|
+
if (id === null) return buildHubInstanceSchema();
|
|
221352
|
+
const [evidence, body] = await Promise.all([deps.registryEvidenceOf(id), instances.getResolved(id)]);
|
|
221353
|
+
return evidence !== null && isHubIntegration({
|
|
221354
|
+
originStableId: evidence.originStableId,
|
|
221355
|
+
info: evidence.info,
|
|
221356
|
+
settings: body === null ? {} : hubSettingsOf(body)
|
|
221357
|
+
}) || body === null ? buildHubInstanceSchema() : { sections: [] };
|
|
221358
|
+
}
|
|
221359
|
+
};
|
|
221360
|
+
}
|
|
221361
|
+
//#endregion
|
|
221362
|
+
//#region src/migrations/integration-rows-to-record.ts
|
|
221363
|
+
/**
|
|
221364
|
+
* Every Reolink integration moves from legacy `integration_settings` rows into its own
|
|
221365
|
+
* `connections` record under the registry id (planning#416, D1007). An NVR / Home Hub integration
|
|
221366
|
+
* carries its connection (host, ports, username, transport, uid, the two debug fields) and its
|
|
221367
|
+
* password, a keychain secret declared by the kind; the default integration carries a name only.
|
|
221368
|
+
* Each integration's record is built from ITS OWN rows, never from another's.
|
|
221369
|
+
*
|
|
221370
|
+
* Keys nothing reads any more are named in a warning, by key, and dropped with the rows once the
|
|
221371
|
+
* record has been read back. A value of the wrong type is left out and named by key as well.
|
|
221372
|
+
*/
|
|
221373
|
+
var REOLINK_ROWS_TO_RECORD_ID = "provider-reolink/2026-11-05-integration-rows-to-record";
|
|
221374
|
+
var TEXT_KEYS = [
|
|
221375
|
+
"host",
|
|
221376
|
+
"username",
|
|
221377
|
+
"uid"
|
|
221378
|
+
];
|
|
221379
|
+
var PORT_KEYS = ["port", "rtspPort"];
|
|
221380
|
+
function portOf(value) {
|
|
221381
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
221382
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
|
|
221383
|
+
}
|
|
221384
|
+
function textOf(value) {
|
|
221385
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
221386
|
+
}
|
|
221387
|
+
/** The record body of one integration, from its resolved legacy rows. Never logs a value. */
|
|
221388
|
+
function reolinkBodyFromRows(values, log, integration) {
|
|
221389
|
+
const body = { name: integration.name };
|
|
221390
|
+
const unusable = [];
|
|
221391
|
+
const known = new Set([
|
|
221392
|
+
...TEXT_KEYS,
|
|
221393
|
+
...PORT_KEYS,
|
|
221394
|
+
"password",
|
|
221395
|
+
"transport",
|
|
221396
|
+
"debugGeneral",
|
|
221397
|
+
"debugSocketLogs"
|
|
221398
|
+
]);
|
|
221399
|
+
for (const key of TEXT_KEYS) {
|
|
221400
|
+
if (!(key in values)) continue;
|
|
221401
|
+
const text = textOf(values[key]);
|
|
221402
|
+
if (text === null) unusable.push(key);
|
|
221403
|
+
else body[key] = text;
|
|
221404
|
+
}
|
|
221405
|
+
for (const key of PORT_KEYS) {
|
|
221406
|
+
if (!(key in values)) continue;
|
|
221407
|
+
const port = portOf(values[key]);
|
|
221408
|
+
if (port === null) unusable.push(key);
|
|
221409
|
+
else body[key] = port;
|
|
221410
|
+
}
|
|
221411
|
+
if ("password" in values) {
|
|
221412
|
+
const password = values["password"];
|
|
221413
|
+
if (typeof password === "string" && password.length > 0) body["password"] = password;
|
|
221414
|
+
else unusable.push("password");
|
|
221415
|
+
}
|
|
221416
|
+
if ("transport" in values) {
|
|
221417
|
+
const transport = values["transport"];
|
|
221418
|
+
if (transport === "tcp" || transport === "udp") body["transport"] = transport;
|
|
221419
|
+
else unusable.push("transport");
|
|
221420
|
+
}
|
|
221421
|
+
if ("debugGeneral" in values) {
|
|
221422
|
+
const flag = values["debugGeneral"];
|
|
221423
|
+
if (typeof flag === "boolean") body["debugGeneral"] = flag;
|
|
221424
|
+
else unusable.push("debugGeneral");
|
|
221425
|
+
}
|
|
221426
|
+
if ("debugSocketLogs" in values) {
|
|
221427
|
+
const flags = values["debugSocketLogs"];
|
|
221428
|
+
const parsed = Array.isArray(flags) ? flags.map((f) => ReolinkSocketDebugFlagSchema.safeParse(f)) : null;
|
|
221429
|
+
if (parsed !== null && parsed.every((p) => p.success)) body["debugSocketLogs"] = parsed.flatMap((p) => p.success ? [p.data] : []);
|
|
221430
|
+
else unusable.push("debugSocketLogs");
|
|
221431
|
+
}
|
|
221432
|
+
const unknown = Object.keys(values).filter((key) => !known.has(key));
|
|
221433
|
+
if (unknown.length > 0) log.warn(`integration '${integration.id}': legacy settings nothing reads any more are dropped: ${unknown.join(", ")}`);
|
|
221434
|
+
if (unusable.length > 0) log.warn(`integration '${integration.id}': legacy settings with an unusable value are left out: ${unusable.join(", ")}`);
|
|
221435
|
+
return reolinkConnectionsEntryKind.schema.parse(body);
|
|
221436
|
+
}
|
|
221437
|
+
var REOLINK_ROWS_TO_RECORD_MIGRATION = {
|
|
221438
|
+
meta: {
|
|
221439
|
+
id: REOLINK_ROWS_TO_RECORD_ID,
|
|
221440
|
+
owner: "provider-reolink",
|
|
221441
|
+
level: "addon-state",
|
|
221442
|
+
seq: 4,
|
|
221443
|
+
dependsOn: [],
|
|
221444
|
+
store: "settings",
|
|
221445
|
+
scope: "cluster",
|
|
221446
|
+
onFailure: "block-owner",
|
|
221447
|
+
bodyKind: "procedure",
|
|
221448
|
+
description: "Move every Reolink integration (the default one and each NVR / Home Hub) into its own connection record keyed by its registry id, the password as a keychain secret, then clear its legacy integration settings rows",
|
|
221449
|
+
fixtureMark: "a legacy setting key the migration does not know"
|
|
221450
|
+
},
|
|
221451
|
+
procedure: (ports) => moveIntegrationRowsToRecords(ports, {
|
|
221452
|
+
addonId: "provider-reolink",
|
|
221453
|
+
secretPaths: reolinkConnectionsEntryKind.declaration.secretPaths,
|
|
221454
|
+
bodyFrom: reolinkBodyFromRows
|
|
221455
|
+
})
|
|
221456
|
+
};
|
|
219744
221457
|
/** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
|
|
219745
221458
|
var REOLINK_MIGRATIONS = [{
|
|
219746
221459
|
meta: {
|
|
@@ -219773,6 +221486,18 @@ var REOLINK_MIGRATIONS = [{
|
|
|
219773
221486
|
},
|
|
219774
221487
|
plan: planDropWebhookIntakeSettings
|
|
219775
221488
|
}];
|
|
221489
|
+
/**
|
|
221490
|
+
* Every persisted camera that names no integration is claimed to the addon's default one. The
|
|
221491
|
+
* integration a hub migration created is never the default, so it is not eligible. The integration
|
|
221492
|
+
* settings themselves stay where they are until this addon's own wave moves them.
|
|
221493
|
+
*/
|
|
221494
|
+
var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
|
|
221495
|
+
addonId: "provider-reolink",
|
|
221496
|
+
date: "2026-11-04",
|
|
221497
|
+
seq: 3,
|
|
221498
|
+
dependsOn: [],
|
|
221499
|
+
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
221500
|
+
});
|
|
219776
221501
|
//#endregion
|
|
219777
221502
|
//#region src/reolink-adoption-provider.ts
|
|
219778
221503
|
var DEVICES_FILTER = {
|
|
@@ -219938,113 +221663,6 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219938
221663
|
};
|
|
219939
221664
|
}
|
|
219940
221665
|
//#endregion
|
|
219941
|
-
//#region src/reolink-discovery-map.ts
|
|
219942
|
-
/**
|
|
219943
|
-
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
219944
|
-
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
219945
|
-
*/
|
|
219946
|
-
function slugifyReolinkHost(host) {
|
|
219947
|
-
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
219948
|
-
}
|
|
219949
|
-
/**
|
|
219950
|
-
* Decide whether a discovered host is a device this provider already owns.
|
|
219951
|
-
*
|
|
219952
|
-
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
219953
|
-
* wins even when the host differs, because the address is the thing that moves.
|
|
219954
|
-
*/
|
|
219955
|
-
function findOnboardedMatch(discovered, onboarded) {
|
|
219956
|
-
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
219957
|
-
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
219958
|
-
if (byUid !== void 0) return byUid;
|
|
219959
|
-
}
|
|
219960
|
-
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
219961
|
-
}
|
|
219962
|
-
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
219963
|
-
var HUB_BAICHUAN_PORT = 9e3;
|
|
219964
|
-
/**
|
|
219965
|
-
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
219966
|
-
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
219967
|
-
* as a camera to add.
|
|
219968
|
-
*/
|
|
219969
|
-
function looksLikeReolinkHub(model) {
|
|
219970
|
-
if (model === void 0) return false;
|
|
219971
|
-
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
219972
|
-
}
|
|
219973
|
-
/**
|
|
219974
|
-
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
219975
|
-
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
219976
|
-
* creates hides this candidate through `originStableId`.
|
|
219977
|
-
*/
|
|
219978
|
-
function reolinkHubStableId(identity) {
|
|
219979
|
-
const uid = identity.uid?.trim() ?? "";
|
|
219980
|
-
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
219981
|
-
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
219982
|
-
}
|
|
219983
|
-
function isKnownHub(d, known) {
|
|
219984
|
-
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
219985
|
-
}
|
|
219986
|
-
/**
|
|
219987
|
-
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
219988
|
-
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
219989
|
-
* first responder for a host wins. Pure: no I/O.
|
|
219990
|
-
*
|
|
219991
|
-
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
219992
|
-
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
219993
|
-
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
219994
|
-
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
219995
|
-
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
219996
|
-
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
219997
|
-
* never a scan result.
|
|
219998
|
-
*
|
|
219999
|
-
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220000
|
-
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220001
|
-
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220002
|
-
* in the first place, so a scan says so instead of offering it again.
|
|
220003
|
-
*/
|
|
220004
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220005
|
-
const username = credentials.username?.trim() ?? "";
|
|
220006
|
-
const password = credentials.password ?? "";
|
|
220007
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
220008
|
-
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220009
|
-
return [...byHost.values()].flatMap((d) => {
|
|
220010
|
-
const displayName = d.name ?? d.model ?? d.host;
|
|
220011
|
-
if (looksLikeReolinkHub(d.model)) {
|
|
220012
|
-
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220013
|
-
return [{
|
|
220014
|
-
stableId: reolinkHubStableId(d),
|
|
220015
|
-
type: DeviceType.Gateway,
|
|
220016
|
-
suggestedName: displayName,
|
|
220017
|
-
prefilledConfig: {
|
|
220018
|
-
host: d.host,
|
|
220019
|
-
port: HUB_BAICHUAN_PORT,
|
|
220020
|
-
...d.uid ? { uid: d.uid } : {},
|
|
220021
|
-
...username ? { username } : {}
|
|
220022
|
-
},
|
|
220023
|
-
setupIntegration: true
|
|
220024
|
-
}];
|
|
220025
|
-
}
|
|
220026
|
-
const match = findOnboardedMatch(d, onboarded);
|
|
220027
|
-
return [{
|
|
220028
|
-
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220029
|
-
type: DeviceType.Camera,
|
|
220030
|
-
suggestedName: displayName,
|
|
220031
|
-
prefilledConfig: {
|
|
220032
|
-
name: displayName,
|
|
220033
|
-
host: d.host,
|
|
220034
|
-
transport: "auto",
|
|
220035
|
-
...d.uid ? { uid: d.uid } : {},
|
|
220036
|
-
...username ? { username } : {},
|
|
220037
|
-
...password ? { password } : {}
|
|
220038
|
-
},
|
|
220039
|
-
...match !== void 0 ? {
|
|
220040
|
-
alreadyOnboarded: true,
|
|
220041
|
-
onboardedDeviceId: match.deviceId,
|
|
220042
|
-
onboardedName: match.name
|
|
220043
|
-
} : {}
|
|
220044
|
-
}];
|
|
220045
|
-
});
|
|
220046
|
-
}
|
|
220047
|
-
//#endregion
|
|
220048
221666
|
//#region src/reolink-gateway-device.ts
|
|
220049
221667
|
var reolinkGatewayDeviceSchema = object({
|
|
220050
221668
|
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
@@ -220149,7 +221767,7 @@ var ReolinkHubManager = class {
|
|
|
220149
221767
|
const { logger } = this.#deps;
|
|
220150
221768
|
const wanted = /* @__PURE__ */ new Set();
|
|
220151
221769
|
for (const integration of integrations) {
|
|
220152
|
-
const read =
|
|
221770
|
+
const { read } = integration;
|
|
220153
221771
|
if (read.kind === "no-host") continue;
|
|
220154
221772
|
wanted.add(integration.id);
|
|
220155
221773
|
if (read.kind !== "ok") {
|
|
@@ -220201,158 +221819,6 @@ function createReolinkHubManager(deps, registry) {
|
|
|
220201
221819
|
connect: connectReolinkHub
|
|
220202
221820
|
});
|
|
220203
221821
|
}
|
|
220204
|
-
function positivePort(value) {
|
|
220205
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
220206
|
-
}
|
|
220207
|
-
var CONNECTION_FIELDS = [
|
|
220208
|
-
"host",
|
|
220209
|
-
"port",
|
|
220210
|
-
"username",
|
|
220211
|
-
"transport"
|
|
220212
|
-
];
|
|
220213
|
-
function str(value) {
|
|
220214
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220215
|
-
}
|
|
220216
|
-
function migratedChildConfig(child, hub, integrationId) {
|
|
220217
|
-
const filled = {};
|
|
220218
|
-
for (const key of CONNECTION_FIELDS) {
|
|
220219
|
-
const own = child[key];
|
|
220220
|
-
const inherited = hub[key];
|
|
220221
|
-
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220222
|
-
}
|
|
220223
|
-
return {
|
|
220224
|
-
...child,
|
|
220225
|
-
...filled,
|
|
220226
|
-
integrationId
|
|
220227
|
-
};
|
|
220228
|
-
}
|
|
220229
|
-
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220230
|
-
const read = readHubConnection(hub.config);
|
|
220231
|
-
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220232
|
-
const { connection } = read;
|
|
220233
|
-
const uid = str(hub.config["uid"]);
|
|
220234
|
-
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220235
|
-
addonId: ports.addonId,
|
|
220236
|
-
name: hub.name,
|
|
220237
|
-
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220238
|
-
originStableId: reolinkHubStableId({
|
|
220239
|
-
...uid !== void 0 ? { uid } : {},
|
|
220240
|
-
host: connection.host
|
|
220241
|
-
}),
|
|
220242
|
-
settings: {
|
|
220243
|
-
host: connection.host,
|
|
220244
|
-
port: connection.port,
|
|
220245
|
-
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220246
|
-
username: connection.username,
|
|
220247
|
-
password: connection.password,
|
|
220248
|
-
transport: connection.transport,
|
|
220249
|
-
...uid !== void 0 ? { uid } : {},
|
|
220250
|
-
debugGeneral: connection.debugGeneral,
|
|
220251
|
-
debugSocketLogs: connection.debugSocketLogs
|
|
220252
|
-
},
|
|
220253
|
-
secretKeys: ["password"]
|
|
220254
|
-
})).id;
|
|
220255
|
-
const rewritten = [];
|
|
220256
|
-
for (const child of children) {
|
|
220257
|
-
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220258
|
-
await ports.persistConfig(child.stableId, config);
|
|
220259
|
-
await ports.setIntegrationId(child.id, integrationId);
|
|
220260
|
-
rewritten.push({
|
|
220261
|
-
...child,
|
|
220262
|
-
config,
|
|
220263
|
-
parentDeviceId: null
|
|
220264
|
-
});
|
|
220265
|
-
}
|
|
220266
|
-
return {
|
|
220267
|
-
rows: rewritten,
|
|
220268
|
-
retirement: {
|
|
220269
|
-
hubDeviceId: hub.id,
|
|
220270
|
-
hubName: hub.name,
|
|
220271
|
-
integrationId
|
|
220272
|
-
}
|
|
220273
|
-
};
|
|
220274
|
-
}
|
|
220275
|
-
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220276
|
-
async function planHubMigration(saved, ports) {
|
|
220277
|
-
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220278
|
-
if (hubs.length === 0) return {
|
|
220279
|
-
rows: saved,
|
|
220280
|
-
retirements: []
|
|
220281
|
-
};
|
|
220282
|
-
let integrations;
|
|
220283
|
-
try {
|
|
220284
|
-
integrations = await ports.listIntegrations();
|
|
220285
|
-
} catch (err) {
|
|
220286
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220287
|
-
return {
|
|
220288
|
-
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220289
|
-
retirements: []
|
|
220290
|
-
};
|
|
220291
|
-
}
|
|
220292
|
-
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220293
|
-
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220294
|
-
const retirements = [];
|
|
220295
|
-
for (const hub of hubs) {
|
|
220296
|
-
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220297
|
-
try {
|
|
220298
|
-
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220299
|
-
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220300
|
-
retirements.push(done.retirement);
|
|
220301
|
-
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220302
|
-
tags: { deviceId: hub.id },
|
|
220303
|
-
meta: {
|
|
220304
|
-
integrationId: done.retirement.integrationId,
|
|
220305
|
-
children: children.length,
|
|
220306
|
-
name: hub.name
|
|
220307
|
-
}
|
|
220308
|
-
});
|
|
220309
|
-
} catch (err) {
|
|
220310
|
-
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220311
|
-
tags: { deviceId: hub.id },
|
|
220312
|
-
meta: {
|
|
220313
|
-
error: errMsg$3(err),
|
|
220314
|
-
name: hub.name
|
|
220315
|
-
}
|
|
220316
|
-
});
|
|
220317
|
-
}
|
|
220318
|
-
}
|
|
220319
|
-
return {
|
|
220320
|
-
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220321
|
-
retirements
|
|
220322
|
-
};
|
|
220323
|
-
}
|
|
220324
|
-
/**
|
|
220325
|
-
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220326
|
-
* and named: removing it would cascade the camera away with it.
|
|
220327
|
-
*/
|
|
220328
|
-
async function retireMigratedHubs(retirements, ports) {
|
|
220329
|
-
let retired = 0;
|
|
220330
|
-
for (const r of retirements) try {
|
|
220331
|
-
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220332
|
-
if (remaining.length > 0) {
|
|
220333
|
-
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220334
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220335
|
-
meta: {
|
|
220336
|
-
remaining: [...remaining],
|
|
220337
|
-
integrationId: r.integrationId
|
|
220338
|
-
}
|
|
220339
|
-
});
|
|
220340
|
-
continue;
|
|
220341
|
-
}
|
|
220342
|
-
await ports.removeDevice(r.hubDeviceId);
|
|
220343
|
-
retired++;
|
|
220344
|
-
ports.logger.info("Reolink hub device retired", {
|
|
220345
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220346
|
-
meta: { integrationId: r.integrationId }
|
|
220347
|
-
});
|
|
220348
|
-
} catch (err) {
|
|
220349
|
-
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220350
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220351
|
-
meta: { error: errMsg$3(err) }
|
|
220352
|
-
});
|
|
220353
|
-
}
|
|
220354
|
-
return retired;
|
|
220355
|
-
}
|
|
220356
221822
|
//#endregion
|
|
220357
221823
|
//#region src/reolink-subnet-scan.ts
|
|
220358
221824
|
/**
|
|
@@ -220669,6 +222135,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220669
222135
|
*/
|
|
220670
222136
|
autodetectCache = new AutodetectCache();
|
|
220671
222137
|
hubManager = null;
|
|
222138
|
+
instances = null;
|
|
220672
222139
|
/**
|
|
220673
222140
|
* The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
|
|
220674
222141
|
* mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
|
|
@@ -220914,25 +222381,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220914
222381
|
}
|
|
220915
222382
|
/**
|
|
220916
222383
|
* Make the hub sessions match the live integration list: one per enabled integration with a
|
|
220917
|
-
* host. Idempotent; runs at boot, after a migration
|
|
220918
|
-
* integration's
|
|
222384
|
+
* host. Idempotent; runs at boot, after a migration, after an instance change and on every
|
|
222385
|
+
* lifecycle event. When any integration's record cannot be read the whole pass is skipped — an unreadable list must
|
|
220919
222386
|
* never close a healthy session.
|
|
220920
222387
|
*/
|
|
220921
222388
|
async reconcileIntegrations() {
|
|
220922
222389
|
const manager = this.hubManager;
|
|
220923
|
-
|
|
222390
|
+
const instances = this.instances;
|
|
222391
|
+
if (manager === null || instances === null) return;
|
|
220924
222392
|
try {
|
|
220925
|
-
const
|
|
220926
|
-
|
|
220927
|
-
for (const integration of mine) {
|
|
220928
|
-
const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
|
|
220929
|
-
withSettings.push({
|
|
220930
|
-
id: integration.id,
|
|
220931
|
-
name: integration.name,
|
|
220932
|
-
settings
|
|
220933
|
-
});
|
|
220934
|
-
}
|
|
220935
|
-
await manager.reconcile(withSettings);
|
|
222393
|
+
const withConnection = await readHubConnections(instances, (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled));
|
|
222394
|
+
await manager.reconcile(withConnection);
|
|
220936
222395
|
} catch (err) {
|
|
220937
222396
|
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
|
|
220938
222397
|
}
|
|
@@ -221105,28 +222564,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221105
222564
|
globalSettingsSchema() {
|
|
221106
222565
|
return { sections: [...buildClipRefreshSettingsSchema().sections] };
|
|
221107
222566
|
}
|
|
221108
|
-
/**
|
|
221109
|
-
* Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
|
|
221110
|
-
* default integration keeps the full schema (`null`). A create wizard has no id yet and is
|
|
221111
|
-
* always setting up a NEW instance — the default one already exists — so it gets the hub form.
|
|
221112
|
-
*/
|
|
221113
|
-
async integrationSettingsSchema(instance) {
|
|
221114
|
-
if (instance.integrationId === null) return buildHubInstanceSchema();
|
|
221115
|
-
try {
|
|
221116
|
-
const [integration, settings] = await Promise.all([this.ctx.api.integrations.get.query({ id: instance.integrationId }), this.ctx.api.integrations.getSettings.query({ id: instance.integrationId })]);
|
|
221117
|
-
return isHubIntegration({
|
|
221118
|
-
originStableId: integration.originStableId,
|
|
221119
|
-
info: integration.info,
|
|
221120
|
-
settings
|
|
221121
|
-
}) ? buildHubInstanceSchema() : null;
|
|
221122
|
-
} catch (err) {
|
|
221123
|
-
this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
|
|
221124
|
-
integrationId: instance.integrationId,
|
|
221125
|
-
error: errMsg$3(err)
|
|
221126
|
-
} });
|
|
221127
|
-
return null;
|
|
221128
|
-
}
|
|
221129
|
-
}
|
|
221130
222567
|
async onInitialize() {
|
|
221131
222568
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
221132
222569
|
this.logChannels = createLogChannelsProvider(this.ctx.logger.child("log-channels"));
|
|
@@ -221171,7 +222608,42 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221171
222608
|
this.wireClipCatalogRefreshers();
|
|
221172
222609
|
await this.serveClipPlanes(this.clipService);
|
|
221173
222610
|
await this.serveWebhookPlane();
|
|
221174
|
-
|
|
222611
|
+
const records = this.ctx.records;
|
|
222612
|
+
if (records === void 0) throw new Error("provider-reolink: ctx.records is required (hub-only addon)");
|
|
222613
|
+
await records.declare(reolinkRecordKinds);
|
|
222614
|
+
const integrationPorts = defaultIntegrationPortsOver({
|
|
222615
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
222616
|
+
records,
|
|
222617
|
+
kind: reolinkConnectionsEntryKind,
|
|
222618
|
+
info: (message) => this.ctx.logger.info(message),
|
|
222619
|
+
warn: (message) => this.ctx.logger.warn(message),
|
|
222620
|
+
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
222621
|
+
});
|
|
222622
|
+
const registryEvidenceOf = async (id) => {
|
|
222623
|
+
try {
|
|
222624
|
+
const integration = await this.ctx.api.integrations.get.query({ id });
|
|
222625
|
+
return {
|
|
222626
|
+
originStableId: integration.originStableId,
|
|
222627
|
+
info: { ...integration.info }
|
|
222628
|
+
};
|
|
222629
|
+
} catch (err) {
|
|
222630
|
+
this.ctx.logger.warn("Reolink integration form: instance lookup failed — hub form shown", { meta: {
|
|
222631
|
+
integrationId: id,
|
|
222632
|
+
error: errMsg$3(err)
|
|
222633
|
+
} });
|
|
222634
|
+
return null;
|
|
222635
|
+
}
|
|
222636
|
+
};
|
|
222637
|
+
const instances = buildReolinkInstances(records, {
|
|
222638
|
+
onChanged: () => this.reconcileIntegrations(),
|
|
222639
|
+
registryEvidenceOf
|
|
222640
|
+
});
|
|
222641
|
+
this.instances = instances;
|
|
222642
|
+
await this.runMigrations([
|
|
222643
|
+
...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()),
|
|
222644
|
+
...defineAddonMigrations([REOLINK_ROWS_TO_RECORD_MIGRATION], integrationPorts),
|
|
222645
|
+
...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], integrationPorts)
|
|
222646
|
+
]);
|
|
221175
222647
|
this.wireEmailBridge();
|
|
221176
222648
|
const wireSleepPushResolvers = (event, via) => {
|
|
221177
222649
|
const data = event.data;
|
|
@@ -221210,7 +222682,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221210
222682
|
capability: deviceRemovalHookCapability,
|
|
221211
222683
|
provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
|
|
221212
222684
|
});
|
|
221213
|
-
|
|
222685
|
+
regs.push({
|
|
222686
|
+
capability: integrationInstancesCapability,
|
|
222687
|
+
provider: reolinkInstancesProvider(instances, { registryEvidenceOf })
|
|
222688
|
+
});
|
|
222689
|
+
return { providers: regs };
|
|
221214
222690
|
}
|
|
221215
222691
|
/** What the catalogue's migration reads and writes (D987). */
|
|
221216
222692
|
migrationPorts() {
|