@camstack/addon-provider-homematic 1.2.163 → 1.2.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/addon.js +1921 -372
- package/dist/addon.mjs +1919 -370
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -5464,7 +5464,7 @@ var ZodIssueCode = {
|
|
|
5464
5464
|
var ZodFirstPartyTypeKind;
|
|
5465
5465
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5466
5466
|
//#endregion
|
|
5467
|
-
//#region ../types/dist/sleep-
|
|
5467
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5468
5468
|
/**
|
|
5469
5469
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5470
5470
|
* window to float samples (D455).
|
|
@@ -7531,6 +7531,7 @@ var BaseAddon = class {
|
|
|
7531
7531
|
* a warning and falls back rather than crashing boot.
|
|
7532
7532
|
*/
|
|
7533
7533
|
state(key, schema, fallback) {
|
|
7534
|
+
this._declaredStateKeys.add(key);
|
|
7534
7535
|
return createDurableState({
|
|
7535
7536
|
key,
|
|
7536
7537
|
schema,
|
|
@@ -7546,6 +7547,96 @@ var BaseAddon = class {
|
|
|
7546
7547
|
} })
|
|
7547
7548
|
});
|
|
7548
7549
|
}
|
|
7550
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7551
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7552
|
+
/**
|
|
7553
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7554
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7555
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7556
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7557
|
+
*/
|
|
7558
|
+
async getDeclaredSettingsKeys() {
|
|
7559
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7560
|
+
}
|
|
7561
|
+
declaredKeySet() {
|
|
7562
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7563
|
+
const schema = this.globalSettingsSchema();
|
|
7564
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7565
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7566
|
+
for (const path of this.secretStatePaths) {
|
|
7567
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7568
|
+
if (root) keys.add(root);
|
|
7569
|
+
}
|
|
7570
|
+
return keys;
|
|
7571
|
+
}
|
|
7572
|
+
/**
|
|
7573
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7574
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7575
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7576
|
+
*/
|
|
7577
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7578
|
+
const settings = this._ctx?.settings;
|
|
7579
|
+
const raw = this.rawSettings();
|
|
7580
|
+
if (!settings || !raw) return {
|
|
7581
|
+
removed: [],
|
|
7582
|
+
refused: keys.map((key) => ({
|
|
7583
|
+
key,
|
|
7584
|
+
reason: "no settings store"
|
|
7585
|
+
})),
|
|
7586
|
+
secretsCleared: 0
|
|
7587
|
+
};
|
|
7588
|
+
let stored;
|
|
7589
|
+
try {
|
|
7590
|
+
stored = await raw.readAddonStore();
|
|
7591
|
+
} catch (error) {
|
|
7592
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7593
|
+
return {
|
|
7594
|
+
removed: [],
|
|
7595
|
+
refused: keys.map((key) => ({
|
|
7596
|
+
key,
|
|
7597
|
+
reason
|
|
7598
|
+
})),
|
|
7599
|
+
secretsCleared: 0
|
|
7600
|
+
};
|
|
7601
|
+
}
|
|
7602
|
+
const declared = this.declaredKeySet();
|
|
7603
|
+
const removed = [];
|
|
7604
|
+
const refused = [];
|
|
7605
|
+
for (const key of keys) {
|
|
7606
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7607
|
+
if (declared.has(base)) refused.push({
|
|
7608
|
+
key,
|
|
7609
|
+
reason: "still declared by the addon"
|
|
7610
|
+
});
|
|
7611
|
+
else if (!(key in stored)) refused.push({
|
|
7612
|
+
key,
|
|
7613
|
+
reason: "not stored"
|
|
7614
|
+
});
|
|
7615
|
+
else removed.push(key);
|
|
7616
|
+
}
|
|
7617
|
+
if (removed.length === 0) return {
|
|
7618
|
+
removed,
|
|
7619
|
+
refused,
|
|
7620
|
+
secretsCleared: 0
|
|
7621
|
+
};
|
|
7622
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7623
|
+
const held = /* @__PURE__ */ new Set();
|
|
7624
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7625
|
+
const target = removed.includes(key) ? held : kept;
|
|
7626
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7627
|
+
}
|
|
7628
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7629
|
+
const patch = {};
|
|
7630
|
+
for (const key of removed) patch[key] = void 0;
|
|
7631
|
+
await settings.writeAddonStore(patch);
|
|
7632
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7633
|
+
await this.resolveConfig();
|
|
7634
|
+
return {
|
|
7635
|
+
removed,
|
|
7636
|
+
refused,
|
|
7637
|
+
secretsCleared
|
|
7638
|
+
};
|
|
7639
|
+
}
|
|
7549
7640
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7550
7641
|
deviceState(deviceId, key, schema, fallback) {
|
|
7551
7642
|
return createDurableState({
|
|
@@ -7816,6 +7907,21 @@ function dropPerNodeFields(fields) {
|
|
|
7816
7907
|
}
|
|
7817
7908
|
return kept;
|
|
7818
7909
|
}
|
|
7910
|
+
function collectFieldKeys(fields) {
|
|
7911
|
+
const collected = [];
|
|
7912
|
+
for (const field of fields) {
|
|
7913
|
+
if (field.type === "group") {
|
|
7914
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
7915
|
+
continue;
|
|
7916
|
+
}
|
|
7917
|
+
if (field.type === "sub-tabs") {
|
|
7918
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
7919
|
+
continue;
|
|
7920
|
+
}
|
|
7921
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
7922
|
+
}
|
|
7923
|
+
return collected;
|
|
7924
|
+
}
|
|
7819
7925
|
function collectPerNodeFieldKeys(fields) {
|
|
7820
7926
|
const collected = [];
|
|
7821
7927
|
for (const field of fields) {
|
|
@@ -16165,8 +16271,172 @@ object({
|
|
|
16165
16271
|
quantity: ReadingQuantitySchema,
|
|
16166
16272
|
unit: string().min(1)
|
|
16167
16273
|
});
|
|
16168
|
-
|
|
16169
|
-
|
|
16274
|
+
new Set([
|
|
16275
|
+
{
|
|
16276
|
+
id: "person",
|
|
16277
|
+
name: "Person"
|
|
16278
|
+
},
|
|
16279
|
+
{
|
|
16280
|
+
id: "vehicle",
|
|
16281
|
+
name: "Vehicle"
|
|
16282
|
+
},
|
|
16283
|
+
{
|
|
16284
|
+
id: "animal",
|
|
16285
|
+
name: "Animal"
|
|
16286
|
+
},
|
|
16287
|
+
{
|
|
16288
|
+
id: "package",
|
|
16289
|
+
name: "Package"
|
|
16290
|
+
}
|
|
16291
|
+
].map((l) => l.id));
|
|
16292
|
+
var COCO_TO_MACRO = {
|
|
16293
|
+
mapping: {
|
|
16294
|
+
person: "person",
|
|
16295
|
+
bicycle: "vehicle",
|
|
16296
|
+
car: "vehicle",
|
|
16297
|
+
motorcycle: "vehicle",
|
|
16298
|
+
airplane: "vehicle",
|
|
16299
|
+
bus: "vehicle",
|
|
16300
|
+
train: "vehicle",
|
|
16301
|
+
truck: "vehicle",
|
|
16302
|
+
boat: "vehicle",
|
|
16303
|
+
bird: "animal",
|
|
16304
|
+
cat: "animal",
|
|
16305
|
+
dog: "animal",
|
|
16306
|
+
horse: "animal",
|
|
16307
|
+
sheep: "animal",
|
|
16308
|
+
cow: "animal",
|
|
16309
|
+
elephant: "animal",
|
|
16310
|
+
bear: "animal",
|
|
16311
|
+
zebra: "animal",
|
|
16312
|
+
giraffe: "animal",
|
|
16313
|
+
suitcase: "package",
|
|
16314
|
+
backpack: "package",
|
|
16315
|
+
handbag: "package"
|
|
16316
|
+
},
|
|
16317
|
+
preserveOriginal: false
|
|
16318
|
+
};
|
|
16319
|
+
/**
|
|
16320
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16321
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16322
|
+
*
|
|
16323
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16324
|
+
* second list. The label that fits depends on the macro class:
|
|
16325
|
+
*
|
|
16326
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16327
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16328
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16329
|
+
* - `package` → a sub-class.
|
|
16330
|
+
*
|
|
16331
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16332
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16333
|
+
*
|
|
16334
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16335
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16336
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16337
|
+
* the subject GETS it — not at the first detection.
|
|
16338
|
+
*/
|
|
16339
|
+
var SubClassLabelSchema = object({
|
|
16340
|
+
kind: literal("sub-class"),
|
|
16341
|
+
className: string().min(1)
|
|
16342
|
+
});
|
|
16343
|
+
var IdentityLabelSchema = object({
|
|
16344
|
+
kind: literal("identity"),
|
|
16345
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16346
|
+
identityId: string().min(1),
|
|
16347
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16348
|
+
name: string().min(1)
|
|
16349
|
+
});
|
|
16350
|
+
var PlateLabelSchema = object({
|
|
16351
|
+
kind: literal("plate"),
|
|
16352
|
+
text: string().min(1)
|
|
16353
|
+
});
|
|
16354
|
+
function macroOf$1(className) {
|
|
16355
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16356
|
+
}
|
|
16357
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16358
|
+
function subClassBelongs(macro, label) {
|
|
16359
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16360
|
+
}
|
|
16361
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16362
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16363
|
+
object({
|
|
16364
|
+
macro: literal("animal"),
|
|
16365
|
+
label: subClassOf("animal").optional()
|
|
16366
|
+
}),
|
|
16367
|
+
object({
|
|
16368
|
+
macro: literal("person"),
|
|
16369
|
+
label: IdentityLabelSchema.optional()
|
|
16370
|
+
}),
|
|
16371
|
+
object({
|
|
16372
|
+
macro: literal("vehicle"),
|
|
16373
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16374
|
+
}),
|
|
16375
|
+
object({
|
|
16376
|
+
macro: literal("package"),
|
|
16377
|
+
label: subClassOf("package").optional()
|
|
16378
|
+
}),
|
|
16379
|
+
object({
|
|
16380
|
+
macro: literal("other"),
|
|
16381
|
+
className: string().min(1)
|
|
16382
|
+
})
|
|
16383
|
+
]);
|
|
16384
|
+
/**
|
|
16385
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16386
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16387
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16388
|
+
*/
|
|
16389
|
+
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" });
|
|
16390
|
+
/**
|
|
16391
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16392
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16393
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16394
|
+
*/
|
|
16395
|
+
function subjectFromClassName(className) {
|
|
16396
|
+
const name = className.trim().toLowerCase();
|
|
16397
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16398
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16399
|
+
if (macro === void 0) return {
|
|
16400
|
+
macro: "other",
|
|
16401
|
+
className
|
|
16402
|
+
};
|
|
16403
|
+
const label = {
|
|
16404
|
+
kind: "sub-class",
|
|
16405
|
+
className: name
|
|
16406
|
+
};
|
|
16407
|
+
switch (macro) {
|
|
16408
|
+
case "animal": return {
|
|
16409
|
+
macro: "animal",
|
|
16410
|
+
label
|
|
16411
|
+
};
|
|
16412
|
+
case "vehicle": return {
|
|
16413
|
+
macro: "vehicle",
|
|
16414
|
+
label
|
|
16415
|
+
};
|
|
16416
|
+
case "package": return {
|
|
16417
|
+
macro: "package",
|
|
16418
|
+
label
|
|
16419
|
+
};
|
|
16420
|
+
default: return {
|
|
16421
|
+
macro: "other",
|
|
16422
|
+
className
|
|
16423
|
+
};
|
|
16424
|
+
}
|
|
16425
|
+
}
|
|
16426
|
+
function macroOnly(macro) {
|
|
16427
|
+
switch (macro) {
|
|
16428
|
+
case "animal": return { macro: "animal" };
|
|
16429
|
+
case "person": return { macro: "person" };
|
|
16430
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16431
|
+
case "package": return { macro: "package" };
|
|
16432
|
+
default: return {
|
|
16433
|
+
macro: "other",
|
|
16434
|
+
className: macro
|
|
16435
|
+
};
|
|
16436
|
+
}
|
|
16437
|
+
}
|
|
16438
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16439
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16170
16440
|
var AutomationConditionOperatorSchema = _enum([
|
|
16171
16441
|
"eq",
|
|
16172
16442
|
"ne",
|
|
@@ -16177,12 +16447,76 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
16177
16447
|
"contains",
|
|
16178
16448
|
"in"
|
|
16179
16449
|
]);
|
|
16180
|
-
|
|
16181
|
-
|
|
16450
|
+
/**
|
|
16451
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16452
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16453
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16454
|
+
* fire; it does not lose it.
|
|
16455
|
+
*/
|
|
16456
|
+
var DeviceBecomesLeafSchema = object({
|
|
16457
|
+
kind: literal("device-becomes"),
|
|
16458
|
+
deviceId: number().int().nonnegative(),
|
|
16459
|
+
cap: string().min(1),
|
|
16460
|
+
fieldPath: string().min(1),
|
|
16461
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16462
|
+
becomes: union([
|
|
16463
|
+
string(),
|
|
16464
|
+
number(),
|
|
16465
|
+
boolean()
|
|
16466
|
+
]).optional(),
|
|
16467
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16468
|
+
edge: boolean().optional(),
|
|
16469
|
+
/** The new value must hold this long before it counts. */
|
|
16470
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
16471
|
+
/** Collapse a burst into one occurrence. */
|
|
16472
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16473
|
+
});
|
|
16474
|
+
var DeviceEventLeafSchema = object({
|
|
16475
|
+
kind: literal("device-event"),
|
|
16476
|
+
/** An `EventCategory` value. */
|
|
16477
|
+
category: string().min(1),
|
|
16478
|
+
deviceId: number().int().nonnegative().optional()
|
|
16479
|
+
});
|
|
16480
|
+
var ScheduleLeafSchema = object({
|
|
16481
|
+
kind: literal("schedule"),
|
|
16482
|
+
cron: string().min(1).max(120)
|
|
16483
|
+
});
|
|
16484
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16485
|
+
var IntervalLeafSchema = object({
|
|
16486
|
+
kind: literal("interval"),
|
|
16487
|
+
minutes: number().int().min(1).max(1440)
|
|
16488
|
+
});
|
|
16489
|
+
var DetectionLeafSchema = object({
|
|
16490
|
+
kind: literal("detection"),
|
|
16491
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
16492
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
16493
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16494
|
+
/** The track must have visited at least one of these zones. */
|
|
16495
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16496
|
+
/** Best-so-far confidence the track must reach. */
|
|
16497
|
+
minScore: number().min(0).max(1).optional()
|
|
16498
|
+
});
|
|
16499
|
+
var StationaryLeafSchema = object({
|
|
16500
|
+
kind: literal("stationary"),
|
|
16501
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16502
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16503
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
16504
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
16505
|
+
phase: _enum(["appeared", "departed"])
|
|
16506
|
+
});
|
|
16507
|
+
/** "is": one device field compared to one value, NOW. */
|
|
16508
|
+
var DeviceIsLeafSchema = object({
|
|
16509
|
+
kind: literal("device-is"),
|
|
16182
16510
|
deviceId: number().int().nonnegative(),
|
|
16183
16511
|
cap: string().min(1),
|
|
16184
16512
|
fieldPath: string().min(1),
|
|
16185
16513
|
operator: AutomationConditionOperatorSchema,
|
|
16514
|
+
/**
|
|
16515
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
16516
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
16517
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
16518
|
+
* one is never evaluated (it fails).
|
|
16519
|
+
*/
|
|
16186
16520
|
value: union([
|
|
16187
16521
|
string(),
|
|
16188
16522
|
number(),
|
|
@@ -16190,24 +16524,26 @@ var AutomationConditionLeafSchema = object({
|
|
|
16190
16524
|
array(union([string(), number()])),
|
|
16191
16525
|
QuantityValueSchema
|
|
16192
16526
|
]),
|
|
16527
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
16193
16528
|
unit: string().min(1).optional()
|
|
16194
16529
|
});
|
|
16195
|
-
|
|
16530
|
+
/**
|
|
16531
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
16532
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
16533
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
16534
|
+
*/
|
|
16535
|
+
var StationaryPresentLeafSchema = object({
|
|
16196
16536
|
kind: literal("stationary-present"),
|
|
16197
16537
|
deviceId: number().int().nonnegative(),
|
|
16198
|
-
|
|
16199
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16200
|
-
/** The parked object must be in at least one of these zones. */
|
|
16538
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
16201
16539
|
zones: array(string().min(1)).min(1).optional()
|
|
16202
16540
|
});
|
|
16203
16541
|
/**
|
|
16204
|
-
* The
|
|
16205
|
-
*
|
|
16206
|
-
*
|
|
16207
|
-
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
16208
|
-
* cannot drift — an expression that one accepts, the other accepts.
|
|
16542
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
16543
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
16544
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
16209
16545
|
*/
|
|
16210
|
-
var
|
|
16546
|
+
var ExpressionLeafSchema = object({
|
|
16211
16547
|
kind: literal("expression"),
|
|
16212
16548
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16213
16549
|
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
@@ -16219,55 +16555,62 @@ var AutomationConditionExpressionSchema = object({
|
|
|
16219
16555
|
path: ["expr"]
|
|
16220
16556
|
});
|
|
16221
16557
|
});
|
|
16222
|
-
var
|
|
16558
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
16223
16559
|
object({
|
|
16224
16560
|
kind: literal("all"),
|
|
16225
|
-
children: array(
|
|
16561
|
+
children: array(AutomationWhenSchema)
|
|
16226
16562
|
}),
|
|
16227
16563
|
object({
|
|
16228
16564
|
kind: literal("any"),
|
|
16229
|
-
children: array(
|
|
16565
|
+
children: array(AutomationWhenSchema)
|
|
16230
16566
|
}),
|
|
16231
16567
|
object({
|
|
16232
16568
|
kind: literal("not"),
|
|
16233
|
-
child:
|
|
16569
|
+
child: AutomationWhenSchema
|
|
16234
16570
|
}),
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16571
|
+
DeviceBecomesLeafSchema,
|
|
16572
|
+
DeviceEventLeafSchema,
|
|
16573
|
+
ScheduleLeafSchema,
|
|
16574
|
+
ManualLeafSchema,
|
|
16575
|
+
IntervalLeafSchema,
|
|
16576
|
+
DetectionLeafSchema,
|
|
16577
|
+
StationaryLeafSchema,
|
|
16578
|
+
DeviceIsLeafSchema,
|
|
16579
|
+
StationaryPresentLeafSchema,
|
|
16580
|
+
ExpressionLeafSchema
|
|
16238
16581
|
]));
|
|
16239
16582
|
/**
|
|
16240
|
-
*
|
|
16241
|
-
*
|
|
16242
|
-
*
|
|
16243
|
-
*
|
|
16244
|
-
*
|
|
16245
|
-
*
|
|
16246
|
-
*
|
|
16247
|
-
*
|
|
16248
|
-
|
|
16249
|
-
|
|
16583
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
16584
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
16585
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
16586
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
16587
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
16588
|
+
* way. Nothing writes this shape any more.
|
|
16589
|
+
*
|
|
16590
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
16591
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
16592
|
+
* a single trigger is not wrapped in `any`.
|
|
16593
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
16594
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
16595
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
16596
|
+
*/
|
|
16597
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
16250
16598
|
object({
|
|
16251
16599
|
kind: literal("device-state"),
|
|
16252
16600
|
deviceId: number().int().nonnegative(),
|
|
16253
16601
|
cap: string().min(1),
|
|
16254
16602
|
fieldPath: string().min(1),
|
|
16255
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16256
16603
|
becomes: union([
|
|
16257
16604
|
string(),
|
|
16258
16605
|
number(),
|
|
16259
16606
|
boolean()
|
|
16260
16607
|
]).optional(),
|
|
16261
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16262
16608
|
edge: boolean().optional(),
|
|
16263
|
-
/** The condition must hold this long before the run starts. */
|
|
16264
16609
|
forMs: number().int().min(0).max(864e5).optional(),
|
|
16265
|
-
/** Collapse a burst into one run. */
|
|
16266
16610
|
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16267
16611
|
}),
|
|
16268
16612
|
object({
|
|
16269
16613
|
kind: literal("device-event"),
|
|
16270
|
-
/** An `EventCategory` value. */
|
|
16271
16614
|
category: string().min(1),
|
|
16272
16615
|
deviceId: number().int().nonnegative().optional()
|
|
16273
16616
|
}),
|
|
@@ -16278,25 +16621,16 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16278
16621
|
object({ kind: literal("manual") }),
|
|
16279
16622
|
object({
|
|
16280
16623
|
kind: literal("detection"),
|
|
16281
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16282
|
-
* matches every raw class that maps to it; any other name matches
|
|
16283
|
-
* exactly. */
|
|
16284
16624
|
classes: array(string().min(1)).min(1),
|
|
16285
16625
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16286
|
-
/** The track must have visited at least one of these zones. */
|
|
16287
16626
|
zones: array(string().min(1)).min(1).optional(),
|
|
16288
|
-
/** Best-so-far confidence the track must reach. */
|
|
16289
16627
|
minScore: number().min(0).max(1).optional()
|
|
16290
16628
|
}),
|
|
16291
16629
|
object({
|
|
16292
16630
|
kind: literal("stationary"),
|
|
16293
16631
|
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16294
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16295
16632
|
classes: array(string().min(1)).min(1).optional(),
|
|
16296
|
-
/** The parked object must be in at least one of these zones. */
|
|
16297
16633
|
zones: array(string().min(1)).min(1).optional(),
|
|
16298
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16299
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16300
16634
|
phase: _enum(["appeared", "departed"])
|
|
16301
16635
|
}),
|
|
16302
16636
|
object({
|
|
@@ -16304,6 +16638,143 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
16304
16638
|
minutes: number().int().min(1).max(1440)
|
|
16305
16639
|
})
|
|
16306
16640
|
]);
|
|
16641
|
+
var LegacyConditionLeafSchema = object({
|
|
16642
|
+
kind: literal("condition"),
|
|
16643
|
+
deviceId: number().int().nonnegative(),
|
|
16644
|
+
cap: string().min(1),
|
|
16645
|
+
fieldPath: string().min(1),
|
|
16646
|
+
operator: AutomationConditionOperatorSchema,
|
|
16647
|
+
value: union([
|
|
16648
|
+
string(),
|
|
16649
|
+
number(),
|
|
16650
|
+
boolean(),
|
|
16651
|
+
array(union([string(), number()])),
|
|
16652
|
+
QuantityValueSchema
|
|
16653
|
+
]),
|
|
16654
|
+
unit: string().min(1).optional()
|
|
16655
|
+
});
|
|
16656
|
+
var LegacyStationaryPresentSchema = object({
|
|
16657
|
+
kind: literal("stationary-present"),
|
|
16658
|
+
deviceId: number().int().nonnegative(),
|
|
16659
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
16660
|
+
zones: array(string().min(1)).min(1).optional()
|
|
16661
|
+
});
|
|
16662
|
+
var LegacyExpressionSchema = object({
|
|
16663
|
+
kind: literal("expression"),
|
|
16664
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
16665
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
16666
|
+
}).superRefine((src, ctx) => {
|
|
16667
|
+
const err = validateExpressionSource(src);
|
|
16668
|
+
if (err !== null) ctx.addIssue({
|
|
16669
|
+
code: "custom",
|
|
16670
|
+
message: err,
|
|
16671
|
+
path: ["expr"]
|
|
16672
|
+
});
|
|
16673
|
+
});
|
|
16674
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
16675
|
+
object({
|
|
16676
|
+
kind: literal("all"),
|
|
16677
|
+
children: array(LegacyConditionSchema)
|
|
16678
|
+
}),
|
|
16679
|
+
object({
|
|
16680
|
+
kind: literal("any"),
|
|
16681
|
+
children: array(LegacyConditionSchema)
|
|
16682
|
+
}),
|
|
16683
|
+
object({
|
|
16684
|
+
kind: literal("not"),
|
|
16685
|
+
child: LegacyConditionSchema
|
|
16686
|
+
}),
|
|
16687
|
+
LegacyConditionLeafSchema,
|
|
16688
|
+
LegacyExpressionSchema,
|
|
16689
|
+
LegacyStationaryPresentSchema
|
|
16690
|
+
]));
|
|
16691
|
+
function convertTrigger(trigger) {
|
|
16692
|
+
switch (trigger.kind) {
|
|
16693
|
+
case "device-state": return {
|
|
16694
|
+
...trigger,
|
|
16695
|
+
kind: "device-becomes"
|
|
16696
|
+
};
|
|
16697
|
+
case "detection": {
|
|
16698
|
+
const { classes, ...rest } = trigger;
|
|
16699
|
+
return {
|
|
16700
|
+
...rest,
|
|
16701
|
+
subjects: classes.map(subjectFromClassName)
|
|
16702
|
+
};
|
|
16703
|
+
}
|
|
16704
|
+
case "stationary": {
|
|
16705
|
+
const { classes, ...rest } = trigger;
|
|
16706
|
+
return {
|
|
16707
|
+
...rest,
|
|
16708
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16709
|
+
};
|
|
16710
|
+
}
|
|
16711
|
+
default: return trigger;
|
|
16712
|
+
}
|
|
16713
|
+
}
|
|
16714
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
16715
|
+
function convertLegacyCondition(condition) {
|
|
16716
|
+
switch (condition.kind) {
|
|
16717
|
+
case "condition": return {
|
|
16718
|
+
...condition,
|
|
16719
|
+
kind: "device-is"
|
|
16720
|
+
};
|
|
16721
|
+
case "stationary-present": {
|
|
16722
|
+
const { classes, ...rest } = condition;
|
|
16723
|
+
return {
|
|
16724
|
+
...rest,
|
|
16725
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
16726
|
+
};
|
|
16727
|
+
}
|
|
16728
|
+
case "expression": return condition;
|
|
16729
|
+
case "not": return {
|
|
16730
|
+
kind: "not",
|
|
16731
|
+
child: convertLegacyCondition(condition.child)
|
|
16732
|
+
};
|
|
16733
|
+
case "all":
|
|
16734
|
+
case "any": return {
|
|
16735
|
+
kind: condition.kind,
|
|
16736
|
+
children: condition.children.map(convertLegacyCondition)
|
|
16737
|
+
};
|
|
16738
|
+
}
|
|
16739
|
+
}
|
|
16740
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
16741
|
+
function convertLegacyWhen(half) {
|
|
16742
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
16743
|
+
const first = leaves[0];
|
|
16744
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
16745
|
+
kind: "any",
|
|
16746
|
+
children: leaves
|
|
16747
|
+
};
|
|
16748
|
+
if (half.conditions === void 0) return triggers;
|
|
16749
|
+
return {
|
|
16750
|
+
kind: "all",
|
|
16751
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
16752
|
+
};
|
|
16753
|
+
}
|
|
16754
|
+
/**
|
|
16755
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
16756
|
+
* becomes code.
|
|
16757
|
+
*
|
|
16758
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
16759
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
16760
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
16761
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
16762
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
16763
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
16764
|
+
*
|
|
16765
|
+
* Two parts:
|
|
16766
|
+
*
|
|
16767
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
16768
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
16769
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
16770
|
+
* ADR "one tree".
|
|
16771
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
16772
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
16773
|
+
* this had to diverge from "verbatim", and why.
|
|
16774
|
+
*
|
|
16775
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
16776
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
16777
|
+
*/
|
|
16307
16778
|
/**
|
|
16308
16779
|
* One action step.
|
|
16309
16780
|
*
|
|
@@ -16406,69 +16877,49 @@ var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
|
16406
16877
|
minutes: number().int().min(1).max(1440)
|
|
16407
16878
|
})
|
|
16408
16879
|
]);
|
|
16409
|
-
var
|
|
16410
|
-
|
|
16411
|
-
conditions: AutomationConditionSchema.optional(),
|
|
16880
|
+
var CurrentAutomationRecipeSchema = object({
|
|
16881
|
+
when: AutomationWhenSchema,
|
|
16412
16882
|
actions: array(AutomationActionSchema),
|
|
16413
16883
|
runMode: AutomationRunModeSchema.optional(),
|
|
16414
16884
|
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
16415
16885
|
keepTraces: number().int().min(1).max(100).optional()
|
|
16416
16886
|
});
|
|
16417
|
-
|
|
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
|
-
bus: "vehicle",
|
|
16443
|
-
train: "vehicle",
|
|
16444
|
-
truck: "vehicle",
|
|
16445
|
-
boat: "vehicle",
|
|
16446
|
-
bird: "animal",
|
|
16447
|
-
cat: "animal",
|
|
16448
|
-
dog: "animal",
|
|
16449
|
-
horse: "animal",
|
|
16450
|
-
sheep: "animal",
|
|
16451
|
-
cow: "animal",
|
|
16452
|
-
elephant: "animal",
|
|
16453
|
-
bear: "animal",
|
|
16454
|
-
zebra: "animal",
|
|
16455
|
-
giraffe: "animal",
|
|
16456
|
-
suitcase: "package",
|
|
16457
|
-
backpack: "package",
|
|
16458
|
-
handbag: "package"
|
|
16459
|
-
},
|
|
16460
|
-
preserveOriginal: false
|
|
16461
|
-
};
|
|
16887
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
16888
|
+
var LegacyAutomationRecipeSchema = object({
|
|
16889
|
+
triggers: array(LegacyTriggerSchema),
|
|
16890
|
+
conditions: LegacyConditionSchema.optional(),
|
|
16891
|
+
actions: array(AutomationActionSchema),
|
|
16892
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
16893
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
16894
|
+
});
|
|
16895
|
+
/** The old shape as the new one, lossless. */
|
|
16896
|
+
function convertLegacyRecipe(legacy) {
|
|
16897
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
16898
|
+
return {
|
|
16899
|
+
when: convertLegacyWhen({
|
|
16900
|
+
triggers,
|
|
16901
|
+
conditions
|
|
16902
|
+
}),
|
|
16903
|
+
...rest
|
|
16904
|
+
};
|
|
16905
|
+
}
|
|
16906
|
+
/**
|
|
16907
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
16908
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
16909
|
+
* it. The output type is always the new one.
|
|
16910
|
+
*/
|
|
16911
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16462
16912
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16463
16913
|
/**
|
|
16464
16914
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16465
16915
|
* so the editor can paint every block green or red with its reason.
|
|
16466
16916
|
*
|
|
16467
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16468
|
-
* every
|
|
16469
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16470
|
-
* each addressed by a stable path (`
|
|
16471
|
-
* for an Action run).
|
|
16917
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
16918
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
16919
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
16920
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
16921
|
+
* for an Action run). Traces written before the single tree carry
|
|
16922
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16472
16923
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16473
16924
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16474
16925
|
*
|
|
@@ -16500,6 +16951,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16500
16951
|
]);
|
|
16501
16952
|
var AutomationTriggerTraceSchema = object({
|
|
16502
16953
|
kind: string().min(1),
|
|
16954
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
16955
|
+
path: string().min(1).optional(),
|
|
16503
16956
|
outcome: _enum(["pass", "fail"]),
|
|
16504
16957
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16505
16958
|
reason: string(),
|
|
@@ -16507,15 +16960,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16507
16960
|
vars: record(string(), TraceValueSchema)
|
|
16508
16961
|
});
|
|
16509
16962
|
var AutomationConditionTraceNodeSchema = object({
|
|
16510
|
-
/** `
|
|
16963
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16511
16964
|
path: string().min(1),
|
|
16512
16965
|
kind: _enum([
|
|
16513
16966
|
"all",
|
|
16514
16967
|
"any",
|
|
16515
16968
|
"not",
|
|
16516
16969
|
"condition",
|
|
16970
|
+
"device-is",
|
|
16517
16971
|
"expression",
|
|
16518
|
-
"stationary-present"
|
|
16972
|
+
"stationary-present",
|
|
16973
|
+
"device-becomes",
|
|
16974
|
+
"device-event",
|
|
16975
|
+
"schedule",
|
|
16976
|
+
"interval",
|
|
16977
|
+
"manual",
|
|
16978
|
+
"detection",
|
|
16979
|
+
"stationary"
|
|
16519
16980
|
]),
|
|
16520
16981
|
outcome: TraceNodeOutcomeSchema,
|
|
16521
16982
|
reason: string(),
|
|
@@ -16549,6 +17010,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16549
17010
|
deviceId: number().int().optional(),
|
|
16550
17011
|
startedAt: number().optional(),
|
|
16551
17012
|
endedAt: number().optional(),
|
|
17013
|
+
/**
|
|
17014
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
17015
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
17016
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
17017
|
+
*/
|
|
17018
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16552
17019
|
outcome: _enum([
|
|
16553
17020
|
"ok",
|
|
16554
17021
|
"error",
|
|
@@ -17298,6 +17765,23 @@ method(object({
|
|
|
17298
17765
|
addonId: string(),
|
|
17299
17766
|
nodeId: string().optional()
|
|
17300
17767
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
17768
|
+
addonId: string(),
|
|
17769
|
+
nodeId: string().optional()
|
|
17770
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
17771
|
+
addonId: string(),
|
|
17772
|
+
nodeId: string().optional(),
|
|
17773
|
+
keys: array(string())
|
|
17774
|
+
}), object({
|
|
17775
|
+
removed: array(string()),
|
|
17776
|
+
refused: array(object({
|
|
17777
|
+
key: string(),
|
|
17778
|
+
reason: string()
|
|
17779
|
+
})),
|
|
17780
|
+
secretsCleared: number()
|
|
17781
|
+
}), {
|
|
17782
|
+
kind: "mutation",
|
|
17783
|
+
auth: "admin"
|
|
17784
|
+
}), method(object({
|
|
17301
17785
|
addonId: string(),
|
|
17302
17786
|
deviceId: number(),
|
|
17303
17787
|
nodeId: string().optional()
|
|
@@ -21327,6 +21811,8 @@ var StorageKindSchema = object({
|
|
|
21327
21811
|
"orphaned",
|
|
21328
21812
|
"dropped"
|
|
21329
21813
|
]),
|
|
21814
|
+
/** An owner declared this kind since the engine started. */
|
|
21815
|
+
declaredThisBoot: boolean(),
|
|
21330
21816
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21331
21817
|
rows: number().int().nonnegative().nullable(),
|
|
21332
21818
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21338,6 +21824,51 @@ var StorageKindSchema = object({
|
|
|
21338
21824
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21339
21825
|
declaration: RecordKindDeclarationSchema
|
|
21340
21826
|
});
|
|
21827
|
+
_enum([
|
|
21828
|
+
"orphaned",
|
|
21829
|
+
"stale",
|
|
21830
|
+
"unknown"
|
|
21831
|
+
]);
|
|
21832
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
21833
|
+
var StaleKindFindingSchema = object({
|
|
21834
|
+
owner: string(),
|
|
21835
|
+
kind: string(),
|
|
21836
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
21837
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
21838
|
+
reason: string(),
|
|
21839
|
+
rows: number().int().nonnegative().nullable(),
|
|
21840
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
21841
|
+
protected: boolean()
|
|
21842
|
+
});
|
|
21843
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
21844
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
21845
|
+
addonId: string(),
|
|
21846
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
21847
|
+
key: string(),
|
|
21848
|
+
/**
|
|
21849
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
21850
|
+
* not orphans yet. Removing the key releases them.
|
|
21851
|
+
*/
|
|
21852
|
+
secretRefs: array(string())
|
|
21853
|
+
});
|
|
21854
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
21855
|
+
var StaleUnknownSchema = object({
|
|
21856
|
+
subject: string(),
|
|
21857
|
+
reason: string()
|
|
21858
|
+
});
|
|
21859
|
+
var StaleDataReportSchema = object({
|
|
21860
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
21861
|
+
scannedAt: number().nullable(),
|
|
21862
|
+
scanning: boolean(),
|
|
21863
|
+
trigger: _enum([
|
|
21864
|
+
"boot",
|
|
21865
|
+
"interval",
|
|
21866
|
+
"manual"
|
|
21867
|
+
]).nullable(),
|
|
21868
|
+
kinds: array(StaleKindFindingSchema),
|
|
21869
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
21870
|
+
unknown: array(StaleUnknownSchema)
|
|
21871
|
+
});
|
|
21341
21872
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21342
21873
|
owner: string().min(1).optional(),
|
|
21343
21874
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21345,7 +21876,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21345
21876
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21346
21877
|
owner: string().min(1),
|
|
21347
21878
|
kind: RecordKindNameSchema
|
|
21348
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
21879
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
21880
|
+
kind: "mutation",
|
|
21881
|
+
auth: "admin"
|
|
21882
|
+
}), method(object({
|
|
21883
|
+
owner: string().min(1),
|
|
21884
|
+
kind: RecordKindNameSchema
|
|
21885
|
+
}), object({
|
|
21886
|
+
deleted: number().int().nonnegative(),
|
|
21887
|
+
secretsReleased: number().int().nonnegative()
|
|
21888
|
+
}), {
|
|
21889
|
+
kind: "mutation",
|
|
21890
|
+
auth: "admin",
|
|
21891
|
+
caller: "required"
|
|
21892
|
+
}), method(object({
|
|
21893
|
+
addonId: string().min(1),
|
|
21894
|
+
key: string().min(1)
|
|
21895
|
+
}), object({
|
|
21896
|
+
removed: boolean(),
|
|
21897
|
+
secretsCleared: number().int().nonnegative()
|
|
21898
|
+
}), {
|
|
21899
|
+
kind: "mutation",
|
|
21900
|
+
auth: "admin",
|
|
21901
|
+
caller: "required"
|
|
21902
|
+
});
|
|
21349
21903
|
/**
|
|
21350
21904
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21351
21905
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21484,6 +22038,12 @@ method(object({
|
|
|
21484
22038
|
}), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
|
|
21485
22039
|
kind: "mutation",
|
|
21486
22040
|
auth: "admin"
|
|
22041
|
+
}), method(object({
|
|
22042
|
+
refs: RefListSchema,
|
|
22043
|
+
onBehalfOf: string().min(1)
|
|
22044
|
+
}), object({ released: number().int() }), {
|
|
22045
|
+
kind: "mutation",
|
|
22046
|
+
auth: "admin"
|
|
21487
22047
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21488
22048
|
/**
|
|
21489
22049
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22386,7 +22946,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22386
22946
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22387
22947
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22388
22948
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22389
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
22949
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
22950
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
22951
|
+
parentStableId: string().optional()
|
|
22390
22952
|
}));
|
|
22391
22953
|
/**
|
|
22392
22954
|
* Status block — populated by the integration after each
|
|
@@ -42769,6 +43331,79 @@ object({
|
|
|
42769
43331
|
metadata: record(string(), unknown()).optional()
|
|
42770
43332
|
});
|
|
42771
43333
|
/**
|
|
43334
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
43335
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
43336
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
43337
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
43338
|
+
* except through a `custom` mode.
|
|
43339
|
+
*/
|
|
43340
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
43341
|
+
object({
|
|
43342
|
+
kind: literal("mdns"),
|
|
43343
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
43344
|
+
serviceType: string().min(1),
|
|
43345
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
43346
|
+
instancePattern: string().optional()
|
|
43347
|
+
}),
|
|
43348
|
+
object({
|
|
43349
|
+
kind: literal("ws-discovery"),
|
|
43350
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
43351
|
+
types: array(string()).optional()
|
|
43352
|
+
}),
|
|
43353
|
+
object({
|
|
43354
|
+
kind: literal("ssdp"),
|
|
43355
|
+
searchTarget: string().min(1)
|
|
43356
|
+
}),
|
|
43357
|
+
object({
|
|
43358
|
+
kind: literal("udp-broadcast"),
|
|
43359
|
+
port: number().int().min(1).max(65535),
|
|
43360
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
43361
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
43362
|
+
listenPort: number().int().min(1).max(65535).optional()
|
|
43363
|
+
}),
|
|
43364
|
+
object({
|
|
43365
|
+
kind: literal("http-probe"),
|
|
43366
|
+
port: number().int().min(1).max(65535),
|
|
43367
|
+
path: string().startsWith("/"),
|
|
43368
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
43369
|
+
bodyText: string().optional(),
|
|
43370
|
+
contentType: string().optional(),
|
|
43371
|
+
tls: boolean().default(false),
|
|
43372
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
43373
|
+
acceptStatus: array(number().int()).optional()
|
|
43374
|
+
}),
|
|
43375
|
+
object({
|
|
43376
|
+
kind: literal("custom"),
|
|
43377
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
43378
|
+
reason: string().min(1)
|
|
43379
|
+
})
|
|
43380
|
+
]);
|
|
43381
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
43382
|
+
var DiscoveryHitSchema = object({
|
|
43383
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
43384
|
+
modeIndex: number().int().min(0),
|
|
43385
|
+
host: string(),
|
|
43386
|
+
port: number().int(),
|
|
43387
|
+
http: object({
|
|
43388
|
+
status: number().int(),
|
|
43389
|
+
headers: record(string(), string()),
|
|
43390
|
+
/** At most 16 KiB. */
|
|
43391
|
+
bodyText: string()
|
|
43392
|
+
}).optional(),
|
|
43393
|
+
mdns: object({
|
|
43394
|
+
instance: string(),
|
|
43395
|
+
host: string(),
|
|
43396
|
+
port: number().int(),
|
|
43397
|
+
txt: record(string(), string())
|
|
43398
|
+
}).optional(),
|
|
43399
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
43400
|
+
wsd: object({
|
|
43401
|
+
xaddrs: array(string()),
|
|
43402
|
+
types: array(string()),
|
|
43403
|
+
scopes: array(string())
|
|
43404
|
+
}).optional()
|
|
43405
|
+
});
|
|
43406
|
+
/**
|
|
42772
43407
|
* Candidate handed back from discovery and accepted by
|
|
42773
43408
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
42774
43409
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -42803,7 +43438,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
42803
43438
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
42804
43439
|
onboardedDeviceId: number().optional(),
|
|
42805
43440
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
42806
|
-
onboardedName: string().optional()
|
|
43441
|
+
onboardedName: string().optional(),
|
|
43442
|
+
/**
|
|
43443
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
43444
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
43445
|
+
* which is still read for one release.
|
|
43446
|
+
*/
|
|
43447
|
+
setupIntegration: boolean().optional(),
|
|
43448
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
43449
|
+
parentStableId: string().optional(),
|
|
43450
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
43451
|
+
selectable: boolean().optional()
|
|
42807
43452
|
});
|
|
42808
43453
|
/**
|
|
42809
43454
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -42900,6 +43545,24 @@ var deviceProviderCapability = {
|
|
|
42900
43545
|
auth: "admin"
|
|
42901
43546
|
}),
|
|
42902
43547
|
/**
|
|
43548
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
43549
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
43550
|
+
*/
|
|
43551
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
43552
|
+
/**
|
|
43553
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
43554
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
43555
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
43556
|
+
* from `getDiscoveryParamsSchema`.
|
|
43557
|
+
*/
|
|
43558
|
+
classifyDiscoveryHits: method(object({
|
|
43559
|
+
hits: array(DiscoveryHitSchema),
|
|
43560
|
+
params: record(string(), unknown()).optional()
|
|
43561
|
+
}), array(DiscoveryCandidateSchema), {
|
|
43562
|
+
kind: "mutation",
|
|
43563
|
+
auth: "admin"
|
|
43564
|
+
}),
|
|
43565
|
+
/**
|
|
42903
43566
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
42904
43567
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
42905
43568
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -43664,17 +44327,23 @@ method(object({
|
|
|
43664
44327
|
}), method(object({}), object({ providers: array(object({
|
|
43665
44328
|
addonId: string(),
|
|
43666
44329
|
label: string()
|
|
43667
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
43668
|
-
|
|
43669
|
-
|
|
43670
|
-
|
|
43671
|
-
|
|
43672
|
-
|
|
43673
|
-
|
|
43674
|
-
|
|
43675
|
-
}),
|
|
44330
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
44331
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
44332
|
+
object({ subnets: array(string()).optional() }),
|
|
44333
|
+
object({ groups: array(object({
|
|
44334
|
+
addonId: string(),
|
|
44335
|
+
label: string(),
|
|
44336
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
44337
|
+
error: string().nullable()
|
|
44338
|
+
})).readonly() }),
|
|
44339
|
+
{
|
|
44340
|
+
kind: "mutation",
|
|
44341
|
+
auth: "admin"
|
|
44342
|
+
}
|
|
44343
|
+
), method(object({
|
|
43676
44344
|
addonId: string(),
|
|
43677
|
-
params: record(string(), unknown()).optional()
|
|
44345
|
+
params: record(string(), unknown()).optional(),
|
|
44346
|
+
subnets: array(string()).optional()
|
|
43678
44347
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
43679
44348
|
kind: "mutation",
|
|
43680
44349
|
auth: "admin"
|
|
@@ -56341,6 +57010,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
56341
57010
|
instanceId: string(),
|
|
56342
57011
|
cameraStableId: string(),
|
|
56343
57012
|
nodeId: string(),
|
|
57013
|
+
/**
|
|
57014
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57015
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57016
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57017
|
+
*/
|
|
57018
|
+
nodeInCluster: boolean(),
|
|
56344
57019
|
profileId: string(),
|
|
56345
57020
|
profileLabel: string(),
|
|
56346
57021
|
name: string(),
|
|
@@ -56390,6 +57065,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
56390
57065
|
auth: "admin"
|
|
56391
57066
|
}), method(object({
|
|
56392
57067
|
instanceId: string().min(1),
|
|
57068
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57069
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
56393
57070
|
name: string().trim().min(1).max(160).optional(),
|
|
56394
57071
|
executable: string().max(1024).optional(),
|
|
56395
57072
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -63016,7 +63693,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63016
63693
|
id: string(),
|
|
63017
63694
|
settings: record(string(), unknown()),
|
|
63018
63695
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63019
|
-
secretKeys: array(string()).optional()
|
|
63696
|
+
secretKeys: array(string()).optional(),
|
|
63697
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
63698
|
+
skipRestart: boolean().optional()
|
|
63020
63699
|
}), object({ success: boolean() }), {
|
|
63021
63700
|
kind: "mutation",
|
|
63022
63701
|
auth: "admin"
|
|
@@ -63644,6 +64323,39 @@ var TlsStatusSchema = object({
|
|
|
63644
64323
|
hasCa: boolean()
|
|
63645
64324
|
}).nullable()
|
|
63646
64325
|
});
|
|
64326
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
64327
|
+
var ScanRequestSchema = object({
|
|
64328
|
+
providerId: string().min(1),
|
|
64329
|
+
modes: array(DiscoveryModeSchema)
|
|
64330
|
+
});
|
|
64331
|
+
var ScanErrorSchema = object({
|
|
64332
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
64333
|
+
providerId: string().optional(),
|
|
64334
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
64335
|
+
mode: string().optional(),
|
|
64336
|
+
message: string()
|
|
64337
|
+
});
|
|
64338
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
64339
|
+
var ScanStatsSchema = object({
|
|
64340
|
+
hostsProbed: number().int(),
|
|
64341
|
+
hits: number().int(),
|
|
64342
|
+
timeouts: number().int(),
|
|
64343
|
+
byMode: record(string(), object({
|
|
64344
|
+
probed: number().int(),
|
|
64345
|
+
hits: number().int(),
|
|
64346
|
+
timeouts: number().int()
|
|
64347
|
+
}))
|
|
64348
|
+
});
|
|
64349
|
+
var ScanNetworkResultSchema = object({
|
|
64350
|
+
hits: array(object({
|
|
64351
|
+
providerId: string(),
|
|
64352
|
+
hit: DiscoveryHitSchema
|
|
64353
|
+
})),
|
|
64354
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
64355
|
+
subnetsScanned: array(string()),
|
|
64356
|
+
errors: array(ScanErrorSchema),
|
|
64357
|
+
stats: ScanStatsSchema
|
|
64358
|
+
});
|
|
63647
64359
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
63648
64360
|
/**
|
|
63649
64361
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -63698,6 +64410,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
63698
64410
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
63699
64411
|
kind: "mutation",
|
|
63700
64412
|
auth: "admin"
|
|
64413
|
+
}), method(object({
|
|
64414
|
+
requests: array(ScanRequestSchema),
|
|
64415
|
+
subnets: array(string()).optional()
|
|
64416
|
+
}), ScanNetworkResultSchema, {
|
|
64417
|
+
kind: "mutation",
|
|
64418
|
+
auth: "admin",
|
|
64419
|
+
timeoutMs: 9e4
|
|
64420
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
64421
|
+
kind: "mutation",
|
|
64422
|
+
auth: "admin"
|
|
63701
64423
|
}), method(_void(), TlsStatusSchema, {
|
|
63702
64424
|
kind: "mutation",
|
|
63703
64425
|
auth: "admin"
|
|
@@ -73280,6 +74002,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73280
74002
|
async discoverDevices(_input) {
|
|
73281
74003
|
return [];
|
|
73282
74004
|
}
|
|
74005
|
+
/**
|
|
74006
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74007
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74008
|
+
*/
|
|
74009
|
+
async getDiscoveryModes() {
|
|
74010
|
+
return [];
|
|
74011
|
+
}
|
|
74012
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74013
|
+
async classifyDiscoveryHits(_input) {
|
|
74014
|
+
return [];
|
|
74015
|
+
}
|
|
73283
74016
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73284
74017
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73285
74018
|
async getDiscoveryParamsSchema() {
|
|
@@ -74148,6 +74881,13 @@ Object.freeze({
|
|
|
74148
74881
|
access: "create",
|
|
74149
74882
|
role: "admin"
|
|
74150
74883
|
},
|
|
74884
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
74885
|
+
capName: "addon-settings",
|
|
74886
|
+
capScope: "system",
|
|
74887
|
+
addonId: null,
|
|
74888
|
+
access: "view",
|
|
74889
|
+
role: "admin"
|
|
74890
|
+
},
|
|
74151
74891
|
"addonSettings.getDeviceSettings": {
|
|
74152
74892
|
capName: "addon-settings",
|
|
74153
74893
|
capScope: "system",
|
|
@@ -74169,6 +74909,13 @@ Object.freeze({
|
|
|
74169
74909
|
access: "view",
|
|
74170
74910
|
role: "admin"
|
|
74171
74911
|
},
|
|
74912
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
74913
|
+
capName: "addon-settings",
|
|
74914
|
+
capScope: "system",
|
|
74915
|
+
addonId: null,
|
|
74916
|
+
access: "create",
|
|
74917
|
+
role: "admin"
|
|
74918
|
+
},
|
|
74172
74919
|
"addonSettings.updateDeviceSettings": {
|
|
74173
74920
|
capName: "addon-settings",
|
|
74174
74921
|
capScope: "system",
|
|
@@ -76584,6 +77331,13 @@ Object.freeze({
|
|
|
76584
77331
|
access: "create",
|
|
76585
77332
|
role: "admin"
|
|
76586
77333
|
},
|
|
77334
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
77335
|
+
capName: "device-provider",
|
|
77336
|
+
capScope: "system",
|
|
77337
|
+
addonId: null,
|
|
77338
|
+
access: "create",
|
|
77339
|
+
role: "admin"
|
|
77340
|
+
},
|
|
76587
77341
|
"deviceProvider.createDevice": {
|
|
76588
77342
|
capName: "device-provider",
|
|
76589
77343
|
capScope: "system",
|
|
@@ -76612,6 +77366,13 @@ Object.freeze({
|
|
|
76612
77366
|
access: "view",
|
|
76613
77367
|
role: "admin"
|
|
76614
77368
|
},
|
|
77369
|
+
"deviceProvider.getDiscoveryModes": {
|
|
77370
|
+
capName: "device-provider",
|
|
77371
|
+
capScope: "system",
|
|
77372
|
+
addonId: null,
|
|
77373
|
+
access: "view",
|
|
77374
|
+
role: "admin"
|
|
77375
|
+
},
|
|
76615
77376
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
76616
77377
|
capName: "device-provider",
|
|
76617
77378
|
capScope: "system",
|
|
@@ -77389,6 +78150,13 @@ Object.freeze({
|
|
|
77389
78150
|
access: "create",
|
|
77390
78151
|
role: "admin"
|
|
77391
78152
|
},
|
|
78153
|
+
"keychain.releaseRecordRefs": {
|
|
78154
|
+
capName: "keychain",
|
|
78155
|
+
capScope: "system",
|
|
78156
|
+
addonId: null,
|
|
78157
|
+
access: "create",
|
|
78158
|
+
role: "admin"
|
|
78159
|
+
},
|
|
77392
78160
|
"keychain.resolve": {
|
|
77393
78161
|
capName: "keychain",
|
|
77394
78162
|
capScope: "system",
|
|
@@ -77809,6 +78577,13 @@ Object.freeze({
|
|
|
77809
78577
|
access: "view",
|
|
77810
78578
|
role: "admin"
|
|
77811
78579
|
},
|
|
78580
|
+
"localNetwork.getScanSubnets": {
|
|
78581
|
+
capName: "local-network",
|
|
78582
|
+
capScope: "system",
|
|
78583
|
+
addonId: null,
|
|
78584
|
+
access: "view",
|
|
78585
|
+
role: "admin"
|
|
78586
|
+
},
|
|
77812
78587
|
"localNetwork.getTlsStatus": {
|
|
77813
78588
|
capName: "local-network",
|
|
77814
78589
|
capScope: "system",
|
|
@@ -77858,6 +78633,13 @@ Object.freeze({
|
|
|
77858
78633
|
access: "create",
|
|
77859
78634
|
role: "admin"
|
|
77860
78635
|
},
|
|
78636
|
+
"localNetwork.scanNetwork": {
|
|
78637
|
+
capName: "local-network",
|
|
78638
|
+
capScope: "system",
|
|
78639
|
+
addonId: null,
|
|
78640
|
+
access: "create",
|
|
78641
|
+
role: "admin"
|
|
78642
|
+
},
|
|
77861
78643
|
"localNetwork.setAddressRole": {
|
|
77862
78644
|
capName: "local-network",
|
|
77863
78645
|
capScope: "system",
|
|
@@ -77879,6 +78661,13 @@ Object.freeze({
|
|
|
77879
78661
|
access: "create",
|
|
77880
78662
|
role: "admin"
|
|
77881
78663
|
},
|
|
78664
|
+
"localNetwork.setScanSubnets": {
|
|
78665
|
+
capName: "local-network",
|
|
78666
|
+
capScope: "system",
|
|
78667
|
+
addonId: null,
|
|
78668
|
+
access: "create",
|
|
78669
|
+
role: "admin"
|
|
78670
|
+
},
|
|
77882
78671
|
"localNetwork.setViewerEndpoints": {
|
|
77883
78672
|
capName: "local-network",
|
|
77884
78673
|
capScope: "system",
|
|
@@ -82100,6 +82889,13 @@ Object.freeze({
|
|
|
82100
82889
|
access: "create",
|
|
82101
82890
|
role: "admin"
|
|
82102
82891
|
},
|
|
82892
|
+
"storageAdmin.dropOrphanedKind": {
|
|
82893
|
+
capName: "storage-admin",
|
|
82894
|
+
capScope: "system",
|
|
82895
|
+
addonId: null,
|
|
82896
|
+
access: "delete",
|
|
82897
|
+
role: "admin"
|
|
82898
|
+
},
|
|
82103
82899
|
"storageAdmin.listKinds": {
|
|
82104
82900
|
capName: "storage-admin",
|
|
82105
82901
|
capScope: "system",
|
|
@@ -82121,6 +82917,27 @@ Object.freeze({
|
|
|
82121
82917
|
access: "view",
|
|
82122
82918
|
role: "admin"
|
|
82123
82919
|
},
|
|
82920
|
+
"storageAdmin.scanStaleData": {
|
|
82921
|
+
capName: "storage-admin",
|
|
82922
|
+
capScope: "system",
|
|
82923
|
+
addonId: null,
|
|
82924
|
+
access: "create",
|
|
82925
|
+
role: "admin"
|
|
82926
|
+
},
|
|
82927
|
+
"storageAdmin.staleDataReport": {
|
|
82928
|
+
capName: "storage-admin",
|
|
82929
|
+
capScope: "system",
|
|
82930
|
+
addonId: null,
|
|
82931
|
+
access: "view",
|
|
82932
|
+
role: "admin"
|
|
82933
|
+
},
|
|
82934
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
82935
|
+
capName: "storage-admin",
|
|
82936
|
+
capScope: "system",
|
|
82937
|
+
addonId: null,
|
|
82938
|
+
access: "create",
|
|
82939
|
+
role: "admin"
|
|
82940
|
+
},
|
|
82124
82941
|
"storageEvictable.evict": {
|
|
82125
82942
|
capName: "storage-evictable",
|
|
82126
82943
|
capScope: "system",
|
|
@@ -95072,36 +95889,22 @@ function buildHmAdoptionProvider(deps) {
|
|
|
95072
95889
|
}
|
|
95073
95890
|
};
|
|
95074
95891
|
}
|
|
95075
|
-
|
|
95076
|
-
|
|
95077
|
-
|
|
95078
|
-
|
|
95079
|
-
|
|
95080
|
-
|
|
95081
|
-
|
|
95082
|
-
|
|
95083
|
-
|
|
95084
|
-
|
|
95085
|
-
|
|
95086
|
-
|
|
95087
|
-
|
|
95088
|
-
|
|
95089
|
-
|
|
95090
|
-
|
|
95091
|
-
return {
|
|
95092
|
-
status: response.status,
|
|
95093
|
-
text: await response.text()
|
|
95094
|
-
};
|
|
95095
|
-
};
|
|
95096
|
-
function osAddresses() {
|
|
95097
|
-
const out = [];
|
|
95098
|
-
for (const list of Object.values((0, node_os.networkInterfaces)())) for (const a of list ?? []) out.push({
|
|
95099
|
-
family: String(a.family),
|
|
95100
|
-
address: a.address,
|
|
95101
|
-
internal: a.internal
|
|
95102
|
-
});
|
|
95103
|
-
return out;
|
|
95104
|
-
}
|
|
95892
|
+
/** The question the WebUI asks before it shows a login; the engine sends it to every host once. */
|
|
95893
|
+
var HOMEMATIC_DISCOVERY_MODES = [{
|
|
95894
|
+
kind: "http-probe",
|
|
95895
|
+
port: 80,
|
|
95896
|
+
path: "/api/homematic.cgi",
|
|
95897
|
+
method: "POST",
|
|
95898
|
+
bodyText: JSON.stringify({
|
|
95899
|
+
method: "CCU.getAuthEnabled",
|
|
95900
|
+
params: {},
|
|
95901
|
+
jsonrpc: "1.1",
|
|
95902
|
+
id: 0
|
|
95903
|
+
}),
|
|
95904
|
+
contentType: "application/json",
|
|
95905
|
+
tls: false,
|
|
95906
|
+
acceptStatus: [200]
|
|
95907
|
+
}];
|
|
95105
95908
|
/**
|
|
95106
95909
|
* Read the answer of `CCU.getAuthEnabled`, the question the WebUI asks before
|
|
95107
95910
|
* it shows a login. A CCU answers a boolean; one that already requires a login
|
|
@@ -95128,63 +95931,26 @@ function parseAuthEnabledAnswer(status, text) {
|
|
|
95128
95931
|
if (result === false || result === "false") return { authEnabled: false };
|
|
95129
95932
|
return null;
|
|
95130
95933
|
}
|
|
95131
|
-
/**
|
|
95132
|
-
* The hosts to ask: every address of the /24 of each external IPv4 address of
|
|
95133
|
-
* this machine, minus the network and broadcast addresses and this machine itself.
|
|
95134
|
-
*/
|
|
95135
|
-
function lanHostsToProbe(addresses) {
|
|
95136
|
-
const own = /* @__PURE__ */ new Set();
|
|
95137
|
-
const subnets = [];
|
|
95138
|
-
for (const a of addresses) {
|
|
95139
|
-
if (a.internal || a.family !== "IPv4" && a.family !== "4") continue;
|
|
95140
|
-
own.add(a.address);
|
|
95141
|
-
const prefix = a.address.split(".").slice(0, 3).join(".");
|
|
95142
|
-
if (prefix.split(".").length === 3 && !subnets.includes(prefix)) subnets.push(prefix);
|
|
95143
|
-
}
|
|
95144
|
-
const hosts = [];
|
|
95145
|
-
for (const prefix of subnets) for (let last = 1; last <= 254; last++) {
|
|
95146
|
-
const host = `${prefix}.${last}`;
|
|
95147
|
-
if (!own.has(host)) hosts.push(host);
|
|
95148
|
-
}
|
|
95149
|
-
return hosts.slice(0, MAX_HOSTS);
|
|
95150
|
-
}
|
|
95151
95934
|
/** Slugify a host into a readable, stable id fragment. */
|
|
95152
95935
|
function slugifyHost(value) {
|
|
95153
95936
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
95154
95937
|
}
|
|
95155
|
-
/**
|
|
95156
|
-
|
|
95157
|
-
* asked `CCU.getAuthEnabled` on the JSON-RPC endpoint, with a short timeout and
|
|
95158
|
-
* a bounded number of probes in flight.
|
|
95159
|
-
*/
|
|
95160
|
-
async function browseHomematicCcus(options = {}) {
|
|
95161
|
-
const { addresses = osAddresses, post = fetchPost, concurrency = 64, timeoutMs = HOMEMATIC_DISCOVERY_TIMEOUT_MS } = options;
|
|
95162
|
-
const hosts = lanHostsToProbe(addresses());
|
|
95163
|
-
const body = JSON.stringify({
|
|
95164
|
-
method: "CCU.getAuthEnabled",
|
|
95165
|
-
params: {},
|
|
95166
|
-
jsonrpc: "1.1",
|
|
95167
|
-
id: 0
|
|
95168
|
-
});
|
|
95938
|
+
/** CCUs among the engine's http-probe hits: the hosts whose answer is the JSON-RPC one. */
|
|
95939
|
+
function ccusFromHits(hits) {
|
|
95169
95940
|
const found = [];
|
|
95170
|
-
|
|
95171
|
-
const
|
|
95172
|
-
|
|
95173
|
-
|
|
95174
|
-
|
|
95175
|
-
|
|
95176
|
-
|
|
95177
|
-
|
|
95178
|
-
|
|
95179
|
-
|
|
95180
|
-
|
|
95181
|
-
|
|
95182
|
-
|
|
95183
|
-
});
|
|
95184
|
-
} catch {}
|
|
95185
|
-
}
|
|
95186
|
-
};
|
|
95187
|
-
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker()));
|
|
95941
|
+
const seen = /* @__PURE__ */ new Set();
|
|
95942
|
+
for (const hit of hits) {
|
|
95943
|
+
if (hit.http === void 0 || seen.has(hit.host)) continue;
|
|
95944
|
+
const answer = parseAuthEnabledAnswer(hit.http.status, hit.http.bodyText);
|
|
95945
|
+
if (answer === null) continue;
|
|
95946
|
+
seen.add(hit.host);
|
|
95947
|
+
found.push({
|
|
95948
|
+
host: hit.host,
|
|
95949
|
+
name: `Homematic CCU (${hit.host})`,
|
|
95950
|
+
id: slugifyHost(hit.host),
|
|
95951
|
+
authEnabled: answer.authEnabled
|
|
95952
|
+
});
|
|
95953
|
+
}
|
|
95188
95954
|
return found.sort((a, b) => a.host.localeCompare(b.host, void 0, { numeric: true }));
|
|
95189
95955
|
}
|
|
95190
95956
|
/** CCUs whose host is already stored on an integration are not proposed again. */
|
|
@@ -95195,11 +95961,10 @@ function proposeHomematicCcus(found, existingHosts) {
|
|
|
95195
95961
|
function homematicDiscoveryCandidates(proposals) {
|
|
95196
95962
|
return proposals.map((found) => ({
|
|
95197
95963
|
stableId: `homematic:${found.id}`,
|
|
95964
|
+
type: DeviceType.Gateway,
|
|
95198
95965
|
suggestedName: found.name,
|
|
95199
|
-
prefilledConfig: {
|
|
95200
|
-
|
|
95201
|
-
[HOMEMATIC_SETUP_INTEGRATION_KEY]: true
|
|
95202
|
-
}
|
|
95966
|
+
prefilledConfig: { host: found.host },
|
|
95967
|
+
setupIntegration: true
|
|
95203
95968
|
}));
|
|
95204
95969
|
}
|
|
95205
95970
|
function homematicDiscoverySummary(foundCount, proposedCount) {
|
|
@@ -112471,6 +113236,19 @@ var CircuitBreakerOpenError = class extends BaseHomematicError {};
|
|
|
112471
113236
|
var AuthFailureError = class extends BaseHomematicError {};
|
|
112472
113237
|
var InternalBackendError = class extends BaseHomematicError {};
|
|
112473
113238
|
var CommandSupersededError = class extends BaseHomematicError {};
|
|
113239
|
+
var CcuManagementError = class extends BaseHomematicError {
|
|
113240
|
+
/** The XML-RPC / JSON-RPC method that failed. */
|
|
113241
|
+
method;
|
|
113242
|
+
/** The underlying typed error (or whatever was thrown). */
|
|
113243
|
+
original;
|
|
113244
|
+
constructor(method, original) {
|
|
113245
|
+
const detail = original instanceof Error ? original.message : String(original);
|
|
113246
|
+
super(`${method} failed: ${detail}`);
|
|
113247
|
+
this.method = method;
|
|
113248
|
+
this.original = original;
|
|
113249
|
+
}
|
|
113250
|
+
};
|
|
113251
|
+
var GroupWriteVerificationError = class extends BaseHomematicError {};
|
|
112474
113252
|
var DescriptionNotFoundError = class extends BaseHomematicError {};
|
|
112475
113253
|
var InvalidStateTransitionError = class extends BaseHomematicError {};
|
|
112476
113254
|
var TimeoutError = class extends BaseHomematicError {};
|
|
@@ -113224,6 +114002,54 @@ var InterfaceClient = class {
|
|
|
113224
114002
|
const result = await this.callGuarded("getInstallMode", []);
|
|
113225
114003
|
return typeof result === "number" ? result : 0;
|
|
113226
114004
|
}
|
|
114005
|
+
/**
|
|
114006
|
+
* `listBidcosInterfaces()` → the radio gateways of a BidCos interface.
|
|
114007
|
+
*
|
|
114008
|
+
* Source: HomeMatic XML-RPC interface spec (rfd), as consumed by the WebUI in
|
|
114009
|
+
* eq-3/occu `WebUI/www/api/methods/interface/listbidcosinterfaces.tcl`
|
|
114010
|
+
* (fields ADDRESS, DESCRIPTION, DUTY_CYCLE, CONNECTED, DEFAULT,
|
|
114011
|
+
* FIRMWARE_VERSION, TYPE). A field the CCU did not send is `null`, never 0.
|
|
114012
|
+
*/
|
|
114013
|
+
async listBidcosInterfaces() {
|
|
114014
|
+
const result = await this.managementCall("listBidcosInterfaces", []);
|
|
114015
|
+
if (!Array.isArray(result)) return [];
|
|
114016
|
+
const gateways = [];
|
|
114017
|
+
for (const entry of result) if (isStruct(entry)) gateways.push(toRadioGateway(entry));
|
|
114018
|
+
return gateways;
|
|
114019
|
+
}
|
|
114020
|
+
/**
|
|
114021
|
+
* `setInstallMode(on[, seconds[, mode]])` — open or close the pairing window.
|
|
114022
|
+
*
|
|
114023
|
+
* Source: HomeMatic XML-RPC interface spec; the WebUI forwards the same call
|
|
114024
|
+
* in eq-3/occu `WebUI/www/api/methods/interface/setinstallmode.tcl` and
|
|
114025
|
+
* `setinstallmodehmip.tcl` (`setInstallMode(bool on, int time)`). `mode`
|
|
114026
|
+
* (1 normal, 2 with the HomeMatic Secure key) is BidCos-only and is sent only
|
|
114027
|
+
* when given.
|
|
114028
|
+
*/
|
|
114029
|
+
async setInstallMode(request) {
|
|
114030
|
+
const params = [request.on];
|
|
114031
|
+
if (request.seconds !== void 0) params.push(request.seconds);
|
|
114032
|
+
if (request.mode !== void 0) params.push(request.mode);
|
|
114033
|
+
await this.managementCall("setInstallMode", params);
|
|
114034
|
+
}
|
|
114035
|
+
/**
|
|
114036
|
+
* `getInstallMode()` → seconds left of the pairing window, or `null` when the
|
|
114037
|
+
* CCU answered something that is not an integer (never a fake 0).
|
|
114038
|
+
*/
|
|
114039
|
+
async getInstallModeRemaining() {
|
|
114040
|
+
const result = await this.managementCall("getInstallMode", []);
|
|
114041
|
+
return typeof result === "number" && Number.isInteger(result) ? result : null;
|
|
114042
|
+
}
|
|
114043
|
+
/**
|
|
114044
|
+
* `deleteDevice(address, flags)` — remove a device from the interface.
|
|
114045
|
+
*
|
|
114046
|
+
* Source: eq-3/occu `WebUI/www/api/methods/interface/deletedevice.tcl`:
|
|
114047
|
+
* flags 0x01 reset to factory, 0x02 force (also when unreachable), 0x04 defer
|
|
114048
|
+
* until the device is next reachable.
|
|
114049
|
+
*/
|
|
114050
|
+
async deleteDevice(address, flags) {
|
|
114051
|
+
await this.managementCall("deleteDevice", [address, flags]);
|
|
114052
|
+
}
|
|
113227
114053
|
/** `getVersion()` (bypass) → backend version string. */
|
|
113228
114054
|
async getVersion() {
|
|
113229
114055
|
return scalarToString(await this.callGuarded("getVersion", [], { bypassBreaker: true }));
|
|
@@ -113234,6 +114060,18 @@ var InterfaceClient = class {
|
|
|
113234
114060
|
return Array.isArray(result) ? result.map(scalarToString) : [];
|
|
113235
114061
|
}
|
|
113236
114062
|
/**
|
|
114063
|
+
* A management call: guarded like any other, but a failure is rethrown as a
|
|
114064
|
+
* {@link CcuManagementError} naming the method and keeping the CCU fault text.
|
|
114065
|
+
*/
|
|
114066
|
+
async managementCall(method, params) {
|
|
114067
|
+
try {
|
|
114068
|
+
return await this.callGuarded(method, params);
|
|
114069
|
+
} catch (err) {
|
|
114070
|
+
if (err instanceof BaseHomematicError) throw new CcuManagementError(method, err);
|
|
114071
|
+
throw err;
|
|
114072
|
+
}
|
|
114073
|
+
}
|
|
114074
|
+
/**
|
|
113237
114075
|
* Invoke `method`, gating non-bypass calls behind the circuit breaker.
|
|
113238
114076
|
* Bypass methods skip the breaker entirely (no success/failure recorded).
|
|
113239
114077
|
*/
|
|
@@ -113288,17 +114126,48 @@ function asDeviceDescriptionArray(value) {
|
|
|
113288
114126
|
function asParamsetDescription(value) {
|
|
113289
114127
|
return asStruct(value);
|
|
113290
114128
|
}
|
|
114129
|
+
function stringOrNull(value) {
|
|
114130
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
114131
|
+
}
|
|
114132
|
+
function numberOrNull(value) {
|
|
114133
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
114134
|
+
}
|
|
114135
|
+
function flagOrNull(value) {
|
|
114136
|
+
if (typeof value === "boolean") return value;
|
|
114137
|
+
if (value === 1) return true;
|
|
114138
|
+
if (value === 0) return false;
|
|
114139
|
+
return null;
|
|
114140
|
+
}
|
|
114141
|
+
function toRadioGateway(entry) {
|
|
114142
|
+
const connected = flagOrNull(entry["CONNECTED"]);
|
|
114143
|
+
return {
|
|
114144
|
+
address: stringOrNull(entry["ADDRESS"]) ?? "",
|
|
114145
|
+
description: stringOrNull(entry["DESCRIPTION"]),
|
|
114146
|
+
dutyCycle: numberOrNull(entry["DUTY_CYCLE"]),
|
|
114147
|
+
connected,
|
|
114148
|
+
isDefault: flagOrNull(entry["DEFAULT"]) === true,
|
|
114149
|
+
firmware: stringOrNull(entry["FIRMWARE_VERSION"]),
|
|
114150
|
+
type: stringOrNull(entry["TYPE"])
|
|
114151
|
+
};
|
|
114152
|
+
}
|
|
113291
114153
|
var SESSION_ID_PARAM = "_session_id_";
|
|
113292
114154
|
var JsonRpcMethod = {
|
|
113293
114155
|
CCU_GET_AUTH_ENABLED: "CCU.getAuthEnabled",
|
|
113294
114156
|
CCU_GET_HTTPS_REDIRECT_ENABLED: "CCU.getHttpsRedirectEnabled",
|
|
114157
|
+
CCU_GET_VERSION: "CCU.getVersion",
|
|
114158
|
+
CCU_GET_SERIAL: "CCU.getSerial",
|
|
114159
|
+
CCU_GET_HEATING_GROUP_LIST: "CCU.getHeatingGroupList",
|
|
113295
114160
|
DEVICE_LIST_ALL_DETAIL: "Device.listAllDetail",
|
|
114161
|
+
DEVICE_GET_REGA_ID_BY_ADDRESS: "Device.getReGaIDByAddress",
|
|
113296
114162
|
DEVICE_SET_NAME: "Device.setName",
|
|
113297
114163
|
CHANNEL_SET_NAME: "Channel.setName",
|
|
113298
114164
|
INTERFACE_LIST_INTERFACES: "Interface.listInterfaces",
|
|
113299
114165
|
INTERFACE_LIST_DEVICES: "Interface.listDevices",
|
|
114166
|
+
INTERFACE_IS_PRESENT: "Interface.isPresent",
|
|
114167
|
+
INTERFACE_GET_DUTY_CYCLE: "Interface.getDutyCycle",
|
|
113300
114168
|
INTERFACE_GET_INSTALL_MODE: "Interface.getInstallMode",
|
|
113301
114169
|
INTERFACE_SET_INSTALL_MODE_HMIP: "Interface.setInstallModeHMIP",
|
|
114170
|
+
INTERFACE_SET_METADATA: "Interface.setMetadata",
|
|
113302
114171
|
PROGRAM_GET_ALL: "Program.getAll",
|
|
113303
114172
|
PROGRAM_EXECUTE: "Program.execute",
|
|
113304
114173
|
SYSVAR_GET_ALL: "SysVar.getAll",
|
|
@@ -113507,6 +114376,691 @@ var SessionManager = class {
|
|
|
113507
114376
|
return Math.min(exp, MAX_BACKOFF_MS);
|
|
113508
114377
|
}
|
|
113509
114378
|
};
|
|
114379
|
+
var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
114380
|
+
var ESCAPE_SAFE = /[A-Za-z0-9@*_+\-./]/;
|
|
114381
|
+
function jsEscape(text) {
|
|
114382
|
+
let out = "";
|
|
114383
|
+
for (const ch of text) {
|
|
114384
|
+
const unit = ch.charCodeAt(0);
|
|
114385
|
+
if (ESCAPE_SAFE.test(ch)) out += ch;
|
|
114386
|
+
else if (ch.length === 1 && unit < 256) out += `%${unit.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
114387
|
+
else for (let i = 0; i < ch.length; i += 1) out += `%u${ch.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
114388
|
+
}
|
|
114389
|
+
return out;
|
|
114390
|
+
}
|
|
114391
|
+
var WebUiHttpClient = class {
|
|
114392
|
+
base;
|
|
114393
|
+
timeoutMs;
|
|
114394
|
+
fetchFn;
|
|
114395
|
+
dispatcher;
|
|
114396
|
+
constructor(options) {
|
|
114397
|
+
this.base = options.url.replace(/\/+$/, "");
|
|
114398
|
+
this.timeoutMs = options.timeoutMs ?? TIMEOUTS.rpc;
|
|
114399
|
+
this.dispatcher = options.rejectUnauthorized === false ? new import_undici.Agent({ connect: { rejectUnauthorized: false } }) : void 0;
|
|
114400
|
+
this.fetchFn = options.fetch ?? this.defaultFetch.bind(this);
|
|
114401
|
+
}
|
|
114402
|
+
get(path$2, sessionId) {
|
|
114403
|
+
return this.request("GET", path$2, sessionId, void 0);
|
|
114404
|
+
}
|
|
114405
|
+
post(path$3, sessionId, body) {
|
|
114406
|
+
return this.request("POST", path$3, sessionId, body);
|
|
114407
|
+
}
|
|
114408
|
+
async close() {
|
|
114409
|
+
await this.dispatcher?.close();
|
|
114410
|
+
}
|
|
114411
|
+
async request(method, path$4, sessionId, body) {
|
|
114412
|
+
const url = `${this.base}${path$4}?sid=${encodeURIComponent(sessionId)}`;
|
|
114413
|
+
const controller = new AbortController();
|
|
114414
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
114415
|
+
try {
|
|
114416
|
+
const response = await this.fetchFn(url, {
|
|
114417
|
+
method,
|
|
114418
|
+
headers: body === void 0 ? {} : { "Content-Type": FORM_CONTENT_TYPE },
|
|
114419
|
+
body,
|
|
114420
|
+
signal: controller.signal
|
|
114421
|
+
});
|
|
114422
|
+
const text = await response.text();
|
|
114423
|
+
if (response.status < 200 || response.status >= 300) throw new ClientError(`HTTP ${response.status}`);
|
|
114424
|
+
return text;
|
|
114425
|
+
} catch (err) {
|
|
114426
|
+
const mapped = err instanceof ClientError ? err : mapTransportError(err);
|
|
114427
|
+
throw new CcuManagementError(`${method} ${path$4}`, /* @__PURE__ */ new Error(`${mapped.name}: ${mapped.message.split(sessionId).join("<sid>")}`));
|
|
114428
|
+
} finally {
|
|
114429
|
+
clearTimeout(timer);
|
|
114430
|
+
}
|
|
114431
|
+
}
|
|
114432
|
+
async defaultFetch(url, init) {
|
|
114433
|
+
const req = {
|
|
114434
|
+
method: init.method,
|
|
114435
|
+
headers: init.headers,
|
|
114436
|
+
signal: init.signal
|
|
114437
|
+
};
|
|
114438
|
+
if (init.body !== void 0) req.body = init.body;
|
|
114439
|
+
if (this.dispatcher) req.dispatcher = this.dispatcher;
|
|
114440
|
+
return (0, import_undici.fetch)(url, req);
|
|
114441
|
+
}
|
|
114442
|
+
};
|
|
114443
|
+
function escapeRegaString(s) {
|
|
114444
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
114445
|
+
}
|
|
114446
|
+
var CONTROL_CHARS = /[\u0000-\u001F]/g;
|
|
114447
|
+
function sanitizeJsonControlChars(s) {
|
|
114448
|
+
return s.replace(CONTROL_CHARS, "");
|
|
114449
|
+
}
|
|
114450
|
+
function decodeRegaName(encoded) {
|
|
114451
|
+
const bytes = [];
|
|
114452
|
+
for (let i = 0; i < encoded.length; i += 1) {
|
|
114453
|
+
const ch = encoded[i];
|
|
114454
|
+
if (ch === "%" && i + 2 < encoded.length) {
|
|
114455
|
+
const hex = encoded.slice(i + 1, i + 3);
|
|
114456
|
+
const byte = Number.parseInt(hex, 16);
|
|
114457
|
+
if (!Number.isNaN(byte) && /^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
114458
|
+
bytes.push(byte);
|
|
114459
|
+
i += 2;
|
|
114460
|
+
continue;
|
|
114461
|
+
}
|
|
114462
|
+
}
|
|
114463
|
+
if (ch === "+") {
|
|
114464
|
+
bytes.push(32);
|
|
114465
|
+
continue;
|
|
114466
|
+
}
|
|
114467
|
+
bytes.push(ch.charCodeAt(0) & 255);
|
|
114468
|
+
}
|
|
114469
|
+
return Buffer.from(bytes).toString("latin1");
|
|
114470
|
+
}
|
|
114471
|
+
function substitute(body, params) {
|
|
114472
|
+
let out = body;
|
|
114473
|
+
for (const [key, value] of Object.entries(params)) {
|
|
114474
|
+
const escaped = escapeRegaString(String(value));
|
|
114475
|
+
out = out.split(`##${key}##`).join(escaped);
|
|
114476
|
+
}
|
|
114477
|
+
return out;
|
|
114478
|
+
}
|
|
114479
|
+
async function runScript(client, sessionId, body, params) {
|
|
114480
|
+
const script = params ? substitute(body, params) : body;
|
|
114481
|
+
const result = await client.post(JsonRpcMethod.REGA_RUN_SCRIPT, { script }, sessionId !== void 0 ? { sessionId } : void 0);
|
|
114482
|
+
if (result === void 0 || result === null) throw new ClientError("ReGa.runScript returned no result");
|
|
114483
|
+
if (typeof result === "string") return JSON.parse(sanitizeJsonControlChars(result));
|
|
114484
|
+
return result;
|
|
114485
|
+
}
|
|
114486
|
+
var SET_SYSTEM_VARIABLE = `object sv = dom.GetObject("##name##");
|
|
114487
|
+
if (sv) {
|
|
114488
|
+
sv.State("##value##");
|
|
114489
|
+
}`;
|
|
114490
|
+
var SET_PROGRAM_STATE = `object prog = dom.GetObject(##id##);
|
|
114491
|
+
if (prog) {
|
|
114492
|
+
prog.Active(##state##);
|
|
114493
|
+
}`;
|
|
114494
|
+
var SYSVAR_DESCRIPTIONS = `string sSysVarId;
|
|
114495
|
+
string sSep = "";
|
|
114496
|
+
WriteLine("[");
|
|
114497
|
+
foreach(sSysVarId, dom.GetObject(ID_SYSTEM_VARIABLES).EnumUsedIDs()) {
|
|
114498
|
+
object oSysVar = dom.GetObject(sSysVarId);
|
|
114499
|
+
Write(sSep);
|
|
114500
|
+
Write("{");
|
|
114501
|
+
Write("\\"id\\":");
|
|
114502
|
+
Write(oSysVar.ID());
|
|
114503
|
+
Write(",\\"description\\":\\"");
|
|
114504
|
+
Write(oSysVar.DPInfo().UriEncode());
|
|
114505
|
+
Write("\\"}");
|
|
114506
|
+
sSep = ",";
|
|
114507
|
+
}
|
|
114508
|
+
WriteLine("]");`;
|
|
114509
|
+
var GET_ROOMS_FUNCTIONS = `string sChnId;
|
|
114510
|
+
string sRoomId;
|
|
114511
|
+
string sFuncId;
|
|
114512
|
+
string sChnSepR = "";
|
|
114513
|
+
string sChnSepF = "";
|
|
114514
|
+
Write("{\\"rooms\\":{");
|
|
114515
|
+
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114516
|
+
object oChn = dom.GetObject(sChnId);
|
|
114517
|
+
if (oChn) {
|
|
114518
|
+
string sAddr = oChn.Address();
|
|
114519
|
+
object oRooms = oChn.ChnRoom();
|
|
114520
|
+
if (oRooms && oRooms.Count() > 0) {
|
|
114521
|
+
Write(sChnSepR);
|
|
114522
|
+
Write("\\"");
|
|
114523
|
+
Write(sAddr);
|
|
114524
|
+
Write("\\":[");
|
|
114525
|
+
string sRoomSep = "";
|
|
114526
|
+
foreach(sRoomId, oRooms) {
|
|
114527
|
+
object oRoom = dom.GetObject(sRoomId);
|
|
114528
|
+
Write(sRoomSep);
|
|
114529
|
+
Write("\\"");
|
|
114530
|
+
Write(oRoom.Name().UriEncode());
|
|
114531
|
+
Write("\\"");
|
|
114532
|
+
sRoomSep = ",";
|
|
114533
|
+
}
|
|
114534
|
+
Write("]");
|
|
114535
|
+
sChnSepR = ",";
|
|
114536
|
+
}
|
|
114537
|
+
}
|
|
114538
|
+
}
|
|
114539
|
+
Write("},\\"functions\\":{");
|
|
114540
|
+
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114541
|
+
object oChn = dom.GetObject(sChnId);
|
|
114542
|
+
if (oChn) {
|
|
114543
|
+
string sAddr = oChn.Address();
|
|
114544
|
+
object oFuncs = oChn.ChnFunction();
|
|
114545
|
+
if (oFuncs && oFuncs.Count() > 0) {
|
|
114546
|
+
Write(sChnSepF);
|
|
114547
|
+
Write("\\"");
|
|
114548
|
+
Write(sAddr);
|
|
114549
|
+
Write("\\":[");
|
|
114550
|
+
string sFuncSep = "";
|
|
114551
|
+
foreach(sFuncId, oFuncs) {
|
|
114552
|
+
object oFunc = dom.GetObject(sFuncId);
|
|
114553
|
+
Write(sFuncSep);
|
|
114554
|
+
Write("\\"");
|
|
114555
|
+
Write(oFunc.Name().UriEncode());
|
|
114556
|
+
Write("\\"");
|
|
114557
|
+
sFuncSep = ",";
|
|
114558
|
+
}
|
|
114559
|
+
Write("]");
|
|
114560
|
+
sChnSepF = ",";
|
|
114561
|
+
}
|
|
114562
|
+
}
|
|
114563
|
+
}
|
|
114564
|
+
Write("}}");`;
|
|
114565
|
+
var GET_SERVICE_MESSAGES = `string sAID;
|
|
114566
|
+
string sSep = "";
|
|
114567
|
+
Write("[");
|
|
114568
|
+
foreach(sAID, dom.GetObject(ID_SERVICES).EnumIDs()) {
|
|
114569
|
+
object oAlarm = dom.GetObject(sAID);
|
|
114570
|
+
if (oAlarm) {
|
|
114571
|
+
if (oAlarm.IsTypeOf(OT_ALARMDP)) {
|
|
114572
|
+
if (oAlarm.AlState() == asOncoming) {
|
|
114573
|
+
string sAddr = "";
|
|
114574
|
+
string sType = "";
|
|
114575
|
+
string sAck = "true";
|
|
114576
|
+
object oLast = dom.GetObject(oAlarm.LastTriggerID());
|
|
114577
|
+
if (oLast) {
|
|
114578
|
+
sType = oLast.HSSID();
|
|
114579
|
+
object oChn = dom.GetObject(oLast.Channel());
|
|
114580
|
+
if (oChn) {
|
|
114581
|
+
sAddr = oChn.Address();
|
|
114582
|
+
}
|
|
114583
|
+
}
|
|
114584
|
+
object oTrig = dom.GetObject(oAlarm.AlTriggerDP());
|
|
114585
|
+
if (oTrig) {
|
|
114586
|
+
if (!(oTrig.Operations() & OPERATION_WRITE)) {
|
|
114587
|
+
sAck = "false";
|
|
114588
|
+
}
|
|
114589
|
+
}
|
|
114590
|
+
Write(sSep);
|
|
114591
|
+
Write("{\\"id\\":");
|
|
114592
|
+
Write(oAlarm.ID());
|
|
114593
|
+
Write(",\\"name\\":\\"");
|
|
114594
|
+
Write(oAlarm.Name().UriEncode());
|
|
114595
|
+
Write("\\",\\"address\\":\\"");
|
|
114596
|
+
Write(sAddr);
|
|
114597
|
+
Write("\\",\\"type\\":\\"");
|
|
114598
|
+
Write(sType);
|
|
114599
|
+
Write("\\",\\"timestamp\\":");
|
|
114600
|
+
Write(oAlarm.AlOccurrenceTime().ToInteger());
|
|
114601
|
+
Write(",\\"acknowledgeable\\":");
|
|
114602
|
+
Write(sAck);
|
|
114603
|
+
Write("}");
|
|
114604
|
+
sSep = ",";
|
|
114605
|
+
}
|
|
114606
|
+
}
|
|
114607
|
+
}
|
|
114608
|
+
}
|
|
114609
|
+
Write("]");`;
|
|
114610
|
+
var ACKNOWLEDGE_SERVICE_MESSAGE = `object oAlarm = dom.GetObject(##id##);
|
|
114611
|
+
if (oAlarm) {
|
|
114612
|
+
oAlarm.AlReceipt();
|
|
114613
|
+
Write("{\\"ok\\":true}");
|
|
114614
|
+
} else {
|
|
114615
|
+
Write("{\\"ok\\":false}");
|
|
114616
|
+
}`;
|
|
114617
|
+
var GET_CCU_PLATFORM = `string sRelease;
|
|
114618
|
+
string sHost;
|
|
114619
|
+
string sErr;
|
|
114620
|
+
system.Exec("cat /VERSION", &sRelease, &sErr);
|
|
114621
|
+
system.Exec("hostname", &sHost, &sErr);
|
|
114622
|
+
Write("{\\"release\\":\\"");
|
|
114623
|
+
Write(sRelease.UriEncode());
|
|
114624
|
+
Write("\\",\\"host\\":\\"");
|
|
114625
|
+
Write(sHost.UriEncode());
|
|
114626
|
+
Write("\\"}");`;
|
|
114627
|
+
var GROUP_CREATE_PATH = "/pages/jpages/group/create";
|
|
114628
|
+
var GROUP_SAVE_PATH = "/pages/jpages/group/save";
|
|
114629
|
+
var GROUP_DELETE_PATH = "/pages/jpages/group/delete";
|
|
114630
|
+
var DRAFT_GROUP_ID = /self\.groupId\s*=\s*(\d+)/;
|
|
114631
|
+
var KNOWN_GROUP_TYPES = ["hmip.heating.group", "HomeMatic.heating"];
|
|
114632
|
+
var CHANNEL_ADDRESS = /^[0-9A-Za-z_-]+:\d+$/;
|
|
114633
|
+
var MAX_GROUP_NAME = 64;
|
|
114634
|
+
var DEFAULT_INSTALL_SECONDS = 60;
|
|
114635
|
+
var MAX_INSTALL_SECONDS = 3600;
|
|
114636
|
+
var SERVICE_MESSAGE_ID = /^\d+$/;
|
|
114637
|
+
function isRecord(value) {
|
|
114638
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
114639
|
+
}
|
|
114640
|
+
function asRecordArray(value) {
|
|
114641
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
114642
|
+
}
|
|
114643
|
+
function strOrNull(value) {
|
|
114644
|
+
if (typeof value === "string") return value === "" ? null : value;
|
|
114645
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
114646
|
+
return null;
|
|
114647
|
+
}
|
|
114648
|
+
function numOrNull(value) {
|
|
114649
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
114650
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
114651
|
+
return Number.isFinite(num) ? num : null;
|
|
114652
|
+
}
|
|
114653
|
+
var INTERFACES_BY_NAME = new Map(Object.values(Interface).map((iface) => [iface.toString(), iface]));
|
|
114654
|
+
function toInterface(name) {
|
|
114655
|
+
const match = INTERFACES_BY_NAME.get(name);
|
|
114656
|
+
if (match === void 0) throw new ValidationError(`Unknown interface "${name}".`);
|
|
114657
|
+
return match;
|
|
114658
|
+
}
|
|
114659
|
+
function toGateway(info) {
|
|
114660
|
+
return { ...info };
|
|
114661
|
+
}
|
|
114662
|
+
function decodedOrNull(value) {
|
|
114663
|
+
if (typeof value !== "string") return null;
|
|
114664
|
+
const text = decodeRegaName(value).trim();
|
|
114665
|
+
return text === "" ? null : text;
|
|
114666
|
+
}
|
|
114667
|
+
function parseKeyValueLines(text) {
|
|
114668
|
+
const out = /* @__PURE__ */ new Map();
|
|
114669
|
+
for (const line of text.split(/\r?\n/)) {
|
|
114670
|
+
const eq = line.indexOf("=");
|
|
114671
|
+
if (eq > 0) out.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
114672
|
+
}
|
|
114673
|
+
return out;
|
|
114674
|
+
}
|
|
114675
|
+
var OPENCCU_PRODUCTS = /* @__PURE__ */ new Set([
|
|
114676
|
+
"raspberrymatic",
|
|
114677
|
+
"openccu",
|
|
114678
|
+
"ova"
|
|
114679
|
+
]);
|
|
114680
|
+
function productFamily(raw) {
|
|
114681
|
+
if (raw === null) return "unknown";
|
|
114682
|
+
const key = raw.toLowerCase();
|
|
114683
|
+
if (key === "ccu3") return "ccu3";
|
|
114684
|
+
return OPENCCU_PRODUCTS.has(key) ? "openccu" : "unknown";
|
|
114685
|
+
}
|
|
114686
|
+
var CcuManagement = class {
|
|
114687
|
+
json;
|
|
114688
|
+
webUi;
|
|
114689
|
+
getSessionId;
|
|
114690
|
+
interfaceClient;
|
|
114691
|
+
configuredInterfaces;
|
|
114692
|
+
constructor(options) {
|
|
114693
|
+
this.json = options.json;
|
|
114694
|
+
this.webUi = options.webUi;
|
|
114695
|
+
this.getSessionId = options.getSessionId;
|
|
114696
|
+
this.interfaceClient = options.interfaceClient;
|
|
114697
|
+
this.configuredInterfaces = options.configuredInterfaces;
|
|
114698
|
+
}
|
|
114699
|
+
/**
|
|
114700
|
+
* Every interface the CCU offers (JSON-RPC `Interface.listInterfaces`), with
|
|
114701
|
+
* connection state, duty cycle and, for BidCos, the radio gateways. Without a
|
|
114702
|
+
* JSON-RPC client only the configured interfaces are listed and every probe
|
|
114703
|
+
* result is `null`.
|
|
114704
|
+
*/
|
|
114705
|
+
async listInterfaces() {
|
|
114706
|
+
const offered = await this.offeredInterfaces();
|
|
114707
|
+
const gatewaysByName = /* @__PURE__ */ new Map();
|
|
114708
|
+
for (const entry of offered) {
|
|
114709
|
+
const iface = INTERFACES_BY_NAME.get(entry.name);
|
|
114710
|
+
const client = iface === void 0 ? void 0 : this.interfaceClient(iface);
|
|
114711
|
+
if (client === void 0 || iface !== "BidCos-RF" && iface !== "HmIP-RF") continue;
|
|
114712
|
+
const gateways = await this.tryOrNull(() => client.listBidcosInterfaces());
|
|
114713
|
+
if (gateways !== null) gatewaysByName.set(entry.name, gateways.map(toGateway));
|
|
114714
|
+
}
|
|
114715
|
+
const fallbackDuty = offered.some((e) => e.name === "BidCos-RF".toString() && (gatewaysByName.get(e.name) ?? []).every((g) => g.dutyCycle === null)) ? await this.ownRadioDutyCycle() : null;
|
|
114716
|
+
const result = [];
|
|
114717
|
+
for (const entry of offered) {
|
|
114718
|
+
const gateways = gatewaysByName.get(entry.name) ?? [];
|
|
114719
|
+
const primary = gateways.find((g) => g.isDefault) ?? gateways[0];
|
|
114720
|
+
const connected = primary?.connected ?? await this.isPresent(entry.name);
|
|
114721
|
+
const dutyCycle = primary?.dutyCycle ?? (entry.name === "BidCos-RF".toString() ? fallbackDuty : null);
|
|
114722
|
+
result.push({
|
|
114723
|
+
name: entry.name,
|
|
114724
|
+
port: entry.port,
|
|
114725
|
+
info: entry.info,
|
|
114726
|
+
connected,
|
|
114727
|
+
dutyCycle,
|
|
114728
|
+
firmware: primary?.firmware ?? null,
|
|
114729
|
+
type: primary?.type ?? null,
|
|
114730
|
+
gateways
|
|
114731
|
+
});
|
|
114732
|
+
}
|
|
114733
|
+
return result;
|
|
114734
|
+
}
|
|
114735
|
+
/** Open (default 60 s) or close the pairing window of a configured interface. */
|
|
114736
|
+
async setInstallMode(request) {
|
|
114737
|
+
const client = this.requireClient(request.interface);
|
|
114738
|
+
if (!request.on) {
|
|
114739
|
+
await client.setInstallMode({ on: false });
|
|
114740
|
+
return;
|
|
114741
|
+
}
|
|
114742
|
+
const seconds = request.seconds ?? DEFAULT_INSTALL_SECONDS;
|
|
114743
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_INSTALL_SECONDS) throw new ValidationError(`seconds must be an integer between 1 and ${MAX_INSTALL_SECONDS}, got ${String(seconds)}.`);
|
|
114744
|
+
const call = request.mode === void 0 ? {
|
|
114745
|
+
on: true,
|
|
114746
|
+
seconds
|
|
114747
|
+
} : {
|
|
114748
|
+
on: true,
|
|
114749
|
+
seconds,
|
|
114750
|
+
mode: request.mode === "secure" ? 2 : 1
|
|
114751
|
+
};
|
|
114752
|
+
await client.setInstallMode(call);
|
|
114753
|
+
}
|
|
114754
|
+
/** Seconds left of the pairing window; `null` when the CCU's answer was unusable. */
|
|
114755
|
+
async getInstallMode(interfaceName) {
|
|
114756
|
+
return this.requireClient(interfaceName).getInstallModeRemaining();
|
|
114757
|
+
}
|
|
114758
|
+
/** Every active service message (ReGa listing of `ID_SERVICES`). */
|
|
114759
|
+
async serviceMessages() {
|
|
114760
|
+
return asRecordArray(await this.rega("ReGa.runScript(serviceMessages)", GET_SERVICE_MESSAGES)).flatMap((entry) => {
|
|
114761
|
+
const id = strOrNull(entry["id"]);
|
|
114762
|
+
if (id === null) return [];
|
|
114763
|
+
const address = strOrNull(entry["address"]);
|
|
114764
|
+
const colon = address === null ? -1 : address.lastIndexOf(":");
|
|
114765
|
+
const device = address === null ? null : colon === -1 ? address : address.slice(0, colon);
|
|
114766
|
+
const channel = address === null || colon === -1 ? null : numOrNull(address.slice(colon + 1));
|
|
114767
|
+
const seconds = numOrNull(entry["timestamp"]);
|
|
114768
|
+
return [{
|
|
114769
|
+
id,
|
|
114770
|
+
name: decodedOrNull(entry["name"]),
|
|
114771
|
+
address: device,
|
|
114772
|
+
channel,
|
|
114773
|
+
type: strOrNull(entry["type"]),
|
|
114774
|
+
timestamp: seconds === null || seconds <= 0 ? null : seconds * 1e3,
|
|
114775
|
+
acknowledgeable: entry["acknowledgeable"] === true
|
|
114776
|
+
}];
|
|
114777
|
+
});
|
|
114778
|
+
}
|
|
114779
|
+
/** Acknowledge one service message by its id. */
|
|
114780
|
+
async acknowledgeServiceMessage(id) {
|
|
114781
|
+
if (!SERVICE_MESSAGE_ID.test(id)) throw new ValidationError(`Invalid service message id "${id}".`);
|
|
114782
|
+
const raw = await this.rega("ReGa.runScript(acknowledgeServiceMessage)", ACKNOWLEDGE_SERVICE_MESSAGE, { id });
|
|
114783
|
+
if (!isRecord(raw) || raw["ok"] !== true) throw new ValidationError(`Unknown service message "${id}".`);
|
|
114784
|
+
}
|
|
114785
|
+
/** Version, product, serial and hostname; each `null` when unanswered. */
|
|
114786
|
+
async ccuInfo() {
|
|
114787
|
+
const [version, serial, platform] = await Promise.allSettled([
|
|
114788
|
+
this.call(JsonRpcMethod.CCU_GET_VERSION),
|
|
114789
|
+
this.call(JsonRpcMethod.CCU_GET_SERIAL),
|
|
114790
|
+
this.rega("ReGa.runScript(ccuPlatform)", GET_CCU_PLATFORM)
|
|
114791
|
+
]);
|
|
114792
|
+
if (version.status === "rejected" && serial.status === "rejected" && platform.status === "rejected") throw version.reason instanceof CcuManagementError ? version.reason : new CcuManagementError(JsonRpcMethod.CCU_GET_VERSION, version.reason);
|
|
114793
|
+
const release = platform.status === "fulfilled" && isRecord(platform.value) ? parseKeyValueLines(decodeRegaName(releaseText(platform.value))) : /* @__PURE__ */ new Map();
|
|
114794
|
+
const hostname = platform.status === "fulfilled" && isRecord(platform.value) ? decodedOrNull(platform.value["host"]) : null;
|
|
114795
|
+
const product = strOrNull(release.get("PRODUCT"));
|
|
114796
|
+
return {
|
|
114797
|
+
version: version.status === "fulfilled" ? strOrNull(version.value) : null,
|
|
114798
|
+
product,
|
|
114799
|
+
productFamily: productFamily(product),
|
|
114800
|
+
platform: strOrNull(release.get("PLATFORM")),
|
|
114801
|
+
serial: serial.status === "fulfilled" ? strOrNull(serial.value) : null,
|
|
114802
|
+
hostname
|
|
114803
|
+
};
|
|
114804
|
+
}
|
|
114805
|
+
/**
|
|
114806
|
+
* The heating groups of the CCU ("Gruppen"), read from `CCU.getHeatingGroupList`
|
|
114807
|
+
* (the HMServer's `/etc/config/groups.gson`). `[]` when the CCU has no groups
|
|
114808
|
+
* file. Writes (`createGroup` / `updateGroup` / `deleteGroup`) go through the
|
|
114809
|
+
* HMServer's WebUI pages instead, see the group-writes section below.
|
|
114810
|
+
*/
|
|
114811
|
+
async groups() {
|
|
114812
|
+
const raw = await this.call(JsonRpcMethod.CCU_GET_HEATING_GROUP_LIST);
|
|
114813
|
+
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
114814
|
+
let parsed;
|
|
114815
|
+
try {
|
|
114816
|
+
parsed = JSON.parse(raw);
|
|
114817
|
+
} catch (err) {
|
|
114818
|
+
throw new CcuManagementError(JsonRpcMethod.CCU_GET_HEATING_GROUP_LIST, err);
|
|
114819
|
+
}
|
|
114820
|
+
if (!isRecord(parsed)) return [];
|
|
114821
|
+
return Object.keys(parsed).sort().flatMap((key) => asRecordArray(parsed[key])).flatMap((entry) => {
|
|
114822
|
+
const group = toGroup(entry);
|
|
114823
|
+
return group === null ? [] : [group];
|
|
114824
|
+
});
|
|
114825
|
+
}
|
|
114826
|
+
/** Create a group; resolves with the group as read back from the CCU. */
|
|
114827
|
+
async createGroup(input) {
|
|
114828
|
+
const name = validateGroupName(input.name);
|
|
114829
|
+
const members = validateMembers(input.members);
|
|
114830
|
+
const before = await this.groups();
|
|
114831
|
+
this.validateType(input.type, before);
|
|
114832
|
+
const { webUi, sid } = await this.requireWebUi("group/create");
|
|
114833
|
+
const page = checkWebUiResponse(GROUP_CREATE_PATH, await webUi.get(GROUP_CREATE_PATH, sid));
|
|
114834
|
+
const draft = DRAFT_GROUP_ID.exec(page);
|
|
114835
|
+
if (draft === null || draft[1] === void 0) throw new CcuManagementError(GROUP_CREATE_PATH, new ClientError("no group id in the page"));
|
|
114836
|
+
const draftId = Number(draft[1]);
|
|
114837
|
+
const id = numOrNull(await this.saveGroup(webUi, sid, {
|
|
114838
|
+
groupId: draftId,
|
|
114839
|
+
groupName: jsEscape(name),
|
|
114840
|
+
groupTypeId: input.type,
|
|
114841
|
+
forbidSingleOperation: false,
|
|
114842
|
+
assignedDevicesIds: members,
|
|
114843
|
+
isNewGroup: true,
|
|
114844
|
+
groupDeviceName: `${name} `
|
|
114845
|
+
})) ?? draftId;
|
|
114846
|
+
await this.flagMembers(members, "true");
|
|
114847
|
+
return this.verifyPresent(id, {
|
|
114848
|
+
name,
|
|
114849
|
+
type: input.type,
|
|
114850
|
+
members
|
|
114851
|
+
});
|
|
114852
|
+
}
|
|
114853
|
+
/** Change a group's name and/or member list; resolves with the group read back. */
|
|
114854
|
+
async updateGroup(id, patch) {
|
|
114855
|
+
if (patch.name === void 0 && patch.members === void 0) throw new ValidationError("updateGroup needs a name and/or members.");
|
|
114856
|
+
const name = patch.name === void 0 ? void 0 : validateGroupName(patch.name);
|
|
114857
|
+
const members = patch.members === void 0 ? void 0 : validateMembers(patch.members);
|
|
114858
|
+
const current = await this.requireGroup(id);
|
|
114859
|
+
const { webUi, sid } = await this.requireWebUi("group/save");
|
|
114860
|
+
const nextName = name ?? current.name;
|
|
114861
|
+
const nextMembers = members ?? current.members.map((m) => m.address);
|
|
114862
|
+
await this.saveGroup(webUi, sid, {
|
|
114863
|
+
groupId: id,
|
|
114864
|
+
groupName: jsEscape(nextName),
|
|
114865
|
+
groupTypeId: current.type,
|
|
114866
|
+
forbidSingleOperation: current.forbidSingleOperation,
|
|
114867
|
+
assignedDevicesIds: nextMembers,
|
|
114868
|
+
isNewGroup: false,
|
|
114869
|
+
groupDeviceName: current.deviceAddress === null ? nextName : `${nextName} ${virtualDeviceSerial(id)}`
|
|
114870
|
+
});
|
|
114871
|
+
await this.flagMembers(nextMembers, "true");
|
|
114872
|
+
const kept = new Set(nextMembers);
|
|
114873
|
+
await this.flagMembers(current.members.map((m) => m.address).filter((a) => !kept.has(a)), "false");
|
|
114874
|
+
return this.verifyPresent(id, {
|
|
114875
|
+
name: nextName,
|
|
114876
|
+
type: current.type,
|
|
114877
|
+
members: nextMembers
|
|
114878
|
+
});
|
|
114879
|
+
}
|
|
114880
|
+
/** Delete a group; verified by the group being gone from the read-back. */
|
|
114881
|
+
async deleteGroup(id) {
|
|
114882
|
+
const current = await this.requireGroup(id);
|
|
114883
|
+
const { webUi, sid } = await this.requireWebUi("group/delete");
|
|
114884
|
+
checkWebUiResponse(GROUP_DELETE_PATH, await webUi.post(GROUP_DELETE_PATH, sid, JSON.stringify({ groupId: id })));
|
|
114885
|
+
await this.flagMembers(current.members.map((m) => m.address), "false");
|
|
114886
|
+
if ((await this.groups()).some((g) => g.id === id)) throw new GroupWriteVerificationError(`Group ${id} is still listed after deleteGroup.`);
|
|
114887
|
+
}
|
|
114888
|
+
async requireGroup(id) {
|
|
114889
|
+
if (!Number.isInteger(id)) throw new ValidationError(`Invalid group id ${String(id)}.`);
|
|
114890
|
+
const found = (await this.groups()).find((g) => g.id === id);
|
|
114891
|
+
if (found === void 0) throw new ValidationError(`Unknown group ${id}.`);
|
|
114892
|
+
return found;
|
|
114893
|
+
}
|
|
114894
|
+
validateType(type, existing) {
|
|
114895
|
+
const known = /* @__PURE__ */ new Set([...KNOWN_GROUP_TYPES, ...existing.map((g) => g.type)]);
|
|
114896
|
+
if (!known.has(type)) throw new ValidationError(`Unknown group type "${type}" (known: ${[...known].join(", ")}).`);
|
|
114897
|
+
}
|
|
114898
|
+
async requireWebUi(label) {
|
|
114899
|
+
if (this.webUi === void 0) throw new ValidationError(`${label} needs WebUI credentials (HMServer pages); none are configured.`);
|
|
114900
|
+
const sid = await this.getSessionId();
|
|
114901
|
+
if (sid === void 0) throw new ValidationError(`${label} needs a WebUI session.`);
|
|
114902
|
+
return {
|
|
114903
|
+
webUi: this.webUi,
|
|
114904
|
+
sid
|
|
114905
|
+
};
|
|
114906
|
+
}
|
|
114907
|
+
/** POST `group/save`; returns the response `content` (the group id as text). */
|
|
114908
|
+
async saveGroup(webUi, sid, payload) {
|
|
114909
|
+
return checkWebUiResponse(GROUP_SAVE_PATH, await webUi.post(GROUP_SAVE_PATH, sid, JSON.stringify(payload)));
|
|
114910
|
+
}
|
|
114911
|
+
/**
|
|
114912
|
+
* The WebUI marks every device it puts in / takes out of a heating group with
|
|
114913
|
+
* `Interface.setMetadata inHeatingGroup` (its pages use the flag to hide
|
|
114914
|
+
* grouped devices). Best effort: the hint is cosmetic and the group itself is
|
|
114915
|
+
* verified separately.
|
|
114916
|
+
*/
|
|
114917
|
+
async flagMembers(addresses, value) {
|
|
114918
|
+
const devices = [...new Set(addresses.map((a) => a.slice(0, a.lastIndexOf(":"))))];
|
|
114919
|
+
for (const address of devices) try {
|
|
114920
|
+
const objectId = await this.call(JsonRpcMethod.DEVICE_GET_REGA_ID_BY_ADDRESS, { address });
|
|
114921
|
+
if (typeof objectId !== "string" || objectId === "" || objectId === "noDeviceFound") continue;
|
|
114922
|
+
await this.call(JsonRpcMethod.INTERFACE_SET_METADATA, {
|
|
114923
|
+
objectId,
|
|
114924
|
+
dataId: "inHeatingGroup",
|
|
114925
|
+
value
|
|
114926
|
+
});
|
|
114927
|
+
} catch {}
|
|
114928
|
+
}
|
|
114929
|
+
async verifyPresent(id, want) {
|
|
114930
|
+
const found = (await this.groups()).find((g) => g.id === id);
|
|
114931
|
+
if (found === void 0) throw new GroupWriteVerificationError(`Group ${id} is not listed after the write.`);
|
|
114932
|
+
const have = found.members.map((m) => m.address).sort();
|
|
114933
|
+
const expected = [...want.members].sort();
|
|
114934
|
+
const sameMembers = have.length === expected.length && have.every((a, i) => a === expected[i]);
|
|
114935
|
+
if (found.name !== want.name || found.type !== want.type || !sameMembers) throw new GroupWriteVerificationError(`Group ${id} reads back as "${found.name}" (${found.type}, ${have.length} members), expected "${want.name}" (${want.type}, ${expected.length} members).`);
|
|
114936
|
+
return found;
|
|
114937
|
+
}
|
|
114938
|
+
requireClient(interfaceName) {
|
|
114939
|
+
const iface = toInterface(interfaceName);
|
|
114940
|
+
const client = this.interfaceClient(iface);
|
|
114941
|
+
if (client === void 0) throw new ValidationError(`Interface "${interfaceName}" is not configured.`);
|
|
114942
|
+
return client;
|
|
114943
|
+
}
|
|
114944
|
+
requireJson(method) {
|
|
114945
|
+
if (this.json === void 0) throw new ValidationError(`${method} needs WebUI credentials (JSON-RPC); none are configured.`);
|
|
114946
|
+
return this.json;
|
|
114947
|
+
}
|
|
114948
|
+
/** A session-bound JSON-RPC call whose failure names the method. */
|
|
114949
|
+
async call(method, params) {
|
|
114950
|
+
const json = this.requireJson(method);
|
|
114951
|
+
try {
|
|
114952
|
+
const sessionId = await this.getSessionId();
|
|
114953
|
+
return await json.post(method, params, sessionId !== void 0 ? { sessionId } : void 0);
|
|
114954
|
+
} catch (err) {
|
|
114955
|
+
throw err instanceof ValidationError ? err : new CcuManagementError(method, err);
|
|
114956
|
+
}
|
|
114957
|
+
}
|
|
114958
|
+
async rega(label, script, params) {
|
|
114959
|
+
const json = this.requireJson(label);
|
|
114960
|
+
try {
|
|
114961
|
+
return await runScript(json, await this.getSessionId(), script, params);
|
|
114962
|
+
} catch (err) {
|
|
114963
|
+
throw err instanceof ValidationError ? err : new CcuManagementError(label, err);
|
|
114964
|
+
}
|
|
114965
|
+
}
|
|
114966
|
+
async tryOrNull(fn) {
|
|
114967
|
+
try {
|
|
114968
|
+
return await fn();
|
|
114969
|
+
} catch {
|
|
114970
|
+
return null;
|
|
114971
|
+
}
|
|
114972
|
+
}
|
|
114973
|
+
async offeredInterfaces() {
|
|
114974
|
+
if (this.json === void 0) return this.configuredInterfaces().map((iface) => ({
|
|
114975
|
+
name: iface,
|
|
114976
|
+
port: null,
|
|
114977
|
+
info: null
|
|
114978
|
+
}));
|
|
114979
|
+
return asRecordArray(await this.call(JsonRpcMethod.INTERFACE_LIST_INTERFACES)).flatMap((entry) => {
|
|
114980
|
+
const name = strOrNull(entry["name"]);
|
|
114981
|
+
return name === null ? [] : [{
|
|
114982
|
+
name,
|
|
114983
|
+
port: numOrNull(entry["port"]),
|
|
114984
|
+
info: strOrNull(entry["info"])
|
|
114985
|
+
}];
|
|
114986
|
+
});
|
|
114987
|
+
}
|
|
114988
|
+
async isPresent(name) {
|
|
114989
|
+
if (this.json === void 0) return null;
|
|
114990
|
+
const answer = await this.tryOrNull(() => this.call(JsonRpcMethod.INTERFACE_IS_PRESENT, { interface: name }));
|
|
114991
|
+
if (typeof answer === "boolean") return answer;
|
|
114992
|
+
if (answer === "true") return true;
|
|
114993
|
+
if (answer === "false") return false;
|
|
114994
|
+
return null;
|
|
114995
|
+
}
|
|
114996
|
+
/**
|
|
114997
|
+
* Fallback when `listBidcosInterfaces` gave no answer: `Interface.getDutyCycle`
|
|
114998
|
+
* (RaspberryMatic / OpenCCU) reports the CCU's own radio module as ONE entry
|
|
114999
|
+
* with string numbers (`"7"`). It cannot be attributed to HmIP, so it only
|
|
115000
|
+
* ever stands in for BidCos-RF; non-numeric or absent is `null`.
|
|
115001
|
+
*/
|
|
115002
|
+
async ownRadioDutyCycle() {
|
|
115003
|
+
if (this.json === void 0) return null;
|
|
115004
|
+
const entries = asRecordArray(await this.tryOrNull(() => this.call(JsonRpcMethod.INTERFACE_GET_DUTY_CYCLE)));
|
|
115005
|
+
return entries.length === 1 ? numOrNull(entries[0]?.["dutyCycle"]) : null;
|
|
115006
|
+
}
|
|
115007
|
+
};
|
|
115008
|
+
function releaseText(value) {
|
|
115009
|
+
const release = value["release"];
|
|
115010
|
+
return typeof release === "string" ? release : "";
|
|
115011
|
+
}
|
|
115012
|
+
var DEVICE_ADDRESS_SUFFIX = /\b(INT\d{7})\s*$/;
|
|
115013
|
+
function toGroup(entry) {
|
|
115014
|
+
const id = numOrNull(entry["id"]);
|
|
115015
|
+
if (id === null) return null;
|
|
115016
|
+
const props = isRecord(entry["groupProperties"]) ? entry["groupProperties"] : {};
|
|
115017
|
+
const groupType = isRecord(entry["groupType"]) ? entry["groupType"] : {};
|
|
115018
|
+
const deviceName = strOrNull(props["GROUP_DEVICE_NAME"]);
|
|
115019
|
+
const deviceMatch = deviceName === null ? null : DEVICE_ADDRESS_SUFFIX.exec(deviceName);
|
|
115020
|
+
const members = asRecordArray(entry["groupMembers"]).flatMap((m) => {
|
|
115021
|
+
const address = strOrNull(m["id"]);
|
|
115022
|
+
if (address === null) return [];
|
|
115023
|
+
return [{
|
|
115024
|
+
address,
|
|
115025
|
+
type: (isRecord(m["memberType"]) ? strOrNull(m["memberType"]["id"]) : null) ?? ""
|
|
115026
|
+
}];
|
|
115027
|
+
});
|
|
115028
|
+
return {
|
|
115029
|
+
id,
|
|
115030
|
+
name: (strOrNull(props["NAME"]) ?? "").trim(),
|
|
115031
|
+
type: strOrNull(groupType["id"]) ?? "",
|
|
115032
|
+
typeLabel: strOrNull(groupType["label"]),
|
|
115033
|
+
deviceAddress: deviceMatch === null ? null : deviceMatch[1] ?? null,
|
|
115034
|
+
forbidSingleOperation: props["FORBID_SINGLE_OPERATION"] === true,
|
|
115035
|
+
members
|
|
115036
|
+
};
|
|
115037
|
+
}
|
|
115038
|
+
function virtualDeviceSerial(id) {
|
|
115039
|
+
return `INT${String(id).padStart(7, "0")}`;
|
|
115040
|
+
}
|
|
115041
|
+
function validateGroupName(raw) {
|
|
115042
|
+
const name = raw.replace(/"/g, "'").trim();
|
|
115043
|
+
if (name === "" || name.length > MAX_GROUP_NAME) throw new ValidationError(`Group name must be 1..${MAX_GROUP_NAME} characters.`);
|
|
115044
|
+
return name;
|
|
115045
|
+
}
|
|
115046
|
+
function validateMembers(members) {
|
|
115047
|
+
for (const address of members) if (!CHANNEL_ADDRESS.test(address)) throw new ValidationError(`Invalid member channel address "${address}".`);
|
|
115048
|
+
return [...new Set(members)];
|
|
115049
|
+
}
|
|
115050
|
+
function checkWebUiResponse(path$5, text) {
|
|
115051
|
+
let parsed;
|
|
115052
|
+
try {
|
|
115053
|
+
parsed = JSON.parse(text);
|
|
115054
|
+
} catch {
|
|
115055
|
+
throw new CcuManagementError(path$5, new ClientError("response was not JSON"));
|
|
115056
|
+
}
|
|
115057
|
+
if (!isRecord(parsed) || parsed["isSuccessful"] !== true) {
|
|
115058
|
+
const code = isRecord(parsed) ? strOrNull(parsed["errorCode"]) : null;
|
|
115059
|
+
throw new CcuManagementError(path$5, new ClientError(code === "42" ? "session expired (errorCode 42)" : `refused (errorCode ${code ?? "none"})`));
|
|
115060
|
+
}
|
|
115061
|
+
const content = parsed["content"];
|
|
115062
|
+
return typeof content === "string" || typeof content === "number" ? String(content) : "";
|
|
115063
|
+
}
|
|
113510
115064
|
var FAULT_METHOD_NOT_FOUND = -32601;
|
|
113511
115065
|
var FAULT_INTERNAL_ERROR = -32603;
|
|
113512
115066
|
var MethodNotFoundError = class extends BaseHomematicError {};
|
|
@@ -114414,128 +115968,6 @@ function asString(value) {
|
|
|
114414
115968
|
if (typeof value === "string" && value !== "") return value;
|
|
114415
115969
|
if (typeof value === "number") return String(value);
|
|
114416
115970
|
}
|
|
114417
|
-
function escapeRegaString(s) {
|
|
114418
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
114419
|
-
}
|
|
114420
|
-
var CONTROL_CHARS = /[\u0000-\u001F]/g;
|
|
114421
|
-
function sanitizeJsonControlChars(s) {
|
|
114422
|
-
return s.replace(CONTROL_CHARS, "");
|
|
114423
|
-
}
|
|
114424
|
-
function decodeRegaName(encoded) {
|
|
114425
|
-
const bytes = [];
|
|
114426
|
-
for (let i = 0; i < encoded.length; i += 1) {
|
|
114427
|
-
const ch = encoded[i];
|
|
114428
|
-
if (ch === "%" && i + 2 < encoded.length) {
|
|
114429
|
-
const hex = encoded.slice(i + 1, i + 3);
|
|
114430
|
-
const byte = Number.parseInt(hex, 16);
|
|
114431
|
-
if (!Number.isNaN(byte) && /^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
114432
|
-
bytes.push(byte);
|
|
114433
|
-
i += 2;
|
|
114434
|
-
continue;
|
|
114435
|
-
}
|
|
114436
|
-
}
|
|
114437
|
-
if (ch === "+") {
|
|
114438
|
-
bytes.push(32);
|
|
114439
|
-
continue;
|
|
114440
|
-
}
|
|
114441
|
-
bytes.push(ch.charCodeAt(0) & 255);
|
|
114442
|
-
}
|
|
114443
|
-
return Buffer.from(bytes).toString("latin1");
|
|
114444
|
-
}
|
|
114445
|
-
function substitute(body, params) {
|
|
114446
|
-
let out = body;
|
|
114447
|
-
for (const [key, value] of Object.entries(params)) {
|
|
114448
|
-
const escaped = escapeRegaString(String(value));
|
|
114449
|
-
out = out.split(`##${key}##`).join(escaped);
|
|
114450
|
-
}
|
|
114451
|
-
return out;
|
|
114452
|
-
}
|
|
114453
|
-
async function runScript(client, sessionId, body, params) {
|
|
114454
|
-
const script = params ? substitute(body, params) : body;
|
|
114455
|
-
const result = await client.post(JsonRpcMethod.REGA_RUN_SCRIPT, { script }, sessionId !== void 0 ? { sessionId } : void 0);
|
|
114456
|
-
if (result === void 0 || result === null) throw new ClientError("ReGa.runScript returned no result");
|
|
114457
|
-
if (typeof result === "string") return JSON.parse(sanitizeJsonControlChars(result));
|
|
114458
|
-
return result;
|
|
114459
|
-
}
|
|
114460
|
-
var SET_SYSTEM_VARIABLE = `object sv = dom.GetObject("##name##");
|
|
114461
|
-
if (sv) {
|
|
114462
|
-
sv.State("##value##");
|
|
114463
|
-
}`;
|
|
114464
|
-
var SET_PROGRAM_STATE = `object prog = dom.GetObject(##id##);
|
|
114465
|
-
if (prog) {
|
|
114466
|
-
prog.Active(##state##);
|
|
114467
|
-
}`;
|
|
114468
|
-
var SYSVAR_DESCRIPTIONS = `string sSysVarId;
|
|
114469
|
-
string sSep = "";
|
|
114470
|
-
WriteLine("[");
|
|
114471
|
-
foreach(sSysVarId, dom.GetObject(ID_SYSTEM_VARIABLES).EnumUsedIDs()) {
|
|
114472
|
-
object oSysVar = dom.GetObject(sSysVarId);
|
|
114473
|
-
Write(sSep);
|
|
114474
|
-
Write("{");
|
|
114475
|
-
Write("\\"id\\":");
|
|
114476
|
-
Write(oSysVar.ID());
|
|
114477
|
-
Write(",\\"description\\":\\"");
|
|
114478
|
-
Write(oSysVar.DPInfo().UriEncode());
|
|
114479
|
-
Write("\\"}");
|
|
114480
|
-
sSep = ",";
|
|
114481
|
-
}
|
|
114482
|
-
WriteLine("]");`;
|
|
114483
|
-
var GET_ROOMS_FUNCTIONS = `string sChnId;
|
|
114484
|
-
string sRoomId;
|
|
114485
|
-
string sFuncId;
|
|
114486
|
-
string sChnSepR = "";
|
|
114487
|
-
string sChnSepF = "";
|
|
114488
|
-
Write("{\\"rooms\\":{");
|
|
114489
|
-
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114490
|
-
object oChn = dom.GetObject(sChnId);
|
|
114491
|
-
if (oChn) {
|
|
114492
|
-
string sAddr = oChn.Address();
|
|
114493
|
-
object oRooms = oChn.ChnRoom();
|
|
114494
|
-
if (oRooms && oRooms.Count() > 0) {
|
|
114495
|
-
Write(sChnSepR);
|
|
114496
|
-
Write("\\"");
|
|
114497
|
-
Write(sAddr);
|
|
114498
|
-
Write("\\":[");
|
|
114499
|
-
string sRoomSep = "";
|
|
114500
|
-
foreach(sRoomId, oRooms) {
|
|
114501
|
-
object oRoom = dom.GetObject(sRoomId);
|
|
114502
|
-
Write(sRoomSep);
|
|
114503
|
-
Write("\\"");
|
|
114504
|
-
Write(oRoom.Name().UriEncode());
|
|
114505
|
-
Write("\\"");
|
|
114506
|
-
sRoomSep = ",";
|
|
114507
|
-
}
|
|
114508
|
-
Write("]");
|
|
114509
|
-
sChnSepR = ",";
|
|
114510
|
-
}
|
|
114511
|
-
}
|
|
114512
|
-
}
|
|
114513
|
-
Write("},\\"functions\\":{");
|
|
114514
|
-
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114515
|
-
object oChn = dom.GetObject(sChnId);
|
|
114516
|
-
if (oChn) {
|
|
114517
|
-
string sAddr = oChn.Address();
|
|
114518
|
-
object oFuncs = oChn.ChnFunction();
|
|
114519
|
-
if (oFuncs && oFuncs.Count() > 0) {
|
|
114520
|
-
Write(sChnSepF);
|
|
114521
|
-
Write("\\"");
|
|
114522
|
-
Write(sAddr);
|
|
114523
|
-
Write("\\":[");
|
|
114524
|
-
string sFuncSep = "";
|
|
114525
|
-
foreach(sFuncId, oFuncs) {
|
|
114526
|
-
object oFunc = dom.GetObject(sFuncId);
|
|
114527
|
-
Write(sFuncSep);
|
|
114528
|
-
Write("\\"");
|
|
114529
|
-
Write(oFunc.Name().UriEncode());
|
|
114530
|
-
Write("\\"");
|
|
114531
|
-
sFuncSep = ",";
|
|
114532
|
-
}
|
|
114533
|
-
Write("]");
|
|
114534
|
-
sChnSepF = ",";
|
|
114535
|
-
}
|
|
114536
|
-
}
|
|
114537
|
-
}
|
|
114538
|
-
Write("}}");`;
|
|
114539
115971
|
var HubValueType = /* @__PURE__ */ ((HubValueType2) => {
|
|
114540
115972
|
HubValueType2["ALARM"] = "ALARM";
|
|
114541
115973
|
HubValueType2["FLOAT"] = "FLOAT";
|
|
@@ -114574,7 +116006,7 @@ function parseSysVarValue(type, raw) {
|
|
|
114574
116006
|
}
|
|
114575
116007
|
}
|
|
114576
116008
|
var WRITABLE_MARKER = "HAHM";
|
|
114577
|
-
function
|
|
116009
|
+
function isRecord2(value) {
|
|
114578
116010
|
return value !== null && typeof value === "object";
|
|
114579
116011
|
}
|
|
114580
116012
|
function asString2(value) {
|
|
@@ -114593,9 +116025,9 @@ function asOptionalNumber(value) {
|
|
|
114593
116025
|
const num = Number(value);
|
|
114594
116026
|
return Number.isFinite(num) ? num : void 0;
|
|
114595
116027
|
}
|
|
114596
|
-
function
|
|
116028
|
+
function asRecordArray2(value) {
|
|
114597
116029
|
if (!Array.isArray(value)) return [];
|
|
114598
|
-
return value.filter(
|
|
116030
|
+
return value.filter(isRecord2);
|
|
114599
116031
|
}
|
|
114600
116032
|
var HubFetcher = class {
|
|
114601
116033
|
client;
|
|
@@ -114614,7 +116046,7 @@ var HubFetcher = class {
|
|
|
114614
116046
|
*/
|
|
114615
116047
|
async fetchSystemVariables() {
|
|
114616
116048
|
const sid = this.sid();
|
|
114617
|
-
const entries =
|
|
116049
|
+
const entries = asRecordArray2(await this.client.post(JsonRpcMethod.SYSVAR_GET_ALL, void 0, sidOpts(sid)));
|
|
114618
116050
|
const descriptions = await this.fetchSysVarDescriptions();
|
|
114619
116051
|
return entries.map((entry) => this.toSystemVariable(entry, descriptions));
|
|
114620
116052
|
}
|
|
@@ -114656,7 +116088,7 @@ var HubFetcher = class {
|
|
|
114656
116088
|
/** Fetch all programs, coercing flags to booleans. */
|
|
114657
116089
|
async fetchPrograms() {
|
|
114658
116090
|
const sid = this.sid();
|
|
114659
|
-
return
|
|
116091
|
+
return asRecordArray2(await this.client.post(JsonRpcMethod.PROGRAM_GET_ALL, void 0, sidOpts(sid))).map((entry) => {
|
|
114660
116092
|
const lastExecuteTime = entry.lastExecuteTime;
|
|
114661
116093
|
return {
|
|
114662
116094
|
id: asString2(entry.id),
|
|
@@ -114688,8 +116120,8 @@ var HubFetcher = class {
|
|
|
114688
116120
|
const sid = this.sid();
|
|
114689
116121
|
const parsed = await runScript(this.client, sid, GET_ROOMS_FUNCTIONS);
|
|
114690
116122
|
return {
|
|
114691
|
-
rooms: toDecodedMap(
|
|
114692
|
-
functions: toDecodedMap(
|
|
116123
|
+
rooms: toDecodedMap(isRecord2(parsed) ? parsed.rooms : void 0),
|
|
116124
|
+
functions: toDecodedMap(isRecord2(parsed) ? parsed.functions : void 0)
|
|
114693
116125
|
};
|
|
114694
116126
|
}
|
|
114695
116127
|
/** Build the `SYSVAR_DESCRIPTIONS` map: sysvar id → description text. */
|
|
@@ -114698,7 +116130,7 @@ var HubFetcher = class {
|
|
|
114698
116130
|
const out = /* @__PURE__ */ new Map();
|
|
114699
116131
|
try {
|
|
114700
116132
|
const parsed = await runScript(this.client, sid, SYSVAR_DESCRIPTIONS);
|
|
114701
|
-
for (const entry of
|
|
116133
|
+
for (const entry of asRecordArray2(parsed)) {
|
|
114702
116134
|
const id = asString2(entry.id);
|
|
114703
116135
|
if (id !== "") out.set(id, asString2(entry.description));
|
|
114704
116136
|
}
|
|
@@ -114735,7 +116167,7 @@ function sidOpts(sid) {
|
|
|
114735
116167
|
}
|
|
114736
116168
|
function toDecodedMap(value) {
|
|
114737
116169
|
const map = /* @__PURE__ */ new Map();
|
|
114738
|
-
if (!
|
|
116170
|
+
if (!isRecord2(value)) return map;
|
|
114739
116171
|
for (const [addr, names] of Object.entries(value)) if (Array.isArray(names)) map.set(addr, names.filter((n) => typeof n === "string").map((n) => decodeRegaName(n)));
|
|
114740
116172
|
return map;
|
|
114741
116173
|
}
|
|
@@ -115264,6 +116696,7 @@ var CentralUnit = class {
|
|
|
115264
116696
|
ifaceById = /* @__PURE__ */ new Map();
|
|
115265
116697
|
jsonClient;
|
|
115266
116698
|
session;
|
|
116699
|
+
webUi;
|
|
115267
116700
|
callbackServer;
|
|
115268
116701
|
router;
|
|
115269
116702
|
recovery;
|
|
@@ -115331,6 +116764,30 @@ var CentralUnit = class {
|
|
|
115331
116764
|
});
|
|
115332
116765
|
}
|
|
115333
116766
|
/**
|
|
116767
|
+
* Build the {@link CcuManagement} bound to this central's interface clients and
|
|
116768
|
+
* (when credentialed) its JSON-RPC client. The session id is ensured per call
|
|
116769
|
+
* through the {@link SessionManager}, so renews apply. INTERNAL: the facade
|
|
116770
|
+
* exposes it; it is not part of the published surface.
|
|
116771
|
+
*/
|
|
116772
|
+
getManagement() {
|
|
116773
|
+
return new CcuManagement({
|
|
116774
|
+
json: this.jsonClient,
|
|
116775
|
+
webUi: this.webUi,
|
|
116776
|
+
getSessionId: () => this.session !== void 0 ? this.session.ensureSession() : Promise.resolve(void 0),
|
|
116777
|
+
interfaceClient: (iface) => {
|
|
116778
|
+
for (const runtime of this.runtimes.values()) if (runtime.iface === iface) return runtime.client;
|
|
116779
|
+
},
|
|
116780
|
+
configuredInterfaces: () => this.interfaces
|
|
116781
|
+
});
|
|
116782
|
+
}
|
|
116783
|
+
/**
|
|
116784
|
+
* Remove a device from the interface owning it, routing by `interfaceId`.
|
|
116785
|
+
* Throws if the interface is unknown.
|
|
116786
|
+
*/
|
|
116787
|
+
async deleteDevice(interfaceId2, address, flags) {
|
|
116788
|
+
await this.clientFor(interfaceId2).deleteDevice(address, flags);
|
|
116789
|
+
}
|
|
116790
|
+
/**
|
|
115334
116791
|
* Read the discovered paramset DESCRIPTION (the spec — types, ranges, flags)
|
|
115335
116792
|
* for a channel + paramset key from the in-memory {@link ParamsetDescriptionCache}.
|
|
115336
116793
|
* Returns `undefined` if the paramset has not been discovered. This is the
|
|
@@ -115429,14 +116886,14 @@ var CentralUnit = class {
|
|
|
115429
116886
|
};
|
|
115430
116887
|
const tls = this.tls;
|
|
115431
116888
|
const port = this.portFor(iface);
|
|
115432
|
-
const path$
|
|
116889
|
+
const path$6 = INTERFACE_REMOTE_PATH[iface];
|
|
115433
116890
|
return new InterfaceClient({
|
|
115434
116891
|
centralName: this.centralName,
|
|
115435
116892
|
interface: iface,
|
|
115436
116893
|
host: this.host,
|
|
115437
116894
|
port,
|
|
115438
116895
|
tls,
|
|
115439
|
-
...path$
|
|
116896
|
+
...path$6 !== void 0 ? { path: path$6 } : {},
|
|
115440
116897
|
...auth !== void 0 ? { auth } : {},
|
|
115441
116898
|
callbackUrlProvider: () => this.callbackUrl()
|
|
115442
116899
|
});
|
|
@@ -115462,10 +116919,14 @@ var CentralUnit = class {
|
|
|
115462
116919
|
}
|
|
115463
116920
|
buildJsonRpc() {
|
|
115464
116921
|
if (this.credentials === void 0) return;
|
|
115465
|
-
if (this.options.jsonClient !== void 0)
|
|
115466
|
-
|
|
115467
|
-
|
|
116922
|
+
if (this.options.jsonClient !== void 0) {
|
|
116923
|
+
this.jsonClient = this.options.jsonClient;
|
|
116924
|
+
this.webUi = this.options.webUiClient;
|
|
116925
|
+
} else {
|
|
116926
|
+
const scheme = this.tls ? "https" : "http";
|
|
116927
|
+
const client = new JsonRpcClient({ url: `${scheme}://${this.host}` });
|
|
115468
116928
|
this.jsonClient = client;
|
|
116929
|
+
this.webUi = this.options.webUiClient ?? new WebUiHttpClient({ url: `${scheme}://${this.host}` });
|
|
115469
116930
|
this.session = new SessionManager({
|
|
115470
116931
|
client,
|
|
115471
116932
|
username: this.credentials.username,
|
|
@@ -116943,7 +118404,7 @@ var TypedEventEmitter = class {
|
|
|
116943
118404
|
};
|
|
116944
118405
|
var DEFAULT_CENTRAL_NAME = "nodehomematic";
|
|
116945
118406
|
var pendingTestInjectables;
|
|
116946
|
-
function
|
|
118407
|
+
function toInterface2(name) {
|
|
116947
118408
|
const match = Object.values(Interface).includes(name) ? name : void 0;
|
|
116948
118409
|
if (match === void 0) throw new ValidationError(`Unknown interface "${name}".`);
|
|
116949
118410
|
return match;
|
|
@@ -116953,7 +118414,7 @@ function deviceAddressOf4(channelAddress2) {
|
|
|
116953
118414
|
return colon === -1 ? channelAddress2 : channelAddress2.slice(0, colon);
|
|
116954
118415
|
}
|
|
116955
118416
|
function buildCentral(options, injectables) {
|
|
116956
|
-
const interfaces = options.interfaces.map(
|
|
118417
|
+
const interfaces = options.interfaces.map(toInterface2);
|
|
116957
118418
|
if (interfaces.length === 0) throw new ValidationError("At least one interface is required.");
|
|
116958
118419
|
if (injectables.central !== void 0) return injectables.central;
|
|
116959
118420
|
return new CentralUnit({
|
|
@@ -116986,6 +118447,8 @@ var Homematic = class {
|
|
|
116986
118447
|
#started = false;
|
|
116987
118448
|
/** Bound hub fetcher, or undefined when no JSON-RPC client is available. */
|
|
116988
118449
|
#hub;
|
|
118450
|
+
/** CCU-management service, built on first use, cleared at stop(). */
|
|
118451
|
+
#management;
|
|
116989
118452
|
/** Snapshot of system variables, refreshed on start / refreshHub. */
|
|
116990
118453
|
#sysvars = [];
|
|
116991
118454
|
/** Snapshot of programs, refreshed on start / refreshHub. */
|
|
@@ -117043,6 +118506,7 @@ var Homematic = class {
|
|
|
117043
118506
|
this.#customEntityList = [];
|
|
117044
118507
|
this.#customEntitiesByKey.clear();
|
|
117045
118508
|
this.#hub = void 0;
|
|
118509
|
+
this.#management = void 0;
|
|
117046
118510
|
this.#sysvars = [];
|
|
117047
118511
|
this.#programs = [];
|
|
117048
118512
|
this.#roomsByDevice.clear();
|
|
@@ -117087,6 +118551,98 @@ var Homematic = class {
|
|
|
117087
118551
|
await this.#central.installFirmware(device.interfaceId, deviceAddress);
|
|
117088
118552
|
}
|
|
117089
118553
|
/**
|
|
118554
|
+
* Every interface the CCU offers (BidCos-RF, HmIP-RF, VirtualDevices, ...)
|
|
118555
|
+
* with connection state, duty cycle, firmware and type where the CCU reports
|
|
118556
|
+
* them. A value the CCU did not answer is `null`, never `0`. Needs a started
|
|
118557
|
+
* facade.
|
|
118558
|
+
*
|
|
118559
|
+
* CCU methods: JSON-RPC `Interface.listInterfaces` / `Interface.isPresent`
|
|
118560
|
+
* (eq-3/occu `WebUI/www/api/methods/interface/`), XML-RPC
|
|
118561
|
+
* `listBidcosInterfaces` on BidCos-RF, JSON-RPC `Interface.getDutyCycle`
|
|
118562
|
+
* (RaspberryMatic / OpenCCU only) for the HmIP access point.
|
|
118563
|
+
*/
|
|
118564
|
+
async listInterfaces() {
|
|
118565
|
+
return this.#requireManagement().listInterfaces();
|
|
118566
|
+
}
|
|
118567
|
+
/**
|
|
118568
|
+
* Open (default 60 s, 1..3600) or close the pairing window of a configured
|
|
118569
|
+
* interface. XML-RPC `setInstallMode`; `mode: 'secure'` (BidCos only) also
|
|
118570
|
+
* teaches the HomeMatic Secure key.
|
|
118571
|
+
*/
|
|
118572
|
+
async setInstallMode(request) {
|
|
118573
|
+
await this.#requireManagement().setInstallMode(request);
|
|
118574
|
+
}
|
|
118575
|
+
/** Seconds left of an interface's pairing window (`null` when the CCU's answer was unusable). XML-RPC `getInstallMode`. */
|
|
118576
|
+
async getInstallMode(interfaceName) {
|
|
118577
|
+
return this.#requireManagement().getInstallMode(interfaceName);
|
|
118578
|
+
}
|
|
118579
|
+
/**
|
|
118580
|
+
* Remove a device from its interface (XML-RPC `deleteDevice`). `flags` is an OR
|
|
118581
|
+
* of {@link HM_DELETE_FLAGS}: `RESET` (0x01, factory reset), `FORCE` (0x02,
|
|
118582
|
+
* remove even when unreachable), `DEFER` (0x04, remove when next reachable);
|
|
118583
|
+
* `0` is a plain removal. The model drops the device once the CCU reports it
|
|
118584
|
+
* through the `deleteDevices` callback. Throws {@link ValidationError} for an
|
|
118585
|
+
* unknown device or invalid flags.
|
|
118586
|
+
*/
|
|
118587
|
+
async deleteDevice(deviceAddress, flags = 0) {
|
|
118588
|
+
this.#requireManagement();
|
|
118589
|
+
const device = this.#devicesByAddress.get(deviceAddress);
|
|
118590
|
+
if (device === void 0) throw new ValidationError(`Unknown device "${deviceAddress}".`);
|
|
118591
|
+
if (!Number.isInteger(flags) || flags < 0 || flags > 7) throw new ValidationError(`Invalid delete flags ${String(flags)} (expected an integer 0..7).`);
|
|
118592
|
+
await this.#central.deleteDevice(device.interfaceId, deviceAddress, flags);
|
|
118593
|
+
}
|
|
118594
|
+
/**
|
|
118595
|
+
* The CCU's active service messages (UNREACH, LOWBAT, CONFIG_PENDING,
|
|
118596
|
+
* STICKY_UNREACH, ...). Needs WebUI credentials. ReGa script over
|
|
118597
|
+
* `ReGa.runScript` (eq-3/occu `WebUI/www/rega/esp/system.fn`, `ID_SERVICES`).
|
|
118598
|
+
*/
|
|
118599
|
+
async serviceMessages() {
|
|
118600
|
+
return this.#requireManagement().serviceMessages();
|
|
118601
|
+
}
|
|
118602
|
+
/** Acknowledge a service message by its `id` (ReGa `AlReceipt`, as the WebUI does). Needs WebUI credentials. */
|
|
118603
|
+
async acknowledgeServiceMessage(id) {
|
|
118604
|
+
await this.#requireManagement().acknowledgeServiceMessage(id);
|
|
118605
|
+
}
|
|
118606
|
+
/**
|
|
118607
|
+
* Version, product (CCU3 / RaspberryMatic / OpenCCU), serial and hostname.
|
|
118608
|
+
* Needs WebUI credentials. JSON-RPC `CCU.getVersion`, `CCU.getSerial`
|
|
118609
|
+
* (eq-3/occu `WebUI/www/api/methods/ccu/`) plus a ReGa read of `/VERSION`.
|
|
118610
|
+
*/
|
|
118611
|
+
async ccuInfo() {
|
|
118612
|
+
return this.#requireManagement().ccuInfo();
|
|
118613
|
+
}
|
|
118614
|
+
/**
|
|
118615
|
+
* The CCU's heating groups ("Gruppen"): HmIP heating groups and HomeMatic
|
|
118616
|
+
* heating groups with their member channels. Writes are separate and
|
|
118617
|
+
* undocumented: {@link Homematic.createGroup}, `updateGroup`, `deleteGroup`.
|
|
118618
|
+
* JSON-RPC `CCU.getHeatingGroupList` (eq-3/occu
|
|
118619
|
+
* `WebUI/www/api/methods/ccu/getheatinggrouplist.tcl`). Needs WebUI credentials.
|
|
118620
|
+
*/
|
|
118621
|
+
async groups() {
|
|
118622
|
+
return this.#requireManagement().groups();
|
|
118623
|
+
}
|
|
118624
|
+
/**
|
|
118625
|
+
* Create a heating group (`type` e.g. `hmip.heating.group`, `members` channel
|
|
118626
|
+
* addresses). Resolves with the group as read back from the CCU; a write the
|
|
118627
|
+
* read-back does not show rejects with `GroupWriteVerificationError`.
|
|
118628
|
+
*
|
|
118629
|
+
* UNDOCUMENTED and firmware-dependent: no CCU API exists for this, so the call
|
|
118630
|
+
* reproduces the WebUI's own HMServer page requests (eq-3/occu
|
|
118631
|
+
* `HMserver/opt/HMServer/pages/GroupEditPage.ftl`, `GroupListPage.ftl`).
|
|
118632
|
+
* Needs WebUI credentials.
|
|
118633
|
+
*/
|
|
118634
|
+
async createGroup(input) {
|
|
118635
|
+
return this.#requireManagement().createGroup(input);
|
|
118636
|
+
}
|
|
118637
|
+
/** Rename a group and/or replace its member list (same caveats as {@link Homematic.createGroup}). */
|
|
118638
|
+
async updateGroup(id, patch) {
|
|
118639
|
+
return this.#requireManagement().updateGroup(id, patch);
|
|
118640
|
+
}
|
|
118641
|
+
/** Delete a group, verified by the read-back (same caveats as {@link Homematic.createGroup}). */
|
|
118642
|
+
async deleteGroup(id) {
|
|
118643
|
+
await this.#requireManagement().deleteGroup(id);
|
|
118644
|
+
}
|
|
118645
|
+
/**
|
|
117090
118646
|
* The MASTER configuration parameters of a channel, with the metadata needed
|
|
117091
118647
|
* to render a form. Empty array when nothing is known for the channel.
|
|
117092
118648
|
*/
|
|
@@ -117474,6 +119030,12 @@ var Homematic = class {
|
|
|
117474
119030
|
if (byName !== void 0) return byName.id;
|
|
117475
119031
|
throw new ValidationError(`Unknown program "${idOrName}".`);
|
|
117476
119032
|
}
|
|
119033
|
+
/** Return the management service (built on first use), or throw before start(). */
|
|
119034
|
+
#requireManagement() {
|
|
119035
|
+
if (!this.#started) throw new ValidationError("Homematic is not started; call start() first.");
|
|
119036
|
+
this.#management ??= this.#central.getManagement();
|
|
119037
|
+
return this.#management;
|
|
119038
|
+
}
|
|
117477
119039
|
/** Return the bound hub fetcher, or throw if none (no credentials). */
|
|
117478
119040
|
#requireHub() {
|
|
117479
119041
|
if (this.#hub === void 0) throw new ValidationError("Hub is unavailable (no WebUI credentials configured).");
|
|
@@ -118491,14 +120053,15 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118491
120053
|
],
|
|
118492
120054
|
customActions: homematicActions,
|
|
118493
120055
|
actionHandlers: { discoverCcu: async () => {
|
|
118494
|
-
const {
|
|
120056
|
+
const { candidates } = await this.ctx.api.deviceManager.discoverProvider.mutate({ addonId: this.ctx.id });
|
|
120057
|
+
const hosts = candidates.map((candidate) => ({
|
|
120058
|
+
host: String(candidate.prefilledConfig["host"] ?? ""),
|
|
120059
|
+
name: candidate.suggestedName,
|
|
120060
|
+
id: candidate.stableId.replace(/^homematic:/, "")
|
|
120061
|
+
}));
|
|
118495
120062
|
return {
|
|
118496
|
-
hosts
|
|
118497
|
-
|
|
118498
|
-
name: found.name,
|
|
118499
|
-
id: found.id
|
|
118500
|
-
})),
|
|
118501
|
-
summary
|
|
120063
|
+
hosts,
|
|
120064
|
+
summary: homematicDiscoverySummary(hosts.length, hosts.length)
|
|
118502
120065
|
};
|
|
118503
120066
|
} }
|
|
118504
120067
|
};
|
|
@@ -118533,18 +120096,21 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118533
120096
|
async supportsManualCreation() {
|
|
118534
120097
|
return false;
|
|
118535
120098
|
}
|
|
118536
|
-
/**
|
|
120099
|
+
/** A CCU is found by the scan engine's http-probe; this addon classifies the hits. */
|
|
118537
120100
|
async supportsDiscovery() {
|
|
118538
120101
|
return true;
|
|
118539
120102
|
}
|
|
118540
|
-
async
|
|
118541
|
-
|
|
118542
|
-
|
|
118543
|
-
|
|
118544
|
-
|
|
118545
|
-
|
|
118546
|
-
|
|
118547
|
-
|
|
120103
|
+
async getDiscoveryModes() {
|
|
120104
|
+
return HOMEMATIC_DISCOVERY_MODES;
|
|
120105
|
+
}
|
|
120106
|
+
async classifyDiscoveryHits(input) {
|
|
120107
|
+
const found = ccusFromHits(input.hits);
|
|
120108
|
+
const proposals = proposeHomematicCcus(found, await this.knownCcuHosts());
|
|
120109
|
+
this.ctx.logger.info("Homematic CCU discovery finished", { meta: {
|
|
120110
|
+
found: found.length,
|
|
120111
|
+
proposed: proposals.length
|
|
120112
|
+
} });
|
|
120113
|
+
return homematicDiscoveryCandidates(proposals);
|
|
118548
120114
|
}
|
|
118549
120115
|
async adoptDiscoveredDevice(_input) {
|
|
118550
120116
|
throw new Error("A Homematic CCU is added from the integration form");
|
|
@@ -118555,23 +120121,6 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
118555
120121
|
async onCreateDevice(_type, _config) {
|
|
118556
120122
|
throw new Error("Homematic devices are adopted from the integration, not created manually");
|
|
118557
120123
|
}
|
|
118558
|
-
async discoverCcus() {
|
|
118559
|
-
let found = [];
|
|
118560
|
-
try {
|
|
118561
|
-
found = await browseHomematicCcus();
|
|
118562
|
-
} catch (err) {
|
|
118563
|
-
this.ctx.logger.warn("Homematic CCU discovery failed", { meta: { error: errMsg(err) } });
|
|
118564
|
-
}
|
|
118565
|
-
const proposals = proposeHomematicCcus(found, await this.knownCcuHosts());
|
|
118566
|
-
this.ctx.logger.info("Homematic CCU discovery finished", { meta: {
|
|
118567
|
-
found: found.length,
|
|
118568
|
-
proposed: proposals.length
|
|
118569
|
-
} });
|
|
118570
|
-
return {
|
|
118571
|
-
proposals,
|
|
118572
|
-
summary: homematicDiscoverySummary(found.length, proposals.length)
|
|
118573
|
-
};
|
|
118574
|
-
}
|
|
118575
120124
|
async knownCcuHosts() {
|
|
118576
120125
|
try {
|
|
118577
120126
|
const all = await this.ctx.api.integrations.list.query();
|