@camstack/addon-provider-homematic 1.2.163 → 1.2.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +978 -234
- package/dist/addon.mjs +978 -234
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -5465,7 +5465,7 @@ var ZodIssueCode = {
|
|
|
5465
5465
|
var ZodFirstPartyTypeKind;
|
|
5466
5466
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5467
5467
|
//#endregion
|
|
5468
|
-
//#region ../types/dist/sleep-
|
|
5468
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5469
5469
|
/**
|
|
5470
5470
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5471
5471
|
* window to float samples (D455).
|
|
@@ -7532,6 +7532,7 @@ var BaseAddon = class {
|
|
|
7532
7532
|
* a warning and falls back rather than crashing boot.
|
|
7533
7533
|
*/
|
|
7534
7534
|
state(key, schema, fallback) {
|
|
7535
|
+
this._declaredStateKeys.add(key);
|
|
7535
7536
|
return createDurableState({
|
|
7536
7537
|
key,
|
|
7537
7538
|
schema,
|
|
@@ -7547,6 +7548,96 @@ var BaseAddon = class {
|
|
|
7547
7548
|
} })
|
|
7548
7549
|
});
|
|
7549
7550
|
}
|
|
7551
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7552
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7553
|
+
/**
|
|
7554
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7555
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7556
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7557
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7558
|
+
*/
|
|
7559
|
+
async getDeclaredSettingsKeys() {
|
|
7560
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7561
|
+
}
|
|
7562
|
+
declaredKeySet() {
|
|
7563
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7564
|
+
const schema = this.globalSettingsSchema();
|
|
7565
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7566
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7567
|
+
for (const path of this.secretStatePaths) {
|
|
7568
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7569
|
+
if (root) keys.add(root);
|
|
7570
|
+
}
|
|
7571
|
+
return keys;
|
|
7572
|
+
}
|
|
7573
|
+
/**
|
|
7574
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7575
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7576
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7577
|
+
*/
|
|
7578
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7579
|
+
const settings = this._ctx?.settings;
|
|
7580
|
+
const raw = this.rawSettings();
|
|
7581
|
+
if (!settings || !raw) return {
|
|
7582
|
+
removed: [],
|
|
7583
|
+
refused: keys.map((key) => ({
|
|
7584
|
+
key,
|
|
7585
|
+
reason: "no settings store"
|
|
7586
|
+
})),
|
|
7587
|
+
secretsCleared: 0
|
|
7588
|
+
};
|
|
7589
|
+
let stored;
|
|
7590
|
+
try {
|
|
7591
|
+
stored = await raw.readAddonStore();
|
|
7592
|
+
} catch (error) {
|
|
7593
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7594
|
+
return {
|
|
7595
|
+
removed: [],
|
|
7596
|
+
refused: keys.map((key) => ({
|
|
7597
|
+
key,
|
|
7598
|
+
reason
|
|
7599
|
+
})),
|
|
7600
|
+
secretsCleared: 0
|
|
7601
|
+
};
|
|
7602
|
+
}
|
|
7603
|
+
const declared = this.declaredKeySet();
|
|
7604
|
+
const removed = [];
|
|
7605
|
+
const refused = [];
|
|
7606
|
+
for (const key of keys) {
|
|
7607
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7608
|
+
if (declared.has(base)) refused.push({
|
|
7609
|
+
key,
|
|
7610
|
+
reason: "still declared by the addon"
|
|
7611
|
+
});
|
|
7612
|
+
else if (!(key in stored)) refused.push({
|
|
7613
|
+
key,
|
|
7614
|
+
reason: "not stored"
|
|
7615
|
+
});
|
|
7616
|
+
else removed.push(key);
|
|
7617
|
+
}
|
|
7618
|
+
if (removed.length === 0) return {
|
|
7619
|
+
removed,
|
|
7620
|
+
refused,
|
|
7621
|
+
secretsCleared: 0
|
|
7622
|
+
};
|
|
7623
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7624
|
+
const held = /* @__PURE__ */ new Set();
|
|
7625
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7626
|
+
const target = removed.includes(key) ? held : kept;
|
|
7627
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7628
|
+
}
|
|
7629
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7630
|
+
const patch = {};
|
|
7631
|
+
for (const key of removed) patch[key] = void 0;
|
|
7632
|
+
await settings.writeAddonStore(patch);
|
|
7633
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7634
|
+
await this.resolveConfig();
|
|
7635
|
+
return {
|
|
7636
|
+
removed,
|
|
7637
|
+
refused,
|
|
7638
|
+
secretsCleared
|
|
7639
|
+
};
|
|
7640
|
+
}
|
|
7550
7641
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7551
7642
|
deviceState(deviceId, key, schema, fallback) {
|
|
7552
7643
|
return createDurableState({
|
|
@@ -7817,6 +7908,21 @@ function dropPerNodeFields(fields) {
|
|
|
7817
7908
|
}
|
|
7818
7909
|
return kept;
|
|
7819
7910
|
}
|
|
7911
|
+
function collectFieldKeys(fields) {
|
|
7912
|
+
const collected = [];
|
|
7913
|
+
for (const field of fields) {
|
|
7914
|
+
if (field.type === "group") {
|
|
7915
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
7916
|
+
continue;
|
|
7917
|
+
}
|
|
7918
|
+
if (field.type === "sub-tabs") {
|
|
7919
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
7920
|
+
continue;
|
|
7921
|
+
}
|
|
7922
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
7923
|
+
}
|
|
7924
|
+
return collected;
|
|
7925
|
+
}
|
|
7820
7926
|
function collectPerNodeFieldKeys(fields) {
|
|
7821
7927
|
const collected = [];
|
|
7822
7928
|
for (const field of fields) {
|
|
@@ -16166,8 +16272,172 @@ object({
|
|
|
16166
16272
|
quantity: ReadingQuantitySchema,
|
|
16167
16273
|
unit: string().min(1)
|
|
16168
16274
|
});
|
|
16169
|
-
|
|
16170
|
-
|
|
16275
|
+
new Set([
|
|
16276
|
+
{
|
|
16277
|
+
id: "person",
|
|
16278
|
+
name: "Person"
|
|
16279
|
+
},
|
|
16280
|
+
{
|
|
16281
|
+
id: "vehicle",
|
|
16282
|
+
name: "Vehicle"
|
|
16283
|
+
},
|
|
16284
|
+
{
|
|
16285
|
+
id: "animal",
|
|
16286
|
+
name: "Animal"
|
|
16287
|
+
},
|
|
16288
|
+
{
|
|
16289
|
+
id: "package",
|
|
16290
|
+
name: "Package"
|
|
16291
|
+
}
|
|
16292
|
+
].map((l) => l.id));
|
|
16293
|
+
var COCO_TO_MACRO = {
|
|
16294
|
+
mapping: {
|
|
16295
|
+
person: "person",
|
|
16296
|
+
bicycle: "vehicle",
|
|
16297
|
+
car: "vehicle",
|
|
16298
|
+
motorcycle: "vehicle",
|
|
16299
|
+
airplane: "vehicle",
|
|
16300
|
+
bus: "vehicle",
|
|
16301
|
+
train: "vehicle",
|
|
16302
|
+
truck: "vehicle",
|
|
16303
|
+
boat: "vehicle",
|
|
16304
|
+
bird: "animal",
|
|
16305
|
+
cat: "animal",
|
|
16306
|
+
dog: "animal",
|
|
16307
|
+
horse: "animal",
|
|
16308
|
+
sheep: "animal",
|
|
16309
|
+
cow: "animal",
|
|
16310
|
+
elephant: "animal",
|
|
16311
|
+
bear: "animal",
|
|
16312
|
+
zebra: "animal",
|
|
16313
|
+
giraffe: "animal",
|
|
16314
|
+
suitcase: "package",
|
|
16315
|
+
backpack: "package",
|
|
16316
|
+
handbag: "package"
|
|
16317
|
+
},
|
|
16318
|
+
preserveOriginal: false
|
|
16319
|
+
};
|
|
16320
|
+
/**
|
|
16321
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16322
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16323
|
+
*
|
|
16324
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16325
|
+
* second list. The label that fits depends on the macro class:
|
|
16326
|
+
*
|
|
16327
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16328
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16329
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16330
|
+
* - `package` → a sub-class.
|
|
16331
|
+
*
|
|
16332
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16333
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16334
|
+
*
|
|
16335
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16336
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16337
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16338
|
+
* the subject GETS it — not at the first detection.
|
|
16339
|
+
*/
|
|
16340
|
+
var SubClassLabelSchema = object({
|
|
16341
|
+
kind: literal("sub-class"),
|
|
16342
|
+
className: string().min(1)
|
|
16343
|
+
});
|
|
16344
|
+
var IdentityLabelSchema = object({
|
|
16345
|
+
kind: literal("identity"),
|
|
16346
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16347
|
+
identityId: string().min(1),
|
|
16348
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16349
|
+
name: string().min(1)
|
|
16350
|
+
});
|
|
16351
|
+
var PlateLabelSchema = object({
|
|
16352
|
+
kind: literal("plate"),
|
|
16353
|
+
text: string().min(1)
|
|
16354
|
+
});
|
|
16355
|
+
function macroOf$1(className) {
|
|
16356
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16357
|
+
}
|
|
16358
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16359
|
+
function subClassBelongs(macro, label) {
|
|
16360
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16361
|
+
}
|
|
16362
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16363
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16364
|
+
object({
|
|
16365
|
+
macro: literal("animal"),
|
|
16366
|
+
label: subClassOf("animal").optional()
|
|
16367
|
+
}),
|
|
16368
|
+
object({
|
|
16369
|
+
macro: literal("person"),
|
|
16370
|
+
label: IdentityLabelSchema.optional()
|
|
16371
|
+
}),
|
|
16372
|
+
object({
|
|
16373
|
+
macro: literal("vehicle"),
|
|
16374
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16375
|
+
}),
|
|
16376
|
+
object({
|
|
16377
|
+
macro: literal("package"),
|
|
16378
|
+
label: subClassOf("package").optional()
|
|
16379
|
+
}),
|
|
16380
|
+
object({
|
|
16381
|
+
macro: literal("other"),
|
|
16382
|
+
className: string().min(1)
|
|
16383
|
+
})
|
|
16384
|
+
]);
|
|
16385
|
+
/**
|
|
16386
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16387
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16388
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16389
|
+
*/
|
|
16390
|
+
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" });
|
|
16391
|
+
/**
|
|
16392
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16393
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16394
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16395
|
+
*/
|
|
16396
|
+
function subjectFromClassName(className) {
|
|
16397
|
+
const name = className.trim().toLowerCase();
|
|
16398
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16399
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16400
|
+
if (macro === void 0) return {
|
|
16401
|
+
macro: "other",
|
|
16402
|
+
className
|
|
16403
|
+
};
|
|
16404
|
+
const label = {
|
|
16405
|
+
kind: "sub-class",
|
|
16406
|
+
className: name
|
|
16407
|
+
};
|
|
16408
|
+
switch (macro) {
|
|
16409
|
+
case "animal": return {
|
|
16410
|
+
macro: "animal",
|
|
16411
|
+
label
|
|
16412
|
+
};
|
|
16413
|
+
case "vehicle": return {
|
|
16414
|
+
macro: "vehicle",
|
|
16415
|
+
label
|
|
16416
|
+
};
|
|
16417
|
+
case "package": return {
|
|
16418
|
+
macro: "package",
|
|
16419
|
+
label
|
|
16420
|
+
};
|
|
16421
|
+
default: return {
|
|
16422
|
+
macro: "other",
|
|
16423
|
+
className
|
|
16424
|
+
};
|
|
16425
|
+
}
|
|
16426
|
+
}
|
|
16427
|
+
function macroOnly(macro) {
|
|
16428
|
+
switch (macro) {
|
|
16429
|
+
case "animal": return { macro: "animal" };
|
|
16430
|
+
case "person": return { macro: "person" };
|
|
16431
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16432
|
+
case "package": return { macro: "package" };
|
|
16433
|
+
default: return {
|
|
16434
|
+
macro: "other",
|
|
16435
|
+
className: macro
|
|
16436
|
+
};
|
|
16437
|
+
}
|
|
16438
|
+
}
|
|
16439
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16440
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16171
16441
|
var AutomationConditionOperatorSchema = _enum([
|
|
16172
16442
|
"eq",
|
|
16173
16443
|
"ne",
|
|
@@ -16178,12 +16448,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16178
16448
|
"contains",
|
|
16179
16449
|
"in"
|
|
16180
16450
|
]);
|
|
16181
|
-
|
|
16182
|
-
|
|
16451
|
+
/**
|
|
16452
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16453
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16454
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16455
|
+
* fire; it does not lose it.
|
|
16456
|
+
*/
|
|
16457
|
+
var DeviceBecomesLeafSchema = object({
|
|
16458
|
+
kind: literal("device-becomes"),
|
|
16459
|
+
deviceId: number().int().nonnegative(),
|
|
16460
|
+
cap: string().min(1),
|
|
16461
|
+
fieldPath: string().min(1),
|
|
16462
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16463
|
+
becomes: union([
|
|
16464
|
+
string(),
|
|
16465
|
+
number(),
|
|
16466
|
+
boolean()
|
|
16467
|
+
]).optional(),
|
|
16468
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16469
|
+
edge: boolean().optional(),
|
|
16470
|
+
/** The new value must hold this long before it counts. */
|
|
16471
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
16472
|
+
/** Collapse a burst into one occurrence. */
|
|
16473
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16474
|
+
});
|
|
16475
|
+
var DeviceEventLeafSchema = object({
|
|
16476
|
+
kind: literal("device-event"),
|
|
16477
|
+
/** An `EventCategory` value. */
|
|
16478
|
+
category: string().min(1),
|
|
16479
|
+
deviceId: number().int().nonnegative().optional()
|
|
16480
|
+
});
|
|
16481
|
+
var ScheduleLeafSchema = object({
|
|
16482
|
+
kind: literal("schedule"),
|
|
16483
|
+
cron: string().min(1).max(120)
|
|
16484
|
+
});
|
|
16485
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16486
|
+
var IntervalLeafSchema = object({
|
|
16487
|
+
kind: literal("interval"),
|
|
16488
|
+
minutes: number().int().min(1).max(1440)
|
|
16489
|
+
});
|
|
16490
|
+
var DetectionLeafSchema = object({
|
|
16491
|
+
kind: literal("detection"),
|
|
16492
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16493
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16494
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16495
|
+
/** The track must have visited at least one of these zones. */
|
|
16496
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16497
|
+
/** Best-so-far confidence the track must reach. */
|
|
16498
|
+
minScore: number().min(0).max(1).optional()
|
|
16499
|
+
});
|
|
16500
|
+
var StationaryLeafSchema = object({
|
|
16501
|
+
kind: literal("stationary"),
|
|
16502
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16503
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16504
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16505
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16506
|
+
phase: _enum(["appeared", "departed"])
|
|
16507
|
+
});
|
|
16508
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16509
|
+
var DeviceIsLeafSchema = object({
|
|
16510
|
+
kind: literal("device-is"),
|
|
16183
16511
|
deviceId: number().int().nonnegative(),
|
|
16184
16512
|
cap: string().min(1),
|
|
16185
16513
|
fieldPath: string().min(1),
|
|
16186
16514
|
operator: AutomationConditionOperatorSchema,
|
|
16515
|
+
/**
|
|
16516
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16517
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16518
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16519
|
+
* one is never evaluated (it fails).
|
|
16520
|
+
*/
|
|
16187
16521
|
value: union([
|
|
16188
16522
|
string(),
|
|
16189
16523
|
number(),
|
|
@@ -16191,24 +16525,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16191
16525
|
array(union([string(), number()])),
|
|
16192
16526
|
QuantityValueSchema
|
|
16193
16527
|
]),
|
|
16528
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16194
16529
|
unit: string().min(1).optional()
|
|
16195
16530
|
});
|
|
16196
|
-
|
|
16531
|
+
/**
|
|
16532
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16533
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16534
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16535
|
+
*/
|
|
16536
|
+
var StationaryPresentLeafSchema = object({
|
|
16197
16537
|
kind: literal("stationary-present"),
|
|
16198
16538
|
deviceId: number().int().nonnegative(),
|
|
16199
|
-
|
|
16200
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16201
|
-
/** The parked object must be in at least one of these zones. */
|
|
16539
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16202
16540
|
zones: array(string().min(1)).min(1).optional()
|
|
16203
16541
|
});
|
|
16204
16542
|
/**
|
|
16205
|
-
* The
|
|
16206
|
-
*
|
|
16207
|
-
*
|
|
16208
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16209
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16543
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16544
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16545
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16210
16546
|
*/
|
|
16211
|
-
var
|
|
16547
|
+
var ExpressionLeafSchema = object({
|
|
16212
16548
|
kind: literal("expression"),
|
|
16213
16549
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16214
16550
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16220,55 +16556,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16220
16556
|
path: ["expr"]
|
|
16221
16557
|
});
|
|
16222
16558
|
});
|
|
16223
|
-
var
|
|
16559
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16224
16560
|
object({
|
|
16225
16561
|
kind: literal("all"),
|
|
16226
|
-
children: array(
|
|
16562
|
+
children: array(AutomationWhenSchema)
|
|
16227
16563
|
}),
|
|
16228
16564
|
object({
|
|
16229
16565
|
kind: literal("any"),
|
|
16230
|
-
children: array(
|
|
16566
|
+
children: array(AutomationWhenSchema)
|
|
16231
16567
|
}),
|
|
16232
16568
|
object({
|
|
16233
16569
|
kind: literal("not"),
|
|
16234
|
-
child:
|
|
16570
|
+
child: AutomationWhenSchema
|
|
16235
16571
|
}),
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16572
|
+
DeviceBecomesLeafSchema,
|
|
16573
|
+
DeviceEventLeafSchema,
|
|
16574
|
+
ScheduleLeafSchema,
|
|
16575
|
+
ManualLeafSchema,
|
|
16576
|
+
IntervalLeafSchema,
|
|
16577
|
+
DetectionLeafSchema,
|
|
16578
|
+
StationaryLeafSchema,
|
|
16579
|
+
DeviceIsLeafSchema,
|
|
16580
|
+
StationaryPresentLeafSchema,
|
|
16581
|
+
ExpressionLeafSchema
|
|
16239
16582
|
]));
|
|
16240
16583
|
/**
|
|
16241
|
-
*
|
|
16242
|
-
*
|
|
16243
|
-
*
|
|
16244
|
-
*
|
|
16245
|
-
*
|
|
16246
|
-
*
|
|
16247
|
-
*
|
|
16248
|
-
*
|
|
16249
|
-
|
|
16250
|
-
|
|
16584
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16585
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16586
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16587
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16588
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16589
|
+
* way. Nothing writes this shape any more.
|
|
16590
|
+
*
|
|
16591
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16592
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16593
|
+
* a single trigger is not wrapped in `any`.
|
|
16594
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16595
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16596
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16597
|
+
*/
|
|
16598
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16251
16599
|
object({
|
|
16252
16600
|
kind: literal("device-state"),
|
|
16253
16601
|
deviceId: number().int().nonnegative(),
|
|
16254
16602
|
cap: string().min(1),
|
|
16255
16603
|
fieldPath: string().min(1),
|
|
16256
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16257
16604
|
becomes: union([
|
|
16258
16605
|
string(),
|
|
16259
16606
|
number(),
|
|
16260
16607
|
boolean()
|
|
16261
16608
|
]).optional(),
|
|
16262
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16263
16609
|
edge: boolean().optional(),
|
|
16264
|
-
/** The condition must hold this long before the run starts. */
|
|
16265
16610
|
forMs: number().int().min(0).max(864e5).optional(),
|
|
16266
|
-
/** Collapse a burst into one run. */
|
|
16267
16611
|
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16268
16612
|
}),
|
|
16269
16613
|
object({
|
|
16270
16614
|
kind: literal("device-event"),
|
|
16271
|
-
/** An `EventCategory` value. */
|
|
16272
16615
|
category: string().min(1),
|
|
16273
16616
|
deviceId: number().int().nonnegative().optional()
|
|
16274
16617
|
}),
|
|
@@ -16279,25 +16622,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16279
16622
|
object({ kind: literal("manual") }),
|
|
16280
16623
|
object({
|
|
16281
16624
|
kind: literal("detection"),
|
|
16282
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16283
|
-
* matches every raw class that maps to it; any other name matches
|
|
16284
|
-
* exactly. */
|
|
16285
16625
|
classes: array(string().min(1)).min(1),
|
|
16286
16626
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16287
|
-
/** The track must have visited at least one of these zones. */
|
|
16288
16627
|
zones: array(string().min(1)).min(1).optional(),
|
|
16289
|
-
/** Best-so-far confidence the track must reach. */
|
|
16290
16628
|
minScore: number().min(0).max(1).optional()
|
|
16291
16629
|
}),
|
|
16292
16630
|
object({
|
|
16293
16631
|
kind: literal("stationary"),
|
|
16294
16632
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16295
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16296
16633
|
classes: array(string().min(1)).min(1).optional(),
|
|
16297
|
-
/** The parked object must be in at least one of these zones. */
|
|
16298
16634
|
zones: array(string().min(1)).min(1).optional(),
|
|
16299
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16300
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16301
16635
|
phase: _enum(["appeared", "departed"])
|
|
16302
16636
|
}),
|
|
16303
16637
|
object({
|
|
@@ -16305,6 +16639,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16305
16639
|
minutes: number().int().min(1).max(1440)
|
|
16306
16640
|
})
|
|
16307
16641
|
]);
|
|
16642
|
+
var LegacyConditionLeafSchema = object({
|
|
16643
|
+
kind: literal("condition"),
|
|
16644
|
+
deviceId: number().int().nonnegative(),
|
|
16645
|
+
cap: string().min(1),
|
|
16646
|
+
fieldPath: string().min(1),
|
|
16647
|
+
operator: AutomationConditionOperatorSchema,
|
|
16648
|
+
value: union([
|
|
16649
|
+
string(),
|
|
16650
|
+
number(),
|
|
16651
|
+
boolean(),
|
|
16652
|
+
array(union([string(), number()])),
|
|
16653
|
+
QuantityValueSchema
|
|
16654
|
+
]),
|
|
16655
|
+
unit: string().min(1).optional()
|
|
16656
|
+
});
|
|
16657
|
+
var LegacyStationaryPresentSchema = object({
|
|
16658
|
+
kind: literal("stationary-present"),
|
|
16659
|
+
deviceId: number().int().nonnegative(),
|
|
16660
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
16661
|
+
zones: array(string().min(1)).min(1).optional()
|
|
16662
|
+
});
|
|
16663
|
+
var LegacyExpressionSchema = object({
|
|
16664
|
+
kind: literal("expression"),
|
|
16665
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16666
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
16667
|
+
}).superRefine((src, ctx) => {
|
|
16668
|
+
const err = validateExpressionSource(src);
|
|
16669
|
+
if (err !== null) ctx.addIssue({
|
|
16670
|
+
code: "custom",
|
|
16671
|
+
message: err,
|
|
16672
|
+
path: ["expr"]
|
|
16673
|
+
});
|
|
16674
|
+
});
|
|
16675
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
16676
|
+
object({
|
|
16677
|
+
kind: literal("all"),
|
|
16678
|
+
children: array(LegacyConditionSchema)
|
|
16679
|
+
}),
|
|
16680
|
+
object({
|
|
16681
|
+
kind: literal("any"),
|
|
16682
|
+
children: array(LegacyConditionSchema)
|
|
16683
|
+
}),
|
|
16684
|
+
object({
|
|
16685
|
+
kind: literal("not"),
|
|
16686
|
+
child: LegacyConditionSchema
|
|
16687
|
+
}),
|
|
16688
|
+
LegacyConditionLeafSchema,
|
|
16689
|
+
LegacyExpressionSchema,
|
|
16690
|
+
LegacyStationaryPresentSchema
|
|
16691
|
+
]));
|
|
16692
|
+
function convertTrigger(trigger) {
|
|
16693
|
+
switch (trigger.kind) {
|
|
16694
|
+
case "device-state": return {
|
|
16695
|
+
...trigger,
|
|
16696
|
+
kind: "device-becomes"
|
|
16697
|
+
};
|
|
16698
|
+
case "detection": {
|
|
16699
|
+
const { classes, ...rest } = trigger;
|
|
16700
|
+
return {
|
|
16701
|
+
...rest,
|
|
16702
|
+
subjects: classes.map(subjectFromClassName)
|
|
16703
|
+
};
|
|
16704
|
+
}
|
|
16705
|
+
case "stationary": {
|
|
16706
|
+
const { classes, ...rest } = trigger;
|
|
16707
|
+
return {
|
|
16708
|
+
...rest,
|
|
16709
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16710
|
+
};
|
|
16711
|
+
}
|
|
16712
|
+
default: return trigger;
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
16716
|
+
function convertLegacyCondition(condition) {
|
|
16717
|
+
switch (condition.kind) {
|
|
16718
|
+
case "condition": return {
|
|
16719
|
+
...condition,
|
|
16720
|
+
kind: "device-is"
|
|
16721
|
+
};
|
|
16722
|
+
case "stationary-present": {
|
|
16723
|
+
const { classes, ...rest } = condition;
|
|
16724
|
+
return {
|
|
16725
|
+
...rest,
|
|
16726
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16727
|
+
};
|
|
16728
|
+
}
|
|
16729
|
+
case "expression": return condition;
|
|
16730
|
+
case "not": return {
|
|
16731
|
+
kind: "not",
|
|
16732
|
+
child: convertLegacyCondition(condition.child)
|
|
16733
|
+
};
|
|
16734
|
+
case "all":
|
|
16735
|
+
case "any": return {
|
|
16736
|
+
kind: condition.kind,
|
|
16737
|
+
children: condition.children.map(convertLegacyCondition)
|
|
16738
|
+
};
|
|
16739
|
+
}
|
|
16740
|
+
}
|
|
16741
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
16742
|
+
function convertLegacyWhen(half) {
|
|
16743
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
16744
|
+
const first = leaves[0];
|
|
16745
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
16746
|
+
kind: "any",
|
|
16747
|
+
children: leaves
|
|
16748
|
+
};
|
|
16749
|
+
if (half.conditions === void 0) return triggers;
|
|
16750
|
+
return {
|
|
16751
|
+
kind: "all",
|
|
16752
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
16753
|
+
};
|
|
16754
|
+
}
|
|
16755
|
+
/**
|
|
16756
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
16757
|
+
* becomes code.
|
|
16758
|
+
*
|
|
16759
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
16760
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
16761
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
16762
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
16763
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
16764
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
16765
|
+
*
|
|
16766
|
+
* Two parts:
|
|
16767
|
+
*
|
|
16768
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
16769
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
16770
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
16771
|
+
* ADR "one tree".
|
|
16772
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
16773
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
16774
|
+
* this had to diverge from "verbatim", and why.
|
|
16775
|
+
*
|
|
16776
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
16777
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
16778
|
+
*/
|
|
16308
16779
|
/**
|
|
16309
16780
|
* One action step.
|
|
16310
16781
|
*
|
|
@@ -16407,69 +16878,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16407
16878
|
minutes: number().int().min(1).max(1440)
|
|
16408
16879
|
})
|
|
16409
16880
|
]);
|
|
16410
|
-
var
|
|
16411
|
-
|
|
16412
|
-
conditions: AutomationConditionSchema.optional(),
|
|
16881
|
+
var CurrentAutomationRecipeSchema = object({
|
|
16882
|
+
when: AutomationWhenSchema,
|
|
16413
16883
|
actions: array(AutomationActionSchema),
|
|
16414
16884
|
runMode: AutomationRunModeSchema.optional(),
|
|
16415
16885
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16416
16886
|
keepTraces: number().int().min(1).max(100).optional()
|
|
16417
16887
|
});
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
bus: "vehicle",
|
|
16444
|
-
train: "vehicle",
|
|
16445
|
-
truck: "vehicle",
|
|
16446
|
-
boat: "vehicle",
|
|
16447
|
-
bird: "animal",
|
|
16448
|
-
cat: "animal",
|
|
16449
|
-
dog: "animal",
|
|
16450
|
-
horse: "animal",
|
|
16451
|
-
sheep: "animal",
|
|
16452
|
-
cow: "animal",
|
|
16453
|
-
elephant: "animal",
|
|
16454
|
-
bear: "animal",
|
|
16455
|
-
zebra: "animal",
|
|
16456
|
-
giraffe: "animal",
|
|
16457
|
-
suitcase: "package",
|
|
16458
|
-
backpack: "package",
|
|
16459
|
-
handbag: "package"
|
|
16460
|
-
},
|
|
16461
|
-
preserveOriginal: false
|
|
16462
|
-
};
|
|
16888
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
16889
|
+
var LegacyAutomationRecipeSchema = object({
|
|
16890
|
+
triggers: array(LegacyTriggerSchema),
|
|
16891
|
+
conditions: LegacyConditionSchema.optional(),
|
|
16892
|
+
actions: array(AutomationActionSchema),
|
|
16893
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
16894
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
16895
|
+
});
|
|
16896
|
+
/** The old shape as the new one, lossless. */
|
|
16897
|
+
function convertLegacyRecipe(legacy) {
|
|
16898
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
16899
|
+
return {
|
|
16900
|
+
when: convertLegacyWhen({
|
|
16901
|
+
triggers,
|
|
16902
|
+
conditions
|
|
16903
|
+
}),
|
|
16904
|
+
...rest
|
|
16905
|
+
};
|
|
16906
|
+
}
|
|
16907
|
+
/**
|
|
16908
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
16909
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
16910
|
+
* it. The output type is always the new one.
|
|
16911
|
+
*/
|
|
16912
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16463
16913
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16464
16914
|
/**
|
|
16465
16915
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16466
16916
|
* so the editor can paint every block green or red with its reason.
|
|
16467
16917
|
*
|
|
16468
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16469
|
-
* every
|
|
16470
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16471
|
-
* each addressed by a stable path (`
|
|
16472
|
-
* for an Action run).
|
|
16918
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
16919
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
16920
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
16921
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
16922
|
+
* for an Action run). Traces written before the single tree carry
|
|
16923
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16473
16924
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16474
16925
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16475
16926
|
*
|
|
@@ -16501,6 +16952,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16501
16952
|
]);
|
|
16502
16953
|
var AutomationTriggerTraceSchema = object({
|
|
16503
16954
|
kind: string().min(1),
|
|
16955
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
16956
|
+
path: string().min(1).optional(),
|
|
16504
16957
|
outcome: _enum(["pass", "fail"]),
|
|
16505
16958
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16506
16959
|
reason: string(),
|
|
@@ -16508,15 +16961,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16508
16961
|
vars: record(string(), TraceValueSchema)
|
|
16509
16962
|
});
|
|
16510
16963
|
var AutomationConditionTraceNodeSchema = object({
|
|
16511
|
-
/** `
|
|
16964
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16512
16965
|
path: string().min(1),
|
|
16513
16966
|
kind: _enum([
|
|
16514
16967
|
"all",
|
|
16515
16968
|
"any",
|
|
16516
16969
|
"not",
|
|
16517
16970
|
"condition",
|
|
16971
|
+
"device-is",
|
|
16518
16972
|
"expression",
|
|
16519
|
-
"stationary-present"
|
|
16973
|
+
"stationary-present",
|
|
16974
|
+
"device-becomes",
|
|
16975
|
+
"device-event",
|
|
16976
|
+
"schedule",
|
|
16977
|
+
"interval",
|
|
16978
|
+
"manual",
|
|
16979
|
+
"detection",
|
|
16980
|
+
"stationary"
|
|
16520
16981
|
]),
|
|
16521
16982
|
outcome: TraceNodeOutcomeSchema,
|
|
16522
16983
|
reason: string(),
|
|
@@ -16550,6 +17011,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16550
17011
|
deviceId: number().int().optional(),
|
|
16551
17012
|
startedAt: number().optional(),
|
|
16552
17013
|
endedAt: number().optional(),
|
|
17014
|
+
/**
|
|
17015
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
17016
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
17017
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
17018
|
+
*/
|
|
17019
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16553
17020
|
outcome: _enum([
|
|
16554
17021
|
"ok",
|
|
16555
17022
|
"error",
|
|
@@ -17299,6 +17766,23 @@ method(object({
|
|
|
17299
17766
|
addonId: string(),
|
|
17300
17767
|
nodeId: string().optional()
|
|
17301
17768
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
17769
|
+
addonId: string(),
|
|
17770
|
+
nodeId: string().optional()
|
|
17771
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
17772
|
+
addonId: string(),
|
|
17773
|
+
nodeId: string().optional(),
|
|
17774
|
+
keys: array(string())
|
|
17775
|
+
}), object({
|
|
17776
|
+
removed: array(string()),
|
|
17777
|
+
refused: array(object({
|
|
17778
|
+
key: string(),
|
|
17779
|
+
reason: string()
|
|
17780
|
+
})),
|
|
17781
|
+
secretsCleared: number()
|
|
17782
|
+
}), {
|
|
17783
|
+
kind: "mutation",
|
|
17784
|
+
auth: "admin"
|
|
17785
|
+
}), method(object({
|
|
17302
17786
|
addonId: string(),
|
|
17303
17787
|
deviceId: number(),
|
|
17304
17788
|
nodeId: string().optional()
|
|
@@ -21328,6 +21812,8 @@ var StorageKindSchema = object({
|
|
|
21328
21812
|
"orphaned",
|
|
21329
21813
|
"dropped"
|
|
21330
21814
|
]),
|
|
21815
|
+
/** An owner declared this kind since the engine started. */
|
|
21816
|
+
declaredThisBoot: boolean(),
|
|
21331
21817
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21332
21818
|
rows: number().int().nonnegative().nullable(),
|
|
21333
21819
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21339,6 +21825,51 @@ var StorageKindSchema = object({
|
|
|
21339
21825
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21340
21826
|
declaration: RecordKindDeclarationSchema
|
|
21341
21827
|
});
|
|
21828
|
+
_enum([
|
|
21829
|
+
"orphaned",
|
|
21830
|
+
"stale",
|
|
21831
|
+
"unknown"
|
|
21832
|
+
]);
|
|
21833
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
21834
|
+
var StaleKindFindingSchema = object({
|
|
21835
|
+
owner: string(),
|
|
21836
|
+
kind: string(),
|
|
21837
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
21838
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
21839
|
+
reason: string(),
|
|
21840
|
+
rows: number().int().nonnegative().nullable(),
|
|
21841
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
21842
|
+
protected: boolean()
|
|
21843
|
+
});
|
|
21844
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
21845
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
21846
|
+
addonId: string(),
|
|
21847
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
21848
|
+
key: string(),
|
|
21849
|
+
/**
|
|
21850
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
21851
|
+
* not orphans yet. Removing the key releases them.
|
|
21852
|
+
*/
|
|
21853
|
+
secretRefs: array(string())
|
|
21854
|
+
});
|
|
21855
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
21856
|
+
var StaleUnknownSchema = object({
|
|
21857
|
+
subject: string(),
|
|
21858
|
+
reason: string()
|
|
21859
|
+
});
|
|
21860
|
+
var StaleDataReportSchema = object({
|
|
21861
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
21862
|
+
scannedAt: number().nullable(),
|
|
21863
|
+
scanning: boolean(),
|
|
21864
|
+
trigger: _enum([
|
|
21865
|
+
"boot",
|
|
21866
|
+
"interval",
|
|
21867
|
+
"manual"
|
|
21868
|
+
]).nullable(),
|
|
21869
|
+
kinds: array(StaleKindFindingSchema),
|
|
21870
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
21871
|
+
unknown: array(StaleUnknownSchema)
|
|
21872
|
+
});
|
|
21342
21873
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21343
21874
|
owner: string().min(1).optional(),
|
|
21344
21875
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21346,7 +21877,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21346
21877
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21347
21878
|
owner: string().min(1),
|
|
21348
21879
|
kind: RecordKindNameSchema
|
|
21349
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
21880
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
21881
|
+
kind: "mutation",
|
|
21882
|
+
auth: "admin"
|
|
21883
|
+
}), method(object({
|
|
21884
|
+
owner: string().min(1),
|
|
21885
|
+
kind: RecordKindNameSchema
|
|
21886
|
+
}), object({
|
|
21887
|
+
deleted: number().int().nonnegative(),
|
|
21888
|
+
secretsReleased: number().int().nonnegative()
|
|
21889
|
+
}), {
|
|
21890
|
+
kind: "mutation",
|
|
21891
|
+
auth: "admin",
|
|
21892
|
+
caller: "required"
|
|
21893
|
+
}), method(object({
|
|
21894
|
+
addonId: string().min(1),
|
|
21895
|
+
key: string().min(1)
|
|
21896
|
+
}), object({
|
|
21897
|
+
removed: boolean(),
|
|
21898
|
+
secretsCleared: number().int().nonnegative()
|
|
21899
|
+
}), {
|
|
21900
|
+
kind: "mutation",
|
|
21901
|
+
auth: "admin",
|
|
21902
|
+
caller: "required"
|
|
21903
|
+
});
|
|
21350
21904
|
/**
|
|
21351
21905
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21352
21906
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21485,6 +22039,12 @@ method(object({
|
|
|
21485
22039
|
}), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
|
|
21486
22040
|
kind: "mutation",
|
|
21487
22041
|
auth: "admin"
|
|
22042
|
+
}), method(object({
|
|
22043
|
+
refs: RefListSchema,
|
|
22044
|
+
onBehalfOf: string().min(1)
|
|
22045
|
+
}), object({ released: number().int() }), {
|
|
22046
|
+
kind: "mutation",
|
|
22047
|
+
auth: "admin"
|
|
21488
22048
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21489
22049
|
/**
|
|
21490
22050
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22387,7 +22947,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22387
22947
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22388
22948
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22389
22949
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22390
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
22950
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
22951
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
22952
|
+
parentStableId: string().optional()
|
|
22391
22953
|
}));
|
|
22392
22954
|
/**
|
|
22393
22955
|
* Status block — populated by the integration after each
|
|
@@ -42770,6 +43332,79 @@ object({
|
|
|
42770
43332
|
metadata: record(string(), unknown()).optional()
|
|
42771
43333
|
});
|
|
42772
43334
|
/**
|
|
43335
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43336
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43337
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43338
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43339
|
+
* except through a `custom` mode.
|
|
43340
|
+
*/
|
|
43341
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43342
|
+
object({
|
|
43343
|
+
kind: literal("mdns"),
|
|
43344
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43345
|
+
serviceType: string().min(1),
|
|
43346
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43347
|
+
instancePattern: string().optional()
|
|
43348
|
+
}),
|
|
43349
|
+
object({
|
|
43350
|
+
kind: literal("ws-discovery"),
|
|
43351
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43352
|
+
types: array(string()).optional()
|
|
43353
|
+
}),
|
|
43354
|
+
object({
|
|
43355
|
+
kind: literal("ssdp"),
|
|
43356
|
+
searchTarget: string().min(1)
|
|
43357
|
+
}),
|
|
43358
|
+
object({
|
|
43359
|
+
kind: literal("udp-broadcast"),
|
|
43360
|
+
port: number().int().min(1).max(65535),
|
|
43361
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43362
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43363
|
+
listenPort: number().int().min(1).max(65535).optional()
|
|
43364
|
+
}),
|
|
43365
|
+
object({
|
|
43366
|
+
kind: literal("http-probe"),
|
|
43367
|
+
port: number().int().min(1).max(65535),
|
|
43368
|
+
path: string().startsWith("/"),
|
|
43369
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43370
|
+
bodyText: string().optional(),
|
|
43371
|
+
contentType: string().optional(),
|
|
43372
|
+
tls: boolean().default(false),
|
|
43373
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43374
|
+
acceptStatus: array(number().int()).optional()
|
|
43375
|
+
}),
|
|
43376
|
+
object({
|
|
43377
|
+
kind: literal("custom"),
|
|
43378
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43379
|
+
reason: string().min(1)
|
|
43380
|
+
})
|
|
43381
|
+
]);
|
|
43382
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43383
|
+
var DiscoveryHitSchema = object({
|
|
43384
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43385
|
+
modeIndex: number().int().min(0),
|
|
43386
|
+
host: string(),
|
|
43387
|
+
port: number().int(),
|
|
43388
|
+
http: object({
|
|
43389
|
+
status: number().int(),
|
|
43390
|
+
headers: record(string(), string()),
|
|
43391
|
+
/** At most 16 KiB. */
|
|
43392
|
+
bodyText: string()
|
|
43393
|
+
}).optional(),
|
|
43394
|
+
mdns: object({
|
|
43395
|
+
instance: string(),
|
|
43396
|
+
host: string(),
|
|
43397
|
+
port: number().int(),
|
|
43398
|
+
txt: record(string(), string())
|
|
43399
|
+
}).optional(),
|
|
43400
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43401
|
+
wsd: object({
|
|
43402
|
+
xaddrs: array(string()),
|
|
43403
|
+
types: array(string()),
|
|
43404
|
+
scopes: array(string())
|
|
43405
|
+
}).optional()
|
|
43406
|
+
});
|
|
43407
|
+
/**
|
|
42773
43408
|
* Candidate handed back from discovery and accepted by
|
|
42774
43409
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
42775
43410
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -42804,7 +43439,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
42804
43439
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
42805
43440
|
onboardedDeviceId: number().optional(),
|
|
42806
43441
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
42807
|
-
onboardedName: string().optional()
|
|
43442
|
+
onboardedName: string().optional(),
|
|
43443
|
+
/**
|
|
43444
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43445
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43446
|
+
* which is still read for one release.
|
|
43447
|
+
*/
|
|
43448
|
+
setupIntegration: boolean().optional(),
|
|
43449
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43450
|
+
parentStableId: string().optional(),
|
|
43451
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43452
|
+
selectable: boolean().optional()
|
|
42808
43453
|
});
|
|
42809
43454
|
/**
|
|
42810
43455
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -42901,6 +43546,24 @@ var deviceProviderCapability = {
|
|
|
42901
43546
|
auth: "admin"
|
|
42902
43547
|
}),
|
|
42903
43548
|
/**
|
|
43549
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43550
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43551
|
+
*/
|
|
43552
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43553
|
+
/**
|
|
43554
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43555
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43556
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43557
|
+
* from `getDiscoveryParamsSchema`.
|
|
43558
|
+
*/
|
|
43559
|
+
classifyDiscoveryHits: method(object({
|
|
43560
|
+
hits: array(DiscoveryHitSchema),
|
|
43561
|
+
params: record(string(), unknown()).optional()
|
|
43562
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43563
|
+
kind: "mutation",
|
|
43564
|
+
auth: "admin"
|
|
43565
|
+
}),
|
|
43566
|
+
/**
|
|
42904
43567
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
42905
43568
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
42906
43569
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -43665,17 +44328,23 @@ method(object({
|
|
|
43665
44328
|
}), method(object({}), object({ providers: array(object({
|
|
43666
44329
|
addonId: string(),
|
|
43667
44330
|
label: string()
|
|
43668
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
43669
|
-
|
|
43670
|
-
|
|
43671
|
-
|
|
43672
|
-
|
|
43673
|
-
|
|
43674
|
-
|
|
43675
|
-
|
|
43676
|
-
}),
|
|
44331
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44332
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44333
|
+
object({ subnets: array(string()).optional() }),
|
|
44334
|
+
object({ groups: array(object({
|
|
44335
|
+
addonId: string(),
|
|
44336
|
+
label: string(),
|
|
44337
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44338
|
+
error: string().nullable()
|
|
44339
|
+
})).readonly() }),
|
|
44340
|
+
{
|
|
44341
|
+
kind: "mutation",
|
|
44342
|
+
auth: "admin"
|
|
44343
|
+
}
|
|
44344
|
+
), method(object({
|
|
43677
44345
|
addonId: string(),
|
|
43678
|
-
params: record(string(), unknown()).optional()
|
|
44346
|
+
params: record(string(), unknown()).optional(),
|
|
44347
|
+
subnets: array(string()).optional()
|
|
43679
44348
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
43680
44349
|
kind: "mutation",
|
|
43681
44350
|
auth: "admin"
|
|
@@ -63017,7 +63686,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63017
63686
|
id: string(),
|
|
63018
63687
|
settings: record(string(), unknown()),
|
|
63019
63688
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63020
|
-
secretKeys: array(string()).optional()
|
|
63689
|
+
secretKeys: array(string()).optional(),
|
|
63690
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
63691
|
+
skipRestart: boolean().optional()
|
|
63021
63692
|
}), object({ success: boolean() }), {
|
|
63022
63693
|
kind: "mutation",
|
|
63023
63694
|
auth: "admin"
|
|
@@ -63645,6 +64316,39 @@ var TlsStatusSchema = object({
|
|
|
63645
64316
|
hasCa: boolean()
|
|
63646
64317
|
}).nullable()
|
|
63647
64318
|
});
|
|
64319
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64320
|
+
var ScanRequestSchema = object({
|
|
64321
|
+
providerId: string().min(1),
|
|
64322
|
+
modes: array(DiscoveryModeSchema)
|
|
64323
|
+
});
|
|
64324
|
+
var ScanErrorSchema = object({
|
|
64325
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64326
|
+
providerId: string().optional(),
|
|
64327
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64328
|
+
mode: string().optional(),
|
|
64329
|
+
message: string()
|
|
64330
|
+
});
|
|
64331
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64332
|
+
var ScanStatsSchema = object({
|
|
64333
|
+
hostsProbed: number().int(),
|
|
64334
|
+
hits: number().int(),
|
|
64335
|
+
timeouts: number().int(),
|
|
64336
|
+
byMode: record(string(), object({
|
|
64337
|
+
probed: number().int(),
|
|
64338
|
+
hits: number().int(),
|
|
64339
|
+
timeouts: number().int()
|
|
64340
|
+
}))
|
|
64341
|
+
});
|
|
64342
|
+
var ScanNetworkResultSchema = object({
|
|
64343
|
+
hits: array(object({
|
|
64344
|
+
providerId: string(),
|
|
64345
|
+
hit: DiscoveryHitSchema
|
|
64346
|
+
})),
|
|
64347
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64348
|
+
subnetsScanned: array(string()),
|
|
64349
|
+
errors: array(ScanErrorSchema),
|
|
64350
|
+
stats: ScanStatsSchema
|
|
64351
|
+
});
|
|
63648
64352
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
63649
64353
|
/**
|
|
63650
64354
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -63699,6 +64403,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
63699
64403
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
63700
64404
|
kind: "mutation",
|
|
63701
64405
|
auth: "admin"
|
|
64406
|
+
}), method(object({
|
|
64407
|
+
requests: array(ScanRequestSchema),
|
|
64408
|
+
subnets: array(string()).optional()
|
|
64409
|
+
}), ScanNetworkResultSchema, {
|
|
64410
|
+
kind: "mutation",
|
|
64411
|
+
auth: "admin",
|
|
64412
|
+
timeoutMs: 9e4
|
|
64413
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64414
|
+
kind: "mutation",
|
|
64415
|
+
auth: "admin"
|
|
63702
64416
|
}), method(_void(), TlsStatusSchema, {
|
|
63703
64417
|
kind: "mutation",
|
|
63704
64418
|
auth: "admin"
|
|
@@ -73281,6 +73995,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73281
73995
|
async discoverDevices(_input) {
|
|
73282
73996
|
return [];
|
|
73283
73997
|
}
|
|
73998
|
+
/**
|
|
73999
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74000
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74001
|
+
*/
|
|
74002
|
+
async getDiscoveryModes() {
|
|
74003
|
+
return [];
|
|
74004
|
+
}
|
|
74005
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74006
|
+
async classifyDiscoveryHits(_input) {
|
|
74007
|
+
return [];
|
|
74008
|
+
}
|
|
73284
74009
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73285
74010
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73286
74011
|
async getDiscoveryParamsSchema() {
|
|
@@ -74149,6 +74874,13 @@ Object.freeze({
|
|
|
74149
74874
|
access: "create",
|
|
74150
74875
|
role: "admin"
|
|
74151
74876
|
},
|
|
74877
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
74878
|
+
capName: "addon-settings",
|
|
74879
|
+
capScope: "system",
|
|
74880
|
+
addonId: null,
|
|
74881
|
+
access: "view",
|
|
74882
|
+
role: "admin"
|
|
74883
|
+
},
|
|
74152
74884
|
"addonSettings.getDeviceSettings": {
|
|
74153
74885
|
capName: "addon-settings",
|
|
74154
74886
|
capScope: "system",
|
|
@@ -74170,6 +74902,13 @@ Object.freeze({
|
|
|
74170
74902
|
access: "view",
|
|
74171
74903
|
role: "admin"
|
|
74172
74904
|
},
|
|
74905
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
74906
|
+
capName: "addon-settings",
|
|
74907
|
+
capScope: "system",
|
|
74908
|
+
addonId: null,
|
|
74909
|
+
access: "create",
|
|
74910
|
+
role: "admin"
|
|
74911
|
+
},
|
|
74173
74912
|
"addonSettings.updateDeviceSettings": {
|
|
74174
74913
|
capName: "addon-settings",
|
|
74175
74914
|
capScope: "system",
|
|
@@ -76585,6 +77324,13 @@ Object.freeze({
|
|
|
76585
77324
|
access: "create",
|
|
76586
77325
|
role: "admin"
|
|
76587
77326
|
},
|
|
77327
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77328
|
+
capName: "device-provider",
|
|
77329
|
+
capScope: "system",
|
|
77330
|
+
addonId: null,
|
|
77331
|
+
access: "create",
|
|
77332
|
+
role: "admin"
|
|
77333
|
+
},
|
|
76588
77334
|
"deviceProvider.createDevice": {
|
|
76589
77335
|
capName: "device-provider",
|
|
76590
77336
|
capScope: "system",
|
|
@@ -76613,6 +77359,13 @@ Object.freeze({
|
|
|
76613
77359
|
access: "view",
|
|
76614
77360
|
role: "admin"
|
|
76615
77361
|
},
|
|
77362
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77363
|
+
capName: "device-provider",
|
|
77364
|
+
capScope: "system",
|
|
77365
|
+
addonId: null,
|
|
77366
|
+
access: "view",
|
|
77367
|
+
role: "admin"
|
|
77368
|
+
},
|
|
76616
77369
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76617
77370
|
capName: "device-provider",
|
|
76618
77371
|
capScope: "system",
|
|
@@ -77390,6 +78143,13 @@ Object.freeze({
|
|
|
77390
78143
|
access: "create",
|
|
77391
78144
|
role: "admin"
|
|
77392
78145
|
},
|
|
78146
|
+
"keychain.releaseRecordRefs": {
|
|
78147
|
+
capName: "keychain",
|
|
78148
|
+
capScope: "system",
|
|
78149
|
+
addonId: null,
|
|
78150
|
+
access: "create",
|
|
78151
|
+
role: "admin"
|
|
78152
|
+
},
|
|
77393
78153
|
"keychain.resolve": {
|
|
77394
78154
|
capName: "keychain",
|
|
77395
78155
|
capScope: "system",
|
|
@@ -77810,6 +78570,13 @@ Object.freeze({
|
|
|
77810
78570
|
access: "view",
|
|
77811
78571
|
role: "admin"
|
|
77812
78572
|
},
|
|
78573
|
+
"localNetwork.getScanSubnets": {
|
|
78574
|
+
capName: "local-network",
|
|
78575
|
+
capScope: "system",
|
|
78576
|
+
addonId: null,
|
|
78577
|
+
access: "view",
|
|
78578
|
+
role: "admin"
|
|
78579
|
+
},
|
|
77813
78580
|
"localNetwork.getTlsStatus": {
|
|
77814
78581
|
capName: "local-network",
|
|
77815
78582
|
capScope: "system",
|
|
@@ -77859,6 +78626,13 @@ Object.freeze({
|
|
|
77859
78626
|
access: "create",
|
|
77860
78627
|
role: "admin"
|
|
77861
78628
|
},
|
|
78629
|
+
"localNetwork.scanNetwork": {
|
|
78630
|
+
capName: "local-network",
|
|
78631
|
+
capScope: "system",
|
|
78632
|
+
addonId: null,
|
|
78633
|
+
access: "create",
|
|
78634
|
+
role: "admin"
|
|
78635
|
+
},
|
|
77862
78636
|
"localNetwork.setAddressRole": {
|
|
77863
78637
|
capName: "local-network",
|
|
77864
78638
|
capScope: "system",
|
|
@@ -77880,6 +78654,13 @@ Object.freeze({
|
|
|
77880
78654
|
access: "create",
|
|
77881
78655
|
role: "admin"
|
|
77882
78656
|
},
|
|
78657
|
+
"localNetwork.setScanSubnets": {
|
|
78658
|
+
capName: "local-network",
|
|
78659
|
+
capScope: "system",
|
|
78660
|
+
addonId: null,
|
|
78661
|
+
access: "create",
|
|
78662
|
+
role: "admin"
|
|
78663
|
+
},
|
|
77883
78664
|
"localNetwork.setViewerEndpoints": {
|
|
77884
78665
|
capName: "local-network",
|
|
77885
78666
|
capScope: "system",
|
|
@@ -82101,6 +82882,13 @@ Object.freeze({
|
|
|
82101
82882
|
access: "create",
|
|
82102
82883
|
role: "admin"
|
|
82103
82884
|
},
|
|
82885
|
+
"storageAdmin.dropOrphanedKind": {
|
|
82886
|
+
capName: "storage-admin",
|
|
82887
|
+
capScope: "system",
|
|
82888
|
+
addonId: null,
|
|
82889
|
+
access: "delete",
|
|
82890
|
+
role: "admin"
|
|
82891
|
+
},
|
|
82104
82892
|
"storageAdmin.listKinds": {
|
|
82105
82893
|
capName: "storage-admin",
|
|
82106
82894
|
capScope: "system",
|
|
@@ -82122,6 +82910,27 @@ Object.freeze({
|
|
|
82122
82910
|
access: "view",
|
|
82123
82911
|
role: "admin"
|
|
82124
82912
|
},
|
|
82913
|
+
"storageAdmin.scanStaleData": {
|
|
82914
|
+
capName: "storage-admin",
|
|
82915
|
+
capScope: "system",
|
|
82916
|
+
addonId: null,
|
|
82917
|
+
access: "create",
|
|
82918
|
+
role: "admin"
|
|
82919
|
+
},
|
|
82920
|
+
"storageAdmin.staleDataReport": {
|
|
82921
|
+
capName: "storage-admin",
|
|
82922
|
+
capScope: "system",
|
|
82923
|
+
addonId: null,
|
|
82924
|
+
access: "view",
|
|
82925
|
+
role: "admin"
|
|
82926
|
+
},
|
|
82927
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
82928
|
+
capName: "storage-admin",
|
|
82929
|
+
capScope: "system",
|
|
82930
|
+
addonId: null,
|
|
82931
|
+
access: "create",
|
|
82932
|
+
role: "admin"
|
|
82933
|
+
},
|
|
82125
82934
|
"storageEvictable.evict": {
|
|
82126
82935
|
capName: "storage-evictable",
|
|
82127
82936
|
capScope: "system",
|
|
@@ -95073,36 +95882,22 @@ function buildHmAdoptionProvider(deps) {
|
|
|
95073
95882
|
}
|
|
95074
95883
|
};
|
|
95075
95884
|
}
|
|
95076
|
-
|
|
95077
|
-
|
|
95078
|
-
|
|
95079
|
-
|
|
95080
|
-
|
|
95081
|
-
|
|
95082
|
-
|
|
95083
|
-
|
|
95084
|
-
|
|
95085
|
-
|
|
95086
|
-
|
|
95087
|
-
|
|
95088
|
-
|
|
95089
|
-
|
|
95090
|
-
|
|
95091
|
-
|
|
95092
|
-
return {
|
|
95093
|
-
status: response.status,
|
|
95094
|
-
text: await response.text()
|
|
95095
|
-
};
|
|
95096
|
-
};
|
|
95097
|
-
function osAddresses() {
|
|
95098
|
-
const out = [];
|
|
95099
|
-
for (const list of Object.values(networkInterfaces())) for (const a of list ?? []) out.push({
|
|
95100
|
-
family: String(a.family),
|
|
95101
|
-
address: a.address,
|
|
95102
|
-
internal: a.internal
|
|
95103
|
-
});
|
|
95104
|
-
return out;
|
|
95105
|
-
}
|
|
95885
|
+
/** The question the WebUI asks before it shows a login; the engine sends it to every host once. */
|
|
95886
|
+
var HOMEMATIC_DISCOVERY_MODES = [{
|
|
95887
|
+
kind: "http-probe",
|
|
95888
|
+
port: 80,
|
|
95889
|
+
path: "/api/homematic.cgi",
|
|
95890
|
+
method: "POST",
|
|
95891
|
+
bodyText: JSON.stringify({
|
|
95892
|
+
method: "CCU.getAuthEnabled",
|
|
95893
|
+
params: {},
|
|
95894
|
+
jsonrpc: "1.1",
|
|
95895
|
+
id: 0
|
|
95896
|
+
}),
|
|
95897
|
+
contentType: "application/json",
|
|
95898
|
+
tls: false,
|
|
95899
|
+
acceptStatus: [200]
|
|
95900
|
+
}];
|
|
95106
95901
|
/**
|
|
95107
95902
|
* Read the answer of `CCU.getAuthEnabled`, the question the WebUI asks before
|
|
95108
95903
|
* it shows a login. A CCU answers a boolean; one that already requires a login
|
|
@@ -95129,63 +95924,26 @@ function parseAuthEnabledAnswer(status, text) {
|
|
|
95129
95924
|
if (result === false || result === "false") return { authEnabled: false };
|
|
95130
95925
|
return null;
|
|
95131
95926
|
}
|
|
95132
|
-
/**
|
|
95133
|
-
* The hosts to ask: every address of the /24 of each external IPv4 address of
|
|
95134
|
-
* this machine, minus the network and broadcast addresses and this machine itself.
|
|
95135
|
-
*/
|
|
95136
|
-
function lanHostsToProbe(addresses) {
|
|
95137
|
-
const own = /* @__PURE__ */ new Set();
|
|
95138
|
-
const subnets = [];
|
|
95139
|
-
for (const a of addresses) {
|
|
95140
|
-
if (a.internal || a.family !== "IPv4" && a.family !== "4") continue;
|
|
95141
|
-
own.add(a.address);
|
|
95142
|
-
const prefix = a.address.split(".").slice(0, 3).join(".");
|
|
95143
|
-
if (prefix.split(".").length === 3 && !subnets.includes(prefix)) subnets.push(prefix);
|
|
95144
|
-
}
|
|
95145
|
-
const hosts = [];
|
|
95146
|
-
for (const prefix of subnets) for (let last = 1; last <= 254; last++) {
|
|
95147
|
-
const host = `${prefix}.${last}`;
|
|
95148
|
-
if (!own.has(host)) hosts.push(host);
|
|
95149
|
-
}
|
|
95150
|
-
return hosts.slice(0, MAX_HOSTS);
|
|
95151
|
-
}
|
|
95152
95927
|
/** Slugify a host into a readable, stable id fragment. */
|
|
95153
95928
|
function slugifyHost(value) {
|
|
95154
95929
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
95155
95930
|
}
|
|
95156
|
-
/**
|
|
95157
|
-
|
|
95158
|
-
* asked `CCU.getAuthEnabled` on the JSON-RPC endpoint, with a short timeout and
|
|
95159
|
-
* a bounded number of probes in flight.
|
|
95160
|
-
*/
|
|
95161
|
-
async function browseHomematicCcus(options = {}) {
|
|
95162
|
-
const { addresses = osAddresses, post = fetchPost, concurrency = 64, timeoutMs = HOMEMATIC_DISCOVERY_TIMEOUT_MS } = options;
|
|
95163
|
-
const hosts = lanHostsToProbe(addresses());
|
|
95164
|
-
const body = JSON.stringify({
|
|
95165
|
-
method: "CCU.getAuthEnabled",
|
|
95166
|
-
params: {},
|
|
95167
|
-
jsonrpc: "1.1",
|
|
95168
|
-
id: 0
|
|
95169
|
-
});
|
|
95931
|
+
/** CCUs among the engine's http-probe hits: the hosts whose answer is the JSON-RPC one. */
|
|
95932
|
+
function ccusFromHits(hits) {
|
|
95170
95933
|
const found = [];
|
|
95171
|
-
|
|
95172
|
-
const
|
|
95173
|
-
|
|
95174
|
-
|
|
95175
|
-
|
|
95176
|
-
|
|
95177
|
-
|
|
95178
|
-
|
|
95179
|
-
|
|
95180
|
-
|
|
95181
|
-
|
|
95182
|
-
|
|
95183
|
-
|
|
95184
|
-
});
|
|
95185
|
-
} catch {}
|
|
95186
|
-
}
|
|
95187
|
-
};
|
|
95188
|
-
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker()));
|
|
95934
|
+
const seen = /* @__PURE__ */ new Set();
|
|
95935
|
+
for (const hit of hits) {
|
|
95936
|
+
if (hit.http === void 0 || seen.has(hit.host)) continue;
|
|
95937
|
+
const answer = parseAuthEnabledAnswer(hit.http.status, hit.http.bodyText);
|
|
95938
|
+
if (answer === null) continue;
|
|
95939
|
+
seen.add(hit.host);
|
|
95940
|
+
found.push({
|
|
95941
|
+
host: hit.host,
|
|
95942
|
+
name: `Homematic CCU (${hit.host})`,
|
|
95943
|
+
id: slugifyHost(hit.host),
|
|
95944
|
+
authEnabled: answer.authEnabled
|
|
95945
|
+
});
|
|
95946
|
+
}
|
|
95189
95947
|
return found.sort((a, b) => a.host.localeCompare(b.host, void 0, { numeric: true }));
|
|
95190
95948
|
}
|
|
95191
95949
|
/** CCUs whose host is already stored on an integration are not proposed again. */
|
|
@@ -95196,11 +95954,10 @@ function proposeHomematicCcus(found, existingHosts) {
|
|
|
95196
95954
|
function homematicDiscoveryCandidates(proposals) {
|
|
95197
95955
|
return proposals.map((found) => ({
|
|
95198
95956
|
stableId: `homematic:${found.id}`,
|
|
95957
|
+
type: DeviceType.Gateway,
|
|
95199
95958
|
suggestedName: found.name,
|
|
95200
|
-
prefilledConfig: {
|
|
95201
|
-
|
|
95202
|
-
[HOMEMATIC_SETUP_INTEGRATION_KEY]: true
|
|
95203
|
-
}
|
|
95959
|
+
prefilledConfig: { host: found.host },
|
|
95960
|
+
setupIntegration: true
|
|
95204
95961
|
}));
|
|
95205
95962
|
}
|
|
95206
95963
|
function homematicDiscoverySummary(foundCount, proposedCount) {
|
|
@@ -118492,14 +119249,15 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118492
119249
|
],
|
|
118493
119250
|
customActions: homematicActions,
|
|
118494
119251
|
actionHandlers: { discoverCcu: async () => {
|
|
118495
|
-
const {
|
|
119252
|
+
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
119253
|
+
const hosts = candidates.map((candidate) => ({
|
|
119254
|
+
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
119255
|
+
name: candidate.suggestedName,
|
|
119256
|
+
id: candidate.stableId.replace(/^homematic:/, "")
|
|
119257
|
+
}));
|
|
118496
119258
|
return {
|
|
118497
|
-
hosts
|
|
118498
|
-
|
|
118499
|
-
name: found.name,
|
|
118500
|
-
id: found.id
|
|
118501
|
-
})),
|
|
118502
|
-
summary
|
|
119259
|
+
hosts,
|
|
119260
|
+
summary: homematicDiscoverySummary(hosts.length, hosts.length)
|
|
118503
119261
|
};
|
|
118504
119262
|
} }
|
|
118505
119263
|
};
|
|
@@ -118534,18 +119292,21 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118534
119292
|
async supportsManualCreation() {
|
|
118535
119293
|
return false;
|
|
118536
119294
|
}
|
|
118537
|
-
/**
|
|
119295
|
+
/** A CCU is found by the scan engine's http-probe; this addon classifies the hits. */
|
|
118538
119296
|
async supportsDiscovery() {
|
|
118539
119297
|
return true;
|
|
118540
119298
|
}
|
|
118541
|
-
async
|
|
118542
|
-
|
|
118543
|
-
|
|
118544
|
-
|
|
118545
|
-
|
|
118546
|
-
|
|
118547
|
-
|
|
118548
|
-
|
|
119299
|
+
async getDiscoveryModes() {
|
|
119300
|
+
return HOMEMATIC_DISCOVERY_MODES;
|
|
119301
|
+
}
|
|
119302
|
+
async classifyDiscoveryHits(input) {
|
|
119303
|
+
const found = ccusFromHits(input.hits);
|
|
119304
|
+
const proposals = proposeHomematicCcus(found, await this.knownCcuHosts());
|
|
119305
|
+
this.ctx.logger.info("Homematic CCU discovery finished", { meta: {
|
|
119306
|
+
found: found.length,
|
|
119307
|
+
proposed: proposals.length
|
|
119308
|
+
} });
|
|
119309
|
+
return homematicDiscoveryCandidates(proposals);
|
|
118549
119310
|
}
|
|
118550
119311
|
async adoptDiscoveredDevice(_input) {
|
|
118551
119312
|
throw new Error("A Homematic CCU is added from the integration form");
|
|
@@ -118556,23 +119317,6 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118556
119317
|
async onCreateDevice(_type, _config) {
|
|
118557
119318
|
throw new Error("Homematic devices are adopted from the integration, not created manually");
|
|
118558
119319
|
}
|
|
118559
|
-
async discoverCcus() {
|
|
118560
|
-
let found = [];
|
|
118561
|
-
try {
|
|
118562
|
-
found = await browseHomematicCcus();
|
|
118563
|
-
} catch (err) {
|
|
118564
|
-
this.ctx.logger.warn("Homematic CCU discovery failed", { meta: { error: errMsg(err) } });
|
|
118565
|
-
}
|
|
118566
|
-
const proposals = proposeHomematicCcus(found, await this.knownCcuHosts());
|
|
118567
|
-
this.ctx.logger.info("Homematic CCU discovery finished", { meta: {
|
|
118568
|
-
found: found.length,
|
|
118569
|
-
proposed: proposals.length
|
|
118570
|
-
} });
|
|
118571
|
-
return {
|
|
118572
|
-
proposals,
|
|
118573
|
-
summary: homematicDiscoverySummary(found.length, proposals.length)
|
|
118574
|
-
};
|
|
118575
|
-
}
|
|
118576
119320
|
async knownCcuHosts() {
|
|
118577
119321
|
try {
|
|
118578
119322
|
const all = await this.ctx.api.integrations.list.query();
|