@camstack/addon-provider-velux 0.2.150 → 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 +6 -0
- package/dist/addon.js +971 -352
- package/dist/addon.mjs +971 -352
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { connect } from "node:tls";
|
|
3
3
|
import { setImmediate as setImmediate$1 } from "timers/promises";
|
|
4
|
-
import { createSocket } from "node:dgram";
|
|
5
4
|
//#region \0rolldown/runtime.js
|
|
6
5
|
var __create = Object.create;
|
|
7
6
|
var __defProp = Object.defineProperty;
|
|
@@ -1387,7 +1386,7 @@ var _emoji$1 = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`;
|
|
|
1387
1386
|
function emoji() {
|
|
1388
1387
|
return new RegExp(_emoji$1, "u");
|
|
1389
1388
|
}
|
|
1390
|
-
var ipv4
|
|
1389
|
+
var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
|
|
1391
1390
|
var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
|
1392
1391
|
var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
1393
1392
|
var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
|
|
@@ -2062,7 +2061,7 @@ var $ZodISODuration = /*@__PURE__*/ $constructor("$ZodISODuration", (inst, def)
|
|
|
2062
2061
|
$ZodStringFormat.init(inst, def);
|
|
2063
2062
|
});
|
|
2064
2063
|
var $ZodIPv4 = /*@__PURE__*/ $constructor("$ZodIPv4", (inst, def) => {
|
|
2065
|
-
def.pattern ?? (def.pattern = ipv4
|
|
2064
|
+
def.pattern ?? (def.pattern = ipv4);
|
|
2066
2065
|
$ZodStringFormat.init(inst, def);
|
|
2067
2066
|
inst._zod.bag.format = `ipv4`;
|
|
2068
2067
|
});
|
|
@@ -5491,7 +5490,7 @@ var ZodIssueCode = {
|
|
|
5491
5490
|
var ZodFirstPartyTypeKind;
|
|
5492
5491
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5493
5492
|
//#endregion
|
|
5494
|
-
//#region ../types/dist/sleep-
|
|
5493
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5495
5494
|
/**
|
|
5496
5495
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5497
5496
|
* window to float samples (D455).
|
|
@@ -7558,6 +7557,7 @@ var BaseAddon = class {
|
|
|
7558
7557
|
* a warning and falls back rather than crashing boot.
|
|
7559
7558
|
*/
|
|
7560
7559
|
state(key, schema, fallback) {
|
|
7560
|
+
this._declaredStateKeys.add(key);
|
|
7561
7561
|
return createDurableState({
|
|
7562
7562
|
key,
|
|
7563
7563
|
schema,
|
|
@@ -7573,6 +7573,96 @@ var BaseAddon = class {
|
|
|
7573
7573
|
} })
|
|
7574
7574
|
});
|
|
7575
7575
|
}
|
|
7576
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7577
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7578
|
+
/**
|
|
7579
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7580
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7581
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7582
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7583
|
+
*/
|
|
7584
|
+
async getDeclaredSettingsKeys() {
|
|
7585
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7586
|
+
}
|
|
7587
|
+
declaredKeySet() {
|
|
7588
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7589
|
+
const schema = this.globalSettingsSchema();
|
|
7590
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7591
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7592
|
+
for (const path of this.secretStatePaths) {
|
|
7593
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7594
|
+
if (root) keys.add(root);
|
|
7595
|
+
}
|
|
7596
|
+
return keys;
|
|
7597
|
+
}
|
|
7598
|
+
/**
|
|
7599
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7600
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7601
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7602
|
+
*/
|
|
7603
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7604
|
+
const settings = this._ctx?.settings;
|
|
7605
|
+
const raw = this.rawSettings();
|
|
7606
|
+
if (!settings || !raw) return {
|
|
7607
|
+
removed: [],
|
|
7608
|
+
refused: keys.map((key) => ({
|
|
7609
|
+
key,
|
|
7610
|
+
reason: "no settings store"
|
|
7611
|
+
})),
|
|
7612
|
+
secretsCleared: 0
|
|
7613
|
+
};
|
|
7614
|
+
let stored;
|
|
7615
|
+
try {
|
|
7616
|
+
stored = await raw.readAddonStore();
|
|
7617
|
+
} catch (error) {
|
|
7618
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7619
|
+
return {
|
|
7620
|
+
removed: [],
|
|
7621
|
+
refused: keys.map((key) => ({
|
|
7622
|
+
key,
|
|
7623
|
+
reason
|
|
7624
|
+
})),
|
|
7625
|
+
secretsCleared: 0
|
|
7626
|
+
};
|
|
7627
|
+
}
|
|
7628
|
+
const declared = this.declaredKeySet();
|
|
7629
|
+
const removed = [];
|
|
7630
|
+
const refused = [];
|
|
7631
|
+
for (const key of keys) {
|
|
7632
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7633
|
+
if (declared.has(base)) refused.push({
|
|
7634
|
+
key,
|
|
7635
|
+
reason: "still declared by the addon"
|
|
7636
|
+
});
|
|
7637
|
+
else if (!(key in stored)) refused.push({
|
|
7638
|
+
key,
|
|
7639
|
+
reason: "not stored"
|
|
7640
|
+
});
|
|
7641
|
+
else removed.push(key);
|
|
7642
|
+
}
|
|
7643
|
+
if (removed.length === 0) return {
|
|
7644
|
+
removed,
|
|
7645
|
+
refused,
|
|
7646
|
+
secretsCleared: 0
|
|
7647
|
+
};
|
|
7648
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7649
|
+
const held = /* @__PURE__ */ new Set();
|
|
7650
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7651
|
+
const target = removed.includes(key) ? held : kept;
|
|
7652
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7653
|
+
}
|
|
7654
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7655
|
+
const patch = {};
|
|
7656
|
+
for (const key of removed) patch[key] = void 0;
|
|
7657
|
+
await settings.writeAddonStore(patch);
|
|
7658
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7659
|
+
await this.resolveConfig();
|
|
7660
|
+
return {
|
|
7661
|
+
removed,
|
|
7662
|
+
refused,
|
|
7663
|
+
secretsCleared
|
|
7664
|
+
};
|
|
7665
|
+
}
|
|
7576
7666
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7577
7667
|
deviceState(deviceId, key, schema, fallback) {
|
|
7578
7668
|
return createDurableState({
|
|
@@ -7843,6 +7933,21 @@ function dropPerNodeFields(fields) {
|
|
|
7843
7933
|
}
|
|
7844
7934
|
return kept;
|
|
7845
7935
|
}
|
|
7936
|
+
function collectFieldKeys(fields) {
|
|
7937
|
+
const collected = [];
|
|
7938
|
+
for (const field of fields) {
|
|
7939
|
+
if (field.type === "group") {
|
|
7940
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
7941
|
+
continue;
|
|
7942
|
+
}
|
|
7943
|
+
if (field.type === "sub-tabs") {
|
|
7944
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
7945
|
+
continue;
|
|
7946
|
+
}
|
|
7947
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
7948
|
+
}
|
|
7949
|
+
return collected;
|
|
7950
|
+
}
|
|
7846
7951
|
function collectPerNodeFieldKeys(fields) {
|
|
7847
7952
|
const collected = [];
|
|
7848
7953
|
for (const field of fields) {
|
|
@@ -16114,8 +16219,172 @@ object({
|
|
|
16114
16219
|
quantity: ReadingQuantitySchema,
|
|
16115
16220
|
unit: string().min(1)
|
|
16116
16221
|
});
|
|
16117
|
-
|
|
16118
|
-
|
|
16222
|
+
new Set([
|
|
16223
|
+
{
|
|
16224
|
+
id: "person",
|
|
16225
|
+
name: "Person"
|
|
16226
|
+
},
|
|
16227
|
+
{
|
|
16228
|
+
id: "vehicle",
|
|
16229
|
+
name: "Vehicle"
|
|
16230
|
+
},
|
|
16231
|
+
{
|
|
16232
|
+
id: "animal",
|
|
16233
|
+
name: "Animal"
|
|
16234
|
+
},
|
|
16235
|
+
{
|
|
16236
|
+
id: "package",
|
|
16237
|
+
name: "Package"
|
|
16238
|
+
}
|
|
16239
|
+
].map((l) => l.id));
|
|
16240
|
+
var COCO_TO_MACRO = {
|
|
16241
|
+
mapping: {
|
|
16242
|
+
person: "person",
|
|
16243
|
+
bicycle: "vehicle",
|
|
16244
|
+
car: "vehicle",
|
|
16245
|
+
motorcycle: "vehicle",
|
|
16246
|
+
airplane: "vehicle",
|
|
16247
|
+
bus: "vehicle",
|
|
16248
|
+
train: "vehicle",
|
|
16249
|
+
truck: "vehicle",
|
|
16250
|
+
boat: "vehicle",
|
|
16251
|
+
bird: "animal",
|
|
16252
|
+
cat: "animal",
|
|
16253
|
+
dog: "animal",
|
|
16254
|
+
horse: "animal",
|
|
16255
|
+
sheep: "animal",
|
|
16256
|
+
cow: "animal",
|
|
16257
|
+
elephant: "animal",
|
|
16258
|
+
bear: "animal",
|
|
16259
|
+
zebra: "animal",
|
|
16260
|
+
giraffe: "animal",
|
|
16261
|
+
suitcase: "package",
|
|
16262
|
+
backpack: "package",
|
|
16263
|
+
handbag: "package"
|
|
16264
|
+
},
|
|
16265
|
+
preserveOriginal: false
|
|
16266
|
+
};
|
|
16267
|
+
/**
|
|
16268
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16269
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16270
|
+
*
|
|
16271
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16272
|
+
* second list. The label that fits depends on the macro class:
|
|
16273
|
+
*
|
|
16274
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16275
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16276
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16277
|
+
* - `package` → a sub-class.
|
|
16278
|
+
*
|
|
16279
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16280
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16281
|
+
*
|
|
16282
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16283
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16284
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16285
|
+
* the subject GETS it — not at the first detection.
|
|
16286
|
+
*/
|
|
16287
|
+
var SubClassLabelSchema = object({
|
|
16288
|
+
kind: literal("sub-class"),
|
|
16289
|
+
className: string().min(1)
|
|
16290
|
+
});
|
|
16291
|
+
var IdentityLabelSchema = object({
|
|
16292
|
+
kind: literal("identity"),
|
|
16293
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16294
|
+
identityId: string().min(1),
|
|
16295
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16296
|
+
name: string().min(1)
|
|
16297
|
+
});
|
|
16298
|
+
var PlateLabelSchema = object({
|
|
16299
|
+
kind: literal("plate"),
|
|
16300
|
+
text: string().min(1)
|
|
16301
|
+
});
|
|
16302
|
+
function macroOf$1(className) {
|
|
16303
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16304
|
+
}
|
|
16305
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16306
|
+
function subClassBelongs(macro, label) {
|
|
16307
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16308
|
+
}
|
|
16309
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16310
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16311
|
+
object({
|
|
16312
|
+
macro: literal("animal"),
|
|
16313
|
+
label: subClassOf("animal").optional()
|
|
16314
|
+
}),
|
|
16315
|
+
object({
|
|
16316
|
+
macro: literal("person"),
|
|
16317
|
+
label: IdentityLabelSchema.optional()
|
|
16318
|
+
}),
|
|
16319
|
+
object({
|
|
16320
|
+
macro: literal("vehicle"),
|
|
16321
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16322
|
+
}),
|
|
16323
|
+
object({
|
|
16324
|
+
macro: literal("package"),
|
|
16325
|
+
label: subClassOf("package").optional()
|
|
16326
|
+
}),
|
|
16327
|
+
object({
|
|
16328
|
+
macro: literal("other"),
|
|
16329
|
+
className: string().min(1)
|
|
16330
|
+
})
|
|
16331
|
+
]);
|
|
16332
|
+
/**
|
|
16333
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16334
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16335
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16336
|
+
*/
|
|
16337
|
+
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" });
|
|
16338
|
+
/**
|
|
16339
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16340
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16341
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16342
|
+
*/
|
|
16343
|
+
function subjectFromClassName(className) {
|
|
16344
|
+
const name = className.trim().toLowerCase();
|
|
16345
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16346
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16347
|
+
if (macro === void 0) return {
|
|
16348
|
+
macro: "other",
|
|
16349
|
+
className
|
|
16350
|
+
};
|
|
16351
|
+
const label = {
|
|
16352
|
+
kind: "sub-class",
|
|
16353
|
+
className: name
|
|
16354
|
+
};
|
|
16355
|
+
switch (macro) {
|
|
16356
|
+
case "animal": return {
|
|
16357
|
+
macro: "animal",
|
|
16358
|
+
label
|
|
16359
|
+
};
|
|
16360
|
+
case "vehicle": return {
|
|
16361
|
+
macro: "vehicle",
|
|
16362
|
+
label
|
|
16363
|
+
};
|
|
16364
|
+
case "package": return {
|
|
16365
|
+
macro: "package",
|
|
16366
|
+
label
|
|
16367
|
+
};
|
|
16368
|
+
default: return {
|
|
16369
|
+
macro: "other",
|
|
16370
|
+
className
|
|
16371
|
+
};
|
|
16372
|
+
}
|
|
16373
|
+
}
|
|
16374
|
+
function macroOnly(macro) {
|
|
16375
|
+
switch (macro) {
|
|
16376
|
+
case "animal": return { macro: "animal" };
|
|
16377
|
+
case "person": return { macro: "person" };
|
|
16378
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16379
|
+
case "package": return { macro: "package" };
|
|
16380
|
+
default: return {
|
|
16381
|
+
macro: "other",
|
|
16382
|
+
className: macro
|
|
16383
|
+
};
|
|
16384
|
+
}
|
|
16385
|
+
}
|
|
16386
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16387
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16119
16388
|
var AutomationConditionOperatorSchema = _enum([
|
|
16120
16389
|
"eq",
|
|
16121
16390
|
"ne",
|
|
@@ -16126,12 +16395,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16126
16395
|
"contains",
|
|
16127
16396
|
"in"
|
|
16128
16397
|
]);
|
|
16129
|
-
|
|
16130
|
-
|
|
16398
|
+
/**
|
|
16399
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16400
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16401
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16402
|
+
* fire; it does not lose it.
|
|
16403
|
+
*/
|
|
16404
|
+
var DeviceBecomesLeafSchema = object({
|
|
16405
|
+
kind: literal("device-becomes"),
|
|
16406
|
+
deviceId: number().int().nonnegative(),
|
|
16407
|
+
cap: string().min(1),
|
|
16408
|
+
fieldPath: string().min(1),
|
|
16409
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16410
|
+
becomes: union([
|
|
16411
|
+
string(),
|
|
16412
|
+
number(),
|
|
16413
|
+
boolean()
|
|
16414
|
+
]).optional(),
|
|
16415
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16416
|
+
edge: boolean().optional(),
|
|
16417
|
+
/** The new value must hold this long before it counts. */
|
|
16418
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
16419
|
+
/** Collapse a burst into one occurrence. */
|
|
16420
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16421
|
+
});
|
|
16422
|
+
var DeviceEventLeafSchema = object({
|
|
16423
|
+
kind: literal("device-event"),
|
|
16424
|
+
/** An `EventCategory` value. */
|
|
16425
|
+
category: string().min(1),
|
|
16426
|
+
deviceId: number().int().nonnegative().optional()
|
|
16427
|
+
});
|
|
16428
|
+
var ScheduleLeafSchema = object({
|
|
16429
|
+
kind: literal("schedule"),
|
|
16430
|
+
cron: string().min(1).max(120)
|
|
16431
|
+
});
|
|
16432
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16433
|
+
var IntervalLeafSchema = object({
|
|
16434
|
+
kind: literal("interval"),
|
|
16435
|
+
minutes: number().int().min(1).max(1440)
|
|
16436
|
+
});
|
|
16437
|
+
var DetectionLeafSchema = object({
|
|
16438
|
+
kind: literal("detection"),
|
|
16439
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16440
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16441
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16442
|
+
/** The track must have visited at least one of these zones. */
|
|
16443
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16444
|
+
/** Best-so-far confidence the track must reach. */
|
|
16445
|
+
minScore: number().min(0).max(1).optional()
|
|
16446
|
+
});
|
|
16447
|
+
var StationaryLeafSchema = object({
|
|
16448
|
+
kind: literal("stationary"),
|
|
16449
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16450
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16451
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16452
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16453
|
+
phase: _enum(["appeared", "departed"])
|
|
16454
|
+
});
|
|
16455
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16456
|
+
var DeviceIsLeafSchema = object({
|
|
16457
|
+
kind: literal("device-is"),
|
|
16131
16458
|
deviceId: number().int().nonnegative(),
|
|
16132
16459
|
cap: string().min(1),
|
|
16133
16460
|
fieldPath: string().min(1),
|
|
16134
16461
|
operator: AutomationConditionOperatorSchema,
|
|
16462
|
+
/**
|
|
16463
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16464
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16465
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16466
|
+
* one is never evaluated (it fails).
|
|
16467
|
+
*/
|
|
16135
16468
|
value: union([
|
|
16136
16469
|
string(),
|
|
16137
16470
|
number(),
|
|
@@ -16139,24 +16472,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16139
16472
|
array(union([string(), number()])),
|
|
16140
16473
|
QuantityValueSchema
|
|
16141
16474
|
]),
|
|
16475
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16142
16476
|
unit: string().min(1).optional()
|
|
16143
16477
|
});
|
|
16144
|
-
|
|
16478
|
+
/**
|
|
16479
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16480
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16481
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16482
|
+
*/
|
|
16483
|
+
var StationaryPresentLeafSchema = object({
|
|
16145
16484
|
kind: literal("stationary-present"),
|
|
16146
16485
|
deviceId: number().int().nonnegative(),
|
|
16147
|
-
|
|
16148
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16149
|
-
/** The parked object must be in at least one of these zones. */
|
|
16486
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16150
16487
|
zones: array(string().min(1)).min(1).optional()
|
|
16151
16488
|
});
|
|
16152
16489
|
/**
|
|
16153
|
-
* The
|
|
16154
|
-
*
|
|
16155
|
-
*
|
|
16156
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16157
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16490
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16491
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16492
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16158
16493
|
*/
|
|
16159
|
-
var
|
|
16494
|
+
var ExpressionLeafSchema = object({
|
|
16160
16495
|
kind: literal("expression"),
|
|
16161
16496
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16162
16497
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16168,55 +16503,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16168
16503
|
path: ["expr"]
|
|
16169
16504
|
});
|
|
16170
16505
|
});
|
|
16171
|
-
var
|
|
16506
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16172
16507
|
object({
|
|
16173
16508
|
kind: literal("all"),
|
|
16174
|
-
children: array(
|
|
16509
|
+
children: array(AutomationWhenSchema)
|
|
16175
16510
|
}),
|
|
16176
16511
|
object({
|
|
16177
16512
|
kind: literal("any"),
|
|
16178
|
-
children: array(
|
|
16513
|
+
children: array(AutomationWhenSchema)
|
|
16179
16514
|
}),
|
|
16180
16515
|
object({
|
|
16181
16516
|
kind: literal("not"),
|
|
16182
|
-
child:
|
|
16517
|
+
child: AutomationWhenSchema
|
|
16183
16518
|
}),
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
|
|
16519
|
+
DeviceBecomesLeafSchema,
|
|
16520
|
+
DeviceEventLeafSchema,
|
|
16521
|
+
ScheduleLeafSchema,
|
|
16522
|
+
ManualLeafSchema,
|
|
16523
|
+
IntervalLeafSchema,
|
|
16524
|
+
DetectionLeafSchema,
|
|
16525
|
+
StationaryLeafSchema,
|
|
16526
|
+
DeviceIsLeafSchema,
|
|
16527
|
+
StationaryPresentLeafSchema,
|
|
16528
|
+
ExpressionLeafSchema
|
|
16187
16529
|
]));
|
|
16188
16530
|
/**
|
|
16189
|
-
*
|
|
16190
|
-
*
|
|
16191
|
-
*
|
|
16192
|
-
*
|
|
16193
|
-
*
|
|
16194
|
-
*
|
|
16195
|
-
*
|
|
16196
|
-
*
|
|
16197
|
-
|
|
16198
|
-
|
|
16531
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16532
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16533
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16534
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16535
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16536
|
+
* way. Nothing writes this shape any more.
|
|
16537
|
+
*
|
|
16538
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16539
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16540
|
+
* a single trigger is not wrapped in `any`.
|
|
16541
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16542
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16543
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16544
|
+
*/
|
|
16545
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16199
16546
|
object({
|
|
16200
16547
|
kind: literal("device-state"),
|
|
16201
16548
|
deviceId: number().int().nonnegative(),
|
|
16202
16549
|
cap: string().min(1),
|
|
16203
16550
|
fieldPath: string().min(1),
|
|
16204
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16205
16551
|
becomes: union([
|
|
16206
16552
|
string(),
|
|
16207
16553
|
number(),
|
|
16208
16554
|
boolean()
|
|
16209
16555
|
]).optional(),
|
|
16210
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16211
16556
|
edge: boolean().optional(),
|
|
16212
|
-
/** The condition must hold this long before the run starts. */
|
|
16213
16557
|
forMs: number().int().min(0).max(864e5).optional(),
|
|
16214
|
-
/** Collapse a burst into one run. */
|
|
16215
16558
|
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16216
16559
|
}),
|
|
16217
16560
|
object({
|
|
16218
16561
|
kind: literal("device-event"),
|
|
16219
|
-
/** An `EventCategory` value. */
|
|
16220
16562
|
category: string().min(1),
|
|
16221
16563
|
deviceId: number().int().nonnegative().optional()
|
|
16222
16564
|
}),
|
|
@@ -16227,25 +16569,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16227
16569
|
object({ kind: literal("manual") }),
|
|
16228
16570
|
object({
|
|
16229
16571
|
kind: literal("detection"),
|
|
16230
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16231
|
-
* matches every raw class that maps to it; any other name matches
|
|
16232
|
-
* exactly. */
|
|
16233
16572
|
classes: array(string().min(1)).min(1),
|
|
16234
16573
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16235
|
-
/** The track must have visited at least one of these zones. */
|
|
16236
16574
|
zones: array(string().min(1)).min(1).optional(),
|
|
16237
|
-
/** Best-so-far confidence the track must reach. */
|
|
16238
16575
|
minScore: number().min(0).max(1).optional()
|
|
16239
16576
|
}),
|
|
16240
16577
|
object({
|
|
16241
16578
|
kind: literal("stationary"),
|
|
16242
16579
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16243
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16244
16580
|
classes: array(string().min(1)).min(1).optional(),
|
|
16245
|
-
/** The parked object must be in at least one of these zones. */
|
|
16246
16581
|
zones: array(string().min(1)).min(1).optional(),
|
|
16247
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16248
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16249
16582
|
phase: _enum(["appeared", "departed"])
|
|
16250
16583
|
}),
|
|
16251
16584
|
object({
|
|
@@ -16253,6 +16586,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16253
16586
|
minutes: number().int().min(1).max(1440)
|
|
16254
16587
|
})
|
|
16255
16588
|
]);
|
|
16589
|
+
var LegacyConditionLeafSchema = object({
|
|
16590
|
+
kind: literal("condition"),
|
|
16591
|
+
deviceId: number().int().nonnegative(),
|
|
16592
|
+
cap: string().min(1),
|
|
16593
|
+
fieldPath: string().min(1),
|
|
16594
|
+
operator: AutomationConditionOperatorSchema,
|
|
16595
|
+
value: union([
|
|
16596
|
+
string(),
|
|
16597
|
+
number(),
|
|
16598
|
+
boolean(),
|
|
16599
|
+
array(union([string(), number()])),
|
|
16600
|
+
QuantityValueSchema
|
|
16601
|
+
]),
|
|
16602
|
+
unit: string().min(1).optional()
|
|
16603
|
+
});
|
|
16604
|
+
var LegacyStationaryPresentSchema = object({
|
|
16605
|
+
kind: literal("stationary-present"),
|
|
16606
|
+
deviceId: number().int().nonnegative(),
|
|
16607
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
16608
|
+
zones: array(string().min(1)).min(1).optional()
|
|
16609
|
+
});
|
|
16610
|
+
var LegacyExpressionSchema = object({
|
|
16611
|
+
kind: literal("expression"),
|
|
16612
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16613
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
16614
|
+
}).superRefine((src, ctx) => {
|
|
16615
|
+
const err = validateExpressionSource(src);
|
|
16616
|
+
if (err !== null) ctx.addIssue({
|
|
16617
|
+
code: "custom",
|
|
16618
|
+
message: err,
|
|
16619
|
+
path: ["expr"]
|
|
16620
|
+
});
|
|
16621
|
+
});
|
|
16622
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
16623
|
+
object({
|
|
16624
|
+
kind: literal("all"),
|
|
16625
|
+
children: array(LegacyConditionSchema)
|
|
16626
|
+
}),
|
|
16627
|
+
object({
|
|
16628
|
+
kind: literal("any"),
|
|
16629
|
+
children: array(LegacyConditionSchema)
|
|
16630
|
+
}),
|
|
16631
|
+
object({
|
|
16632
|
+
kind: literal("not"),
|
|
16633
|
+
child: LegacyConditionSchema
|
|
16634
|
+
}),
|
|
16635
|
+
LegacyConditionLeafSchema,
|
|
16636
|
+
LegacyExpressionSchema,
|
|
16637
|
+
LegacyStationaryPresentSchema
|
|
16638
|
+
]));
|
|
16639
|
+
function convertTrigger(trigger) {
|
|
16640
|
+
switch (trigger.kind) {
|
|
16641
|
+
case "device-state": return {
|
|
16642
|
+
...trigger,
|
|
16643
|
+
kind: "device-becomes"
|
|
16644
|
+
};
|
|
16645
|
+
case "detection": {
|
|
16646
|
+
const { classes, ...rest } = trigger;
|
|
16647
|
+
return {
|
|
16648
|
+
...rest,
|
|
16649
|
+
subjects: classes.map(subjectFromClassName)
|
|
16650
|
+
};
|
|
16651
|
+
}
|
|
16652
|
+
case "stationary": {
|
|
16653
|
+
const { classes, ...rest } = trigger;
|
|
16654
|
+
return {
|
|
16655
|
+
...rest,
|
|
16656
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16657
|
+
};
|
|
16658
|
+
}
|
|
16659
|
+
default: return trigger;
|
|
16660
|
+
}
|
|
16661
|
+
}
|
|
16662
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
16663
|
+
function convertLegacyCondition(condition) {
|
|
16664
|
+
switch (condition.kind) {
|
|
16665
|
+
case "condition": return {
|
|
16666
|
+
...condition,
|
|
16667
|
+
kind: "device-is"
|
|
16668
|
+
};
|
|
16669
|
+
case "stationary-present": {
|
|
16670
|
+
const { classes, ...rest } = condition;
|
|
16671
|
+
return {
|
|
16672
|
+
...rest,
|
|
16673
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16674
|
+
};
|
|
16675
|
+
}
|
|
16676
|
+
case "expression": return condition;
|
|
16677
|
+
case "not": return {
|
|
16678
|
+
kind: "not",
|
|
16679
|
+
child: convertLegacyCondition(condition.child)
|
|
16680
|
+
};
|
|
16681
|
+
case "all":
|
|
16682
|
+
case "any": return {
|
|
16683
|
+
kind: condition.kind,
|
|
16684
|
+
children: condition.children.map(convertLegacyCondition)
|
|
16685
|
+
};
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16688
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
16689
|
+
function convertLegacyWhen(half) {
|
|
16690
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
16691
|
+
const first = leaves[0];
|
|
16692
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
16693
|
+
kind: "any",
|
|
16694
|
+
children: leaves
|
|
16695
|
+
};
|
|
16696
|
+
if (half.conditions === void 0) return triggers;
|
|
16697
|
+
return {
|
|
16698
|
+
kind: "all",
|
|
16699
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
16700
|
+
};
|
|
16701
|
+
}
|
|
16702
|
+
/**
|
|
16703
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
16704
|
+
* becomes code.
|
|
16705
|
+
*
|
|
16706
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
16707
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
16708
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
16709
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
16710
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
16711
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
16712
|
+
*
|
|
16713
|
+
* Two parts:
|
|
16714
|
+
*
|
|
16715
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
16716
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
16717
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
16718
|
+
* ADR "one tree".
|
|
16719
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
16720
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
16721
|
+
* this had to diverge from "verbatim", and why.
|
|
16722
|
+
*
|
|
16723
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
16724
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
16725
|
+
*/
|
|
16256
16726
|
/**
|
|
16257
16727
|
* One action step.
|
|
16258
16728
|
*
|
|
@@ -16355,69 +16825,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16355
16825
|
minutes: number().int().min(1).max(1440)
|
|
16356
16826
|
})
|
|
16357
16827
|
]);
|
|
16358
|
-
var
|
|
16359
|
-
|
|
16360
|
-
conditions: AutomationConditionSchema.optional(),
|
|
16828
|
+
var CurrentAutomationRecipeSchema = object({
|
|
16829
|
+
when: AutomationWhenSchema,
|
|
16361
16830
|
actions: array(AutomationActionSchema),
|
|
16362
16831
|
runMode: AutomationRunModeSchema.optional(),
|
|
16363
16832
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16364
16833
|
keepTraces: number().int().min(1).max(100).optional()
|
|
16365
16834
|
});
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
bus: "vehicle",
|
|
16392
|
-
train: "vehicle",
|
|
16393
|
-
truck: "vehicle",
|
|
16394
|
-
boat: "vehicle",
|
|
16395
|
-
bird: "animal",
|
|
16396
|
-
cat: "animal",
|
|
16397
|
-
dog: "animal",
|
|
16398
|
-
horse: "animal",
|
|
16399
|
-
sheep: "animal",
|
|
16400
|
-
cow: "animal",
|
|
16401
|
-
elephant: "animal",
|
|
16402
|
-
bear: "animal",
|
|
16403
|
-
zebra: "animal",
|
|
16404
|
-
giraffe: "animal",
|
|
16405
|
-
suitcase: "package",
|
|
16406
|
-
backpack: "package",
|
|
16407
|
-
handbag: "package"
|
|
16408
|
-
},
|
|
16409
|
-
preserveOriginal: false
|
|
16410
|
-
};
|
|
16835
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
16836
|
+
var LegacyAutomationRecipeSchema = object({
|
|
16837
|
+
triggers: array(LegacyTriggerSchema),
|
|
16838
|
+
conditions: LegacyConditionSchema.optional(),
|
|
16839
|
+
actions: array(AutomationActionSchema),
|
|
16840
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
16841
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
16842
|
+
});
|
|
16843
|
+
/** The old shape as the new one, lossless. */
|
|
16844
|
+
function convertLegacyRecipe(legacy) {
|
|
16845
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
16846
|
+
return {
|
|
16847
|
+
when: convertLegacyWhen({
|
|
16848
|
+
triggers,
|
|
16849
|
+
conditions
|
|
16850
|
+
}),
|
|
16851
|
+
...rest
|
|
16852
|
+
};
|
|
16853
|
+
}
|
|
16854
|
+
/**
|
|
16855
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
16856
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
16857
|
+
* it. The output type is always the new one.
|
|
16858
|
+
*/
|
|
16859
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16411
16860
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16412
16861
|
/**
|
|
16413
16862
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16414
16863
|
* so the editor can paint every block green or red with its reason.
|
|
16415
16864
|
*
|
|
16416
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16417
|
-
* every
|
|
16418
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16419
|
-
* each addressed by a stable path (`
|
|
16420
|
-
* for an Action run).
|
|
16865
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
16866
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
16867
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
16868
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
16869
|
+
* for an Action run). Traces written before the single tree carry
|
|
16870
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16421
16871
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16422
16872
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16423
16873
|
*
|
|
@@ -16449,6 +16899,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16449
16899
|
]);
|
|
16450
16900
|
var AutomationTriggerTraceSchema = object({
|
|
16451
16901
|
kind: string().min(1),
|
|
16902
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
16903
|
+
path: string().min(1).optional(),
|
|
16452
16904
|
outcome: _enum(["pass", "fail"]),
|
|
16453
16905
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16454
16906
|
reason: string(),
|
|
@@ -16456,15 +16908,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16456
16908
|
vars: record(string(), TraceValueSchema)
|
|
16457
16909
|
});
|
|
16458
16910
|
var AutomationConditionTraceNodeSchema = object({
|
|
16459
|
-
/** `
|
|
16911
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16460
16912
|
path: string().min(1),
|
|
16461
16913
|
kind: _enum([
|
|
16462
16914
|
"all",
|
|
16463
16915
|
"any",
|
|
16464
16916
|
"not",
|
|
16465
16917
|
"condition",
|
|
16918
|
+
"device-is",
|
|
16466
16919
|
"expression",
|
|
16467
|
-
"stationary-present"
|
|
16920
|
+
"stationary-present",
|
|
16921
|
+
"device-becomes",
|
|
16922
|
+
"device-event",
|
|
16923
|
+
"schedule",
|
|
16924
|
+
"interval",
|
|
16925
|
+
"manual",
|
|
16926
|
+
"detection",
|
|
16927
|
+
"stationary"
|
|
16468
16928
|
]),
|
|
16469
16929
|
outcome: TraceNodeOutcomeSchema,
|
|
16470
16930
|
reason: string(),
|
|
@@ -16498,6 +16958,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16498
16958
|
deviceId: number().int().optional(),
|
|
16499
16959
|
startedAt: number().optional(),
|
|
16500
16960
|
endedAt: number().optional(),
|
|
16961
|
+
/**
|
|
16962
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
16963
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
16964
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
16965
|
+
*/
|
|
16966
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16501
16967
|
outcome: _enum([
|
|
16502
16968
|
"ok",
|
|
16503
16969
|
"error",
|
|
@@ -17247,6 +17713,23 @@ method(object({
|
|
|
17247
17713
|
addonId: string(),
|
|
17248
17714
|
nodeId: string().optional()
|
|
17249
17715
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
17716
|
+
addonId: string(),
|
|
17717
|
+
nodeId: string().optional()
|
|
17718
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
17719
|
+
addonId: string(),
|
|
17720
|
+
nodeId: string().optional(),
|
|
17721
|
+
keys: array(string())
|
|
17722
|
+
}), object({
|
|
17723
|
+
removed: array(string()),
|
|
17724
|
+
refused: array(object({
|
|
17725
|
+
key: string(),
|
|
17726
|
+
reason: string()
|
|
17727
|
+
})),
|
|
17728
|
+
secretsCleared: number()
|
|
17729
|
+
}), {
|
|
17730
|
+
kind: "mutation",
|
|
17731
|
+
auth: "admin"
|
|
17732
|
+
}), method(object({
|
|
17250
17733
|
addonId: string(),
|
|
17251
17734
|
deviceId: number(),
|
|
17252
17735
|
nodeId: string().optional()
|
|
@@ -21276,6 +21759,8 @@ var StorageKindSchema = object({
|
|
|
21276
21759
|
"orphaned",
|
|
21277
21760
|
"dropped"
|
|
21278
21761
|
]),
|
|
21762
|
+
/** An owner declared this kind since the engine started. */
|
|
21763
|
+
declaredThisBoot: boolean(),
|
|
21279
21764
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21280
21765
|
rows: number().int().nonnegative().nullable(),
|
|
21281
21766
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21287,6 +21772,51 @@ var StorageKindSchema = object({
|
|
|
21287
21772
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21288
21773
|
declaration: RecordKindDeclarationSchema
|
|
21289
21774
|
});
|
|
21775
|
+
_enum([
|
|
21776
|
+
"orphaned",
|
|
21777
|
+
"stale",
|
|
21778
|
+
"unknown"
|
|
21779
|
+
]);
|
|
21780
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
21781
|
+
var StaleKindFindingSchema = object({
|
|
21782
|
+
owner: string(),
|
|
21783
|
+
kind: string(),
|
|
21784
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
21785
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
21786
|
+
reason: string(),
|
|
21787
|
+
rows: number().int().nonnegative().nullable(),
|
|
21788
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
21789
|
+
protected: boolean()
|
|
21790
|
+
});
|
|
21791
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
21792
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
21793
|
+
addonId: string(),
|
|
21794
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
21795
|
+
key: string(),
|
|
21796
|
+
/**
|
|
21797
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
21798
|
+
* not orphans yet. Removing the key releases them.
|
|
21799
|
+
*/
|
|
21800
|
+
secretRefs: array(string())
|
|
21801
|
+
});
|
|
21802
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
21803
|
+
var StaleUnknownSchema = object({
|
|
21804
|
+
subject: string(),
|
|
21805
|
+
reason: string()
|
|
21806
|
+
});
|
|
21807
|
+
var StaleDataReportSchema = object({
|
|
21808
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
21809
|
+
scannedAt: number().nullable(),
|
|
21810
|
+
scanning: boolean(),
|
|
21811
|
+
trigger: _enum([
|
|
21812
|
+
"boot",
|
|
21813
|
+
"interval",
|
|
21814
|
+
"manual"
|
|
21815
|
+
]).nullable(),
|
|
21816
|
+
kinds: array(StaleKindFindingSchema),
|
|
21817
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
21818
|
+
unknown: array(StaleUnknownSchema)
|
|
21819
|
+
});
|
|
21290
21820
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21291
21821
|
owner: string().min(1).optional(),
|
|
21292
21822
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21294,7 +21824,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21294
21824
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21295
21825
|
owner: string().min(1),
|
|
21296
21826
|
kind: RecordKindNameSchema
|
|
21297
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
21827
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
21828
|
+
kind: "mutation",
|
|
21829
|
+
auth: "admin"
|
|
21830
|
+
}), method(object({
|
|
21831
|
+
owner: string().min(1),
|
|
21832
|
+
kind: RecordKindNameSchema
|
|
21833
|
+
}), object({
|
|
21834
|
+
deleted: number().int().nonnegative(),
|
|
21835
|
+
secretsReleased: number().int().nonnegative()
|
|
21836
|
+
}), {
|
|
21837
|
+
kind: "mutation",
|
|
21838
|
+
auth: "admin",
|
|
21839
|
+
caller: "required"
|
|
21840
|
+
}), method(object({
|
|
21841
|
+
addonId: string().min(1),
|
|
21842
|
+
key: string().min(1)
|
|
21843
|
+
}), object({
|
|
21844
|
+
removed: boolean(),
|
|
21845
|
+
secretsCleared: number().int().nonnegative()
|
|
21846
|
+
}), {
|
|
21847
|
+
kind: "mutation",
|
|
21848
|
+
auth: "admin",
|
|
21849
|
+
caller: "required"
|
|
21850
|
+
});
|
|
21298
21851
|
/**
|
|
21299
21852
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21300
21853
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21433,6 +21986,12 @@ method(object({
|
|
|
21433
21986
|
}), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
|
|
21434
21987
|
kind: "mutation",
|
|
21435
21988
|
auth: "admin"
|
|
21989
|
+
}), method(object({
|
|
21990
|
+
refs: RefListSchema,
|
|
21991
|
+
onBehalfOf: string().min(1)
|
|
21992
|
+
}), object({ released: number().int() }), {
|
|
21993
|
+
kind: "mutation",
|
|
21994
|
+
auth: "admin"
|
|
21436
21995
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21437
21996
|
/**
|
|
21438
21997
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22335,7 +22894,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22335
22894
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22336
22895
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22337
22896
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22338
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
22897
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
22898
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
22899
|
+
parentStableId: string().optional()
|
|
22339
22900
|
}));
|
|
22340
22901
|
/**
|
|
22341
22902
|
* Status block — populated by the integration after each
|
|
@@ -42718,6 +43279,79 @@ object({
|
|
|
42718
43279
|
metadata: record(string(), unknown()).optional()
|
|
42719
43280
|
});
|
|
42720
43281
|
/**
|
|
43282
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43283
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43284
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43285
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43286
|
+
* except through a `custom` mode.
|
|
43287
|
+
*/
|
|
43288
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43289
|
+
object({
|
|
43290
|
+
kind: literal("mdns"),
|
|
43291
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43292
|
+
serviceType: string().min(1),
|
|
43293
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43294
|
+
instancePattern: string().optional()
|
|
43295
|
+
}),
|
|
43296
|
+
object({
|
|
43297
|
+
kind: literal("ws-discovery"),
|
|
43298
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43299
|
+
types: array(string()).optional()
|
|
43300
|
+
}),
|
|
43301
|
+
object({
|
|
43302
|
+
kind: literal("ssdp"),
|
|
43303
|
+
searchTarget: string().min(1)
|
|
43304
|
+
}),
|
|
43305
|
+
object({
|
|
43306
|
+
kind: literal("udp-broadcast"),
|
|
43307
|
+
port: number().int().min(1).max(65535),
|
|
43308
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43309
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43310
|
+
listenPort: number().int().min(1).max(65535).optional()
|
|
43311
|
+
}),
|
|
43312
|
+
object({
|
|
43313
|
+
kind: literal("http-probe"),
|
|
43314
|
+
port: number().int().min(1).max(65535),
|
|
43315
|
+
path: string().startsWith("/"),
|
|
43316
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43317
|
+
bodyText: string().optional(),
|
|
43318
|
+
contentType: string().optional(),
|
|
43319
|
+
tls: boolean().default(false),
|
|
43320
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43321
|
+
acceptStatus: array(number().int()).optional()
|
|
43322
|
+
}),
|
|
43323
|
+
object({
|
|
43324
|
+
kind: literal("custom"),
|
|
43325
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43326
|
+
reason: string().min(1)
|
|
43327
|
+
})
|
|
43328
|
+
]);
|
|
43329
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43330
|
+
var DiscoveryHitSchema = object({
|
|
43331
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43332
|
+
modeIndex: number().int().min(0),
|
|
43333
|
+
host: string(),
|
|
43334
|
+
port: number().int(),
|
|
43335
|
+
http: object({
|
|
43336
|
+
status: number().int(),
|
|
43337
|
+
headers: record(string(), string()),
|
|
43338
|
+
/** At most 16 KiB. */
|
|
43339
|
+
bodyText: string()
|
|
43340
|
+
}).optional(),
|
|
43341
|
+
mdns: object({
|
|
43342
|
+
instance: string(),
|
|
43343
|
+
host: string(),
|
|
43344
|
+
port: number().int(),
|
|
43345
|
+
txt: record(string(), string())
|
|
43346
|
+
}).optional(),
|
|
43347
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43348
|
+
wsd: object({
|
|
43349
|
+
xaddrs: array(string()),
|
|
43350
|
+
types: array(string()),
|
|
43351
|
+
scopes: array(string())
|
|
43352
|
+
}).optional()
|
|
43353
|
+
});
|
|
43354
|
+
/**
|
|
42721
43355
|
* Candidate handed back from discovery and accepted by
|
|
42722
43356
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
42723
43357
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -42752,7 +43386,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
42752
43386
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
42753
43387
|
onboardedDeviceId: number().optional(),
|
|
42754
43388
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
42755
|
-
onboardedName: string().optional()
|
|
43389
|
+
onboardedName: string().optional(),
|
|
43390
|
+
/**
|
|
43391
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43392
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43393
|
+
* which is still read for one release.
|
|
43394
|
+
*/
|
|
43395
|
+
setupIntegration: boolean().optional(),
|
|
43396
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43397
|
+
parentStableId: string().optional(),
|
|
43398
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43399
|
+
selectable: boolean().optional()
|
|
42756
43400
|
});
|
|
42757
43401
|
/**
|
|
42758
43402
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -42849,6 +43493,24 @@ var deviceProviderCapability = {
|
|
|
42849
43493
|
auth: "admin"
|
|
42850
43494
|
}),
|
|
42851
43495
|
/**
|
|
43496
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43497
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43498
|
+
*/
|
|
43499
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43500
|
+
/**
|
|
43501
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43502
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43503
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43504
|
+
* from `getDiscoveryParamsSchema`.
|
|
43505
|
+
*/
|
|
43506
|
+
classifyDiscoveryHits: method(object({
|
|
43507
|
+
hits: array(DiscoveryHitSchema),
|
|
43508
|
+
params: record(string(), unknown()).optional()
|
|
43509
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43510
|
+
kind: "mutation",
|
|
43511
|
+
auth: "admin"
|
|
43512
|
+
}),
|
|
43513
|
+
/**
|
|
42852
43514
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
42853
43515
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
42854
43516
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -43613,17 +44275,23 @@ method(object({
|
|
|
43613
44275
|
}), method(object({}), object({ providers: array(object({
|
|
43614
44276
|
addonId: string(),
|
|
43615
44277
|
label: string()
|
|
43616
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
43617
|
-
|
|
43618
|
-
|
|
43619
|
-
|
|
43620
|
-
|
|
43621
|
-
|
|
43622
|
-
|
|
43623
|
-
|
|
43624
|
-
}),
|
|
44278
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44279
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44280
|
+
object({ subnets: array(string()).optional() }),
|
|
44281
|
+
object({ groups: array(object({
|
|
44282
|
+
addonId: string(),
|
|
44283
|
+
label: string(),
|
|
44284
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44285
|
+
error: string().nullable()
|
|
44286
|
+
})).readonly() }),
|
|
44287
|
+
{
|
|
44288
|
+
kind: "mutation",
|
|
44289
|
+
auth: "admin"
|
|
44290
|
+
}
|
|
44291
|
+
), method(object({
|
|
43625
44292
|
addonId: string(),
|
|
43626
|
-
params: record(string(), unknown()).optional()
|
|
44293
|
+
params: record(string(), unknown()).optional(),
|
|
44294
|
+
subnets: array(string()).optional()
|
|
43627
44295
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
43628
44296
|
kind: "mutation",
|
|
43629
44297
|
auth: "admin"
|
|
@@ -62965,7 +63633,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
62965
63633
|
id: string(),
|
|
62966
63634
|
settings: record(string(), unknown()),
|
|
62967
63635
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
62968
|
-
secretKeys: array(string()).optional()
|
|
63636
|
+
secretKeys: array(string()).optional(),
|
|
63637
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
63638
|
+
skipRestart: boolean().optional()
|
|
62969
63639
|
}), object({ success: boolean() }), {
|
|
62970
63640
|
kind: "mutation",
|
|
62971
63641
|
auth: "admin"
|
|
@@ -63593,6 +64263,39 @@ var TlsStatusSchema = object({
|
|
|
63593
64263
|
hasCa: boolean()
|
|
63594
64264
|
}).nullable()
|
|
63595
64265
|
});
|
|
64266
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64267
|
+
var ScanRequestSchema = object({
|
|
64268
|
+
providerId: string().min(1),
|
|
64269
|
+
modes: array(DiscoveryModeSchema)
|
|
64270
|
+
});
|
|
64271
|
+
var ScanErrorSchema = object({
|
|
64272
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64273
|
+
providerId: string().optional(),
|
|
64274
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64275
|
+
mode: string().optional(),
|
|
64276
|
+
message: string()
|
|
64277
|
+
});
|
|
64278
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64279
|
+
var ScanStatsSchema = object({
|
|
64280
|
+
hostsProbed: number().int(),
|
|
64281
|
+
hits: number().int(),
|
|
64282
|
+
timeouts: number().int(),
|
|
64283
|
+
byMode: record(string(), object({
|
|
64284
|
+
probed: number().int(),
|
|
64285
|
+
hits: number().int(),
|
|
64286
|
+
timeouts: number().int()
|
|
64287
|
+
}))
|
|
64288
|
+
});
|
|
64289
|
+
var ScanNetworkResultSchema = object({
|
|
64290
|
+
hits: array(object({
|
|
64291
|
+
providerId: string(),
|
|
64292
|
+
hit: DiscoveryHitSchema
|
|
64293
|
+
})),
|
|
64294
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64295
|
+
subnetsScanned: array(string()),
|
|
64296
|
+
errors: array(ScanErrorSchema),
|
|
64297
|
+
stats: ScanStatsSchema
|
|
64298
|
+
});
|
|
63596
64299
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
63597
64300
|
/**
|
|
63598
64301
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -63647,6 +64350,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
63647
64350
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
63648
64351
|
kind: "mutation",
|
|
63649
64352
|
auth: "admin"
|
|
64353
|
+
}), method(object({
|
|
64354
|
+
requests: array(ScanRequestSchema),
|
|
64355
|
+
subnets: array(string()).optional()
|
|
64356
|
+
}), ScanNetworkResultSchema, {
|
|
64357
|
+
kind: "mutation",
|
|
64358
|
+
auth: "admin",
|
|
64359
|
+
timeoutMs: 9e4
|
|
64360
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64361
|
+
kind: "mutation",
|
|
64362
|
+
auth: "admin"
|
|
63650
64363
|
}), method(_void(), TlsStatusSchema, {
|
|
63651
64364
|
kind: "mutation",
|
|
63652
64365
|
auth: "admin"
|
|
@@ -73255,6 +73968,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73255
73968
|
async discoverDevices(_input) {
|
|
73256
73969
|
return [];
|
|
73257
73970
|
}
|
|
73971
|
+
/**
|
|
73972
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
73973
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
73974
|
+
*/
|
|
73975
|
+
async getDiscoveryModes() {
|
|
73976
|
+
return [];
|
|
73977
|
+
}
|
|
73978
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
73979
|
+
async classifyDiscoveryHits(_input) {
|
|
73980
|
+
return [];
|
|
73981
|
+
}
|
|
73258
73982
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73259
73983
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73260
73984
|
async getDiscoveryParamsSchema() {
|
|
@@ -74123,6 +74847,13 @@ Object.freeze({
|
|
|
74123
74847
|
access: "create",
|
|
74124
74848
|
role: "admin"
|
|
74125
74849
|
},
|
|
74850
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
74851
|
+
capName: "addon-settings",
|
|
74852
|
+
capScope: "system",
|
|
74853
|
+
addonId: null,
|
|
74854
|
+
access: "view",
|
|
74855
|
+
role: "admin"
|
|
74856
|
+
},
|
|
74126
74857
|
"addonSettings.getDeviceSettings": {
|
|
74127
74858
|
capName: "addon-settings",
|
|
74128
74859
|
capScope: "system",
|
|
@@ -74144,6 +74875,13 @@ Object.freeze({
|
|
|
74144
74875
|
access: "view",
|
|
74145
74876
|
role: "admin"
|
|
74146
74877
|
},
|
|
74878
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
74879
|
+
capName: "addon-settings",
|
|
74880
|
+
capScope: "system",
|
|
74881
|
+
addonId: null,
|
|
74882
|
+
access: "create",
|
|
74883
|
+
role: "admin"
|
|
74884
|
+
},
|
|
74147
74885
|
"addonSettings.updateDeviceSettings": {
|
|
74148
74886
|
capName: "addon-settings",
|
|
74149
74887
|
capScope: "system",
|
|
@@ -76559,6 +77297,13 @@ Object.freeze({
|
|
|
76559
77297
|
access: "create",
|
|
76560
77298
|
role: "admin"
|
|
76561
77299
|
},
|
|
77300
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77301
|
+
capName: "device-provider",
|
|
77302
|
+
capScope: "system",
|
|
77303
|
+
addonId: null,
|
|
77304
|
+
access: "create",
|
|
77305
|
+
role: "admin"
|
|
77306
|
+
},
|
|
76562
77307
|
"deviceProvider.createDevice": {
|
|
76563
77308
|
capName: "device-provider",
|
|
76564
77309
|
capScope: "system",
|
|
@@ -76587,6 +77332,13 @@ Object.freeze({
|
|
|
76587
77332
|
access: "view",
|
|
76588
77333
|
role: "admin"
|
|
76589
77334
|
},
|
|
77335
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77336
|
+
capName: "device-provider",
|
|
77337
|
+
capScope: "system",
|
|
77338
|
+
addonId: null,
|
|
77339
|
+
access: "view",
|
|
77340
|
+
role: "admin"
|
|
77341
|
+
},
|
|
76590
77342
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76591
77343
|
capName: "device-provider",
|
|
76592
77344
|
capScope: "system",
|
|
@@ -77364,6 +78116,13 @@ Object.freeze({
|
|
|
77364
78116
|
access: "create",
|
|
77365
78117
|
role: "admin"
|
|
77366
78118
|
},
|
|
78119
|
+
"keychain.releaseRecordRefs": {
|
|
78120
|
+
capName: "keychain",
|
|
78121
|
+
capScope: "system",
|
|
78122
|
+
addonId: null,
|
|
78123
|
+
access: "create",
|
|
78124
|
+
role: "admin"
|
|
78125
|
+
},
|
|
77367
78126
|
"keychain.resolve": {
|
|
77368
78127
|
capName: "keychain",
|
|
77369
78128
|
capScope: "system",
|
|
@@ -77784,6 +78543,13 @@ Object.freeze({
|
|
|
77784
78543
|
access: "view",
|
|
77785
78544
|
role: "admin"
|
|
77786
78545
|
},
|
|
78546
|
+
"localNetwork.getScanSubnets": {
|
|
78547
|
+
capName: "local-network",
|
|
78548
|
+
capScope: "system",
|
|
78549
|
+
addonId: null,
|
|
78550
|
+
access: "view",
|
|
78551
|
+
role: "admin"
|
|
78552
|
+
},
|
|
77787
78553
|
"localNetwork.getTlsStatus": {
|
|
77788
78554
|
capName: "local-network",
|
|
77789
78555
|
capScope: "system",
|
|
@@ -77833,6 +78599,13 @@ Object.freeze({
|
|
|
77833
78599
|
access: "create",
|
|
77834
78600
|
role: "admin"
|
|
77835
78601
|
},
|
|
78602
|
+
"localNetwork.scanNetwork": {
|
|
78603
|
+
capName: "local-network",
|
|
78604
|
+
capScope: "system",
|
|
78605
|
+
addonId: null,
|
|
78606
|
+
access: "create",
|
|
78607
|
+
role: "admin"
|
|
78608
|
+
},
|
|
77836
78609
|
"localNetwork.setAddressRole": {
|
|
77837
78610
|
capName: "local-network",
|
|
77838
78611
|
capScope: "system",
|
|
@@ -77854,6 +78627,13 @@ Object.freeze({
|
|
|
77854
78627
|
access: "create",
|
|
77855
78628
|
role: "admin"
|
|
77856
78629
|
},
|
|
78630
|
+
"localNetwork.setScanSubnets": {
|
|
78631
|
+
capName: "local-network",
|
|
78632
|
+
capScope: "system",
|
|
78633
|
+
addonId: null,
|
|
78634
|
+
access: "create",
|
|
78635
|
+
role: "admin"
|
|
78636
|
+
},
|
|
77857
78637
|
"localNetwork.setViewerEndpoints": {
|
|
77858
78638
|
capName: "local-network",
|
|
77859
78639
|
capScope: "system",
|
|
@@ -82075,6 +82855,13 @@ Object.freeze({
|
|
|
82075
82855
|
access: "create",
|
|
82076
82856
|
role: "admin"
|
|
82077
82857
|
},
|
|
82858
|
+
"storageAdmin.dropOrphanedKind": {
|
|
82859
|
+
capName: "storage-admin",
|
|
82860
|
+
capScope: "system",
|
|
82861
|
+
addonId: null,
|
|
82862
|
+
access: "delete",
|
|
82863
|
+
role: "admin"
|
|
82864
|
+
},
|
|
82078
82865
|
"storageAdmin.listKinds": {
|
|
82079
82866
|
capName: "storage-admin",
|
|
82080
82867
|
capScope: "system",
|
|
@@ -82096,6 +82883,27 @@ Object.freeze({
|
|
|
82096
82883
|
access: "view",
|
|
82097
82884
|
role: "admin"
|
|
82098
82885
|
},
|
|
82886
|
+
"storageAdmin.scanStaleData": {
|
|
82887
|
+
capName: "storage-admin",
|
|
82888
|
+
capScope: "system",
|
|
82889
|
+
addonId: null,
|
|
82890
|
+
access: "create",
|
|
82891
|
+
role: "admin"
|
|
82892
|
+
},
|
|
82893
|
+
"storageAdmin.staleDataReport": {
|
|
82894
|
+
capName: "storage-admin",
|
|
82895
|
+
capScope: "system",
|
|
82896
|
+
addonId: null,
|
|
82897
|
+
access: "view",
|
|
82898
|
+
role: "admin"
|
|
82899
|
+
},
|
|
82900
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
82901
|
+
capName: "storage-admin",
|
|
82902
|
+
capScope: "system",
|
|
82903
|
+
addonId: null,
|
|
82904
|
+
access: "create",
|
|
82905
|
+
role: "admin"
|
|
82906
|
+
},
|
|
82099
82907
|
"storageEvictable.evict": {
|
|
82100
82908
|
capName: "storage-evictable",
|
|
82101
82909
|
capScope: "system",
|
|
@@ -100918,79 +101726,18 @@ function setIntegrationDevicesOnline(devices, integrationId, online) {
|
|
|
100918
101726
|
}
|
|
100919
101727
|
//#endregion
|
|
100920
101728
|
//#region src/velux-gateway-discovery.ts
|
|
100921
|
-
/** `prefilledConfig` flag: the scan opens the integration form instead of adopting a device. */
|
|
100922
|
-
var VELUX_SETUP_INTEGRATION_KEY = "setupIntegration";
|
|
100923
|
-
/** Top bit of the question class: ask for a unicast reply (RFC 6762). */
|
|
100924
|
-
var MDNS_QU_CLASS = 32769;
|
|
100925
101729
|
/** The KLF-200 announces itself as an `_http._tcp` service named `VELUX_KLF_<id>`. */
|
|
100926
|
-
var VELUX_MDNS_SERVICE = "_http._tcp
|
|
101730
|
+
var VELUX_MDNS_SERVICE = "_http._tcp";
|
|
100927
101731
|
var VELUX_GATEWAY_NAME_PREFIX = "VELUX_KLF";
|
|
100928
|
-
|
|
100929
|
-
var
|
|
100930
|
-
|
|
100931
|
-
|
|
100932
|
-
|
|
100933
|
-
|
|
101732
|
+
/** How the scan engine finds a KLF-200: one mDNS browse, filtered on the instance name. */
|
|
101733
|
+
var VELUX_DISCOVERY_MODES = [{
|
|
101734
|
+
kind: "mdns",
|
|
101735
|
+
serviceType: VELUX_MDNS_SERVICE,
|
|
101736
|
+
instancePattern: `^${VELUX_GATEWAY_NAME_PREFIX}`
|
|
101737
|
+
}];
|
|
100934
101738
|
function isVeluxGatewayName(name) {
|
|
100935
101739
|
return (name.split("._")[0] ?? name).toLowerCase().startsWith(VELUX_GATEWAY_NAME_PREFIX.toLowerCase());
|
|
100936
101740
|
}
|
|
100937
|
-
function readName(buf, start) {
|
|
100938
|
-
const labels = [];
|
|
100939
|
-
let pos = start;
|
|
100940
|
-
let end = start;
|
|
100941
|
-
let jumped = false;
|
|
100942
|
-
for (let hops = 0; hops < 16; hops++) {
|
|
100943
|
-
if (pos < 0 || pos >= buf.length) return null;
|
|
100944
|
-
const len = buf[pos] ?? 0;
|
|
100945
|
-
if (len === 0) {
|
|
100946
|
-
if (!jumped) end = pos + 1;
|
|
100947
|
-
return {
|
|
100948
|
-
name: labels.join("."),
|
|
100949
|
-
end
|
|
100950
|
-
};
|
|
100951
|
-
}
|
|
100952
|
-
if ((len & 192) === 192) {
|
|
100953
|
-
if (pos + 1 >= buf.length) return null;
|
|
100954
|
-
if (!jumped) end = pos + 2;
|
|
100955
|
-
jumped = true;
|
|
100956
|
-
pos = (len & 63) << 8 | (buf[pos + 1] ?? 0);
|
|
100957
|
-
continue;
|
|
100958
|
-
}
|
|
100959
|
-
if ((len & 192) !== 0 || pos + 1 + len > buf.length) return null;
|
|
100960
|
-
labels.push(buf.subarray(pos + 1, pos + 1 + len).toString("utf8"));
|
|
100961
|
-
pos += 1 + len;
|
|
100962
|
-
if (!jumped) end = pos;
|
|
100963
|
-
}
|
|
100964
|
-
return null;
|
|
100965
|
-
}
|
|
100966
|
-
function readRecords(buf, offset, count) {
|
|
100967
|
-
const records = [];
|
|
100968
|
-
let pos = offset;
|
|
100969
|
-
for (let i = 0; i < count; i++) {
|
|
100970
|
-
const name = readName(buf, pos);
|
|
100971
|
-
if (name === null || name.end + 10 > buf.length) return null;
|
|
100972
|
-
const type = buf.readUInt16BE(name.end);
|
|
100973
|
-
const dataLength = buf.readUInt16BE(name.end + 8);
|
|
100974
|
-
const dataOffset = name.end + 10;
|
|
100975
|
-
if (dataOffset + dataLength > buf.length) return null;
|
|
100976
|
-
records.push({
|
|
100977
|
-
name: name.name,
|
|
100978
|
-
type,
|
|
100979
|
-
message: buf,
|
|
100980
|
-
dataOffset,
|
|
100981
|
-
dataLength
|
|
100982
|
-
});
|
|
100983
|
-
pos = dataOffset + dataLength;
|
|
100984
|
-
}
|
|
100985
|
-
return {
|
|
100986
|
-
records,
|
|
100987
|
-
next: pos
|
|
100988
|
-
};
|
|
100989
|
-
}
|
|
100990
|
-
function ipv4(buf, offset, length) {
|
|
100991
|
-
if (length !== 4 || offset + 4 > buf.length) return null;
|
|
100992
|
-
return `${buf[offset] ?? 0}.${buf[offset + 1] ?? 0}.${buf[offset + 2] ?? 0}.${buf[offset + 3] ?? 0}`;
|
|
100993
|
-
}
|
|
100994
101741
|
/** Identity used to recognise a gateway already stored on an integration. */
|
|
100995
101742
|
function veluxGatewayId(input) {
|
|
100996
101743
|
const service = input.serviceHost?.trim().toLowerCase() ?? "";
|
|
@@ -100999,40 +101746,24 @@ function veluxGatewayId(input) {
|
|
|
100999
101746
|
if (serial.length > 0) return serial;
|
|
101000
101747
|
return input.host;
|
|
101001
101748
|
}
|
|
101002
|
-
|
|
101003
|
-
|
|
101004
|
-
const srv = /* @__PURE__ */ new Map();
|
|
101005
|
-
const addresses = /* @__PURE__ */ new Map();
|
|
101006
|
-
for (const rec of records) {
|
|
101007
|
-
const key = rec.name.toLowerCase();
|
|
101008
|
-
if (rec.type === PTR) {
|
|
101009
|
-
const target = readName(rec.message, rec.dataOffset);
|
|
101010
|
-
if (target !== null && isVeluxGatewayName(target.name)) instances.push(target.name);
|
|
101011
|
-
} else if (rec.type === SRV && rec.dataLength >= 6) {
|
|
101012
|
-
const target = readName(rec.message, rec.dataOffset + 6);
|
|
101013
|
-
if (target !== null) srv.set(key, target.name);
|
|
101014
|
-
} else if (rec.type === A) {
|
|
101015
|
-
const ip = ipv4(rec.message, rec.dataOffset, rec.dataLength);
|
|
101016
|
-
if (ip !== null) addresses.set(key, ip);
|
|
101017
|
-
}
|
|
101018
|
-
}
|
|
101749
|
+
/** Gateways among the engine's mDNS hits (a `VELUX_KLF…` instance), one per host. */
|
|
101750
|
+
function veluxGatewaysFromHits(hits) {
|
|
101019
101751
|
const out = [];
|
|
101020
101752
|
const seen = /* @__PURE__ */ new Set();
|
|
101021
|
-
for (const
|
|
101022
|
-
const
|
|
101023
|
-
|
|
101024
|
-
if (host ===
|
|
101025
|
-
seen.add(host);
|
|
101026
|
-
const
|
|
101027
|
-
const id = veluxGatewayId({
|
|
101028
|
-
name: label,
|
|
101029
|
-
host,
|
|
101030
|
-
...viaSrv !== void 0 ? { serviceHost: viaSrv } : {}
|
|
101031
|
-
});
|
|
101753
|
+
for (const hit of hits) {
|
|
101754
|
+
const mdns = hit.mdns;
|
|
101755
|
+
if (mdns === void 0 || !isVeluxGatewayName(mdns.instance)) continue;
|
|
101756
|
+
if (hit.host.length === 0 || seen.has(hit.host)) continue;
|
|
101757
|
+
seen.add(hit.host);
|
|
101758
|
+
const name = mdns.instance.split("._")[0] ?? mdns.instance;
|
|
101032
101759
|
out.push({
|
|
101033
|
-
host,
|
|
101034
|
-
name
|
|
101035
|
-
id
|
|
101760
|
+
host: hit.host,
|
|
101761
|
+
name,
|
|
101762
|
+
id: veluxGatewayId({
|
|
101763
|
+
name,
|
|
101764
|
+
host: hit.host,
|
|
101765
|
+
serviceHost: mdns.host
|
|
101766
|
+
})
|
|
101036
101767
|
});
|
|
101037
101768
|
}
|
|
101038
101769
|
return out;
|
|
@@ -101052,11 +101783,10 @@ function proposeVeluxGateways(found, existingHosts) {
|
|
|
101052
101783
|
function veluxDiscoveryCandidates(proposals) {
|
|
101053
101784
|
return proposals.map((found) => ({
|
|
101054
101785
|
stableId: `velux:${found.id}`,
|
|
101786
|
+
type: DeviceType.Gateway,
|
|
101055
101787
|
suggestedName: found.name,
|
|
101056
|
-
prefilledConfig: {
|
|
101057
|
-
|
|
101058
|
-
[VELUX_SETUP_INTEGRATION_KEY]: true
|
|
101059
|
-
}
|
|
101788
|
+
prefilledConfig: { host: found.host },
|
|
101789
|
+
setupIntegration: true
|
|
101060
101790
|
}));
|
|
101061
101791
|
}
|
|
101062
101792
|
function veluxDiscoverySummary(foundCount, proposedCount) {
|
|
@@ -101064,101 +101794,6 @@ function veluxDiscoverySummary(foundCount, proposedCount) {
|
|
|
101064
101794
|
if (proposedCount === 0) return "No gateway found on the network.";
|
|
101065
101795
|
return `Found ${proposedCount} ${proposedCount === 1 ? "gateway" : "gateways"}.`;
|
|
101066
101796
|
}
|
|
101067
|
-
/** Gateways advertised as a KLF-200 (`VELUX_KLF…` instance) on one mDNS message. */
|
|
101068
|
-
function veluxGatewaysFromDnsMessage(buf) {
|
|
101069
|
-
if (buf.length < 12) return [];
|
|
101070
|
-
const questions = buf.readUInt16BE(4);
|
|
101071
|
-
const answers = buf.readUInt16BE(6);
|
|
101072
|
-
const authorities = buf.readUInt16BE(8);
|
|
101073
|
-
const extra = buf.readUInt16BE(10);
|
|
101074
|
-
let pos = 12;
|
|
101075
|
-
for (let i = 0; i < questions; i++) {
|
|
101076
|
-
const name = readName(buf, pos);
|
|
101077
|
-
if (name === null) return [];
|
|
101078
|
-
pos = name.end + 4;
|
|
101079
|
-
if (pos > buf.length) return [];
|
|
101080
|
-
}
|
|
101081
|
-
const answerRecords = readRecords(buf, pos, answers);
|
|
101082
|
-
if (answerRecords === null) return [];
|
|
101083
|
-
const authorityRecords = readRecords(buf, answerRecords.next, authorities);
|
|
101084
|
-
if (authorityRecords === null) return [];
|
|
101085
|
-
const extraRecords = readRecords(buf, authorityRecords.next, extra);
|
|
101086
|
-
if (extraRecords === null) return [];
|
|
101087
|
-
return gatewaysFromRecords([
|
|
101088
|
-
...answerRecords.records,
|
|
101089
|
-
...authorityRecords.records,
|
|
101090
|
-
...extraRecords.records
|
|
101091
|
-
]);
|
|
101092
|
-
}
|
|
101093
|
-
function mdnsPtrQuery(service) {
|
|
101094
|
-
const encoded = service.split(".").filter((part) => part.length > 0).map((label) => Buffer.concat([Buffer.from([label.length]), Buffer.from(label)]));
|
|
101095
|
-
const header = Buffer.alloc(12);
|
|
101096
|
-
header.writeUInt16BE(1, 4);
|
|
101097
|
-
const tail = Buffer.alloc(4);
|
|
101098
|
-
tail.writeUInt16BE(PTR, 0);
|
|
101099
|
-
tail.writeUInt16BE(MDNS_QU_CLASS, 2);
|
|
101100
|
-
return Buffer.concat([
|
|
101101
|
-
header,
|
|
101102
|
-
...encoded,
|
|
101103
|
-
Buffer.from([0]),
|
|
101104
|
-
tail
|
|
101105
|
-
]);
|
|
101106
|
-
}
|
|
101107
|
-
function closeQuietly(socket) {
|
|
101108
|
-
try {
|
|
101109
|
-
socket.close();
|
|
101110
|
-
} catch {}
|
|
101111
|
-
}
|
|
101112
|
-
function exchangeMdns(query, timeoutMs) {
|
|
101113
|
-
return new Promise((resolve) => {
|
|
101114
|
-
let socket;
|
|
101115
|
-
try {
|
|
101116
|
-
socket = createSocket({
|
|
101117
|
-
type: "udp4",
|
|
101118
|
-
reuseAddr: true
|
|
101119
|
-
});
|
|
101120
|
-
} catch {
|
|
101121
|
-
resolve([]);
|
|
101122
|
-
return;
|
|
101123
|
-
}
|
|
101124
|
-
const packets = [];
|
|
101125
|
-
let settled = false;
|
|
101126
|
-
const finish = () => {
|
|
101127
|
-
if (settled) return;
|
|
101128
|
-
settled = true;
|
|
101129
|
-
closeQuietly(socket);
|
|
101130
|
-
resolve(packets);
|
|
101131
|
-
};
|
|
101132
|
-
socket.on("error", () => finish());
|
|
101133
|
-
socket.on("message", (msg) => {
|
|
101134
|
-
packets.push(Buffer.from(msg));
|
|
101135
|
-
});
|
|
101136
|
-
socket.bind(0, () => {
|
|
101137
|
-
try {
|
|
101138
|
-
socket.send(query, MDNS_PORT, MDNS_ADDRESS);
|
|
101139
|
-
} catch {
|
|
101140
|
-
finish();
|
|
101141
|
-
}
|
|
101142
|
-
});
|
|
101143
|
-
setTimeout(finish, timeoutMs);
|
|
101144
|
-
});
|
|
101145
|
-
}
|
|
101146
|
-
async function browseVeluxGateways(exchange = exchangeMdns) {
|
|
101147
|
-
let packets;
|
|
101148
|
-
try {
|
|
101149
|
-
packets = await exchange(mdnsPtrQuery(VELUX_MDNS_SERVICE), VELUX_DISCOVERY_TIMEOUT_MS);
|
|
101150
|
-
} catch {
|
|
101151
|
-
return [];
|
|
101152
|
-
}
|
|
101153
|
-
const hosts = [];
|
|
101154
|
-
const seen = /* @__PURE__ */ new Set();
|
|
101155
|
-
for (const packet of packets) for (const found of veluxGatewaysFromDnsMessage(packet)) {
|
|
101156
|
-
if (seen.has(found.host)) continue;
|
|
101157
|
-
seen.add(found.host);
|
|
101158
|
-
hosts.push(found);
|
|
101159
|
-
}
|
|
101160
|
-
return hosts;
|
|
101161
|
-
}
|
|
101162
101797
|
//#endregion
|
|
101163
101798
|
//#region src/addon.ts
|
|
101164
101799
|
/**
|
|
@@ -101213,14 +101848,15 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
101213
101848
|
],
|
|
101214
101849
|
customActions: veluxActions,
|
|
101215
101850
|
actionHandlers: { discoverGateway: async () => {
|
|
101216
|
-
const {
|
|
101851
|
+
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
101852
|
+
const hosts = candidates.map((candidate) => ({
|
|
101853
|
+
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
101854
|
+
name: candidate.suggestedName,
|
|
101855
|
+
id: candidate.stableId.replace(/^velux:/, "")
|
|
101856
|
+
}));
|
|
101217
101857
|
return {
|
|
101218
|
-
hosts
|
|
101219
|
-
|
|
101220
|
-
name: found.name,
|
|
101221
|
-
id: found.id
|
|
101222
|
-
})),
|
|
101223
|
-
summary
|
|
101858
|
+
hosts,
|
|
101859
|
+
summary: veluxDiscoverySummary(hosts.length, hosts.length)
|
|
101224
101860
|
};
|
|
101225
101861
|
} }
|
|
101226
101862
|
};
|
|
@@ -101241,21 +101877,21 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
101241
101877
|
async supportsManualCreation() {
|
|
101242
101878
|
return false;
|
|
101243
101879
|
}
|
|
101244
|
-
/**
|
|
101245
|
-
* LAN mDNS probe for a KLF-200. This addon is hub-only, so the socket is the
|
|
101246
|
-
* hub's. The probe asks for a unicast reply and does not bind 5353.
|
|
101247
|
-
*/
|
|
101880
|
+
/** A KLF-200 is found by the scan engine's mDNS browse; this addon only classifies the hits. */
|
|
101248
101881
|
async supportsDiscovery() {
|
|
101249
101882
|
return true;
|
|
101250
101883
|
}
|
|
101251
|
-
async
|
|
101252
|
-
|
|
101253
|
-
|
|
101254
|
-
|
|
101255
|
-
|
|
101256
|
-
|
|
101257
|
-
|
|
101258
|
-
|
|
101884
|
+
async getDiscoveryModes() {
|
|
101885
|
+
return VELUX_DISCOVERY_MODES;
|
|
101886
|
+
}
|
|
101887
|
+
async classifyDiscoveryHits(input) {
|
|
101888
|
+
const found = veluxGatewaysFromHits(input.hits);
|
|
101889
|
+
const proposals = proposeVeluxGateways(found, await this.knownGatewayHosts());
|
|
101890
|
+
if (found.length > proposals.length) this.ctx.logger.info("Velux discovery skipped gateways already added", { meta: {
|
|
101891
|
+
found: found.length,
|
|
101892
|
+
proposed: proposals.length
|
|
101893
|
+
} });
|
|
101894
|
+
return veluxDiscoveryCandidates(proposals);
|
|
101259
101895
|
}
|
|
101260
101896
|
async adoptDiscoveredDevice(_input) {
|
|
101261
101897
|
throw new Error("A Velux gateway is added from the integration form");
|
|
@@ -101272,23 +101908,6 @@ var VeluxProviderAddon = class extends BaseDeviceProvider {
|
|
|
101272
101908
|
if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("provider-velux generateStableId: nativeId required");
|
|
101273
101909
|
return `velux:${nativeId}`;
|
|
101274
101910
|
}
|
|
101275
|
-
async discoverGateways() {
|
|
101276
|
-
let found = [];
|
|
101277
|
-
try {
|
|
101278
|
-
found = await browseVeluxGateways();
|
|
101279
|
-
} catch (err) {
|
|
101280
|
-
this.ctx.logger.warn("Velux gateway discovery failed", { meta: { error: errMsg(err) } });
|
|
101281
|
-
}
|
|
101282
|
-
const proposals = proposeVeluxGateways(found, await this.knownGatewayHosts());
|
|
101283
|
-
if (found.length > proposals.length) this.ctx.logger.info("Velux discovery skipped gateways already added", { meta: {
|
|
101284
|
-
found: found.length,
|
|
101285
|
-
proposed: proposals.length
|
|
101286
|
-
} });
|
|
101287
|
-
return {
|
|
101288
|
-
proposals,
|
|
101289
|
-
summary: veluxDiscoverySummary(found.length, proposals.length)
|
|
101290
|
-
};
|
|
101291
|
-
}
|
|
101292
101911
|
async knownGatewayHosts() {
|
|
101293
101912
|
try {
|
|
101294
101913
|
const all = await this.ctx.api.integrations.list.query();
|