@camstack/addon-provider-rademacher 0.2.149 → 0.2.151
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 +1012 -167
- package/dist/addon.mjs +1012 -167
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -5457,7 +5457,7 @@ var ZodIssueCode = {
|
|
|
5457
5457
|
var ZodFirstPartyTypeKind;
|
|
5458
5458
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5459
5459
|
//#endregion
|
|
5460
|
-
//#region ../types/dist/sleep-
|
|
5460
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5461
5461
|
/**
|
|
5462
5462
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5463
5463
|
* window to float samples (D455).
|
|
@@ -7524,6 +7524,7 @@ var BaseAddon = class {
|
|
|
7524
7524
|
* a warning and falls back rather than crashing boot.
|
|
7525
7525
|
*/
|
|
7526
7526
|
state(key, schema, fallback) {
|
|
7527
|
+
this._declaredStateKeys.add(key);
|
|
7527
7528
|
return createDurableState({
|
|
7528
7529
|
key,
|
|
7529
7530
|
schema,
|
|
@@ -7539,6 +7540,96 @@ var BaseAddon = class {
|
|
|
7539
7540
|
} })
|
|
7540
7541
|
});
|
|
7541
7542
|
}
|
|
7543
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7544
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7545
|
+
/**
|
|
7546
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7547
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7548
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7549
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7550
|
+
*/
|
|
7551
|
+
async getDeclaredSettingsKeys() {
|
|
7552
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7553
|
+
}
|
|
7554
|
+
declaredKeySet() {
|
|
7555
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7556
|
+
const schema = this.globalSettingsSchema();
|
|
7557
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7558
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7559
|
+
for (const path of this.secretStatePaths) {
|
|
7560
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7561
|
+
if (root) keys.add(root);
|
|
7562
|
+
}
|
|
7563
|
+
return keys;
|
|
7564
|
+
}
|
|
7565
|
+
/**
|
|
7566
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7567
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7568
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7569
|
+
*/
|
|
7570
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7571
|
+
const settings = this._ctx?.settings;
|
|
7572
|
+
const raw = this.rawSettings();
|
|
7573
|
+
if (!settings || !raw) return {
|
|
7574
|
+
removed: [],
|
|
7575
|
+
refused: keys.map((key) => ({
|
|
7576
|
+
key,
|
|
7577
|
+
reason: "no settings store"
|
|
7578
|
+
})),
|
|
7579
|
+
secretsCleared: 0
|
|
7580
|
+
};
|
|
7581
|
+
let stored;
|
|
7582
|
+
try {
|
|
7583
|
+
stored = await raw.readAddonStore();
|
|
7584
|
+
} catch (error) {
|
|
7585
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7586
|
+
return {
|
|
7587
|
+
removed: [],
|
|
7588
|
+
refused: keys.map((key) => ({
|
|
7589
|
+
key,
|
|
7590
|
+
reason
|
|
7591
|
+
})),
|
|
7592
|
+
secretsCleared: 0
|
|
7593
|
+
};
|
|
7594
|
+
}
|
|
7595
|
+
const declared = this.declaredKeySet();
|
|
7596
|
+
const removed = [];
|
|
7597
|
+
const refused = [];
|
|
7598
|
+
for (const key of keys) {
|
|
7599
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7600
|
+
if (declared.has(base)) refused.push({
|
|
7601
|
+
key,
|
|
7602
|
+
reason: "still declared by the addon"
|
|
7603
|
+
});
|
|
7604
|
+
else if (!(key in stored)) refused.push({
|
|
7605
|
+
key,
|
|
7606
|
+
reason: "not stored"
|
|
7607
|
+
});
|
|
7608
|
+
else removed.push(key);
|
|
7609
|
+
}
|
|
7610
|
+
if (removed.length === 0) return {
|
|
7611
|
+
removed,
|
|
7612
|
+
refused,
|
|
7613
|
+
secretsCleared: 0
|
|
7614
|
+
};
|
|
7615
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7616
|
+
const held = /* @__PURE__ */ new Set();
|
|
7617
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7618
|
+
const target = removed.includes(key) ? held : kept;
|
|
7619
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7620
|
+
}
|
|
7621
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7622
|
+
const patch = {};
|
|
7623
|
+
for (const key of removed) patch[key] = void 0;
|
|
7624
|
+
await settings.writeAddonStore(patch);
|
|
7625
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7626
|
+
await this.resolveConfig();
|
|
7627
|
+
return {
|
|
7628
|
+
removed,
|
|
7629
|
+
refused,
|
|
7630
|
+
secretsCleared
|
|
7631
|
+
};
|
|
7632
|
+
}
|
|
7542
7633
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7543
7634
|
deviceState(deviceId, key, schema, fallback) {
|
|
7544
7635
|
return createDurableState({
|
|
@@ -7809,6 +7900,21 @@ function dropPerNodeFields(fields) {
|
|
|
7809
7900
|
}
|
|
7810
7901
|
return kept;
|
|
7811
7902
|
}
|
|
7903
|
+
function collectFieldKeys(fields) {
|
|
7904
|
+
const collected = [];
|
|
7905
|
+
for (const field of fields) {
|
|
7906
|
+
if (field.type === "group") {
|
|
7907
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
7908
|
+
continue;
|
|
7909
|
+
}
|
|
7910
|
+
if (field.type === "sub-tabs") {
|
|
7911
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
7912
|
+
continue;
|
|
7913
|
+
}
|
|
7914
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
7915
|
+
}
|
|
7916
|
+
return collected;
|
|
7917
|
+
}
|
|
7812
7918
|
function collectPerNodeFieldKeys(fields) {
|
|
7813
7919
|
const collected = [];
|
|
7814
7920
|
for (const field of fields) {
|
|
@@ -16080,8 +16186,172 @@ object({
|
|
|
16080
16186
|
quantity: ReadingQuantitySchema,
|
|
16081
16187
|
unit: string().min(1)
|
|
16082
16188
|
});
|
|
16083
|
-
|
|
16084
|
-
|
|
16189
|
+
new Set([
|
|
16190
|
+
{
|
|
16191
|
+
id: "person",
|
|
16192
|
+
name: "Person"
|
|
16193
|
+
},
|
|
16194
|
+
{
|
|
16195
|
+
id: "vehicle",
|
|
16196
|
+
name: "Vehicle"
|
|
16197
|
+
},
|
|
16198
|
+
{
|
|
16199
|
+
id: "animal",
|
|
16200
|
+
name: "Animal"
|
|
16201
|
+
},
|
|
16202
|
+
{
|
|
16203
|
+
id: "package",
|
|
16204
|
+
name: "Package"
|
|
16205
|
+
}
|
|
16206
|
+
].map((l) => l.id));
|
|
16207
|
+
var COCO_TO_MACRO = {
|
|
16208
|
+
mapping: {
|
|
16209
|
+
person: "person",
|
|
16210
|
+
bicycle: "vehicle",
|
|
16211
|
+
car: "vehicle",
|
|
16212
|
+
motorcycle: "vehicle",
|
|
16213
|
+
airplane: "vehicle",
|
|
16214
|
+
bus: "vehicle",
|
|
16215
|
+
train: "vehicle",
|
|
16216
|
+
truck: "vehicle",
|
|
16217
|
+
boat: "vehicle",
|
|
16218
|
+
bird: "animal",
|
|
16219
|
+
cat: "animal",
|
|
16220
|
+
dog: "animal",
|
|
16221
|
+
horse: "animal",
|
|
16222
|
+
sheep: "animal",
|
|
16223
|
+
cow: "animal",
|
|
16224
|
+
elephant: "animal",
|
|
16225
|
+
bear: "animal",
|
|
16226
|
+
zebra: "animal",
|
|
16227
|
+
giraffe: "animal",
|
|
16228
|
+
suitcase: "package",
|
|
16229
|
+
backpack: "package",
|
|
16230
|
+
handbag: "package"
|
|
16231
|
+
},
|
|
16232
|
+
preserveOriginal: false
|
|
16233
|
+
};
|
|
16234
|
+
/**
|
|
16235
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16236
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16237
|
+
*
|
|
16238
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16239
|
+
* second list. The label that fits depends on the macro class:
|
|
16240
|
+
*
|
|
16241
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16242
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16243
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16244
|
+
* - `package` → a sub-class.
|
|
16245
|
+
*
|
|
16246
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16247
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16248
|
+
*
|
|
16249
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16250
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16251
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16252
|
+
* the subject GETS it — not at the first detection.
|
|
16253
|
+
*/
|
|
16254
|
+
var SubClassLabelSchema = object({
|
|
16255
|
+
kind: literal("sub-class"),
|
|
16256
|
+
className: string().min(1)
|
|
16257
|
+
});
|
|
16258
|
+
var IdentityLabelSchema = object({
|
|
16259
|
+
kind: literal("identity"),
|
|
16260
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16261
|
+
identityId: string().min(1),
|
|
16262
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16263
|
+
name: string().min(1)
|
|
16264
|
+
});
|
|
16265
|
+
var PlateLabelSchema = object({
|
|
16266
|
+
kind: literal("plate"),
|
|
16267
|
+
text: string().min(1)
|
|
16268
|
+
});
|
|
16269
|
+
function macroOf$1(className) {
|
|
16270
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16271
|
+
}
|
|
16272
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16273
|
+
function subClassBelongs(macro, label) {
|
|
16274
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16275
|
+
}
|
|
16276
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16277
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16278
|
+
object({
|
|
16279
|
+
macro: literal("animal"),
|
|
16280
|
+
label: subClassOf("animal").optional()
|
|
16281
|
+
}),
|
|
16282
|
+
object({
|
|
16283
|
+
macro: literal("person"),
|
|
16284
|
+
label: IdentityLabelSchema.optional()
|
|
16285
|
+
}),
|
|
16286
|
+
object({
|
|
16287
|
+
macro: literal("vehicle"),
|
|
16288
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16289
|
+
}),
|
|
16290
|
+
object({
|
|
16291
|
+
macro: literal("package"),
|
|
16292
|
+
label: subClassOf("package").optional()
|
|
16293
|
+
}),
|
|
16294
|
+
object({
|
|
16295
|
+
macro: literal("other"),
|
|
16296
|
+
className: string().min(1)
|
|
16297
|
+
})
|
|
16298
|
+
]);
|
|
16299
|
+
/**
|
|
16300
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16301
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16302
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16303
|
+
*/
|
|
16304
|
+
var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
|
|
16305
|
+
/**
|
|
16306
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16307
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16308
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16309
|
+
*/
|
|
16310
|
+
function subjectFromClassName(className) {
|
|
16311
|
+
const name = className.trim().toLowerCase();
|
|
16312
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16313
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16314
|
+
if (macro === void 0) return {
|
|
16315
|
+
macro: "other",
|
|
16316
|
+
className
|
|
16317
|
+
};
|
|
16318
|
+
const label = {
|
|
16319
|
+
kind: "sub-class",
|
|
16320
|
+
className: name
|
|
16321
|
+
};
|
|
16322
|
+
switch (macro) {
|
|
16323
|
+
case "animal": return {
|
|
16324
|
+
macro: "animal",
|
|
16325
|
+
label
|
|
16326
|
+
};
|
|
16327
|
+
case "vehicle": return {
|
|
16328
|
+
macro: "vehicle",
|
|
16329
|
+
label
|
|
16330
|
+
};
|
|
16331
|
+
case "package": return {
|
|
16332
|
+
macro: "package",
|
|
16333
|
+
label
|
|
16334
|
+
};
|
|
16335
|
+
default: return {
|
|
16336
|
+
macro: "other",
|
|
16337
|
+
className
|
|
16338
|
+
};
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
function macroOnly(macro) {
|
|
16342
|
+
switch (macro) {
|
|
16343
|
+
case "animal": return { macro: "animal" };
|
|
16344
|
+
case "person": return { macro: "person" };
|
|
16345
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16346
|
+
case "package": return { macro: "package" };
|
|
16347
|
+
default: return {
|
|
16348
|
+
macro: "other",
|
|
16349
|
+
className: macro
|
|
16350
|
+
};
|
|
16351
|
+
}
|
|
16352
|
+
}
|
|
16353
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16354
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16085
16355
|
var AutomationConditionOperatorSchema = _enum([
|
|
16086
16356
|
"eq",
|
|
16087
16357
|
"ne",
|
|
@@ -16092,12 +16362,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16092
16362
|
"contains",
|
|
16093
16363
|
"in"
|
|
16094
16364
|
]);
|
|
16095
|
-
|
|
16096
|
-
|
|
16365
|
+
/**
|
|
16366
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16367
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16368
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16369
|
+
* fire; it does not lose it.
|
|
16370
|
+
*/
|
|
16371
|
+
var DeviceBecomesLeafSchema = object({
|
|
16372
|
+
kind: literal("device-becomes"),
|
|
16373
|
+
deviceId: number().int().nonnegative(),
|
|
16374
|
+
cap: string().min(1),
|
|
16375
|
+
fieldPath: string().min(1),
|
|
16376
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16377
|
+
becomes: union([
|
|
16378
|
+
string(),
|
|
16379
|
+
number(),
|
|
16380
|
+
boolean()
|
|
16381
|
+
]).optional(),
|
|
16382
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16383
|
+
edge: boolean().optional(),
|
|
16384
|
+
/** The new value must hold this long before it counts. */
|
|
16385
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
16386
|
+
/** Collapse a burst into one occurrence. */
|
|
16387
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16388
|
+
});
|
|
16389
|
+
var DeviceEventLeafSchema = object({
|
|
16390
|
+
kind: literal("device-event"),
|
|
16391
|
+
/** An `EventCategory` value. */
|
|
16392
|
+
category: string().min(1),
|
|
16393
|
+
deviceId: number().int().nonnegative().optional()
|
|
16394
|
+
});
|
|
16395
|
+
var ScheduleLeafSchema = object({
|
|
16396
|
+
kind: literal("schedule"),
|
|
16397
|
+
cron: string().min(1).max(120)
|
|
16398
|
+
});
|
|
16399
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16400
|
+
var IntervalLeafSchema = object({
|
|
16401
|
+
kind: literal("interval"),
|
|
16402
|
+
minutes: number().int().min(1).max(1440)
|
|
16403
|
+
});
|
|
16404
|
+
var DetectionLeafSchema = object({
|
|
16405
|
+
kind: literal("detection"),
|
|
16406
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16407
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16408
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16409
|
+
/** The track must have visited at least one of these zones. */
|
|
16410
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16411
|
+
/** Best-so-far confidence the track must reach. */
|
|
16412
|
+
minScore: number().min(0).max(1).optional()
|
|
16413
|
+
});
|
|
16414
|
+
var StationaryLeafSchema = object({
|
|
16415
|
+
kind: literal("stationary"),
|
|
16416
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16417
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16418
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16419
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16420
|
+
phase: _enum(["appeared", "departed"])
|
|
16421
|
+
});
|
|
16422
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16423
|
+
var DeviceIsLeafSchema = object({
|
|
16424
|
+
kind: literal("device-is"),
|
|
16097
16425
|
deviceId: number().int().nonnegative(),
|
|
16098
16426
|
cap: string().min(1),
|
|
16099
16427
|
fieldPath: string().min(1),
|
|
16100
16428
|
operator: AutomationConditionOperatorSchema,
|
|
16429
|
+
/**
|
|
16430
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16431
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16432
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16433
|
+
* one is never evaluated (it fails).
|
|
16434
|
+
*/
|
|
16101
16435
|
value: union([
|
|
16102
16436
|
string(),
|
|
16103
16437
|
number(),
|
|
@@ -16105,24 +16439,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16105
16439
|
array(union([string(), number()])),
|
|
16106
16440
|
QuantityValueSchema
|
|
16107
16441
|
]),
|
|
16442
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16108
16443
|
unit: string().min(1).optional()
|
|
16109
16444
|
});
|
|
16110
|
-
|
|
16445
|
+
/**
|
|
16446
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16447
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16448
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16449
|
+
*/
|
|
16450
|
+
var StationaryPresentLeafSchema = object({
|
|
16111
16451
|
kind: literal("stationary-present"),
|
|
16112
16452
|
deviceId: number().int().nonnegative(),
|
|
16113
|
-
|
|
16114
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16115
|
-
/** The parked object must be in at least one of these zones. */
|
|
16453
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16116
16454
|
zones: array(string().min(1)).min(1).optional()
|
|
16117
16455
|
});
|
|
16118
16456
|
/**
|
|
16119
|
-
* The
|
|
16120
|
-
*
|
|
16121
|
-
*
|
|
16122
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16123
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16457
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16458
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16459
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16124
16460
|
*/
|
|
16125
|
-
var
|
|
16461
|
+
var ExpressionLeafSchema = object({
|
|
16126
16462
|
kind: literal("expression"),
|
|
16127
16463
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16128
16464
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16134,55 +16470,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16134
16470
|
path: ["expr"]
|
|
16135
16471
|
});
|
|
16136
16472
|
});
|
|
16137
|
-
var
|
|
16473
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16138
16474
|
object({
|
|
16139
16475
|
kind: literal("all"),
|
|
16140
|
-
children: array(
|
|
16476
|
+
children: array(AutomationWhenSchema)
|
|
16141
16477
|
}),
|
|
16142
16478
|
object({
|
|
16143
16479
|
kind: literal("any"),
|
|
16144
|
-
children: array(
|
|
16480
|
+
children: array(AutomationWhenSchema)
|
|
16145
16481
|
}),
|
|
16146
16482
|
object({
|
|
16147
16483
|
kind: literal("not"),
|
|
16148
|
-
child:
|
|
16484
|
+
child: AutomationWhenSchema
|
|
16149
16485
|
}),
|
|
16150
|
-
|
|
16151
|
-
|
|
16152
|
-
|
|
16486
|
+
DeviceBecomesLeafSchema,
|
|
16487
|
+
DeviceEventLeafSchema,
|
|
16488
|
+
ScheduleLeafSchema,
|
|
16489
|
+
ManualLeafSchema,
|
|
16490
|
+
IntervalLeafSchema,
|
|
16491
|
+
DetectionLeafSchema,
|
|
16492
|
+
StationaryLeafSchema,
|
|
16493
|
+
DeviceIsLeafSchema,
|
|
16494
|
+
StationaryPresentLeafSchema,
|
|
16495
|
+
ExpressionLeafSchema
|
|
16153
16496
|
]));
|
|
16154
16497
|
/**
|
|
16155
|
-
*
|
|
16156
|
-
*
|
|
16157
|
-
*
|
|
16158
|
-
*
|
|
16159
|
-
*
|
|
16160
|
-
*
|
|
16161
|
-
*
|
|
16162
|
-
*
|
|
16163
|
-
|
|
16164
|
-
|
|
16498
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16499
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16500
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16501
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16502
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16503
|
+
* way. Nothing writes this shape any more.
|
|
16504
|
+
*
|
|
16505
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16506
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16507
|
+
* a single trigger is not wrapped in `any`.
|
|
16508
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16509
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16510
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16511
|
+
*/
|
|
16512
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16165
16513
|
object({
|
|
16166
16514
|
kind: literal("device-state"),
|
|
16167
16515
|
deviceId: number().int().nonnegative(),
|
|
16168
16516
|
cap: string().min(1),
|
|
16169
16517
|
fieldPath: string().min(1),
|
|
16170
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16171
16518
|
becomes: union([
|
|
16172
16519
|
string(),
|
|
16173
16520
|
number(),
|
|
16174
16521
|
boolean()
|
|
16175
16522
|
]).optional(),
|
|
16176
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16177
16523
|
edge: boolean().optional(),
|
|
16178
|
-
/** The condition must hold this long before the run starts. */
|
|
16179
16524
|
forMs: number().int().min(0).max(864e5).optional(),
|
|
16180
|
-
/** Collapse a burst into one run. */
|
|
16181
16525
|
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16182
16526
|
}),
|
|
16183
16527
|
object({
|
|
16184
16528
|
kind: literal("device-event"),
|
|
16185
|
-
/** An `EventCategory` value. */
|
|
16186
16529
|
category: string().min(1),
|
|
16187
16530
|
deviceId: number().int().nonnegative().optional()
|
|
16188
16531
|
}),
|
|
@@ -16193,25 +16536,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16193
16536
|
object({ kind: literal("manual") }),
|
|
16194
16537
|
object({
|
|
16195
16538
|
kind: literal("detection"),
|
|
16196
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16197
|
-
* matches every raw class that maps to it; any other name matches
|
|
16198
|
-
* exactly. */
|
|
16199
16539
|
classes: array(string().min(1)).min(1),
|
|
16200
16540
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16201
|
-
/** The track must have visited at least one of these zones. */
|
|
16202
16541
|
zones: array(string().min(1)).min(1).optional(),
|
|
16203
|
-
/** Best-so-far confidence the track must reach. */
|
|
16204
16542
|
minScore: number().min(0).max(1).optional()
|
|
16205
16543
|
}),
|
|
16206
16544
|
object({
|
|
16207
16545
|
kind: literal("stationary"),
|
|
16208
16546
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16209
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16210
16547
|
classes: array(string().min(1)).min(1).optional(),
|
|
16211
|
-
/** The parked object must be in at least one of these zones. */
|
|
16212
16548
|
zones: array(string().min(1)).min(1).optional(),
|
|
16213
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16214
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16215
16549
|
phase: _enum(["appeared", "departed"])
|
|
16216
16550
|
}),
|
|
16217
16551
|
object({
|
|
@@ -16219,6 +16553,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16219
16553
|
minutes: number().int().min(1).max(1440)
|
|
16220
16554
|
})
|
|
16221
16555
|
]);
|
|
16556
|
+
var LegacyConditionLeafSchema = object({
|
|
16557
|
+
kind: literal("condition"),
|
|
16558
|
+
deviceId: number().int().nonnegative(),
|
|
16559
|
+
cap: string().min(1),
|
|
16560
|
+
fieldPath: string().min(1),
|
|
16561
|
+
operator: AutomationConditionOperatorSchema,
|
|
16562
|
+
value: union([
|
|
16563
|
+
string(),
|
|
16564
|
+
number(),
|
|
16565
|
+
boolean(),
|
|
16566
|
+
array(union([string(), number()])),
|
|
16567
|
+
QuantityValueSchema
|
|
16568
|
+
]),
|
|
16569
|
+
unit: string().min(1).optional()
|
|
16570
|
+
});
|
|
16571
|
+
var LegacyStationaryPresentSchema = object({
|
|
16572
|
+
kind: literal("stationary-present"),
|
|
16573
|
+
deviceId: number().int().nonnegative(),
|
|
16574
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
16575
|
+
zones: array(string().min(1)).min(1).optional()
|
|
16576
|
+
});
|
|
16577
|
+
var LegacyExpressionSchema = object({
|
|
16578
|
+
kind: literal("expression"),
|
|
16579
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16580
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
16581
|
+
}).superRefine((src, ctx) => {
|
|
16582
|
+
const err = validateExpressionSource(src);
|
|
16583
|
+
if (err !== null) ctx.addIssue({
|
|
16584
|
+
code: "custom",
|
|
16585
|
+
message: err,
|
|
16586
|
+
path: ["expr"]
|
|
16587
|
+
});
|
|
16588
|
+
});
|
|
16589
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
16590
|
+
object({
|
|
16591
|
+
kind: literal("all"),
|
|
16592
|
+
children: array(LegacyConditionSchema)
|
|
16593
|
+
}),
|
|
16594
|
+
object({
|
|
16595
|
+
kind: literal("any"),
|
|
16596
|
+
children: array(LegacyConditionSchema)
|
|
16597
|
+
}),
|
|
16598
|
+
object({
|
|
16599
|
+
kind: literal("not"),
|
|
16600
|
+
child: LegacyConditionSchema
|
|
16601
|
+
}),
|
|
16602
|
+
LegacyConditionLeafSchema,
|
|
16603
|
+
LegacyExpressionSchema,
|
|
16604
|
+
LegacyStationaryPresentSchema
|
|
16605
|
+
]));
|
|
16606
|
+
function convertTrigger(trigger) {
|
|
16607
|
+
switch (trigger.kind) {
|
|
16608
|
+
case "device-state": return {
|
|
16609
|
+
...trigger,
|
|
16610
|
+
kind: "device-becomes"
|
|
16611
|
+
};
|
|
16612
|
+
case "detection": {
|
|
16613
|
+
const { classes, ...rest } = trigger;
|
|
16614
|
+
return {
|
|
16615
|
+
...rest,
|
|
16616
|
+
subjects: classes.map(subjectFromClassName)
|
|
16617
|
+
};
|
|
16618
|
+
}
|
|
16619
|
+
case "stationary": {
|
|
16620
|
+
const { classes, ...rest } = trigger;
|
|
16621
|
+
return {
|
|
16622
|
+
...rest,
|
|
16623
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16624
|
+
};
|
|
16625
|
+
}
|
|
16626
|
+
default: return trigger;
|
|
16627
|
+
}
|
|
16628
|
+
}
|
|
16629
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
16630
|
+
function convertLegacyCondition(condition) {
|
|
16631
|
+
switch (condition.kind) {
|
|
16632
|
+
case "condition": return {
|
|
16633
|
+
...condition,
|
|
16634
|
+
kind: "device-is"
|
|
16635
|
+
};
|
|
16636
|
+
case "stationary-present": {
|
|
16637
|
+
const { classes, ...rest } = condition;
|
|
16638
|
+
return {
|
|
16639
|
+
...rest,
|
|
16640
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16641
|
+
};
|
|
16642
|
+
}
|
|
16643
|
+
case "expression": return condition;
|
|
16644
|
+
case "not": return {
|
|
16645
|
+
kind: "not",
|
|
16646
|
+
child: convertLegacyCondition(condition.child)
|
|
16647
|
+
};
|
|
16648
|
+
case "all":
|
|
16649
|
+
case "any": return {
|
|
16650
|
+
kind: condition.kind,
|
|
16651
|
+
children: condition.children.map(convertLegacyCondition)
|
|
16652
|
+
};
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
16656
|
+
function convertLegacyWhen(half) {
|
|
16657
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
16658
|
+
const first = leaves[0];
|
|
16659
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
16660
|
+
kind: "any",
|
|
16661
|
+
children: leaves
|
|
16662
|
+
};
|
|
16663
|
+
if (half.conditions === void 0) return triggers;
|
|
16664
|
+
return {
|
|
16665
|
+
kind: "all",
|
|
16666
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
16667
|
+
};
|
|
16668
|
+
}
|
|
16669
|
+
/**
|
|
16670
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
16671
|
+
* becomes code.
|
|
16672
|
+
*
|
|
16673
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
16674
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
16675
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
16676
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
16677
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
16678
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
16679
|
+
*
|
|
16680
|
+
* Two parts:
|
|
16681
|
+
*
|
|
16682
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
16683
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
16684
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
16685
|
+
* ADR "one tree".
|
|
16686
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
16687
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
16688
|
+
* this had to diverge from "verbatim", and why.
|
|
16689
|
+
*
|
|
16690
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
16691
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
16692
|
+
*/
|
|
16222
16693
|
/**
|
|
16223
16694
|
* One action step.
|
|
16224
16695
|
*
|
|
@@ -16321,69 +16792,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16321
16792
|
minutes: number().int().min(1).max(1440)
|
|
16322
16793
|
})
|
|
16323
16794
|
]);
|
|
16324
|
-
var
|
|
16325
|
-
|
|
16326
|
-
conditions: AutomationConditionSchema.optional(),
|
|
16795
|
+
var CurrentAutomationRecipeSchema = object({
|
|
16796
|
+
when: AutomationWhenSchema,
|
|
16327
16797
|
actions: array(AutomationActionSchema),
|
|
16328
16798
|
runMode: AutomationRunModeSchema.optional(),
|
|
16329
16799
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16330
16800
|
keepTraces: number().int().min(1).max(100).optional()
|
|
16331
16801
|
});
|
|
16332
|
-
|
|
16333
|
-
|
|
16334
|
-
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
|
|
16342
|
-
|
|
16343
|
-
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
bus: "vehicle",
|
|
16358
|
-
train: "vehicle",
|
|
16359
|
-
truck: "vehicle",
|
|
16360
|
-
boat: "vehicle",
|
|
16361
|
-
bird: "animal",
|
|
16362
|
-
cat: "animal",
|
|
16363
|
-
dog: "animal",
|
|
16364
|
-
horse: "animal",
|
|
16365
|
-
sheep: "animal",
|
|
16366
|
-
cow: "animal",
|
|
16367
|
-
elephant: "animal",
|
|
16368
|
-
bear: "animal",
|
|
16369
|
-
zebra: "animal",
|
|
16370
|
-
giraffe: "animal",
|
|
16371
|
-
suitcase: "package",
|
|
16372
|
-
backpack: "package",
|
|
16373
|
-
handbag: "package"
|
|
16374
|
-
},
|
|
16375
|
-
preserveOriginal: false
|
|
16376
|
-
};
|
|
16802
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
16803
|
+
var LegacyAutomationRecipeSchema = object({
|
|
16804
|
+
triggers: array(LegacyTriggerSchema),
|
|
16805
|
+
conditions: LegacyConditionSchema.optional(),
|
|
16806
|
+
actions: array(AutomationActionSchema),
|
|
16807
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
16808
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
16809
|
+
});
|
|
16810
|
+
/** The old shape as the new one, lossless. */
|
|
16811
|
+
function convertLegacyRecipe(legacy) {
|
|
16812
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
16813
|
+
return {
|
|
16814
|
+
when: convertLegacyWhen({
|
|
16815
|
+
triggers,
|
|
16816
|
+
conditions
|
|
16817
|
+
}),
|
|
16818
|
+
...rest
|
|
16819
|
+
};
|
|
16820
|
+
}
|
|
16821
|
+
/**
|
|
16822
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
16823
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
16824
|
+
* it. The output type is always the new one.
|
|
16825
|
+
*/
|
|
16826
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16377
16827
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16378
16828
|
/**
|
|
16379
16829
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16380
16830
|
* so the editor can paint every block green or red with its reason.
|
|
16381
16831
|
*
|
|
16382
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16383
|
-
* every
|
|
16384
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16385
|
-
* each addressed by a stable path (`
|
|
16386
|
-
* for an Action run).
|
|
16832
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
16833
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
16834
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
16835
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
16836
|
+
* for an Action run). Traces written before the single tree carry
|
|
16837
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16387
16838
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16388
16839
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16389
16840
|
*
|
|
@@ -16415,6 +16866,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16415
16866
|
]);
|
|
16416
16867
|
var AutomationTriggerTraceSchema = object({
|
|
16417
16868
|
kind: string().min(1),
|
|
16869
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
16870
|
+
path: string().min(1).optional(),
|
|
16418
16871
|
outcome: _enum(["pass", "fail"]),
|
|
16419
16872
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16420
16873
|
reason: string(),
|
|
@@ -16422,15 +16875,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16422
16875
|
vars: record(string(), TraceValueSchema)
|
|
16423
16876
|
});
|
|
16424
16877
|
var AutomationConditionTraceNodeSchema = object({
|
|
16425
|
-
/** `
|
|
16878
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16426
16879
|
path: string().min(1),
|
|
16427
16880
|
kind: _enum([
|
|
16428
16881
|
"all",
|
|
16429
16882
|
"any",
|
|
16430
16883
|
"not",
|
|
16431
16884
|
"condition",
|
|
16885
|
+
"device-is",
|
|
16432
16886
|
"expression",
|
|
16433
|
-
"stationary-present"
|
|
16887
|
+
"stationary-present",
|
|
16888
|
+
"device-becomes",
|
|
16889
|
+
"device-event",
|
|
16890
|
+
"schedule",
|
|
16891
|
+
"interval",
|
|
16892
|
+
"manual",
|
|
16893
|
+
"detection",
|
|
16894
|
+
"stationary"
|
|
16434
16895
|
]),
|
|
16435
16896
|
outcome: TraceNodeOutcomeSchema,
|
|
16436
16897
|
reason: string(),
|
|
@@ -16464,6 +16925,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16464
16925
|
deviceId: number().int().optional(),
|
|
16465
16926
|
startedAt: number().optional(),
|
|
16466
16927
|
endedAt: number().optional(),
|
|
16928
|
+
/**
|
|
16929
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
16930
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
16931
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
16932
|
+
*/
|
|
16933
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16467
16934
|
outcome: _enum([
|
|
16468
16935
|
"ok",
|
|
16469
16936
|
"error",
|
|
@@ -17213,6 +17680,23 @@ method(object({
|
|
|
17213
17680
|
addonId: string(),
|
|
17214
17681
|
nodeId: string().optional()
|
|
17215
17682
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
17683
|
+
addonId: string(),
|
|
17684
|
+
nodeId: string().optional()
|
|
17685
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
17686
|
+
addonId: string(),
|
|
17687
|
+
nodeId: string().optional(),
|
|
17688
|
+
keys: array(string())
|
|
17689
|
+
}), object({
|
|
17690
|
+
removed: array(string()),
|
|
17691
|
+
refused: array(object({
|
|
17692
|
+
key: string(),
|
|
17693
|
+
reason: string()
|
|
17694
|
+
})),
|
|
17695
|
+
secretsCleared: number()
|
|
17696
|
+
}), {
|
|
17697
|
+
kind: "mutation",
|
|
17698
|
+
auth: "admin"
|
|
17699
|
+
}), method(object({
|
|
17216
17700
|
addonId: string(),
|
|
17217
17701
|
deviceId: number(),
|
|
17218
17702
|
nodeId: string().optional()
|
|
@@ -21242,6 +21726,8 @@ var StorageKindSchema = object({
|
|
|
21242
21726
|
"orphaned",
|
|
21243
21727
|
"dropped"
|
|
21244
21728
|
]),
|
|
21729
|
+
/** An owner declared this kind since the engine started. */
|
|
21730
|
+
declaredThisBoot: boolean(),
|
|
21245
21731
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21246
21732
|
rows: number().int().nonnegative().nullable(),
|
|
21247
21733
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21253,6 +21739,51 @@ var StorageKindSchema = object({
|
|
|
21253
21739
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21254
21740
|
declaration: RecordKindDeclarationSchema
|
|
21255
21741
|
});
|
|
21742
|
+
_enum([
|
|
21743
|
+
"orphaned",
|
|
21744
|
+
"stale",
|
|
21745
|
+
"unknown"
|
|
21746
|
+
]);
|
|
21747
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
21748
|
+
var StaleKindFindingSchema = object({
|
|
21749
|
+
owner: string(),
|
|
21750
|
+
kind: string(),
|
|
21751
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
21752
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
21753
|
+
reason: string(),
|
|
21754
|
+
rows: number().int().nonnegative().nullable(),
|
|
21755
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
21756
|
+
protected: boolean()
|
|
21757
|
+
});
|
|
21758
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
21759
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
21760
|
+
addonId: string(),
|
|
21761
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
21762
|
+
key: string(),
|
|
21763
|
+
/**
|
|
21764
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
21765
|
+
* not orphans yet. Removing the key releases them.
|
|
21766
|
+
*/
|
|
21767
|
+
secretRefs: array(string())
|
|
21768
|
+
});
|
|
21769
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
21770
|
+
var StaleUnknownSchema = object({
|
|
21771
|
+
subject: string(),
|
|
21772
|
+
reason: string()
|
|
21773
|
+
});
|
|
21774
|
+
var StaleDataReportSchema = object({
|
|
21775
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
21776
|
+
scannedAt: number().nullable(),
|
|
21777
|
+
scanning: boolean(),
|
|
21778
|
+
trigger: _enum([
|
|
21779
|
+
"boot",
|
|
21780
|
+
"interval",
|
|
21781
|
+
"manual"
|
|
21782
|
+
]).nullable(),
|
|
21783
|
+
kinds: array(StaleKindFindingSchema),
|
|
21784
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
21785
|
+
unknown: array(StaleUnknownSchema)
|
|
21786
|
+
});
|
|
21256
21787
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21257
21788
|
owner: string().min(1).optional(),
|
|
21258
21789
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21260,7 +21791,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21260
21791
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21261
21792
|
owner: string().min(1),
|
|
21262
21793
|
kind: RecordKindNameSchema
|
|
21263
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
21794
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
21795
|
+
kind: "mutation",
|
|
21796
|
+
auth: "admin"
|
|
21797
|
+
}), method(object({
|
|
21798
|
+
owner: string().min(1),
|
|
21799
|
+
kind: RecordKindNameSchema
|
|
21800
|
+
}), object({
|
|
21801
|
+
deleted: number().int().nonnegative(),
|
|
21802
|
+
secretsReleased: number().int().nonnegative()
|
|
21803
|
+
}), {
|
|
21804
|
+
kind: "mutation",
|
|
21805
|
+
auth: "admin",
|
|
21806
|
+
caller: "required"
|
|
21807
|
+
}), method(object({
|
|
21808
|
+
addonId: string().min(1),
|
|
21809
|
+
key: string().min(1)
|
|
21810
|
+
}), object({
|
|
21811
|
+
removed: boolean(),
|
|
21812
|
+
secretsCleared: number().int().nonnegative()
|
|
21813
|
+
}), {
|
|
21814
|
+
kind: "mutation",
|
|
21815
|
+
auth: "admin",
|
|
21816
|
+
caller: "required"
|
|
21817
|
+
});
|
|
21264
21818
|
/**
|
|
21265
21819
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21266
21820
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21399,6 +21953,12 @@ method(object({
|
|
|
21399
21953
|
}), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
|
|
21400
21954
|
kind: "mutation",
|
|
21401
21955
|
auth: "admin"
|
|
21956
|
+
}), method(object({
|
|
21957
|
+
refs: RefListSchema,
|
|
21958
|
+
onBehalfOf: string().min(1)
|
|
21959
|
+
}), object({ released: number().int() }), {
|
|
21960
|
+
kind: "mutation",
|
|
21961
|
+
auth: "admin"
|
|
21402
21962
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21403
21963
|
/**
|
|
21404
21964
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22301,7 +22861,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22301
22861
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22302
22862
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22303
22863
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22304
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
22864
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
22865
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
22866
|
+
parentStableId: string().optional()
|
|
22305
22867
|
}));
|
|
22306
22868
|
/**
|
|
22307
22869
|
* Status block — populated by the integration after each
|
|
@@ -42684,6 +43246,79 @@ object({
|
|
|
42684
43246
|
metadata: record(string(), unknown()).optional()
|
|
42685
43247
|
});
|
|
42686
43248
|
/**
|
|
43249
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43250
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43251
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43252
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43253
|
+
* except through a `custom` mode.
|
|
43254
|
+
*/
|
|
43255
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43256
|
+
object({
|
|
43257
|
+
kind: literal("mdns"),
|
|
43258
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43259
|
+
serviceType: string().min(1),
|
|
43260
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43261
|
+
instancePattern: string().optional()
|
|
43262
|
+
}),
|
|
43263
|
+
object({
|
|
43264
|
+
kind: literal("ws-discovery"),
|
|
43265
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43266
|
+
types: array(string()).optional()
|
|
43267
|
+
}),
|
|
43268
|
+
object({
|
|
43269
|
+
kind: literal("ssdp"),
|
|
43270
|
+
searchTarget: string().min(1)
|
|
43271
|
+
}),
|
|
43272
|
+
object({
|
|
43273
|
+
kind: literal("udp-broadcast"),
|
|
43274
|
+
port: number().int().min(1).max(65535),
|
|
43275
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43276
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43277
|
+
listenPort: number().int().min(1).max(65535).optional()
|
|
43278
|
+
}),
|
|
43279
|
+
object({
|
|
43280
|
+
kind: literal("http-probe"),
|
|
43281
|
+
port: number().int().min(1).max(65535),
|
|
43282
|
+
path: string().startsWith("/"),
|
|
43283
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43284
|
+
bodyText: string().optional(),
|
|
43285
|
+
contentType: string().optional(),
|
|
43286
|
+
tls: boolean().default(false),
|
|
43287
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43288
|
+
acceptStatus: array(number().int()).optional()
|
|
43289
|
+
}),
|
|
43290
|
+
object({
|
|
43291
|
+
kind: literal("custom"),
|
|
43292
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43293
|
+
reason: string().min(1)
|
|
43294
|
+
})
|
|
43295
|
+
]);
|
|
43296
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43297
|
+
var DiscoveryHitSchema = object({
|
|
43298
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43299
|
+
modeIndex: number().int().min(0),
|
|
43300
|
+
host: string(),
|
|
43301
|
+
port: number().int(),
|
|
43302
|
+
http: object({
|
|
43303
|
+
status: number().int(),
|
|
43304
|
+
headers: record(string(), string()),
|
|
43305
|
+
/** At most 16 KiB. */
|
|
43306
|
+
bodyText: string()
|
|
43307
|
+
}).optional(),
|
|
43308
|
+
mdns: object({
|
|
43309
|
+
instance: string(),
|
|
43310
|
+
host: string(),
|
|
43311
|
+
port: number().int(),
|
|
43312
|
+
txt: record(string(), string())
|
|
43313
|
+
}).optional(),
|
|
43314
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43315
|
+
wsd: object({
|
|
43316
|
+
xaddrs: array(string()),
|
|
43317
|
+
types: array(string()),
|
|
43318
|
+
scopes: array(string())
|
|
43319
|
+
}).optional()
|
|
43320
|
+
});
|
|
43321
|
+
/**
|
|
42687
43322
|
* Candidate handed back from discovery and accepted by
|
|
42688
43323
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
42689
43324
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -42718,7 +43353,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
42718
43353
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
42719
43354
|
onboardedDeviceId: number().optional(),
|
|
42720
43355
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
42721
|
-
onboardedName: string().optional()
|
|
43356
|
+
onboardedName: string().optional(),
|
|
43357
|
+
/**
|
|
43358
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43359
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43360
|
+
* which is still read for one release.
|
|
43361
|
+
*/
|
|
43362
|
+
setupIntegration: boolean().optional(),
|
|
43363
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43364
|
+
parentStableId: string().optional(),
|
|
43365
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43366
|
+
selectable: boolean().optional()
|
|
42722
43367
|
});
|
|
42723
43368
|
/**
|
|
42724
43369
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -42815,6 +43460,24 @@ var deviceProviderCapability = {
|
|
|
42815
43460
|
auth: "admin"
|
|
42816
43461
|
}),
|
|
42817
43462
|
/**
|
|
43463
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43464
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43465
|
+
*/
|
|
43466
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43467
|
+
/**
|
|
43468
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43469
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43470
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43471
|
+
* from `getDiscoveryParamsSchema`.
|
|
43472
|
+
*/
|
|
43473
|
+
classifyDiscoveryHits: method(object({
|
|
43474
|
+
hits: array(DiscoveryHitSchema),
|
|
43475
|
+
params: record(string(), unknown()).optional()
|
|
43476
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43477
|
+
kind: "mutation",
|
|
43478
|
+
auth: "admin"
|
|
43479
|
+
}),
|
|
43480
|
+
/**
|
|
42818
43481
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
42819
43482
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
42820
43483
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -43579,17 +44242,23 @@ method(object({
|
|
|
43579
44242
|
}), method(object({}), object({ providers: array(object({
|
|
43580
44243
|
addonId: string(),
|
|
43581
44244
|
label: string()
|
|
43582
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
43583
|
-
|
|
43584
|
-
|
|
43585
|
-
|
|
43586
|
-
|
|
43587
|
-
|
|
43588
|
-
|
|
43589
|
-
|
|
43590
|
-
}),
|
|
44245
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44246
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44247
|
+
object({ subnets: array(string()).optional() }),
|
|
44248
|
+
object({ groups: array(object({
|
|
44249
|
+
addonId: string(),
|
|
44250
|
+
label: string(),
|
|
44251
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44252
|
+
error: string().nullable()
|
|
44253
|
+
})).readonly() }),
|
|
44254
|
+
{
|
|
44255
|
+
kind: "mutation",
|
|
44256
|
+
auth: "admin"
|
|
44257
|
+
}
|
|
44258
|
+
), method(object({
|
|
43591
44259
|
addonId: string(),
|
|
43592
|
-
params: record(string(), unknown()).optional()
|
|
44260
|
+
params: record(string(), unknown()).optional(),
|
|
44261
|
+
subnets: array(string()).optional()
|
|
43593
44262
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
43594
44263
|
kind: "mutation",
|
|
43595
44264
|
auth: "admin"
|
|
@@ -56256,6 +56925,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
56256
56925
|
instanceId: string(),
|
|
56257
56926
|
cameraStableId: string(),
|
|
56258
56927
|
nodeId: string(),
|
|
56928
|
+
/**
|
|
56929
|
+
* False while the node the instance is bound to is not in the cluster
|
|
56930
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
56931
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
56932
|
+
*/
|
|
56933
|
+
nodeInCluster: boolean(),
|
|
56259
56934
|
profileId: string(),
|
|
56260
56935
|
profileLabel: string(),
|
|
56261
56936
|
name: string(),
|
|
@@ -56305,6 +56980,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
56305
56980
|
auth: "admin"
|
|
56306
56981
|
}), method(object({
|
|
56307
56982
|
instanceId: string().min(1),
|
|
56983
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
56984
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
56308
56985
|
name: string().trim().min(1).max(160).optional(),
|
|
56309
56986
|
executable: string().max(1024).optional(),
|
|
56310
56987
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -62931,7 +63608,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
62931
63608
|
id: string(),
|
|
62932
63609
|
settings: record(string(), unknown()),
|
|
62933
63610
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
62934
|
-
secretKeys: array(string()).optional()
|
|
63611
|
+
secretKeys: array(string()).optional(),
|
|
63612
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
63613
|
+
skipRestart: boolean().optional()
|
|
62935
63614
|
}), object({ success: boolean() }), {
|
|
62936
63615
|
kind: "mutation",
|
|
62937
63616
|
auth: "admin"
|
|
@@ -63559,6 +64238,39 @@ var TlsStatusSchema = object({
|
|
|
63559
64238
|
hasCa: boolean()
|
|
63560
64239
|
}).nullable()
|
|
63561
64240
|
});
|
|
64241
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64242
|
+
var ScanRequestSchema = object({
|
|
64243
|
+
providerId: string().min(1),
|
|
64244
|
+
modes: array(DiscoveryModeSchema)
|
|
64245
|
+
});
|
|
64246
|
+
var ScanErrorSchema = object({
|
|
64247
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64248
|
+
providerId: string().optional(),
|
|
64249
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64250
|
+
mode: string().optional(),
|
|
64251
|
+
message: string()
|
|
64252
|
+
});
|
|
64253
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64254
|
+
var ScanStatsSchema = object({
|
|
64255
|
+
hostsProbed: number().int(),
|
|
64256
|
+
hits: number().int(),
|
|
64257
|
+
timeouts: number().int(),
|
|
64258
|
+
byMode: record(string(), object({
|
|
64259
|
+
probed: number().int(),
|
|
64260
|
+
hits: number().int(),
|
|
64261
|
+
timeouts: number().int()
|
|
64262
|
+
}))
|
|
64263
|
+
});
|
|
64264
|
+
var ScanNetworkResultSchema = object({
|
|
64265
|
+
hits: array(object({
|
|
64266
|
+
providerId: string(),
|
|
64267
|
+
hit: DiscoveryHitSchema
|
|
64268
|
+
})),
|
|
64269
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64270
|
+
subnetsScanned: array(string()),
|
|
64271
|
+
errors: array(ScanErrorSchema),
|
|
64272
|
+
stats: ScanStatsSchema
|
|
64273
|
+
});
|
|
63562
64274
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
63563
64275
|
/**
|
|
63564
64276
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -63613,6 +64325,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
63613
64325
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
63614
64326
|
kind: "mutation",
|
|
63615
64327
|
auth: "admin"
|
|
64328
|
+
}), method(object({
|
|
64329
|
+
requests: array(ScanRequestSchema),
|
|
64330
|
+
subnets: array(string()).optional()
|
|
64331
|
+
}), ScanNetworkResultSchema, {
|
|
64332
|
+
kind: "mutation",
|
|
64333
|
+
auth: "admin",
|
|
64334
|
+
timeoutMs: 9e4
|
|
64335
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64336
|
+
kind: "mutation",
|
|
64337
|
+
auth: "admin"
|
|
63616
64338
|
}), method(_void(), TlsStatusSchema, {
|
|
63617
64339
|
kind: "mutation",
|
|
63618
64340
|
auth: "admin"
|
|
@@ -73195,6 +73917,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73195
73917
|
async discoverDevices(_input) {
|
|
73196
73918
|
return [];
|
|
73197
73919
|
}
|
|
73920
|
+
/**
|
|
73921
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
73922
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
73923
|
+
*/
|
|
73924
|
+
async getDiscoveryModes() {
|
|
73925
|
+
return [];
|
|
73926
|
+
}
|
|
73927
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
73928
|
+
async classifyDiscoveryHits(_input) {
|
|
73929
|
+
return [];
|
|
73930
|
+
}
|
|
73198
73931
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73199
73932
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73200
73933
|
async getDiscoveryParamsSchema() {
|
|
@@ -74063,6 +74796,13 @@ Object.freeze({
|
|
|
74063
74796
|
access: "create",
|
|
74064
74797
|
role: "admin"
|
|
74065
74798
|
},
|
|
74799
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
74800
|
+
capName: "addon-settings",
|
|
74801
|
+
capScope: "system",
|
|
74802
|
+
addonId: null,
|
|
74803
|
+
access: "view",
|
|
74804
|
+
role: "admin"
|
|
74805
|
+
},
|
|
74066
74806
|
"addonSettings.getDeviceSettings": {
|
|
74067
74807
|
capName: "addon-settings",
|
|
74068
74808
|
capScope: "system",
|
|
@@ -74084,6 +74824,13 @@ Object.freeze({
|
|
|
74084
74824
|
access: "view",
|
|
74085
74825
|
role: "admin"
|
|
74086
74826
|
},
|
|
74827
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
74828
|
+
capName: "addon-settings",
|
|
74829
|
+
capScope: "system",
|
|
74830
|
+
addonId: null,
|
|
74831
|
+
access: "create",
|
|
74832
|
+
role: "admin"
|
|
74833
|
+
},
|
|
74087
74834
|
"addonSettings.updateDeviceSettings": {
|
|
74088
74835
|
capName: "addon-settings",
|
|
74089
74836
|
capScope: "system",
|
|
@@ -76499,6 +77246,13 @@ Object.freeze({
|
|
|
76499
77246
|
access: "create",
|
|
76500
77247
|
role: "admin"
|
|
76501
77248
|
},
|
|
77249
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77250
|
+
capName: "device-provider",
|
|
77251
|
+
capScope: "system",
|
|
77252
|
+
addonId: null,
|
|
77253
|
+
access: "create",
|
|
77254
|
+
role: "admin"
|
|
77255
|
+
},
|
|
76502
77256
|
"deviceProvider.createDevice": {
|
|
76503
77257
|
capName: "device-provider",
|
|
76504
77258
|
capScope: "system",
|
|
@@ -76527,6 +77281,13 @@ Object.freeze({
|
|
|
76527
77281
|
access: "view",
|
|
76528
77282
|
role: "admin"
|
|
76529
77283
|
},
|
|
77284
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77285
|
+
capName: "device-provider",
|
|
77286
|
+
capScope: "system",
|
|
77287
|
+
addonId: null,
|
|
77288
|
+
access: "view",
|
|
77289
|
+
role: "admin"
|
|
77290
|
+
},
|
|
76530
77291
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76531
77292
|
capName: "device-provider",
|
|
76532
77293
|
capScope: "system",
|
|
@@ -77304,6 +78065,13 @@ Object.freeze({
|
|
|
77304
78065
|
access: "create",
|
|
77305
78066
|
role: "admin"
|
|
77306
78067
|
},
|
|
78068
|
+
"keychain.releaseRecordRefs": {
|
|
78069
|
+
capName: "keychain",
|
|
78070
|
+
capScope: "system",
|
|
78071
|
+
addonId: null,
|
|
78072
|
+
access: "create",
|
|
78073
|
+
role: "admin"
|
|
78074
|
+
},
|
|
77307
78075
|
"keychain.resolve": {
|
|
77308
78076
|
capName: "keychain",
|
|
77309
78077
|
capScope: "system",
|
|
@@ -77724,6 +78492,13 @@ Object.freeze({
|
|
|
77724
78492
|
access: "view",
|
|
77725
78493
|
role: "admin"
|
|
77726
78494
|
},
|
|
78495
|
+
"localNetwork.getScanSubnets": {
|
|
78496
|
+
capName: "local-network",
|
|
78497
|
+
capScope: "system",
|
|
78498
|
+
addonId: null,
|
|
78499
|
+
access: "view",
|
|
78500
|
+
role: "admin"
|
|
78501
|
+
},
|
|
77727
78502
|
"localNetwork.getTlsStatus": {
|
|
77728
78503
|
capName: "local-network",
|
|
77729
78504
|
capScope: "system",
|
|
@@ -77773,6 +78548,13 @@ Object.freeze({
|
|
|
77773
78548
|
access: "create",
|
|
77774
78549
|
role: "admin"
|
|
77775
78550
|
},
|
|
78551
|
+
"localNetwork.scanNetwork": {
|
|
78552
|
+
capName: "local-network",
|
|
78553
|
+
capScope: "system",
|
|
78554
|
+
addonId: null,
|
|
78555
|
+
access: "create",
|
|
78556
|
+
role: "admin"
|
|
78557
|
+
},
|
|
77776
78558
|
"localNetwork.setAddressRole": {
|
|
77777
78559
|
capName: "local-network",
|
|
77778
78560
|
capScope: "system",
|
|
@@ -77794,6 +78576,13 @@ Object.freeze({
|
|
|
77794
78576
|
access: "create",
|
|
77795
78577
|
role: "admin"
|
|
77796
78578
|
},
|
|
78579
|
+
"localNetwork.setScanSubnets": {
|
|
78580
|
+
capName: "local-network",
|
|
78581
|
+
capScope: "system",
|
|
78582
|
+
addonId: null,
|
|
78583
|
+
access: "create",
|
|
78584
|
+
role: "admin"
|
|
78585
|
+
},
|
|
77797
78586
|
"localNetwork.setViewerEndpoints": {
|
|
77798
78587
|
capName: "local-network",
|
|
77799
78588
|
capScope: "system",
|
|
@@ -82015,6 +82804,13 @@ Object.freeze({
|
|
|
82015
82804
|
access: "create",
|
|
82016
82805
|
role: "admin"
|
|
82017
82806
|
},
|
|
82807
|
+
"storageAdmin.dropOrphanedKind": {
|
|
82808
|
+
capName: "storage-admin",
|
|
82809
|
+
capScope: "system",
|
|
82810
|
+
addonId: null,
|
|
82811
|
+
access: "delete",
|
|
82812
|
+
role: "admin"
|
|
82813
|
+
},
|
|
82018
82814
|
"storageAdmin.listKinds": {
|
|
82019
82815
|
capName: "storage-admin",
|
|
82020
82816
|
capScope: "system",
|
|
@@ -82036,6 +82832,27 @@ Object.freeze({
|
|
|
82036
82832
|
access: "view",
|
|
82037
82833
|
role: "admin"
|
|
82038
82834
|
},
|
|
82835
|
+
"storageAdmin.scanStaleData": {
|
|
82836
|
+
capName: "storage-admin",
|
|
82837
|
+
capScope: "system",
|
|
82838
|
+
addonId: null,
|
|
82839
|
+
access: "create",
|
|
82840
|
+
role: "admin"
|
|
82841
|
+
},
|
|
82842
|
+
"storageAdmin.staleDataReport": {
|
|
82843
|
+
capName: "storage-admin",
|
|
82844
|
+
capScope: "system",
|
|
82845
|
+
addonId: null,
|
|
82846
|
+
access: "view",
|
|
82847
|
+
role: "admin"
|
|
82848
|
+
},
|
|
82849
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
82850
|
+
capName: "storage-admin",
|
|
82851
|
+
capScope: "system",
|
|
82852
|
+
addonId: null,
|
|
82853
|
+
access: "create",
|
|
82854
|
+
role: "admin"
|
|
82855
|
+
},
|
|
82039
82856
|
"storageEvictable.evict": {
|
|
82040
82857
|
capName: "storage-evictable",
|
|
82041
82858
|
capScope: "system",
|
|
@@ -92505,7 +93322,8 @@ function resolveNativeId(integrationId, covers, childNativeId) {
|
|
|
92505
93322
|
/**
|
|
92506
93323
|
* The adoption candidates of one integration, pure and in a fixed order: the hub
|
|
92507
93324
|
* box ({@link DeviceType.Gateway}), then one {@link DeviceType.Cover} per roller
|
|
92508
|
-
* shutter.
|
|
93325
|
+
* shutter. Each cover names the gateway through `parentStableId`, so the
|
|
93326
|
+
* adoption modal nests it under the hub.
|
|
92509
93327
|
*/
|
|
92510
93328
|
function buildRademacherCandidates(input) {
|
|
92511
93329
|
const gatewayId = gatewayNativeId(input.integrationId);
|
|
@@ -92537,7 +93355,8 @@ function buildRademacherCandidates(input) {
|
|
|
92537
93355
|
},
|
|
92538
93356
|
alreadyAdopted: adoptedId !== null,
|
|
92539
93357
|
adoptedDeviceId: adoptedId,
|
|
92540
|
-
capabilities: [coverCapability.name]
|
|
93358
|
+
capabilities: [coverCapability.name],
|
|
93359
|
+
parentStableId: gatewayId
|
|
92541
93360
|
});
|
|
92542
93361
|
}
|
|
92543
93362
|
return out;
|
|
@@ -93224,7 +94043,7 @@ var HubDevice = class extends BaseDevice {
|
|
|
93224
94043
|
var VERSION_PATH = "/service/system-update-image/version";
|
|
93225
94044
|
var NODENAME_PATH = "/service/system/networkmgr/v1/nodename";
|
|
93226
94045
|
var INTERFACES_PATH = "/service/system/networkmgr/v1/interfaces";
|
|
93227
|
-
var SALT_PATH = "/authentication/password_salt";
|
|
94046
|
+
var SALT_PATH$1 = "/authentication/password_salt";
|
|
93228
94047
|
async function tryJson(fetchImpl, url, timeoutMs) {
|
|
93229
94048
|
try {
|
|
93230
94049
|
const res = await fetchImpl(url, {
|
|
@@ -93263,7 +94082,7 @@ async function probeHub(fetchImpl, host, timeoutMs = 1500) {
|
|
|
93263
94082
|
}
|
|
93264
94083
|
async function saltStatus(fetchImpl, host, basePath, timeoutMs) {
|
|
93265
94084
|
try {
|
|
93266
|
-
return (await fetchImpl(`http://${host}${basePath}${SALT_PATH}`, {
|
|
94085
|
+
return (await fetchImpl(`http://${host}${basePath}${SALT_PATH$1}`, {
|
|
93267
94086
|
method: "POST",
|
|
93268
94087
|
timeoutMs
|
|
93269
94088
|
})).statusCode;
|
|
@@ -94141,8 +94960,28 @@ function slugifyHost(value) {
|
|
|
94141
94960
|
}
|
|
94142
94961
|
//#endregion
|
|
94143
94962
|
//#region src/rademacher-hub-discovery.ts
|
|
94144
|
-
/**
|
|
94145
|
-
|
|
94963
|
+
/**
|
|
94964
|
+
* The request the library's probe opens with: a POST on the password-salt
|
|
94965
|
+
* endpoint, which a HomePilot / Start2Smart hub answers 200 (protected) or 500
|
|
94966
|
+
* (open). The api base path is `''` on v1 and `/hp` on v2, so there are two modes.
|
|
94967
|
+
*/
|
|
94968
|
+
var SALT_PATH = "/authentication/password_salt";
|
|
94969
|
+
var SALT_STATUSES = [200, 500];
|
|
94970
|
+
var RADEMACHER_DISCOVERY_MODES = [{
|
|
94971
|
+
kind: "http-probe",
|
|
94972
|
+
port: 80,
|
|
94973
|
+
path: SALT_PATH,
|
|
94974
|
+
method: "POST",
|
|
94975
|
+
tls: false,
|
|
94976
|
+
acceptStatus: [...SALT_STATUSES]
|
|
94977
|
+
}, {
|
|
94978
|
+
kind: "http-probe",
|
|
94979
|
+
port: 80,
|
|
94980
|
+
path: `/hp${SALT_PATH}`,
|
|
94981
|
+
method: "POST",
|
|
94982
|
+
tls: false,
|
|
94983
|
+
acceptStatus: [...SALT_STATUSES]
|
|
94984
|
+
}];
|
|
94146
94985
|
/** Project the library's probe result onto the shape the form and the candidates share. */
|
|
94147
94986
|
function hubFromIdentity(identity) {
|
|
94148
94987
|
const uid = identity.uid?.trim().toLowerCase() ?? "";
|
|
@@ -94155,14 +94994,35 @@ function hubFromIdentity(identity) {
|
|
|
94155
94994
|
basePath: identity.basePath
|
|
94156
94995
|
};
|
|
94157
94996
|
}
|
|
94158
|
-
|
|
94159
|
-
|
|
94160
|
-
|
|
94997
|
+
var probeFetch = async (url, options) => {
|
|
94998
|
+
const res = await fetch(url, {
|
|
94999
|
+
method: options?.method ?? "GET",
|
|
95000
|
+
...options?.body !== void 0 ? { body: options.body } : {},
|
|
95001
|
+
...options?.headers !== void 0 ? { headers: { ...options.headers } } : {},
|
|
95002
|
+
signal: AbortSignal.timeout(options?.timeoutMs ?? 1500)
|
|
95003
|
+
});
|
|
95004
|
+
return {
|
|
95005
|
+
statusCode: res.status,
|
|
95006
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
95007
|
+
json: () => res.json(),
|
|
95008
|
+
text: () => res.text()
|
|
95009
|
+
};
|
|
95010
|
+
};
|
|
95011
|
+
var probeRademacherHost = (host) => probeHub(probeFetch, host);
|
|
95012
|
+
/**
|
|
95013
|
+
* Hubs among the engine's http-probe hits. A hit only says the salt endpoint
|
|
95014
|
+
* answered; the library's identity check (a unicast follow-up to that host)
|
|
95015
|
+
* decides whether it really is a hub and reads its name, model and MAC.
|
|
95016
|
+
*/
|
|
95017
|
+
async function hubsFromHits(hits, probe = probeRademacherHost) {
|
|
94161
95018
|
const out = [];
|
|
94162
|
-
|
|
94163
|
-
|
|
94164
|
-
|
|
94165
|
-
|
|
95019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
95020
|
+
for (const hit of hits) {
|
|
95021
|
+
if (hit.http === void 0 || !SALT_STATUSES.includes(hit.http.status)) continue;
|
|
95022
|
+
if (seen.has(hit.host)) continue;
|
|
95023
|
+
seen.add(hit.host);
|
|
95024
|
+
const identity = await probe(hit.host).catch(() => null);
|
|
95025
|
+
if (identity !== null) out.push(hubFromIdentity(identity));
|
|
94166
95026
|
}
|
|
94167
95027
|
return out;
|
|
94168
95028
|
}
|
|
@@ -94177,12 +95037,13 @@ function proposeRademacherHubs(found, existingHosts) {
|
|
|
94177
95037
|
function rademacherDiscoveryCandidates(proposals) {
|
|
94178
95038
|
return proposals.map((found) => ({
|
|
94179
95039
|
stableId: `rademacher:${found.id}`,
|
|
95040
|
+
type: DeviceType.Gateway,
|
|
94180
95041
|
suggestedName: found.name,
|
|
94181
95042
|
prefilledConfig: {
|
|
94182
95043
|
host: found.host,
|
|
94183
|
-
...found.basePath.length > 0 ? { basePath: found.basePath } : {}
|
|
94184
|
-
|
|
94185
|
-
|
|
95044
|
+
...found.basePath.length > 0 ? { basePath: found.basePath } : {}
|
|
95045
|
+
},
|
|
95046
|
+
setupIntegration: true
|
|
94186
95047
|
}));
|
|
94187
95048
|
}
|
|
94188
95049
|
function rademacherDiscoverySummary(foundCount, proposedCount) {
|
|
@@ -94238,14 +95099,15 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
94238
95099
|
],
|
|
94239
95100
|
customActions: rademacherActions,
|
|
94240
95101
|
actionHandlers: { discoverHub: async () => {
|
|
94241
|
-
const {
|
|
95102
|
+
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
95103
|
+
const hosts = candidates.map((candidate) => ({
|
|
95104
|
+
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
95105
|
+
name: candidate.suggestedName,
|
|
95106
|
+
id: candidate.stableId.replace(/^rademacher:/, "")
|
|
95107
|
+
}));
|
|
94242
95108
|
return {
|
|
94243
|
-
hosts
|
|
94244
|
-
|
|
94245
|
-
name: found.name,
|
|
94246
|
-
id: found.id
|
|
94247
|
-
})),
|
|
94248
|
-
summary
|
|
95109
|
+
hosts,
|
|
95110
|
+
summary: rademacherDiscoverySummary(hosts.length, hosts.length)
|
|
94249
95111
|
};
|
|
94250
95112
|
} }
|
|
94251
95113
|
};
|
|
@@ -94265,21 +95127,21 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
94265
95127
|
async supportsManualCreation() {
|
|
94266
95128
|
return false;
|
|
94267
95129
|
}
|
|
94268
|
-
/**
|
|
94269
|
-
* LAN scan for HomePilot / Start2Smart hubs: the library's HTTP probe over the
|
|
94270
|
-
* local /24 networks of this (hub-only) addon's host.
|
|
94271
|
-
*/
|
|
95130
|
+
/** A hub is found by the scan engine's http-probe; this addon classifies the hits. */
|
|
94272
95131
|
async supportsDiscovery() {
|
|
94273
95132
|
return true;
|
|
94274
95133
|
}
|
|
94275
|
-
async
|
|
94276
|
-
|
|
94277
|
-
|
|
94278
|
-
|
|
94279
|
-
|
|
94280
|
-
|
|
94281
|
-
|
|
94282
|
-
|
|
95134
|
+
async getDiscoveryModes() {
|
|
95135
|
+
return RADEMACHER_DISCOVERY_MODES;
|
|
95136
|
+
}
|
|
95137
|
+
async classifyDiscoveryHits(input) {
|
|
95138
|
+
const found = await hubsFromHits(input.hits);
|
|
95139
|
+
const proposals = proposeRademacherHubs(found, await this.knownHubHosts());
|
|
95140
|
+
if (found.length > proposals.length) this.ctx.logger.info("Rademacher discovery skipped hubs already added", { meta: {
|
|
95141
|
+
found: found.length,
|
|
95142
|
+
proposed: proposals.length
|
|
95143
|
+
} });
|
|
95144
|
+
return rademacherDiscoveryCandidates(proposals);
|
|
94283
95145
|
}
|
|
94284
95146
|
async adoptDiscoveredDevice(_input) {
|
|
94285
95147
|
throw new Error("A Rademacher hub is added from the integration form");
|
|
@@ -94296,23 +95158,6 @@ var RademacherProviderAddon = class extends BaseDeviceProvider {
|
|
|
94296
95158
|
if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("provider-rademacher generateStableId: nativeId required");
|
|
94297
95159
|
return `rademacher:${nativeId}`;
|
|
94298
95160
|
}
|
|
94299
|
-
async discoverHubs() {
|
|
94300
|
-
let found = [];
|
|
94301
|
-
try {
|
|
94302
|
-
found = await browseRademacherHubs();
|
|
94303
|
-
} catch (err) {
|
|
94304
|
-
this.ctx.logger.warn("Rademacher hub discovery failed", { meta: { error: errMsg(err) } });
|
|
94305
|
-
}
|
|
94306
|
-
const proposals = proposeRademacherHubs(found, await this.knownHubHosts());
|
|
94307
|
-
if (found.length > proposals.length) this.ctx.logger.info("Rademacher discovery skipped hubs already added", { meta: {
|
|
94308
|
-
found: found.length,
|
|
94309
|
-
proposed: proposals.length
|
|
94310
|
-
} });
|
|
94311
|
-
return {
|
|
94312
|
-
proposals,
|
|
94313
|
-
summary: rademacherDiscoverySummary(found.length, proposals.length)
|
|
94314
|
-
};
|
|
94315
|
-
}
|
|
94316
95161
|
async knownHubHosts() {
|
|
94317
95162
|
try {
|
|
94318
95163
|
const all = await this.ctx.api.integrations.list.query();
|