@camstack/addon-post-analysis 1.2.25 → 1.2.27
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/dist/{dist-Bi1fiPc0.js → dist-Be0WkqVJ.js} +3694 -778
- package/dist/{dist-DsCSHZ9N.mjs → dist-D7FtjvAJ.mjs} +3666 -792
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DFKBSXUx.js → node-C5glqf4X.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-QDqT3XHa.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CgiwmstG.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DvTbKmnz.mjs → hostInit-BWwP7qzu.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +589 -15
- package/dist/pipeline-analytics/index.mjs +588 -14
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -6513,6 +6513,109 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
|
|
|
6513
6513
|
function nodePin(nodeId) {
|
|
6514
6514
|
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
|
|
6515
6515
|
}
|
|
6516
|
+
/**
|
|
6517
|
+
* AUTO-GENERATED by scripts/generate-device-scoped-caps.ts — DO NOT EDIT.
|
|
6518
|
+
*
|
|
6519
|
+
* Every `scope: 'device'` capability name, as plain data — so a forked runner
|
|
6520
|
+
* can answer "may a rule actuate this?" without importing the schema barrel
|
|
6521
|
+
* (~144MB RSS per runner, D28).
|
|
6522
|
+
*
|
|
6523
|
+
* Coverage: 80 device-scoped capabilities.
|
|
6524
|
+
*/
|
|
6525
|
+
var DEVICE_SCOPED_CAPS = new Set([
|
|
6526
|
+
"accessories",
|
|
6527
|
+
"air-quality-sensor",
|
|
6528
|
+
"alarm-panel",
|
|
6529
|
+
"ambient-light-sensor",
|
|
6530
|
+
"audio-analysis",
|
|
6531
|
+
"audio-metrics",
|
|
6532
|
+
"automation-control",
|
|
6533
|
+
"battery",
|
|
6534
|
+
"binary",
|
|
6535
|
+
"brightness",
|
|
6536
|
+
"button",
|
|
6537
|
+
"camera-credentials",
|
|
6538
|
+
"camera-pipeline-config",
|
|
6539
|
+
"camera-streams",
|
|
6540
|
+
"carbon-monoxide",
|
|
6541
|
+
"climate-control",
|
|
6542
|
+
"color",
|
|
6543
|
+
"connectivity",
|
|
6544
|
+
"consumables",
|
|
6545
|
+
"contact",
|
|
6546
|
+
"control",
|
|
6547
|
+
"cover",
|
|
6548
|
+
"day-night",
|
|
6549
|
+
"detection-pipeline",
|
|
6550
|
+
"device-discovery",
|
|
6551
|
+
"device-ops",
|
|
6552
|
+
"device-status",
|
|
6553
|
+
"doorbell",
|
|
6554
|
+
"enum-sensor",
|
|
6555
|
+
"event-emitter",
|
|
6556
|
+
"events",
|
|
6557
|
+
"fan-control",
|
|
6558
|
+
"feature-probe",
|
|
6559
|
+
"flood",
|
|
6560
|
+
"gas",
|
|
6561
|
+
"humidifier",
|
|
6562
|
+
"humidity-sensor",
|
|
6563
|
+
"image",
|
|
6564
|
+
"image-settings",
|
|
6565
|
+
"intercom",
|
|
6566
|
+
"lawn-mower-control",
|
|
6567
|
+
"lock-control",
|
|
6568
|
+
"media-player",
|
|
6569
|
+
"motion",
|
|
6570
|
+
"motion-detection",
|
|
6571
|
+
"motion-trigger",
|
|
6572
|
+
"motion-zones",
|
|
6573
|
+
"native-object-detection",
|
|
6574
|
+
"notifier",
|
|
6575
|
+
"numeric-sensor",
|
|
6576
|
+
"osd",
|
|
6577
|
+
"pet-feeder",
|
|
6578
|
+
"pipeline-analytics",
|
|
6579
|
+
"power-meter",
|
|
6580
|
+
"presence",
|
|
6581
|
+
"pressure-sensor",
|
|
6582
|
+
"privacy-mask",
|
|
6583
|
+
"ptz",
|
|
6584
|
+
"ptz-autotrack",
|
|
6585
|
+
"reboot",
|
|
6586
|
+
"scene-monitor",
|
|
6587
|
+
"script-runner",
|
|
6588
|
+
"smoke",
|
|
6589
|
+
"snapshot",
|
|
6590
|
+
"stream-catalog",
|
|
6591
|
+
"stream-params",
|
|
6592
|
+
"switch",
|
|
6593
|
+
"tamper",
|
|
6594
|
+
"temperature-sensor",
|
|
6595
|
+
"update",
|
|
6596
|
+
"vacuum-control",
|
|
6597
|
+
"valve",
|
|
6598
|
+
"vibration",
|
|
6599
|
+
"videoclips",
|
|
6600
|
+
"water-heater",
|
|
6601
|
+
"weather",
|
|
6602
|
+
"webrtc-session",
|
|
6603
|
+
"zone-analytics",
|
|
6604
|
+
"zone-rules",
|
|
6605
|
+
"zones"
|
|
6606
|
+
]);
|
|
6607
|
+
/**
|
|
6608
|
+
* True when `capName` is a device capability.
|
|
6609
|
+
*
|
|
6610
|
+
* This is the ONLY boundary on what a notification rule may actuate. A rule can
|
|
6611
|
+
* be authored by a non-admin and the runner executes with the addon's
|
|
6612
|
+
* privileges, so an unbounded action would be an arbitrary RPC channel with a
|
|
6613
|
+
* privilege escalation attached. Device scope excludes the system caps
|
|
6614
|
+
* (`device-manager.removeDevice` and friends) by construction.
|
|
6615
|
+
*/
|
|
6616
|
+
function isDeviceScopedCap(capName) {
|
|
6617
|
+
return DEVICE_SCOPED_CAPS.has(capName);
|
|
6618
|
+
}
|
|
6516
6619
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
6517
6620
|
DeviceType["Camera"] = "camera";
|
|
6518
6621
|
DeviceType["Hub"] = "hub";
|
|
@@ -6898,6 +7001,10 @@ function systemMethod(input, output, options) {
|
|
|
6898
7001
|
systemOnly: true
|
|
6899
7002
|
};
|
|
6900
7003
|
}
|
|
7004
|
+
/** Shorthand to define an event schema */
|
|
7005
|
+
function event(data) {
|
|
7006
|
+
return { data };
|
|
7007
|
+
}
|
|
6901
7008
|
var StaticDirOutputSchema$1 = object({ staticDir: string() });
|
|
6902
7009
|
var VersionOutputSchema$1 = object({ version: string() });
|
|
6903
7010
|
method(_void(), StaticDirOutputSchema$1), method(_void(), VersionOutputSchema$1);
|
|
@@ -8392,6 +8499,60 @@ function buildNcTaxonomy() {
|
|
|
8392
8499
|
/** The frozen NC taxonomy, derived once from the taxonomy dictionary. */
|
|
8393
8500
|
var NC_TAXONOMY = Object.freeze(buildNcTaxonomy());
|
|
8394
8501
|
/**
|
|
8502
|
+
* Most specific first. Extending this list is how a new device kind becomes
|
|
8503
|
+
* gateable; nothing else needs to change.
|
|
8504
|
+
*/
|
|
8505
|
+
var DEVICE_STATE_READERS = [
|
|
8506
|
+
{
|
|
8507
|
+
cap: "alarm-panel",
|
|
8508
|
+
field: "state"
|
|
8509
|
+
},
|
|
8510
|
+
{
|
|
8511
|
+
cap: "cover",
|
|
8512
|
+
field: "state"
|
|
8513
|
+
},
|
|
8514
|
+
{
|
|
8515
|
+
cap: "presence",
|
|
8516
|
+
field: "state"
|
|
8517
|
+
},
|
|
8518
|
+
{
|
|
8519
|
+
cap: "lock",
|
|
8520
|
+
field: "locked",
|
|
8521
|
+
booleanWords: ["locked", "unlocked"]
|
|
8522
|
+
},
|
|
8523
|
+
{
|
|
8524
|
+
cap: "contact",
|
|
8525
|
+
field: "entryOpen",
|
|
8526
|
+
booleanWords: ["open", "closed"]
|
|
8527
|
+
},
|
|
8528
|
+
{
|
|
8529
|
+
cap: "switch",
|
|
8530
|
+
field: "on",
|
|
8531
|
+
booleanWords: ["on", "off"]
|
|
8532
|
+
},
|
|
8533
|
+
{
|
|
8534
|
+
cap: "binary",
|
|
8535
|
+
field: "on",
|
|
8536
|
+
booleanWords: ["on", "off"]
|
|
8537
|
+
}
|
|
8538
|
+
];
|
|
8539
|
+
/**
|
|
8540
|
+
* Collapse a device's full runtime state to the one string a rule compares
|
|
8541
|
+
* against, or `undefined` when nothing in the table applies.
|
|
8542
|
+
*
|
|
8543
|
+
* `undefined` is the safe answer everywhere: the gate treats it as "does not
|
|
8544
|
+
* match", so a device whose kind we cannot read simply never arms a rule.
|
|
8545
|
+
*/
|
|
8546
|
+
function readDeviceStateFrom(runtimeState) {
|
|
8547
|
+
for (const reader of DEVICE_STATE_READERS) {
|
|
8548
|
+
const slice = runtimeState[reader.cap];
|
|
8549
|
+
if (slice === null || typeof slice !== "object") continue;
|
|
8550
|
+
const value = slice[reader.field];
|
|
8551
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
8552
|
+
if (typeof value === "boolean" && reader.booleanWords !== void 0) return value ? reader.booleanWords[0] : reader.booleanWords[1];
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
/**
|
|
8395
8556
|
* Error types for the safe expression engine. Two distinct classes so callers
|
|
8396
8557
|
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
8397
8558
|
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
@@ -9026,6 +9187,249 @@ var AccessoryKind = {
|
|
|
9026
9187
|
};
|
|
9027
9188
|
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
9028
9189
|
DeviceFeature.BatteryOperated;
|
|
9190
|
+
var DeviceConfig = class DeviceConfig {
|
|
9191
|
+
schema;
|
|
9192
|
+
data;
|
|
9193
|
+
persistFn;
|
|
9194
|
+
constructor(schema, data, persist) {
|
|
9195
|
+
this.schema = schema;
|
|
9196
|
+
this.data = data;
|
|
9197
|
+
this.persistFn = persist;
|
|
9198
|
+
}
|
|
9199
|
+
/**
|
|
9200
|
+
* Build a `DeviceConfig` from a persisted blob, with automatic
|
|
9201
|
+
* recovery from schema-validation failures. Boot must never be
|
|
9202
|
+
* blocked by stale persisted values: if Zod rejects the blob,
|
|
9203
|
+
* we drop every offending top-level field, retry, and persist
|
|
9204
|
+
* the cleaned blob so the bad value is healed in the DB on next
|
|
9205
|
+
* write. The most common trigger is a tightened range constraint
|
|
9206
|
+
* (e.g. `max(100) → max(50)`) on a field that already has an
|
|
9207
|
+
* out-of-range value persisted from the previous schema. Without
|
|
9208
|
+
* this safety net, the device would fail to instantiate and end
|
|
9209
|
+
* up with no caps registered — exactly the failure mode that
|
|
9210
|
+
* stranded device 15 when `motionSensitivity: 90` no longer fit
|
|
9211
|
+
* the new `1..50` schema.
|
|
9212
|
+
*
|
|
9213
|
+
* Recovery rules:
|
|
9214
|
+
* 1. Try `safeParse(initialData)`. If it succeeds, done.
|
|
9215
|
+
* 2. On failure, walk `error.issues`, collect the top-level path
|
|
9216
|
+
* of each issue, and drop those keys from `initialData`.
|
|
9217
|
+
* 3. Re-run `safeParse`. If the cleaned blob now passes (Zod
|
|
9218
|
+
* fills the missing keys with schema defaults / undefined for
|
|
9219
|
+
* `.optional()`), persist it via `persist()` so the bad
|
|
9220
|
+
* values disappear from the DB, and return the device.
|
|
9221
|
+
* 4. If the cleaned blob STILL fails (very rare — would require
|
|
9222
|
+
* a non-recoverable required field), fall back to
|
|
9223
|
+
* `schema.parse({})` so the device still boots with pure
|
|
9224
|
+
* schema defaults. Persist nothing in that path so the next
|
|
9225
|
+
* successful `setAll` still writes a coherent blob.
|
|
9226
|
+
*/
|
|
9227
|
+
static fromSchema(schema, persist, initialData = {}, onRecover) {
|
|
9228
|
+
const first = schema.safeParse(initialData);
|
|
9229
|
+
if (first.success) return new DeviceConfig(schema, first.data, persist);
|
|
9230
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
9231
|
+
for (const issue of first.error.issues) {
|
|
9232
|
+
const top = issue.path[0];
|
|
9233
|
+
if (typeof top === "string") droppedKeys.add(top);
|
|
9234
|
+
}
|
|
9235
|
+
const cleaned = { ...initialData };
|
|
9236
|
+
for (const k of droppedKeys) delete cleaned[k];
|
|
9237
|
+
const second = schema.safeParse(cleaned);
|
|
9238
|
+
onRecover?.({
|
|
9239
|
+
droppedKeys: [...droppedKeys],
|
|
9240
|
+
issues: first.error.issues
|
|
9241
|
+
});
|
|
9242
|
+
if (second.success) {
|
|
9243
|
+
persist(second.data).catch(() => {});
|
|
9244
|
+
return new DeviceConfig(schema, second.data, persist);
|
|
9245
|
+
}
|
|
9246
|
+
return new DeviceConfig(schema, schema.parse({}), persist);
|
|
9247
|
+
}
|
|
9248
|
+
get values() {
|
|
9249
|
+
return this.data;
|
|
9250
|
+
}
|
|
9251
|
+
get(key) {
|
|
9252
|
+
return this.data[key];
|
|
9253
|
+
}
|
|
9254
|
+
async set(key, value) {
|
|
9255
|
+
const next = this.schema.parse({
|
|
9256
|
+
...this.data,
|
|
9257
|
+
[key]: value
|
|
9258
|
+
});
|
|
9259
|
+
this.data = next;
|
|
9260
|
+
await this.persistFn(this.data);
|
|
9261
|
+
}
|
|
9262
|
+
/**
|
|
9263
|
+
* Merge an untyped patch onto the current config and persist. Accepts
|
|
9264
|
+
* `Record<string, unknown>` because the patch typically comes from the
|
|
9265
|
+
* UI form layer (a `ConfigField.key → value` map) where the caller
|
|
9266
|
+
* doesn't hold the Zod schema's static type. Runtime validation is
|
|
9267
|
+
* authoritative: `this.schema.parse` rejects unknown keys or invalid
|
|
9268
|
+
* shapes before touching storage.
|
|
9269
|
+
*/
|
|
9270
|
+
async setAll(partial) {
|
|
9271
|
+
const next = this.schema.parse({
|
|
9272
|
+
...this.data,
|
|
9273
|
+
...partial
|
|
9274
|
+
});
|
|
9275
|
+
this.data = next;
|
|
9276
|
+
await this.persistFn(this.data);
|
|
9277
|
+
}
|
|
9278
|
+
async deleteKey(key) {
|
|
9279
|
+
const { [key]: _, ...rest } = this.data;
|
|
9280
|
+
const next = this.schema.parse(rest);
|
|
9281
|
+
this.data = next;
|
|
9282
|
+
await this.persistFn(this.data);
|
|
9283
|
+
}
|
|
9284
|
+
entries() {
|
|
9285
|
+
const shape = this.schema.shape;
|
|
9286
|
+
return Object.entries(shape).map(([key, fieldSchema]) => ({
|
|
9287
|
+
key,
|
|
9288
|
+
schema: fieldSchema,
|
|
9289
|
+
value: this.data[key],
|
|
9290
|
+
description: fieldSchema.description
|
|
9291
|
+
}));
|
|
9292
|
+
}
|
|
9293
|
+
};
|
|
9294
|
+
/**
|
|
9295
|
+
* Concrete implementation. Routes every successful write through
|
|
9296
|
+
* `writer(capName, slice)` — the kernel hooks this up to
|
|
9297
|
+
* `device-state.setCapSlice`, the canonical cross-layer write
|
|
9298
|
+
* entrypoint, which handles disk persistence (debounced on the hub)
|
|
9299
|
+
* and mirror updates.
|
|
9300
|
+
*
|
|
9301
|
+
* Schema validation runs in-process before the writer is called —
|
|
9302
|
+
* the round-trip should never carry an invalid slice. `flush()`
|
|
9303
|
+
* awaits any in-flight writer promises so shutdown is lossless.
|
|
9304
|
+
*
|
|
9305
|
+
* `initial` is the persisted blob loaded at boot. Slices for caps
|
|
9306
|
+
* whose schema hasn't been installed yet are kept in-memory verbatim
|
|
9307
|
+
* and validated when the cap registers later.
|
|
9308
|
+
*/
|
|
9309
|
+
var DeviceRuntimeState = class DeviceRuntimeState {
|
|
9310
|
+
writer;
|
|
9311
|
+
/** In-flight writer promises tracked so `flush()` can await them. */
|
|
9312
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
9313
|
+
/** Per-cap committed slice — after schema validation when known. */
|
|
9314
|
+
slices;
|
|
9315
|
+
/** Per-cap registered schema (set by `installCapSchema`). */
|
|
9316
|
+
schemas = /* @__PURE__ */ new Map();
|
|
9317
|
+
listeners = /* @__PURE__ */ new Set();
|
|
9318
|
+
capListeners = /* @__PURE__ */ new Map();
|
|
9319
|
+
constructor(initial, writer) {
|
|
9320
|
+
this.writer = writer;
|
|
9321
|
+
this.slices = /* @__PURE__ */ new Map();
|
|
9322
|
+
for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
|
|
9323
|
+
}
|
|
9324
|
+
static fromInitial(initial, writer) {
|
|
9325
|
+
return new DeviceRuntimeState(initial, writer);
|
|
9326
|
+
}
|
|
9327
|
+
installCapSchema(capName, schema) {
|
|
9328
|
+
const existing = this.schemas.get(capName);
|
|
9329
|
+
if (existing) {
|
|
9330
|
+
if (existing !== schema) throw new Error(`[DeviceRuntimeState] capability "${capName}" registered a different runtime-state schema; each cap must declare ONE shape across every provider`);
|
|
9331
|
+
return;
|
|
9332
|
+
}
|
|
9333
|
+
this.schemas.set(capName, schema);
|
|
9334
|
+
const stored = this.slices.get(capName);
|
|
9335
|
+
if (stored) {
|
|
9336
|
+
const result = schema.safeParse(stored);
|
|
9337
|
+
if (result.success) this.slices.set(capName, result.data);
|
|
9338
|
+
else this.slices.delete(capName);
|
|
9339
|
+
}
|
|
9340
|
+
}
|
|
9341
|
+
getCapState(capName) {
|
|
9342
|
+
const slice = this.slices.get(capName);
|
|
9343
|
+
if (!slice) return void 0;
|
|
9344
|
+
return Object.freeze({ ...slice });
|
|
9345
|
+
}
|
|
9346
|
+
getCapField(capName, key) {
|
|
9347
|
+
return this.slices.get(capName)?.[key];
|
|
9348
|
+
}
|
|
9349
|
+
setCapState(capName, value) {
|
|
9350
|
+
this.applyCapWrite(capName, value, false);
|
|
9351
|
+
}
|
|
9352
|
+
patchCapState(capName, partial) {
|
|
9353
|
+
this.applyCapWrite(capName, partial, true);
|
|
9354
|
+
}
|
|
9355
|
+
/**
|
|
9356
|
+
* Internal worker. `merge` controls whether `value` replaces or
|
|
9357
|
+
* shallow-merges into the existing slice. Schema validation runs
|
|
9358
|
+
* on the FINAL composed object regardless.
|
|
9359
|
+
*/
|
|
9360
|
+
applyCapWrite(capName, value, merge) {
|
|
9361
|
+
const schema = this.schemas.get(capName);
|
|
9362
|
+
if (!schema) throw new Error(`[DeviceRuntimeState] no schema registered for cap "${capName}" — did the device register it via ctx.registerNativeCap before writing?`);
|
|
9363
|
+
const current = this.slices.get(capName) ?? {};
|
|
9364
|
+
const next = merge ? {
|
|
9365
|
+
...current,
|
|
9366
|
+
...value
|
|
9367
|
+
} : { ...value };
|
|
9368
|
+
const parsed = schema.parse(next);
|
|
9369
|
+
if (shallowEqual(current, parsed)) return;
|
|
9370
|
+
this.slices.set(capName, parsed);
|
|
9371
|
+
this.fireListeners([capName]);
|
|
9372
|
+
const writePromise = this.writer(capName, { ...parsed }).catch(() => {});
|
|
9373
|
+
this.pendingWrites.add(writePromise);
|
|
9374
|
+
writePromise.finally(() => {
|
|
9375
|
+
this.pendingWrites.delete(writePromise);
|
|
9376
|
+
});
|
|
9377
|
+
}
|
|
9378
|
+
fireListeners(changed) {
|
|
9379
|
+
const snap = this.snapshot();
|
|
9380
|
+
for (const cb of this.listeners) try {
|
|
9381
|
+
cb(changed, snap);
|
|
9382
|
+
} catch {}
|
|
9383
|
+
for (const capName of changed) {
|
|
9384
|
+
const subs = this.capListeners.get(capName);
|
|
9385
|
+
if (!subs) continue;
|
|
9386
|
+
const slice = this.getCapState(capName);
|
|
9387
|
+
for (const cb of subs) try {
|
|
9388
|
+
cb(slice);
|
|
9389
|
+
} catch {}
|
|
9390
|
+
}
|
|
9391
|
+
}
|
|
9392
|
+
subscribe(cb) {
|
|
9393
|
+
this.listeners.add(cb);
|
|
9394
|
+
return () => {
|
|
9395
|
+
this.listeners.delete(cb);
|
|
9396
|
+
};
|
|
9397
|
+
}
|
|
9398
|
+
subscribeCap(capName, cb) {
|
|
9399
|
+
let subs = this.capListeners.get(capName);
|
|
9400
|
+
if (!subs) {
|
|
9401
|
+
subs = /* @__PURE__ */ new Set();
|
|
9402
|
+
this.capListeners.set(capName, subs);
|
|
9403
|
+
}
|
|
9404
|
+
const adapter = (slice) => {
|
|
9405
|
+
cb(slice);
|
|
9406
|
+
};
|
|
9407
|
+
subs.add(adapter);
|
|
9408
|
+
return () => {
|
|
9409
|
+
const set = this.capListeners.get(capName);
|
|
9410
|
+
if (!set) return;
|
|
9411
|
+
set.delete(adapter);
|
|
9412
|
+
if (set.size === 0) this.capListeners.delete(capName);
|
|
9413
|
+
};
|
|
9414
|
+
}
|
|
9415
|
+
snapshot() {
|
|
9416
|
+
const out = {};
|
|
9417
|
+
for (const [k, v] of this.slices) out[k] = Object.freeze({ ...v });
|
|
9418
|
+
return Object.freeze(out);
|
|
9419
|
+
}
|
|
9420
|
+
async flush() {
|
|
9421
|
+
if (this.pendingWrites.size === 0) return;
|
|
9422
|
+
const inflight = [...this.pendingWrites];
|
|
9423
|
+
await Promise.allSettled(inflight);
|
|
9424
|
+
}
|
|
9425
|
+
};
|
|
9426
|
+
function shallowEqual(a, b) {
|
|
9427
|
+
const ak = Object.keys(a);
|
|
9428
|
+
const bk = Object.keys(b);
|
|
9429
|
+
if (ak.length !== bk.length) return false;
|
|
9430
|
+
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
9431
|
+
return true;
|
|
9432
|
+
}
|
|
9029
9433
|
new Set(["devices", "classes"]);
|
|
9030
9434
|
/**
|
|
9031
9435
|
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
@@ -9218,7 +9622,68 @@ var NcZoneConditionSchema = object({
|
|
|
9218
9622
|
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9219
9623
|
* membership lists are OR within the list (spec §2.3).
|
|
9220
9624
|
*/
|
|
9625
|
+
/**
|
|
9626
|
+
* What a rule may actuate.
|
|
9627
|
+
*
|
|
9628
|
+
* **No hand-maintained allowlist** (operator decision, and the right one — a
|
|
9629
|
+
* written list of methods is a third parallel map to keep aligned, and this
|
|
9630
|
+
* repo has paid for those). The boundary instead comes from a property the
|
|
9631
|
+
* capabilities already carry: an action may target only a **device-scoped**
|
|
9632
|
+
* capability method.
|
|
9633
|
+
*
|
|
9634
|
+
* That is not decoration. A rule can be authored by a NON-ADMIN — personal
|
|
9635
|
+
* rules are a supported flow — and the executor runs with the addon's
|
|
9636
|
+
* privileges, so an unbounded action is an arbitrary RPC channel with a
|
|
9637
|
+
* privilege escalation attached. Restricting to device scope excludes the
|
|
9638
|
+
* system caps (`device-manager.removeDevice` and friends) by construction,
|
|
9639
|
+
* costs nothing to maintain, and cannot rot: a cap that stops being
|
|
9640
|
+
* device-scoped stops being actuatable in the same change.
|
|
9641
|
+
*
|
|
9642
|
+
* The executor enforces it; {@link NcRuleActionSchema} carries the intent.
|
|
9643
|
+
*/
|
|
9644
|
+
/**
|
|
9645
|
+
* One step of a sequence.
|
|
9646
|
+
*
|
|
9647
|
+
* `wait` is a first-class step rather than a property of the next action: it is
|
|
9648
|
+
* what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
|
|
9649
|
+
* cannot be expressed otherwise.
|
|
9650
|
+
*/
|
|
9651
|
+
var NcRuleActionSchema = discriminatedUnion("kind", [object({
|
|
9652
|
+
kind: literal("wait"),
|
|
9653
|
+
seconds: number().min(0).max(300)
|
|
9654
|
+
}), object({
|
|
9655
|
+
kind: literal("cap"),
|
|
9656
|
+
deviceId: number().int(),
|
|
9657
|
+
/** Capability name, e.g. `alarm-panel`. */
|
|
9658
|
+
cap: string().min(1),
|
|
9659
|
+
/** Method on it. The executor refuses a non-device-scoped cap. */
|
|
9660
|
+
method: string().min(1),
|
|
9661
|
+
/** Method arguments, minus `deviceId` (the executor injects it). */
|
|
9662
|
+
args: record(string(), unknown()).optional()
|
|
9663
|
+
})]);
|
|
9664
|
+
/**
|
|
9665
|
+
* Sequences a rule runs, by hook point.
|
|
9666
|
+
*
|
|
9667
|
+
* ONLY `onTrigger` is here, deliberately. The reference also has activation /
|
|
9668
|
+
* deactivation / reset / post-generation hooks, and they are wanted — but this
|
|
9669
|
+
* repo's expensive failure mode is declaring a surface nothing produces, so a
|
|
9670
|
+
* hook appears here in the same change that produces its edge, never before.
|
|
9671
|
+
*/
|
|
9672
|
+
var NcRuleActionsSchema = object({
|
|
9673
|
+
/** Runs when the rule MATCHES. */
|
|
9674
|
+
onTrigger: array(object({
|
|
9675
|
+
name: string().min(1).max(120),
|
|
9676
|
+
enabled: boolean(),
|
|
9677
|
+
minDelaySec: number().int().min(0).max(86400).optional(),
|
|
9678
|
+
actions: array(NcRuleActionSchema).min(1)
|
|
9679
|
+
})).optional() });
|
|
9221
9680
|
var NcConditionsSchema = object({
|
|
9681
|
+
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
9682
|
+
deviceState: object({
|
|
9683
|
+
deviceId: number().int(),
|
|
9684
|
+
/** Any of these matches. */
|
|
9685
|
+
states: array(string().min(1)).min(1)
|
|
9686
|
+
}).optional(),
|
|
9222
9687
|
/** Device scope — absent = all devices. */
|
|
9223
9688
|
devices: array(number()).optional(),
|
|
9224
9689
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -9512,7 +9977,16 @@ var NcRuleInputSchema = object({
|
|
|
9512
9977
|
* read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
|
|
9513
9978
|
* by this flag — see the scope rules on that function.
|
|
9514
9979
|
*/
|
|
9515
|
-
snoozeAllowGlobal: boolean().optional()
|
|
9980
|
+
snoozeAllowGlobal: boolean().optional(),
|
|
9981
|
+
/**
|
|
9982
|
+
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
9983
|
+
*
|
|
9984
|
+
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
9985
|
+
* being a special case: arming is
|
|
9986
|
+
* `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
|
|
9987
|
+
* shape as every other actuation.
|
|
9988
|
+
*/
|
|
9989
|
+
actions: NcRuleActionsSchema.optional()
|
|
9516
9990
|
});
|
|
9517
9991
|
/**
|
|
9518
9992
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -9583,7 +10057,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
9583
10057
|
"packagePhase",
|
|
9584
10058
|
"crossingSelect",
|
|
9585
10059
|
"polygonDraw",
|
|
9586
|
-
"occupancy"
|
|
10060
|
+
"occupancy",
|
|
10061
|
+
"deviceState"
|
|
9587
10062
|
]),
|
|
9588
10063
|
operator: _enum([
|
|
9589
10064
|
"in",
|
|
@@ -9842,6 +10317,21 @@ var NC_CONDITION_CATALOG = [
|
|
|
9842
10317
|
phase: "P1",
|
|
9843
10318
|
description: "pipeline / onboard / sensor; a record with no stamped source counts as pipeline."
|
|
9844
10319
|
},
|
|
10320
|
+
{
|
|
10321
|
+
id: "deviceState",
|
|
10322
|
+
group: "scope",
|
|
10323
|
+
label: "Device state",
|
|
10324
|
+
valueType: "deviceState",
|
|
10325
|
+
operator: "in",
|
|
10326
|
+
appliesTo: [
|
|
10327
|
+
"immediate",
|
|
10328
|
+
"track-end",
|
|
10329
|
+
"device-event",
|
|
10330
|
+
"package-event"
|
|
10331
|
+
],
|
|
10332
|
+
phase: "P2",
|
|
10333
|
+
description: "Only fire while another device is in one of the chosen states — the alarm armed, a switch on, a contact closed. A state that cannot be read does NOT fire."
|
|
10334
|
+
},
|
|
9845
10335
|
{
|
|
9846
10336
|
id: "sensorKinds",
|
|
9847
10337
|
group: "device",
|
|
@@ -10294,10 +10784,25 @@ var DeviceStatusSchema = object({
|
|
|
10294
10784
|
* apart "just came online" from "still online". */
|
|
10295
10785
|
lastChangedAt: number()
|
|
10296
10786
|
});
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10787
|
+
var deviceStatusCapability = {
|
|
10788
|
+
name: "device-status",
|
|
10789
|
+
scope: "device",
|
|
10790
|
+
deviceNative: true,
|
|
10791
|
+
mode: "singleton",
|
|
10792
|
+
methods: {},
|
|
10793
|
+
events: {
|
|
10794
|
+
/** Emitted when `online` transitions. Mirrors the semantics of
|
|
10795
|
+
* `battery.onStatusChanged`. */
|
|
10796
|
+
onStatusChanged: { data: object({
|
|
10797
|
+
deviceId: number(),
|
|
10798
|
+
status: DeviceStatusSchema
|
|
10799
|
+
}) } },
|
|
10800
|
+
status: {
|
|
10801
|
+
schema: DeviceStatusSchema,
|
|
10802
|
+
kind: "push"
|
|
10803
|
+
},
|
|
10804
|
+
runtimeState: DeviceStatusSchema
|
|
10805
|
+
};
|
|
10301
10806
|
/**
|
|
10302
10807
|
* Per-device feature/identity probe slice. Holds the runtime-resolved
|
|
10303
10808
|
* truth about what a device CAN do — which the kernel uses to:
|
|
@@ -10355,11 +10860,35 @@ var FeatureProbeStatusSchema = object({
|
|
|
10355
10860
|
*/
|
|
10356
10861
|
lastFetchedAt: number()
|
|
10357
10862
|
});
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10863
|
+
var featureProbeCapability = {
|
|
10864
|
+
name: "feature-probe",
|
|
10865
|
+
scope: "device",
|
|
10866
|
+
deviceNative: true,
|
|
10867
|
+
mode: "singleton",
|
|
10868
|
+
methods: {},
|
|
10869
|
+
events: {
|
|
10870
|
+
/** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
|
|
10871
|
+
* or driver-initiated re-detect after a state change). */
|
|
10872
|
+
onProbeChanged: { data: object({
|
|
10873
|
+
deviceId: number(),
|
|
10874
|
+
status: FeatureProbeStatusSchema
|
|
10875
|
+
}) } },
|
|
10876
|
+
status: {
|
|
10877
|
+
schema: FeatureProbeStatusSchema,
|
|
10878
|
+
kind: "push"
|
|
10879
|
+
},
|
|
10880
|
+
runtimeState: FeatureProbeStatusSchema
|
|
10881
|
+
};
|
|
10882
|
+
/**
|
|
10883
|
+
* Multi-metric air-quality slice. Covers CO₂, total VOCs, particulate
|
|
10884
|
+
* matter at PM2.5 / PM10, and a derived AQI index — all optional so
|
|
10885
|
+
* a single-metric source populates only what it observes. Mirrors
|
|
10886
|
+
* the HA `sensor` device_class set (`co2`, `volatile_organic_compounds`,
|
|
10887
|
+
* `pm25`, `pm10`, `aqi`) collapsed into one cap because a typical
|
|
10888
|
+
* air-quality node reports several of these together; modelling them
|
|
10889
|
+
* as siblings keeps a single timestamp + one slice subscription.
|
|
10890
|
+
*/
|
|
10891
|
+
var AirQualitySensorStatusSchema = object({
|
|
10363
10892
|
/** Carbon dioxide concentration in ppm. */
|
|
10364
10893
|
co2Ppm: number().min(0).optional(),
|
|
10365
10894
|
/** Total volatile organic compounds in ppb. */
|
|
@@ -10383,7 +10912,19 @@ object({
|
|
|
10383
10912
|
* auto-formatting when absent. */
|
|
10384
10913
|
precision: number().int().min(0).max(10).optional()
|
|
10385
10914
|
});
|
|
10386
|
-
|
|
10915
|
+
var airQualitySensorCapability = {
|
|
10916
|
+
name: "air-quality-sensor",
|
|
10917
|
+
scope: "device",
|
|
10918
|
+
deviceNative: true,
|
|
10919
|
+
mode: "singleton",
|
|
10920
|
+
deviceTypes: [DeviceType.Sensor],
|
|
10921
|
+
methods: {},
|
|
10922
|
+
status: {
|
|
10923
|
+
schema: AirQualitySensorStatusSchema,
|
|
10924
|
+
kind: "push"
|
|
10925
|
+
},
|
|
10926
|
+
runtimeState: AirQualitySensorStatusSchema
|
|
10927
|
+
};
|
|
10387
10928
|
/**
|
|
10388
10929
|
* Alarm-panel cap. Models HA `alarm_control_panel.*` on
|
|
10389
10930
|
* `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
|
|
@@ -10419,7 +10960,7 @@ var AlarmArmModeSchema = _enum([
|
|
|
10419
10960
|
"vacation",
|
|
10420
10961
|
"custom_bypass"
|
|
10421
10962
|
]);
|
|
10422
|
-
object({
|
|
10963
|
+
var AlarmPanelStatusSchema = object({
|
|
10423
10964
|
/** Current lifecycle state. */
|
|
10424
10965
|
state: AlarmStateSchema,
|
|
10425
10966
|
/** Subset of arm modes the panel accepts. UI renders one button per
|
|
@@ -10431,26 +10972,58 @@ object({
|
|
|
10431
10972
|
/** Ms epoch when the slice was last updated. */
|
|
10432
10973
|
lastChangedAt: number()
|
|
10433
10974
|
});
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10975
|
+
var alarmPanelCapability = {
|
|
10976
|
+
name: "alarm-panel",
|
|
10977
|
+
scope: "device",
|
|
10978
|
+
deviceNative: true,
|
|
10979
|
+
mode: "singleton",
|
|
10980
|
+
deviceTypes: [DeviceType.AlarmPanel],
|
|
10981
|
+
methods: {
|
|
10982
|
+
arm: method(object({
|
|
10983
|
+
deviceId: number().int().nonnegative(),
|
|
10984
|
+
mode: AlarmArmModeSchema,
|
|
10985
|
+
/** Optional PIN code. Required when `requiresCode === true`.
|
|
10986
|
+
* Passed through to the upstream service; never persisted. */
|
|
10987
|
+
code: string().min(1).optional()
|
|
10988
|
+
}), _void(), {
|
|
10989
|
+
kind: "mutation",
|
|
10990
|
+
auth: "admin"
|
|
10991
|
+
}),
|
|
10992
|
+
disarm: method(object({
|
|
10993
|
+
deviceId: number().int().nonnegative(),
|
|
10994
|
+
code: string().min(1).optional()
|
|
10995
|
+
}), _void(), {
|
|
10996
|
+
kind: "mutation",
|
|
10997
|
+
auth: "admin"
|
|
10998
|
+
}),
|
|
10999
|
+
/**
|
|
11000
|
+
* Force the panel into the `triggered` state — used by HA
|
|
11001
|
+
* automations to surface external sensor events through the panel
|
|
11002
|
+
* (e.g. a Reolink camera intrusion event firing the security
|
|
11003
|
+
* system). Provider rejects when the panel hardware doesn't
|
|
11004
|
+
* support a software-initiated trigger.
|
|
11005
|
+
*/
|
|
11006
|
+
trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11007
|
+
kind: "mutation",
|
|
11008
|
+
auth: "admin"
|
|
11009
|
+
})
|
|
11010
|
+
},
|
|
11011
|
+
status: {
|
|
11012
|
+
schema: AlarmPanelStatusSchema,
|
|
11013
|
+
kind: "push"
|
|
11014
|
+
},
|
|
11015
|
+
/**
|
|
11016
|
+
* Runtime-state slice — mirrored by the kernel. UI panel reads the
|
|
11017
|
+
* full slice; renders an arm button per `availableModes` entry and
|
|
11018
|
+
* a PIN field iff `requiresCode === true`.
|
|
11019
|
+
*/
|
|
11020
|
+
runtimeState: AlarmPanelStatusSchema
|
|
11021
|
+
};
|
|
11022
|
+
/**
|
|
11023
|
+
* Ambient illuminance reading in lux. Drives Home Assistant `sensor`
|
|
11024
|
+
* entries with `device_class: illuminance`.
|
|
11025
|
+
*/
|
|
11026
|
+
var AmbientLightSensorStatusSchema = object({
|
|
10454
11027
|
/** Current illuminance in lux (lx). */
|
|
10455
11028
|
lux: number().min(0),
|
|
10456
11029
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -10465,7 +11038,19 @@ object({
|
|
|
10465
11038
|
* auto-formatting when absent. */
|
|
10466
11039
|
precision: number().int().min(0).max(10).optional()
|
|
10467
11040
|
});
|
|
10468
|
-
|
|
11041
|
+
var ambientLightSensorCapability = {
|
|
11042
|
+
name: "ambient-light-sensor",
|
|
11043
|
+
scope: "device",
|
|
11044
|
+
deviceNative: true,
|
|
11045
|
+
mode: "singleton",
|
|
11046
|
+
deviceTypes: [DeviceType.Sensor],
|
|
11047
|
+
methods: {},
|
|
11048
|
+
status: {
|
|
11049
|
+
schema: AmbientLightSensorStatusSchema,
|
|
11050
|
+
kind: "push"
|
|
11051
|
+
},
|
|
11052
|
+
runtimeState: AmbientLightSensorStatusSchema
|
|
11053
|
+
};
|
|
10469
11054
|
/**
|
|
10470
11055
|
* Per-class audio metrics aggregated over a sliding window.
|
|
10471
11056
|
*/
|
|
@@ -10584,7 +11169,18 @@ var audioMetricsCapability = {
|
|
|
10584
11169
|
/** Reactive runtime-state mirror — live `device.state.audioMetrics.value`. */
|
|
10585
11170
|
runtimeState: AudioMetricsSnapshotSchema
|
|
10586
11171
|
};
|
|
10587
|
-
|
|
11172
|
+
/**
|
|
11173
|
+
* Automation-control cap. Models HA `automation.*` entities on
|
|
11174
|
+
* `DeviceType.Automation`. An automation is a trigger+condition+
|
|
11175
|
+
* action rule that can be enabled / disabled and manually fired
|
|
11176
|
+
* via the `trigger` method.
|
|
11177
|
+
*
|
|
11178
|
+
* `trigger` accepts an optional `skipCondition` flag — when true,
|
|
11179
|
+
* the automation's action block runs WITHOUT evaluating its
|
|
11180
|
+
* condition block. Pair with `DeviceFeature.AutomationSkipCondition`
|
|
11181
|
+
* to gate the UI checkbox for the manual-trigger dialog.
|
|
11182
|
+
*/
|
|
11183
|
+
var AutomationControlStatusSchema = object({
|
|
10588
11184
|
/** Whether the automation is currently enabled. Disabled automations
|
|
10589
11185
|
* ignore their trigger block — manual `trigger` still works. */
|
|
10590
11186
|
enabled: boolean(),
|
|
@@ -10598,22 +11194,43 @@ object({
|
|
|
10598
11194
|
/** Ms epoch when the slice was last updated. */
|
|
10599
11195
|
lastChangedAt: number()
|
|
10600
11196
|
});
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
11197
|
+
var automationControlCapability = {
|
|
11198
|
+
name: "automation-control",
|
|
11199
|
+
scope: "device",
|
|
11200
|
+
deviceNative: true,
|
|
11201
|
+
mode: "singleton",
|
|
11202
|
+
deviceTypes: [DeviceType.Automation],
|
|
11203
|
+
methods: {
|
|
11204
|
+
enable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11205
|
+
kind: "mutation",
|
|
11206
|
+
auth: "admin"
|
|
11207
|
+
}),
|
|
11208
|
+
disable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11209
|
+
kind: "mutation",
|
|
11210
|
+
auth: "admin"
|
|
11211
|
+
}),
|
|
11212
|
+
trigger: method(object({
|
|
11213
|
+
deviceId: number().int().nonnegative(),
|
|
11214
|
+
/** When true, fires the action block while bypassing the
|
|
11215
|
+
* automation's condition evaluation. Gated by
|
|
11216
|
+
* `DeviceFeature.AutomationSkipCondition`. */
|
|
11217
|
+
skipCondition: boolean().optional()
|
|
11218
|
+
}), _void(), {
|
|
11219
|
+
kind: "mutation",
|
|
11220
|
+
auth: "admin"
|
|
11221
|
+
})
|
|
11222
|
+
},
|
|
11223
|
+
status: {
|
|
11224
|
+
schema: AutomationControlStatusSchema,
|
|
11225
|
+
kind: "push"
|
|
11226
|
+
},
|
|
11227
|
+
/**
|
|
11228
|
+
* Runtime-state slice — mirrored by the kernel. UI automation tile
|
|
11229
|
+
* reads `enabled` (toggle) + `isRunning` (spinner) + `lastError`
|
|
11230
|
+
* (badge) directly.
|
|
11231
|
+
*/
|
|
11232
|
+
runtimeState: AutomationControlStatusSchema
|
|
11233
|
+
};
|
|
10617
11234
|
/**
|
|
10618
11235
|
* Battery status snapshot. Emitted by providers whose device is
|
|
10619
11236
|
* battery-operated (cameras with `DeviceFeature.BatteryOperated`,
|
|
@@ -10652,40 +11269,156 @@ var BatteryStatusSchema = object({
|
|
|
10652
11269
|
*/
|
|
10653
11270
|
binary: boolean().optional()
|
|
10654
11271
|
});
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
11272
|
+
var batteryCapability = {
|
|
11273
|
+
name: "battery",
|
|
11274
|
+
scope: "device",
|
|
11275
|
+
deviceNative: true,
|
|
11276
|
+
mode: "singleton",
|
|
11277
|
+
deviceTypes: [
|
|
11278
|
+
DeviceType.Camera,
|
|
11279
|
+
DeviceType.Sensor,
|
|
11280
|
+
DeviceType.Button,
|
|
11281
|
+
DeviceType.Switch
|
|
11282
|
+
],
|
|
11283
|
+
methods: {
|
|
11284
|
+
/**
|
|
11285
|
+
* Explicitly wake the camera from low-power sleep ahead of a
|
|
11286
|
+
* streaming session start. Consumers that initiate a stream
|
|
11287
|
+
* against a sleeping battery cam (HomeKit Secure Video, Alexa
|
|
11288
|
+
* RTCSession, snapshot wrappers) call this with a short timeout
|
|
11289
|
+
* before establishing the media pipeline — the broker's own
|
|
11290
|
+
* passive wake-on-dial works but adds 5–7 seconds to first-frame,
|
|
11291
|
+
* during which the consumer renders a black screen. Pre-waking
|
|
11292
|
+
* compresses that gap.
|
|
11293
|
+
*
|
|
11294
|
+
* Returns `awoke: true` when the firmware acknowledged the wake
|
|
11295
|
+
* before `timeoutMs`. Returns `awoke: false` when it timed out OR
|
|
11296
|
+
* the cap surface is unavailable (no Baichuan / firmware
|
|
11297
|
+
* channel); the caller should still attempt the stream — the
|
|
11298
|
+
* passive broker wake remains as fallback.
|
|
11299
|
+
*/
|
|
11300
|
+
wakeForStream: method(object({
|
|
11301
|
+
deviceId: number(),
|
|
11302
|
+
/** Bound on the wait. Sensible range 3000–10000ms. */
|
|
11303
|
+
timeoutMs: number().int().min(500).max(3e4).default(8e3)
|
|
11304
|
+
}), object({
|
|
11305
|
+
awoke: boolean(),
|
|
11306
|
+
durationMs: number()
|
|
11307
|
+
}), { kind: "mutation" }) },
|
|
11308
|
+
events: {
|
|
11309
|
+
/**
|
|
11310
|
+
* Emitted whenever the cached status changes (firmware push OR
|
|
11311
|
+
* poll observes a delta). The DeviceEventPropagator mirrors this
|
|
11312
|
+
* event on the parent chain — subscribing to a camera's source
|
|
11313
|
+
* receives battery events from child accessories automatically.
|
|
11314
|
+
*/
|
|
11315
|
+
onStatusChanged: { data: object({
|
|
11316
|
+
deviceId: number(),
|
|
11317
|
+
status: BatteryStatusSchema
|
|
11318
|
+
}) } },
|
|
11319
|
+
status: {
|
|
11320
|
+
schema: BatteryStatusSchema,
|
|
11321
|
+
kind: "push",
|
|
11322
|
+
empty: {
|
|
11323
|
+
percentage: 0,
|
|
11324
|
+
charging: "none",
|
|
11325
|
+
sleeping: false,
|
|
11326
|
+
lastUpdated: 0
|
|
11327
|
+
}
|
|
11328
|
+
},
|
|
11329
|
+
/**
|
|
11330
|
+
* Runtime-state slice — every provider that registers this cap
|
|
11331
|
+
* stores the same shape under `device.runtimeState[battery]`.
|
|
11332
|
+
* Cross-provider uniformity: a Reolink Argus, a Frigate sensor
|
|
11333
|
+
* proxy, an ONVIF battery cam all read/write the same keys.
|
|
11334
|
+
* Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
|
|
11335
|
+
* via `device.runtimeState.getCapState('battery')` regardless of
|
|
11336
|
+
* the underlying driver.
|
|
11337
|
+
*/
|
|
11338
|
+
runtimeState: BatteryStatusSchema
|
|
11339
|
+
};
|
|
11340
|
+
/**
|
|
11341
|
+
* Generic boolean sensor — last-resort fallback when no domain-
|
|
11342
|
+
* specific binary cap fits (Home Assistant `binary_sensor` without a
|
|
11343
|
+
* known `device_class`, or a domain we haven't typed yet). Pure
|
|
11344
|
+
* pass-through: just the bool + timestamp. Push-driven.
|
|
11345
|
+
*
|
|
11346
|
+
* Prefer the typed alternatives (`contact`, `flood`, `smoke`,
|
|
11347
|
+
* `carbon-monoxide`, `gas`, `tamper`, `vibration`, `connectivity`,
|
|
11348
|
+
* `motion`) when the semantics match — export adapters render those
|
|
11349
|
+
* with the right HomeKit / Alexa display category.
|
|
11350
|
+
*/
|
|
11351
|
+
var BinaryStatusSchema = object({
|
|
10667
11352
|
on: boolean(),
|
|
10668
11353
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
10669
11354
|
lastChangedAt: number()
|
|
10670
11355
|
});
|
|
10671
|
-
|
|
10672
|
-
|
|
11356
|
+
var binaryCapability = {
|
|
11357
|
+
name: "binary",
|
|
11358
|
+
scope: "device",
|
|
11359
|
+
deviceNative: true,
|
|
11360
|
+
mode: "singleton",
|
|
11361
|
+
deviceTypes: [DeviceType.Sensor],
|
|
11362
|
+
methods: {},
|
|
11363
|
+
status: {
|
|
11364
|
+
schema: BinaryStatusSchema,
|
|
11365
|
+
kind: "push"
|
|
11366
|
+
},
|
|
11367
|
+
runtimeState: BinaryStatusSchema
|
|
11368
|
+
};
|
|
11369
|
+
/**
|
|
11370
|
+
* Dimmable-light brightness control. Co-exists with `switch` on the
|
|
11371
|
+
* same device — the switch toggles on/off, this cap sets the level
|
|
11372
|
+
* applied when the light is on. Drivers map their per-vendor dim
|
|
11373
|
+
* controls to this single-method surface.
|
|
11374
|
+
*
|
|
11375
|
+
* The cap is intentionally minimal: a single `setBrightness({deviceId,
|
|
11376
|
+
* percentage})` mutation plus the auto-injected `getStatus`. Drivers
|
|
11377
|
+
* that expose richer controls (color temperature, scenes, schedules)
|
|
11378
|
+
* should surface those via the device's `getSettingsUISchema()`
|
|
11379
|
+
* instead of bloating this cap.
|
|
11380
|
+
*/
|
|
11381
|
+
var BrightnessStatusSchema = object({
|
|
10673
11382
|
/** Current level as 0..100 inclusive. Firmware-reported. */
|
|
10674
11383
|
percentage: number().min(0).max(100),
|
|
10675
11384
|
/** Ms epoch of the last operator-driven change. Useful for UI freshness. */
|
|
10676
11385
|
lastChangedAt: number()
|
|
10677
11386
|
});
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
11387
|
+
var brightnessCapability = {
|
|
11388
|
+
name: "brightness",
|
|
11389
|
+
scope: "device",
|
|
11390
|
+
deviceNative: true,
|
|
11391
|
+
mode: "singleton",
|
|
11392
|
+
deviceTypes: [DeviceType.Light],
|
|
11393
|
+
methods: { setBrightness: method(object({
|
|
11394
|
+
deviceId: number().int().nonnegative(),
|
|
11395
|
+
percentage: number().min(0).max(100)
|
|
11396
|
+
}), _void(), {
|
|
11397
|
+
kind: "mutation",
|
|
11398
|
+
auth: "admin"
|
|
11399
|
+
}) },
|
|
11400
|
+
events: {
|
|
11401
|
+
/**
|
|
11402
|
+
* Emitted whenever the brightness changes — operator action OR
|
|
11403
|
+
* firmware push. Subscribers (UI sliders, automation engines) react
|
|
11404
|
+
* without polling.
|
|
11405
|
+
*/
|
|
11406
|
+
onBrightnessChanged: { data: object({
|
|
11407
|
+
deviceId: number(),
|
|
11408
|
+
percentage: number().min(0).max(100),
|
|
11409
|
+
lastChangedAt: number()
|
|
11410
|
+
}) } },
|
|
11411
|
+
status: {
|
|
11412
|
+
schema: BrightnessStatusSchema,
|
|
11413
|
+
kind: "command-driven"
|
|
11414
|
+
},
|
|
11415
|
+
/**
|
|
11416
|
+
* Runtime-state slice — the last applied brightness level, mirrored
|
|
11417
|
+
* by the kernel. Read via `device.state.brightness.value` so UI
|
|
11418
|
+
* sliders surface the current level without polling the provider.
|
|
11419
|
+
*/
|
|
11420
|
+
runtimeState: BrightnessStatusSchema
|
|
11421
|
+
};
|
|
10689
11422
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
10690
11423
|
var StreamFormatSchema = _enum([
|
|
10691
11424
|
"webrtc",
|
|
@@ -11105,44 +11838,155 @@ var PickedCamStreamSchema = object({
|
|
|
11105
11838
|
/** One-line explanation of why this stream won — for logs / debug UI. */
|
|
11106
11839
|
reason: string()
|
|
11107
11840
|
});
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11841
|
+
/**
|
|
11842
|
+
* Camera streams — device-scoped facade over the system `stream-broker`.
|
|
11843
|
+
*
|
|
11844
|
+
* Mirrors the slice of broker state relevant to a single device:
|
|
11845
|
+
* - `getCameraStreams()`: the pool of physical streams published for
|
|
11846
|
+
* this device. UI uses it to populate the "Camera Stream" dropdown
|
|
11847
|
+
* under each quality section.
|
|
11848
|
+
* - `getBrokerStreams()`: the (up to 3) profile slots `high/mid/low`
|
|
11849
|
+
* with their current assignment + runtime status. UI uses it for
|
|
11850
|
+
* the WebRTC quality picker, recording target selection, etc.
|
|
11851
|
+
*
|
|
11852
|
+
* Registered for every camera device that has at least one published
|
|
11853
|
+
* cam stream. The provider is owned by the stream-broker addon; reads
|
|
11854
|
+
* go against the broker's in-memory registries. Mutations (assign /
|
|
11855
|
+
* unassign / publish / retract) do NOT live here — they stay on the
|
|
11856
|
+
* system `stream-broker` cap so cross-device / addon-driven flows keep
|
|
11857
|
+
* a single namespace.
|
|
11858
|
+
*/
|
|
11859
|
+
var cameraStreamsCapability = {
|
|
11860
|
+
name: "camera-streams",
|
|
11861
|
+
scope: "device",
|
|
11862
|
+
mode: "singleton",
|
|
11863
|
+
kind: "wrapper",
|
|
11864
|
+
defaultActive: true,
|
|
11865
|
+
deviceTypes: [DeviceType.Camera],
|
|
11866
|
+
methods: {
|
|
11867
|
+
getCameraStreams: method(object({ deviceId: number().int().nonnegative() }), array(CameraStreamSchema).readonly()),
|
|
11868
|
+
getBrokerStreams: method(object({ deviceId: number().int().nonnegative() }), array(ProfileSlotSchema).readonly()),
|
|
11869
|
+
/**
|
|
11870
|
+
* Per-device RAW RTSP restream entries — one per published camStream
|
|
11871
|
+
* that has RTSP restream enabled (`native:main`, `rtsp:sub`, …).
|
|
11872
|
+
*
|
|
11873
|
+
* LIVE-VIEW ONLY. This is the surface the device-details stream
|
|
11874
|
+
* picker uses so an operator can hit each physical stream directly.
|
|
11875
|
+
* Programmatic / external consumers (HAP, Alexa, ha-mqtt, recording)
|
|
11876
|
+
* MUST use `getProfileRtspEntries` instead — picking from raw
|
|
11877
|
+
* variants makes two consumers of the same camera land on two
|
|
11878
|
+
* different physical pulls (e.g. Reolink `native:main` vs
|
|
11879
|
+
* `rtsp:main`) and trips the camera's concurrent-session limit.
|
|
11880
|
+
*/
|
|
11881
|
+
getRtspEntries: method(object({
|
|
11882
|
+
deviceId: number().int().nonnegative(),
|
|
11883
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11884
|
+
hostname: string().optional()
|
|
11885
|
+
}), array(RtspRestreamEntrySchema).readonly()),
|
|
11886
|
+
/**
|
|
11887
|
+
* Per-device PROFILE RTSP restream entries — one per ASSIGNED
|
|
11888
|
+
* profile slot (high/mid/low). Each entry's `url` is a profile-keyed
|
|
11889
|
+
* broker restream that aliases the profile's assigned source broker,
|
|
11890
|
+
* so HAP / Alexa / recording / WebRTC all converge on the broker's
|
|
11891
|
+
* single on-demand pull for that profile. This is the supported
|
|
11892
|
+
* exporter-facing surface; the raw `getRtspEntries` is live-view
|
|
11893
|
+
* only. Returns `[]` for a device with no assigned profiles
|
|
11894
|
+
* (cold-start before first publish).
|
|
11895
|
+
*/
|
|
11896
|
+
getProfileRtspEntries: method(object({
|
|
11897
|
+
deviceId: number().int().nonnegative(),
|
|
11898
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11899
|
+
hostname: string().optional()
|
|
11900
|
+
}), array(ProfileRtspEntrySchema).readonly()),
|
|
11901
|
+
/**
|
|
11902
|
+
* "Best source stream for these decode constraints". Returns the
|
|
11903
|
+
* camStreamId the caller should dial (or null when no stream
|
|
11904
|
+
* matches). See `PickStreamRequirementsSchema` for the filter shape
|
|
11905
|
+
* and `PickStreamPreferencesSchema` for the ranking inputs.
|
|
11906
|
+
*
|
|
11907
|
+
* Returning null instructs the caller to fall back to its existing
|
|
11908
|
+
* path (derived-broker transcode, profile-slot pick, etc.) — the
|
|
11909
|
+
* picker NEVER ranks `derived:*` candidates as a "match" because
|
|
11910
|
+
* its whole job is to avoid the transcode.
|
|
11911
|
+
*/
|
|
11912
|
+
pickStream: method(object({
|
|
11913
|
+
deviceId: number().int().nonnegative(),
|
|
11914
|
+
requirements: PickStreamRequirementsSchema,
|
|
11915
|
+
preferences: PickStreamPreferencesSchema.optional()
|
|
11916
|
+
}), PickedCamStreamSchema.nullable())
|
|
11917
|
+
},
|
|
11918
|
+
events: {
|
|
11919
|
+
/** Fires on publishCameraStream / retractCameraStream. */
|
|
11920
|
+
onCamStreamsChanged: event(object({
|
|
11921
|
+
deviceId: number().int().nonnegative(),
|
|
11922
|
+
camStreams: array(CameraStreamSchema).readonly()
|
|
11923
|
+
})),
|
|
11924
|
+
/** Fires on assignProfile / unassignProfile / runtime status change. */
|
|
11925
|
+
onProfileSlotsChanged: event(object({
|
|
11926
|
+
deviceId: number().int().nonnegative(),
|
|
11927
|
+
profileSlots: array(ProfileSlotSchema).readonly()
|
|
11928
|
+
}))
|
|
11929
|
+
},
|
|
11930
|
+
/**
|
|
11931
|
+
* Per-device live stream-broker state. Persistent settings (RTSP
|
|
11932
|
+
* tokens, profile assignments, pre-buffer config, RTSP-enabled toggles,
|
|
11933
|
+
* streamingDebug) stay in the broker's addon store — they survive
|
|
11934
|
+
* restarts. The slice below carries ONLY what's truly runtime:
|
|
11935
|
+
*
|
|
11936
|
+
* - `online` — at least one profile slot is currently `'streaming'`.
|
|
11937
|
+
* Drivers without a firmware liveness signal (RTSP, ONVIF…) can
|
|
11938
|
+
* subscribe and mirror this into `state.deviceStatus.online`.
|
|
11939
|
+
* - `slotStatuses` — current `ProfileSlotStatus` per profile,
|
|
11940
|
+
* mirroring the runtime-mutable subset of `ProfileSlot`.
|
|
11941
|
+
* - `slotErrors` — last error message per profile (only set when the
|
|
11942
|
+
* corresponding slot is in `'error'`).
|
|
11943
|
+
* - `lastChangedAt` — freshness signal for consumers that want to
|
|
11944
|
+
* reason about how stale the slice is.
|
|
11945
|
+
*
|
|
11946
|
+
* Written by the stream-broker manager on every transition that
|
|
11947
|
+
* affects these aggregates. Read via `device.state.cameraStreams.<field>`
|
|
11948
|
+
* (BaseDevice proxy) or, cross-process, via
|
|
11949
|
+
* `device-state.getCapSlice({deviceId, capName: 'camera-streams'})`.
|
|
11950
|
+
* The cap's `onChanged` event fires automatically on each write so
|
|
11951
|
+
* subscribers get push semantics for free.
|
|
11952
|
+
*/
|
|
11953
|
+
runtimeState: object({
|
|
11954
|
+
online: boolean(),
|
|
11955
|
+
slotStatuses: object({
|
|
11956
|
+
high: ProfileSlotStatusSchema.optional(),
|
|
11957
|
+
mid: ProfileSlotStatusSchema.optional(),
|
|
11958
|
+
low: ProfileSlotStatusSchema.optional()
|
|
11959
|
+
}),
|
|
11960
|
+
slotErrors: object({
|
|
11961
|
+
high: string().optional(),
|
|
11962
|
+
mid: string().optional(),
|
|
11963
|
+
low: string().optional()
|
|
11964
|
+
}),
|
|
11965
|
+
lastChangedAt: number()
|
|
11966
|
+
})
|
|
11967
|
+
};
|
|
11968
|
+
/**
|
|
11969
|
+
* Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
|
|
11970
|
+
* entries with `device_class: carbon_monoxide`. Push-driven.
|
|
11971
|
+
*/
|
|
11972
|
+
var CarbonMonoxideStatusSchema = object({
|
|
11141
11973
|
detected: boolean(),
|
|
11142
11974
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11143
11975
|
lastChangedAt: number()
|
|
11144
11976
|
});
|
|
11145
|
-
|
|
11977
|
+
var carbonMonoxideCapability = {
|
|
11978
|
+
name: "carbon-monoxide",
|
|
11979
|
+
scope: "device",
|
|
11980
|
+
deviceNative: true,
|
|
11981
|
+
mode: "singleton",
|
|
11982
|
+
deviceTypes: [DeviceType.Sensor],
|
|
11983
|
+
methods: {},
|
|
11984
|
+
status: {
|
|
11985
|
+
schema: CarbonMonoxideStatusSchema,
|
|
11986
|
+
kind: "push"
|
|
11987
|
+
},
|
|
11988
|
+
runtimeState: CarbonMonoxideStatusSchema
|
|
11989
|
+
};
|
|
11146
11990
|
/**
|
|
11147
11991
|
* HVAC / climate control cap. Models the full surface of a HA
|
|
11148
11992
|
* `climate.*` entity (thermostat, A/C, heat pump, dehumidifier)
|
|
@@ -11183,7 +12027,7 @@ var HvacModeSchema = _enum([
|
|
|
11183
12027
|
"fan_only",
|
|
11184
12028
|
"dry"
|
|
11185
12029
|
]);
|
|
11186
|
-
object({
|
|
12030
|
+
var ClimateControlStatusSchema = object({
|
|
11187
12031
|
/** Active HVAC mode. */
|
|
11188
12032
|
mode: HvacModeSchema,
|
|
11189
12033
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -11227,56 +12071,82 @@ object({
|
|
|
11227
12071
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
11228
12072
|
lastFetchedAt: number()
|
|
11229
12073
|
});
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
}),
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
}),
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
}),
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
}), _void(), {
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
})
|
|
12074
|
+
var climateControlCapability = {
|
|
12075
|
+
name: "climate-control",
|
|
12076
|
+
scope: "device",
|
|
12077
|
+
deviceNative: true,
|
|
12078
|
+
mode: "singleton",
|
|
12079
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
12080
|
+
methods: {
|
|
12081
|
+
setMode: method(object({
|
|
12082
|
+
deviceId: number().int().nonnegative(),
|
|
12083
|
+
mode: HvacModeSchema
|
|
12084
|
+
}), _void(), {
|
|
12085
|
+
kind: "mutation",
|
|
12086
|
+
auth: "admin"
|
|
12087
|
+
}),
|
|
12088
|
+
setFanMode: method(object({
|
|
12089
|
+
deviceId: number().int().nonnegative(),
|
|
12090
|
+
fanMode: string().min(1)
|
|
12091
|
+
}), _void(), {
|
|
12092
|
+
kind: "mutation",
|
|
12093
|
+
auth: "admin"
|
|
12094
|
+
}),
|
|
12095
|
+
setPreset: method(object({
|
|
12096
|
+
deviceId: number().int().nonnegative(),
|
|
12097
|
+
preset: string().min(1)
|
|
12098
|
+
}), _void(), {
|
|
12099
|
+
kind: "mutation",
|
|
12100
|
+
auth: "admin"
|
|
12101
|
+
}),
|
|
12102
|
+
setTarget: method(object({
|
|
12103
|
+
deviceId: number().int().nonnegative(),
|
|
12104
|
+
target: number()
|
|
12105
|
+
}), _void(), {
|
|
12106
|
+
kind: "mutation",
|
|
12107
|
+
auth: "admin"
|
|
12108
|
+
}),
|
|
12109
|
+
setTargetRange: method(object({
|
|
12110
|
+
deviceId: number().int().nonnegative(),
|
|
12111
|
+
targetLow: number(),
|
|
12112
|
+
targetHigh: number()
|
|
12113
|
+
}), _void(), {
|
|
12114
|
+
kind: "mutation",
|
|
12115
|
+
auth: "admin"
|
|
12116
|
+
}),
|
|
12117
|
+
setTargetHumidity: method(object({
|
|
12118
|
+
deviceId: number().int().nonnegative(),
|
|
12119
|
+
targetHumidity: number().min(0).max(100)
|
|
12120
|
+
}), _void(), {
|
|
12121
|
+
kind: "mutation",
|
|
12122
|
+
auth: "admin"
|
|
12123
|
+
}),
|
|
12124
|
+
setSwingVertical: method(object({
|
|
12125
|
+
deviceId: number().int().nonnegative(),
|
|
12126
|
+
on: boolean()
|
|
12127
|
+
}), _void(), {
|
|
12128
|
+
kind: "mutation",
|
|
12129
|
+
auth: "admin"
|
|
12130
|
+
}),
|
|
12131
|
+
setSwingHorizontal: method(object({
|
|
12132
|
+
deviceId: number().int().nonnegative(),
|
|
12133
|
+
on: boolean()
|
|
12134
|
+
}), _void(), {
|
|
12135
|
+
kind: "mutation",
|
|
12136
|
+
auth: "admin"
|
|
12137
|
+
})
|
|
12138
|
+
},
|
|
12139
|
+
status: {
|
|
12140
|
+
schema: ClimateControlStatusSchema,
|
|
12141
|
+
kind: "push"
|
|
12142
|
+
},
|
|
12143
|
+
/**
|
|
12144
|
+
* Runtime-state slice — mirrored by the kernel. UI thermostats read
|
|
12145
|
+
* the full slice via `device.state.climate-control.value` and refresh
|
|
12146
|
+
* on every push without re-querying the provider.
|
|
12147
|
+
*/
|
|
12148
|
+
runtimeState: ClimateControlStatusSchema
|
|
12149
|
+
};
|
|
11280
12150
|
/**
|
|
11281
12151
|
* Color-light cap. Coexists with `switch` (on/off) and `brightness`
|
|
11282
12152
|
* (level) on the same device — `switch` toggles the bulb, `brightness`
|
|
@@ -11328,7 +12198,7 @@ var ColorInputSchema = discriminatedUnion("mode", [
|
|
|
11328
12198
|
mireds: number().int().min(50).max(1e3)
|
|
11329
12199
|
})
|
|
11330
12200
|
]);
|
|
11331
|
-
object({
|
|
12201
|
+
var ColorStatusSchema = object({
|
|
11332
12202
|
/** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
|
|
11333
12203
|
* the bulb's current state. */
|
|
11334
12204
|
mode: _enum([
|
|
@@ -11345,63 +12215,209 @@ object({
|
|
|
11345
12215
|
/** Ms epoch of the last operator-driven change. */
|
|
11346
12216
|
lastChangedAt: number()
|
|
11347
12217
|
});
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
"
|
|
11359
|
-
"
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
12218
|
+
var colorCapability = {
|
|
12219
|
+
name: "color",
|
|
12220
|
+
scope: "device",
|
|
12221
|
+
deviceNative: true,
|
|
12222
|
+
mode: "singleton",
|
|
12223
|
+
deviceTypes: [DeviceType.Light],
|
|
12224
|
+
methods: { setColor: method(object({
|
|
12225
|
+
deviceId: number().int().nonnegative(),
|
|
12226
|
+
color: ColorInputSchema
|
|
12227
|
+
}), _void(), {
|
|
12228
|
+
kind: "mutation",
|
|
12229
|
+
auth: "admin"
|
|
12230
|
+
}) },
|
|
12231
|
+
events: {
|
|
12232
|
+
/**
|
|
12233
|
+
* Emitted whenever the color changes — operator action OR firmware
|
|
12234
|
+
* push. Subscribers (UI color pickers, automation engines) react
|
|
12235
|
+
* without polling.
|
|
12236
|
+
*/
|
|
12237
|
+
onColorChanged: { data: object({
|
|
12238
|
+
deviceId: number(),
|
|
12239
|
+
mode: _enum([
|
|
12240
|
+
"rgb",
|
|
12241
|
+
"hsv",
|
|
12242
|
+
"mired"
|
|
12243
|
+
]),
|
|
12244
|
+
rgb: RgbTripletSchema.optional(),
|
|
12245
|
+
hsv: HsvTripletSchema.optional(),
|
|
12246
|
+
mireds: number().int().optional(),
|
|
12247
|
+
lastChangedAt: number()
|
|
12248
|
+
}) } },
|
|
12249
|
+
status: {
|
|
12250
|
+
schema: ColorStatusSchema,
|
|
12251
|
+
kind: "command-driven"
|
|
12252
|
+
},
|
|
12253
|
+
/**
|
|
12254
|
+
* Runtime-state slice — the last applied color, mirrored by the
|
|
12255
|
+
* kernel. Read via `device.state.color.value` so UI pickers surface
|
|
12256
|
+
* the current chromaticity without polling the provider.
|
|
12257
|
+
*/
|
|
12258
|
+
runtimeState: ColorStatusSchema
|
|
12259
|
+
};
|
|
12260
|
+
/**
|
|
12261
|
+
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
12262
|
+
* which is the kernel-managed online/offline flag for the device's
|
|
12263
|
+
* own transport. This cap surfaces an external entity's view of
|
|
12264
|
+
* whether the device is reachable (typical use: HA's
|
|
12265
|
+
* `binary_sensor` with `device_class: connectivity` for a remote
|
|
12266
|
+
* gateway or bridge). Push-driven.
|
|
12267
|
+
*/
|
|
12268
|
+
var ConnectivityStatusSchema = object({
|
|
11367
12269
|
/** True when the upstream system considers the entity connected. */
|
|
11368
12270
|
connected: boolean(),
|
|
11369
12271
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11370
12272
|
lastChangedAt: number()
|
|
11371
12273
|
});
|
|
11372
|
-
|
|
12274
|
+
var connectivityCapability = {
|
|
12275
|
+
name: "connectivity",
|
|
12276
|
+
scope: "device",
|
|
12277
|
+
deviceNative: true,
|
|
12278
|
+
mode: "singleton",
|
|
12279
|
+
deviceTypes: [DeviceType.Sensor],
|
|
12280
|
+
methods: {},
|
|
12281
|
+
status: {
|
|
12282
|
+
schema: ConnectivityStatusSchema,
|
|
12283
|
+
kind: "push"
|
|
12284
|
+
},
|
|
12285
|
+
runtimeState: ConnectivityStatusSchema
|
|
12286
|
+
};
|
|
12287
|
+
/**
|
|
12288
|
+
* Generic device-consumables capability — surfaces a device's
|
|
12289
|
+
* maintenance items (vacuum filters/brushes, replaceable cartridges,
|
|
12290
|
+
* descaling cycles, …) with their remaining life and an optional
|
|
12291
|
+
* "Replaced" reset action. Device-agnostic: any provider that knows its
|
|
12292
|
+
* device tracks consumables can register it; the cap declares no
|
|
12293
|
+
* vocabulary of its own — the provider names each item verbatim.
|
|
12294
|
+
*
|
|
12295
|
+
* Like `childLayout`, the cap is INERT until a provider sets items: no
|
|
12296
|
+
* provider populates it by guessing (no HA inference). The UI renders a
|
|
12297
|
+
* "No consumables reported" placeholder when `items` is empty.
|
|
12298
|
+
*/
|
|
12299
|
+
/** A single consumable item. Either a continuous `level` (remaining
|
|
12300
|
+
* life %) or a discrete `status` may be known — both may be null when a
|
|
12301
|
+
* provider only knows the item exists. `level` and `status` are not
|
|
12302
|
+
* mutually exclusive; a provider may report both. */
|
|
12303
|
+
var ConsumableItemSchema = object({
|
|
12304
|
+
/** Stable id, e.g. 'main-brush'. */
|
|
12305
|
+
key: string().min(1),
|
|
12306
|
+
/** Display name. */
|
|
12307
|
+
label: string().min(1),
|
|
12308
|
+
/** Remaining life % when known (0..100). */
|
|
12309
|
+
level: number().min(0).max(100).nullable(),
|
|
12310
|
+
/** Discrete state when known (binary mode). */
|
|
12311
|
+
status: _enum(["ok", "replace"]).nullable(),
|
|
12312
|
+
/** Ms epoch of the last replace, when known. */
|
|
12313
|
+
lastResetAt: number().nullable(),
|
|
12314
|
+
/** Whether `reset()` is meaningful for this item. */
|
|
12315
|
+
resettable: boolean()
|
|
12316
|
+
});
|
|
11373
12317
|
var ConsumablesStatusSchema = object({
|
|
11374
|
-
items: array(
|
|
11375
|
-
/** Stable id, e.g. 'main-brush'. */
|
|
11376
|
-
key: string().min(1),
|
|
11377
|
-
/** Display name. */
|
|
11378
|
-
label: string().min(1),
|
|
11379
|
-
/** Remaining life % when known (0..100). */
|
|
11380
|
-
level: number().min(0).max(100).nullable(),
|
|
11381
|
-
/** Discrete state when known (binary mode). */
|
|
11382
|
-
status: _enum(["ok", "replace"]).nullable(),
|
|
11383
|
-
/** Ms epoch of the last replace, when known. */
|
|
11384
|
-
lastResetAt: number().nullable(),
|
|
11385
|
-
/** Whether `reset()` is meaningful for this item. */
|
|
11386
|
-
resettable: boolean()
|
|
11387
|
-
})),
|
|
12318
|
+
items: array(ConsumableItemSchema),
|
|
11388
12319
|
lastChangedAt: number()
|
|
11389
12320
|
});
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
12321
|
+
var consumablesCapability = {
|
|
12322
|
+
name: "consumables",
|
|
12323
|
+
scope: "device",
|
|
12324
|
+
deviceNative: true,
|
|
12325
|
+
mode: "singleton",
|
|
12326
|
+
deviceTypes: Object.values(DeviceType),
|
|
12327
|
+
deviceConfig: { ui: {
|
|
12328
|
+
kind: "widget",
|
|
12329
|
+
widgetId: "host/consumables-panel",
|
|
12330
|
+
tab: "consumables",
|
|
12331
|
+
topTab: true,
|
|
12332
|
+
label: "Consumables",
|
|
12333
|
+
order: 5
|
|
12334
|
+
} },
|
|
12335
|
+
methods: {
|
|
12336
|
+
/** Mark a consumable as replaced — resets its remaining life. Only
|
|
12337
|
+
* meaningful when the item's `resettable` is true. */
|
|
12338
|
+
reset: method(object({
|
|
12339
|
+
deviceId: number().int().nonnegative(),
|
|
12340
|
+
key: string().min(1)
|
|
12341
|
+
}), _void(), {
|
|
12342
|
+
kind: "mutation",
|
|
12343
|
+
auth: "admin"
|
|
12344
|
+
}) },
|
|
12345
|
+
status: {
|
|
12346
|
+
schema: ConsumablesStatusSchema,
|
|
12347
|
+
kind: "push",
|
|
12348
|
+
empty: {
|
|
12349
|
+
items: [],
|
|
12350
|
+
lastChangedAt: 0
|
|
12351
|
+
},
|
|
12352
|
+
itemArray: {
|
|
12353
|
+
path: "items",
|
|
12354
|
+
keyField: "key",
|
|
12355
|
+
labelField: "label",
|
|
12356
|
+
itemSchema: ConsumableItemSchema,
|
|
12357
|
+
emptyItem: {
|
|
12358
|
+
key: "",
|
|
12359
|
+
label: "",
|
|
12360
|
+
level: null,
|
|
12361
|
+
status: null,
|
|
12362
|
+
lastResetAt: null,
|
|
12363
|
+
resettable: false
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
},
|
|
12367
|
+
runtimeState: ConsumablesStatusSchema.extend({ lastFetchedAt: number() })
|
|
12368
|
+
};
|
|
12369
|
+
/**
|
|
12370
|
+
* Door / window / opening / garage / valve contact sensor. Boolean
|
|
12371
|
+
* "is the entry currently open" with the timestamp of the last
|
|
12372
|
+
* transition. Drives Home Assistant `binary_sensor` entries whose
|
|
12373
|
+
* `device_class` is `door`, `window`, `opening`, `garage`, or
|
|
12374
|
+
* `garage_door` — and any future native integration that needs
|
|
12375
|
+
* the same semantics.
|
|
12376
|
+
*
|
|
12377
|
+
* Push-driven: providers update the slice on transition events from
|
|
12378
|
+
* the upstream source (HA WebSocket `state_changed`, ZWave
|
|
12379
|
+
* `notification` …). Consumers read the slice; no polling.
|
|
12380
|
+
*/
|
|
12381
|
+
var ContactStatusSchema = object({
|
|
11398
12382
|
/** True when the entry is open; false when closed. */
|
|
11399
12383
|
entryOpen: boolean(),
|
|
11400
12384
|
/** Ms epoch of the last open↔closed transition. 0 if never observed. */
|
|
11401
12385
|
lastChangedAt: number()
|
|
11402
12386
|
});
|
|
11403
|
-
|
|
11404
|
-
|
|
12387
|
+
var contactCapability = {
|
|
12388
|
+
name: "contact",
|
|
12389
|
+
scope: "device",
|
|
12390
|
+
deviceNative: true,
|
|
12391
|
+
mode: "singleton",
|
|
12392
|
+
deviceTypes: [DeviceType.Sensor],
|
|
12393
|
+
methods: {},
|
|
12394
|
+
status: {
|
|
12395
|
+
schema: ContactStatusSchema,
|
|
12396
|
+
kind: "push"
|
|
12397
|
+
},
|
|
12398
|
+
runtimeState: ContactStatusSchema
|
|
12399
|
+
};
|
|
12400
|
+
/**
|
|
12401
|
+
* Status slice — flat object (the framework's `runtimeState` contract
|
|
12402
|
+
* requires a `ZodObject`, not a discriminated union). The `kind`
|
|
12403
|
+
* field discriminates the value type at the type level via the
|
|
12404
|
+
* `ControlNumericValue` / `ControlStringValue` aliases; consumers
|
|
12405
|
+
* narrow with a kind check.
|
|
12406
|
+
*
|
|
12407
|
+
* The `value` field is typed as `number | string`:
|
|
12408
|
+
* - `kind === 'numeric'` → `value` is `number`
|
|
12409
|
+
* - `kind === 'select' | 'text' | 'datetime'` → `value` is `string`
|
|
12410
|
+
*
|
|
12411
|
+
* The `options` array is populated only when `kind === 'select'`
|
|
12412
|
+
* (empty array for the other kinds). It lives in the slice because
|
|
12413
|
+
* HA `select.*` entities may update their options at runtime.
|
|
12414
|
+
*
|
|
12415
|
+
* The `unit`/`min`/`max`/`step` fields are meaningful for
|
|
12416
|
+
* `kind === 'numeric'` and are absent (undefined) for all other
|
|
12417
|
+
* kinds. They are populated live from HA attributes on each state
|
|
12418
|
+
* push — consistent with `options` and the sensor-unit approach.
|
|
12419
|
+
*/
|
|
12420
|
+
var ControlStatusSchema = object({
|
|
11405
12421
|
kind: _enum([
|
|
11406
12422
|
"numeric",
|
|
11407
12423
|
"select",
|
|
@@ -11463,14 +12479,31 @@ var ControlSetValueInputSchema = discriminatedUnion("kind", [
|
|
|
11463
12479
|
value: string().min(1)
|
|
11464
12480
|
})
|
|
11465
12481
|
]);
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
12482
|
+
var controlCapability = {
|
|
12483
|
+
name: "control",
|
|
12484
|
+
scope: "device",
|
|
12485
|
+
deviceNative: true,
|
|
12486
|
+
mode: "singleton",
|
|
12487
|
+
deviceTypes: [DeviceType.Control],
|
|
12488
|
+
methods: { setValue: method(object({
|
|
12489
|
+
deviceId: number().int().nonnegative(),
|
|
12490
|
+
control: ControlSetValueInputSchema
|
|
12491
|
+
}), _void(), {
|
|
12492
|
+
kind: "mutation",
|
|
12493
|
+
auth: "admin"
|
|
12494
|
+
}) },
|
|
12495
|
+
status: {
|
|
12496
|
+
schema: ControlStatusSchema,
|
|
12497
|
+
kind: "push"
|
|
12498
|
+
},
|
|
12499
|
+
/**
|
|
12500
|
+
* Runtime-state slice — mirrored by the kernel. UI widgets (slider /
|
|
12501
|
+
* dropdown / text field / date picker) read the slice's discriminant
|
|
12502
|
+
* and value directly without polling the provider.
|
|
12503
|
+
*/
|
|
12504
|
+
runtimeState: ControlStatusSchema
|
|
12505
|
+
};
|
|
12506
|
+
var CoverStatusSchema = object({
|
|
11474
12507
|
/** Lifecycle state of the cover. */
|
|
11475
12508
|
state: _enum([
|
|
11476
12509
|
"open",
|
|
@@ -11487,28 +12520,50 @@ object({
|
|
|
11487
12520
|
/** Ms epoch when the slice was last updated. */
|
|
11488
12521
|
lastChangedAt: number()
|
|
11489
12522
|
});
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
}),
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
12523
|
+
var coverCapability = {
|
|
12524
|
+
name: "cover",
|
|
12525
|
+
scope: "device",
|
|
12526
|
+
deviceNative: true,
|
|
12527
|
+
mode: "singleton",
|
|
12528
|
+
deviceTypes: [DeviceType.Cover],
|
|
12529
|
+
methods: {
|
|
12530
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12531
|
+
kind: "mutation",
|
|
12532
|
+
auth: "admin"
|
|
12533
|
+
}),
|
|
12534
|
+
close: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12535
|
+
kind: "mutation",
|
|
12536
|
+
auth: "admin"
|
|
12537
|
+
}),
|
|
12538
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12539
|
+
kind: "mutation",
|
|
12540
|
+
auth: "admin"
|
|
12541
|
+
}),
|
|
12542
|
+
setPosition: method(object({
|
|
12543
|
+
deviceId: number().int().nonnegative(),
|
|
12544
|
+
position: number().min(0).max(100)
|
|
12545
|
+
}), _void(), {
|
|
12546
|
+
kind: "mutation",
|
|
12547
|
+
auth: "admin"
|
|
12548
|
+
}),
|
|
12549
|
+
setTiltPosition: method(object({
|
|
12550
|
+
deviceId: number().int().nonnegative(),
|
|
12551
|
+
tiltPosition: number().min(0).max(100)
|
|
12552
|
+
}), _void(), {
|
|
12553
|
+
kind: "mutation",
|
|
12554
|
+
auth: "admin"
|
|
12555
|
+
})
|
|
12556
|
+
},
|
|
12557
|
+
status: {
|
|
12558
|
+
schema: CoverStatusSchema,
|
|
12559
|
+
kind: "push"
|
|
12560
|
+
},
|
|
12561
|
+
/**
|
|
12562
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch
|
|
12563
|
+
* the slice for live position changes during a move.
|
|
12564
|
+
*/
|
|
12565
|
+
runtimeState: CoverStatusSchema
|
|
12566
|
+
};
|
|
11512
12567
|
/**
|
|
11513
12568
|
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
11514
12569
|
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
@@ -11538,7 +12593,12 @@ var NormalizedRangeSchema$1 = object({
|
|
|
11538
12593
|
max: number(),
|
|
11539
12594
|
step: number()
|
|
11540
12595
|
});
|
|
11541
|
-
|
|
12596
|
+
/**
|
|
12597
|
+
* Current day/night state. Optional fields are absent when the camera
|
|
12598
|
+
* does not expose that knob (a photocell-less model reports no
|
|
12599
|
+
* `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
|
|
12600
|
+
*/
|
|
12601
|
+
var DayNightStatusSchema = object({
|
|
11542
12602
|
mode: DayNightModeSchema,
|
|
11543
12603
|
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
11544
12604
|
sensitivity: number().optional(),
|
|
@@ -11571,13 +12631,33 @@ var DayNightSettingsPatchSchema = object({
|
|
|
11571
12631
|
sensitivity: number().optional(),
|
|
11572
12632
|
switchDelaySec: number().optional()
|
|
11573
12633
|
});
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
12634
|
+
var dayNightCapability = {
|
|
12635
|
+
name: "day-night",
|
|
12636
|
+
scope: "device",
|
|
12637
|
+
deviceNative: true,
|
|
12638
|
+
mode: "singleton",
|
|
12639
|
+
deviceTypes: [DeviceType.Camera],
|
|
12640
|
+
deviceConfig: { ui: {
|
|
12641
|
+
kind: "derived-form",
|
|
12642
|
+
builderId: "day-night",
|
|
12643
|
+
tab: "image"
|
|
12644
|
+
} },
|
|
12645
|
+
methods: {
|
|
12646
|
+
getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
|
|
12647
|
+
setSettings: method(object({
|
|
12648
|
+
deviceId: number(),
|
|
12649
|
+
settings: DayNightSettingsPatchSchema
|
|
12650
|
+
}), _void(), {
|
|
12651
|
+
kind: "mutation",
|
|
12652
|
+
auth: "admin"
|
|
12653
|
+
})
|
|
12654
|
+
},
|
|
12655
|
+
status: {
|
|
12656
|
+
schema: DayNightStatusSchema,
|
|
12657
|
+
kind: "poll"
|
|
12658
|
+
},
|
|
12659
|
+
runtimeState: DayNightStatusSchema
|
|
12660
|
+
};
|
|
11581
12661
|
/**
|
|
11582
12662
|
* Identity envelope for a device's upstream-system metadata.
|
|
11583
12663
|
*
|
|
@@ -11621,6 +12701,66 @@ var SourceInfoSchema = object({
|
|
|
11621
12701
|
raw: record(string(), unknown()).optional()
|
|
11622
12702
|
});
|
|
11623
12703
|
/**
|
|
12704
|
+
* Build the synthetic SourceInfo every existing provider falls back to
|
|
12705
|
+
* when no upstream value has been persisted. Keeps non-migrated providers
|
|
12706
|
+
* (Reolink / Hikvision / ONVIF / Frigate / RTSP) functional without code
|
|
12707
|
+
* changes — `id` reuses the CamStack stableId, `system` reuses the addon
|
|
12708
|
+
* id. Real upstream identity replaces this once a provider migrates and
|
|
12709
|
+
* calls `updateSourceInfo()` with concrete values.
|
|
12710
|
+
*/
|
|
12711
|
+
function synthesizeSourceInfo(input) {
|
|
12712
|
+
return {
|
|
12713
|
+
id: input.stableId,
|
|
12714
|
+
system: input.addonId
|
|
12715
|
+
};
|
|
12716
|
+
}
|
|
12717
|
+
/**
|
|
12718
|
+
* Shallow-merge a partial patch over a prior SourceInfo. `undefined`
|
|
12719
|
+
* patch values are ignored (no field clobber); to clear a field
|
|
12720
|
+
* explicitly, providers omit it from the patch and rebuild from scratch
|
|
12721
|
+
* via `updateSourceInfo({})` cycles — uncommon enough that we don't
|
|
12722
|
+
* bother with a `null`-means-delete sentinel.
|
|
12723
|
+
*
|
|
12724
|
+
* The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
|
|
12725
|
+
* merges over existing `raw` keys instead of replacing the whole bag.
|
|
12726
|
+
*/
|
|
12727
|
+
function mergeSourceInfo(prev, patch) {
|
|
12728
|
+
const next = { ...prev };
|
|
12729
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
12730
|
+
if (v === void 0) continue;
|
|
12731
|
+
if (k === "raw" && typeof v === "object" && v !== null) {
|
|
12732
|
+
next.raw = {
|
|
12733
|
+
...prev.raw,
|
|
12734
|
+
...v
|
|
12735
|
+
};
|
|
12736
|
+
continue;
|
|
12737
|
+
}
|
|
12738
|
+
next[k] = v;
|
|
12739
|
+
}
|
|
12740
|
+
return next;
|
|
12741
|
+
}
|
|
12742
|
+
/**
|
|
12743
|
+
* Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
|
|
12744
|
+
* blob is a free-form `Record<string, unknown>` shared with hardware-
|
|
12745
|
+
* identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
|
|
12746
|
+
* lives under a single nested key to keep it composable with those.
|
|
12747
|
+
*/
|
|
12748
|
+
var SOURCE_INFO_METADATA_KEY = "sourceInfo";
|
|
12749
|
+
/**
|
|
12750
|
+
* Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
|
|
12751
|
+
* with Zod. Returns `null` when the blob is missing or the nested
|
|
12752
|
+
* `sourceInfo` key fails validation — callers fall back to the
|
|
12753
|
+
* synthetic default in that case (see `synthesizeSourceInfo`).
|
|
12754
|
+
*/
|
|
12755
|
+
function extractSourceInfoFromMetadata(metadata) {
|
|
12756
|
+
if (!metadata) return null;
|
|
12757
|
+
const raw = metadata[SOURCE_INFO_METADATA_KEY];
|
|
12758
|
+
if (!raw) return null;
|
|
12759
|
+
const parsed = SourceInfoSchema.safeParse(raw);
|
|
12760
|
+
if (!parsed.success) return null;
|
|
12761
|
+
return parsed.data;
|
|
12762
|
+
}
|
|
12763
|
+
/**
|
|
11624
12764
|
* device-discovery — device-scoped capability for parents that host /
|
|
11625
12765
|
* enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
|
|
11626
12766
|
* integrations). The parent advertises a list of discoverable children,
|
|
@@ -11707,38 +12847,129 @@ var DeviceDiscoveryStatusSchema = object({
|
|
|
11707
12847
|
/** Last error surfaced from the source (rendered as a banner). */
|
|
11708
12848
|
lastError: string().nullable()
|
|
11709
12849
|
});
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
}),
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
12850
|
+
var deviceDiscoveryCapability = {
|
|
12851
|
+
name: "device-discovery",
|
|
12852
|
+
scope: "device",
|
|
12853
|
+
deviceNative: true,
|
|
12854
|
+
mode: "singleton",
|
|
12855
|
+
deviceTypes: [DeviceType.Hub],
|
|
12856
|
+
status: {
|
|
12857
|
+
schema: DeviceDiscoveryStatusSchema,
|
|
12858
|
+
kind: "poll"
|
|
12859
|
+
},
|
|
12860
|
+
runtimeState: DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int().nonnegative() }),
|
|
12861
|
+
methods: {
|
|
12862
|
+
/**
|
|
12863
|
+
* Snapshot of the current `discovered` list. Returns the
|
|
12864
|
+
* runtime-state cache — call `refreshDiscovery` first if a
|
|
12865
|
+
* fresh round-trip to the source is required.
|
|
12866
|
+
*/
|
|
12867
|
+
listDiscovered: method(object({ deviceId: number().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly()),
|
|
12868
|
+
/**
|
|
12869
|
+
* Force the integration to re-enumerate and update the
|
|
12870
|
+
* runtime-state slice. Returns the freshly-enumerated list (also
|
|
12871
|
+
* available via `listDiscovered` post-call).
|
|
12872
|
+
*/
|
|
12873
|
+
refreshDiscovery: method(object({ deviceId: number().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly(), {
|
|
12874
|
+
kind: "mutation",
|
|
12875
|
+
auth: "admin"
|
|
12876
|
+
}),
|
|
12877
|
+
/**
|
|
12878
|
+
* Promote a discovered entry to a real child device. The framework
|
|
12879
|
+
* creates the child via `kernel.devices.create()` with
|
|
12880
|
+
* `parentDeviceId = parent.id` and seeds the child's config from
|
|
12881
|
+
* `childInitialConfig` (driver-defined; usually carries channel +
|
|
12882
|
+
* uid + parent reference). Returns the kernel-assigned numeric id.
|
|
12883
|
+
*/
|
|
12884
|
+
adoptDevice: method(object({
|
|
12885
|
+
deviceId: number().int().nonnegative(),
|
|
12886
|
+
childNativeId: string(),
|
|
12887
|
+
/** Optional override for the child's display name. */
|
|
12888
|
+
name: string().optional()
|
|
12889
|
+
}), object({
|
|
12890
|
+
deviceId: number().int().nonnegative(),
|
|
12891
|
+
stableId: string()
|
|
12892
|
+
}), {
|
|
12893
|
+
kind: "mutation",
|
|
12894
|
+
auth: "admin"
|
|
12895
|
+
}),
|
|
12896
|
+
/**
|
|
12897
|
+
* Inverse of `adoptDevice`: removes the child device from the
|
|
12898
|
+
* kernel registry. The discovered entry remains in the
|
|
12899
|
+
* enumeration (status updates resume) so the operator can re-adopt
|
|
12900
|
+
* it later without a fresh refresh.
|
|
12901
|
+
*/
|
|
12902
|
+
releaseDevice: method(object({
|
|
12903
|
+
deviceId: number().int().nonnegative(),
|
|
12904
|
+
childDeviceId: number().int().nonnegative()
|
|
12905
|
+
}), _void(), {
|
|
12906
|
+
kind: "mutation",
|
|
12907
|
+
auth: "admin"
|
|
12908
|
+
})
|
|
12909
|
+
}
|
|
12910
|
+
};
|
|
12911
|
+
/**
|
|
12912
|
+
* Doorbell button cap. Two kinds of providers coexist behind this cap
|
|
12913
|
+
* name (same pattern as `snapshot`):
|
|
12914
|
+
*
|
|
12915
|
+
* - **Native** providers: registered per-device by device-driver
|
|
12916
|
+
* addons via `ctx.registerNativeCap` — either on a
|
|
12917
|
+
* `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
|
|
12918
|
+
* or directly on the camera (Reolink registers at camera level).
|
|
12919
|
+
* Emits an `onPressed` event every time the firmware pushes a
|
|
12920
|
+
* ring; status tracks the last press and a pressCount since start
|
|
12921
|
+
* (diagnostic).
|
|
12922
|
+
*
|
|
12923
|
+
* - **Wrapper** provider: the `virtual-doorbell` system builtin
|
|
12924
|
+
* (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
|
|
12925
|
+
* device (contact / switch / event-emitter …) into a doorbell for
|
|
12926
|
+
* a camera. `defaultActive: false` — the operator explicitly binds
|
|
12927
|
+
* it per camera in the device-bindings UI, then picks the source
|
|
12928
|
+
* device + trigger in the per-device settings.
|
|
12929
|
+
*
|
|
12930
|
+
* The DeviceEventPropagator re-emits `onPressed` on the camera parent
|
|
12931
|
+
* — subscribers listening at the camera level receive ring events
|
|
12932
|
+
* with `via[]` populated. No code on the parent needed.
|
|
12933
|
+
*/
|
|
12934
|
+
var DoorbellStatusSchema = object({
|
|
11732
12935
|
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
11733
12936
|
lastPressedAt: number().nullable(),
|
|
11734
12937
|
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
11735
12938
|
pressCountSinceStart: number()
|
|
11736
12939
|
});
|
|
11737
|
-
object({
|
|
12940
|
+
var DoorbellPressEventSchema = object({
|
|
11738
12941
|
deviceId: number(),
|
|
11739
12942
|
timestamp: number()
|
|
11740
12943
|
});
|
|
11741
|
-
|
|
12944
|
+
var doorbellCapability = {
|
|
12945
|
+
name: "doorbell",
|
|
12946
|
+
scope: "device",
|
|
12947
|
+
deviceNative: true,
|
|
12948
|
+
mode: "singleton",
|
|
12949
|
+
kind: "wrapper",
|
|
12950
|
+
defaultActive: false,
|
|
12951
|
+
deviceTypes: [DeviceType.Button, DeviceType.Camera],
|
|
12952
|
+
exposesDeviceSettings: true,
|
|
12953
|
+
methods: {},
|
|
12954
|
+
events: {
|
|
12955
|
+
/**
|
|
12956
|
+
* Fires once per physical press. Reolink delivers via Baichuan
|
|
12957
|
+
* push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
|
|
12958
|
+
* release/duration — it's a pulse.
|
|
12959
|
+
*/
|
|
12960
|
+
onPressed: { data: DoorbellPressEventSchema } },
|
|
12961
|
+
status: {
|
|
12962
|
+
schema: DoorbellStatusSchema,
|
|
12963
|
+
kind: "push"
|
|
12964
|
+
},
|
|
12965
|
+
/**
|
|
12966
|
+
* Runtime-state slice — last press timestamp + lifetime press count.
|
|
12967
|
+
* Mirrored by the kernel and readable via
|
|
12968
|
+
* `device.state.doorbell.value`. UIs can show "last ring 5m ago"
|
|
12969
|
+
* without subscribing.
|
|
12970
|
+
*/
|
|
12971
|
+
runtimeState: DoorbellStatusSchema
|
|
12972
|
+
};
|
|
11742
12973
|
/**
|
|
11743
12974
|
* Enum-state sensor — a string value picked from a finite option set.
|
|
11744
12975
|
* Drives HA `sensor` entries with `state_class: enum` (HVAC action
|
|
@@ -11754,7 +12985,7 @@ var EnumSensorDateTimeFormatSchema = _enum([
|
|
|
11754
12985
|
"time",
|
|
11755
12986
|
"datetime"
|
|
11756
12987
|
]);
|
|
11757
|
-
object({
|
|
12988
|
+
var EnumSensorStatusSchema = object({
|
|
11758
12989
|
value: string(),
|
|
11759
12990
|
/**
|
|
11760
12991
|
* Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
|
|
@@ -11766,7 +12997,19 @@ object({
|
|
|
11766
12997
|
/** Ms epoch when the slice was last updated. */
|
|
11767
12998
|
lastFetchedAt: number()
|
|
11768
12999
|
});
|
|
11769
|
-
|
|
13000
|
+
var enumSensorCapability = {
|
|
13001
|
+
name: "enum-sensor",
|
|
13002
|
+
scope: "device",
|
|
13003
|
+
deviceNative: true,
|
|
13004
|
+
mode: "singleton",
|
|
13005
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13006
|
+
methods: {},
|
|
13007
|
+
status: {
|
|
13008
|
+
schema: EnumSensorStatusSchema,
|
|
13009
|
+
kind: "push"
|
|
13010
|
+
},
|
|
13011
|
+
runtimeState: EnumSensorStatusSchema
|
|
13012
|
+
};
|
|
11770
13013
|
/**
|
|
11771
13014
|
* Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
|
|
11772
13015
|
* device. Carries the device's EXACT declared event vocabulary verbatim
|
|
@@ -11784,12 +13027,25 @@ var EventFireSchema = object({
|
|
|
11784
13027
|
timestamp: number(),
|
|
11785
13028
|
seq: number()
|
|
11786
13029
|
});
|
|
11787
|
-
object({
|
|
13030
|
+
var EventEmitterStatusSchema = object({
|
|
11788
13031
|
eventTypes: array(string()),
|
|
11789
13032
|
lastEvent: EventFireSchema.nullable(),
|
|
11790
13033
|
eventCountSinceStart: number()
|
|
11791
13034
|
});
|
|
11792
|
-
|
|
13035
|
+
var eventEmitterCapability = {
|
|
13036
|
+
name: "event-emitter",
|
|
13037
|
+
scope: "device",
|
|
13038
|
+
deviceNative: true,
|
|
13039
|
+
mode: "singleton",
|
|
13040
|
+
deviceTypes: [DeviceType.EventEmitter],
|
|
13041
|
+
methods: {},
|
|
13042
|
+
events: { onEvent: { data: EventFireSchema } },
|
|
13043
|
+
status: {
|
|
13044
|
+
schema: EventEmitterStatusSchema,
|
|
13045
|
+
kind: "push"
|
|
13046
|
+
},
|
|
13047
|
+
runtimeState: EventEmitterStatusSchema
|
|
13048
|
+
};
|
|
11793
13049
|
/**
|
|
11794
13050
|
* Fan-control cap. Models HA `fan.*` entity-specific surfaces:
|
|
11795
13051
|
* speed percentage, preset modes, ceiling-fan direction, and
|
|
@@ -11809,7 +13065,7 @@ DeviceType.EventEmitter;
|
|
|
11809
13065
|
* - `FanOscillating` — oscillation toggle
|
|
11810
13066
|
*/
|
|
11811
13067
|
var FanDirectionSchema = _enum(["forward", "reverse"]);
|
|
11812
|
-
object({
|
|
13068
|
+
var FanControlStatusSchema = object({
|
|
11813
13069
|
/** Active speed as 0..100 inclusive. Null when the device has no
|
|
11814
13070
|
* speed surface (single-speed fan). */
|
|
11815
13071
|
percentage: number().min(0).max(100).nullable(),
|
|
@@ -11830,45 +13086,118 @@ object({
|
|
|
11830
13086
|
/** Ms epoch when the slice was last updated. */
|
|
11831
13087
|
lastChangedAt: number()
|
|
11832
13088
|
});
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
})
|
|
11858
|
-
|
|
13089
|
+
var fanControlCapability = {
|
|
13090
|
+
name: "fan-control",
|
|
13091
|
+
scope: "device",
|
|
13092
|
+
deviceNative: true,
|
|
13093
|
+
mode: "singleton",
|
|
13094
|
+
deviceTypes: [DeviceType.Fan],
|
|
13095
|
+
methods: {
|
|
13096
|
+
setPercentage: method(object({
|
|
13097
|
+
deviceId: number().int().nonnegative(),
|
|
13098
|
+
percentage: number().min(0).max(100)
|
|
13099
|
+
}), _void(), {
|
|
13100
|
+
kind: "mutation",
|
|
13101
|
+
auth: "admin"
|
|
13102
|
+
}),
|
|
13103
|
+
setPreset: method(object({
|
|
13104
|
+
deviceId: number().int().nonnegative(),
|
|
13105
|
+
preset: string().min(1)
|
|
13106
|
+
}), _void(), {
|
|
13107
|
+
kind: "mutation",
|
|
13108
|
+
auth: "admin"
|
|
13109
|
+
}),
|
|
13110
|
+
setDirection: method(object({
|
|
13111
|
+
deviceId: number().int().nonnegative(),
|
|
13112
|
+
direction: FanDirectionSchema
|
|
13113
|
+
}), _void(), {
|
|
13114
|
+
kind: "mutation",
|
|
13115
|
+
auth: "admin"
|
|
13116
|
+
}),
|
|
13117
|
+
setOscillating: method(object({
|
|
13118
|
+
deviceId: number().int().nonnegative(),
|
|
13119
|
+
oscillating: boolean()
|
|
13120
|
+
}), _void(), {
|
|
13121
|
+
kind: "mutation",
|
|
13122
|
+
auth: "admin"
|
|
13123
|
+
})
|
|
13124
|
+
},
|
|
13125
|
+
status: {
|
|
13126
|
+
schema: FanControlStatusSchema,
|
|
13127
|
+
kind: "push"
|
|
13128
|
+
},
|
|
13129
|
+
/**
|
|
13130
|
+
* Runtime-state slice — mirrored by the kernel. UI fan speed
|
|
13131
|
+
* sliders read `percentage` for live updates.
|
|
13132
|
+
*/
|
|
13133
|
+
runtimeState: FanControlStatusSchema
|
|
13134
|
+
};
|
|
13135
|
+
/**
|
|
13136
|
+
* Water leak / moisture sensor. Boolean "is liquid currently
|
|
13137
|
+
* detected" with the timestamp of the last transition. Drives Home
|
|
13138
|
+
* Assistant `binary_sensor` entries with `device_class: moisture`,
|
|
13139
|
+
* and any future native flood sensor.
|
|
13140
|
+
*
|
|
13141
|
+
* Push-driven from the upstream source.
|
|
13142
|
+
*/
|
|
13143
|
+
var FloodStatusSchema = object({
|
|
11859
13144
|
/** True when leak is currently detected. */
|
|
11860
13145
|
flooded: boolean(),
|
|
11861
13146
|
/** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
|
|
11862
13147
|
lastChangedAt: number()
|
|
11863
13148
|
});
|
|
11864
|
-
|
|
11865
|
-
|
|
13149
|
+
var floodCapability = {
|
|
13150
|
+
name: "flood",
|
|
13151
|
+
scope: "device",
|
|
13152
|
+
deviceNative: true,
|
|
13153
|
+
mode: "singleton",
|
|
13154
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13155
|
+
methods: {},
|
|
13156
|
+
status: {
|
|
13157
|
+
schema: FloodStatusSchema,
|
|
13158
|
+
kind: "push"
|
|
13159
|
+
},
|
|
13160
|
+
runtimeState: FloodStatusSchema
|
|
13161
|
+
};
|
|
13162
|
+
/**
|
|
13163
|
+
* Combustible-gas (LPG / methane / hydrogen) alarm sensor. Drives
|
|
13164
|
+
* Home Assistant `binary_sensor` entries with `device_class: gas`.
|
|
13165
|
+
* Push-driven.
|
|
13166
|
+
*/
|
|
13167
|
+
var GasStatusSchema = object({
|
|
11866
13168
|
detected: boolean(),
|
|
11867
13169
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11868
13170
|
lastChangedAt: number()
|
|
11869
13171
|
});
|
|
11870
|
-
|
|
11871
|
-
|
|
13172
|
+
var gasCapability = {
|
|
13173
|
+
name: "gas",
|
|
13174
|
+
scope: "device",
|
|
13175
|
+
deviceNative: true,
|
|
13176
|
+
mode: "singleton",
|
|
13177
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13178
|
+
methods: {},
|
|
13179
|
+
status: {
|
|
13180
|
+
schema: GasStatusSchema,
|
|
13181
|
+
kind: "push"
|
|
13182
|
+
},
|
|
13183
|
+
runtimeState: GasStatusSchema
|
|
13184
|
+
};
|
|
13185
|
+
/**
|
|
13186
|
+
* Humidifier / dehumidifier cap. Models HA `humidifier.*` entities —
|
|
13187
|
+
* an on/off actuator with an optional target-humidity setpoint and an
|
|
13188
|
+
* optional vendor mode (`auto` / `normal` / `baby` / …).
|
|
13189
|
+
*
|
|
13190
|
+
* A climate-family sibling: the slice carries the on/off state, the
|
|
13191
|
+
* current + target relative humidity (0..100), the active mode and its
|
|
13192
|
+
* available set, plus HA's free-form `action` readout
|
|
13193
|
+
* (`humidifying` / `drying` / `idle` / `off`). `minHumidity` /
|
|
13194
|
+
* `maxHumidity` mirror HA's `min_humidity` / `max_humidity` attributes
|
|
13195
|
+
* (null → the UI falls back to a 0..100 range).
|
|
13196
|
+
*
|
|
13197
|
+
* Providers populate only what the hardware reports — `mode` and the
|
|
13198
|
+
* humidity fields stay null when the device has no such surface.
|
|
13199
|
+
*/
|
|
13200
|
+
var HumidifierStatusSchema = object({
|
|
11872
13201
|
/** Whether the humidifier is currently on. */
|
|
11873
13202
|
on: boolean(),
|
|
11874
13203
|
/** Current measured relative humidity (0..100). Null when not reported. */
|
|
@@ -11890,26 +13219,54 @@ object({
|
|
|
11890
13219
|
/** Ms epoch when the slice was last updated. */
|
|
11891
13220
|
lastChangedAt: number()
|
|
11892
13221
|
});
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
13222
|
+
var humidifierCapability = {
|
|
13223
|
+
name: "humidifier",
|
|
13224
|
+
scope: "device",
|
|
13225
|
+
deviceNative: true,
|
|
13226
|
+
mode: "singleton",
|
|
13227
|
+
deviceTypes: [DeviceType.Humidifier],
|
|
13228
|
+
methods: {
|
|
13229
|
+
setOn: method(object({
|
|
13230
|
+
deviceId: number().int().nonnegative(),
|
|
13231
|
+
on: boolean()
|
|
13232
|
+
}), _void(), {
|
|
13233
|
+
kind: "mutation",
|
|
13234
|
+
auth: "admin"
|
|
13235
|
+
}),
|
|
13236
|
+
setTargetHumidity: method(object({
|
|
13237
|
+
deviceId: number().int().nonnegative(),
|
|
13238
|
+
humidity: number().min(0).max(100)
|
|
13239
|
+
}), _void(), {
|
|
13240
|
+
kind: "mutation",
|
|
13241
|
+
auth: "admin"
|
|
13242
|
+
}),
|
|
13243
|
+
setMode: method(object({
|
|
13244
|
+
deviceId: number().int().nonnegative(),
|
|
13245
|
+
mode: string().min(1)
|
|
13246
|
+
}), _void(), {
|
|
13247
|
+
kind: "mutation",
|
|
13248
|
+
auth: "admin"
|
|
13249
|
+
})
|
|
13250
|
+
},
|
|
13251
|
+
status: {
|
|
13252
|
+
schema: HumidifierStatusSchema,
|
|
13253
|
+
kind: "push"
|
|
13254
|
+
},
|
|
13255
|
+
/**
|
|
13256
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
13257
|
+
* slice for live humidity / mode changes.
|
|
13258
|
+
*/
|
|
13259
|
+
runtimeState: HumidifierStatusSchema
|
|
13260
|
+
};
|
|
13261
|
+
/**
|
|
13262
|
+
* Single-metric humidity reading. Drives Home Assistant `sensor`
|
|
13263
|
+
* entries with `device_class: humidity`.
|
|
13264
|
+
*
|
|
13265
|
+
* Unit normalisation: percent. The canonical display unit (`%`) is a
|
|
13266
|
+
* descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
13267
|
+
* `sourceInfo`.
|
|
13268
|
+
*/
|
|
13269
|
+
var HumiditySensorStatusSchema = object({
|
|
11913
13270
|
/** Current relative humidity, 0..100. */
|
|
11914
13271
|
percent: number().min(0).max(100),
|
|
11915
13272
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -11924,15 +13281,57 @@ object({
|
|
|
11924
13281
|
* auto-formatting when absent. */
|
|
11925
13282
|
precision: number().int().min(0).max(10).optional()
|
|
11926
13283
|
});
|
|
11927
|
-
|
|
11928
|
-
|
|
13284
|
+
var humiditySensorCapability = {
|
|
13285
|
+
name: "humidity-sensor",
|
|
13286
|
+
scope: "device",
|
|
13287
|
+
deviceNative: true,
|
|
13288
|
+
mode: "singleton",
|
|
13289
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
|
|
13290
|
+
methods: {},
|
|
13291
|
+
status: {
|
|
13292
|
+
schema: HumiditySensorStatusSchema,
|
|
13293
|
+
kind: "push"
|
|
13294
|
+
},
|
|
13295
|
+
runtimeState: HumiditySensorStatusSchema
|
|
13296
|
+
};
|
|
13297
|
+
/**
|
|
13298
|
+
* Image display cap. Models a single still image exposed by an integration —
|
|
13299
|
+
* a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
|
|
13300
|
+
*
|
|
13301
|
+
* Read-only: there are no setters. The provider resolves whatever upstream
|
|
13302
|
+
* source it has into an ABSOLUTE URL the browser loads directly:
|
|
13303
|
+
* - HA `image.*` entities → the `entity_picture` signed-token path
|
|
13304
|
+
* (token stays in the query string, so no auth header is needed);
|
|
13305
|
+
* - a Dreame/robot map → the cloud/OSS map-image URL (or an addon
|
|
13306
|
+
* data-plane URL serving the rendered map bytes), exposed as its own
|
|
13307
|
+
* Image child device grouped under the robot's container.
|
|
13308
|
+
* The slice carries that URL plus the upstream last-updated timestamp; the
|
|
13309
|
+
* image changes when the source's last-updated marker changes.
|
|
13310
|
+
*/
|
|
13311
|
+
var ImageStatusSchema = object({
|
|
11929
13312
|
/** Absolute signed URL the browser loads directly. Null when the
|
|
11930
13313
|
* entity exposes no `entity_picture` (yet). */
|
|
11931
13314
|
url: string().nullable(),
|
|
11932
13315
|
/** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
|
|
11933
13316
|
lastUpdated: number().nullable()
|
|
11934
13317
|
});
|
|
11935
|
-
|
|
13318
|
+
var imageCapability = {
|
|
13319
|
+
name: "image",
|
|
13320
|
+
scope: "device",
|
|
13321
|
+
deviceNative: true,
|
|
13322
|
+
mode: "singleton",
|
|
13323
|
+
deviceTypes: [DeviceType.Image],
|
|
13324
|
+
methods: {},
|
|
13325
|
+
status: {
|
|
13326
|
+
schema: ImageStatusSchema,
|
|
13327
|
+
kind: "push"
|
|
13328
|
+
},
|
|
13329
|
+
/**
|
|
13330
|
+
* Runtime-state slice — mirrored by the kernel. The UI reads `url`
|
|
13331
|
+
* directly and renders the still image.
|
|
13332
|
+
*/
|
|
13333
|
+
runtimeState: ImageStatusSchema
|
|
13334
|
+
};
|
|
11936
13335
|
/**
|
|
11937
13336
|
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
11938
13337
|
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
@@ -11984,7 +13383,12 @@ var NormalizedRangeSchema = object({
|
|
|
11984
13383
|
max: number(),
|
|
11985
13384
|
step: number()
|
|
11986
13385
|
});
|
|
11987
|
-
|
|
13386
|
+
/**
|
|
13387
|
+
* Current image-adjustment state. Every field optional — absent when the
|
|
13388
|
+
* camera does not expose that control. Slider values are NORMALIZED 0–100.
|
|
13389
|
+
* `lastFetchedAt` feeds the runtime-state bridge.
|
|
13390
|
+
*/
|
|
13391
|
+
var ImageSettingsStatusSchema = object({
|
|
11988
13392
|
/** Normalized 0–100. */
|
|
11989
13393
|
brightness: number().optional(),
|
|
11990
13394
|
/** Normalized 0–100. */
|
|
@@ -12050,13 +13454,33 @@ var ImageSettingsPatchSchema = object({
|
|
|
12050
13454
|
exposureMode: ExposureModeSchema.optional(),
|
|
12051
13455
|
backlightMode: BacklightModeSchema.optional()
|
|
12052
13456
|
});
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
13457
|
+
var imageSettingsCapability = {
|
|
13458
|
+
name: "image-settings",
|
|
13459
|
+
scope: "device",
|
|
13460
|
+
deviceNative: true,
|
|
13461
|
+
mode: "singleton",
|
|
13462
|
+
deviceTypes: [DeviceType.Camera],
|
|
13463
|
+
deviceConfig: { ui: {
|
|
13464
|
+
kind: "derived-form",
|
|
13465
|
+
builderId: "image-settings",
|
|
13466
|
+
tab: "image"
|
|
13467
|
+
} },
|
|
13468
|
+
methods: {
|
|
13469
|
+
getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
|
|
13470
|
+
setSettings: method(object({
|
|
13471
|
+
deviceId: number(),
|
|
13472
|
+
settings: ImageSettingsPatchSchema
|
|
13473
|
+
}), _void(), {
|
|
13474
|
+
kind: "mutation",
|
|
13475
|
+
auth: "admin"
|
|
13476
|
+
})
|
|
13477
|
+
},
|
|
13478
|
+
status: {
|
|
13479
|
+
schema: ImageSettingsStatusSchema,
|
|
13480
|
+
kind: "poll"
|
|
13481
|
+
},
|
|
13482
|
+
runtimeState: ImageSettingsStatusSchema
|
|
13483
|
+
};
|
|
12060
13484
|
/**
|
|
12061
13485
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
12062
13486
|
* with a mowing lifecycle plus a dock action.
|
|
@@ -12081,7 +13505,7 @@ var DeviceCodeSeveritySchema = _enum([
|
|
|
12081
13505
|
"warning",
|
|
12082
13506
|
"error"
|
|
12083
13507
|
]);
|
|
12084
|
-
object({
|
|
13508
|
+
var LawnMowerControlStatusSchema = object({
|
|
12085
13509
|
/** Lifecycle activity of the mower. */
|
|
12086
13510
|
activity: LawnMowerActivitySchema,
|
|
12087
13511
|
/** 0..100 battery percentage. Null when the device has no battery
|
|
@@ -12101,17 +13525,37 @@ object({
|
|
|
12101
13525
|
/** Ms epoch when the slice was last updated. */
|
|
12102
13526
|
lastChangedAt: number()
|
|
12103
13527
|
});
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
13528
|
+
var lawnMowerControlCapability = {
|
|
13529
|
+
name: "lawn-mower-control",
|
|
13530
|
+
scope: "device",
|
|
13531
|
+
deviceNative: true,
|
|
13532
|
+
mode: "singleton",
|
|
13533
|
+
deviceTypes: [DeviceType.LawnMower],
|
|
13534
|
+
methods: {
|
|
13535
|
+
startMowing: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13536
|
+
kind: "mutation",
|
|
13537
|
+
auth: "admin"
|
|
13538
|
+
}),
|
|
13539
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13540
|
+
kind: "mutation",
|
|
13541
|
+
auth: "admin"
|
|
13542
|
+
}),
|
|
13543
|
+
dock: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13544
|
+
kind: "mutation",
|
|
13545
|
+
auth: "admin"
|
|
13546
|
+
})
|
|
13547
|
+
},
|
|
13548
|
+
status: {
|
|
13549
|
+
schema: LawnMowerControlStatusSchema,
|
|
13550
|
+
kind: "push"
|
|
13551
|
+
},
|
|
13552
|
+
/**
|
|
13553
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
13554
|
+
* slice for live activity + battery changes.
|
|
13555
|
+
*/
|
|
13556
|
+
runtimeState: LawnMowerControlStatusSchema
|
|
13557
|
+
};
|
|
13558
|
+
var LockControlStatusSchema = object({
|
|
12115
13559
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
12116
13560
|
* failure to reach the target — operator intervention required. */
|
|
12117
13561
|
state: _enum([
|
|
@@ -12124,24 +13568,45 @@ object({
|
|
|
12124
13568
|
/** Ms epoch when the slice was last updated. */
|
|
12125
13569
|
lastChangedAt: number()
|
|
12126
13570
|
});
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
13571
|
+
var lockControlCapability = {
|
|
13572
|
+
name: "lock-control",
|
|
13573
|
+
scope: "device",
|
|
13574
|
+
deviceNative: true,
|
|
13575
|
+
mode: "singleton",
|
|
13576
|
+
deviceTypes: [DeviceType.Lock],
|
|
13577
|
+
methods: {
|
|
13578
|
+
lock: method(object({
|
|
13579
|
+
deviceId: number().int().nonnegative(),
|
|
13580
|
+
/** Optional PIN code required by some keypad locks. NOT
|
|
13581
|
+
* persisted — passed directly to the upstream service. */
|
|
13582
|
+
code: string().min(1).optional()
|
|
13583
|
+
}), _void(), {
|
|
13584
|
+
kind: "mutation",
|
|
13585
|
+
auth: "admin"
|
|
13586
|
+
}),
|
|
13587
|
+
unlock: method(object({
|
|
13588
|
+
deviceId: number().int().nonnegative(),
|
|
13589
|
+
code: string().min(1).optional()
|
|
13590
|
+
}), _void(), {
|
|
13591
|
+
kind: "mutation",
|
|
13592
|
+
auth: "admin"
|
|
13593
|
+
}),
|
|
13594
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13595
|
+
kind: "mutation",
|
|
13596
|
+
auth: "admin"
|
|
13597
|
+
})
|
|
13598
|
+
},
|
|
13599
|
+
status: {
|
|
13600
|
+
schema: LockControlStatusSchema,
|
|
13601
|
+
kind: "push"
|
|
13602
|
+
},
|
|
13603
|
+
/**
|
|
13604
|
+
* Runtime-state slice — mirrored by the kernel. UI lock buttons
|
|
13605
|
+
* read `state` and disable themselves during `locking`/`unlocking`
|
|
13606
|
+
* transitions.
|
|
13607
|
+
*/
|
|
13608
|
+
runtimeState: LockControlStatusSchema
|
|
13609
|
+
};
|
|
12145
13610
|
/**
|
|
12146
13611
|
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
12147
13612
|
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
@@ -12181,7 +13646,7 @@ var MediaInfoSchema = object({
|
|
|
12181
13646
|
/** Optional cover-art / thumbnail URL. */
|
|
12182
13647
|
imageUrl: string().optional()
|
|
12183
13648
|
});
|
|
12184
|
-
object({
|
|
13649
|
+
var MediaPlayerStatusSchema = object({
|
|
12185
13650
|
/** Playback lifecycle state. */
|
|
12186
13651
|
state: MediaPlayerStateSchema,
|
|
12187
13652
|
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
@@ -12212,68 +13677,98 @@ object({
|
|
|
12212
13677
|
/** Ms epoch when the slice was last updated. */
|
|
12213
13678
|
lastChangedAt: number()
|
|
12214
13679
|
});
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
}),
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
}), _void(), {
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
}),
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
}),
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
}), _void(), {
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
})
|
|
13680
|
+
var mediaPlayerCapability = {
|
|
13681
|
+
name: "media-player",
|
|
13682
|
+
scope: "device",
|
|
13683
|
+
deviceNative: true,
|
|
13684
|
+
mode: "singleton",
|
|
13685
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
13686
|
+
methods: {
|
|
13687
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13688
|
+
kind: "mutation",
|
|
13689
|
+
auth: "admin"
|
|
13690
|
+
}),
|
|
13691
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13692
|
+
kind: "mutation",
|
|
13693
|
+
auth: "admin"
|
|
13694
|
+
}),
|
|
13695
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13696
|
+
kind: "mutation",
|
|
13697
|
+
auth: "admin"
|
|
13698
|
+
}),
|
|
13699
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13700
|
+
kind: "mutation",
|
|
13701
|
+
auth: "admin"
|
|
13702
|
+
}),
|
|
13703
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13704
|
+
kind: "mutation",
|
|
13705
|
+
auth: "admin"
|
|
13706
|
+
}),
|
|
13707
|
+
seek: method(object({
|
|
13708
|
+
deviceId: number().int().nonnegative(),
|
|
13709
|
+
positionMs: number().int().nonnegative()
|
|
13710
|
+
}), _void(), {
|
|
13711
|
+
kind: "mutation",
|
|
13712
|
+
auth: "admin"
|
|
13713
|
+
}),
|
|
13714
|
+
setVolume: method(object({
|
|
13715
|
+
deviceId: number().int().nonnegative(),
|
|
13716
|
+
volumeLevel: number().min(0).max(100)
|
|
13717
|
+
}), _void(), {
|
|
13718
|
+
kind: "mutation",
|
|
13719
|
+
auth: "admin"
|
|
13720
|
+
}),
|
|
13721
|
+
setMute: method(object({
|
|
13722
|
+
deviceId: number().int().nonnegative(),
|
|
13723
|
+
muted: boolean()
|
|
13724
|
+
}), _void(), {
|
|
13725
|
+
kind: "mutation",
|
|
13726
|
+
auth: "admin"
|
|
13727
|
+
}),
|
|
13728
|
+
setShuffle: method(object({
|
|
13729
|
+
deviceId: number().int().nonnegative(),
|
|
13730
|
+
shuffle: boolean()
|
|
13731
|
+
}), _void(), {
|
|
13732
|
+
kind: "mutation",
|
|
13733
|
+
auth: "admin"
|
|
13734
|
+
}),
|
|
13735
|
+
setRepeat: method(object({
|
|
13736
|
+
deviceId: number().int().nonnegative(),
|
|
13737
|
+
repeat: MediaPlayerRepeatSchema
|
|
13738
|
+
}), _void(), {
|
|
13739
|
+
kind: "mutation",
|
|
13740
|
+
auth: "admin"
|
|
13741
|
+
}),
|
|
13742
|
+
selectSource: method(object({
|
|
13743
|
+
deviceId: number().int().nonnegative(),
|
|
13744
|
+
source: string().min(1)
|
|
13745
|
+
}), _void(), {
|
|
13746
|
+
kind: "mutation",
|
|
13747
|
+
auth: "admin"
|
|
13748
|
+
}),
|
|
13749
|
+
playMedia: method(object({
|
|
13750
|
+
deviceId: number().int().nonnegative(),
|
|
13751
|
+
/** Media identifier / URL. */
|
|
13752
|
+
mediaId: string().min(1),
|
|
13753
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
13754
|
+
* passes it through to the upstream service. */
|
|
13755
|
+
mediaType: string().min(1)
|
|
13756
|
+
}), _void(), {
|
|
13757
|
+
kind: "mutation",
|
|
13758
|
+
auth: "admin"
|
|
13759
|
+
})
|
|
13760
|
+
},
|
|
13761
|
+
status: {
|
|
13762
|
+
schema: MediaPlayerStatusSchema,
|
|
13763
|
+
kind: "push"
|
|
13764
|
+
},
|
|
13765
|
+
/**
|
|
13766
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
13767
|
+
* full slice for live now-playing, volume, and progress updates
|
|
13768
|
+
* without polling.
|
|
13769
|
+
*/
|
|
13770
|
+
runtimeState: MediaPlayerStatusSchema
|
|
13771
|
+
};
|
|
12277
13772
|
/** Shared Zod schemas used across detection capabilities. */
|
|
12278
13773
|
/**
|
|
12279
13774
|
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
@@ -12825,33 +14320,66 @@ var PolygonPointSchema = object({
|
|
|
12825
14320
|
});
|
|
12826
14321
|
/** A camera detection zone — pure geometry/identity. */
|
|
12827
14322
|
var ZoneSchema = object({
|
|
12828
|
-
id: string(),
|
|
12829
|
-
name: string(),
|
|
12830
|
-
kind: ZoneKindEnum.default("polygon"),
|
|
12831
|
-
/** Polygon vertices, fraction of frame (0–1). */
|
|
12832
|
-
polygon: array(PolygonPointSchema).readonly(),
|
|
12833
|
-
/** Visual color for UI rendering. */
|
|
12834
|
-
color: string().default("#3b82f6")
|
|
12835
|
-
});
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
14323
|
+
id: string(),
|
|
14324
|
+
name: string(),
|
|
14325
|
+
kind: ZoneKindEnum.default("polygon"),
|
|
14326
|
+
/** Polygon vertices, fraction of frame (0–1). */
|
|
14327
|
+
polygon: array(PolygonPointSchema).readonly(),
|
|
14328
|
+
/** Visual color for UI rendering. */
|
|
14329
|
+
color: string().default("#3b82f6")
|
|
14330
|
+
});
|
|
14331
|
+
/**
|
|
14332
|
+
* Zones capability — per-camera CRUD over polygon detection zones.
|
|
14333
|
+
*
|
|
14334
|
+
* Provider lives in `addon-pipeline-orchestrator` (hub-only). Persists
|
|
14335
|
+
* to per-device settings and mirrors into the `zones` device-state
|
|
14336
|
+
* slice on every mutation, so downstream consumers can subscribe via
|
|
14337
|
+
* `dev.state.zones.onChanged`.
|
|
14338
|
+
*
|
|
14339
|
+
* The cap surface only handles geometry + identity; filtering
|
|
14340
|
+
* behaviour (per-class, include/exclude, threshold) lives in the
|
|
14341
|
+
* consumer addons' rule arrays — see `ZoneRuleSchema` exported from
|
|
14342
|
+
* `capabilities/schemas/zone-rule.js`.
|
|
14343
|
+
*/
|
|
14344
|
+
var zonesCapability = {
|
|
14345
|
+
name: "zones",
|
|
14346
|
+
scope: "device",
|
|
14347
|
+
mode: "singleton",
|
|
14348
|
+
deviceTypes: [DeviceType.Camera],
|
|
14349
|
+
methods: {
|
|
14350
|
+
listZones: method(object({ deviceId: number() }), array(ZoneSchema).readonly()),
|
|
14351
|
+
addZone: method(object({
|
|
14352
|
+
deviceId: number(),
|
|
14353
|
+
zone: ZoneSchema
|
|
14354
|
+
}), _void(), {
|
|
14355
|
+
kind: "mutation",
|
|
14356
|
+
auth: "admin"
|
|
14357
|
+
}),
|
|
14358
|
+
removeZone: method(object({
|
|
14359
|
+
deviceId: number(),
|
|
14360
|
+
zoneId: string()
|
|
14361
|
+
}), _void(), {
|
|
14362
|
+
kind: "mutation",
|
|
14363
|
+
auth: "admin"
|
|
14364
|
+
}),
|
|
14365
|
+
updateZone: method(object({
|
|
14366
|
+
deviceId: number(),
|
|
14367
|
+
zone: ZoneSchema
|
|
14368
|
+
}), _void(), {
|
|
14369
|
+
kind: "mutation",
|
|
14370
|
+
auth: "admin"
|
|
14371
|
+
})
|
|
14372
|
+
},
|
|
14373
|
+
/**
|
|
14374
|
+
* Runtime-state slice — the live zone catalogue mirrored by the
|
|
14375
|
+
* orchestrator on every CRUD mutation. Consumers read via
|
|
14376
|
+
* `device.state.zones.value` / `.watch(...)` without round-tripping
|
|
14377
|
+
* the cap, and the codegen DeviceProxy auto-wires the reactive
|
|
14378
|
+
* handle. Slice shape is `{ zones: Zone[] }` so future extensions
|
|
14379
|
+
* (e.g. zone groupings) can sit alongside the polygon list.
|
|
14380
|
+
*/
|
|
14381
|
+
runtimeState: object({ zones: array(ZoneSchema).readonly() })
|
|
14382
|
+
};
|
|
12855
14383
|
/**
|
|
12856
14384
|
* A bounding box in NORMALIZED [0,1] frame coordinates for `getNativeCrop`. The
|
|
12857
14385
|
* decode worker resolves it against the RETAINED native frame's real pixel dims,
|
|
@@ -13267,7 +14795,19 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
13267
14795
|
parent: DetailParentSchema,
|
|
13268
14796
|
steps: array(string()).optional()
|
|
13269
14797
|
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
|
|
13270
|
-
|
|
14798
|
+
/**
|
|
14799
|
+
* Hardware / firmware motion sensor cap — binary detected state plus
|
|
14800
|
+
* a timestamp of the last observation. Distinct from
|
|
14801
|
+
* `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
|
|
14802
|
+
* `motion` is the lightweight readout from on-camera motion (Reolink
|
|
14803
|
+
* `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
|
|
14804
|
+
*
|
|
14805
|
+
* Native-motion providers also fan out to `detection.camera-native`
|
|
14806
|
+
* with `source: 'onboard'` so cross-cutting system services
|
|
14807
|
+
* (alert-center, advanced-notifier) can subscribe once and receive
|
|
14808
|
+
* motion from every camera.
|
|
14809
|
+
*/
|
|
14810
|
+
var MotionStatusSchema = object({
|
|
13271
14811
|
detected: boolean(),
|
|
13272
14812
|
/** Ms epoch of the last detected-true observation. Null if never detected. */
|
|
13273
14813
|
lastDetectedAt: number().nullable(),
|
|
@@ -13278,32 +14818,137 @@ object({
|
|
|
13278
14818
|
*/
|
|
13279
14819
|
autoClearAfterMs: number().nullable()
|
|
13280
14820
|
});
|
|
13281
|
-
|
|
14821
|
+
/**
|
|
14822
|
+
* Payload of `motion.onMotionChanged` event + the corresponding bus
|
|
14823
|
+
* event `EventCategory.MotionOnMotionChanged`. Single source of truth
|
|
14824
|
+
* — both the cap event surface and the bus payload type alias to this
|
|
14825
|
+
* schema.
|
|
14826
|
+
*/
|
|
14827
|
+
var MotionOnMotionChangedDataSchema = object({
|
|
13282
14828
|
deviceId: number(),
|
|
13283
14829
|
detected: boolean(),
|
|
13284
14830
|
timestamp: number(),
|
|
13285
14831
|
source: MotionSourceEnum,
|
|
13286
14832
|
regions: array(MotionRegionSchema).readonly().optional()
|
|
13287
14833
|
});
|
|
13288
|
-
|
|
13289
|
-
|
|
14834
|
+
var motionCapability = {
|
|
14835
|
+
name: "motion",
|
|
14836
|
+
scope: "device",
|
|
14837
|
+
mode: "singleton",
|
|
14838
|
+
/**
|
|
14839
|
+
* Providers register per-device natives via `ctx.registerNativeCap`
|
|
14840
|
+
* (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
|
|
14841
|
+
* NO system singleton provider. Without this flag `resolveCapMount`
|
|
14842
|
+
* derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
|
|
14843
|
+
* resolved via `registry.getSingleton('motion')` (always null) and every
|
|
14844
|
+
* call 412'd "provider not available" while bindings listed a live
|
|
14845
|
+
* `motion` native (2026-08-02). The flag routes the router through
|
|
14846
|
+
* `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
|
|
14847
|
+
* `snapshot` and every other per-device native cap.
|
|
14848
|
+
*/
|
|
14849
|
+
deviceNative: true,
|
|
14850
|
+
deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
|
|
14851
|
+
methods: {
|
|
14852
|
+
/**
|
|
14853
|
+
* Pull the current motion state synchronously. Convenience shortcut
|
|
14854
|
+
* for consumers that don't need the full status object; equivalent
|
|
14855
|
+
* to `getStatus()?.detected ?? false`. Will likely be folded into
|
|
14856
|
+
* `getStatus` once the auto-injected status surface lands in every
|
|
14857
|
+
* consumer.
|
|
14858
|
+
*/
|
|
14859
|
+
isDetected: method(object({ deviceId: number() }), boolean()) },
|
|
14860
|
+
events: {
|
|
14861
|
+
/**
|
|
14862
|
+
* Fires every time the runner transitions a camera between
|
|
14863
|
+
* `watching` and `active` phases. `source` carries which motion
|
|
14864
|
+
* path drove the transition; `regions` is populated only for
|
|
14865
|
+
* `source: 'analyzer'` (frame-diff regions from the ML motion
|
|
14866
|
+
* detector) — onboard sources don't carry per-frame regions
|
|
14867
|
+
* here (camera-provided zones / AI metadata live in dedicated
|
|
14868
|
+
* channels: `detection.camera-native`, future zone capability).
|
|
14869
|
+
*
|
|
14870
|
+
* Consumers that want all motion pushes (even with `detected`
|
|
14871
|
+
* unchanged) should subscribe to the status subscription via
|
|
14872
|
+
* `device-manager.subscribeDeviceStatusAggregate` instead.
|
|
14873
|
+
*/
|
|
14874
|
+
onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
|
|
14875
|
+
status: {
|
|
14876
|
+
schema: MotionStatusSchema,
|
|
14877
|
+
kind: "push"
|
|
14878
|
+
},
|
|
14879
|
+
/**
|
|
14880
|
+
* Runtime-state slice — the last observed motion snapshot, mirrored
|
|
14881
|
+
* by the kernel and readable cross-process via
|
|
14882
|
+
* `device.state.motion.value`. Reads never invoke the provider, so
|
|
14883
|
+
* UIs and other addons can poll the cached state safely.
|
|
14884
|
+
*/
|
|
14885
|
+
runtimeState: MotionStatusSchema
|
|
14886
|
+
};
|
|
14887
|
+
/**
|
|
14888
|
+
* Motion-trigger toggle for accessory devices.
|
|
14889
|
+
*
|
|
14890
|
+
* "Motion trigger" means: when the parent camera detects motion, the
|
|
14891
|
+
* accessory activates automatically. The cap exposes a single boolean
|
|
14892
|
+
* — `enabled` — that drivers map to the vendor-specific firmware
|
|
14893
|
+
* action (Reolink: `setSirenOnMotion`, `setFloodlightOnMotion`; ONVIF
|
|
14894
|
+
* relay: schedule binding; …).
|
|
14895
|
+
*
|
|
14896
|
+
* The accessory still has its own `switch` cap for direct on/off; this
|
|
14897
|
+
* cap is independent. Toggling motion-trigger on does NOT necessarily
|
|
14898
|
+
* toggle the switch on — it just instructs the firmware to flip the
|
|
14899
|
+
* switch when motion fires.
|
|
14900
|
+
*
|
|
14901
|
+
* Driver-specific knobs (motion duration window, brightness while
|
|
14902
|
+
* triggered, schedule windows) live in the device's
|
|
14903
|
+
* `getSettingsUISchema()` instead of bloating this cap — same
|
|
14904
|
+
* principle as `switch` and `brightness` keeping their surface
|
|
14905
|
+
* minimal.
|
|
14906
|
+
*/
|
|
14907
|
+
var MotionTriggerStatusSchema = object({
|
|
13290
14908
|
enabled: boolean(),
|
|
13291
14909
|
/** Ms epoch of the last operator-driven change. */
|
|
13292
14910
|
lastChangedAt: number()
|
|
13293
|
-
})
|
|
14911
|
+
});
|
|
14912
|
+
/**
|
|
14913
|
+
* Persistent slice mirrored across restarts. The provider writes here
|
|
14914
|
+
* on every successful firmware fetch / setMotionTrigger push; the cap
|
|
14915
|
+
* router and admin-ui hero read straight from this snapshot via
|
|
14916
|
+
* `device.state.motionTrigger.value` instead of re-issuing a firmware
|
|
14917
|
+
* round-trip on every UI mount. `lastFetchedAt` lets the framework
|
|
14918
|
+
* helper (`createRuntimeStateBridge`) stale-check before deciding
|
|
14919
|
+
* whether to refresh from the camera.
|
|
14920
|
+
*/
|
|
14921
|
+
var MotionTriggerRuntimeStateSchema = MotionTriggerStatusSchema.extend({
|
|
13294
14922
|
/** Ms epoch of the last successful camera fetch (0 = never). */
|
|
13295
14923
|
lastFetchedAt: number() });
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
14924
|
+
var motionTriggerCapability = {
|
|
14925
|
+
name: "motion-trigger",
|
|
14926
|
+
scope: "device",
|
|
14927
|
+
deviceNative: true,
|
|
14928
|
+
mode: "singleton",
|
|
14929
|
+
deviceTypes: [
|
|
14930
|
+
DeviceType.Light,
|
|
14931
|
+
DeviceType.Siren,
|
|
14932
|
+
DeviceType.Switch
|
|
14933
|
+
],
|
|
14934
|
+
methods: { setMotionTrigger: method(object({
|
|
14935
|
+
deviceId: number().int().nonnegative(),
|
|
14936
|
+
enabled: boolean()
|
|
14937
|
+
}), _void(), {
|
|
14938
|
+
kind: "mutation",
|
|
14939
|
+
auth: "admin"
|
|
14940
|
+
}) },
|
|
14941
|
+
events: { onMotionTriggerChanged: { data: object({
|
|
14942
|
+
deviceId: number(),
|
|
14943
|
+
enabled: boolean(),
|
|
14944
|
+
lastChangedAt: number()
|
|
14945
|
+
}) } },
|
|
14946
|
+
status: {
|
|
14947
|
+
schema: MotionTriggerStatusSchema,
|
|
14948
|
+
kind: "command-driven"
|
|
14949
|
+
},
|
|
14950
|
+
runtimeState: MotionTriggerRuntimeStateSchema
|
|
14951
|
+
};
|
|
13307
14952
|
/**
|
|
13308
14953
|
* Motion-zones share the same MaskShape vocabulary as privacy-mask — the
|
|
13309
14954
|
* on-camera motion-detection mask is a single `grid` region (a row-major
|
|
@@ -13316,7 +14961,9 @@ var MotionZoneRegionSchema = object({
|
|
|
13316
14961
|
enabled: boolean(),
|
|
13317
14962
|
shape: MaskGridShapeSchema
|
|
13318
14963
|
});
|
|
13319
|
-
|
|
14964
|
+
/** Current on-camera motion-detection state — master enable + sensitivity +
|
|
14965
|
+
* the grid region(s). */
|
|
14966
|
+
var MotionZoneStatusSchema = object({
|
|
13320
14967
|
enabled: boolean(),
|
|
13321
14968
|
sensitivity: number(),
|
|
13322
14969
|
/** Grid region(s). Today exactly one `grid` shape. */
|
|
@@ -13341,13 +14988,34 @@ var MotionZonePatchSchema = object({
|
|
|
13341
14988
|
sensitivity: number().optional(),
|
|
13342
14989
|
regions: array(MotionZoneRegionSchema).optional()
|
|
13343
14990
|
});
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
14991
|
+
var motionZonesCapability = {
|
|
14992
|
+
name: "motion-zones",
|
|
14993
|
+
scope: "device",
|
|
14994
|
+
deviceNative: true,
|
|
14995
|
+
mode: "singleton",
|
|
14996
|
+
deviceTypes: [DeviceType.Camera],
|
|
14997
|
+
deviceConfig: { ui: {
|
|
14998
|
+
kind: "widget",
|
|
14999
|
+
widgetId: "host/motion-zones-grid",
|
|
15000
|
+
tab: "motion",
|
|
15001
|
+
label: "Motion Zones"
|
|
15002
|
+
} },
|
|
15003
|
+
methods: {
|
|
15004
|
+
getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
|
|
15005
|
+
setZone: method(object({
|
|
15006
|
+
deviceId: number(),
|
|
15007
|
+
patch: MotionZonePatchSchema
|
|
15008
|
+
}), _void(), {
|
|
15009
|
+
kind: "mutation",
|
|
15010
|
+
auth: "admin"
|
|
15011
|
+
})
|
|
15012
|
+
},
|
|
15013
|
+
status: {
|
|
15014
|
+
schema: MotionZoneStatusSchema,
|
|
15015
|
+
kind: "poll"
|
|
15016
|
+
},
|
|
15017
|
+
runtimeState: MotionZoneStatusSchema
|
|
15018
|
+
};
|
|
13351
15019
|
/**
|
|
13352
15020
|
* On-camera AI object detection cap. Surfaces per-device the classes
|
|
13353
15021
|
* the firmware can detect and the last-seen instance of each. The
|
|
@@ -13374,7 +15042,7 @@ var NativeDetectionSchema = object({
|
|
|
13374
15042
|
/** Firmware-provided confidence [0..1]. Reolink pushes don't carry it → undefined. */
|
|
13375
15043
|
confidence: number().min(0).max(1).optional()
|
|
13376
15044
|
});
|
|
13377
|
-
object({
|
|
15045
|
+
var NativeObjectDetectionStatusSchema = object({
|
|
13378
15046
|
/**
|
|
13379
15047
|
* Last observed instance per class. Missing entries mean the class
|
|
13380
15048
|
* is supported but nothing has been seen since the provider started.
|
|
@@ -13395,19 +15063,33 @@ object({
|
|
|
13395
15063
|
* churn the tracker, so forwarding stays off until the operator enables it.
|
|
13396
15064
|
*/
|
|
13397
15065
|
enabled: boolean()
|
|
13398
|
-
})
|
|
15066
|
+
});
|
|
15067
|
+
var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
|
|
13399
15068
|
/** Required by createRuntimeStateBridge — epoch ms of last refresh. */
|
|
13400
15069
|
lastFetchedAt: number() });
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
})
|
|
15070
|
+
var nativeObjectDetectionCapability = {
|
|
15071
|
+
name: "native-object-detection",
|
|
15072
|
+
scope: "device",
|
|
15073
|
+
deviceNative: true,
|
|
15074
|
+
mode: "singleton",
|
|
15075
|
+
deviceTypes: [DeviceType.Camera],
|
|
15076
|
+
methods: { setEnabled: method(object({
|
|
15077
|
+
deviceId: number(),
|
|
15078
|
+
enabled: boolean()
|
|
15079
|
+
}), _void(), {
|
|
15080
|
+
kind: "mutation",
|
|
15081
|
+
auth: "admin"
|
|
15082
|
+
}) },
|
|
15083
|
+
events: { onDetected: { data: object({
|
|
15084
|
+
deviceId: number(),
|
|
15085
|
+
detection: NativeDetectionSchema
|
|
15086
|
+
}) } },
|
|
15087
|
+
status: {
|
|
15088
|
+
schema: NativeObjectDetectionStatusSchema,
|
|
15089
|
+
kind: "push"
|
|
15090
|
+
},
|
|
15091
|
+
runtimeState: NativeObjectDetectionRuntimeStateSchema
|
|
15092
|
+
};
|
|
13411
15093
|
/**
|
|
13412
15094
|
* Notification delivery cap. Models HA `notify.<service>` plus future
|
|
13413
15095
|
* native targets (Telegram / Discord / ntfy / SMTP, …). Designed at
|
|
@@ -13459,7 +15141,7 @@ var NotifierSupportsSchema = object({
|
|
|
13459
15141
|
* Pair with `DeviceFeature.NotifierRecipients`. */
|
|
13460
15142
|
recipients: boolean()
|
|
13461
15143
|
});
|
|
13462
|
-
object({
|
|
15144
|
+
var NotifierStatusSchema = object({
|
|
13463
15145
|
/** Ms epoch of the most recent successful send. 0 if none yet. */
|
|
13464
15146
|
lastSentAt: number(),
|
|
13465
15147
|
/** Failure description from the most recent send attempt. Null on
|
|
@@ -13500,23 +15182,67 @@ var NotifierSendResultSchema = object({
|
|
|
13500
15182
|
/** Ms epoch when the notifier accepted the send (not when delivered). */
|
|
13501
15183
|
acceptedAt: number()
|
|
13502
15184
|
});
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
15185
|
+
var notifierCapability = {
|
|
15186
|
+
name: "notifier",
|
|
15187
|
+
scope: "device",
|
|
15188
|
+
deviceNative: true,
|
|
15189
|
+
mode: "singleton",
|
|
15190
|
+
deviceTypes: [DeviceType.Notifier],
|
|
15191
|
+
methods: {
|
|
15192
|
+
send: method(NotifierSendInputSchema, NotifierSendResultSchema, {
|
|
15193
|
+
kind: "mutation",
|
|
15194
|
+
auth: "admin"
|
|
15195
|
+
}),
|
|
15196
|
+
cancel: method(object({
|
|
15197
|
+
deviceId: number().int().nonnegative(),
|
|
15198
|
+
notificationId: string().min(1)
|
|
15199
|
+
}), _void(), {
|
|
15200
|
+
kind: "mutation",
|
|
15201
|
+
auth: "admin"
|
|
15202
|
+
})
|
|
15203
|
+
},
|
|
15204
|
+
events: {
|
|
15205
|
+
/**
|
|
15206
|
+
* Emitted after every send attempt — success or failure. Subscribers
|
|
15207
|
+
* (admin UI history pane, automation engines, retry workers) react
|
|
15208
|
+
* without polling the provider's `lastSentAt`.
|
|
15209
|
+
*/
|
|
15210
|
+
onSent: { data: object({
|
|
15211
|
+
deviceId: number(),
|
|
15212
|
+
notificationId: string(),
|
|
15213
|
+
success: boolean(),
|
|
15214
|
+
error: string().nullable(),
|
|
15215
|
+
acceptedAt: number()
|
|
15216
|
+
}) } },
|
|
15217
|
+
status: {
|
|
15218
|
+
schema: NotifierStatusSchema,
|
|
15219
|
+
kind: "command-driven"
|
|
15220
|
+
},
|
|
15221
|
+
/**
|
|
15222
|
+
* Runtime-state slice — diagnostics + supports matrix. UI compose
|
|
15223
|
+
* form reads `supports` to gate optional fields; history pane reads
|
|
15224
|
+
* `lastSentAt` / `lastError` / `queueDepth`.
|
|
15225
|
+
*/
|
|
15226
|
+
runtimeState: NotifierStatusSchema
|
|
15227
|
+
};
|
|
15228
|
+
/**
|
|
15229
|
+
* Generic numeric sensor — last-resort fallback when no typed numeric
|
|
15230
|
+
* cap fits (HA `sensor` whose `device_class` we don't have a typed cap
|
|
15231
|
+
* for: water flow, distance, weight, frequency, signal strength, …).
|
|
15232
|
+
*
|
|
15233
|
+
* The `unit` and `precision` fields carry the live display metadata that
|
|
15234
|
+
* the upstream source provides with each state push (HA
|
|
15235
|
+
* `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
|
|
15236
|
+
* The UI reads them directly from the slice — they are the single source
|
|
15237
|
+
* of truth for generic numeric rendering and do NOT live on `sourceInfo`.
|
|
15238
|
+
*
|
|
15239
|
+
* Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
|
|
15240
|
+
* `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
|
|
15241
|
+
* `air-quality-sensor`, `battery`) when the metric matches — export
|
|
15242
|
+
* adapters render those with the right service / category. Generic
|
|
15243
|
+
* numeric values land here.
|
|
15244
|
+
*/
|
|
15245
|
+
var NumericSensorStatusSchema = object({
|
|
13520
15246
|
value: number(),
|
|
13521
15247
|
/** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
|
|
13522
15248
|
* Populated live from the upstream source on each state push.
|
|
@@ -13529,7 +15255,19 @@ object({
|
|
|
13529
15255
|
/** Ms epoch when the slice was last updated. */
|
|
13530
15256
|
lastFetchedAt: number()
|
|
13531
15257
|
});
|
|
13532
|
-
|
|
15258
|
+
var numericSensorCapability = {
|
|
15259
|
+
name: "numeric-sensor",
|
|
15260
|
+
scope: "device",
|
|
15261
|
+
deviceNative: true,
|
|
15262
|
+
mode: "singleton",
|
|
15263
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15264
|
+
methods: {},
|
|
15265
|
+
status: {
|
|
15266
|
+
schema: NumericSensorStatusSchema,
|
|
15267
|
+
kind: "push"
|
|
15268
|
+
},
|
|
15269
|
+
runtimeState: NumericSensorStatusSchema
|
|
15270
|
+
};
|
|
13533
15271
|
/**
|
|
13534
15272
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
13535
15273
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
@@ -13541,7 +15279,7 @@ var PetFeederDeviceStatusSchema = _enum([
|
|
|
13541
15279
|
"on_batteries"
|
|
13542
15280
|
]);
|
|
13543
15281
|
var gramsPortion = number().int().min(4).max(200);
|
|
13544
|
-
object({
|
|
15282
|
+
var PetFeederStatusSchema = object({
|
|
13545
15283
|
/** Food currently in the bowl (grams). Null when the device has not
|
|
13546
15284
|
* reported a reading yet. On dual-hopper models this is the combined
|
|
13547
15285
|
* bowl reading; per-hopper levels live in `food1`/`food2`. */
|
|
@@ -13586,58 +15324,117 @@ object({
|
|
|
13586
15324
|
/** Ms epoch when the slice was last refreshed from the cloud. */
|
|
13587
15325
|
lastFetchedAt: number()
|
|
13588
15326
|
});
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
}), _void(), {
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
}),
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
}), _void(), {
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
})
|
|
13640
|
-
|
|
15327
|
+
var petFeederCapability = {
|
|
15328
|
+
name: "pet-feeder",
|
|
15329
|
+
scope: "device",
|
|
15330
|
+
deviceNative: true,
|
|
15331
|
+
mode: "singleton",
|
|
15332
|
+
deviceTypes: [DeviceType.PetFeeder],
|
|
15333
|
+
methods: {
|
|
15334
|
+
/**
|
|
15335
|
+
* Dispense food now. Single-hopper feeders take `grams`; dual-hopper
|
|
15336
|
+
* feeders (D4S/D4SH) accept `hopper1`/`hopper2` to target one or both
|
|
15337
|
+
* hoppers. All portions honour the 4–200 g hardware range. At least
|
|
15338
|
+
* one of the three must be present — the provider rejects an empty
|
|
15339
|
+
* request.
|
|
15340
|
+
*/
|
|
15341
|
+
feed: method(object({
|
|
15342
|
+
deviceId: number().int().nonnegative(),
|
|
15343
|
+
grams: gramsPortion.optional(),
|
|
15344
|
+
hopper1: gramsPortion.optional(),
|
|
15345
|
+
hopper2: gramsPortion.optional()
|
|
15346
|
+
}), _void(), {
|
|
15347
|
+
kind: "mutation",
|
|
15348
|
+
auth: "admin"
|
|
15349
|
+
}),
|
|
15350
|
+
/** Cancel an in-progress manual feed. */
|
|
15351
|
+
cancelFeed: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15352
|
+
kind: "mutation",
|
|
15353
|
+
auth: "admin"
|
|
15354
|
+
}),
|
|
15355
|
+
/** Reset the desiccant "days remaining" counter after replacing it. */
|
|
15356
|
+
resetDesiccant: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15357
|
+
kind: "mutation",
|
|
15358
|
+
auth: "admin"
|
|
15359
|
+
}),
|
|
15360
|
+
/** Mark a hopper as refilled (D4H/D4S/D4SH). */
|
|
15361
|
+
markFoodReplenished: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15362
|
+
kind: "mutation",
|
|
15363
|
+
auth: "admin"
|
|
15364
|
+
}),
|
|
15365
|
+
/** Call the pet with the recorded prompt (D3). */
|
|
15366
|
+
callPet: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15367
|
+
kind: "mutation",
|
|
15368
|
+
auth: "admin"
|
|
15369
|
+
}),
|
|
15370
|
+
/** Play a stored sound by id (D3 / D4H / D4SH). */
|
|
15371
|
+
playSound: method(object({
|
|
15372
|
+
deviceId: number().int().nonnegative(),
|
|
15373
|
+
soundId: number().int().nonnegative()
|
|
15374
|
+
}), _void(), {
|
|
15375
|
+
kind: "mutation",
|
|
15376
|
+
auth: "admin"
|
|
15377
|
+
}),
|
|
15378
|
+
/** Toggle the child-lock (manual-lock) setting. */
|
|
15379
|
+
setChildLock: method(object({
|
|
15380
|
+
deviceId: number().int().nonnegative(),
|
|
15381
|
+
on: boolean()
|
|
15382
|
+
}), _void(), {
|
|
15383
|
+
kind: "mutation",
|
|
15384
|
+
auth: "admin"
|
|
15385
|
+
}),
|
|
15386
|
+
/** Toggle the front indicator light. */
|
|
15387
|
+
setIndicatorLight: method(object({
|
|
15388
|
+
deviceId: number().int().nonnegative(),
|
|
15389
|
+
on: boolean()
|
|
15390
|
+
}), _void(), {
|
|
15391
|
+
kind: "mutation",
|
|
15392
|
+
auth: "admin"
|
|
15393
|
+
}),
|
|
15394
|
+
/** Toggle the dispense chime. */
|
|
15395
|
+
setFeedSound: method(object({
|
|
15396
|
+
deviceId: number().int().nonnegative(),
|
|
15397
|
+
on: boolean()
|
|
15398
|
+
}), _void(), {
|
|
15399
|
+
kind: "mutation",
|
|
15400
|
+
auth: "admin"
|
|
15401
|
+
}),
|
|
15402
|
+
/** Set the speaker / prompt volume level. */
|
|
15403
|
+
setVolume: method(object({
|
|
15404
|
+
deviceId: number().int().nonnegative(),
|
|
15405
|
+
level: number().int().nonnegative()
|
|
15406
|
+
}), _void(), {
|
|
15407
|
+
kind: "mutation",
|
|
15408
|
+
auth: "admin"
|
|
15409
|
+
})
|
|
15410
|
+
},
|
|
15411
|
+
status: {
|
|
15412
|
+
schema: PetFeederStatusSchema,
|
|
15413
|
+
kind: "poll"
|
|
15414
|
+
},
|
|
15415
|
+
/**
|
|
15416
|
+
* Runtime-state slice — mirrored by the kernel. UI feeder cards read
|
|
15417
|
+
* the full slice via `device.state.petFeeder.value` and refresh on
|
|
15418
|
+
* every poll without re-querying the provider.
|
|
15419
|
+
*/
|
|
15420
|
+
runtimeState: PetFeederStatusSchema
|
|
15421
|
+
};
|
|
15422
|
+
/**
|
|
15423
|
+
* Multi-metric electrical meter. One slice can carry any combination
|
|
15424
|
+
* of instantaneous power (W), cumulative energy (kWh), voltage (V),
|
|
15425
|
+
* and current (A) — all fields optional so a single-metric source
|
|
15426
|
+
* (HA `sensor` with `device_class: power`) populates only what it
|
|
15427
|
+
* has and aggregators (energy dashboards, billing exports) compose
|
|
15428
|
+
* across providers without needing one cap per metric.
|
|
15429
|
+
*
|
|
15430
|
+
* Trade-off acknowledged: a HomeKit export that wants to surface
|
|
15431
|
+
* "power" and "energy" as separate services has to crack open the
|
|
15432
|
+
* slice and emit two characteristics from one cap. Worth it to avoid
|
|
15433
|
+
* exploding the cap catalog with `power-w` / `energy-kwh` /
|
|
15434
|
+
* `voltage-v` / `current-a` quartets that always travel together
|
|
15435
|
+
* in real-world devices.
|
|
15436
|
+
*/
|
|
15437
|
+
var PowerMeterStatusSchema = object({
|
|
13641
15438
|
/** Instantaneous power draw in watts. */
|
|
13642
15439
|
watts: number().optional(),
|
|
13643
15440
|
/** Cumulative energy in kilowatt-hours since the meter was reset. */
|
|
@@ -13664,7 +15461,19 @@ object({
|
|
|
13664
15461
|
* auto-formatting when absent. */
|
|
13665
15462
|
precision: number().int().min(0).max(10).optional()
|
|
13666
15463
|
});
|
|
13667
|
-
|
|
15464
|
+
var powerMeterCapability = {
|
|
15465
|
+
name: "power-meter",
|
|
15466
|
+
scope: "device",
|
|
15467
|
+
deviceNative: true,
|
|
15468
|
+
mode: "singleton",
|
|
15469
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15470
|
+
methods: {},
|
|
15471
|
+
status: {
|
|
15472
|
+
schema: PowerMeterStatusSchema,
|
|
15473
|
+
kind: "push"
|
|
15474
|
+
},
|
|
15475
|
+
runtimeState: PowerMeterStatusSchema
|
|
15476
|
+
};
|
|
13668
15477
|
/**
|
|
13669
15478
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
13670
15479
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -13689,7 +15498,7 @@ var GpsLocationSchema = object({
|
|
|
13689
15498
|
/** Reported accuracy in meters (lower = better). */
|
|
13690
15499
|
accuracyMeters: number().nonnegative()
|
|
13691
15500
|
});
|
|
13692
|
-
object({
|
|
15501
|
+
var PresenceStatusSchema = object({
|
|
13693
15502
|
/** `home` / `not_home` / any user-defined zone name. */
|
|
13694
15503
|
state: string(),
|
|
13695
15504
|
/** Optional textual location label (zone name, city, address). Null
|
|
@@ -13703,8 +15512,34 @@ object({
|
|
|
13703
15512
|
/** Ms epoch when the slice was last updated. */
|
|
13704
15513
|
lastChangedAt: number()
|
|
13705
15514
|
});
|
|
13706
|
-
|
|
13707
|
-
|
|
15515
|
+
var presenceCapability = {
|
|
15516
|
+
name: "presence",
|
|
15517
|
+
scope: "device",
|
|
15518
|
+
deviceNative: true,
|
|
15519
|
+
mode: "singleton",
|
|
15520
|
+
deviceTypes: [DeviceType.Presence],
|
|
15521
|
+
methods: {},
|
|
15522
|
+
status: {
|
|
15523
|
+
schema: PresenceStatusSchema,
|
|
15524
|
+
kind: "push"
|
|
15525
|
+
},
|
|
15526
|
+
/**
|
|
15527
|
+
* Runtime-state slice — mirrored by the kernel. UI presence card
|
|
15528
|
+
* reads `state` + `location` for the text label; iff `gps !== null`
|
|
15529
|
+
* the map pin is rendered (use `DeviceFeature.PresenceGps` for the
|
|
15530
|
+
* pre-fetch fast-path check).
|
|
15531
|
+
*/
|
|
15532
|
+
runtimeState: PresenceStatusSchema
|
|
15533
|
+
};
|
|
15534
|
+
/**
|
|
15535
|
+
* Atmospheric pressure reading in hectopascals. Drives Home Assistant
|
|
15536
|
+
* `sensor` entries with `device_class: pressure`.
|
|
15537
|
+
*
|
|
15538
|
+
* Unit normalisation: hPa. The canonical display unit (`hPa`) is a
|
|
15539
|
+
* descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
15540
|
+
* `sourceInfo`.
|
|
15541
|
+
*/
|
|
15542
|
+
var PressureSensorStatusSchema = object({
|
|
13708
15543
|
/** Current pressure in hPa. */
|
|
13709
15544
|
hpa: number(),
|
|
13710
15545
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -13719,7 +15554,19 @@ object({
|
|
|
13719
15554
|
* auto-formatting when absent. */
|
|
13720
15555
|
precision: number().int().min(0).max(10).optional()
|
|
13721
15556
|
});
|
|
13722
|
-
|
|
15557
|
+
var pressureSensorCapability = {
|
|
15558
|
+
name: "pressure-sensor",
|
|
15559
|
+
scope: "device",
|
|
15560
|
+
deviceNative: true,
|
|
15561
|
+
mode: "singleton",
|
|
15562
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15563
|
+
methods: {},
|
|
15564
|
+
status: {
|
|
15565
|
+
schema: PressureSensorStatusSchema,
|
|
15566
|
+
kind: "push"
|
|
15567
|
+
},
|
|
15568
|
+
runtimeState: PressureSensorStatusSchema
|
|
15569
|
+
};
|
|
13723
15570
|
/**
|
|
13724
15571
|
* Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
|
|
13725
15572
|
* cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
|
|
@@ -13738,7 +15585,8 @@ var PrivacyMaskRegionSchema = object({
|
|
|
13738
15585
|
enabled: boolean(),
|
|
13739
15586
|
shape: PrivacyMaskShapeSchema
|
|
13740
15587
|
});
|
|
13741
|
-
|
|
15588
|
+
/** Current on-camera privacy-mask state — master enable + zones. */
|
|
15589
|
+
var PrivacyMaskStatusSchema = object({
|
|
13742
15590
|
enabled: boolean(),
|
|
13743
15591
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
13744
15592
|
regions: array(PrivacyMaskRegionSchema),
|
|
@@ -13758,13 +15606,34 @@ var PrivacyMaskPatchSchema = object({
|
|
|
13758
15606
|
enabled: boolean().optional(),
|
|
13759
15607
|
regions: array(PrivacyMaskRegionSchema).optional()
|
|
13760
15608
|
});
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
15609
|
+
var privacyMaskCapability = {
|
|
15610
|
+
name: "privacy-mask",
|
|
15611
|
+
scope: "device",
|
|
15612
|
+
deviceNative: true,
|
|
15613
|
+
mode: "singleton",
|
|
15614
|
+
deviceTypes: [DeviceType.Camera],
|
|
15615
|
+
deviceConfig: { ui: {
|
|
15616
|
+
kind: "widget",
|
|
15617
|
+
widgetId: "host/privacy-mask-grid",
|
|
15618
|
+
tab: "image",
|
|
15619
|
+
label: "Privacy Mask"
|
|
15620
|
+
} },
|
|
15621
|
+
methods: {
|
|
15622
|
+
getOptions: method(object({ deviceId: number() }), PrivacyMaskOptionsSchema),
|
|
15623
|
+
setMask: method(object({
|
|
15624
|
+
deviceId: number(),
|
|
15625
|
+
patch: PrivacyMaskPatchSchema
|
|
15626
|
+
}), _void(), {
|
|
15627
|
+
kind: "mutation",
|
|
15628
|
+
auth: "admin"
|
|
15629
|
+
})
|
|
15630
|
+
},
|
|
15631
|
+
status: {
|
|
15632
|
+
schema: PrivacyMaskStatusSchema,
|
|
15633
|
+
kind: "poll"
|
|
15634
|
+
},
|
|
15635
|
+
runtimeState: PrivacyMaskStatusSchema
|
|
15636
|
+
};
|
|
13768
15637
|
var PtzAutotrackSettingsSchema = object({
|
|
13769
15638
|
targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
|
|
13770
15639
|
stopDelaySeconds: number().int().min(0).max(300),
|
|
@@ -13804,19 +15673,85 @@ var PtzAutotrackStatusSchema = object({
|
|
|
13804
15673
|
*/
|
|
13805
15674
|
supportedTargetTypes: array(PtzAutotrackTargetOptionSchema)
|
|
13806
15675
|
});
|
|
13807
|
-
|
|
15676
|
+
/**
|
|
15677
|
+
* Runtime-state slice owned by this cap. Persists the last-known
|
|
15678
|
+
* camera-derived snapshot across restarts, so cold-start callers
|
|
15679
|
+
* have a meaningful baseline even before the first refresh round-trip.
|
|
15680
|
+
*
|
|
15681
|
+
* Adds `lastFetchedAt` on top of the status shape so the framework
|
|
15682
|
+
* (or the provider) can stale-check before deciding whether to
|
|
15683
|
+
* round-trip the camera again.
|
|
15684
|
+
*/
|
|
15685
|
+
var PtzAutotrackRuntimeStateSchema = PtzAutotrackStatusSchema.extend({
|
|
13808
15686
|
/** Ms epoch of the last successful camera fetch (0 = never). */
|
|
13809
15687
|
lastFetchedAt: number() });
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
15688
|
+
var ptzAutotrackCapability = {
|
|
15689
|
+
name: "ptz-autotrack",
|
|
15690
|
+
scope: "device",
|
|
15691
|
+
deviceNative: true,
|
|
15692
|
+
mode: "singleton",
|
|
15693
|
+
deviceTypes: [DeviceType.Camera],
|
|
15694
|
+
deviceConfig: { ui: {
|
|
15695
|
+
kind: "widget",
|
|
15696
|
+
widgetId: "host/ptz-autotrack",
|
|
15697
|
+
tab: "ptz",
|
|
15698
|
+
topTab: true,
|
|
15699
|
+
label: "Auto-Tracking",
|
|
15700
|
+
order: 5
|
|
15701
|
+
} },
|
|
15702
|
+
methods: {
|
|
15703
|
+
/**
|
|
15704
|
+
* Read the current on/off state + last-applied settings.
|
|
15705
|
+
* Drivers may serve from a cache + refresh on a schedule —
|
|
15706
|
+
* callers should NOT poll faster than ~5s.
|
|
15707
|
+
*/
|
|
15708
|
+
getStatus: method(object({ deviceId: number() }), PtzAutotrackStatusSchema),
|
|
15709
|
+
/**
|
|
15710
|
+
* Arm or disarm autotrack. Resolves once the camera has
|
|
15711
|
+
* acknowledged the transition; the `onStatusChanged` event
|
|
15712
|
+
* fires shortly after with the live state.
|
|
15713
|
+
*/
|
|
15714
|
+
setEnabled: method(object({
|
|
15715
|
+
deviceId: number(),
|
|
15716
|
+
enabled: boolean()
|
|
15717
|
+
}), _void(), { kind: "mutation" }),
|
|
15718
|
+
/**
|
|
15719
|
+
* Read settings (target type + delays). Mirrors the
|
|
15720
|
+
* `getStatus().currentSettings` payload but exposes it as a
|
|
15721
|
+
* standalone read for callers that want settings without the
|
|
15722
|
+
* status surface around them. `null` until the driver has
|
|
15723
|
+
* harvested the firmware state at least once.
|
|
15724
|
+
*/
|
|
15725
|
+
getSettings: method(object({ deviceId: number() }), PtzAutotrackSettingsSchema.nullable()),
|
|
15726
|
+
/**
|
|
15727
|
+
* Update one or more settings. Partial — keys that aren't
|
|
15728
|
+
* supplied keep their last persisted value. The driver is
|
|
15729
|
+
* responsible for clamping vendor-specific ranges if narrower
|
|
15730
|
+
* than the cross-vendor max declared on the schema.
|
|
15731
|
+
*/
|
|
15732
|
+
setSettings: method(object({
|
|
15733
|
+
deviceId: number(),
|
|
15734
|
+
settings: PtzAutotrackSettingsSchema.partial()
|
|
15735
|
+
}), _void(), { kind: "mutation" })
|
|
15736
|
+
},
|
|
15737
|
+
events: { onStatusChanged: { data: object({
|
|
15738
|
+
deviceId: number(),
|
|
15739
|
+
status: PtzAutotrackStatusSchema
|
|
15740
|
+
}) } },
|
|
15741
|
+
status: {
|
|
15742
|
+
schema: PtzAutotrackStatusSchema,
|
|
15743
|
+
kind: "command-driven"
|
|
15744
|
+
},
|
|
15745
|
+
/**
|
|
15746
|
+
* Persistent slice mirrored across restarts. The provider writes
|
|
15747
|
+
* here on every successful firmware fetch / settings push;
|
|
15748
|
+
* `getStatus` and `getSettings` then read this snapshot synchronously
|
|
15749
|
+
* with a stale-driven async refresh in the background. Keeps the
|
|
15750
|
+
* fetch / cache / fallback logic out of the four cap methods —
|
|
15751
|
+
* they become trampolines over `runtimeState`.
|
|
15752
|
+
*/
|
|
15753
|
+
runtimeState: PtzAutotrackRuntimeStateSchema
|
|
15754
|
+
};
|
|
13820
15755
|
/**
|
|
13821
15756
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
13822
15757
|
* a rect ROI on a camera frame and names one or more states; the engine
|
|
@@ -13886,72 +15821,108 @@ var SceneMonitorStatusSchema = object({
|
|
|
13886
15821
|
monitors: array(SceneMonitorSchema),
|
|
13887
15822
|
lastFetchedAt: number()
|
|
13888
15823
|
});
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
"
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
}),
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
})
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
}),
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
|
|
15824
|
+
var sceneMonitorCapability = {
|
|
15825
|
+
name: "scene-monitor",
|
|
15826
|
+
scope: "device",
|
|
15827
|
+
mode: "singleton",
|
|
15828
|
+
kind: "wrapper",
|
|
15829
|
+
defaultActive: true,
|
|
15830
|
+
deviceTypes: [DeviceType.Camera],
|
|
15831
|
+
deviceConfig: { ui: {
|
|
15832
|
+
kind: "widget",
|
|
15833
|
+
widgetId: "host/scene-monitor-editor",
|
|
15834
|
+
tab: "scenes",
|
|
15835
|
+
label: "Scenes"
|
|
15836
|
+
} },
|
|
15837
|
+
methods: {
|
|
15838
|
+
listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
|
|
15839
|
+
createScene: method(object({
|
|
15840
|
+
deviceId: number(),
|
|
15841
|
+
label: string(),
|
|
15842
|
+
roi: MaskRectShapeSchema,
|
|
15843
|
+
check: SceneCheckSchema,
|
|
15844
|
+
triggerMode: _enum([
|
|
15845
|
+
"periodic",
|
|
15846
|
+
"on-motion",
|
|
15847
|
+
"both"
|
|
15848
|
+
]).optional(),
|
|
15849
|
+
checkIntervalSec: number().optional()
|
|
15850
|
+
}), SceneMonitorSchema, {
|
|
15851
|
+
kind: "mutation",
|
|
15852
|
+
auth: "admin"
|
|
15853
|
+
}),
|
|
15854
|
+
updateScene: method(object({
|
|
15855
|
+
deviceId: number(),
|
|
15856
|
+
monitorId: string(),
|
|
15857
|
+
patch: object({
|
|
15858
|
+
label: string().optional(),
|
|
15859
|
+
roi: MaskRectShapeSchema.optional(),
|
|
15860
|
+
enabled: boolean().optional(),
|
|
15861
|
+
triggerMode: _enum([
|
|
15862
|
+
"periodic",
|
|
15863
|
+
"on-motion",
|
|
15864
|
+
"both"
|
|
15865
|
+
]).optional(),
|
|
15866
|
+
checkIntervalSec: number().optional(),
|
|
15867
|
+
check: SceneCheckSchema.optional()
|
|
15868
|
+
})
|
|
15869
|
+
}), _void(), {
|
|
15870
|
+
kind: "mutation",
|
|
15871
|
+
auth: "admin"
|
|
15872
|
+
}),
|
|
15873
|
+
deleteScene: method(object({
|
|
15874
|
+
deviceId: number(),
|
|
15875
|
+
monitorId: string()
|
|
15876
|
+
}), _void(), {
|
|
15877
|
+
kind: "mutation",
|
|
15878
|
+
auth: "admin"
|
|
15879
|
+
}),
|
|
15880
|
+
captureReference: method(object({
|
|
15881
|
+
deviceId: number(),
|
|
15882
|
+
monitorId: string(),
|
|
15883
|
+
stateId: string().optional(),
|
|
15884
|
+
label: string().optional(),
|
|
15885
|
+
condition: SceneConditionSchema.optional()
|
|
15886
|
+
}), object({
|
|
15887
|
+
stateId: string(),
|
|
15888
|
+
referenceCount: number()
|
|
15889
|
+
}), {
|
|
15890
|
+
kind: "mutation",
|
|
15891
|
+
auth: "admin"
|
|
15892
|
+
}),
|
|
15893
|
+
deleteReference: method(object({
|
|
15894
|
+
deviceId: number(),
|
|
15895
|
+
monitorId: string(),
|
|
15896
|
+
stateId: string(),
|
|
15897
|
+
index: number()
|
|
15898
|
+
}), _void(), {
|
|
15899
|
+
kind: "mutation",
|
|
15900
|
+
auth: "admin"
|
|
15901
|
+
}),
|
|
15902
|
+
recheckNow: method(object({
|
|
15903
|
+
deviceId: number(),
|
|
15904
|
+
monitorId: string()
|
|
15905
|
+
}), _void(), {
|
|
15906
|
+
kind: "mutation",
|
|
15907
|
+
auth: "admin"
|
|
15908
|
+
})
|
|
15909
|
+
},
|
|
15910
|
+
status: {
|
|
15911
|
+
schema: SceneMonitorStatusSchema,
|
|
15912
|
+
kind: "push"
|
|
15913
|
+
},
|
|
15914
|
+
runtimeState: SceneMonitorStatusSchema
|
|
15915
|
+
};
|
|
15916
|
+
/**
|
|
15917
|
+
* Script-runner cap. Models HA `script.*` entities on
|
|
15918
|
+
* `DeviceType.Script`. A Script is a pre-recorded action sequence
|
|
15919
|
+
* that can be invoked imperatively — optionally with a variables
|
|
15920
|
+
* map when the script declares input fields.
|
|
15921
|
+
*
|
|
15922
|
+
* Variable-support is signalled by `DeviceFeature.ScriptVariables`
|
|
15923
|
+
* so the UI gates a "with parameters" form without a slice fetch.
|
|
15924
|
+
*/
|
|
15925
|
+
var ScriptRunnerStatusSchema = object({
|
|
13955
15926
|
/** Whether the script is currently executing. */
|
|
13956
15927
|
isRunning: boolean(),
|
|
13957
15928
|
/** Ms epoch of the last invocation start. 0 when never run. */
|
|
@@ -13965,25 +15936,68 @@ object({
|
|
|
13965
15936
|
/** Ms epoch when the slice was last updated. */
|
|
13966
15937
|
lastChangedAt: number()
|
|
13967
15938
|
});
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
15939
|
+
var scriptRunnerCapability = {
|
|
15940
|
+
name: "script-runner",
|
|
15941
|
+
scope: "device",
|
|
15942
|
+
deviceNative: true,
|
|
15943
|
+
mode: "singleton",
|
|
15944
|
+
deviceTypes: [DeviceType.Script],
|
|
15945
|
+
methods: {
|
|
15946
|
+
run: method(object({
|
|
15947
|
+
deviceId: number().int().nonnegative(),
|
|
15948
|
+
/** Optional variables map — passed through to the upstream
|
|
15949
|
+
* script. Provider rejects when the script doesn't declare
|
|
15950
|
+
* input fields (gated by `DeviceFeature.ScriptVariables`). */
|
|
15951
|
+
variables: record(string(), unknown()).optional()
|
|
15952
|
+
}), _void(), {
|
|
15953
|
+
kind: "mutation",
|
|
15954
|
+
auth: "admin"
|
|
15955
|
+
}),
|
|
15956
|
+
/** Cancel a running script. Provider rejects when the script
|
|
15957
|
+
* isn't currently running. */
|
|
15958
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15959
|
+
kind: "mutation",
|
|
15960
|
+
auth: "admin"
|
|
15961
|
+
})
|
|
15962
|
+
},
|
|
15963
|
+
status: {
|
|
15964
|
+
schema: ScriptRunnerStatusSchema,
|
|
15965
|
+
kind: "push"
|
|
15966
|
+
},
|
|
15967
|
+
/**
|
|
15968
|
+
* Runtime-state slice — mirrored by the kernel. UI script tile reads
|
|
15969
|
+
* `isRunning` to render a spinner during execution and surfaces
|
|
15970
|
+
* `lastError` / `lastRunSuccess` in the recent-runs panel.
|
|
15971
|
+
*/
|
|
15972
|
+
runtimeState: ScriptRunnerStatusSchema
|
|
15973
|
+
};
|
|
15974
|
+
/**
|
|
15975
|
+
* Smoke alarm sensor — boolean "is smoke currently detected" with
|
|
15976
|
+
* timestamp of the last transition. Drives Home Assistant
|
|
15977
|
+
* `binary_sensor` entries with `device_class: smoke`.
|
|
15978
|
+
*
|
|
15979
|
+
* Push-driven: a smoke event is critical, so the slice updates
|
|
15980
|
+
* immediately on the upstream signal. Auto-clearing back to false is
|
|
15981
|
+
* provider-controlled (some alarms latch until manually reset).
|
|
15982
|
+
*/
|
|
15983
|
+
var SmokeStatusSchema = object({
|
|
13982
15984
|
detected: boolean(),
|
|
13983
15985
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
13984
15986
|
lastChangedAt: number()
|
|
13985
15987
|
});
|
|
13986
|
-
|
|
15988
|
+
var smokeCapability = {
|
|
15989
|
+
name: "smoke",
|
|
15990
|
+
scope: "device",
|
|
15991
|
+
deviceNative: true,
|
|
15992
|
+
mode: "singleton",
|
|
15993
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15994
|
+
methods: {},
|
|
15995
|
+
status: {
|
|
15996
|
+
schema: SmokeStatusSchema,
|
|
15997
|
+
kind: "push"
|
|
15998
|
+
},
|
|
15999
|
+
runtimeState: SmokeStatusSchema
|
|
16000
|
+
};
|
|
13987
16001
|
/** One of the camera's stream profiles. */
|
|
13988
16002
|
var StreamProfileSchema = _enum([
|
|
13989
16003
|
"main",
|
|
@@ -14006,7 +16020,7 @@ var StreamProfileConfigSchema = object({
|
|
|
14006
16020
|
gop: number().optional(),
|
|
14007
16021
|
audio: boolean().optional()
|
|
14008
16022
|
});
|
|
14009
|
-
object({
|
|
16023
|
+
var StreamParamsStatusSchema = object({
|
|
14010
16024
|
/** Per-profile current config. A profile absent = the camera doesn't have it. */
|
|
14011
16025
|
main: StreamProfileConfigSchema.optional(),
|
|
14012
16026
|
sub: StreamProfileConfigSchema.optional(),
|
|
@@ -14060,39 +16074,150 @@ var StreamProfilePatchSchema = object({
|
|
|
14060
16074
|
gop: number().optional(),
|
|
14061
16075
|
audio: boolean().optional()
|
|
14062
16076
|
});
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
16077
|
+
var streamParamsCapability = {
|
|
16078
|
+
name: "stream-params",
|
|
16079
|
+
scope: "device",
|
|
16080
|
+
deviceNative: true,
|
|
16081
|
+
mode: "singleton",
|
|
16082
|
+
deviceTypes: [DeviceType.Camera],
|
|
16083
|
+
deviceConfig: { ui: {
|
|
16084
|
+
kind: "derived-form",
|
|
16085
|
+
builderId: "stream-params",
|
|
16086
|
+
tab: "streaming"
|
|
16087
|
+
} },
|
|
16088
|
+
methods: {
|
|
16089
|
+
getOptions: method(object({ deviceId: number() }), StreamParamsOptionsSchema),
|
|
16090
|
+
setProfile: method(object({
|
|
16091
|
+
deviceId: number(),
|
|
16092
|
+
profile: StreamProfileSchema,
|
|
16093
|
+
patch: StreamProfilePatchSchema
|
|
16094
|
+
}), _void(), {
|
|
16095
|
+
kind: "mutation",
|
|
16096
|
+
auth: "admin"
|
|
16097
|
+
}),
|
|
16098
|
+
/**
|
|
16099
|
+
* Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
|
|
16100
|
+
* shape) for this camera's stream-encoder settings — one section per
|
|
16101
|
+
* profile (main / sub / ext) with the resolution / codec / framerate
|
|
16102
|
+
* / bitrate / bitrate-mode / encoder-profile / GOP controls the
|
|
16103
|
+
* firmware actually exposes.
|
|
16104
|
+
*
|
|
16105
|
+
* Driven by `getOptions` (camera-probed availability) + `getStatus`
|
|
16106
|
+
* (current per-profile config); each field's `default` is seeded
|
|
16107
|
+
* from the live config so the form renders the camera state in one
|
|
16108
|
+
* pass. Returns `null` when the camera exposes no configurable
|
|
16109
|
+
* stream property — the renderer then shows the unsupported message.
|
|
16110
|
+
*
|
|
16111
|
+
* Output is `z.unknown().nullable()` — the same convention every
|
|
16112
|
+
* other `ConfigUISchema`-returning cap method uses (`device-ops`,
|
|
16113
|
+
* `device-manager`); `ConfigUISchema` is a TS-only type with no
|
|
16114
|
+
* companion Zod schema, and a concrete object would collapse
|
|
16115
|
+
* unrelated AppRouter branches to `unknown` during codegen.
|
|
16116
|
+
*/
|
|
16117
|
+
getConfigSchema: method(object({ deviceId: number() }), unknown().nullable())
|
|
16118
|
+
},
|
|
16119
|
+
status: {
|
|
16120
|
+
schema: StreamParamsStatusSchema,
|
|
16121
|
+
kind: "poll"
|
|
16122
|
+
},
|
|
16123
|
+
runtimeState: StreamParamsStatusSchema
|
|
16124
|
+
};
|
|
16125
|
+
/**
|
|
16126
|
+
* Generic on/off switch cap for accessory children (siren, floodlight,
|
|
16127
|
+
* spotlight, PIR toggle, chime silencer, autotrack enable, …). The cap
|
|
16128
|
+
* owns ONLY the boolean state — driver-specific settings (brightness,
|
|
16129
|
+
* sensitivity, schedule, duration) live on the device's own
|
|
16130
|
+
* `getSettingsUISchema()` surface.
|
|
16131
|
+
*/
|
|
16132
|
+
var SwitchStatusSchema = object({
|
|
14072
16133
|
on: boolean(),
|
|
14073
16134
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
14074
16135
|
lastChangedAt: number()
|
|
14075
16136
|
});
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
16137
|
+
var switchCapability = {
|
|
16138
|
+
name: "switch",
|
|
16139
|
+
scope: "device",
|
|
16140
|
+
deviceNative: true,
|
|
16141
|
+
mode: "singleton",
|
|
16142
|
+
deviceTypes: [
|
|
16143
|
+
DeviceType.Switch,
|
|
16144
|
+
DeviceType.Siren,
|
|
16145
|
+
DeviceType.Light
|
|
16146
|
+
],
|
|
16147
|
+
methods: { setState: method(object({
|
|
16148
|
+
deviceId: number(),
|
|
16149
|
+
on: boolean()
|
|
16150
|
+
}), _void(), {
|
|
16151
|
+
kind: "mutation",
|
|
16152
|
+
auth: "admin"
|
|
16153
|
+
}) },
|
|
16154
|
+
events: { onStateChanged: { data: object({
|
|
16155
|
+
deviceId: number(),
|
|
16156
|
+
on: boolean(),
|
|
16157
|
+
lastChangedAt: number()
|
|
16158
|
+
}) } },
|
|
16159
|
+
status: {
|
|
16160
|
+
schema: SwitchStatusSchema,
|
|
16161
|
+
kind: "command-driven"
|
|
16162
|
+
},
|
|
16163
|
+
/**
|
|
16164
|
+
* Runtime-state slice — the last applied on/off state, mirrored by
|
|
16165
|
+
* the kernel. Read via `device.state.switch.value`; UI toggles do
|
|
16166
|
+
* not need to re-query the provider after a setState mutation.
|
|
16167
|
+
*/
|
|
16168
|
+
runtimeState: SwitchStatusSchema,
|
|
16169
|
+
settings: { bindings: [{
|
|
16170
|
+
kind: "scalar",
|
|
16171
|
+
statusPath: "on",
|
|
16172
|
+
method: "setState",
|
|
16173
|
+
valueArg: "on",
|
|
16174
|
+
field: {
|
|
16175
|
+
label: "On",
|
|
16176
|
+
kind: "boolean"
|
|
16177
|
+
}
|
|
16178
|
+
}] }
|
|
16179
|
+
};
|
|
16180
|
+
/**
|
|
16181
|
+
* Tamper / case-open detection sensor. Drives Home Assistant
|
|
16182
|
+
* `binary_sensor` entries with `device_class: tamper`. Push-driven.
|
|
16183
|
+
*/
|
|
16184
|
+
var TamperStatusSchema = object({
|
|
14088
16185
|
/** True when the device's tamper switch / case-open contact is
|
|
14089
16186
|
* currently triggered. */
|
|
14090
16187
|
tampered: boolean(),
|
|
14091
16188
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
14092
16189
|
lastChangedAt: number()
|
|
14093
16190
|
});
|
|
14094
|
-
|
|
14095
|
-
|
|
16191
|
+
var tamperCapability = {
|
|
16192
|
+
name: "tamper",
|
|
16193
|
+
scope: "device",
|
|
16194
|
+
deviceNative: true,
|
|
16195
|
+
mode: "singleton",
|
|
16196
|
+
deviceTypes: [DeviceType.Sensor],
|
|
16197
|
+
methods: {},
|
|
16198
|
+
status: {
|
|
16199
|
+
schema: TamperStatusSchema,
|
|
16200
|
+
kind: "push"
|
|
16201
|
+
},
|
|
16202
|
+
runtimeState: TamperStatusSchema
|
|
16203
|
+
};
|
|
16204
|
+
/**
|
|
16205
|
+
* Single-metric temperature reading. Drives Home Assistant `sensor`
|
|
16206
|
+
* entries with `device_class: temperature` and any future native
|
|
16207
|
+
* thermometer.
|
|
16208
|
+
*
|
|
16209
|
+
* Unit normalisation: providers convert to Celsius before storing.
|
|
16210
|
+
* The slice value is always Celsius so cross-cap aggregators
|
|
16211
|
+
* (climate-control's `currentTemp`, energy analytics) can compose
|
|
16212
|
+
* without per-source unit fixups. The canonical display unit (`°C`) is
|
|
16213
|
+
* a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
16214
|
+
* `sourceInfo`.
|
|
16215
|
+
*
|
|
16216
|
+
* Status `lastFetchedAt` lets staleness-aware consumers detect a
|
|
16217
|
+
* frozen feed (provider hung) distinct from a "temperature hasn't
|
|
16218
|
+
* changed" steady state.
|
|
16219
|
+
*/
|
|
16220
|
+
var TemperatureSensorStatusSchema = object({
|
|
14096
16221
|
/** Current temperature in Celsius. */
|
|
14097
16222
|
celsius: number(),
|
|
14098
16223
|
/** Ms epoch when the slice was last updated (push or poll). */
|
|
@@ -14108,8 +16233,29 @@ object({
|
|
|
14108
16233
|
* auto-formatting when absent. */
|
|
14109
16234
|
precision: number().int().min(0).max(10).optional()
|
|
14110
16235
|
});
|
|
14111
|
-
|
|
14112
|
-
|
|
16236
|
+
var temperatureSensorCapability = {
|
|
16237
|
+
name: "temperature-sensor",
|
|
16238
|
+
scope: "device",
|
|
16239
|
+
deviceNative: true,
|
|
16240
|
+
mode: "singleton",
|
|
16241
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
|
|
16242
|
+
methods: {},
|
|
16243
|
+
status: {
|
|
16244
|
+
schema: TemperatureSensorStatusSchema,
|
|
16245
|
+
kind: "push"
|
|
16246
|
+
},
|
|
16247
|
+
runtimeState: TemperatureSensorStatusSchema
|
|
16248
|
+
};
|
|
16249
|
+
/**
|
|
16250
|
+
* Firmware/software update entity. Installed on a `DeviceType.Update` device.
|
|
16251
|
+
* Surfaces current vs available version, whether an update is available, the
|
|
16252
|
+
* update state, and an install action. Read-mostly and POLL-driven: backends
|
|
16253
|
+
* such as the Homematic CCU do not broadcast firmware-descriptor changes, so
|
|
16254
|
+
* the client re-queries `getStatus` (which reads the live descriptor) rather
|
|
16255
|
+
* than relying on push delivery. `installUpdate` triggers the install
|
|
16256
|
+
* (best-effort — providers throw if the backend rejects it).
|
|
16257
|
+
*/
|
|
16258
|
+
var UpdateStatusSchema = object({
|
|
14113
16259
|
currentVersion: string().nullable(),
|
|
14114
16260
|
availableVersion: string().nullable(),
|
|
14115
16261
|
/**
|
|
@@ -14122,10 +16268,22 @@ object({
|
|
|
14122
16268
|
state: string().nullable(),
|
|
14123
16269
|
inProgress: boolean()
|
|
14124
16270
|
});
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
16271
|
+
var updateCapability = {
|
|
16272
|
+
name: "update",
|
|
16273
|
+
scope: "device",
|
|
16274
|
+
deviceNative: true,
|
|
16275
|
+
mode: "singleton",
|
|
16276
|
+
deviceTypes: [DeviceType.Update],
|
|
16277
|
+
methods: { installUpdate: method(_void(), _void(), {
|
|
16278
|
+
kind: "mutation",
|
|
16279
|
+
auth: "admin"
|
|
16280
|
+
}) },
|
|
16281
|
+
status: {
|
|
16282
|
+
schema: UpdateStatusSchema,
|
|
16283
|
+
kind: "poll"
|
|
16284
|
+
},
|
|
16285
|
+
runtimeState: UpdateStatusSchema
|
|
16286
|
+
};
|
|
14129
16287
|
/**
|
|
14130
16288
|
* Robot-vacuum cap. Models HA `vacuum.*` entities — anything with a
|
|
14131
16289
|
* cleaning lifecycle plus a return-to-base / locate surface and an
|
|
@@ -14177,7 +16335,7 @@ var TankStatusSchema = object({
|
|
|
14177
16335
|
"full"
|
|
14178
16336
|
]).nullable()
|
|
14179
16337
|
});
|
|
14180
|
-
object({
|
|
16338
|
+
var VacuumControlStatusSchema = object({
|
|
14181
16339
|
/** Lifecycle state of the vacuum. */
|
|
14182
16340
|
state: VacuumStateSchema,
|
|
14183
16341
|
/** 0..100 battery percentage. Null when the device has no battery
|
|
@@ -14189,45 +16347,68 @@ object({
|
|
|
14189
16347
|
/** Speed tokens the hardware accepts — drives the UI selector. */
|
|
14190
16348
|
availableFanSpeeds: array(string()),
|
|
14191
16349
|
/** Clean-water (mop) tank. Null when the hardware has no clean-water tank. */
|
|
14192
|
-
cleanWater: TankStatusSchema.nullable(),
|
|
14193
|
-
/** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
|
|
14194
|
-
dirtyWater: TankStatusSchema.nullable(),
|
|
14195
|
-
/** Detergent tank. Null when the hardware has no detergent tank. */
|
|
14196
|
-
detergent: TankStatusSchema.nullable(),
|
|
14197
|
-
/** Dust bin. Null when the hardware has no dust bin. */
|
|
14198
|
-
dustBin: TankStatusSchema.nullable(),
|
|
14199
|
-
/** 0..100 cleaning-completion percentage of the current task, or null. */
|
|
14200
|
-
progressPercent: number().min(0).max(100).nullable(),
|
|
14201
|
-
/** Current error code (0 / null = no error). */
|
|
14202
|
-
errorCode: number().nullable(),
|
|
14203
|
-
/** Human label for {@link errorCode}, or null when none / undecodable. */
|
|
14204
|
-
errorLabel: string().nullable(),
|
|
14205
|
-
/** Ms epoch when the slice was last updated. */
|
|
14206
|
-
lastChangedAt: number()
|
|
14207
|
-
});
|
|
14208
|
-
DeviceType.Vacuum, method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
14209
|
-
kind: "mutation",
|
|
14210
|
-
auth: "admin"
|
|
14211
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
14212
|
-
kind: "mutation",
|
|
14213
|
-
auth: "admin"
|
|
14214
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
14215
|
-
kind: "mutation",
|
|
14216
|
-
auth: "admin"
|
|
14217
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
14218
|
-
kind: "mutation",
|
|
14219
|
-
auth: "admin"
|
|
14220
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
14221
|
-
kind: "mutation",
|
|
14222
|
-
auth: "admin"
|
|
14223
|
-
}), method(object({
|
|
14224
|
-
deviceId: number().int().nonnegative(),
|
|
14225
|
-
speed: string().min(1)
|
|
14226
|
-
}), _void(), {
|
|
14227
|
-
kind: "mutation",
|
|
14228
|
-
auth: "admin"
|
|
16350
|
+
cleanWater: TankStatusSchema.nullable(),
|
|
16351
|
+
/** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
|
|
16352
|
+
dirtyWater: TankStatusSchema.nullable(),
|
|
16353
|
+
/** Detergent tank. Null when the hardware has no detergent tank. */
|
|
16354
|
+
detergent: TankStatusSchema.nullable(),
|
|
16355
|
+
/** Dust bin. Null when the hardware has no dust bin. */
|
|
16356
|
+
dustBin: TankStatusSchema.nullable(),
|
|
16357
|
+
/** 0..100 cleaning-completion percentage of the current task, or null. */
|
|
16358
|
+
progressPercent: number().min(0).max(100).nullable(),
|
|
16359
|
+
/** Current error code (0 / null = no error). */
|
|
16360
|
+
errorCode: number().nullable(),
|
|
16361
|
+
/** Human label for {@link errorCode}, or null when none / undecodable. */
|
|
16362
|
+
errorLabel: string().nullable(),
|
|
16363
|
+
/** Ms epoch when the slice was last updated. */
|
|
16364
|
+
lastChangedAt: number()
|
|
14229
16365
|
});
|
|
14230
|
-
|
|
16366
|
+
var vacuumControlCapability = {
|
|
16367
|
+
name: "vacuum-control",
|
|
16368
|
+
scope: "device",
|
|
16369
|
+
deviceNative: true,
|
|
16370
|
+
mode: "singleton",
|
|
16371
|
+
deviceTypes: [DeviceType.Vacuum],
|
|
16372
|
+
methods: {
|
|
16373
|
+
start: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16374
|
+
kind: "mutation",
|
|
16375
|
+
auth: "admin"
|
|
16376
|
+
}),
|
|
16377
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16378
|
+
kind: "mutation",
|
|
16379
|
+
auth: "admin"
|
|
16380
|
+
}),
|
|
16381
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16382
|
+
kind: "mutation",
|
|
16383
|
+
auth: "admin"
|
|
16384
|
+
}),
|
|
16385
|
+
returnToBase: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16386
|
+
kind: "mutation",
|
|
16387
|
+
auth: "admin"
|
|
16388
|
+
}),
|
|
16389
|
+
locate: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16390
|
+
kind: "mutation",
|
|
16391
|
+
auth: "admin"
|
|
16392
|
+
}),
|
|
16393
|
+
setFanSpeed: method(object({
|
|
16394
|
+
deviceId: number().int().nonnegative(),
|
|
16395
|
+
speed: string().min(1)
|
|
16396
|
+
}), _void(), {
|
|
16397
|
+
kind: "mutation",
|
|
16398
|
+
auth: "admin"
|
|
16399
|
+
})
|
|
16400
|
+
},
|
|
16401
|
+
status: {
|
|
16402
|
+
schema: VacuumControlStatusSchema,
|
|
16403
|
+
kind: "push"
|
|
16404
|
+
},
|
|
16405
|
+
/**
|
|
16406
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16407
|
+
* slice for live state + battery + fan-speed changes.
|
|
16408
|
+
*/
|
|
16409
|
+
runtimeState: VacuumControlStatusSchema
|
|
16410
|
+
};
|
|
16411
|
+
var ValveStatusSchema = object({
|
|
14231
16412
|
/** Lifecycle state of the valve. */
|
|
14232
16413
|
state: _enum([
|
|
14233
16414
|
"open",
|
|
@@ -14242,29 +16423,80 @@ object({
|
|
|
14242
16423
|
/** Ms epoch when the slice was last updated. */
|
|
14243
16424
|
lastChangedAt: number()
|
|
14244
16425
|
});
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
|
|
14260
|
-
})
|
|
14261
|
-
|
|
16426
|
+
var valveCapability = {
|
|
16427
|
+
name: "valve",
|
|
16428
|
+
scope: "device",
|
|
16429
|
+
deviceNative: true,
|
|
16430
|
+
mode: "singleton",
|
|
16431
|
+
deviceTypes: [DeviceType.Valve],
|
|
16432
|
+
methods: {
|
|
16433
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16434
|
+
kind: "mutation",
|
|
16435
|
+
auth: "admin"
|
|
16436
|
+
}),
|
|
16437
|
+
close: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16438
|
+
kind: "mutation",
|
|
16439
|
+
auth: "admin"
|
|
16440
|
+
}),
|
|
16441
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16442
|
+
kind: "mutation",
|
|
16443
|
+
auth: "admin"
|
|
16444
|
+
}),
|
|
16445
|
+
setPosition: method(object({
|
|
16446
|
+
deviceId: number().int().nonnegative(),
|
|
16447
|
+
position: number().min(0).max(100)
|
|
16448
|
+
}), _void(), {
|
|
16449
|
+
kind: "mutation",
|
|
16450
|
+
auth: "admin"
|
|
16451
|
+
})
|
|
16452
|
+
},
|
|
16453
|
+
status: {
|
|
16454
|
+
schema: ValveStatusSchema,
|
|
16455
|
+
kind: "push"
|
|
16456
|
+
},
|
|
16457
|
+
/**
|
|
16458
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16459
|
+
* slice for live position changes during a move.
|
|
16460
|
+
*/
|
|
16461
|
+
runtimeState: ValveStatusSchema
|
|
16462
|
+
};
|
|
16463
|
+
/**
|
|
16464
|
+
* Vibration / shake / impact sensor. Drives Home Assistant
|
|
16465
|
+
* `binary_sensor` entries with `device_class: vibration`. Push-driven.
|
|
16466
|
+
*/
|
|
16467
|
+
var VibrationStatusSchema = object({
|
|
14262
16468
|
detected: boolean(),
|
|
14263
16469
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
14264
16470
|
lastChangedAt: number()
|
|
14265
16471
|
});
|
|
14266
|
-
|
|
14267
|
-
|
|
16472
|
+
var vibrationCapability = {
|
|
16473
|
+
name: "vibration",
|
|
16474
|
+
scope: "device",
|
|
16475
|
+
deviceNative: true,
|
|
16476
|
+
mode: "singleton",
|
|
16477
|
+
deviceTypes: [DeviceType.Sensor],
|
|
16478
|
+
methods: {},
|
|
16479
|
+
status: {
|
|
16480
|
+
schema: VibrationStatusSchema,
|
|
16481
|
+
kind: "push"
|
|
16482
|
+
},
|
|
16483
|
+
runtimeState: VibrationStatusSchema
|
|
16484
|
+
};
|
|
16485
|
+
/**
|
|
16486
|
+
* Water heater / boiler cap. Models HA `water_heater.*` entities — a
|
|
16487
|
+
* climate-family actuator with a target temperature, an operation-mode
|
|
16488
|
+
* selector (`eco` / `electric` / `gas` / `heat_pump` / `high_demand` /
|
|
16489
|
+
* `performance` / `off`), and an optional away-mode toggle.
|
|
16490
|
+
*
|
|
16491
|
+
* The slice carries the current + target temperature, the active
|
|
16492
|
+
* operation mode (HA's `state`) and its available set (HA's
|
|
16493
|
+
* `operation_list`), the away flag (HA `away_mode` 'on'/'off' → bool,
|
|
16494
|
+
* null when unsupported), plus the `min_temp` / `max_temp` bounds.
|
|
16495
|
+
*
|
|
16496
|
+
* Providers populate only what the hardware reports — temperature and
|
|
16497
|
+
* away fields stay null when the device has no such surface.
|
|
16498
|
+
*/
|
|
16499
|
+
var WaterHeaterStatusSchema = object({
|
|
14268
16500
|
/** Current measured temperature. Null when not reported. */
|
|
14269
16501
|
currentTemp: number().nullable(),
|
|
14270
16502
|
/** Target temperature setpoint. Null when no setpoint surface. */
|
|
@@ -14285,26 +16517,65 @@ object({
|
|
|
14285
16517
|
/** Ms epoch when the slice was last updated. */
|
|
14286
16518
|
lastChangedAt: number()
|
|
14287
16519
|
});
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
16520
|
+
var waterHeaterCapability = {
|
|
16521
|
+
name: "water-heater",
|
|
16522
|
+
scope: "device",
|
|
16523
|
+
deviceNative: true,
|
|
16524
|
+
mode: "singleton",
|
|
16525
|
+
deviceTypes: [DeviceType.WaterHeater],
|
|
16526
|
+
methods: {
|
|
16527
|
+
setTargetTemp: method(object({
|
|
16528
|
+
deviceId: number().int().nonnegative(),
|
|
16529
|
+
temp: number().finite()
|
|
16530
|
+
}), _void(), {
|
|
16531
|
+
kind: "mutation",
|
|
16532
|
+
auth: "admin"
|
|
16533
|
+
}),
|
|
16534
|
+
setOperationMode: method(object({
|
|
16535
|
+
deviceId: number().int().nonnegative(),
|
|
16536
|
+
mode: string().min(1)
|
|
16537
|
+
}), _void(), {
|
|
16538
|
+
kind: "mutation",
|
|
16539
|
+
auth: "admin"
|
|
16540
|
+
}),
|
|
16541
|
+
setAway: method(object({
|
|
16542
|
+
deviceId: number().int().nonnegative(),
|
|
16543
|
+
on: boolean()
|
|
16544
|
+
}), _void(), {
|
|
16545
|
+
kind: "mutation",
|
|
16546
|
+
auth: "admin"
|
|
16547
|
+
})
|
|
16548
|
+
},
|
|
16549
|
+
status: {
|
|
16550
|
+
schema: WaterHeaterStatusSchema,
|
|
16551
|
+
kind: "push"
|
|
16552
|
+
},
|
|
16553
|
+
/**
|
|
16554
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16555
|
+
* slice for live temperature / mode / away changes.
|
|
16556
|
+
*/
|
|
16557
|
+
runtimeState: WaterHeaterStatusSchema
|
|
16558
|
+
};
|
|
16559
|
+
/**
|
|
16560
|
+
* Weather provider cap. Models HA `weather.*` entities — a read-only
|
|
16561
|
+
* snapshot of the CURRENT conditions a weather integration reports.
|
|
16562
|
+
*
|
|
16563
|
+
* Read-only: there are no setters. The slice is populated from upstream
|
|
16564
|
+
* pushes (the HA weather entity's state + attributes) and rendered by
|
|
16565
|
+
* the UI as a sky scene + readouts.
|
|
16566
|
+
*
|
|
16567
|
+
* `condition` is the verbatim HA state string (`sunny` / `cloudy` /
|
|
16568
|
+
* `rainy` / `snowy` / `partlycloudy` / `pouring` / `lightning` /
|
|
16569
|
+
* `lightning-rainy` / `fog` / `windy` / `windy-variant` / `hail` /
|
|
16570
|
+
* `clear-night` / `exceptional` / …). The UI maps it to a glyph + tint;
|
|
16571
|
+
* unknown strings fall back to a neutral cloud.
|
|
16572
|
+
*
|
|
16573
|
+
* Every numeric reading is nullable — a given weather integration only
|
|
16574
|
+
* populates the metrics it actually provides.
|
|
16575
|
+
*
|
|
16576
|
+
* Forecast deferred — current conditions only for v1.
|
|
16577
|
+
*/
|
|
16578
|
+
var WeatherStatusSchema = object({
|
|
14308
16579
|
/** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
|
|
14309
16580
|
* when no condition has been reported yet. */
|
|
14310
16581
|
condition: string().nullable(),
|
|
@@ -14327,7 +16598,23 @@ object({
|
|
|
14327
16598
|
/** Ms epoch when the slice was last updated. */
|
|
14328
16599
|
lastFetchedAt: number()
|
|
14329
16600
|
});
|
|
14330
|
-
|
|
16601
|
+
var weatherCapability = {
|
|
16602
|
+
name: "weather",
|
|
16603
|
+
scope: "device",
|
|
16604
|
+
deviceNative: true,
|
|
16605
|
+
mode: "singleton",
|
|
16606
|
+
deviceTypes: [DeviceType.Weather],
|
|
16607
|
+
methods: {},
|
|
16608
|
+
status: {
|
|
16609
|
+
schema: WeatherStatusSchema,
|
|
16610
|
+
kind: "push"
|
|
16611
|
+
},
|
|
16612
|
+
/**
|
|
16613
|
+
* Runtime-state slice — mirrored by the kernel. The UI reads the
|
|
16614
|
+
* current conditions directly from the slice on each weather push.
|
|
16615
|
+
*/
|
|
16616
|
+
runtimeState: WeatherStatusSchema
|
|
16617
|
+
};
|
|
14331
16618
|
/**
|
|
14332
16619
|
* Per-zone occupancy aggregation produced by the analytics frame
|
|
14333
16620
|
* processor on every inference result. Covers the full combinatorial
|
|
@@ -14590,21 +16877,604 @@ var ZoneRuleStageEnum = _enum([
|
|
|
14590
16877
|
"detection",
|
|
14591
16878
|
"package"
|
|
14592
16879
|
]);
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
16880
|
+
/**
|
|
16881
|
+
* Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
|
|
16882
|
+
* `state` getter looks up the cap definition here to construct a
|
|
16883
|
+
* `sliceProxy()` lazily on first access. Generated alongside the type
|
|
16884
|
+
* so type and runtime registry can never drift apart.
|
|
16885
|
+
*/
|
|
16886
|
+
var DEVICE_LOCAL_STATE_CAPS = {
|
|
16887
|
+
airQualitySensor: airQualitySensorCapability,
|
|
16888
|
+
alarmPanel: alarmPanelCapability,
|
|
16889
|
+
ambientLightSensor: ambientLightSensorCapability,
|
|
16890
|
+
audioMetrics: audioMetricsCapability,
|
|
16891
|
+
automationControl: automationControlCapability,
|
|
16892
|
+
battery: batteryCapability,
|
|
16893
|
+
binary: binaryCapability,
|
|
16894
|
+
brightness: brightnessCapability,
|
|
16895
|
+
cameraStreams: cameraStreamsCapability,
|
|
16896
|
+
carbonMonoxide: carbonMonoxideCapability,
|
|
16897
|
+
climateControl: climateControlCapability,
|
|
16898
|
+
color: colorCapability,
|
|
16899
|
+
connectivity: connectivityCapability,
|
|
16900
|
+
consumables: consumablesCapability,
|
|
16901
|
+
contact: contactCapability,
|
|
16902
|
+
control: controlCapability,
|
|
16903
|
+
cover: coverCapability,
|
|
16904
|
+
dayNight: dayNightCapability,
|
|
16905
|
+
deviceDiscovery: deviceDiscoveryCapability,
|
|
16906
|
+
deviceStatus: deviceStatusCapability,
|
|
16907
|
+
doorbell: doorbellCapability,
|
|
16908
|
+
enumSensor: enumSensorCapability,
|
|
16909
|
+
eventEmitter: eventEmitterCapability,
|
|
16910
|
+
fanControl: fanControlCapability,
|
|
16911
|
+
featureProbe: featureProbeCapability,
|
|
16912
|
+
flood: floodCapability,
|
|
16913
|
+
gas: gasCapability,
|
|
16914
|
+
humidifier: humidifierCapability,
|
|
16915
|
+
humiditySensor: humiditySensorCapability,
|
|
16916
|
+
image: imageCapability,
|
|
16917
|
+
imageSettings: imageSettingsCapability,
|
|
16918
|
+
lawnMowerControl: lawnMowerControlCapability,
|
|
16919
|
+
lockControl: lockControlCapability,
|
|
16920
|
+
mediaPlayer: mediaPlayerCapability,
|
|
16921
|
+
motion: motionCapability,
|
|
16922
|
+
motionTrigger: motionTriggerCapability,
|
|
16923
|
+
motionZones: motionZonesCapability,
|
|
16924
|
+
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
16925
|
+
notifier: notifierCapability,
|
|
16926
|
+
numericSensor: numericSensorCapability,
|
|
16927
|
+
petFeeder: petFeederCapability,
|
|
16928
|
+
powerMeter: powerMeterCapability,
|
|
16929
|
+
presence: presenceCapability,
|
|
16930
|
+
pressureSensor: pressureSensorCapability,
|
|
16931
|
+
privacyMask: privacyMaskCapability,
|
|
16932
|
+
ptzAutotrack: ptzAutotrackCapability,
|
|
16933
|
+
sceneMonitor: sceneMonitorCapability,
|
|
16934
|
+
scriptRunner: scriptRunnerCapability,
|
|
16935
|
+
smoke: smokeCapability,
|
|
16936
|
+
streamParams: streamParamsCapability,
|
|
16937
|
+
switch: switchCapability,
|
|
16938
|
+
tamper: tamperCapability,
|
|
16939
|
+
temperatureSensor: temperatureSensorCapability,
|
|
16940
|
+
update: updateCapability,
|
|
16941
|
+
vacuumControl: vacuumControlCapability,
|
|
16942
|
+
valve: valveCapability,
|
|
16943
|
+
vibration: vibrationCapability,
|
|
16944
|
+
waterHeater: waterHeaterCapability,
|
|
16945
|
+
weather: weatherCapability,
|
|
16946
|
+
zoneAnalytics: zoneAnalyticsCapability,
|
|
16947
|
+
zoneRules: {
|
|
16948
|
+
name: "zone-rules",
|
|
16949
|
+
scope: "device",
|
|
16950
|
+
mode: "singleton",
|
|
16951
|
+
deviceTypes: [DeviceType.Camera],
|
|
16952
|
+
methods: {
|
|
16953
|
+
/** Read the full rule list for a given stage (empty when no rules
|
|
16954
|
+
* are defined yet). */
|
|
16955
|
+
listRules: method(object({
|
|
16956
|
+
deviceId: number(),
|
|
16957
|
+
stage: ZoneRuleStageEnum
|
|
16958
|
+
}), array(ZoneRuleSchema).readonly()),
|
|
16959
|
+
/** Bulk-replace the rule list for one stage. The provider validates
|
|
16960
|
+
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
16961
|
+
* thresholds in range) and rejects the whole patch if any entry
|
|
16962
|
+
* is invalid — partial writes are a configuration footgun. */
|
|
16963
|
+
setRules: method(object({
|
|
16964
|
+
deviceId: number(),
|
|
16965
|
+
stage: ZoneRuleStageEnum,
|
|
16966
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
16967
|
+
}), _void(), {
|
|
16968
|
+
kind: "mutation",
|
|
16969
|
+
auth: "admin"
|
|
16970
|
+
})
|
|
16971
|
+
},
|
|
16972
|
+
/**
|
|
16973
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
16974
|
+
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
16975
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
16976
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
16977
|
+
* complete current set. Consumers that only care about one stage
|
|
16978
|
+
* just read the matching property.
|
|
16979
|
+
*
|
|
16980
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
16981
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
16982
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
16983
|
+
* The orchestrator provider writes this stage as a first-class slice
|
|
16984
|
+
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
16985
|
+
* package}` shape, so consumers read the current package rules directly
|
|
16986
|
+
* off `device.state.zoneRules.value.package`.
|
|
16987
|
+
*/
|
|
16988
|
+
runtimeState: object({
|
|
16989
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
16990
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
16991
|
+
package: array(ZoneRuleSchema).readonly()
|
|
16992
|
+
})
|
|
16993
|
+
},
|
|
16994
|
+
zones: zonesCapability
|
|
16995
|
+
};
|
|
16996
|
+
var BaseDevice = class {
|
|
16997
|
+
id;
|
|
16998
|
+
stableId;
|
|
16999
|
+
type;
|
|
17000
|
+
name;
|
|
17001
|
+
parentDeviceId;
|
|
17002
|
+
role;
|
|
17003
|
+
/**
|
|
17004
|
+
* Cap-keyed runtime-state slice is the single source of truth for
|
|
17005
|
+
* `online`. Both getter and setter proxy to the slice — drivers can
|
|
17006
|
+
* write `this.online = true` ergonomically, and the cap event fires
|
|
17007
|
+
* automatically through the runtime-state writer. `markOnline()` is
|
|
17008
|
+
* kept as the explicit method form mandated by `IDevice`.
|
|
17009
|
+
*/
|
|
17010
|
+
get online() {
|
|
17011
|
+
return this.runtimeState.getCapState("device-status")?.online ?? false;
|
|
17012
|
+
}
|
|
17013
|
+
set online(value) {
|
|
17014
|
+
this.markOnline(value);
|
|
17015
|
+
}
|
|
17016
|
+
/**
|
|
17017
|
+
* Generic per-cap runtime-state namespace. One entry per cap with
|
|
17018
|
+
* `runtimeState:` declared, auto-generated by codegen — see
|
|
17019
|
+
* `device-local-state.ts`. Drivers access via:
|
|
17020
|
+
*
|
|
17021
|
+
* `this.state.battery.sleeping = true` // patches the battery slice
|
|
17022
|
+
* `const pct = this.state.battery.percentage` // reads the battery slice
|
|
17023
|
+
* `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
|
|
17024
|
+
*
|
|
17025
|
+
* Adding a new cap with `runtimeState:` automatically extends this
|
|
17026
|
+
* namespace — drivers don't have to declare proxies. Reads return
|
|
17027
|
+
* `undefined` when the slice hasn't been seeded; writes patch via
|
|
17028
|
+
* `runtimeState.patchCapState` and validate against the cap's schema
|
|
17029
|
+
* (so partial writes need the slice to be seeded with the required
|
|
17030
|
+
* fields first — drivers do this on cap registration).
|
|
17031
|
+
*
|
|
17032
|
+
* For caps not exposed in `DeviceLocalState`, drivers can build their
|
|
17033
|
+
* own typed proxy via `this.sliceProxy(cap)`.
|
|
17034
|
+
*/
|
|
17035
|
+
get state() {
|
|
17036
|
+
if (!this._stateProxyCache) {
|
|
17037
|
+
const cache = {};
|
|
17038
|
+
const handler = { get: (_target, key) => {
|
|
17039
|
+
const k = key;
|
|
17040
|
+
if (k in cache) return cache[k];
|
|
17041
|
+
const cap = DEVICE_LOCAL_STATE_CAPS[k];
|
|
17042
|
+
if (!cap) return void 0;
|
|
17043
|
+
const proxy = this.sliceProxy(cap);
|
|
17044
|
+
cache[k] = proxy;
|
|
17045
|
+
return proxy;
|
|
17046
|
+
} };
|
|
17047
|
+
this._stateProxyCache = new Proxy(cache, handler);
|
|
17048
|
+
}
|
|
17049
|
+
return this._stateProxyCache;
|
|
17050
|
+
}
|
|
17051
|
+
_stateProxyCache;
|
|
17052
|
+
config;
|
|
17053
|
+
/**
|
|
17054
|
+
* Per-device runtime state, cap-keyed. Always installed — slices
|
|
17055
|
+
* for individual caps materialise as those caps register their
|
|
17056
|
+
* native providers (`ctx.registerNativeCap`). The cap's own
|
|
17057
|
+
* `runtimeState` schema is the source of truth for the slice
|
|
17058
|
+
* shape; drivers don't redeclare it, they just write through.
|
|
17059
|
+
*
|
|
17060
|
+
* Read: `this.runtimeState.getCapState('battery')` →
|
|
17061
|
+
* `{percentage, charging, sleeping, lastUpdated}` for any
|
|
17062
|
+
* provider that registers `batteryCapability`.
|
|
17063
|
+
* Write: `this.runtimeState.setCapState('battery', { … })`.
|
|
17064
|
+
*
|
|
17065
|
+
* Cross-process consumers reach this state through the
|
|
17066
|
+
* `deviceState` cap router (or via cap-specific events the driver
|
|
17067
|
+
* emits — e.g. `battery.onStatusChanged`). The local handle is
|
|
17068
|
+
* accessed in-process by the driver to avoid roundtrips.
|
|
17069
|
+
*/
|
|
17070
|
+
runtimeState;
|
|
17071
|
+
ctx;
|
|
17072
|
+
/**
|
|
17073
|
+
* Operator-organisational location label (room / area / zone).
|
|
17074
|
+
* Read from `ctx.deviceMeta.location`; mutated via
|
|
17075
|
+
* `kernel.devices.setLocation(id, value)`. Free-text — providers
|
|
17076
|
+
* don't interpret it; the UI groups devices by this for filters
|
|
17077
|
+
* like "show me all cameras in Kitchen". `null` when unset.
|
|
17078
|
+
*/
|
|
17079
|
+
location;
|
|
17080
|
+
/**
|
|
17081
|
+
* Soft-disabled flag. When `true`, the device class is still
|
|
17082
|
+
* instantiated and visible in the UI (so the operator can flip
|
|
17083
|
+
* back on without re-adding) but lifecycle hooks (publishToBroker,
|
|
17084
|
+
* alarm-stream subscribe, …) MUST be gated by the driver to skip
|
|
17085
|
+
* work. The `BaseDevice` enforces this by exposing the flag here;
|
|
17086
|
+
* it does NOT mutate cap behaviour automatically — drivers consult
|
|
17087
|
+
* `this.disabled` at the top of their lifecycle methods. Read from
|
|
17088
|
+
* `ctx.deviceMeta.disabled`; mutated via
|
|
17089
|
+
* `kernel.devices.setDisabled(id, value)`.
|
|
17090
|
+
*/
|
|
17091
|
+
disabled;
|
|
17092
|
+
/**
|
|
17093
|
+
* Cached materialised `SourceInfo` — either the value persisted under
|
|
17094
|
+
* `metadata.sourceInfo` at construction time, or a synthetic
|
|
17095
|
+
* `{ id: stableId, system: addonId }` for providers that haven't
|
|
17096
|
+
* migrated yet. Lazily populated on first `sourceInfo` read so the
|
|
17097
|
+
* cost of Zod-parsing the meta blob is paid once per device boot.
|
|
17098
|
+
* Invalidated by `updateSourceInfo()` so providers see the new value
|
|
17099
|
+
* back through the getter without a re-fetch from the meta surface.
|
|
17100
|
+
*/
|
|
17101
|
+
_sourceInfoCache = null;
|
|
17102
|
+
constructor(ctx, schema, options) {
|
|
17103
|
+
this.ctx = ctx;
|
|
17104
|
+
this.id = ctx.id;
|
|
17105
|
+
this.stableId = ctx.stableId;
|
|
17106
|
+
this.type = options.type;
|
|
17107
|
+
if (!ctx.deviceMeta) throw new Error(`BaseDevice constructor: ctx.deviceMeta is required (id=${ctx.id} stableId=${ctx.stableId})`);
|
|
17108
|
+
this.name = ctx.deviceMeta.name;
|
|
17109
|
+
this.location = ctx.deviceMeta.location;
|
|
17110
|
+
this.disabled = ctx.deviceMeta.disabled;
|
|
17111
|
+
this.role = options.role;
|
|
17112
|
+
this.parentDeviceId = ctx.parentDeviceId;
|
|
17113
|
+
const seedData = ctx.persistedConfig ?? {};
|
|
17114
|
+
this.config = DeviceConfig.fromSchema(schema, (data) => ctx.persistConfig(data), seedData, ({ droppedKeys, issues }) => {
|
|
17115
|
+
ctx.logger.warn("Device config recovery: dropping invalid persisted fields", {
|
|
17116
|
+
tags: {
|
|
17117
|
+
deviceId: ctx.id,
|
|
17118
|
+
stableId: ctx.stableId
|
|
17119
|
+
},
|
|
17120
|
+
meta: {
|
|
17121
|
+
droppedKeys: [...droppedKeys],
|
|
17122
|
+
firstIssue: issues[0]?.message ?? null
|
|
17123
|
+
}
|
|
17124
|
+
});
|
|
17125
|
+
});
|
|
17126
|
+
let cachedProxy = null;
|
|
17127
|
+
const writer = async (capName, slice) => {
|
|
17128
|
+
if (!cachedProxy) cachedProxy = ctx.fetchDevice(ctx.id);
|
|
17129
|
+
await (await cachedProxy).deviceState.setCapSlice({
|
|
17130
|
+
capName,
|
|
17131
|
+
slice
|
|
17132
|
+
});
|
|
17133
|
+
};
|
|
17134
|
+
const initial = ctx.initialRuntimeState ?? {};
|
|
17135
|
+
this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer);
|
|
17136
|
+
ctx.bindRuntimeState?.(this.runtimeState);
|
|
17137
|
+
ctx.registerNativeCap?.(deviceStatusCapability, {});
|
|
17138
|
+
const seed = {
|
|
17139
|
+
online: true,
|
|
17140
|
+
lastChangedAt: Date.now()
|
|
17141
|
+
};
|
|
17142
|
+
this.runtimeState.setCapState("device-status", seed);
|
|
17143
|
+
ctx.registerNativeCap?.(featureProbeCapability, {});
|
|
17144
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
17145
|
+
flags: {},
|
|
17146
|
+
deviceType: null,
|
|
17147
|
+
model: null,
|
|
17148
|
+
channelCount: null,
|
|
17149
|
+
lastProbedAt: 0,
|
|
17150
|
+
lastFetchedAt: 0
|
|
17151
|
+
});
|
|
17152
|
+
}
|
|
17153
|
+
deviceActions = /* @__PURE__ */ new Map();
|
|
17154
|
+
/** Declare a device custom action + its typed handler. Idempotent per name. */
|
|
17155
|
+
registerDeviceAction(name, spec, handler) {
|
|
17156
|
+
this.deviceActions.set(name, {
|
|
17157
|
+
spec,
|
|
17158
|
+
handler
|
|
17159
|
+
});
|
|
17160
|
+
}
|
|
17161
|
+
/** Invoke a registered device action. Validates input against the spec. */
|
|
17162
|
+
async runDeviceAction(action, input) {
|
|
17163
|
+
const entry = this.deviceActions.get(action);
|
|
17164
|
+
if (!entry) throw new Error(`unknown device action "${action}" on device ${this.id}`);
|
|
17165
|
+
const parsed = entry.spec.input.parse(input);
|
|
17166
|
+
return entry.handler(parsed);
|
|
17167
|
+
}
|
|
17168
|
+
async removeDevice() {}
|
|
17169
|
+
/**
|
|
17170
|
+
* Set the device's online flag. Called by `BaseDeviceProvider` after
|
|
17171
|
+
* aggregating per-profile stream-broker health, or directly by drivers
|
|
17172
|
+
* that have provider-side liveness signals (e.g. ONVIF heartbeats,
|
|
17173
|
+
* Reolink Baichuan firmware push events). Mirrors the new value into
|
|
17174
|
+
* the `device-status` runtime-state slice so cross-process consumers
|
|
17175
|
+
* pick it up via the standard cap-state channel. Subclasses can
|
|
17176
|
+
* override to gate side effects on the transition.
|
|
17177
|
+
*/
|
|
17178
|
+
markOnline(online) {
|
|
17179
|
+
if (this.online === online) return;
|
|
17180
|
+
const next = {
|
|
17181
|
+
online,
|
|
17182
|
+
lastChangedAt: Date.now()
|
|
17183
|
+
};
|
|
17184
|
+
this.runtimeState.setCapState("device-status", next);
|
|
17185
|
+
}
|
|
17186
|
+
/**
|
|
17187
|
+
* Upstream-system identity + rendering envelope for this device. See
|
|
17188
|
+
* `SourceInfo` for the field contract. Always returns a valid object:
|
|
17189
|
+
* if the persisted `metadata.sourceInfo` blob is absent or fails Zod
|
|
17190
|
+
* validation, falls back to a synthetic `{ id: stableId, system: addonId }`
|
|
17191
|
+
* so providers that haven't migrated keep working without code changes.
|
|
17192
|
+
*
|
|
17193
|
+
* The value is cached after the first read. `updateSourceInfo()`
|
|
17194
|
+
* invalidates the cache so subsequent reads see the new patch. The
|
|
17195
|
+
* returned object is frozen to prevent accidental in-place mutation —
|
|
17196
|
+
* use `updateSourceInfo({ patch })` to change fields.
|
|
17197
|
+
*/
|
|
17198
|
+
get sourceInfo() {
|
|
17199
|
+
if (this._sourceInfoCache) return this._sourceInfoCache;
|
|
17200
|
+
const resolved = extractSourceInfoFromMetadata(this.ctx.deviceMeta.metadata) ?? synthesizeSourceInfo({
|
|
17201
|
+
stableId: this.stableId,
|
|
17202
|
+
addonId: this.ctx.deviceMeta.addonId
|
|
17203
|
+
});
|
|
17204
|
+
this._sourceInfoCache = Object.freeze({ ...resolved });
|
|
17205
|
+
return this._sourceInfoCache;
|
|
17206
|
+
}
|
|
17207
|
+
/**
|
|
17208
|
+
* Convenience accessor for the upstream dispatch key. Equivalent to
|
|
17209
|
+
* `this.sourceInfo.id` — providers use this to keep a
|
|
17210
|
+
* `Map<sourceId, IDevice>` for routing inbound push events.
|
|
17211
|
+
*/
|
|
17212
|
+
get sourceId() {
|
|
17213
|
+
return this.sourceInfo.id;
|
|
17214
|
+
}
|
|
17215
|
+
/**
|
|
17216
|
+
* Patch the device's `SourceInfo`. Shallow-merges `patch` over the
|
|
17217
|
+
* current value, persists the merged result under
|
|
17218
|
+
* `metadata.sourceInfo` via the `device-manager.setMetadata` cap, and
|
|
17219
|
+
* emits `EventCategory.DeviceSourceInfoChanged` for live consumers.
|
|
17220
|
+
*
|
|
17221
|
+
* Safe to call from anywhere in the device's lifetime — the call is
|
|
17222
|
+
* idempotent for `undefined` patch values (ignored) and best-effort
|
|
17223
|
+
* for persistence (a transient device-manager error doesn't unwind
|
|
17224
|
+
* the local cache update, so subsequent reads still see the patch).
|
|
17225
|
+
*
|
|
17226
|
+
* Drivers populate this on adoption + on every metadata change push
|
|
17227
|
+
* from the upstream source. Subscribers (UI, export adapters) react
|
|
17228
|
+
* via the `DeviceSourceInfoChanged` event without polling.
|
|
17229
|
+
*/
|
|
17230
|
+
async updateSourceInfo(patch) {
|
|
17231
|
+
const next = mergeSourceInfo(this.sourceInfo, patch);
|
|
17232
|
+
this._sourceInfoCache = Object.freeze({ ...next });
|
|
17233
|
+
const action = this.ctx.api?.deviceManager?.setMetadata;
|
|
17234
|
+
if (action) try {
|
|
17235
|
+
await action.mutate({
|
|
17236
|
+
deviceId: this.id,
|
|
17237
|
+
patch: { [SOURCE_INFO_METADATA_KEY]: next }
|
|
17238
|
+
});
|
|
17239
|
+
} catch {}
|
|
17240
|
+
this.ctx.eventBus.emit(createEvent("device.source-info-changed", {
|
|
17241
|
+
type: "device",
|
|
17242
|
+
id: this.stableId
|
|
17243
|
+
}, {
|
|
17244
|
+
deviceId: this.id,
|
|
17245
|
+
sourceInfo: next
|
|
17246
|
+
}));
|
|
17247
|
+
}
|
|
17248
|
+
/**
|
|
17249
|
+
* Re-publish the device's current `features` array to the persisted
|
|
17250
|
+
* meta blob. Drivers call this after a probe finishes when the live
|
|
17251
|
+
* `features` getter has gained new flags (e.g. `hasIntercom` flips
|
|
17252
|
+
* to true → `DeviceFeature.TwoWayAudio` joins the list).
|
|
17253
|
+
*
|
|
17254
|
+
* Without this, only the construction-time snapshot is written —
|
|
17255
|
+
* `deviceManager.registerDevice` is invoked once per boot, so probe-
|
|
17256
|
+
* driven additions don't reach the persisted index until the next
|
|
17257
|
+
* server restart, and `getDevice` / `listAll` keep returning the
|
|
17258
|
+
* stale list for forked-worker devices (whose live IDevice instance
|
|
17259
|
+
* is invisible to the hub registry).
|
|
17260
|
+
*
|
|
17261
|
+
* Idempotent: re-calling with the same features just no-ops on the
|
|
17262
|
+
* persisted meta. Best-effort: lookup or write failures are logged
|
|
17263
|
+
* at debug and swallowed — the live `device.features` getter is
|
|
17264
|
+
* still authoritative within this process, so callers never block
|
|
17265
|
+
* device boot on a meta refresh.
|
|
17266
|
+
*/
|
|
17267
|
+
async refreshFeatures() {
|
|
17268
|
+
const action = this.ctx.api?.deviceManager?.registerDevice;
|
|
17269
|
+
if (!action) return;
|
|
17270
|
+
try {
|
|
17271
|
+
await action.mutate({
|
|
17272
|
+
addonId: this.ctx.deviceMeta.addonId,
|
|
17273
|
+
stableId: this.stableId,
|
|
17274
|
+
id: this.id,
|
|
17275
|
+
type: this.type,
|
|
17276
|
+
name: this.name,
|
|
17277
|
+
parentDeviceId: this.parentDeviceId,
|
|
17278
|
+
features: [...this.features],
|
|
17279
|
+
config: {}
|
|
17280
|
+
});
|
|
17281
|
+
} catch (err) {}
|
|
17282
|
+
}
|
|
17283
|
+
/**
|
|
17284
|
+
* Typed read-through to a cap-keyed runtime-state slice. Drivers
|
|
17285
|
+
* call `this.getCapSlice(batteryCapability)` and the return type
|
|
17286
|
+
* is inferred from the cap's `runtimeState` Zod schema — no string
|
|
17287
|
+
* key, no manual generic. Returns `null` when the slice hasn't
|
|
17288
|
+
* been written yet (e.g. driver hasn't seeded battery yet).
|
|
17289
|
+
*/
|
|
17290
|
+
getCapSlice(cap) {
|
|
17291
|
+
return this.runtimeState.getCapState(cap.name) ?? null;
|
|
17292
|
+
}
|
|
17293
|
+
/**
|
|
17294
|
+
* Typed writer to a cap-keyed runtime-state slice. Routes through
|
|
17295
|
+
* the runtime-state writer (validate → persist → emit cap event).
|
|
17296
|
+
* Equivalent to `this.runtimeState.setCapState(cap.name, value)`
|
|
17297
|
+
* but with the cap's `runtimeState` schema enforcing the value
|
|
17298
|
+
* shape at compile time. Mirrors the symmetry of
|
|
17299
|
+
* `getCapSlice` / `setCapSlice` for cross-cap consistency.
|
|
17300
|
+
*/
|
|
17301
|
+
setCapSlice(cap, value) {
|
|
17302
|
+
this.runtimeState.setCapState(cap.name, value);
|
|
17303
|
+
}
|
|
17304
|
+
/**
|
|
17305
|
+
* Field-level read/write proxy over a cap's runtime-state slice.
|
|
17306
|
+
* Drivers that want ergonomic per-field access declare:
|
|
17307
|
+
*
|
|
17308
|
+
* ```ts
|
|
17309
|
+
* protected battery = this.sliceProxy(batteryCapability)
|
|
17310
|
+
* // …
|
|
17311
|
+
* this.battery.sleeping = true // patches the slice
|
|
17312
|
+
* const charging = this.battery.charging // reads the slice
|
|
17313
|
+
* ```
|
|
17314
|
+
*
|
|
17315
|
+
* Reads return `undefined` when the slice hasn't been seeded yet
|
|
17316
|
+
* (cap not registered, or seeded but the field is absent). Writes
|
|
17317
|
+
* route through `runtimeState.patchCapState` so the cap's `runtimeState`
|
|
17318
|
+
* schema validates the merged result and the cap event fires.
|
|
17319
|
+
*
|
|
17320
|
+
* Pattern is generic — same shape works for `battery`, `device-status`,
|
|
17321
|
+
* `motion`, `doorbell`, anything with a `runtimeState:` schema. Drivers
|
|
17322
|
+
* declare one proxy per cap they read/write directly.
|
|
17323
|
+
*/
|
|
17324
|
+
sliceProxy(cap) {
|
|
17325
|
+
return new Proxy({}, {
|
|
17326
|
+
get: (_, key) => {
|
|
17327
|
+
return this.runtimeState.getCapState(cap.name)?.[key];
|
|
17328
|
+
},
|
|
17329
|
+
set: (_, key, value) => {
|
|
17330
|
+
this.runtimeState.patchCapState(cap.name, { [key]: value });
|
|
17331
|
+
return true;
|
|
17332
|
+
},
|
|
17333
|
+
has: (_, key) => {
|
|
17334
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17335
|
+
return slice ? key in slice : false;
|
|
17336
|
+
},
|
|
17337
|
+
ownKeys: () => {
|
|
17338
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17339
|
+
return slice ? Object.keys(slice) : [];
|
|
17340
|
+
},
|
|
17341
|
+
getOwnPropertyDescriptor: (_, key) => {
|
|
17342
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17343
|
+
if (!slice || !(key in slice)) return void 0;
|
|
17344
|
+
return {
|
|
17345
|
+
configurable: true,
|
|
17346
|
+
enumerable: true,
|
|
17347
|
+
value: slice[key]
|
|
17348
|
+
};
|
|
17349
|
+
}
|
|
17350
|
+
});
|
|
17351
|
+
}
|
|
17352
|
+
/**
|
|
17353
|
+
* Default empty settings UI. Drivers override this to expose an
|
|
17354
|
+
* editable form in the device-details page. Returning an empty sections
|
|
17355
|
+
* array signals "nothing to contribute" — the aggregator drops the
|
|
17356
|
+
* contribution entirely rather than rendering a blank panel.
|
|
17357
|
+
*/
|
|
17358
|
+
getSettingsUISchema() {
|
|
17359
|
+
return { sections: [] };
|
|
17360
|
+
}
|
|
17361
|
+
/**
|
|
17362
|
+
* Default write path: forward the flat patch directly to storage.
|
|
17363
|
+
* Drivers that project a UI shape different from storage (e.g. `RtspCamera`
|
|
17364
|
+
* exposing `mainStreamUrl`/`subStreamUrl` over `streams[]`) override this
|
|
17365
|
+
* to reshape before `config.setAll`.
|
|
17366
|
+
*/
|
|
17367
|
+
async applySettingsPatch(patch) {
|
|
17368
|
+
await this.config.setAll(patch);
|
|
17369
|
+
}
|
|
17370
|
+
/**
|
|
17371
|
+
* Phase 3 — populate device-scoped state needed by downstream phases
|
|
17372
|
+
* (accessory reconciliation, public `features` array, optional cap
|
|
17373
|
+
* registration). Called ONCE per construction, after register but
|
|
17374
|
+
* before `getAccessoryChildren()`.
|
|
17375
|
+
*
|
|
17376
|
+
* Drivers write the `feature-probe` runtime-state slice via
|
|
17377
|
+
* `this.runtimeState.setCapState('feature-probe', {...})` — flag bag
|
|
17378
|
+
* is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
|
|
17379
|
+
* `hasSupplementalLight/hasAlarmIo`, etc).
|
|
17380
|
+
*
|
|
17381
|
+
* Default: nothing to probe → mark the device PROBED (set `lastProbedAt`) so
|
|
17382
|
+
* the kernel treats it as ready immediately. A device that derives its shape
|
|
17383
|
+
* from a spec (a container, or an accessory sensor) rather than from a
|
|
17384
|
+
* hardware probe has no probe to "complete"; without stamping `lastProbedAt`
|
|
17385
|
+
* it would look perpetually un-probed — logging "Initial probe did not
|
|
17386
|
+
* complete" on every boot and spinning a pointless retry chain. Drivers that
|
|
17387
|
+
* DO probe override this and write their own `feature-probe` slice (including
|
|
17388
|
+
* `lastProbedAt`) once their probe actually succeeds.
|
|
17389
|
+
*/
|
|
17390
|
+
async onProbe() {
|
|
17391
|
+
const base = this.runtimeState.getCapState("feature-probe") ?? {
|
|
17392
|
+
flags: {},
|
|
17393
|
+
deviceType: null,
|
|
17394
|
+
model: null,
|
|
17395
|
+
channelCount: null,
|
|
17396
|
+
lastProbedAt: 0,
|
|
17397
|
+
lastFetchedAt: 0
|
|
17398
|
+
};
|
|
17399
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
17400
|
+
...base,
|
|
17401
|
+
lastProbedAt: Date.now()
|
|
17402
|
+
});
|
|
17403
|
+
}
|
|
17404
|
+
/**
|
|
17405
|
+
* Phase 5 — fired after the device + its accessories are registered.
|
|
17406
|
+
* Drivers publish streams to the broker, kick off background tasks,
|
|
17407
|
+
* or subscribe to lib events that need a fully-registered device id.
|
|
17408
|
+
*
|
|
17409
|
+
* Default: no-op.
|
|
17410
|
+
*
|
|
17411
|
+
* RENAMED FROM `onCreated` (which still exists for back-compat in this
|
|
17412
|
+
* pass). The new name reflects the post-probe, post-accessory contract.
|
|
17413
|
+
*/
|
|
17414
|
+
async onActivate() {}
|
|
17415
|
+
/**
|
|
17416
|
+
* Re-run the probe + reconcile accessories + refresh features meta.
|
|
17417
|
+
* Drivers call this when device-side state changes (battery cam wakes,
|
|
17418
|
+
* firmware update, manual operator trigger).
|
|
17419
|
+
*
|
|
17420
|
+
* The kernel injects `_kernelReprobe` on registration so this method
|
|
17421
|
+
* delegates to the same orchestrator that runs the boot-time phase
|
|
17422
|
+
* 3 + 4 sequence. Drivers should NOT override this — they override
|
|
17423
|
+
* `onProbe()` instead.
|
|
17424
|
+
*/
|
|
17425
|
+
async reprobe() {
|
|
17426
|
+
if (this._kernelReprobe) await this._kernelReprobe();
|
|
17427
|
+
else await this.onProbe();
|
|
17428
|
+
}
|
|
17429
|
+
/**
|
|
17430
|
+
* Kernel-injected callback that runs the full post-probe orchestration
|
|
17431
|
+
* (onProbe → registerDevice meta refresh → accessory reconciliation).
|
|
17432
|
+
* Set by `device-cap-proxy.register()`. Drivers should not touch this
|
|
17433
|
+
* directly — call `reprobe()` instead.
|
|
17434
|
+
*/
|
|
17435
|
+
_kernelReprobe;
|
|
17436
|
+
/**
|
|
17437
|
+
* Declare accessory child devices the kernel should auto-spawn
|
|
17438
|
+
* after `onProbe()` resolves. Each spec fully describes one child
|
|
17439
|
+
* — stableId suffix (deterministic per kind for restore-safety),
|
|
17440
|
+
* meta (type / name / location), config (initial blob the child
|
|
17441
|
+
* self-hydrates), and a factory that constructs the concrete
|
|
17442
|
+
* class with whatever closure-captured refs it needs (typically
|
|
17443
|
+
* `this` for the parent reference).
|
|
17444
|
+
*
|
|
17445
|
+
* The kernel handles the rest: allocateDeviceId, persistInitialConfig
|
|
17446
|
+
* (skipped on restore when the row already exists),
|
|
17447
|
+
* persistInitialMeta, createContext, factory invocation, register,
|
|
17448
|
+
* and recursive lifecycle (probe + accessories + activate).
|
|
17449
|
+
*
|
|
17450
|
+
* Implementations should derive children from
|
|
17451
|
+
* `this.runtimeState.getCapState('feature-probe')` (post-probe truth).
|
|
17452
|
+
* Drivers can use the `getProbeFlags()` helper to read the flag bag
|
|
17453
|
+
* with a typed cast.
|
|
17454
|
+
*
|
|
17455
|
+
* Default: no children.
|
|
17456
|
+
*/
|
|
17457
|
+
getAccessoryChildren() {
|
|
17458
|
+
return [];
|
|
17459
|
+
}
|
|
17460
|
+
/**
|
|
17461
|
+
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
17462
|
+
* for `getAccessoryChildren()` and `features` getters that derive
|
|
17463
|
+
* outputs from the probe results.
|
|
17464
|
+
*/
|
|
17465
|
+
getProbeFlags() {
|
|
17466
|
+
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
17467
|
+
}
|
|
17468
|
+
/**
|
|
17469
|
+
* Returns true once `onProbe` has completed at least once
|
|
17470
|
+
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
17471
|
+
* to avoid spawning stale accessories on a fresh device whose probe
|
|
17472
|
+
* hasn't landed yet.
|
|
17473
|
+
*/
|
|
17474
|
+
hasProbed() {
|
|
17475
|
+
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
17476
|
+
}
|
|
17477
|
+
};
|
|
14608
17478
|
var ProviderStatusSchema = object({
|
|
14609
17479
|
connected: boolean(),
|
|
14610
17480
|
deviceCount: number(),
|
|
@@ -15688,6 +18558,10 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
|
|
|
15688
18558
|
auth: "admin"
|
|
15689
18559
|
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
15690
18560
|
DeviceType.Camera;
|
|
18561
|
+
/** kebab-case cap name → camelCase router-map key. */
|
|
18562
|
+
function kebabToCamel(s) {
|
|
18563
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
18564
|
+
}
|
|
15691
18565
|
/**
|
|
15692
18566
|
* Identity — preserves literal types for downstream inference.
|
|
15693
18567
|
*
|
|
@@ -27741,4 +30615,4 @@ Object.freeze({
|
|
|
27741
30615
|
"smtp-provider": "email"
|
|
27742
30616
|
});
|
|
27743
30617
|
//#endregion
|
|
27744
|
-
export {
|
|
30618
|
+
export { EventCategory as $, notificationRulesCapability as A, createEvent as B, cosineSimilarity as C, faceGalleryCapability as D, embeddingEncoderCapability as E, videoclipsCapability as F, array as G, isDeviceScopedCap as H, zoneAnalyticsCapability as I, number as J, boolean as K, errMsg as L, plateGalleryCapability as M, readDeviceStateFrom as N, hfModelUrl as O, subKindsOf as P, unknown as Q, BaseAddon as R, buildEventKindDescriptor as S, defineCustomActions as T, nodePin as U, hydrateSchema as V, _enum as W, record as X, object as Y, string as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, alarmPanelCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, pipelineAnalyticsCapability as j, kebabToCamel as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, literal as q, EVENT_KIND_BY_CAP as r, NC_TAXONOMY as s, BaseDevice as t, NcRulePatchSchema as u, TimelapseRuleSchema as v, customAction as w, audioMetricsCapability as x, addonWidgetsSourceCapability as y, DeviceType as z };
|