@camstack/addon-post-analysis 1.2.26 → 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-CP-FTAl5.js → dist-Be0WkqVJ.js} +3629 -791
- package/dist/{dist-O227Je-Q.mjs → dist-D7FtjvAJ.mjs} +3617 -815
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-BQUEteve.js → node-C5glqf4X.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-aG07ehD0.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CgiwmstG.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-66T4FwIA.mjs → hostInit-BWwP7qzu.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +505 -3
- package/dist/pipeline-analytics/index.mjs +504 -2
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -6535,6 +6535,109 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
|
|
|
6535
6535
|
function nodePin(nodeId) {
|
|
6536
6536
|
return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
|
|
6537
6537
|
}
|
|
6538
|
+
/**
|
|
6539
|
+
* AUTO-GENERATED by scripts/generate-device-scoped-caps.ts — DO NOT EDIT.
|
|
6540
|
+
*
|
|
6541
|
+
* Every `scope: 'device'` capability name, as plain data — so a forked runner
|
|
6542
|
+
* can answer "may a rule actuate this?" without importing the schema barrel
|
|
6543
|
+
* (~144MB RSS per runner, D28).
|
|
6544
|
+
*
|
|
6545
|
+
* Coverage: 80 device-scoped capabilities.
|
|
6546
|
+
*/
|
|
6547
|
+
var DEVICE_SCOPED_CAPS = new Set([
|
|
6548
|
+
"accessories",
|
|
6549
|
+
"air-quality-sensor",
|
|
6550
|
+
"alarm-panel",
|
|
6551
|
+
"ambient-light-sensor",
|
|
6552
|
+
"audio-analysis",
|
|
6553
|
+
"audio-metrics",
|
|
6554
|
+
"automation-control",
|
|
6555
|
+
"battery",
|
|
6556
|
+
"binary",
|
|
6557
|
+
"brightness",
|
|
6558
|
+
"button",
|
|
6559
|
+
"camera-credentials",
|
|
6560
|
+
"camera-pipeline-config",
|
|
6561
|
+
"camera-streams",
|
|
6562
|
+
"carbon-monoxide",
|
|
6563
|
+
"climate-control",
|
|
6564
|
+
"color",
|
|
6565
|
+
"connectivity",
|
|
6566
|
+
"consumables",
|
|
6567
|
+
"contact",
|
|
6568
|
+
"control",
|
|
6569
|
+
"cover",
|
|
6570
|
+
"day-night",
|
|
6571
|
+
"detection-pipeline",
|
|
6572
|
+
"device-discovery",
|
|
6573
|
+
"device-ops",
|
|
6574
|
+
"device-status",
|
|
6575
|
+
"doorbell",
|
|
6576
|
+
"enum-sensor",
|
|
6577
|
+
"event-emitter",
|
|
6578
|
+
"events",
|
|
6579
|
+
"fan-control",
|
|
6580
|
+
"feature-probe",
|
|
6581
|
+
"flood",
|
|
6582
|
+
"gas",
|
|
6583
|
+
"humidifier",
|
|
6584
|
+
"humidity-sensor",
|
|
6585
|
+
"image",
|
|
6586
|
+
"image-settings",
|
|
6587
|
+
"intercom",
|
|
6588
|
+
"lawn-mower-control",
|
|
6589
|
+
"lock-control",
|
|
6590
|
+
"media-player",
|
|
6591
|
+
"motion",
|
|
6592
|
+
"motion-detection",
|
|
6593
|
+
"motion-trigger",
|
|
6594
|
+
"motion-zones",
|
|
6595
|
+
"native-object-detection",
|
|
6596
|
+
"notifier",
|
|
6597
|
+
"numeric-sensor",
|
|
6598
|
+
"osd",
|
|
6599
|
+
"pet-feeder",
|
|
6600
|
+
"pipeline-analytics",
|
|
6601
|
+
"power-meter",
|
|
6602
|
+
"presence",
|
|
6603
|
+
"pressure-sensor",
|
|
6604
|
+
"privacy-mask",
|
|
6605
|
+
"ptz",
|
|
6606
|
+
"ptz-autotrack",
|
|
6607
|
+
"reboot",
|
|
6608
|
+
"scene-monitor",
|
|
6609
|
+
"script-runner",
|
|
6610
|
+
"smoke",
|
|
6611
|
+
"snapshot",
|
|
6612
|
+
"stream-catalog",
|
|
6613
|
+
"stream-params",
|
|
6614
|
+
"switch",
|
|
6615
|
+
"tamper",
|
|
6616
|
+
"temperature-sensor",
|
|
6617
|
+
"update",
|
|
6618
|
+
"vacuum-control",
|
|
6619
|
+
"valve",
|
|
6620
|
+
"vibration",
|
|
6621
|
+
"videoclips",
|
|
6622
|
+
"water-heater",
|
|
6623
|
+
"weather",
|
|
6624
|
+
"webrtc-session",
|
|
6625
|
+
"zone-analytics",
|
|
6626
|
+
"zone-rules",
|
|
6627
|
+
"zones"
|
|
6628
|
+
]);
|
|
6629
|
+
/**
|
|
6630
|
+
* True when `capName` is a device capability.
|
|
6631
|
+
*
|
|
6632
|
+
* This is the ONLY boundary on what a notification rule may actuate. A rule can
|
|
6633
|
+
* be authored by a non-admin and the runner executes with the addon's
|
|
6634
|
+
* privileges, so an unbounded action would be an arbitrary RPC channel with a
|
|
6635
|
+
* privilege escalation attached. Device scope excludes the system caps
|
|
6636
|
+
* (`device-manager.removeDevice` and friends) by construction.
|
|
6637
|
+
*/
|
|
6638
|
+
function isDeviceScopedCap(capName) {
|
|
6639
|
+
return DEVICE_SCOPED_CAPS.has(capName);
|
|
6640
|
+
}
|
|
6538
6641
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
6539
6642
|
DeviceType["Camera"] = "camera";
|
|
6540
6643
|
DeviceType["Hub"] = "hub";
|
|
@@ -6920,6 +7023,10 @@ function systemMethod(input, output, options) {
|
|
|
6920
7023
|
systemOnly: true
|
|
6921
7024
|
};
|
|
6922
7025
|
}
|
|
7026
|
+
/** Shorthand to define an event schema */
|
|
7027
|
+
function event(data) {
|
|
7028
|
+
return { data };
|
|
7029
|
+
}
|
|
6923
7030
|
var StaticDirOutputSchema$1 = object({ staticDir: string() });
|
|
6924
7031
|
var VersionOutputSchema$1 = object({ version: string() });
|
|
6925
7032
|
method(_void(), StaticDirOutputSchema$1), method(_void(), VersionOutputSchema$1);
|
|
@@ -9102,6 +9209,249 @@ var AccessoryKind = {
|
|
|
9102
9209
|
};
|
|
9103
9210
|
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
9104
9211
|
DeviceFeature.BatteryOperated;
|
|
9212
|
+
var DeviceConfig = class DeviceConfig {
|
|
9213
|
+
schema;
|
|
9214
|
+
data;
|
|
9215
|
+
persistFn;
|
|
9216
|
+
constructor(schema, data, persist) {
|
|
9217
|
+
this.schema = schema;
|
|
9218
|
+
this.data = data;
|
|
9219
|
+
this.persistFn = persist;
|
|
9220
|
+
}
|
|
9221
|
+
/**
|
|
9222
|
+
* Build a `DeviceConfig` from a persisted blob, with automatic
|
|
9223
|
+
* recovery from schema-validation failures. Boot must never be
|
|
9224
|
+
* blocked by stale persisted values: if Zod rejects the blob,
|
|
9225
|
+
* we drop every offending top-level field, retry, and persist
|
|
9226
|
+
* the cleaned blob so the bad value is healed in the DB on next
|
|
9227
|
+
* write. The most common trigger is a tightened range constraint
|
|
9228
|
+
* (e.g. `max(100) → max(50)`) on a field that already has an
|
|
9229
|
+
* out-of-range value persisted from the previous schema. Without
|
|
9230
|
+
* this safety net, the device would fail to instantiate and end
|
|
9231
|
+
* up with no caps registered — exactly the failure mode that
|
|
9232
|
+
* stranded device 15 when `motionSensitivity: 90` no longer fit
|
|
9233
|
+
* the new `1..50` schema.
|
|
9234
|
+
*
|
|
9235
|
+
* Recovery rules:
|
|
9236
|
+
* 1. Try `safeParse(initialData)`. If it succeeds, done.
|
|
9237
|
+
* 2. On failure, walk `error.issues`, collect the top-level path
|
|
9238
|
+
* of each issue, and drop those keys from `initialData`.
|
|
9239
|
+
* 3. Re-run `safeParse`. If the cleaned blob now passes (Zod
|
|
9240
|
+
* fills the missing keys with schema defaults / undefined for
|
|
9241
|
+
* `.optional()`), persist it via `persist()` so the bad
|
|
9242
|
+
* values disappear from the DB, and return the device.
|
|
9243
|
+
* 4. If the cleaned blob STILL fails (very rare — would require
|
|
9244
|
+
* a non-recoverable required field), fall back to
|
|
9245
|
+
* `schema.parse({})` so the device still boots with pure
|
|
9246
|
+
* schema defaults. Persist nothing in that path so the next
|
|
9247
|
+
* successful `setAll` still writes a coherent blob.
|
|
9248
|
+
*/
|
|
9249
|
+
static fromSchema(schema, persist, initialData = {}, onRecover) {
|
|
9250
|
+
const first = schema.safeParse(initialData);
|
|
9251
|
+
if (first.success) return new DeviceConfig(schema, first.data, persist);
|
|
9252
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
9253
|
+
for (const issue of first.error.issues) {
|
|
9254
|
+
const top = issue.path[0];
|
|
9255
|
+
if (typeof top === "string") droppedKeys.add(top);
|
|
9256
|
+
}
|
|
9257
|
+
const cleaned = { ...initialData };
|
|
9258
|
+
for (const k of droppedKeys) delete cleaned[k];
|
|
9259
|
+
const second = schema.safeParse(cleaned);
|
|
9260
|
+
onRecover?.({
|
|
9261
|
+
droppedKeys: [...droppedKeys],
|
|
9262
|
+
issues: first.error.issues
|
|
9263
|
+
});
|
|
9264
|
+
if (second.success) {
|
|
9265
|
+
persist(second.data).catch(() => {});
|
|
9266
|
+
return new DeviceConfig(schema, second.data, persist);
|
|
9267
|
+
}
|
|
9268
|
+
return new DeviceConfig(schema, schema.parse({}), persist);
|
|
9269
|
+
}
|
|
9270
|
+
get values() {
|
|
9271
|
+
return this.data;
|
|
9272
|
+
}
|
|
9273
|
+
get(key) {
|
|
9274
|
+
return this.data[key];
|
|
9275
|
+
}
|
|
9276
|
+
async set(key, value) {
|
|
9277
|
+
const next = this.schema.parse({
|
|
9278
|
+
...this.data,
|
|
9279
|
+
[key]: value
|
|
9280
|
+
});
|
|
9281
|
+
this.data = next;
|
|
9282
|
+
await this.persistFn(this.data);
|
|
9283
|
+
}
|
|
9284
|
+
/**
|
|
9285
|
+
* Merge an untyped patch onto the current config and persist. Accepts
|
|
9286
|
+
* `Record<string, unknown>` because the patch typically comes from the
|
|
9287
|
+
* UI form layer (a `ConfigField.key → value` map) where the caller
|
|
9288
|
+
* doesn't hold the Zod schema's static type. Runtime validation is
|
|
9289
|
+
* authoritative: `this.schema.parse` rejects unknown keys or invalid
|
|
9290
|
+
* shapes before touching storage.
|
|
9291
|
+
*/
|
|
9292
|
+
async setAll(partial) {
|
|
9293
|
+
const next = this.schema.parse({
|
|
9294
|
+
...this.data,
|
|
9295
|
+
...partial
|
|
9296
|
+
});
|
|
9297
|
+
this.data = next;
|
|
9298
|
+
await this.persistFn(this.data);
|
|
9299
|
+
}
|
|
9300
|
+
async deleteKey(key) {
|
|
9301
|
+
const { [key]: _, ...rest } = this.data;
|
|
9302
|
+
const next = this.schema.parse(rest);
|
|
9303
|
+
this.data = next;
|
|
9304
|
+
await this.persistFn(this.data);
|
|
9305
|
+
}
|
|
9306
|
+
entries() {
|
|
9307
|
+
const shape = this.schema.shape;
|
|
9308
|
+
return Object.entries(shape).map(([key, fieldSchema]) => ({
|
|
9309
|
+
key,
|
|
9310
|
+
schema: fieldSchema,
|
|
9311
|
+
value: this.data[key],
|
|
9312
|
+
description: fieldSchema.description
|
|
9313
|
+
}));
|
|
9314
|
+
}
|
|
9315
|
+
};
|
|
9316
|
+
/**
|
|
9317
|
+
* Concrete implementation. Routes every successful write through
|
|
9318
|
+
* `writer(capName, slice)` — the kernel hooks this up to
|
|
9319
|
+
* `device-state.setCapSlice`, the canonical cross-layer write
|
|
9320
|
+
* entrypoint, which handles disk persistence (debounced on the hub)
|
|
9321
|
+
* and mirror updates.
|
|
9322
|
+
*
|
|
9323
|
+
* Schema validation runs in-process before the writer is called —
|
|
9324
|
+
* the round-trip should never carry an invalid slice. `flush()`
|
|
9325
|
+
* awaits any in-flight writer promises so shutdown is lossless.
|
|
9326
|
+
*
|
|
9327
|
+
* `initial` is the persisted blob loaded at boot. Slices for caps
|
|
9328
|
+
* whose schema hasn't been installed yet are kept in-memory verbatim
|
|
9329
|
+
* and validated when the cap registers later.
|
|
9330
|
+
*/
|
|
9331
|
+
var DeviceRuntimeState = class DeviceRuntimeState {
|
|
9332
|
+
writer;
|
|
9333
|
+
/** In-flight writer promises tracked so `flush()` can await them. */
|
|
9334
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
9335
|
+
/** Per-cap committed slice — after schema validation when known. */
|
|
9336
|
+
slices;
|
|
9337
|
+
/** Per-cap registered schema (set by `installCapSchema`). */
|
|
9338
|
+
schemas = /* @__PURE__ */ new Map();
|
|
9339
|
+
listeners = /* @__PURE__ */ new Set();
|
|
9340
|
+
capListeners = /* @__PURE__ */ new Map();
|
|
9341
|
+
constructor(initial, writer) {
|
|
9342
|
+
this.writer = writer;
|
|
9343
|
+
this.slices = /* @__PURE__ */ new Map();
|
|
9344
|
+
for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
|
|
9345
|
+
}
|
|
9346
|
+
static fromInitial(initial, writer) {
|
|
9347
|
+
return new DeviceRuntimeState(initial, writer);
|
|
9348
|
+
}
|
|
9349
|
+
installCapSchema(capName, schema) {
|
|
9350
|
+
const existing = this.schemas.get(capName);
|
|
9351
|
+
if (existing) {
|
|
9352
|
+
if (existing !== schema) throw new Error(`[DeviceRuntimeState] capability "${capName}" registered a different runtime-state schema; each cap must declare ONE shape across every provider`);
|
|
9353
|
+
return;
|
|
9354
|
+
}
|
|
9355
|
+
this.schemas.set(capName, schema);
|
|
9356
|
+
const stored = this.slices.get(capName);
|
|
9357
|
+
if (stored) {
|
|
9358
|
+
const result = schema.safeParse(stored);
|
|
9359
|
+
if (result.success) this.slices.set(capName, result.data);
|
|
9360
|
+
else this.slices.delete(capName);
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9363
|
+
getCapState(capName) {
|
|
9364
|
+
const slice = this.slices.get(capName);
|
|
9365
|
+
if (!slice) return void 0;
|
|
9366
|
+
return Object.freeze({ ...slice });
|
|
9367
|
+
}
|
|
9368
|
+
getCapField(capName, key) {
|
|
9369
|
+
return this.slices.get(capName)?.[key];
|
|
9370
|
+
}
|
|
9371
|
+
setCapState(capName, value) {
|
|
9372
|
+
this.applyCapWrite(capName, value, false);
|
|
9373
|
+
}
|
|
9374
|
+
patchCapState(capName, partial) {
|
|
9375
|
+
this.applyCapWrite(capName, partial, true);
|
|
9376
|
+
}
|
|
9377
|
+
/**
|
|
9378
|
+
* Internal worker. `merge` controls whether `value` replaces or
|
|
9379
|
+
* shallow-merges into the existing slice. Schema validation runs
|
|
9380
|
+
* on the FINAL composed object regardless.
|
|
9381
|
+
*/
|
|
9382
|
+
applyCapWrite(capName, value, merge) {
|
|
9383
|
+
const schema = this.schemas.get(capName);
|
|
9384
|
+
if (!schema) throw new Error(`[DeviceRuntimeState] no schema registered for cap "${capName}" — did the device register it via ctx.registerNativeCap before writing?`);
|
|
9385
|
+
const current = this.slices.get(capName) ?? {};
|
|
9386
|
+
const next = merge ? {
|
|
9387
|
+
...current,
|
|
9388
|
+
...value
|
|
9389
|
+
} : { ...value };
|
|
9390
|
+
const parsed = schema.parse(next);
|
|
9391
|
+
if (shallowEqual(current, parsed)) return;
|
|
9392
|
+
this.slices.set(capName, parsed);
|
|
9393
|
+
this.fireListeners([capName]);
|
|
9394
|
+
const writePromise = this.writer(capName, { ...parsed }).catch(() => {});
|
|
9395
|
+
this.pendingWrites.add(writePromise);
|
|
9396
|
+
writePromise.finally(() => {
|
|
9397
|
+
this.pendingWrites.delete(writePromise);
|
|
9398
|
+
});
|
|
9399
|
+
}
|
|
9400
|
+
fireListeners(changed) {
|
|
9401
|
+
const snap = this.snapshot();
|
|
9402
|
+
for (const cb of this.listeners) try {
|
|
9403
|
+
cb(changed, snap);
|
|
9404
|
+
} catch {}
|
|
9405
|
+
for (const capName of changed) {
|
|
9406
|
+
const subs = this.capListeners.get(capName);
|
|
9407
|
+
if (!subs) continue;
|
|
9408
|
+
const slice = this.getCapState(capName);
|
|
9409
|
+
for (const cb of subs) try {
|
|
9410
|
+
cb(slice);
|
|
9411
|
+
} catch {}
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
subscribe(cb) {
|
|
9415
|
+
this.listeners.add(cb);
|
|
9416
|
+
return () => {
|
|
9417
|
+
this.listeners.delete(cb);
|
|
9418
|
+
};
|
|
9419
|
+
}
|
|
9420
|
+
subscribeCap(capName, cb) {
|
|
9421
|
+
let subs = this.capListeners.get(capName);
|
|
9422
|
+
if (!subs) {
|
|
9423
|
+
subs = /* @__PURE__ */ new Set();
|
|
9424
|
+
this.capListeners.set(capName, subs);
|
|
9425
|
+
}
|
|
9426
|
+
const adapter = (slice) => {
|
|
9427
|
+
cb(slice);
|
|
9428
|
+
};
|
|
9429
|
+
subs.add(adapter);
|
|
9430
|
+
return () => {
|
|
9431
|
+
const set = this.capListeners.get(capName);
|
|
9432
|
+
if (!set) return;
|
|
9433
|
+
set.delete(adapter);
|
|
9434
|
+
if (set.size === 0) this.capListeners.delete(capName);
|
|
9435
|
+
};
|
|
9436
|
+
}
|
|
9437
|
+
snapshot() {
|
|
9438
|
+
const out = {};
|
|
9439
|
+
for (const [k, v] of this.slices) out[k] = Object.freeze({ ...v });
|
|
9440
|
+
return Object.freeze(out);
|
|
9441
|
+
}
|
|
9442
|
+
async flush() {
|
|
9443
|
+
if (this.pendingWrites.size === 0) return;
|
|
9444
|
+
const inflight = [...this.pendingWrites];
|
|
9445
|
+
await Promise.allSettled(inflight);
|
|
9446
|
+
}
|
|
9447
|
+
};
|
|
9448
|
+
function shallowEqual(a, b) {
|
|
9449
|
+
const ak = Object.keys(a);
|
|
9450
|
+
const bk = Object.keys(b);
|
|
9451
|
+
if (ak.length !== bk.length) return false;
|
|
9452
|
+
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
9453
|
+
return true;
|
|
9454
|
+
}
|
|
9105
9455
|
new Set(["devices", "classes"]);
|
|
9106
9456
|
/**
|
|
9107
9457
|
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
@@ -9290,6 +9640,65 @@ var NcZoneConditionSchema = object({
|
|
|
9290
9640
|
/** Quantifier over `ids` — at least one / every one visited. */
|
|
9291
9641
|
match: _enum(["any", "all"]).default("any")
|
|
9292
9642
|
});
|
|
9643
|
+
/**
|
|
9644
|
+
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9645
|
+
* membership lists are OR within the list (spec §2.3).
|
|
9646
|
+
*/
|
|
9647
|
+
/**
|
|
9648
|
+
* What a rule may actuate.
|
|
9649
|
+
*
|
|
9650
|
+
* **No hand-maintained allowlist** (operator decision, and the right one — a
|
|
9651
|
+
* written list of methods is a third parallel map to keep aligned, and this
|
|
9652
|
+
* repo has paid for those). The boundary instead comes from a property the
|
|
9653
|
+
* capabilities already carry: an action may target only a **device-scoped**
|
|
9654
|
+
* capability method.
|
|
9655
|
+
*
|
|
9656
|
+
* That is not decoration. A rule can be authored by a NON-ADMIN — personal
|
|
9657
|
+
* rules are a supported flow — and the executor runs with the addon's
|
|
9658
|
+
* privileges, so an unbounded action is an arbitrary RPC channel with a
|
|
9659
|
+
* privilege escalation attached. Restricting to device scope excludes the
|
|
9660
|
+
* system caps (`device-manager.removeDevice` and friends) by construction,
|
|
9661
|
+
* costs nothing to maintain, and cannot rot: a cap that stops being
|
|
9662
|
+
* device-scoped stops being actuatable in the same change.
|
|
9663
|
+
*
|
|
9664
|
+
* The executor enforces it; {@link NcRuleActionSchema} carries the intent.
|
|
9665
|
+
*/
|
|
9666
|
+
/**
|
|
9667
|
+
* One step of a sequence.
|
|
9668
|
+
*
|
|
9669
|
+
* `wait` is a first-class step rather than a property of the next action: it is
|
|
9670
|
+
* what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
|
|
9671
|
+
* cannot be expressed otherwise.
|
|
9672
|
+
*/
|
|
9673
|
+
var NcRuleActionSchema = discriminatedUnion("kind", [object({
|
|
9674
|
+
kind: literal("wait"),
|
|
9675
|
+
seconds: number().min(0).max(300)
|
|
9676
|
+
}), object({
|
|
9677
|
+
kind: literal("cap"),
|
|
9678
|
+
deviceId: number().int(),
|
|
9679
|
+
/** Capability name, e.g. `alarm-panel`. */
|
|
9680
|
+
cap: string().min(1),
|
|
9681
|
+
/** Method on it. The executor refuses a non-device-scoped cap. */
|
|
9682
|
+
method: string().min(1),
|
|
9683
|
+
/** Method arguments, minus `deviceId` (the executor injects it). */
|
|
9684
|
+
args: record(string(), unknown()).optional()
|
|
9685
|
+
})]);
|
|
9686
|
+
/**
|
|
9687
|
+
* Sequences a rule runs, by hook point.
|
|
9688
|
+
*
|
|
9689
|
+
* ONLY `onTrigger` is here, deliberately. The reference also has activation /
|
|
9690
|
+
* deactivation / reset / post-generation hooks, and they are wanted — but this
|
|
9691
|
+
* repo's expensive failure mode is declaring a surface nothing produces, so a
|
|
9692
|
+
* hook appears here in the same change that produces its edge, never before.
|
|
9693
|
+
*/
|
|
9694
|
+
var NcRuleActionsSchema = object({
|
|
9695
|
+
/** Runs when the rule MATCHES. */
|
|
9696
|
+
onTrigger: array(object({
|
|
9697
|
+
name: string().min(1).max(120),
|
|
9698
|
+
enabled: boolean(),
|
|
9699
|
+
minDelaySec: number().int().min(0).max(86400).optional(),
|
|
9700
|
+
actions: array(NcRuleActionSchema).min(1)
|
|
9701
|
+
})).optional() });
|
|
9293
9702
|
var NcConditionsSchema = object({
|
|
9294
9703
|
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
9295
9704
|
deviceState: object({
|
|
@@ -9590,7 +9999,16 @@ var NcRuleInputSchema = object({
|
|
|
9590
9999
|
* read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
|
|
9591
10000
|
* by this flag — see the scope rules on that function.
|
|
9592
10001
|
*/
|
|
9593
|
-
snoozeAllowGlobal: boolean().optional()
|
|
10002
|
+
snoozeAllowGlobal: boolean().optional(),
|
|
10003
|
+
/**
|
|
10004
|
+
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
10005
|
+
*
|
|
10006
|
+
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
10007
|
+
* being a special case: arming is
|
|
10008
|
+
* `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
|
|
10009
|
+
* shape as every other actuation.
|
|
10010
|
+
*/
|
|
10011
|
+
actions: NcRuleActionsSchema.optional()
|
|
9594
10012
|
});
|
|
9595
10013
|
/**
|
|
9596
10014
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -10388,10 +10806,25 @@ var DeviceStatusSchema = object({
|
|
|
10388
10806
|
* apart "just came online" from "still online". */
|
|
10389
10807
|
lastChangedAt: number()
|
|
10390
10808
|
});
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10809
|
+
var deviceStatusCapability = {
|
|
10810
|
+
name: "device-status",
|
|
10811
|
+
scope: "device",
|
|
10812
|
+
deviceNative: true,
|
|
10813
|
+
mode: "singleton",
|
|
10814
|
+
methods: {},
|
|
10815
|
+
events: {
|
|
10816
|
+
/** Emitted when `online` transitions. Mirrors the semantics of
|
|
10817
|
+
* `battery.onStatusChanged`. */
|
|
10818
|
+
onStatusChanged: { data: object({
|
|
10819
|
+
deviceId: number(),
|
|
10820
|
+
status: DeviceStatusSchema
|
|
10821
|
+
}) } },
|
|
10822
|
+
status: {
|
|
10823
|
+
schema: DeviceStatusSchema,
|
|
10824
|
+
kind: "push"
|
|
10825
|
+
},
|
|
10826
|
+
runtimeState: DeviceStatusSchema
|
|
10827
|
+
};
|
|
10395
10828
|
/**
|
|
10396
10829
|
* Per-device feature/identity probe slice. Holds the runtime-resolved
|
|
10397
10830
|
* truth about what a device CAN do — which the kernel uses to:
|
|
@@ -10449,11 +10882,35 @@ var FeatureProbeStatusSchema = object({
|
|
|
10449
10882
|
*/
|
|
10450
10883
|
lastFetchedAt: number()
|
|
10451
10884
|
});
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10885
|
+
var featureProbeCapability = {
|
|
10886
|
+
name: "feature-probe",
|
|
10887
|
+
scope: "device",
|
|
10888
|
+
deviceNative: true,
|
|
10889
|
+
mode: "singleton",
|
|
10890
|
+
methods: {},
|
|
10891
|
+
events: {
|
|
10892
|
+
/** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
|
|
10893
|
+
* or driver-initiated re-detect after a state change). */
|
|
10894
|
+
onProbeChanged: { data: object({
|
|
10895
|
+
deviceId: number(),
|
|
10896
|
+
status: FeatureProbeStatusSchema
|
|
10897
|
+
}) } },
|
|
10898
|
+
status: {
|
|
10899
|
+
schema: FeatureProbeStatusSchema,
|
|
10900
|
+
kind: "push"
|
|
10901
|
+
},
|
|
10902
|
+
runtimeState: FeatureProbeStatusSchema
|
|
10903
|
+
};
|
|
10904
|
+
/**
|
|
10905
|
+
* Multi-metric air-quality slice. Covers CO₂, total VOCs, particulate
|
|
10906
|
+
* matter at PM2.5 / PM10, and a derived AQI index — all optional so
|
|
10907
|
+
* a single-metric source populates only what it observes. Mirrors
|
|
10908
|
+
* the HA `sensor` device_class set (`co2`, `volatile_organic_compounds`,
|
|
10909
|
+
* `pm25`, `pm10`, `aqi`) collapsed into one cap because a typical
|
|
10910
|
+
* air-quality node reports several of these together; modelling them
|
|
10911
|
+
* as siblings keeps a single timestamp + one slice subscription.
|
|
10912
|
+
*/
|
|
10913
|
+
var AirQualitySensorStatusSchema = object({
|
|
10457
10914
|
/** Carbon dioxide concentration in ppm. */
|
|
10458
10915
|
co2Ppm: number().min(0).optional(),
|
|
10459
10916
|
/** Total volatile organic compounds in ppb. */
|
|
@@ -10477,7 +10934,19 @@ object({
|
|
|
10477
10934
|
* auto-formatting when absent. */
|
|
10478
10935
|
precision: number().int().min(0).max(10).optional()
|
|
10479
10936
|
});
|
|
10480
|
-
|
|
10937
|
+
var airQualitySensorCapability = {
|
|
10938
|
+
name: "air-quality-sensor",
|
|
10939
|
+
scope: "device",
|
|
10940
|
+
deviceNative: true,
|
|
10941
|
+
mode: "singleton",
|
|
10942
|
+
deviceTypes: [DeviceType.Sensor],
|
|
10943
|
+
methods: {},
|
|
10944
|
+
status: {
|
|
10945
|
+
schema: AirQualitySensorStatusSchema,
|
|
10946
|
+
kind: "push"
|
|
10947
|
+
},
|
|
10948
|
+
runtimeState: AirQualitySensorStatusSchema
|
|
10949
|
+
};
|
|
10481
10950
|
/**
|
|
10482
10951
|
* Alarm-panel cap. Models HA `alarm_control_panel.*` on
|
|
10483
10952
|
* `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
|
|
@@ -10513,7 +10982,7 @@ var AlarmArmModeSchema = _enum([
|
|
|
10513
10982
|
"vacation",
|
|
10514
10983
|
"custom_bypass"
|
|
10515
10984
|
]);
|
|
10516
|
-
object({
|
|
10985
|
+
var AlarmPanelStatusSchema = object({
|
|
10517
10986
|
/** Current lifecycle state. */
|
|
10518
10987
|
state: AlarmStateSchema,
|
|
10519
10988
|
/** Subset of arm modes the panel accepts. UI renders one button per
|
|
@@ -10525,26 +10994,58 @@ object({
|
|
|
10525
10994
|
/** Ms epoch when the slice was last updated. */
|
|
10526
10995
|
lastChangedAt: number()
|
|
10527
10996
|
});
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10997
|
+
var alarmPanelCapability = {
|
|
10998
|
+
name: "alarm-panel",
|
|
10999
|
+
scope: "device",
|
|
11000
|
+
deviceNative: true,
|
|
11001
|
+
mode: "singleton",
|
|
11002
|
+
deviceTypes: [DeviceType.AlarmPanel],
|
|
11003
|
+
methods: {
|
|
11004
|
+
arm: method(object({
|
|
11005
|
+
deviceId: number().int().nonnegative(),
|
|
11006
|
+
mode: AlarmArmModeSchema,
|
|
11007
|
+
/** Optional PIN code. Required when `requiresCode === true`.
|
|
11008
|
+
* Passed through to the upstream service; never persisted. */
|
|
11009
|
+
code: string().min(1).optional()
|
|
11010
|
+
}), _void(), {
|
|
11011
|
+
kind: "mutation",
|
|
11012
|
+
auth: "admin"
|
|
11013
|
+
}),
|
|
11014
|
+
disarm: method(object({
|
|
11015
|
+
deviceId: number().int().nonnegative(),
|
|
11016
|
+
code: string().min(1).optional()
|
|
11017
|
+
}), _void(), {
|
|
11018
|
+
kind: "mutation",
|
|
11019
|
+
auth: "admin"
|
|
11020
|
+
}),
|
|
11021
|
+
/**
|
|
11022
|
+
* Force the panel into the `triggered` state — used by HA
|
|
11023
|
+
* automations to surface external sensor events through the panel
|
|
11024
|
+
* (e.g. a Reolink camera intrusion event firing the security
|
|
11025
|
+
* system). Provider rejects when the panel hardware doesn't
|
|
11026
|
+
* support a software-initiated trigger.
|
|
11027
|
+
*/
|
|
11028
|
+
trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11029
|
+
kind: "mutation",
|
|
11030
|
+
auth: "admin"
|
|
11031
|
+
})
|
|
11032
|
+
},
|
|
11033
|
+
status: {
|
|
11034
|
+
schema: AlarmPanelStatusSchema,
|
|
11035
|
+
kind: "push"
|
|
11036
|
+
},
|
|
11037
|
+
/**
|
|
11038
|
+
* Runtime-state slice — mirrored by the kernel. UI panel reads the
|
|
11039
|
+
* full slice; renders an arm button per `availableModes` entry and
|
|
11040
|
+
* a PIN field iff `requiresCode === true`.
|
|
11041
|
+
*/
|
|
11042
|
+
runtimeState: AlarmPanelStatusSchema
|
|
11043
|
+
};
|
|
11044
|
+
/**
|
|
11045
|
+
* Ambient illuminance reading in lux. Drives Home Assistant `sensor`
|
|
11046
|
+
* entries with `device_class: illuminance`.
|
|
11047
|
+
*/
|
|
11048
|
+
var AmbientLightSensorStatusSchema = object({
|
|
10548
11049
|
/** Current illuminance in lux (lx). */
|
|
10549
11050
|
lux: number().min(0),
|
|
10550
11051
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -10559,7 +11060,19 @@ object({
|
|
|
10559
11060
|
* auto-formatting when absent. */
|
|
10560
11061
|
precision: number().int().min(0).max(10).optional()
|
|
10561
11062
|
});
|
|
10562
|
-
|
|
11063
|
+
var ambientLightSensorCapability = {
|
|
11064
|
+
name: "ambient-light-sensor",
|
|
11065
|
+
scope: "device",
|
|
11066
|
+
deviceNative: true,
|
|
11067
|
+
mode: "singleton",
|
|
11068
|
+
deviceTypes: [DeviceType.Sensor],
|
|
11069
|
+
methods: {},
|
|
11070
|
+
status: {
|
|
11071
|
+
schema: AmbientLightSensorStatusSchema,
|
|
11072
|
+
kind: "push"
|
|
11073
|
+
},
|
|
11074
|
+
runtimeState: AmbientLightSensorStatusSchema
|
|
11075
|
+
};
|
|
10563
11076
|
/**
|
|
10564
11077
|
* Per-class audio metrics aggregated over a sliding window.
|
|
10565
11078
|
*/
|
|
@@ -10678,7 +11191,18 @@ var audioMetricsCapability = {
|
|
|
10678
11191
|
/** Reactive runtime-state mirror — live `device.state.audioMetrics.value`. */
|
|
10679
11192
|
runtimeState: AudioMetricsSnapshotSchema
|
|
10680
11193
|
};
|
|
10681
|
-
|
|
11194
|
+
/**
|
|
11195
|
+
* Automation-control cap. Models HA `automation.*` entities on
|
|
11196
|
+
* `DeviceType.Automation`. An automation is a trigger+condition+
|
|
11197
|
+
* action rule that can be enabled / disabled and manually fired
|
|
11198
|
+
* via the `trigger` method.
|
|
11199
|
+
*
|
|
11200
|
+
* `trigger` accepts an optional `skipCondition` flag — when true,
|
|
11201
|
+
* the automation's action block runs WITHOUT evaluating its
|
|
11202
|
+
* condition block. Pair with `DeviceFeature.AutomationSkipCondition`
|
|
11203
|
+
* to gate the UI checkbox for the manual-trigger dialog.
|
|
11204
|
+
*/
|
|
11205
|
+
var AutomationControlStatusSchema = object({
|
|
10682
11206
|
/** Whether the automation is currently enabled. Disabled automations
|
|
10683
11207
|
* ignore their trigger block — manual `trigger` still works. */
|
|
10684
11208
|
enabled: boolean(),
|
|
@@ -10692,22 +11216,43 @@ object({
|
|
|
10692
11216
|
/** Ms epoch when the slice was last updated. */
|
|
10693
11217
|
lastChangedAt: number()
|
|
10694
11218
|
});
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
11219
|
+
var automationControlCapability = {
|
|
11220
|
+
name: "automation-control",
|
|
11221
|
+
scope: "device",
|
|
11222
|
+
deviceNative: true,
|
|
11223
|
+
mode: "singleton",
|
|
11224
|
+
deviceTypes: [DeviceType.Automation],
|
|
11225
|
+
methods: {
|
|
11226
|
+
enable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11227
|
+
kind: "mutation",
|
|
11228
|
+
auth: "admin"
|
|
11229
|
+
}),
|
|
11230
|
+
disable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
11231
|
+
kind: "mutation",
|
|
11232
|
+
auth: "admin"
|
|
11233
|
+
}),
|
|
11234
|
+
trigger: method(object({
|
|
11235
|
+
deviceId: number().int().nonnegative(),
|
|
11236
|
+
/** When true, fires the action block while bypassing the
|
|
11237
|
+
* automation's condition evaluation. Gated by
|
|
11238
|
+
* `DeviceFeature.AutomationSkipCondition`. */
|
|
11239
|
+
skipCondition: boolean().optional()
|
|
11240
|
+
}), _void(), {
|
|
11241
|
+
kind: "mutation",
|
|
11242
|
+
auth: "admin"
|
|
11243
|
+
})
|
|
11244
|
+
},
|
|
11245
|
+
status: {
|
|
11246
|
+
schema: AutomationControlStatusSchema,
|
|
11247
|
+
kind: "push"
|
|
11248
|
+
},
|
|
11249
|
+
/**
|
|
11250
|
+
* Runtime-state slice — mirrored by the kernel. UI automation tile
|
|
11251
|
+
* reads `enabled` (toggle) + `isRunning` (spinner) + `lastError`
|
|
11252
|
+
* (badge) directly.
|
|
11253
|
+
*/
|
|
11254
|
+
runtimeState: AutomationControlStatusSchema
|
|
11255
|
+
};
|
|
10711
11256
|
/**
|
|
10712
11257
|
* Battery status snapshot. Emitted by providers whose device is
|
|
10713
11258
|
* battery-operated (cameras with `DeviceFeature.BatteryOperated`,
|
|
@@ -10746,40 +11291,156 @@ var BatteryStatusSchema = object({
|
|
|
10746
11291
|
*/
|
|
10747
11292
|
binary: boolean().optional()
|
|
10748
11293
|
});
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
11294
|
+
var batteryCapability = {
|
|
11295
|
+
name: "battery",
|
|
11296
|
+
scope: "device",
|
|
11297
|
+
deviceNative: true,
|
|
11298
|
+
mode: "singleton",
|
|
11299
|
+
deviceTypes: [
|
|
11300
|
+
DeviceType.Camera,
|
|
11301
|
+
DeviceType.Sensor,
|
|
11302
|
+
DeviceType.Button,
|
|
11303
|
+
DeviceType.Switch
|
|
11304
|
+
],
|
|
11305
|
+
methods: {
|
|
11306
|
+
/**
|
|
11307
|
+
* Explicitly wake the camera from low-power sleep ahead of a
|
|
11308
|
+
* streaming session start. Consumers that initiate a stream
|
|
11309
|
+
* against a sleeping battery cam (HomeKit Secure Video, Alexa
|
|
11310
|
+
* RTCSession, snapshot wrappers) call this with a short timeout
|
|
11311
|
+
* before establishing the media pipeline — the broker's own
|
|
11312
|
+
* passive wake-on-dial works but adds 5–7 seconds to first-frame,
|
|
11313
|
+
* during which the consumer renders a black screen. Pre-waking
|
|
11314
|
+
* compresses that gap.
|
|
11315
|
+
*
|
|
11316
|
+
* Returns `awoke: true` when the firmware acknowledged the wake
|
|
11317
|
+
* before `timeoutMs`. Returns `awoke: false` when it timed out OR
|
|
11318
|
+
* the cap surface is unavailable (no Baichuan / firmware
|
|
11319
|
+
* channel); the caller should still attempt the stream — the
|
|
11320
|
+
* passive broker wake remains as fallback.
|
|
11321
|
+
*/
|
|
11322
|
+
wakeForStream: method(object({
|
|
11323
|
+
deviceId: number(),
|
|
11324
|
+
/** Bound on the wait. Sensible range 3000–10000ms. */
|
|
11325
|
+
timeoutMs: number().int().min(500).max(3e4).default(8e3)
|
|
11326
|
+
}), object({
|
|
11327
|
+
awoke: boolean(),
|
|
11328
|
+
durationMs: number()
|
|
11329
|
+
}), { kind: "mutation" }) },
|
|
11330
|
+
events: {
|
|
11331
|
+
/**
|
|
11332
|
+
* Emitted whenever the cached status changes (firmware push OR
|
|
11333
|
+
* poll observes a delta). The DeviceEventPropagator mirrors this
|
|
11334
|
+
* event on the parent chain — subscribing to a camera's source
|
|
11335
|
+
* receives battery events from child accessories automatically.
|
|
11336
|
+
*/
|
|
11337
|
+
onStatusChanged: { data: object({
|
|
11338
|
+
deviceId: number(),
|
|
11339
|
+
status: BatteryStatusSchema
|
|
11340
|
+
}) } },
|
|
11341
|
+
status: {
|
|
11342
|
+
schema: BatteryStatusSchema,
|
|
11343
|
+
kind: "push",
|
|
11344
|
+
empty: {
|
|
11345
|
+
percentage: 0,
|
|
11346
|
+
charging: "none",
|
|
11347
|
+
sleeping: false,
|
|
11348
|
+
lastUpdated: 0
|
|
11349
|
+
}
|
|
11350
|
+
},
|
|
11351
|
+
/**
|
|
11352
|
+
* Runtime-state slice — every provider that registers this cap
|
|
11353
|
+
* stores the same shape under `device.runtimeState[battery]`.
|
|
11354
|
+
* Cross-provider uniformity: a Reolink Argus, a Frigate sensor
|
|
11355
|
+
* proxy, an ONVIF battery cam all read/write the same keys.
|
|
11356
|
+
* Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
|
|
11357
|
+
* via `device.runtimeState.getCapState('battery')` regardless of
|
|
11358
|
+
* the underlying driver.
|
|
11359
|
+
*/
|
|
11360
|
+
runtimeState: BatteryStatusSchema
|
|
11361
|
+
};
|
|
11362
|
+
/**
|
|
11363
|
+
* Generic boolean sensor — last-resort fallback when no domain-
|
|
11364
|
+
* specific binary cap fits (Home Assistant `binary_sensor` without a
|
|
11365
|
+
* known `device_class`, or a domain we haven't typed yet). Pure
|
|
11366
|
+
* pass-through: just the bool + timestamp. Push-driven.
|
|
11367
|
+
*
|
|
11368
|
+
* Prefer the typed alternatives (`contact`, `flood`, `smoke`,
|
|
11369
|
+
* `carbon-monoxide`, `gas`, `tamper`, `vibration`, `connectivity`,
|
|
11370
|
+
* `motion`) when the semantics match — export adapters render those
|
|
11371
|
+
* with the right HomeKit / Alexa display category.
|
|
11372
|
+
*/
|
|
11373
|
+
var BinaryStatusSchema = object({
|
|
10761
11374
|
on: boolean(),
|
|
10762
11375
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
10763
11376
|
lastChangedAt: number()
|
|
10764
11377
|
});
|
|
10765
|
-
|
|
10766
|
-
|
|
11378
|
+
var binaryCapability = {
|
|
11379
|
+
name: "binary",
|
|
11380
|
+
scope: "device",
|
|
11381
|
+
deviceNative: true,
|
|
11382
|
+
mode: "singleton",
|
|
11383
|
+
deviceTypes: [DeviceType.Sensor],
|
|
11384
|
+
methods: {},
|
|
11385
|
+
status: {
|
|
11386
|
+
schema: BinaryStatusSchema,
|
|
11387
|
+
kind: "push"
|
|
11388
|
+
},
|
|
11389
|
+
runtimeState: BinaryStatusSchema
|
|
11390
|
+
};
|
|
11391
|
+
/**
|
|
11392
|
+
* Dimmable-light brightness control. Co-exists with `switch` on the
|
|
11393
|
+
* same device — the switch toggles on/off, this cap sets the level
|
|
11394
|
+
* applied when the light is on. Drivers map their per-vendor dim
|
|
11395
|
+
* controls to this single-method surface.
|
|
11396
|
+
*
|
|
11397
|
+
* The cap is intentionally minimal: a single `setBrightness({deviceId,
|
|
11398
|
+
* percentage})` mutation plus the auto-injected `getStatus`. Drivers
|
|
11399
|
+
* that expose richer controls (color temperature, scenes, schedules)
|
|
11400
|
+
* should surface those via the device's `getSettingsUISchema()`
|
|
11401
|
+
* instead of bloating this cap.
|
|
11402
|
+
*/
|
|
11403
|
+
var BrightnessStatusSchema = object({
|
|
10767
11404
|
/** Current level as 0..100 inclusive. Firmware-reported. */
|
|
10768
11405
|
percentage: number().min(0).max(100),
|
|
10769
11406
|
/** Ms epoch of the last operator-driven change. Useful for UI freshness. */
|
|
10770
11407
|
lastChangedAt: number()
|
|
10771
11408
|
});
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
11409
|
+
var brightnessCapability = {
|
|
11410
|
+
name: "brightness",
|
|
11411
|
+
scope: "device",
|
|
11412
|
+
deviceNative: true,
|
|
11413
|
+
mode: "singleton",
|
|
11414
|
+
deviceTypes: [DeviceType.Light],
|
|
11415
|
+
methods: { setBrightness: method(object({
|
|
11416
|
+
deviceId: number().int().nonnegative(),
|
|
11417
|
+
percentage: number().min(0).max(100)
|
|
11418
|
+
}), _void(), {
|
|
11419
|
+
kind: "mutation",
|
|
11420
|
+
auth: "admin"
|
|
11421
|
+
}) },
|
|
11422
|
+
events: {
|
|
11423
|
+
/**
|
|
11424
|
+
* Emitted whenever the brightness changes — operator action OR
|
|
11425
|
+
* firmware push. Subscribers (UI sliders, automation engines) react
|
|
11426
|
+
* without polling.
|
|
11427
|
+
*/
|
|
11428
|
+
onBrightnessChanged: { data: object({
|
|
11429
|
+
deviceId: number(),
|
|
11430
|
+
percentage: number().min(0).max(100),
|
|
11431
|
+
lastChangedAt: number()
|
|
11432
|
+
}) } },
|
|
11433
|
+
status: {
|
|
11434
|
+
schema: BrightnessStatusSchema,
|
|
11435
|
+
kind: "command-driven"
|
|
11436
|
+
},
|
|
11437
|
+
/**
|
|
11438
|
+
* Runtime-state slice — the last applied brightness level, mirrored
|
|
11439
|
+
* by the kernel. Read via `device.state.brightness.value` so UI
|
|
11440
|
+
* sliders surface the current level without polling the provider.
|
|
11441
|
+
*/
|
|
11442
|
+
runtimeState: BrightnessStatusSchema
|
|
11443
|
+
};
|
|
10783
11444
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
10784
11445
|
var StreamFormatSchema = _enum([
|
|
10785
11446
|
"webrtc",
|
|
@@ -11199,67 +11860,178 @@ var PickedCamStreamSchema = object({
|
|
|
11199
11860
|
/** One-line explanation of why this stream won — for logs / debug UI. */
|
|
11200
11861
|
reason: string()
|
|
11201
11862
|
});
|
|
11202
|
-
DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(CameraStreamSchema).readonly()), method(object({ deviceId: number().int().nonnegative() }), array(ProfileSlotSchema).readonly()), method(object({
|
|
11203
|
-
deviceId: number().int().nonnegative(),
|
|
11204
|
-
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11205
|
-
hostname: string().optional()
|
|
11206
|
-
}), array(RtspRestreamEntrySchema).readonly()), method(object({
|
|
11207
|
-
deviceId: number().int().nonnegative(),
|
|
11208
|
-
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11209
|
-
hostname: string().optional()
|
|
11210
|
-
}), array(ProfileRtspEntrySchema).readonly()), method(object({
|
|
11211
|
-
deviceId: number().int().nonnegative(),
|
|
11212
|
-
requirements: PickStreamRequirementsSchema,
|
|
11213
|
-
preferences: PickStreamPreferencesSchema.optional()
|
|
11214
|
-
}), PickedCamStreamSchema.nullable()), object({
|
|
11215
|
-
deviceId: number().int().nonnegative(),
|
|
11216
|
-
camStreams: array(CameraStreamSchema).readonly()
|
|
11217
|
-
}), object({
|
|
11218
|
-
deviceId: number().int().nonnegative(),
|
|
11219
|
-
profileSlots: array(ProfileSlotSchema).readonly()
|
|
11220
|
-
}), object({
|
|
11221
|
-
online: boolean(),
|
|
11222
|
-
slotStatuses: object({
|
|
11223
|
-
high: ProfileSlotStatusSchema.optional(),
|
|
11224
|
-
mid: ProfileSlotStatusSchema.optional(),
|
|
11225
|
-
low: ProfileSlotStatusSchema.optional()
|
|
11226
|
-
}),
|
|
11227
|
-
slotErrors: object({
|
|
11228
|
-
high: string().optional(),
|
|
11229
|
-
mid: string().optional(),
|
|
11230
|
-
low: string().optional()
|
|
11231
|
-
}),
|
|
11232
|
-
lastChangedAt: number()
|
|
11233
|
-
});
|
|
11234
|
-
object({
|
|
11235
|
-
detected: boolean(),
|
|
11236
|
-
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11237
|
-
lastChangedAt: number()
|
|
11238
|
-
});
|
|
11239
|
-
DeviceType.Sensor;
|
|
11240
11863
|
/**
|
|
11241
|
-
*
|
|
11242
|
-
* `climate.*` entity (thermostat, A/C, heat pump, dehumidifier)
|
|
11243
|
-
* with a single coherent slice — mode + fan + preset + target +
|
|
11244
|
-
* dual-setpoint range + humidity + read-only current readings.
|
|
11864
|
+
* Camera streams — device-scoped facade over the system `stream-broker`.
|
|
11245
11865
|
*
|
|
11246
|
-
*
|
|
11247
|
-
*
|
|
11248
|
-
*
|
|
11249
|
-
*
|
|
11250
|
-
*
|
|
11251
|
-
*
|
|
11252
|
-
*
|
|
11253
|
-
* use vastly different vocabularies (`auto`/`low`/`high` vs
|
|
11254
|
-
* `quiet`/`normal`/`turbo`; `eco`/`away`/`sleep` vs `home`/`night`).
|
|
11255
|
-
* The provider populates `availableFanModes` / `availablePresets`
|
|
11256
|
-
* on the slice so the UI renders a closed-list picker.
|
|
11866
|
+
* Mirrors the slice of broker state relevant to a single device:
|
|
11867
|
+
* - `getCameraStreams()`: the pool of physical streams published for
|
|
11868
|
+
* this device. UI uses it to populate the "Camera Stream" dropdown
|
|
11869
|
+
* under each quality section.
|
|
11870
|
+
* - `getBrokerStreams()`: the (up to 3) profile slots `high/mid/low`
|
|
11871
|
+
* with their current assignment + runtime status. UI uses it for
|
|
11872
|
+
* the WebRTC quality picker, recording target selection, etc.
|
|
11257
11873
|
*
|
|
11258
|
-
*
|
|
11259
|
-
*
|
|
11260
|
-
*
|
|
11261
|
-
*
|
|
11262
|
-
*
|
|
11874
|
+
* Registered for every camera device that has at least one published
|
|
11875
|
+
* cam stream. The provider is owned by the stream-broker addon; reads
|
|
11876
|
+
* go against the broker's in-memory registries. Mutations (assign /
|
|
11877
|
+
* unassign / publish / retract) do NOT live here — they stay on the
|
|
11878
|
+
* system `stream-broker` cap so cross-device / addon-driven flows keep
|
|
11879
|
+
* a single namespace.
|
|
11880
|
+
*/
|
|
11881
|
+
var cameraStreamsCapability = {
|
|
11882
|
+
name: "camera-streams",
|
|
11883
|
+
scope: "device",
|
|
11884
|
+
mode: "singleton",
|
|
11885
|
+
kind: "wrapper",
|
|
11886
|
+
defaultActive: true,
|
|
11887
|
+
deviceTypes: [DeviceType.Camera],
|
|
11888
|
+
methods: {
|
|
11889
|
+
getCameraStreams: method(object({ deviceId: number().int().nonnegative() }), array(CameraStreamSchema).readonly()),
|
|
11890
|
+
getBrokerStreams: method(object({ deviceId: number().int().nonnegative() }), array(ProfileSlotSchema).readonly()),
|
|
11891
|
+
/**
|
|
11892
|
+
* Per-device RAW RTSP restream entries — one per published camStream
|
|
11893
|
+
* that has RTSP restream enabled (`native:main`, `rtsp:sub`, …).
|
|
11894
|
+
*
|
|
11895
|
+
* LIVE-VIEW ONLY. This is the surface the device-details stream
|
|
11896
|
+
* picker uses so an operator can hit each physical stream directly.
|
|
11897
|
+
* Programmatic / external consumers (HAP, Alexa, ha-mqtt, recording)
|
|
11898
|
+
* MUST use `getProfileRtspEntries` instead — picking from raw
|
|
11899
|
+
* variants makes two consumers of the same camera land on two
|
|
11900
|
+
* different physical pulls (e.g. Reolink `native:main` vs
|
|
11901
|
+
* `rtsp:main`) and trips the camera's concurrent-session limit.
|
|
11902
|
+
*/
|
|
11903
|
+
getRtspEntries: method(object({
|
|
11904
|
+
deviceId: number().int().nonnegative(),
|
|
11905
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11906
|
+
hostname: string().optional()
|
|
11907
|
+
}), array(RtspRestreamEntrySchema).readonly()),
|
|
11908
|
+
/**
|
|
11909
|
+
* Per-device PROFILE RTSP restream entries — one per ASSIGNED
|
|
11910
|
+
* profile slot (high/mid/low). Each entry's `url` is a profile-keyed
|
|
11911
|
+
* broker restream that aliases the profile's assigned source broker,
|
|
11912
|
+
* so HAP / Alexa / recording / WebRTC all converge on the broker's
|
|
11913
|
+
* single on-demand pull for that profile. This is the supported
|
|
11914
|
+
* exporter-facing surface; the raw `getRtspEntries` is live-view
|
|
11915
|
+
* only. Returns `[]` for a device with no assigned profiles
|
|
11916
|
+
* (cold-start before first publish).
|
|
11917
|
+
*/
|
|
11918
|
+
getProfileRtspEntries: method(object({
|
|
11919
|
+
deviceId: number().int().nonnegative(),
|
|
11920
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
11921
|
+
hostname: string().optional()
|
|
11922
|
+
}), array(ProfileRtspEntrySchema).readonly()),
|
|
11923
|
+
/**
|
|
11924
|
+
* "Best source stream for these decode constraints". Returns the
|
|
11925
|
+
* camStreamId the caller should dial (or null when no stream
|
|
11926
|
+
* matches). See `PickStreamRequirementsSchema` for the filter shape
|
|
11927
|
+
* and `PickStreamPreferencesSchema` for the ranking inputs.
|
|
11928
|
+
*
|
|
11929
|
+
* Returning null instructs the caller to fall back to its existing
|
|
11930
|
+
* path (derived-broker transcode, profile-slot pick, etc.) — the
|
|
11931
|
+
* picker NEVER ranks `derived:*` candidates as a "match" because
|
|
11932
|
+
* its whole job is to avoid the transcode.
|
|
11933
|
+
*/
|
|
11934
|
+
pickStream: method(object({
|
|
11935
|
+
deviceId: number().int().nonnegative(),
|
|
11936
|
+
requirements: PickStreamRequirementsSchema,
|
|
11937
|
+
preferences: PickStreamPreferencesSchema.optional()
|
|
11938
|
+
}), PickedCamStreamSchema.nullable())
|
|
11939
|
+
},
|
|
11940
|
+
events: {
|
|
11941
|
+
/** Fires on publishCameraStream / retractCameraStream. */
|
|
11942
|
+
onCamStreamsChanged: event(object({
|
|
11943
|
+
deviceId: number().int().nonnegative(),
|
|
11944
|
+
camStreams: array(CameraStreamSchema).readonly()
|
|
11945
|
+
})),
|
|
11946
|
+
/** Fires on assignProfile / unassignProfile / runtime status change. */
|
|
11947
|
+
onProfileSlotsChanged: event(object({
|
|
11948
|
+
deviceId: number().int().nonnegative(),
|
|
11949
|
+
profileSlots: array(ProfileSlotSchema).readonly()
|
|
11950
|
+
}))
|
|
11951
|
+
},
|
|
11952
|
+
/**
|
|
11953
|
+
* Per-device live stream-broker state. Persistent settings (RTSP
|
|
11954
|
+
* tokens, profile assignments, pre-buffer config, RTSP-enabled toggles,
|
|
11955
|
+
* streamingDebug) stay in the broker's addon store — they survive
|
|
11956
|
+
* restarts. The slice below carries ONLY what's truly runtime:
|
|
11957
|
+
*
|
|
11958
|
+
* - `online` — at least one profile slot is currently `'streaming'`.
|
|
11959
|
+
* Drivers without a firmware liveness signal (RTSP, ONVIF…) can
|
|
11960
|
+
* subscribe and mirror this into `state.deviceStatus.online`.
|
|
11961
|
+
* - `slotStatuses` — current `ProfileSlotStatus` per profile,
|
|
11962
|
+
* mirroring the runtime-mutable subset of `ProfileSlot`.
|
|
11963
|
+
* - `slotErrors` — last error message per profile (only set when the
|
|
11964
|
+
* corresponding slot is in `'error'`).
|
|
11965
|
+
* - `lastChangedAt` — freshness signal for consumers that want to
|
|
11966
|
+
* reason about how stale the slice is.
|
|
11967
|
+
*
|
|
11968
|
+
* Written by the stream-broker manager on every transition that
|
|
11969
|
+
* affects these aggregates. Read via `device.state.cameraStreams.<field>`
|
|
11970
|
+
* (BaseDevice proxy) or, cross-process, via
|
|
11971
|
+
* `device-state.getCapSlice({deviceId, capName: 'camera-streams'})`.
|
|
11972
|
+
* The cap's `onChanged` event fires automatically on each write so
|
|
11973
|
+
* subscribers get push semantics for free.
|
|
11974
|
+
*/
|
|
11975
|
+
runtimeState: object({
|
|
11976
|
+
online: boolean(),
|
|
11977
|
+
slotStatuses: object({
|
|
11978
|
+
high: ProfileSlotStatusSchema.optional(),
|
|
11979
|
+
mid: ProfileSlotStatusSchema.optional(),
|
|
11980
|
+
low: ProfileSlotStatusSchema.optional()
|
|
11981
|
+
}),
|
|
11982
|
+
slotErrors: object({
|
|
11983
|
+
high: string().optional(),
|
|
11984
|
+
mid: string().optional(),
|
|
11985
|
+
low: string().optional()
|
|
11986
|
+
}),
|
|
11987
|
+
lastChangedAt: number()
|
|
11988
|
+
})
|
|
11989
|
+
};
|
|
11990
|
+
/**
|
|
11991
|
+
* Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
|
|
11992
|
+
* entries with `device_class: carbon_monoxide`. Push-driven.
|
|
11993
|
+
*/
|
|
11994
|
+
var CarbonMonoxideStatusSchema = object({
|
|
11995
|
+
detected: boolean(),
|
|
11996
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11997
|
+
lastChangedAt: number()
|
|
11998
|
+
});
|
|
11999
|
+
var carbonMonoxideCapability = {
|
|
12000
|
+
name: "carbon-monoxide",
|
|
12001
|
+
scope: "device",
|
|
12002
|
+
deviceNative: true,
|
|
12003
|
+
mode: "singleton",
|
|
12004
|
+
deviceTypes: [DeviceType.Sensor],
|
|
12005
|
+
methods: {},
|
|
12006
|
+
status: {
|
|
12007
|
+
schema: CarbonMonoxideStatusSchema,
|
|
12008
|
+
kind: "push"
|
|
12009
|
+
},
|
|
12010
|
+
runtimeState: CarbonMonoxideStatusSchema
|
|
12011
|
+
};
|
|
12012
|
+
/**
|
|
12013
|
+
* HVAC / climate control cap. Models the full surface of a HA
|
|
12014
|
+
* `climate.*` entity (thermostat, A/C, heat pump, dehumidifier)
|
|
12015
|
+
* with a single coherent slice — mode + fan + preset + target +
|
|
12016
|
+
* dual-setpoint range + humidity + read-only current readings.
|
|
12017
|
+
*
|
|
12018
|
+
* Operation modes (`HvacMode`) follow HA's canonical set; not every
|
|
12019
|
+
* device supports every mode — the provider rejects unsupported
|
|
12020
|
+
* values at runtime. Min/max/step bounds for the setpoint are surfaced
|
|
12021
|
+
* via `getOptions` on the provider — they are static per-device hardware
|
|
12022
|
+
* properties persisted in the device config blob.
|
|
12023
|
+
*
|
|
12024
|
+
* `fanMode` / `preset` are free-form strings because device vendors
|
|
12025
|
+
* use vastly different vocabularies (`auto`/`low`/`high` vs
|
|
12026
|
+
* `quiet`/`normal`/`turbo`; `eco`/`away`/`sleep` vs `home`/`night`).
|
|
12027
|
+
* The provider populates `availableFanModes` / `availablePresets`
|
|
12028
|
+
* on the slice so the UI renders a closed-list picker.
|
|
12029
|
+
*
|
|
12030
|
+
* Sub-surface gating paired with `DeviceFeature` flags:
|
|
12031
|
+
* - `ClimateDualSetpoint` — heat_cool dual-target range support
|
|
12032
|
+
* - `ClimateHumidity` — currentHumidity / targetHumidity surfaces
|
|
12033
|
+
* - `ClimateFanMode` — fan-mode selector
|
|
12034
|
+
* - `ClimatePreset` — preset selector
|
|
11263
12035
|
* - `ClimateSwingVertical` — vertical louver swing toggle
|
|
11264
12036
|
* - `ClimateSwingHorizontal` — horizontal louver swing toggle
|
|
11265
12037
|
*
|
|
@@ -11277,7 +12049,7 @@ var HvacModeSchema = _enum([
|
|
|
11277
12049
|
"fan_only",
|
|
11278
12050
|
"dry"
|
|
11279
12051
|
]);
|
|
11280
|
-
object({
|
|
12052
|
+
var ClimateControlStatusSchema = object({
|
|
11281
12053
|
/** Active HVAC mode. */
|
|
11282
12054
|
mode: HvacModeSchema,
|
|
11283
12055
|
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
@@ -11321,56 +12093,82 @@ object({
|
|
|
11321
12093
|
/** Ms epoch when the slice was last updated (push or command). */
|
|
11322
12094
|
lastFetchedAt: number()
|
|
11323
12095
|
});
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
}),
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
}),
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
}),
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
}), _void(), {
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
})
|
|
12096
|
+
var climateControlCapability = {
|
|
12097
|
+
name: "climate-control",
|
|
12098
|
+
scope: "device",
|
|
12099
|
+
deviceNative: true,
|
|
12100
|
+
mode: "singleton",
|
|
12101
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
12102
|
+
methods: {
|
|
12103
|
+
setMode: method(object({
|
|
12104
|
+
deviceId: number().int().nonnegative(),
|
|
12105
|
+
mode: HvacModeSchema
|
|
12106
|
+
}), _void(), {
|
|
12107
|
+
kind: "mutation",
|
|
12108
|
+
auth: "admin"
|
|
12109
|
+
}),
|
|
12110
|
+
setFanMode: method(object({
|
|
12111
|
+
deviceId: number().int().nonnegative(),
|
|
12112
|
+
fanMode: string().min(1)
|
|
12113
|
+
}), _void(), {
|
|
12114
|
+
kind: "mutation",
|
|
12115
|
+
auth: "admin"
|
|
12116
|
+
}),
|
|
12117
|
+
setPreset: method(object({
|
|
12118
|
+
deviceId: number().int().nonnegative(),
|
|
12119
|
+
preset: string().min(1)
|
|
12120
|
+
}), _void(), {
|
|
12121
|
+
kind: "mutation",
|
|
12122
|
+
auth: "admin"
|
|
12123
|
+
}),
|
|
12124
|
+
setTarget: method(object({
|
|
12125
|
+
deviceId: number().int().nonnegative(),
|
|
12126
|
+
target: number()
|
|
12127
|
+
}), _void(), {
|
|
12128
|
+
kind: "mutation",
|
|
12129
|
+
auth: "admin"
|
|
12130
|
+
}),
|
|
12131
|
+
setTargetRange: method(object({
|
|
12132
|
+
deviceId: number().int().nonnegative(),
|
|
12133
|
+
targetLow: number(),
|
|
12134
|
+
targetHigh: number()
|
|
12135
|
+
}), _void(), {
|
|
12136
|
+
kind: "mutation",
|
|
12137
|
+
auth: "admin"
|
|
12138
|
+
}),
|
|
12139
|
+
setTargetHumidity: method(object({
|
|
12140
|
+
deviceId: number().int().nonnegative(),
|
|
12141
|
+
targetHumidity: number().min(0).max(100)
|
|
12142
|
+
}), _void(), {
|
|
12143
|
+
kind: "mutation",
|
|
12144
|
+
auth: "admin"
|
|
12145
|
+
}),
|
|
12146
|
+
setSwingVertical: method(object({
|
|
12147
|
+
deviceId: number().int().nonnegative(),
|
|
12148
|
+
on: boolean()
|
|
12149
|
+
}), _void(), {
|
|
12150
|
+
kind: "mutation",
|
|
12151
|
+
auth: "admin"
|
|
12152
|
+
}),
|
|
12153
|
+
setSwingHorizontal: method(object({
|
|
12154
|
+
deviceId: number().int().nonnegative(),
|
|
12155
|
+
on: boolean()
|
|
12156
|
+
}), _void(), {
|
|
12157
|
+
kind: "mutation",
|
|
12158
|
+
auth: "admin"
|
|
12159
|
+
})
|
|
12160
|
+
},
|
|
12161
|
+
status: {
|
|
12162
|
+
schema: ClimateControlStatusSchema,
|
|
12163
|
+
kind: "push"
|
|
12164
|
+
},
|
|
12165
|
+
/**
|
|
12166
|
+
* Runtime-state slice — mirrored by the kernel. UI thermostats read
|
|
12167
|
+
* the full slice via `device.state.climate-control.value` and refresh
|
|
12168
|
+
* on every push without re-querying the provider.
|
|
12169
|
+
*/
|
|
12170
|
+
runtimeState: ClimateControlStatusSchema
|
|
12171
|
+
};
|
|
11374
12172
|
/**
|
|
11375
12173
|
* Color-light cap. Coexists with `switch` (on/off) and `brightness`
|
|
11376
12174
|
* (level) on the same device — `switch` toggles the bulb, `brightness`
|
|
@@ -11422,7 +12220,7 @@ var ColorInputSchema = discriminatedUnion("mode", [
|
|
|
11422
12220
|
mireds: number().int().min(50).max(1e3)
|
|
11423
12221
|
})
|
|
11424
12222
|
]);
|
|
11425
|
-
object({
|
|
12223
|
+
var ColorStatusSchema = object({
|
|
11426
12224
|
/** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
|
|
11427
12225
|
* the bulb's current state. */
|
|
11428
12226
|
mode: _enum([
|
|
@@ -11439,63 +12237,209 @@ object({
|
|
|
11439
12237
|
/** Ms epoch of the last operator-driven change. */
|
|
11440
12238
|
lastChangedAt: number()
|
|
11441
12239
|
});
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
"
|
|
11453
|
-
"
|
|
11454
|
-
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
12240
|
+
var colorCapability = {
|
|
12241
|
+
name: "color",
|
|
12242
|
+
scope: "device",
|
|
12243
|
+
deviceNative: true,
|
|
12244
|
+
mode: "singleton",
|
|
12245
|
+
deviceTypes: [DeviceType.Light],
|
|
12246
|
+
methods: { setColor: method(object({
|
|
12247
|
+
deviceId: number().int().nonnegative(),
|
|
12248
|
+
color: ColorInputSchema
|
|
12249
|
+
}), _void(), {
|
|
12250
|
+
kind: "mutation",
|
|
12251
|
+
auth: "admin"
|
|
12252
|
+
}) },
|
|
12253
|
+
events: {
|
|
12254
|
+
/**
|
|
12255
|
+
* Emitted whenever the color changes — operator action OR firmware
|
|
12256
|
+
* push. Subscribers (UI color pickers, automation engines) react
|
|
12257
|
+
* without polling.
|
|
12258
|
+
*/
|
|
12259
|
+
onColorChanged: { data: object({
|
|
12260
|
+
deviceId: number(),
|
|
12261
|
+
mode: _enum([
|
|
12262
|
+
"rgb",
|
|
12263
|
+
"hsv",
|
|
12264
|
+
"mired"
|
|
12265
|
+
]),
|
|
12266
|
+
rgb: RgbTripletSchema.optional(),
|
|
12267
|
+
hsv: HsvTripletSchema.optional(),
|
|
12268
|
+
mireds: number().int().optional(),
|
|
12269
|
+
lastChangedAt: number()
|
|
12270
|
+
}) } },
|
|
12271
|
+
status: {
|
|
12272
|
+
schema: ColorStatusSchema,
|
|
12273
|
+
kind: "command-driven"
|
|
12274
|
+
},
|
|
12275
|
+
/**
|
|
12276
|
+
* Runtime-state slice — the last applied color, mirrored by the
|
|
12277
|
+
* kernel. Read via `device.state.color.value` so UI pickers surface
|
|
12278
|
+
* the current chromaticity without polling the provider.
|
|
12279
|
+
*/
|
|
12280
|
+
runtimeState: ColorStatusSchema
|
|
12281
|
+
};
|
|
12282
|
+
/**
|
|
12283
|
+
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
12284
|
+
* which is the kernel-managed online/offline flag for the device's
|
|
12285
|
+
* own transport. This cap surfaces an external entity's view of
|
|
12286
|
+
* whether the device is reachable (typical use: HA's
|
|
12287
|
+
* `binary_sensor` with `device_class: connectivity` for a remote
|
|
12288
|
+
* gateway or bridge). Push-driven.
|
|
12289
|
+
*/
|
|
12290
|
+
var ConnectivityStatusSchema = object({
|
|
11461
12291
|
/** True when the upstream system considers the entity connected. */
|
|
11462
12292
|
connected: boolean(),
|
|
11463
12293
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11464
12294
|
lastChangedAt: number()
|
|
11465
12295
|
});
|
|
11466
|
-
|
|
12296
|
+
var connectivityCapability = {
|
|
12297
|
+
name: "connectivity",
|
|
12298
|
+
scope: "device",
|
|
12299
|
+
deviceNative: true,
|
|
12300
|
+
mode: "singleton",
|
|
12301
|
+
deviceTypes: [DeviceType.Sensor],
|
|
12302
|
+
methods: {},
|
|
12303
|
+
status: {
|
|
12304
|
+
schema: ConnectivityStatusSchema,
|
|
12305
|
+
kind: "push"
|
|
12306
|
+
},
|
|
12307
|
+
runtimeState: ConnectivityStatusSchema
|
|
12308
|
+
};
|
|
12309
|
+
/**
|
|
12310
|
+
* Generic device-consumables capability — surfaces a device's
|
|
12311
|
+
* maintenance items (vacuum filters/brushes, replaceable cartridges,
|
|
12312
|
+
* descaling cycles, …) with their remaining life and an optional
|
|
12313
|
+
* "Replaced" reset action. Device-agnostic: any provider that knows its
|
|
12314
|
+
* device tracks consumables can register it; the cap declares no
|
|
12315
|
+
* vocabulary of its own — the provider names each item verbatim.
|
|
12316
|
+
*
|
|
12317
|
+
* Like `childLayout`, the cap is INERT until a provider sets items: no
|
|
12318
|
+
* provider populates it by guessing (no HA inference). The UI renders a
|
|
12319
|
+
* "No consumables reported" placeholder when `items` is empty.
|
|
12320
|
+
*/
|
|
12321
|
+
/** A single consumable item. Either a continuous `level` (remaining
|
|
12322
|
+
* life %) or a discrete `status` may be known — both may be null when a
|
|
12323
|
+
* provider only knows the item exists. `level` and `status` are not
|
|
12324
|
+
* mutually exclusive; a provider may report both. */
|
|
12325
|
+
var ConsumableItemSchema = object({
|
|
12326
|
+
/** Stable id, e.g. 'main-brush'. */
|
|
12327
|
+
key: string().min(1),
|
|
12328
|
+
/** Display name. */
|
|
12329
|
+
label: string().min(1),
|
|
12330
|
+
/** Remaining life % when known (0..100). */
|
|
12331
|
+
level: number().min(0).max(100).nullable(),
|
|
12332
|
+
/** Discrete state when known (binary mode). */
|
|
12333
|
+
status: _enum(["ok", "replace"]).nullable(),
|
|
12334
|
+
/** Ms epoch of the last replace, when known. */
|
|
12335
|
+
lastResetAt: number().nullable(),
|
|
12336
|
+
/** Whether `reset()` is meaningful for this item. */
|
|
12337
|
+
resettable: boolean()
|
|
12338
|
+
});
|
|
11467
12339
|
var ConsumablesStatusSchema = object({
|
|
11468
|
-
items: array(
|
|
11469
|
-
/** Stable id, e.g. 'main-brush'. */
|
|
11470
|
-
key: string().min(1),
|
|
11471
|
-
/** Display name. */
|
|
11472
|
-
label: string().min(1),
|
|
11473
|
-
/** Remaining life % when known (0..100). */
|
|
11474
|
-
level: number().min(0).max(100).nullable(),
|
|
11475
|
-
/** Discrete state when known (binary mode). */
|
|
11476
|
-
status: _enum(["ok", "replace"]).nullable(),
|
|
11477
|
-
/** Ms epoch of the last replace, when known. */
|
|
11478
|
-
lastResetAt: number().nullable(),
|
|
11479
|
-
/** Whether `reset()` is meaningful for this item. */
|
|
11480
|
-
resettable: boolean()
|
|
11481
|
-
})),
|
|
12340
|
+
items: array(ConsumableItemSchema),
|
|
11482
12341
|
lastChangedAt: number()
|
|
11483
12342
|
});
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
12343
|
+
var consumablesCapability = {
|
|
12344
|
+
name: "consumables",
|
|
12345
|
+
scope: "device",
|
|
12346
|
+
deviceNative: true,
|
|
12347
|
+
mode: "singleton",
|
|
12348
|
+
deviceTypes: Object.values(DeviceType),
|
|
12349
|
+
deviceConfig: { ui: {
|
|
12350
|
+
kind: "widget",
|
|
12351
|
+
widgetId: "host/consumables-panel",
|
|
12352
|
+
tab: "consumables",
|
|
12353
|
+
topTab: true,
|
|
12354
|
+
label: "Consumables",
|
|
12355
|
+
order: 5
|
|
12356
|
+
} },
|
|
12357
|
+
methods: {
|
|
12358
|
+
/** Mark a consumable as replaced — resets its remaining life. Only
|
|
12359
|
+
* meaningful when the item's `resettable` is true. */
|
|
12360
|
+
reset: method(object({
|
|
12361
|
+
deviceId: number().int().nonnegative(),
|
|
12362
|
+
key: string().min(1)
|
|
12363
|
+
}), _void(), {
|
|
12364
|
+
kind: "mutation",
|
|
12365
|
+
auth: "admin"
|
|
12366
|
+
}) },
|
|
12367
|
+
status: {
|
|
12368
|
+
schema: ConsumablesStatusSchema,
|
|
12369
|
+
kind: "push",
|
|
12370
|
+
empty: {
|
|
12371
|
+
items: [],
|
|
12372
|
+
lastChangedAt: 0
|
|
12373
|
+
},
|
|
12374
|
+
itemArray: {
|
|
12375
|
+
path: "items",
|
|
12376
|
+
keyField: "key",
|
|
12377
|
+
labelField: "label",
|
|
12378
|
+
itemSchema: ConsumableItemSchema,
|
|
12379
|
+
emptyItem: {
|
|
12380
|
+
key: "",
|
|
12381
|
+
label: "",
|
|
12382
|
+
level: null,
|
|
12383
|
+
status: null,
|
|
12384
|
+
lastResetAt: null,
|
|
12385
|
+
resettable: false
|
|
12386
|
+
}
|
|
12387
|
+
}
|
|
12388
|
+
},
|
|
12389
|
+
runtimeState: ConsumablesStatusSchema.extend({ lastFetchedAt: number() })
|
|
12390
|
+
};
|
|
12391
|
+
/**
|
|
12392
|
+
* Door / window / opening / garage / valve contact sensor. Boolean
|
|
12393
|
+
* "is the entry currently open" with the timestamp of the last
|
|
12394
|
+
* transition. Drives Home Assistant `binary_sensor` entries whose
|
|
12395
|
+
* `device_class` is `door`, `window`, `opening`, `garage`, or
|
|
12396
|
+
* `garage_door` — and any future native integration that needs
|
|
12397
|
+
* the same semantics.
|
|
12398
|
+
*
|
|
12399
|
+
* Push-driven: providers update the slice on transition events from
|
|
12400
|
+
* the upstream source (HA WebSocket `state_changed`, ZWave
|
|
12401
|
+
* `notification` …). Consumers read the slice; no polling.
|
|
12402
|
+
*/
|
|
12403
|
+
var ContactStatusSchema = object({
|
|
11492
12404
|
/** True when the entry is open; false when closed. */
|
|
11493
12405
|
entryOpen: boolean(),
|
|
11494
12406
|
/** Ms epoch of the last open↔closed transition. 0 if never observed. */
|
|
11495
12407
|
lastChangedAt: number()
|
|
11496
12408
|
});
|
|
11497
|
-
|
|
11498
|
-
|
|
12409
|
+
var contactCapability = {
|
|
12410
|
+
name: "contact",
|
|
12411
|
+
scope: "device",
|
|
12412
|
+
deviceNative: true,
|
|
12413
|
+
mode: "singleton",
|
|
12414
|
+
deviceTypes: [DeviceType.Sensor],
|
|
12415
|
+
methods: {},
|
|
12416
|
+
status: {
|
|
12417
|
+
schema: ContactStatusSchema,
|
|
12418
|
+
kind: "push"
|
|
12419
|
+
},
|
|
12420
|
+
runtimeState: ContactStatusSchema
|
|
12421
|
+
};
|
|
12422
|
+
/**
|
|
12423
|
+
* Status slice — flat object (the framework's `runtimeState` contract
|
|
12424
|
+
* requires a `ZodObject`, not a discriminated union). The `kind`
|
|
12425
|
+
* field discriminates the value type at the type level via the
|
|
12426
|
+
* `ControlNumericValue` / `ControlStringValue` aliases; consumers
|
|
12427
|
+
* narrow with a kind check.
|
|
12428
|
+
*
|
|
12429
|
+
* The `value` field is typed as `number | string`:
|
|
12430
|
+
* - `kind === 'numeric'` → `value` is `number`
|
|
12431
|
+
* - `kind === 'select' | 'text' | 'datetime'` → `value` is `string`
|
|
12432
|
+
*
|
|
12433
|
+
* The `options` array is populated only when `kind === 'select'`
|
|
12434
|
+
* (empty array for the other kinds). It lives in the slice because
|
|
12435
|
+
* HA `select.*` entities may update their options at runtime.
|
|
12436
|
+
*
|
|
12437
|
+
* The `unit`/`min`/`max`/`step` fields are meaningful for
|
|
12438
|
+
* `kind === 'numeric'` and are absent (undefined) for all other
|
|
12439
|
+
* kinds. They are populated live from HA attributes on each state
|
|
12440
|
+
* push — consistent with `options` and the sensor-unit approach.
|
|
12441
|
+
*/
|
|
12442
|
+
var ControlStatusSchema = object({
|
|
11499
12443
|
kind: _enum([
|
|
11500
12444
|
"numeric",
|
|
11501
12445
|
"select",
|
|
@@ -11557,14 +12501,31 @@ var ControlSetValueInputSchema = discriminatedUnion("kind", [
|
|
|
11557
12501
|
value: string().min(1)
|
|
11558
12502
|
})
|
|
11559
12503
|
]);
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
12504
|
+
var controlCapability = {
|
|
12505
|
+
name: "control",
|
|
12506
|
+
scope: "device",
|
|
12507
|
+
deviceNative: true,
|
|
12508
|
+
mode: "singleton",
|
|
12509
|
+
deviceTypes: [DeviceType.Control],
|
|
12510
|
+
methods: { setValue: method(object({
|
|
12511
|
+
deviceId: number().int().nonnegative(),
|
|
12512
|
+
control: ControlSetValueInputSchema
|
|
12513
|
+
}), _void(), {
|
|
12514
|
+
kind: "mutation",
|
|
12515
|
+
auth: "admin"
|
|
12516
|
+
}) },
|
|
12517
|
+
status: {
|
|
12518
|
+
schema: ControlStatusSchema,
|
|
12519
|
+
kind: "push"
|
|
12520
|
+
},
|
|
12521
|
+
/**
|
|
12522
|
+
* Runtime-state slice — mirrored by the kernel. UI widgets (slider /
|
|
12523
|
+
* dropdown / text field / date picker) read the slice's discriminant
|
|
12524
|
+
* and value directly without polling the provider.
|
|
12525
|
+
*/
|
|
12526
|
+
runtimeState: ControlStatusSchema
|
|
12527
|
+
};
|
|
12528
|
+
var CoverStatusSchema = object({
|
|
11568
12529
|
/** Lifecycle state of the cover. */
|
|
11569
12530
|
state: _enum([
|
|
11570
12531
|
"open",
|
|
@@ -11581,28 +12542,50 @@ object({
|
|
|
11581
12542
|
/** Ms epoch when the slice was last updated. */
|
|
11582
12543
|
lastChangedAt: number()
|
|
11583
12544
|
});
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
}),
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
12545
|
+
var coverCapability = {
|
|
12546
|
+
name: "cover",
|
|
12547
|
+
scope: "device",
|
|
12548
|
+
deviceNative: true,
|
|
12549
|
+
mode: "singleton",
|
|
12550
|
+
deviceTypes: [DeviceType.Cover],
|
|
12551
|
+
methods: {
|
|
12552
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12553
|
+
kind: "mutation",
|
|
12554
|
+
auth: "admin"
|
|
12555
|
+
}),
|
|
12556
|
+
close: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12557
|
+
kind: "mutation",
|
|
12558
|
+
auth: "admin"
|
|
12559
|
+
}),
|
|
12560
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
12561
|
+
kind: "mutation",
|
|
12562
|
+
auth: "admin"
|
|
12563
|
+
}),
|
|
12564
|
+
setPosition: method(object({
|
|
12565
|
+
deviceId: number().int().nonnegative(),
|
|
12566
|
+
position: number().min(0).max(100)
|
|
12567
|
+
}), _void(), {
|
|
12568
|
+
kind: "mutation",
|
|
12569
|
+
auth: "admin"
|
|
12570
|
+
}),
|
|
12571
|
+
setTiltPosition: method(object({
|
|
12572
|
+
deviceId: number().int().nonnegative(),
|
|
12573
|
+
tiltPosition: number().min(0).max(100)
|
|
12574
|
+
}), _void(), {
|
|
12575
|
+
kind: "mutation",
|
|
12576
|
+
auth: "admin"
|
|
12577
|
+
})
|
|
12578
|
+
},
|
|
12579
|
+
status: {
|
|
12580
|
+
schema: CoverStatusSchema,
|
|
12581
|
+
kind: "push"
|
|
12582
|
+
},
|
|
12583
|
+
/**
|
|
12584
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch
|
|
12585
|
+
* the slice for live position changes during a move.
|
|
12586
|
+
*/
|
|
12587
|
+
runtimeState: CoverStatusSchema
|
|
12588
|
+
};
|
|
11606
12589
|
/**
|
|
11607
12590
|
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
11608
12591
|
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
@@ -11632,7 +12615,12 @@ var NormalizedRangeSchema$1 = object({
|
|
|
11632
12615
|
max: number(),
|
|
11633
12616
|
step: number()
|
|
11634
12617
|
});
|
|
11635
|
-
|
|
12618
|
+
/**
|
|
12619
|
+
* Current day/night state. Optional fields are absent when the camera
|
|
12620
|
+
* does not expose that knob (a photocell-less model reports no
|
|
12621
|
+
* `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
|
|
12622
|
+
*/
|
|
12623
|
+
var DayNightStatusSchema = object({
|
|
11636
12624
|
mode: DayNightModeSchema,
|
|
11637
12625
|
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
11638
12626
|
sensitivity: number().optional(),
|
|
@@ -11665,13 +12653,33 @@ var DayNightSettingsPatchSchema = object({
|
|
|
11665
12653
|
sensitivity: number().optional(),
|
|
11666
12654
|
switchDelaySec: number().optional()
|
|
11667
12655
|
});
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
12656
|
+
var dayNightCapability = {
|
|
12657
|
+
name: "day-night",
|
|
12658
|
+
scope: "device",
|
|
12659
|
+
deviceNative: true,
|
|
12660
|
+
mode: "singleton",
|
|
12661
|
+
deviceTypes: [DeviceType.Camera],
|
|
12662
|
+
deviceConfig: { ui: {
|
|
12663
|
+
kind: "derived-form",
|
|
12664
|
+
builderId: "day-night",
|
|
12665
|
+
tab: "image"
|
|
12666
|
+
} },
|
|
12667
|
+
methods: {
|
|
12668
|
+
getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
|
|
12669
|
+
setSettings: method(object({
|
|
12670
|
+
deviceId: number(),
|
|
12671
|
+
settings: DayNightSettingsPatchSchema
|
|
12672
|
+
}), _void(), {
|
|
12673
|
+
kind: "mutation",
|
|
12674
|
+
auth: "admin"
|
|
12675
|
+
})
|
|
12676
|
+
},
|
|
12677
|
+
status: {
|
|
12678
|
+
schema: DayNightStatusSchema,
|
|
12679
|
+
kind: "poll"
|
|
12680
|
+
},
|
|
12681
|
+
runtimeState: DayNightStatusSchema
|
|
12682
|
+
};
|
|
11675
12683
|
/**
|
|
11676
12684
|
* Identity envelope for a device's upstream-system metadata.
|
|
11677
12685
|
*
|
|
@@ -11715,6 +12723,66 @@ var SourceInfoSchema = object({
|
|
|
11715
12723
|
raw: record(string(), unknown()).optional()
|
|
11716
12724
|
});
|
|
11717
12725
|
/**
|
|
12726
|
+
* Build the synthetic SourceInfo every existing provider falls back to
|
|
12727
|
+
* when no upstream value has been persisted. Keeps non-migrated providers
|
|
12728
|
+
* (Reolink / Hikvision / ONVIF / Frigate / RTSP) functional without code
|
|
12729
|
+
* changes — `id` reuses the CamStack stableId, `system` reuses the addon
|
|
12730
|
+
* id. Real upstream identity replaces this once a provider migrates and
|
|
12731
|
+
* calls `updateSourceInfo()` with concrete values.
|
|
12732
|
+
*/
|
|
12733
|
+
function synthesizeSourceInfo(input) {
|
|
12734
|
+
return {
|
|
12735
|
+
id: input.stableId,
|
|
12736
|
+
system: input.addonId
|
|
12737
|
+
};
|
|
12738
|
+
}
|
|
12739
|
+
/**
|
|
12740
|
+
* Shallow-merge a partial patch over a prior SourceInfo. `undefined`
|
|
12741
|
+
* patch values are ignored (no field clobber); to clear a field
|
|
12742
|
+
* explicitly, providers omit it from the patch and rebuild from scratch
|
|
12743
|
+
* via `updateSourceInfo({})` cycles — uncommon enough that we don't
|
|
12744
|
+
* bother with a `null`-means-delete sentinel.
|
|
12745
|
+
*
|
|
12746
|
+
* The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
|
|
12747
|
+
* merges over existing `raw` keys instead of replacing the whole bag.
|
|
12748
|
+
*/
|
|
12749
|
+
function mergeSourceInfo(prev, patch) {
|
|
12750
|
+
const next = { ...prev };
|
|
12751
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
12752
|
+
if (v === void 0) continue;
|
|
12753
|
+
if (k === "raw" && typeof v === "object" && v !== null) {
|
|
12754
|
+
next.raw = {
|
|
12755
|
+
...prev.raw,
|
|
12756
|
+
...v
|
|
12757
|
+
};
|
|
12758
|
+
continue;
|
|
12759
|
+
}
|
|
12760
|
+
next[k] = v;
|
|
12761
|
+
}
|
|
12762
|
+
return next;
|
|
12763
|
+
}
|
|
12764
|
+
/**
|
|
12765
|
+
* Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
|
|
12766
|
+
* blob is a free-form `Record<string, unknown>` shared with hardware-
|
|
12767
|
+
* identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
|
|
12768
|
+
* lives under a single nested key to keep it composable with those.
|
|
12769
|
+
*/
|
|
12770
|
+
var SOURCE_INFO_METADATA_KEY = "sourceInfo";
|
|
12771
|
+
/**
|
|
12772
|
+
* Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
|
|
12773
|
+
* with Zod. Returns `null` when the blob is missing or the nested
|
|
12774
|
+
* `sourceInfo` key fails validation — callers fall back to the
|
|
12775
|
+
* synthetic default in that case (see `synthesizeSourceInfo`).
|
|
12776
|
+
*/
|
|
12777
|
+
function extractSourceInfoFromMetadata(metadata) {
|
|
12778
|
+
if (!metadata) return null;
|
|
12779
|
+
const raw = metadata[SOURCE_INFO_METADATA_KEY];
|
|
12780
|
+
if (!raw) return null;
|
|
12781
|
+
const parsed = SourceInfoSchema.safeParse(raw);
|
|
12782
|
+
if (!parsed.success) return null;
|
|
12783
|
+
return parsed.data;
|
|
12784
|
+
}
|
|
12785
|
+
/**
|
|
11718
12786
|
* device-discovery — device-scoped capability for parents that host /
|
|
11719
12787
|
* enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
|
|
11720
12788
|
* integrations). The parent advertises a list of discoverable children,
|
|
@@ -11801,38 +12869,129 @@ var DeviceDiscoveryStatusSchema = object({
|
|
|
11801
12869
|
/** Last error surfaced from the source (rendered as a banner). */
|
|
11802
12870
|
lastError: string().nullable()
|
|
11803
12871
|
});
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
}),
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
12872
|
+
var deviceDiscoveryCapability = {
|
|
12873
|
+
name: "device-discovery",
|
|
12874
|
+
scope: "device",
|
|
12875
|
+
deviceNative: true,
|
|
12876
|
+
mode: "singleton",
|
|
12877
|
+
deviceTypes: [DeviceType.Hub],
|
|
12878
|
+
status: {
|
|
12879
|
+
schema: DeviceDiscoveryStatusSchema,
|
|
12880
|
+
kind: "poll"
|
|
12881
|
+
},
|
|
12882
|
+
runtimeState: DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int().nonnegative() }),
|
|
12883
|
+
methods: {
|
|
12884
|
+
/**
|
|
12885
|
+
* Snapshot of the current `discovered` list. Returns the
|
|
12886
|
+
* runtime-state cache — call `refreshDiscovery` first if a
|
|
12887
|
+
* fresh round-trip to the source is required.
|
|
12888
|
+
*/
|
|
12889
|
+
listDiscovered: method(object({ deviceId: number().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly()),
|
|
12890
|
+
/**
|
|
12891
|
+
* Force the integration to re-enumerate and update the
|
|
12892
|
+
* runtime-state slice. Returns the freshly-enumerated list (also
|
|
12893
|
+
* available via `listDiscovered` post-call).
|
|
12894
|
+
*/
|
|
12895
|
+
refreshDiscovery: method(object({ deviceId: number().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly(), {
|
|
12896
|
+
kind: "mutation",
|
|
12897
|
+
auth: "admin"
|
|
12898
|
+
}),
|
|
12899
|
+
/**
|
|
12900
|
+
* Promote a discovered entry to a real child device. The framework
|
|
12901
|
+
* creates the child via `kernel.devices.create()` with
|
|
12902
|
+
* `parentDeviceId = parent.id` and seeds the child's config from
|
|
12903
|
+
* `childInitialConfig` (driver-defined; usually carries channel +
|
|
12904
|
+
* uid + parent reference). Returns the kernel-assigned numeric id.
|
|
12905
|
+
*/
|
|
12906
|
+
adoptDevice: method(object({
|
|
12907
|
+
deviceId: number().int().nonnegative(),
|
|
12908
|
+
childNativeId: string(),
|
|
12909
|
+
/** Optional override for the child's display name. */
|
|
12910
|
+
name: string().optional()
|
|
12911
|
+
}), object({
|
|
12912
|
+
deviceId: number().int().nonnegative(),
|
|
12913
|
+
stableId: string()
|
|
12914
|
+
}), {
|
|
12915
|
+
kind: "mutation",
|
|
12916
|
+
auth: "admin"
|
|
12917
|
+
}),
|
|
12918
|
+
/**
|
|
12919
|
+
* Inverse of `adoptDevice`: removes the child device from the
|
|
12920
|
+
* kernel registry. The discovered entry remains in the
|
|
12921
|
+
* enumeration (status updates resume) so the operator can re-adopt
|
|
12922
|
+
* it later without a fresh refresh.
|
|
12923
|
+
*/
|
|
12924
|
+
releaseDevice: method(object({
|
|
12925
|
+
deviceId: number().int().nonnegative(),
|
|
12926
|
+
childDeviceId: number().int().nonnegative()
|
|
12927
|
+
}), _void(), {
|
|
12928
|
+
kind: "mutation",
|
|
12929
|
+
auth: "admin"
|
|
12930
|
+
})
|
|
12931
|
+
}
|
|
12932
|
+
};
|
|
12933
|
+
/**
|
|
12934
|
+
* Doorbell button cap. Two kinds of providers coexist behind this cap
|
|
12935
|
+
* name (same pattern as `snapshot`):
|
|
12936
|
+
*
|
|
12937
|
+
* - **Native** providers: registered per-device by device-driver
|
|
12938
|
+
* addons via `ctx.registerNativeCap` — either on a
|
|
12939
|
+
* `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
|
|
12940
|
+
* or directly on the camera (Reolink registers at camera level).
|
|
12941
|
+
* Emits an `onPressed` event every time the firmware pushes a
|
|
12942
|
+
* ring; status tracks the last press and a pressCount since start
|
|
12943
|
+
* (diagnostic).
|
|
12944
|
+
*
|
|
12945
|
+
* - **Wrapper** provider: the `virtual-doorbell` system builtin
|
|
12946
|
+
* (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
|
|
12947
|
+
* device (contact / switch / event-emitter …) into a doorbell for
|
|
12948
|
+
* a camera. `defaultActive: false` — the operator explicitly binds
|
|
12949
|
+
* it per camera in the device-bindings UI, then picks the source
|
|
12950
|
+
* device + trigger in the per-device settings.
|
|
12951
|
+
*
|
|
12952
|
+
* The DeviceEventPropagator re-emits `onPressed` on the camera parent
|
|
12953
|
+
* — subscribers listening at the camera level receive ring events
|
|
12954
|
+
* with `via[]` populated. No code on the parent needed.
|
|
12955
|
+
*/
|
|
12956
|
+
var DoorbellStatusSchema = object({
|
|
11826
12957
|
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
11827
12958
|
lastPressedAt: number().nullable(),
|
|
11828
12959
|
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
11829
12960
|
pressCountSinceStart: number()
|
|
11830
12961
|
});
|
|
11831
|
-
object({
|
|
12962
|
+
var DoorbellPressEventSchema = object({
|
|
11832
12963
|
deviceId: number(),
|
|
11833
12964
|
timestamp: number()
|
|
11834
12965
|
});
|
|
11835
|
-
|
|
12966
|
+
var doorbellCapability = {
|
|
12967
|
+
name: "doorbell",
|
|
12968
|
+
scope: "device",
|
|
12969
|
+
deviceNative: true,
|
|
12970
|
+
mode: "singleton",
|
|
12971
|
+
kind: "wrapper",
|
|
12972
|
+
defaultActive: false,
|
|
12973
|
+
deviceTypes: [DeviceType.Button, DeviceType.Camera],
|
|
12974
|
+
exposesDeviceSettings: true,
|
|
12975
|
+
methods: {},
|
|
12976
|
+
events: {
|
|
12977
|
+
/**
|
|
12978
|
+
* Fires once per physical press. Reolink delivers via Baichuan
|
|
12979
|
+
* push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
|
|
12980
|
+
* release/duration — it's a pulse.
|
|
12981
|
+
*/
|
|
12982
|
+
onPressed: { data: DoorbellPressEventSchema } },
|
|
12983
|
+
status: {
|
|
12984
|
+
schema: DoorbellStatusSchema,
|
|
12985
|
+
kind: "push"
|
|
12986
|
+
},
|
|
12987
|
+
/**
|
|
12988
|
+
* Runtime-state slice — last press timestamp + lifetime press count.
|
|
12989
|
+
* Mirrored by the kernel and readable via
|
|
12990
|
+
* `device.state.doorbell.value`. UIs can show "last ring 5m ago"
|
|
12991
|
+
* without subscribing.
|
|
12992
|
+
*/
|
|
12993
|
+
runtimeState: DoorbellStatusSchema
|
|
12994
|
+
};
|
|
11836
12995
|
/**
|
|
11837
12996
|
* Enum-state sensor — a string value picked from a finite option set.
|
|
11838
12997
|
* Drives HA `sensor` entries with `state_class: enum` (HVAC action
|
|
@@ -11848,7 +13007,7 @@ var EnumSensorDateTimeFormatSchema = _enum([
|
|
|
11848
13007
|
"time",
|
|
11849
13008
|
"datetime"
|
|
11850
13009
|
]);
|
|
11851
|
-
object({
|
|
13010
|
+
var EnumSensorStatusSchema = object({
|
|
11852
13011
|
value: string(),
|
|
11853
13012
|
/**
|
|
11854
13013
|
* Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
|
|
@@ -11860,7 +13019,19 @@ object({
|
|
|
11860
13019
|
/** Ms epoch when the slice was last updated. */
|
|
11861
13020
|
lastFetchedAt: number()
|
|
11862
13021
|
});
|
|
11863
|
-
|
|
13022
|
+
var enumSensorCapability = {
|
|
13023
|
+
name: "enum-sensor",
|
|
13024
|
+
scope: "device",
|
|
13025
|
+
deviceNative: true,
|
|
13026
|
+
mode: "singleton",
|
|
13027
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13028
|
+
methods: {},
|
|
13029
|
+
status: {
|
|
13030
|
+
schema: EnumSensorStatusSchema,
|
|
13031
|
+
kind: "push"
|
|
13032
|
+
},
|
|
13033
|
+
runtimeState: EnumSensorStatusSchema
|
|
13034
|
+
};
|
|
11864
13035
|
/**
|
|
11865
13036
|
* Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
|
|
11866
13037
|
* device. Carries the device's EXACT declared event vocabulary verbatim
|
|
@@ -11878,12 +13049,25 @@ var EventFireSchema = object({
|
|
|
11878
13049
|
timestamp: number(),
|
|
11879
13050
|
seq: number()
|
|
11880
13051
|
});
|
|
11881
|
-
object({
|
|
13052
|
+
var EventEmitterStatusSchema = object({
|
|
11882
13053
|
eventTypes: array(string()),
|
|
11883
13054
|
lastEvent: EventFireSchema.nullable(),
|
|
11884
13055
|
eventCountSinceStart: number()
|
|
11885
13056
|
});
|
|
11886
|
-
|
|
13057
|
+
var eventEmitterCapability = {
|
|
13058
|
+
name: "event-emitter",
|
|
13059
|
+
scope: "device",
|
|
13060
|
+
deviceNative: true,
|
|
13061
|
+
mode: "singleton",
|
|
13062
|
+
deviceTypes: [DeviceType.EventEmitter],
|
|
13063
|
+
methods: {},
|
|
13064
|
+
events: { onEvent: { data: EventFireSchema } },
|
|
13065
|
+
status: {
|
|
13066
|
+
schema: EventEmitterStatusSchema,
|
|
13067
|
+
kind: "push"
|
|
13068
|
+
},
|
|
13069
|
+
runtimeState: EventEmitterStatusSchema
|
|
13070
|
+
};
|
|
11887
13071
|
/**
|
|
11888
13072
|
* Fan-control cap. Models HA `fan.*` entity-specific surfaces:
|
|
11889
13073
|
* speed percentage, preset modes, ceiling-fan direction, and
|
|
@@ -11903,7 +13087,7 @@ DeviceType.EventEmitter;
|
|
|
11903
13087
|
* - `FanOscillating` — oscillation toggle
|
|
11904
13088
|
*/
|
|
11905
13089
|
var FanDirectionSchema = _enum(["forward", "reverse"]);
|
|
11906
|
-
object({
|
|
13090
|
+
var FanControlStatusSchema = object({
|
|
11907
13091
|
/** Active speed as 0..100 inclusive. Null when the device has no
|
|
11908
13092
|
* speed surface (single-speed fan). */
|
|
11909
13093
|
percentage: number().min(0).max(100).nullable(),
|
|
@@ -11924,45 +13108,118 @@ object({
|
|
|
11924
13108
|
/** Ms epoch when the slice was last updated. */
|
|
11925
13109
|
lastChangedAt: number()
|
|
11926
13110
|
});
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
})
|
|
11952
|
-
|
|
13111
|
+
var fanControlCapability = {
|
|
13112
|
+
name: "fan-control",
|
|
13113
|
+
scope: "device",
|
|
13114
|
+
deviceNative: true,
|
|
13115
|
+
mode: "singleton",
|
|
13116
|
+
deviceTypes: [DeviceType.Fan],
|
|
13117
|
+
methods: {
|
|
13118
|
+
setPercentage: method(object({
|
|
13119
|
+
deviceId: number().int().nonnegative(),
|
|
13120
|
+
percentage: number().min(0).max(100)
|
|
13121
|
+
}), _void(), {
|
|
13122
|
+
kind: "mutation",
|
|
13123
|
+
auth: "admin"
|
|
13124
|
+
}),
|
|
13125
|
+
setPreset: method(object({
|
|
13126
|
+
deviceId: number().int().nonnegative(),
|
|
13127
|
+
preset: string().min(1)
|
|
13128
|
+
}), _void(), {
|
|
13129
|
+
kind: "mutation",
|
|
13130
|
+
auth: "admin"
|
|
13131
|
+
}),
|
|
13132
|
+
setDirection: method(object({
|
|
13133
|
+
deviceId: number().int().nonnegative(),
|
|
13134
|
+
direction: FanDirectionSchema
|
|
13135
|
+
}), _void(), {
|
|
13136
|
+
kind: "mutation",
|
|
13137
|
+
auth: "admin"
|
|
13138
|
+
}),
|
|
13139
|
+
setOscillating: method(object({
|
|
13140
|
+
deviceId: number().int().nonnegative(),
|
|
13141
|
+
oscillating: boolean()
|
|
13142
|
+
}), _void(), {
|
|
13143
|
+
kind: "mutation",
|
|
13144
|
+
auth: "admin"
|
|
13145
|
+
})
|
|
13146
|
+
},
|
|
13147
|
+
status: {
|
|
13148
|
+
schema: FanControlStatusSchema,
|
|
13149
|
+
kind: "push"
|
|
13150
|
+
},
|
|
13151
|
+
/**
|
|
13152
|
+
* Runtime-state slice — mirrored by the kernel. UI fan speed
|
|
13153
|
+
* sliders read `percentage` for live updates.
|
|
13154
|
+
*/
|
|
13155
|
+
runtimeState: FanControlStatusSchema
|
|
13156
|
+
};
|
|
13157
|
+
/**
|
|
13158
|
+
* Water leak / moisture sensor. Boolean "is liquid currently
|
|
13159
|
+
* detected" with the timestamp of the last transition. Drives Home
|
|
13160
|
+
* Assistant `binary_sensor` entries with `device_class: moisture`,
|
|
13161
|
+
* and any future native flood sensor.
|
|
13162
|
+
*
|
|
13163
|
+
* Push-driven from the upstream source.
|
|
13164
|
+
*/
|
|
13165
|
+
var FloodStatusSchema = object({
|
|
11953
13166
|
/** True when leak is currently detected. */
|
|
11954
13167
|
flooded: boolean(),
|
|
11955
13168
|
/** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
|
|
11956
13169
|
lastChangedAt: number()
|
|
11957
13170
|
});
|
|
11958
|
-
|
|
11959
|
-
|
|
13171
|
+
var floodCapability = {
|
|
13172
|
+
name: "flood",
|
|
13173
|
+
scope: "device",
|
|
13174
|
+
deviceNative: true,
|
|
13175
|
+
mode: "singleton",
|
|
13176
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13177
|
+
methods: {},
|
|
13178
|
+
status: {
|
|
13179
|
+
schema: FloodStatusSchema,
|
|
13180
|
+
kind: "push"
|
|
13181
|
+
},
|
|
13182
|
+
runtimeState: FloodStatusSchema
|
|
13183
|
+
};
|
|
13184
|
+
/**
|
|
13185
|
+
* Combustible-gas (LPG / methane / hydrogen) alarm sensor. Drives
|
|
13186
|
+
* Home Assistant `binary_sensor` entries with `device_class: gas`.
|
|
13187
|
+
* Push-driven.
|
|
13188
|
+
*/
|
|
13189
|
+
var GasStatusSchema = object({
|
|
11960
13190
|
detected: boolean(),
|
|
11961
13191
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
11962
13192
|
lastChangedAt: number()
|
|
11963
13193
|
});
|
|
11964
|
-
|
|
11965
|
-
|
|
13194
|
+
var gasCapability = {
|
|
13195
|
+
name: "gas",
|
|
13196
|
+
scope: "device",
|
|
13197
|
+
deviceNative: true,
|
|
13198
|
+
mode: "singleton",
|
|
13199
|
+
deviceTypes: [DeviceType.Sensor],
|
|
13200
|
+
methods: {},
|
|
13201
|
+
status: {
|
|
13202
|
+
schema: GasStatusSchema,
|
|
13203
|
+
kind: "push"
|
|
13204
|
+
},
|
|
13205
|
+
runtimeState: GasStatusSchema
|
|
13206
|
+
};
|
|
13207
|
+
/**
|
|
13208
|
+
* Humidifier / dehumidifier cap. Models HA `humidifier.*` entities —
|
|
13209
|
+
* an on/off actuator with an optional target-humidity setpoint and an
|
|
13210
|
+
* optional vendor mode (`auto` / `normal` / `baby` / …).
|
|
13211
|
+
*
|
|
13212
|
+
* A climate-family sibling: the slice carries the on/off state, the
|
|
13213
|
+
* current + target relative humidity (0..100), the active mode and its
|
|
13214
|
+
* available set, plus HA's free-form `action` readout
|
|
13215
|
+
* (`humidifying` / `drying` / `idle` / `off`). `minHumidity` /
|
|
13216
|
+
* `maxHumidity` mirror HA's `min_humidity` / `max_humidity` attributes
|
|
13217
|
+
* (null → the UI falls back to a 0..100 range).
|
|
13218
|
+
*
|
|
13219
|
+
* Providers populate only what the hardware reports — `mode` and the
|
|
13220
|
+
* humidity fields stay null when the device has no such surface.
|
|
13221
|
+
*/
|
|
13222
|
+
var HumidifierStatusSchema = object({
|
|
11966
13223
|
/** Whether the humidifier is currently on. */
|
|
11967
13224
|
on: boolean(),
|
|
11968
13225
|
/** Current measured relative humidity (0..100). Null when not reported. */
|
|
@@ -11984,26 +13241,54 @@ object({
|
|
|
11984
13241
|
/** Ms epoch when the slice was last updated. */
|
|
11985
13242
|
lastChangedAt: number()
|
|
11986
13243
|
});
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
13244
|
+
var humidifierCapability = {
|
|
13245
|
+
name: "humidifier",
|
|
13246
|
+
scope: "device",
|
|
13247
|
+
deviceNative: true,
|
|
13248
|
+
mode: "singleton",
|
|
13249
|
+
deviceTypes: [DeviceType.Humidifier],
|
|
13250
|
+
methods: {
|
|
13251
|
+
setOn: method(object({
|
|
13252
|
+
deviceId: number().int().nonnegative(),
|
|
13253
|
+
on: boolean()
|
|
13254
|
+
}), _void(), {
|
|
13255
|
+
kind: "mutation",
|
|
13256
|
+
auth: "admin"
|
|
13257
|
+
}),
|
|
13258
|
+
setTargetHumidity: method(object({
|
|
13259
|
+
deviceId: number().int().nonnegative(),
|
|
13260
|
+
humidity: number().min(0).max(100)
|
|
13261
|
+
}), _void(), {
|
|
13262
|
+
kind: "mutation",
|
|
13263
|
+
auth: "admin"
|
|
13264
|
+
}),
|
|
13265
|
+
setMode: method(object({
|
|
13266
|
+
deviceId: number().int().nonnegative(),
|
|
13267
|
+
mode: string().min(1)
|
|
13268
|
+
}), _void(), {
|
|
13269
|
+
kind: "mutation",
|
|
13270
|
+
auth: "admin"
|
|
13271
|
+
})
|
|
13272
|
+
},
|
|
13273
|
+
status: {
|
|
13274
|
+
schema: HumidifierStatusSchema,
|
|
13275
|
+
kind: "push"
|
|
13276
|
+
},
|
|
13277
|
+
/**
|
|
13278
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
13279
|
+
* slice for live humidity / mode changes.
|
|
13280
|
+
*/
|
|
13281
|
+
runtimeState: HumidifierStatusSchema
|
|
13282
|
+
};
|
|
13283
|
+
/**
|
|
13284
|
+
* Single-metric humidity reading. Drives Home Assistant `sensor`
|
|
13285
|
+
* entries with `device_class: humidity`.
|
|
13286
|
+
*
|
|
13287
|
+
* Unit normalisation: percent. The canonical display unit (`%`) is a
|
|
13288
|
+
* descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
13289
|
+
* `sourceInfo`.
|
|
13290
|
+
*/
|
|
13291
|
+
var HumiditySensorStatusSchema = object({
|
|
12007
13292
|
/** Current relative humidity, 0..100. */
|
|
12008
13293
|
percent: number().min(0).max(100),
|
|
12009
13294
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -12018,15 +13303,57 @@ object({
|
|
|
12018
13303
|
* auto-formatting when absent. */
|
|
12019
13304
|
precision: number().int().min(0).max(10).optional()
|
|
12020
13305
|
});
|
|
12021
|
-
|
|
12022
|
-
|
|
13306
|
+
var humiditySensorCapability = {
|
|
13307
|
+
name: "humidity-sensor",
|
|
13308
|
+
scope: "device",
|
|
13309
|
+
deviceNative: true,
|
|
13310
|
+
mode: "singleton",
|
|
13311
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
|
|
13312
|
+
methods: {},
|
|
13313
|
+
status: {
|
|
13314
|
+
schema: HumiditySensorStatusSchema,
|
|
13315
|
+
kind: "push"
|
|
13316
|
+
},
|
|
13317
|
+
runtimeState: HumiditySensorStatusSchema
|
|
13318
|
+
};
|
|
13319
|
+
/**
|
|
13320
|
+
* Image display cap. Models a single still image exposed by an integration —
|
|
13321
|
+
* a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
|
|
13322
|
+
*
|
|
13323
|
+
* Read-only: there are no setters. The provider resolves whatever upstream
|
|
13324
|
+
* source it has into an ABSOLUTE URL the browser loads directly:
|
|
13325
|
+
* - HA `image.*` entities → the `entity_picture` signed-token path
|
|
13326
|
+
* (token stays in the query string, so no auth header is needed);
|
|
13327
|
+
* - a Dreame/robot map → the cloud/OSS map-image URL (or an addon
|
|
13328
|
+
* data-plane URL serving the rendered map bytes), exposed as its own
|
|
13329
|
+
* Image child device grouped under the robot's container.
|
|
13330
|
+
* The slice carries that URL plus the upstream last-updated timestamp; the
|
|
13331
|
+
* image changes when the source's last-updated marker changes.
|
|
13332
|
+
*/
|
|
13333
|
+
var ImageStatusSchema = object({
|
|
12023
13334
|
/** Absolute signed URL the browser loads directly. Null when the
|
|
12024
13335
|
* entity exposes no `entity_picture` (yet). */
|
|
12025
13336
|
url: string().nullable(),
|
|
12026
13337
|
/** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
|
|
12027
13338
|
lastUpdated: number().nullable()
|
|
12028
13339
|
});
|
|
12029
|
-
|
|
13340
|
+
var imageCapability = {
|
|
13341
|
+
name: "image",
|
|
13342
|
+
scope: "device",
|
|
13343
|
+
deviceNative: true,
|
|
13344
|
+
mode: "singleton",
|
|
13345
|
+
deviceTypes: [DeviceType.Image],
|
|
13346
|
+
methods: {},
|
|
13347
|
+
status: {
|
|
13348
|
+
schema: ImageStatusSchema,
|
|
13349
|
+
kind: "push"
|
|
13350
|
+
},
|
|
13351
|
+
/**
|
|
13352
|
+
* Runtime-state slice — mirrored by the kernel. The UI reads `url`
|
|
13353
|
+
* directly and renders the still image.
|
|
13354
|
+
*/
|
|
13355
|
+
runtimeState: ImageStatusSchema
|
|
13356
|
+
};
|
|
12030
13357
|
/**
|
|
12031
13358
|
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
12032
13359
|
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
@@ -12078,7 +13405,12 @@ var NormalizedRangeSchema = object({
|
|
|
12078
13405
|
max: number(),
|
|
12079
13406
|
step: number()
|
|
12080
13407
|
});
|
|
12081
|
-
|
|
13408
|
+
/**
|
|
13409
|
+
* Current image-adjustment state. Every field optional — absent when the
|
|
13410
|
+
* camera does not expose that control. Slider values are NORMALIZED 0–100.
|
|
13411
|
+
* `lastFetchedAt` feeds the runtime-state bridge.
|
|
13412
|
+
*/
|
|
13413
|
+
var ImageSettingsStatusSchema = object({
|
|
12082
13414
|
/** Normalized 0–100. */
|
|
12083
13415
|
brightness: number().optional(),
|
|
12084
13416
|
/** Normalized 0–100. */
|
|
@@ -12144,13 +13476,33 @@ var ImageSettingsPatchSchema = object({
|
|
|
12144
13476
|
exposureMode: ExposureModeSchema.optional(),
|
|
12145
13477
|
backlightMode: BacklightModeSchema.optional()
|
|
12146
13478
|
});
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
13479
|
+
var imageSettingsCapability = {
|
|
13480
|
+
name: "image-settings",
|
|
13481
|
+
scope: "device",
|
|
13482
|
+
deviceNative: true,
|
|
13483
|
+
mode: "singleton",
|
|
13484
|
+
deviceTypes: [DeviceType.Camera],
|
|
13485
|
+
deviceConfig: { ui: {
|
|
13486
|
+
kind: "derived-form",
|
|
13487
|
+
builderId: "image-settings",
|
|
13488
|
+
tab: "image"
|
|
13489
|
+
} },
|
|
13490
|
+
methods: {
|
|
13491
|
+
getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
|
|
13492
|
+
setSettings: method(object({
|
|
13493
|
+
deviceId: number(),
|
|
13494
|
+
settings: ImageSettingsPatchSchema
|
|
13495
|
+
}), _void(), {
|
|
13496
|
+
kind: "mutation",
|
|
13497
|
+
auth: "admin"
|
|
13498
|
+
})
|
|
13499
|
+
},
|
|
13500
|
+
status: {
|
|
13501
|
+
schema: ImageSettingsStatusSchema,
|
|
13502
|
+
kind: "poll"
|
|
13503
|
+
},
|
|
13504
|
+
runtimeState: ImageSettingsStatusSchema
|
|
13505
|
+
};
|
|
12154
13506
|
/**
|
|
12155
13507
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
12156
13508
|
* with a mowing lifecycle plus a dock action.
|
|
@@ -12175,7 +13527,7 @@ var DeviceCodeSeveritySchema = _enum([
|
|
|
12175
13527
|
"warning",
|
|
12176
13528
|
"error"
|
|
12177
13529
|
]);
|
|
12178
|
-
object({
|
|
13530
|
+
var LawnMowerControlStatusSchema = object({
|
|
12179
13531
|
/** Lifecycle activity of the mower. */
|
|
12180
13532
|
activity: LawnMowerActivitySchema,
|
|
12181
13533
|
/** 0..100 battery percentage. Null when the device has no battery
|
|
@@ -12195,17 +13547,37 @@ object({
|
|
|
12195
13547
|
/** Ms epoch when the slice was last updated. */
|
|
12196
13548
|
lastChangedAt: number()
|
|
12197
13549
|
});
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
13550
|
+
var lawnMowerControlCapability = {
|
|
13551
|
+
name: "lawn-mower-control",
|
|
13552
|
+
scope: "device",
|
|
13553
|
+
deviceNative: true,
|
|
13554
|
+
mode: "singleton",
|
|
13555
|
+
deviceTypes: [DeviceType.LawnMower],
|
|
13556
|
+
methods: {
|
|
13557
|
+
startMowing: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13558
|
+
kind: "mutation",
|
|
13559
|
+
auth: "admin"
|
|
13560
|
+
}),
|
|
13561
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13562
|
+
kind: "mutation",
|
|
13563
|
+
auth: "admin"
|
|
13564
|
+
}),
|
|
13565
|
+
dock: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13566
|
+
kind: "mutation",
|
|
13567
|
+
auth: "admin"
|
|
13568
|
+
})
|
|
13569
|
+
},
|
|
13570
|
+
status: {
|
|
13571
|
+
schema: LawnMowerControlStatusSchema,
|
|
13572
|
+
kind: "push"
|
|
13573
|
+
},
|
|
13574
|
+
/**
|
|
13575
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
13576
|
+
* slice for live activity + battery changes.
|
|
13577
|
+
*/
|
|
13578
|
+
runtimeState: LawnMowerControlStatusSchema
|
|
13579
|
+
};
|
|
13580
|
+
var LockControlStatusSchema = object({
|
|
12209
13581
|
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
12210
13582
|
* failure to reach the target — operator intervention required. */
|
|
12211
13583
|
state: _enum([
|
|
@@ -12218,24 +13590,45 @@ object({
|
|
|
12218
13590
|
/** Ms epoch when the slice was last updated. */
|
|
12219
13591
|
lastChangedAt: number()
|
|
12220
13592
|
});
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
13593
|
+
var lockControlCapability = {
|
|
13594
|
+
name: "lock-control",
|
|
13595
|
+
scope: "device",
|
|
13596
|
+
deviceNative: true,
|
|
13597
|
+
mode: "singleton",
|
|
13598
|
+
deviceTypes: [DeviceType.Lock],
|
|
13599
|
+
methods: {
|
|
13600
|
+
lock: method(object({
|
|
13601
|
+
deviceId: number().int().nonnegative(),
|
|
13602
|
+
/** Optional PIN code required by some keypad locks. NOT
|
|
13603
|
+
* persisted — passed directly to the upstream service. */
|
|
13604
|
+
code: string().min(1).optional()
|
|
13605
|
+
}), _void(), {
|
|
13606
|
+
kind: "mutation",
|
|
13607
|
+
auth: "admin"
|
|
13608
|
+
}),
|
|
13609
|
+
unlock: method(object({
|
|
13610
|
+
deviceId: number().int().nonnegative(),
|
|
13611
|
+
code: string().min(1).optional()
|
|
13612
|
+
}), _void(), {
|
|
13613
|
+
kind: "mutation",
|
|
13614
|
+
auth: "admin"
|
|
13615
|
+
}),
|
|
13616
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13617
|
+
kind: "mutation",
|
|
13618
|
+
auth: "admin"
|
|
13619
|
+
})
|
|
13620
|
+
},
|
|
13621
|
+
status: {
|
|
13622
|
+
schema: LockControlStatusSchema,
|
|
13623
|
+
kind: "push"
|
|
13624
|
+
},
|
|
13625
|
+
/**
|
|
13626
|
+
* Runtime-state slice — mirrored by the kernel. UI lock buttons
|
|
13627
|
+
* read `state` and disable themselves during `locking`/`unlocking`
|
|
13628
|
+
* transitions.
|
|
13629
|
+
*/
|
|
13630
|
+
runtimeState: LockControlStatusSchema
|
|
13631
|
+
};
|
|
12239
13632
|
/**
|
|
12240
13633
|
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
12241
13634
|
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
@@ -12275,7 +13668,7 @@ var MediaInfoSchema = object({
|
|
|
12275
13668
|
/** Optional cover-art / thumbnail URL. */
|
|
12276
13669
|
imageUrl: string().optional()
|
|
12277
13670
|
});
|
|
12278
|
-
object({
|
|
13671
|
+
var MediaPlayerStatusSchema = object({
|
|
12279
13672
|
/** Playback lifecycle state. */
|
|
12280
13673
|
state: MediaPlayerStateSchema,
|
|
12281
13674
|
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
@@ -12306,68 +13699,98 @@ object({
|
|
|
12306
13699
|
/** Ms epoch when the slice was last updated. */
|
|
12307
13700
|
lastChangedAt: number()
|
|
12308
13701
|
});
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
}),
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
}), _void(), {
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
}),
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
}),
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
}), _void(), {
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
})
|
|
13702
|
+
var mediaPlayerCapability = {
|
|
13703
|
+
name: "media-player",
|
|
13704
|
+
scope: "device",
|
|
13705
|
+
deviceNative: true,
|
|
13706
|
+
mode: "singleton",
|
|
13707
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
13708
|
+
methods: {
|
|
13709
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13710
|
+
kind: "mutation",
|
|
13711
|
+
auth: "admin"
|
|
13712
|
+
}),
|
|
13713
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13714
|
+
kind: "mutation",
|
|
13715
|
+
auth: "admin"
|
|
13716
|
+
}),
|
|
13717
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13718
|
+
kind: "mutation",
|
|
13719
|
+
auth: "admin"
|
|
13720
|
+
}),
|
|
13721
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13722
|
+
kind: "mutation",
|
|
13723
|
+
auth: "admin"
|
|
13724
|
+
}),
|
|
13725
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
13726
|
+
kind: "mutation",
|
|
13727
|
+
auth: "admin"
|
|
13728
|
+
}),
|
|
13729
|
+
seek: method(object({
|
|
13730
|
+
deviceId: number().int().nonnegative(),
|
|
13731
|
+
positionMs: number().int().nonnegative()
|
|
13732
|
+
}), _void(), {
|
|
13733
|
+
kind: "mutation",
|
|
13734
|
+
auth: "admin"
|
|
13735
|
+
}),
|
|
13736
|
+
setVolume: method(object({
|
|
13737
|
+
deviceId: number().int().nonnegative(),
|
|
13738
|
+
volumeLevel: number().min(0).max(100)
|
|
13739
|
+
}), _void(), {
|
|
13740
|
+
kind: "mutation",
|
|
13741
|
+
auth: "admin"
|
|
13742
|
+
}),
|
|
13743
|
+
setMute: method(object({
|
|
13744
|
+
deviceId: number().int().nonnegative(),
|
|
13745
|
+
muted: boolean()
|
|
13746
|
+
}), _void(), {
|
|
13747
|
+
kind: "mutation",
|
|
13748
|
+
auth: "admin"
|
|
13749
|
+
}),
|
|
13750
|
+
setShuffle: method(object({
|
|
13751
|
+
deviceId: number().int().nonnegative(),
|
|
13752
|
+
shuffle: boolean()
|
|
13753
|
+
}), _void(), {
|
|
13754
|
+
kind: "mutation",
|
|
13755
|
+
auth: "admin"
|
|
13756
|
+
}),
|
|
13757
|
+
setRepeat: method(object({
|
|
13758
|
+
deviceId: number().int().nonnegative(),
|
|
13759
|
+
repeat: MediaPlayerRepeatSchema
|
|
13760
|
+
}), _void(), {
|
|
13761
|
+
kind: "mutation",
|
|
13762
|
+
auth: "admin"
|
|
13763
|
+
}),
|
|
13764
|
+
selectSource: method(object({
|
|
13765
|
+
deviceId: number().int().nonnegative(),
|
|
13766
|
+
source: string().min(1)
|
|
13767
|
+
}), _void(), {
|
|
13768
|
+
kind: "mutation",
|
|
13769
|
+
auth: "admin"
|
|
13770
|
+
}),
|
|
13771
|
+
playMedia: method(object({
|
|
13772
|
+
deviceId: number().int().nonnegative(),
|
|
13773
|
+
/** Media identifier / URL. */
|
|
13774
|
+
mediaId: string().min(1),
|
|
13775
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
13776
|
+
* passes it through to the upstream service. */
|
|
13777
|
+
mediaType: string().min(1)
|
|
13778
|
+
}), _void(), {
|
|
13779
|
+
kind: "mutation",
|
|
13780
|
+
auth: "admin"
|
|
13781
|
+
})
|
|
13782
|
+
},
|
|
13783
|
+
status: {
|
|
13784
|
+
schema: MediaPlayerStatusSchema,
|
|
13785
|
+
kind: "push"
|
|
13786
|
+
},
|
|
13787
|
+
/**
|
|
13788
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
13789
|
+
* full slice for live now-playing, volume, and progress updates
|
|
13790
|
+
* without polling.
|
|
13791
|
+
*/
|
|
13792
|
+
runtimeState: MediaPlayerStatusSchema
|
|
13793
|
+
};
|
|
12371
13794
|
/** Shared Zod schemas used across detection capabilities. */
|
|
12372
13795
|
/**
|
|
12373
13796
|
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
@@ -12927,25 +14350,58 @@ var ZoneSchema = object({
|
|
|
12927
14350
|
/** Visual color for UI rendering. */
|
|
12928
14351
|
color: string().default("#3b82f6")
|
|
12929
14352
|
});
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
14353
|
+
/**
|
|
14354
|
+
* Zones capability — per-camera CRUD over polygon detection zones.
|
|
14355
|
+
*
|
|
14356
|
+
* Provider lives in `addon-pipeline-orchestrator` (hub-only). Persists
|
|
14357
|
+
* to per-device settings and mirrors into the `zones` device-state
|
|
14358
|
+
* slice on every mutation, so downstream consumers can subscribe via
|
|
14359
|
+
* `dev.state.zones.onChanged`.
|
|
14360
|
+
*
|
|
14361
|
+
* The cap surface only handles geometry + identity; filtering
|
|
14362
|
+
* behaviour (per-class, include/exclude, threshold) lives in the
|
|
14363
|
+
* consumer addons' rule arrays — see `ZoneRuleSchema` exported from
|
|
14364
|
+
* `capabilities/schemas/zone-rule.js`.
|
|
14365
|
+
*/
|
|
14366
|
+
var zonesCapability = {
|
|
14367
|
+
name: "zones",
|
|
14368
|
+
scope: "device",
|
|
14369
|
+
mode: "singleton",
|
|
14370
|
+
deviceTypes: [DeviceType.Camera],
|
|
14371
|
+
methods: {
|
|
14372
|
+
listZones: method(object({ deviceId: number() }), array(ZoneSchema).readonly()),
|
|
14373
|
+
addZone: method(object({
|
|
14374
|
+
deviceId: number(),
|
|
14375
|
+
zone: ZoneSchema
|
|
14376
|
+
}), _void(), {
|
|
14377
|
+
kind: "mutation",
|
|
14378
|
+
auth: "admin"
|
|
14379
|
+
}),
|
|
14380
|
+
removeZone: method(object({
|
|
14381
|
+
deviceId: number(),
|
|
14382
|
+
zoneId: string()
|
|
14383
|
+
}), _void(), {
|
|
14384
|
+
kind: "mutation",
|
|
14385
|
+
auth: "admin"
|
|
14386
|
+
}),
|
|
14387
|
+
updateZone: method(object({
|
|
14388
|
+
deviceId: number(),
|
|
14389
|
+
zone: ZoneSchema
|
|
14390
|
+
}), _void(), {
|
|
14391
|
+
kind: "mutation",
|
|
14392
|
+
auth: "admin"
|
|
14393
|
+
})
|
|
14394
|
+
},
|
|
14395
|
+
/**
|
|
14396
|
+
* Runtime-state slice — the live zone catalogue mirrored by the
|
|
14397
|
+
* orchestrator on every CRUD mutation. Consumers read via
|
|
14398
|
+
* `device.state.zones.value` / `.watch(...)` without round-tripping
|
|
14399
|
+
* the cap, and the codegen DeviceProxy auto-wires the reactive
|
|
14400
|
+
* handle. Slice shape is `{ zones: Zone[] }` so future extensions
|
|
14401
|
+
* (e.g. zone groupings) can sit alongside the polygon list.
|
|
14402
|
+
*/
|
|
14403
|
+
runtimeState: object({ zones: array(ZoneSchema).readonly() })
|
|
14404
|
+
};
|
|
12949
14405
|
/**
|
|
12950
14406
|
* A bounding box in NORMALIZED [0,1] frame coordinates for `getNativeCrop`. The
|
|
12951
14407
|
* decode worker resolves it against the RETAINED native frame's real pixel dims,
|
|
@@ -13361,7 +14817,19 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
13361
14817
|
parent: DetailParentSchema,
|
|
13362
14818
|
steps: array(string()).optional()
|
|
13363
14819
|
}), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
|
|
13364
|
-
|
|
14820
|
+
/**
|
|
14821
|
+
* Hardware / firmware motion sensor cap — binary detected state plus
|
|
14822
|
+
* a timestamp of the last observation. Distinct from
|
|
14823
|
+
* `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
|
|
14824
|
+
* `motion` is the lightweight readout from on-camera motion (Reolink
|
|
14825
|
+
* `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
|
|
14826
|
+
*
|
|
14827
|
+
* Native-motion providers also fan out to `detection.camera-native`
|
|
14828
|
+
* with `source: 'onboard'` so cross-cutting system services
|
|
14829
|
+
* (alert-center, advanced-notifier) can subscribe once and receive
|
|
14830
|
+
* motion from every camera.
|
|
14831
|
+
*/
|
|
14832
|
+
var MotionStatusSchema = object({
|
|
13365
14833
|
detected: boolean(),
|
|
13366
14834
|
/** Ms epoch of the last detected-true observation. Null if never detected. */
|
|
13367
14835
|
lastDetectedAt: number().nullable(),
|
|
@@ -13372,32 +14840,137 @@ object({
|
|
|
13372
14840
|
*/
|
|
13373
14841
|
autoClearAfterMs: number().nullable()
|
|
13374
14842
|
});
|
|
13375
|
-
|
|
14843
|
+
/**
|
|
14844
|
+
* Payload of `motion.onMotionChanged` event + the corresponding bus
|
|
14845
|
+
* event `EventCategory.MotionOnMotionChanged`. Single source of truth
|
|
14846
|
+
* — both the cap event surface and the bus payload type alias to this
|
|
14847
|
+
* schema.
|
|
14848
|
+
*/
|
|
14849
|
+
var MotionOnMotionChangedDataSchema = object({
|
|
13376
14850
|
deviceId: number(),
|
|
13377
14851
|
detected: boolean(),
|
|
13378
14852
|
timestamp: number(),
|
|
13379
14853
|
source: MotionSourceEnum,
|
|
13380
14854
|
regions: array(MotionRegionSchema).readonly().optional()
|
|
13381
14855
|
});
|
|
13382
|
-
|
|
13383
|
-
|
|
14856
|
+
var motionCapability = {
|
|
14857
|
+
name: "motion",
|
|
14858
|
+
scope: "device",
|
|
14859
|
+
mode: "singleton",
|
|
14860
|
+
/**
|
|
14861
|
+
* Providers register per-device natives via `ctx.registerNativeCap`
|
|
14862
|
+
* (Hikvision/Reolink/Amcrest/Wyze/HA/Homematic/Alexa/Matter) — there is
|
|
14863
|
+
* NO system singleton provider. Without this flag `resolveCapMount`
|
|
14864
|
+
* derived `{ kind: 'singleton' }`, so `motion.getStatus`/`isDetected`
|
|
14865
|
+
* resolved via `registry.getSingleton('motion')` (always null) and every
|
|
14866
|
+
* call 412'd "provider not available" while bindings listed a live
|
|
14867
|
+
* `motion` native (2026-08-02). The flag routes the router through
|
|
14868
|
+
* `requireDeviceScoped` → `getProviderForDevice`, like `motion-trigger`,
|
|
14869
|
+
* `snapshot` and every other per-device native cap.
|
|
14870
|
+
*/
|
|
14871
|
+
deviceNative: true,
|
|
14872
|
+
deviceTypes: [DeviceType.Camera, DeviceType.Sensor],
|
|
14873
|
+
methods: {
|
|
14874
|
+
/**
|
|
14875
|
+
* Pull the current motion state synchronously. Convenience shortcut
|
|
14876
|
+
* for consumers that don't need the full status object; equivalent
|
|
14877
|
+
* to `getStatus()?.detected ?? false`. Will likely be folded into
|
|
14878
|
+
* `getStatus` once the auto-injected status surface lands in every
|
|
14879
|
+
* consumer.
|
|
14880
|
+
*/
|
|
14881
|
+
isDetected: method(object({ deviceId: number() }), boolean()) },
|
|
14882
|
+
events: {
|
|
14883
|
+
/**
|
|
14884
|
+
* Fires every time the runner transitions a camera between
|
|
14885
|
+
* `watching` and `active` phases. `source` carries which motion
|
|
14886
|
+
* path drove the transition; `regions` is populated only for
|
|
14887
|
+
* `source: 'analyzer'` (frame-diff regions from the ML motion
|
|
14888
|
+
* detector) — onboard sources don't carry per-frame regions
|
|
14889
|
+
* here (camera-provided zones / AI metadata live in dedicated
|
|
14890
|
+
* channels: `detection.camera-native`, future zone capability).
|
|
14891
|
+
*
|
|
14892
|
+
* Consumers that want all motion pushes (even with `detected`
|
|
14893
|
+
* unchanged) should subscribe to the status subscription via
|
|
14894
|
+
* `device-manager.subscribeDeviceStatusAggregate` instead.
|
|
14895
|
+
*/
|
|
14896
|
+
onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
|
|
14897
|
+
status: {
|
|
14898
|
+
schema: MotionStatusSchema,
|
|
14899
|
+
kind: "push"
|
|
14900
|
+
},
|
|
14901
|
+
/**
|
|
14902
|
+
* Runtime-state slice — the last observed motion snapshot, mirrored
|
|
14903
|
+
* by the kernel and readable cross-process via
|
|
14904
|
+
* `device.state.motion.value`. Reads never invoke the provider, so
|
|
14905
|
+
* UIs and other addons can poll the cached state safely.
|
|
14906
|
+
*/
|
|
14907
|
+
runtimeState: MotionStatusSchema
|
|
14908
|
+
};
|
|
14909
|
+
/**
|
|
14910
|
+
* Motion-trigger toggle for accessory devices.
|
|
14911
|
+
*
|
|
14912
|
+
* "Motion trigger" means: when the parent camera detects motion, the
|
|
14913
|
+
* accessory activates automatically. The cap exposes a single boolean
|
|
14914
|
+
* — `enabled` — that drivers map to the vendor-specific firmware
|
|
14915
|
+
* action (Reolink: `setSirenOnMotion`, `setFloodlightOnMotion`; ONVIF
|
|
14916
|
+
* relay: schedule binding; …).
|
|
14917
|
+
*
|
|
14918
|
+
* The accessory still has its own `switch` cap for direct on/off; this
|
|
14919
|
+
* cap is independent. Toggling motion-trigger on does NOT necessarily
|
|
14920
|
+
* toggle the switch on — it just instructs the firmware to flip the
|
|
14921
|
+
* switch when motion fires.
|
|
14922
|
+
*
|
|
14923
|
+
* Driver-specific knobs (motion duration window, brightness while
|
|
14924
|
+
* triggered, schedule windows) live in the device's
|
|
14925
|
+
* `getSettingsUISchema()` instead of bloating this cap — same
|
|
14926
|
+
* principle as `switch` and `brightness` keeping their surface
|
|
14927
|
+
* minimal.
|
|
14928
|
+
*/
|
|
14929
|
+
var MotionTriggerStatusSchema = object({
|
|
13384
14930
|
enabled: boolean(),
|
|
13385
14931
|
/** Ms epoch of the last operator-driven change. */
|
|
13386
14932
|
lastChangedAt: number()
|
|
13387
|
-
})
|
|
14933
|
+
});
|
|
14934
|
+
/**
|
|
14935
|
+
* Persistent slice mirrored across restarts. The provider writes here
|
|
14936
|
+
* on every successful firmware fetch / setMotionTrigger push; the cap
|
|
14937
|
+
* router and admin-ui hero read straight from this snapshot via
|
|
14938
|
+
* `device.state.motionTrigger.value` instead of re-issuing a firmware
|
|
14939
|
+
* round-trip on every UI mount. `lastFetchedAt` lets the framework
|
|
14940
|
+
* helper (`createRuntimeStateBridge`) stale-check before deciding
|
|
14941
|
+
* whether to refresh from the camera.
|
|
14942
|
+
*/
|
|
14943
|
+
var MotionTriggerRuntimeStateSchema = MotionTriggerStatusSchema.extend({
|
|
13388
14944
|
/** Ms epoch of the last successful camera fetch (0 = never). */
|
|
13389
14945
|
lastFetchedAt: number() });
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
14946
|
+
var motionTriggerCapability = {
|
|
14947
|
+
name: "motion-trigger",
|
|
14948
|
+
scope: "device",
|
|
14949
|
+
deviceNative: true,
|
|
14950
|
+
mode: "singleton",
|
|
14951
|
+
deviceTypes: [
|
|
14952
|
+
DeviceType.Light,
|
|
14953
|
+
DeviceType.Siren,
|
|
14954
|
+
DeviceType.Switch
|
|
14955
|
+
],
|
|
14956
|
+
methods: { setMotionTrigger: method(object({
|
|
14957
|
+
deviceId: number().int().nonnegative(),
|
|
14958
|
+
enabled: boolean()
|
|
14959
|
+
}), _void(), {
|
|
14960
|
+
kind: "mutation",
|
|
14961
|
+
auth: "admin"
|
|
14962
|
+
}) },
|
|
14963
|
+
events: { onMotionTriggerChanged: { data: object({
|
|
14964
|
+
deviceId: number(),
|
|
14965
|
+
enabled: boolean(),
|
|
14966
|
+
lastChangedAt: number()
|
|
14967
|
+
}) } },
|
|
14968
|
+
status: {
|
|
14969
|
+
schema: MotionTriggerStatusSchema,
|
|
14970
|
+
kind: "command-driven"
|
|
14971
|
+
},
|
|
14972
|
+
runtimeState: MotionTriggerRuntimeStateSchema
|
|
14973
|
+
};
|
|
13401
14974
|
/**
|
|
13402
14975
|
* Motion-zones share the same MaskShape vocabulary as privacy-mask — the
|
|
13403
14976
|
* on-camera motion-detection mask is a single `grid` region (a row-major
|
|
@@ -13410,7 +14983,9 @@ var MotionZoneRegionSchema = object({
|
|
|
13410
14983
|
enabled: boolean(),
|
|
13411
14984
|
shape: MaskGridShapeSchema
|
|
13412
14985
|
});
|
|
13413
|
-
|
|
14986
|
+
/** Current on-camera motion-detection state — master enable + sensitivity +
|
|
14987
|
+
* the grid region(s). */
|
|
14988
|
+
var MotionZoneStatusSchema = object({
|
|
13414
14989
|
enabled: boolean(),
|
|
13415
14990
|
sensitivity: number(),
|
|
13416
14991
|
/** Grid region(s). Today exactly one `grid` shape. */
|
|
@@ -13435,13 +15010,34 @@ var MotionZonePatchSchema = object({
|
|
|
13435
15010
|
sensitivity: number().optional(),
|
|
13436
15011
|
regions: array(MotionZoneRegionSchema).optional()
|
|
13437
15012
|
});
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
15013
|
+
var motionZonesCapability = {
|
|
15014
|
+
name: "motion-zones",
|
|
15015
|
+
scope: "device",
|
|
15016
|
+
deviceNative: true,
|
|
15017
|
+
mode: "singleton",
|
|
15018
|
+
deviceTypes: [DeviceType.Camera],
|
|
15019
|
+
deviceConfig: { ui: {
|
|
15020
|
+
kind: "widget",
|
|
15021
|
+
widgetId: "host/motion-zones-grid",
|
|
15022
|
+
tab: "motion",
|
|
15023
|
+
label: "Motion Zones"
|
|
15024
|
+
} },
|
|
15025
|
+
methods: {
|
|
15026
|
+
getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
|
|
15027
|
+
setZone: method(object({
|
|
15028
|
+
deviceId: number(),
|
|
15029
|
+
patch: MotionZonePatchSchema
|
|
15030
|
+
}), _void(), {
|
|
15031
|
+
kind: "mutation",
|
|
15032
|
+
auth: "admin"
|
|
15033
|
+
})
|
|
15034
|
+
},
|
|
15035
|
+
status: {
|
|
15036
|
+
schema: MotionZoneStatusSchema,
|
|
15037
|
+
kind: "poll"
|
|
15038
|
+
},
|
|
15039
|
+
runtimeState: MotionZoneStatusSchema
|
|
15040
|
+
};
|
|
13445
15041
|
/**
|
|
13446
15042
|
* On-camera AI object detection cap. Surfaces per-device the classes
|
|
13447
15043
|
* the firmware can detect and the last-seen instance of each. The
|
|
@@ -13468,7 +15064,7 @@ var NativeDetectionSchema = object({
|
|
|
13468
15064
|
/** Firmware-provided confidence [0..1]. Reolink pushes don't carry it → undefined. */
|
|
13469
15065
|
confidence: number().min(0).max(1).optional()
|
|
13470
15066
|
});
|
|
13471
|
-
object({
|
|
15067
|
+
var NativeObjectDetectionStatusSchema = object({
|
|
13472
15068
|
/**
|
|
13473
15069
|
* Last observed instance per class. Missing entries mean the class
|
|
13474
15070
|
* is supported but nothing has been seen since the provider started.
|
|
@@ -13489,19 +15085,33 @@ object({
|
|
|
13489
15085
|
* churn the tracker, so forwarding stays off until the operator enables it.
|
|
13490
15086
|
*/
|
|
13491
15087
|
enabled: boolean()
|
|
13492
|
-
})
|
|
15088
|
+
});
|
|
15089
|
+
var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
|
|
13493
15090
|
/** Required by createRuntimeStateBridge — epoch ms of last refresh. */
|
|
13494
15091
|
lastFetchedAt: number() });
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
})
|
|
15092
|
+
var nativeObjectDetectionCapability = {
|
|
15093
|
+
name: "native-object-detection",
|
|
15094
|
+
scope: "device",
|
|
15095
|
+
deviceNative: true,
|
|
15096
|
+
mode: "singleton",
|
|
15097
|
+
deviceTypes: [DeviceType.Camera],
|
|
15098
|
+
methods: { setEnabled: method(object({
|
|
15099
|
+
deviceId: number(),
|
|
15100
|
+
enabled: boolean()
|
|
15101
|
+
}), _void(), {
|
|
15102
|
+
kind: "mutation",
|
|
15103
|
+
auth: "admin"
|
|
15104
|
+
}) },
|
|
15105
|
+
events: { onDetected: { data: object({
|
|
15106
|
+
deviceId: number(),
|
|
15107
|
+
detection: NativeDetectionSchema
|
|
15108
|
+
}) } },
|
|
15109
|
+
status: {
|
|
15110
|
+
schema: NativeObjectDetectionStatusSchema,
|
|
15111
|
+
kind: "push"
|
|
15112
|
+
},
|
|
15113
|
+
runtimeState: NativeObjectDetectionRuntimeStateSchema
|
|
15114
|
+
};
|
|
13505
15115
|
/**
|
|
13506
15116
|
* Notification delivery cap. Models HA `notify.<service>` plus future
|
|
13507
15117
|
* native targets (Telegram / Discord / ntfy / SMTP, …). Designed at
|
|
@@ -13553,7 +15163,7 @@ var NotifierSupportsSchema = object({
|
|
|
13553
15163
|
* Pair with `DeviceFeature.NotifierRecipients`. */
|
|
13554
15164
|
recipients: boolean()
|
|
13555
15165
|
});
|
|
13556
|
-
object({
|
|
15166
|
+
var NotifierStatusSchema = object({
|
|
13557
15167
|
/** Ms epoch of the most recent successful send. 0 if none yet. */
|
|
13558
15168
|
lastSentAt: number(),
|
|
13559
15169
|
/** Failure description from the most recent send attempt. Null on
|
|
@@ -13594,23 +15204,67 @@ var NotifierSendResultSchema = object({
|
|
|
13594
15204
|
/** Ms epoch when the notifier accepted the send (not when delivered). */
|
|
13595
15205
|
acceptedAt: number()
|
|
13596
15206
|
});
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
15207
|
+
var notifierCapability = {
|
|
15208
|
+
name: "notifier",
|
|
15209
|
+
scope: "device",
|
|
15210
|
+
deviceNative: true,
|
|
15211
|
+
mode: "singleton",
|
|
15212
|
+
deviceTypes: [DeviceType.Notifier],
|
|
15213
|
+
methods: {
|
|
15214
|
+
send: method(NotifierSendInputSchema, NotifierSendResultSchema, {
|
|
15215
|
+
kind: "mutation",
|
|
15216
|
+
auth: "admin"
|
|
15217
|
+
}),
|
|
15218
|
+
cancel: method(object({
|
|
15219
|
+
deviceId: number().int().nonnegative(),
|
|
15220
|
+
notificationId: string().min(1)
|
|
15221
|
+
}), _void(), {
|
|
15222
|
+
kind: "mutation",
|
|
15223
|
+
auth: "admin"
|
|
15224
|
+
})
|
|
15225
|
+
},
|
|
15226
|
+
events: {
|
|
15227
|
+
/**
|
|
15228
|
+
* Emitted after every send attempt — success or failure. Subscribers
|
|
15229
|
+
* (admin UI history pane, automation engines, retry workers) react
|
|
15230
|
+
* without polling the provider's `lastSentAt`.
|
|
15231
|
+
*/
|
|
15232
|
+
onSent: { data: object({
|
|
15233
|
+
deviceId: number(),
|
|
15234
|
+
notificationId: string(),
|
|
15235
|
+
success: boolean(),
|
|
15236
|
+
error: string().nullable(),
|
|
15237
|
+
acceptedAt: number()
|
|
15238
|
+
}) } },
|
|
15239
|
+
status: {
|
|
15240
|
+
schema: NotifierStatusSchema,
|
|
15241
|
+
kind: "command-driven"
|
|
15242
|
+
},
|
|
15243
|
+
/**
|
|
15244
|
+
* Runtime-state slice — diagnostics + supports matrix. UI compose
|
|
15245
|
+
* form reads `supports` to gate optional fields; history pane reads
|
|
15246
|
+
* `lastSentAt` / `lastError` / `queueDepth`.
|
|
15247
|
+
*/
|
|
15248
|
+
runtimeState: NotifierStatusSchema
|
|
15249
|
+
};
|
|
15250
|
+
/**
|
|
15251
|
+
* Generic numeric sensor — last-resort fallback when no typed numeric
|
|
15252
|
+
* cap fits (HA `sensor` whose `device_class` we don't have a typed cap
|
|
15253
|
+
* for: water flow, distance, weight, frequency, signal strength, …).
|
|
15254
|
+
*
|
|
15255
|
+
* The `unit` and `precision` fields carry the live display metadata that
|
|
15256
|
+
* the upstream source provides with each state push (HA
|
|
15257
|
+
* `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
|
|
15258
|
+
* The UI reads them directly from the slice — they are the single source
|
|
15259
|
+
* of truth for generic numeric rendering and do NOT live on `sourceInfo`.
|
|
15260
|
+
*
|
|
15261
|
+
* Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
|
|
15262
|
+
* `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
|
|
15263
|
+
* `air-quality-sensor`, `battery`) when the metric matches — export
|
|
15264
|
+
* adapters render those with the right service / category. Generic
|
|
15265
|
+
* numeric values land here.
|
|
15266
|
+
*/
|
|
15267
|
+
var NumericSensorStatusSchema = object({
|
|
13614
15268
|
value: number(),
|
|
13615
15269
|
/** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
|
|
13616
15270
|
* Populated live from the upstream source on each state push.
|
|
@@ -13623,7 +15277,19 @@ object({
|
|
|
13623
15277
|
/** Ms epoch when the slice was last updated. */
|
|
13624
15278
|
lastFetchedAt: number()
|
|
13625
15279
|
});
|
|
13626
|
-
|
|
15280
|
+
var numericSensorCapability = {
|
|
15281
|
+
name: "numeric-sensor",
|
|
15282
|
+
scope: "device",
|
|
15283
|
+
deviceNative: true,
|
|
15284
|
+
mode: "singleton",
|
|
15285
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15286
|
+
methods: {},
|
|
15287
|
+
status: {
|
|
15288
|
+
schema: NumericSensorStatusSchema,
|
|
15289
|
+
kind: "push"
|
|
15290
|
+
},
|
|
15291
|
+
runtimeState: NumericSensorStatusSchema
|
|
15292
|
+
};
|
|
13627
15293
|
/**
|
|
13628
15294
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
13629
15295
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
@@ -13635,7 +15301,7 @@ var PetFeederDeviceStatusSchema = _enum([
|
|
|
13635
15301
|
"on_batteries"
|
|
13636
15302
|
]);
|
|
13637
15303
|
var gramsPortion = number().int().min(4).max(200);
|
|
13638
|
-
object({
|
|
15304
|
+
var PetFeederStatusSchema = object({
|
|
13639
15305
|
/** Food currently in the bowl (grams). Null when the device has not
|
|
13640
15306
|
* reported a reading yet. On dual-hopper models this is the combined
|
|
13641
15307
|
* bowl reading; per-hopper levels live in `food1`/`food2`. */
|
|
@@ -13680,58 +15346,117 @@ object({
|
|
|
13680
15346
|
/** Ms epoch when the slice was last refreshed from the cloud. */
|
|
13681
15347
|
lastFetchedAt: number()
|
|
13682
15348
|
});
|
|
13683
|
-
|
|
13684
|
-
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
13693
|
-
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
}), _void(), {
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
}),
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
}), _void(), {
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
})
|
|
13734
|
-
|
|
15349
|
+
var petFeederCapability = {
|
|
15350
|
+
name: "pet-feeder",
|
|
15351
|
+
scope: "device",
|
|
15352
|
+
deviceNative: true,
|
|
15353
|
+
mode: "singleton",
|
|
15354
|
+
deviceTypes: [DeviceType.PetFeeder],
|
|
15355
|
+
methods: {
|
|
15356
|
+
/**
|
|
15357
|
+
* Dispense food now. Single-hopper feeders take `grams`; dual-hopper
|
|
15358
|
+
* feeders (D4S/D4SH) accept `hopper1`/`hopper2` to target one or both
|
|
15359
|
+
* hoppers. All portions honour the 4–200 g hardware range. At least
|
|
15360
|
+
* one of the three must be present — the provider rejects an empty
|
|
15361
|
+
* request.
|
|
15362
|
+
*/
|
|
15363
|
+
feed: method(object({
|
|
15364
|
+
deviceId: number().int().nonnegative(),
|
|
15365
|
+
grams: gramsPortion.optional(),
|
|
15366
|
+
hopper1: gramsPortion.optional(),
|
|
15367
|
+
hopper2: gramsPortion.optional()
|
|
15368
|
+
}), _void(), {
|
|
15369
|
+
kind: "mutation",
|
|
15370
|
+
auth: "admin"
|
|
15371
|
+
}),
|
|
15372
|
+
/** Cancel an in-progress manual feed. */
|
|
15373
|
+
cancelFeed: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15374
|
+
kind: "mutation",
|
|
15375
|
+
auth: "admin"
|
|
15376
|
+
}),
|
|
15377
|
+
/** Reset the desiccant "days remaining" counter after replacing it. */
|
|
15378
|
+
resetDesiccant: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15379
|
+
kind: "mutation",
|
|
15380
|
+
auth: "admin"
|
|
15381
|
+
}),
|
|
15382
|
+
/** Mark a hopper as refilled (D4H/D4S/D4SH). */
|
|
15383
|
+
markFoodReplenished: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15384
|
+
kind: "mutation",
|
|
15385
|
+
auth: "admin"
|
|
15386
|
+
}),
|
|
15387
|
+
/** Call the pet with the recorded prompt (D3). */
|
|
15388
|
+
callPet: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15389
|
+
kind: "mutation",
|
|
15390
|
+
auth: "admin"
|
|
15391
|
+
}),
|
|
15392
|
+
/** Play a stored sound by id (D3 / D4H / D4SH). */
|
|
15393
|
+
playSound: method(object({
|
|
15394
|
+
deviceId: number().int().nonnegative(),
|
|
15395
|
+
soundId: number().int().nonnegative()
|
|
15396
|
+
}), _void(), {
|
|
15397
|
+
kind: "mutation",
|
|
15398
|
+
auth: "admin"
|
|
15399
|
+
}),
|
|
15400
|
+
/** Toggle the child-lock (manual-lock) setting. */
|
|
15401
|
+
setChildLock: method(object({
|
|
15402
|
+
deviceId: number().int().nonnegative(),
|
|
15403
|
+
on: boolean()
|
|
15404
|
+
}), _void(), {
|
|
15405
|
+
kind: "mutation",
|
|
15406
|
+
auth: "admin"
|
|
15407
|
+
}),
|
|
15408
|
+
/** Toggle the front indicator light. */
|
|
15409
|
+
setIndicatorLight: method(object({
|
|
15410
|
+
deviceId: number().int().nonnegative(),
|
|
15411
|
+
on: boolean()
|
|
15412
|
+
}), _void(), {
|
|
15413
|
+
kind: "mutation",
|
|
15414
|
+
auth: "admin"
|
|
15415
|
+
}),
|
|
15416
|
+
/** Toggle the dispense chime. */
|
|
15417
|
+
setFeedSound: method(object({
|
|
15418
|
+
deviceId: number().int().nonnegative(),
|
|
15419
|
+
on: boolean()
|
|
15420
|
+
}), _void(), {
|
|
15421
|
+
kind: "mutation",
|
|
15422
|
+
auth: "admin"
|
|
15423
|
+
}),
|
|
15424
|
+
/** Set the speaker / prompt volume level. */
|
|
15425
|
+
setVolume: method(object({
|
|
15426
|
+
deviceId: number().int().nonnegative(),
|
|
15427
|
+
level: number().int().nonnegative()
|
|
15428
|
+
}), _void(), {
|
|
15429
|
+
kind: "mutation",
|
|
15430
|
+
auth: "admin"
|
|
15431
|
+
})
|
|
15432
|
+
},
|
|
15433
|
+
status: {
|
|
15434
|
+
schema: PetFeederStatusSchema,
|
|
15435
|
+
kind: "poll"
|
|
15436
|
+
},
|
|
15437
|
+
/**
|
|
15438
|
+
* Runtime-state slice — mirrored by the kernel. UI feeder cards read
|
|
15439
|
+
* the full slice via `device.state.petFeeder.value` and refresh on
|
|
15440
|
+
* every poll without re-querying the provider.
|
|
15441
|
+
*/
|
|
15442
|
+
runtimeState: PetFeederStatusSchema
|
|
15443
|
+
};
|
|
15444
|
+
/**
|
|
15445
|
+
* Multi-metric electrical meter. One slice can carry any combination
|
|
15446
|
+
* of instantaneous power (W), cumulative energy (kWh), voltage (V),
|
|
15447
|
+
* and current (A) — all fields optional so a single-metric source
|
|
15448
|
+
* (HA `sensor` with `device_class: power`) populates only what it
|
|
15449
|
+
* has and aggregators (energy dashboards, billing exports) compose
|
|
15450
|
+
* across providers without needing one cap per metric.
|
|
15451
|
+
*
|
|
15452
|
+
* Trade-off acknowledged: a HomeKit export that wants to surface
|
|
15453
|
+
* "power" and "energy" as separate services has to crack open the
|
|
15454
|
+
* slice and emit two characteristics from one cap. Worth it to avoid
|
|
15455
|
+
* exploding the cap catalog with `power-w` / `energy-kwh` /
|
|
15456
|
+
* `voltage-v` / `current-a` quartets that always travel together
|
|
15457
|
+
* in real-world devices.
|
|
15458
|
+
*/
|
|
15459
|
+
var PowerMeterStatusSchema = object({
|
|
13735
15460
|
/** Instantaneous power draw in watts. */
|
|
13736
15461
|
watts: number().optional(),
|
|
13737
15462
|
/** Cumulative energy in kilowatt-hours since the meter was reset. */
|
|
@@ -13758,7 +15483,19 @@ object({
|
|
|
13758
15483
|
* auto-formatting when absent. */
|
|
13759
15484
|
precision: number().int().min(0).max(10).optional()
|
|
13760
15485
|
});
|
|
13761
|
-
|
|
15486
|
+
var powerMeterCapability = {
|
|
15487
|
+
name: "power-meter",
|
|
15488
|
+
scope: "device",
|
|
15489
|
+
deviceNative: true,
|
|
15490
|
+
mode: "singleton",
|
|
15491
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15492
|
+
methods: {},
|
|
15493
|
+
status: {
|
|
15494
|
+
schema: PowerMeterStatusSchema,
|
|
15495
|
+
kind: "push"
|
|
15496
|
+
},
|
|
15497
|
+
runtimeState: PowerMeterStatusSchema
|
|
15498
|
+
};
|
|
13762
15499
|
/**
|
|
13763
15500
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
13764
15501
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -13783,7 +15520,7 @@ var GpsLocationSchema = object({
|
|
|
13783
15520
|
/** Reported accuracy in meters (lower = better). */
|
|
13784
15521
|
accuracyMeters: number().nonnegative()
|
|
13785
15522
|
});
|
|
13786
|
-
object({
|
|
15523
|
+
var PresenceStatusSchema = object({
|
|
13787
15524
|
/** `home` / `not_home` / any user-defined zone name. */
|
|
13788
15525
|
state: string(),
|
|
13789
15526
|
/** Optional textual location label (zone name, city, address). Null
|
|
@@ -13797,8 +15534,34 @@ object({
|
|
|
13797
15534
|
/** Ms epoch when the slice was last updated. */
|
|
13798
15535
|
lastChangedAt: number()
|
|
13799
15536
|
});
|
|
13800
|
-
|
|
13801
|
-
|
|
15537
|
+
var presenceCapability = {
|
|
15538
|
+
name: "presence",
|
|
15539
|
+
scope: "device",
|
|
15540
|
+
deviceNative: true,
|
|
15541
|
+
mode: "singleton",
|
|
15542
|
+
deviceTypes: [DeviceType.Presence],
|
|
15543
|
+
methods: {},
|
|
15544
|
+
status: {
|
|
15545
|
+
schema: PresenceStatusSchema,
|
|
15546
|
+
kind: "push"
|
|
15547
|
+
},
|
|
15548
|
+
/**
|
|
15549
|
+
* Runtime-state slice — mirrored by the kernel. UI presence card
|
|
15550
|
+
* reads `state` + `location` for the text label; iff `gps !== null`
|
|
15551
|
+
* the map pin is rendered (use `DeviceFeature.PresenceGps` for the
|
|
15552
|
+
* pre-fetch fast-path check).
|
|
15553
|
+
*/
|
|
15554
|
+
runtimeState: PresenceStatusSchema
|
|
15555
|
+
};
|
|
15556
|
+
/**
|
|
15557
|
+
* Atmospheric pressure reading in hectopascals. Drives Home Assistant
|
|
15558
|
+
* `sensor` entries with `device_class: pressure`.
|
|
15559
|
+
*
|
|
15560
|
+
* Unit normalisation: hPa. The canonical display unit (`hPa`) is a
|
|
15561
|
+
* descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
15562
|
+
* `sourceInfo`.
|
|
15563
|
+
*/
|
|
15564
|
+
var PressureSensorStatusSchema = object({
|
|
13802
15565
|
/** Current pressure in hPa. */
|
|
13803
15566
|
hpa: number(),
|
|
13804
15567
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -13813,7 +15576,19 @@ object({
|
|
|
13813
15576
|
* auto-formatting when absent. */
|
|
13814
15577
|
precision: number().int().min(0).max(10).optional()
|
|
13815
15578
|
});
|
|
13816
|
-
|
|
15579
|
+
var pressureSensorCapability = {
|
|
15580
|
+
name: "pressure-sensor",
|
|
15581
|
+
scope: "device",
|
|
15582
|
+
deviceNative: true,
|
|
15583
|
+
mode: "singleton",
|
|
15584
|
+
deviceTypes: [DeviceType.Sensor],
|
|
15585
|
+
methods: {},
|
|
15586
|
+
status: {
|
|
15587
|
+
schema: PressureSensorStatusSchema,
|
|
15588
|
+
kind: "push"
|
|
15589
|
+
},
|
|
15590
|
+
runtimeState: PressureSensorStatusSchema
|
|
15591
|
+
};
|
|
13817
15592
|
/**
|
|
13818
15593
|
* Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
|
|
13819
15594
|
* cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
|
|
@@ -13832,7 +15607,8 @@ var PrivacyMaskRegionSchema = object({
|
|
|
13832
15607
|
enabled: boolean(),
|
|
13833
15608
|
shape: PrivacyMaskShapeSchema
|
|
13834
15609
|
});
|
|
13835
|
-
|
|
15610
|
+
/** Current on-camera privacy-mask state — master enable + zones. */
|
|
15611
|
+
var PrivacyMaskStatusSchema = object({
|
|
13836
15612
|
enabled: boolean(),
|
|
13837
15613
|
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
13838
15614
|
regions: array(PrivacyMaskRegionSchema),
|
|
@@ -13852,13 +15628,34 @@ var PrivacyMaskPatchSchema = object({
|
|
|
13852
15628
|
enabled: boolean().optional(),
|
|
13853
15629
|
regions: array(PrivacyMaskRegionSchema).optional()
|
|
13854
15630
|
});
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
15631
|
+
var privacyMaskCapability = {
|
|
15632
|
+
name: "privacy-mask",
|
|
15633
|
+
scope: "device",
|
|
15634
|
+
deviceNative: true,
|
|
15635
|
+
mode: "singleton",
|
|
15636
|
+
deviceTypes: [DeviceType.Camera],
|
|
15637
|
+
deviceConfig: { ui: {
|
|
15638
|
+
kind: "widget",
|
|
15639
|
+
widgetId: "host/privacy-mask-grid",
|
|
15640
|
+
tab: "image",
|
|
15641
|
+
label: "Privacy Mask"
|
|
15642
|
+
} },
|
|
15643
|
+
methods: {
|
|
15644
|
+
getOptions: method(object({ deviceId: number() }), PrivacyMaskOptionsSchema),
|
|
15645
|
+
setMask: method(object({
|
|
15646
|
+
deviceId: number(),
|
|
15647
|
+
patch: PrivacyMaskPatchSchema
|
|
15648
|
+
}), _void(), {
|
|
15649
|
+
kind: "mutation",
|
|
15650
|
+
auth: "admin"
|
|
15651
|
+
})
|
|
15652
|
+
},
|
|
15653
|
+
status: {
|
|
15654
|
+
schema: PrivacyMaskStatusSchema,
|
|
15655
|
+
kind: "poll"
|
|
15656
|
+
},
|
|
15657
|
+
runtimeState: PrivacyMaskStatusSchema
|
|
15658
|
+
};
|
|
13862
15659
|
var PtzAutotrackSettingsSchema = object({
|
|
13863
15660
|
targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
|
|
13864
15661
|
stopDelaySeconds: number().int().min(0).max(300),
|
|
@@ -13898,19 +15695,85 @@ var PtzAutotrackStatusSchema = object({
|
|
|
13898
15695
|
*/
|
|
13899
15696
|
supportedTargetTypes: array(PtzAutotrackTargetOptionSchema)
|
|
13900
15697
|
});
|
|
13901
|
-
|
|
15698
|
+
/**
|
|
15699
|
+
* Runtime-state slice owned by this cap. Persists the last-known
|
|
15700
|
+
* camera-derived snapshot across restarts, so cold-start callers
|
|
15701
|
+
* have a meaningful baseline even before the first refresh round-trip.
|
|
15702
|
+
*
|
|
15703
|
+
* Adds `lastFetchedAt` on top of the status shape so the framework
|
|
15704
|
+
* (or the provider) can stale-check before deciding whether to
|
|
15705
|
+
* round-trip the camera again.
|
|
15706
|
+
*/
|
|
15707
|
+
var PtzAutotrackRuntimeStateSchema = PtzAutotrackStatusSchema.extend({
|
|
13902
15708
|
/** Ms epoch of the last successful camera fetch (0 = never). */
|
|
13903
15709
|
lastFetchedAt: number() });
|
|
13904
|
-
|
|
13905
|
-
|
|
13906
|
-
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
15710
|
+
var ptzAutotrackCapability = {
|
|
15711
|
+
name: "ptz-autotrack",
|
|
15712
|
+
scope: "device",
|
|
15713
|
+
deviceNative: true,
|
|
15714
|
+
mode: "singleton",
|
|
15715
|
+
deviceTypes: [DeviceType.Camera],
|
|
15716
|
+
deviceConfig: { ui: {
|
|
15717
|
+
kind: "widget",
|
|
15718
|
+
widgetId: "host/ptz-autotrack",
|
|
15719
|
+
tab: "ptz",
|
|
15720
|
+
topTab: true,
|
|
15721
|
+
label: "Auto-Tracking",
|
|
15722
|
+
order: 5
|
|
15723
|
+
} },
|
|
15724
|
+
methods: {
|
|
15725
|
+
/**
|
|
15726
|
+
* Read the current on/off state + last-applied settings.
|
|
15727
|
+
* Drivers may serve from a cache + refresh on a schedule —
|
|
15728
|
+
* callers should NOT poll faster than ~5s.
|
|
15729
|
+
*/
|
|
15730
|
+
getStatus: method(object({ deviceId: number() }), PtzAutotrackStatusSchema),
|
|
15731
|
+
/**
|
|
15732
|
+
* Arm or disarm autotrack. Resolves once the camera has
|
|
15733
|
+
* acknowledged the transition; the `onStatusChanged` event
|
|
15734
|
+
* fires shortly after with the live state.
|
|
15735
|
+
*/
|
|
15736
|
+
setEnabled: method(object({
|
|
15737
|
+
deviceId: number(),
|
|
15738
|
+
enabled: boolean()
|
|
15739
|
+
}), _void(), { kind: "mutation" }),
|
|
15740
|
+
/**
|
|
15741
|
+
* Read settings (target type + delays). Mirrors the
|
|
15742
|
+
* `getStatus().currentSettings` payload but exposes it as a
|
|
15743
|
+
* standalone read for callers that want settings without the
|
|
15744
|
+
* status surface around them. `null` until the driver has
|
|
15745
|
+
* harvested the firmware state at least once.
|
|
15746
|
+
*/
|
|
15747
|
+
getSettings: method(object({ deviceId: number() }), PtzAutotrackSettingsSchema.nullable()),
|
|
15748
|
+
/**
|
|
15749
|
+
* Update one or more settings. Partial — keys that aren't
|
|
15750
|
+
* supplied keep their last persisted value. The driver is
|
|
15751
|
+
* responsible for clamping vendor-specific ranges if narrower
|
|
15752
|
+
* than the cross-vendor max declared on the schema.
|
|
15753
|
+
*/
|
|
15754
|
+
setSettings: method(object({
|
|
15755
|
+
deviceId: number(),
|
|
15756
|
+
settings: PtzAutotrackSettingsSchema.partial()
|
|
15757
|
+
}), _void(), { kind: "mutation" })
|
|
15758
|
+
},
|
|
15759
|
+
events: { onStatusChanged: { data: object({
|
|
15760
|
+
deviceId: number(),
|
|
15761
|
+
status: PtzAutotrackStatusSchema
|
|
15762
|
+
}) } },
|
|
15763
|
+
status: {
|
|
15764
|
+
schema: PtzAutotrackStatusSchema,
|
|
15765
|
+
kind: "command-driven"
|
|
15766
|
+
},
|
|
15767
|
+
/**
|
|
15768
|
+
* Persistent slice mirrored across restarts. The provider writes
|
|
15769
|
+
* here on every successful firmware fetch / settings push;
|
|
15770
|
+
* `getStatus` and `getSettings` then read this snapshot synchronously
|
|
15771
|
+
* with a stale-driven async refresh in the background. Keeps the
|
|
15772
|
+
* fetch / cache / fallback logic out of the four cap methods —
|
|
15773
|
+
* they become trampolines over `runtimeState`.
|
|
15774
|
+
*/
|
|
15775
|
+
runtimeState: PtzAutotrackRuntimeStateSchema
|
|
15776
|
+
};
|
|
13914
15777
|
/**
|
|
13915
15778
|
* scene-monitor — device-scoped reference-region state cap. An operator marks
|
|
13916
15779
|
* a rect ROI on a camera frame and names one or more states; the engine
|
|
@@ -13980,72 +15843,108 @@ var SceneMonitorStatusSchema = object({
|
|
|
13980
15843
|
monitors: array(SceneMonitorSchema),
|
|
13981
15844
|
lastFetchedAt: number()
|
|
13982
15845
|
});
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
"
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
}),
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
})
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
}),
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
15846
|
+
var sceneMonitorCapability = {
|
|
15847
|
+
name: "scene-monitor",
|
|
15848
|
+
scope: "device",
|
|
15849
|
+
mode: "singleton",
|
|
15850
|
+
kind: "wrapper",
|
|
15851
|
+
defaultActive: true,
|
|
15852
|
+
deviceTypes: [DeviceType.Camera],
|
|
15853
|
+
deviceConfig: { ui: {
|
|
15854
|
+
kind: "widget",
|
|
15855
|
+
widgetId: "host/scene-monitor-editor",
|
|
15856
|
+
tab: "scenes",
|
|
15857
|
+
label: "Scenes"
|
|
15858
|
+
} },
|
|
15859
|
+
methods: {
|
|
15860
|
+
listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
|
|
15861
|
+
createScene: method(object({
|
|
15862
|
+
deviceId: number(),
|
|
15863
|
+
label: string(),
|
|
15864
|
+
roi: MaskRectShapeSchema,
|
|
15865
|
+
check: SceneCheckSchema,
|
|
15866
|
+
triggerMode: _enum([
|
|
15867
|
+
"periodic",
|
|
15868
|
+
"on-motion",
|
|
15869
|
+
"both"
|
|
15870
|
+
]).optional(),
|
|
15871
|
+
checkIntervalSec: number().optional()
|
|
15872
|
+
}), SceneMonitorSchema, {
|
|
15873
|
+
kind: "mutation",
|
|
15874
|
+
auth: "admin"
|
|
15875
|
+
}),
|
|
15876
|
+
updateScene: method(object({
|
|
15877
|
+
deviceId: number(),
|
|
15878
|
+
monitorId: string(),
|
|
15879
|
+
patch: object({
|
|
15880
|
+
label: string().optional(),
|
|
15881
|
+
roi: MaskRectShapeSchema.optional(),
|
|
15882
|
+
enabled: boolean().optional(),
|
|
15883
|
+
triggerMode: _enum([
|
|
15884
|
+
"periodic",
|
|
15885
|
+
"on-motion",
|
|
15886
|
+
"both"
|
|
15887
|
+
]).optional(),
|
|
15888
|
+
checkIntervalSec: number().optional(),
|
|
15889
|
+
check: SceneCheckSchema.optional()
|
|
15890
|
+
})
|
|
15891
|
+
}), _void(), {
|
|
15892
|
+
kind: "mutation",
|
|
15893
|
+
auth: "admin"
|
|
15894
|
+
}),
|
|
15895
|
+
deleteScene: method(object({
|
|
15896
|
+
deviceId: number(),
|
|
15897
|
+
monitorId: string()
|
|
15898
|
+
}), _void(), {
|
|
15899
|
+
kind: "mutation",
|
|
15900
|
+
auth: "admin"
|
|
15901
|
+
}),
|
|
15902
|
+
captureReference: method(object({
|
|
15903
|
+
deviceId: number(),
|
|
15904
|
+
monitorId: string(),
|
|
15905
|
+
stateId: string().optional(),
|
|
15906
|
+
label: string().optional(),
|
|
15907
|
+
condition: SceneConditionSchema.optional()
|
|
15908
|
+
}), object({
|
|
15909
|
+
stateId: string(),
|
|
15910
|
+
referenceCount: number()
|
|
15911
|
+
}), {
|
|
15912
|
+
kind: "mutation",
|
|
15913
|
+
auth: "admin"
|
|
15914
|
+
}),
|
|
15915
|
+
deleteReference: method(object({
|
|
15916
|
+
deviceId: number(),
|
|
15917
|
+
monitorId: string(),
|
|
15918
|
+
stateId: string(),
|
|
15919
|
+
index: number()
|
|
15920
|
+
}), _void(), {
|
|
15921
|
+
kind: "mutation",
|
|
15922
|
+
auth: "admin"
|
|
15923
|
+
}),
|
|
15924
|
+
recheckNow: method(object({
|
|
15925
|
+
deviceId: number(),
|
|
15926
|
+
monitorId: string()
|
|
15927
|
+
}), _void(), {
|
|
15928
|
+
kind: "mutation",
|
|
15929
|
+
auth: "admin"
|
|
15930
|
+
})
|
|
15931
|
+
},
|
|
15932
|
+
status: {
|
|
15933
|
+
schema: SceneMonitorStatusSchema,
|
|
15934
|
+
kind: "push"
|
|
15935
|
+
},
|
|
15936
|
+
runtimeState: SceneMonitorStatusSchema
|
|
15937
|
+
};
|
|
15938
|
+
/**
|
|
15939
|
+
* Script-runner cap. Models HA `script.*` entities on
|
|
15940
|
+
* `DeviceType.Script`. A Script is a pre-recorded action sequence
|
|
15941
|
+
* that can be invoked imperatively — optionally with a variables
|
|
15942
|
+
* map when the script declares input fields.
|
|
15943
|
+
*
|
|
15944
|
+
* Variable-support is signalled by `DeviceFeature.ScriptVariables`
|
|
15945
|
+
* so the UI gates a "with parameters" form without a slice fetch.
|
|
15946
|
+
*/
|
|
15947
|
+
var ScriptRunnerStatusSchema = object({
|
|
14049
15948
|
/** Whether the script is currently executing. */
|
|
14050
15949
|
isRunning: boolean(),
|
|
14051
15950
|
/** Ms epoch of the last invocation start. 0 when never run. */
|
|
@@ -14059,25 +15958,68 @@ object({
|
|
|
14059
15958
|
/** Ms epoch when the slice was last updated. */
|
|
14060
15959
|
lastChangedAt: number()
|
|
14061
15960
|
});
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
15961
|
+
var scriptRunnerCapability = {
|
|
15962
|
+
name: "script-runner",
|
|
15963
|
+
scope: "device",
|
|
15964
|
+
deviceNative: true,
|
|
15965
|
+
mode: "singleton",
|
|
15966
|
+
deviceTypes: [DeviceType.Script],
|
|
15967
|
+
methods: {
|
|
15968
|
+
run: method(object({
|
|
15969
|
+
deviceId: number().int().nonnegative(),
|
|
15970
|
+
/** Optional variables map — passed through to the upstream
|
|
15971
|
+
* script. Provider rejects when the script doesn't declare
|
|
15972
|
+
* input fields (gated by `DeviceFeature.ScriptVariables`). */
|
|
15973
|
+
variables: record(string(), unknown()).optional()
|
|
15974
|
+
}), _void(), {
|
|
15975
|
+
kind: "mutation",
|
|
15976
|
+
auth: "admin"
|
|
15977
|
+
}),
|
|
15978
|
+
/** Cancel a running script. Provider rejects when the script
|
|
15979
|
+
* isn't currently running. */
|
|
15980
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
15981
|
+
kind: "mutation",
|
|
15982
|
+
auth: "admin"
|
|
15983
|
+
})
|
|
15984
|
+
},
|
|
15985
|
+
status: {
|
|
15986
|
+
schema: ScriptRunnerStatusSchema,
|
|
15987
|
+
kind: "push"
|
|
15988
|
+
},
|
|
15989
|
+
/**
|
|
15990
|
+
* Runtime-state slice — mirrored by the kernel. UI script tile reads
|
|
15991
|
+
* `isRunning` to render a spinner during execution and surfaces
|
|
15992
|
+
* `lastError` / `lastRunSuccess` in the recent-runs panel.
|
|
15993
|
+
*/
|
|
15994
|
+
runtimeState: ScriptRunnerStatusSchema
|
|
15995
|
+
};
|
|
15996
|
+
/**
|
|
15997
|
+
* Smoke alarm sensor — boolean "is smoke currently detected" with
|
|
15998
|
+
* timestamp of the last transition. Drives Home Assistant
|
|
15999
|
+
* `binary_sensor` entries with `device_class: smoke`.
|
|
16000
|
+
*
|
|
16001
|
+
* Push-driven: a smoke event is critical, so the slice updates
|
|
16002
|
+
* immediately on the upstream signal. Auto-clearing back to false is
|
|
16003
|
+
* provider-controlled (some alarms latch until manually reset).
|
|
16004
|
+
*/
|
|
16005
|
+
var SmokeStatusSchema = object({
|
|
14076
16006
|
detected: boolean(),
|
|
14077
16007
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
14078
16008
|
lastChangedAt: number()
|
|
14079
16009
|
});
|
|
14080
|
-
|
|
16010
|
+
var smokeCapability = {
|
|
16011
|
+
name: "smoke",
|
|
16012
|
+
scope: "device",
|
|
16013
|
+
deviceNative: true,
|
|
16014
|
+
mode: "singleton",
|
|
16015
|
+
deviceTypes: [DeviceType.Sensor],
|
|
16016
|
+
methods: {},
|
|
16017
|
+
status: {
|
|
16018
|
+
schema: SmokeStatusSchema,
|
|
16019
|
+
kind: "push"
|
|
16020
|
+
},
|
|
16021
|
+
runtimeState: SmokeStatusSchema
|
|
16022
|
+
};
|
|
14081
16023
|
/** One of the camera's stream profiles. */
|
|
14082
16024
|
var StreamProfileSchema = _enum([
|
|
14083
16025
|
"main",
|
|
@@ -14100,7 +16042,7 @@ var StreamProfileConfigSchema = object({
|
|
|
14100
16042
|
gop: number().optional(),
|
|
14101
16043
|
audio: boolean().optional()
|
|
14102
16044
|
});
|
|
14103
|
-
object({
|
|
16045
|
+
var StreamParamsStatusSchema = object({
|
|
14104
16046
|
/** Per-profile current config. A profile absent = the camera doesn't have it. */
|
|
14105
16047
|
main: StreamProfileConfigSchema.optional(),
|
|
14106
16048
|
sub: StreamProfileConfigSchema.optional(),
|
|
@@ -14154,39 +16096,150 @@ var StreamProfilePatchSchema = object({
|
|
|
14154
16096
|
gop: number().optional(),
|
|
14155
16097
|
audio: boolean().optional()
|
|
14156
16098
|
});
|
|
14157
|
-
|
|
14158
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
|
|
16099
|
+
var streamParamsCapability = {
|
|
16100
|
+
name: "stream-params",
|
|
16101
|
+
scope: "device",
|
|
16102
|
+
deviceNative: true,
|
|
16103
|
+
mode: "singleton",
|
|
16104
|
+
deviceTypes: [DeviceType.Camera],
|
|
16105
|
+
deviceConfig: { ui: {
|
|
16106
|
+
kind: "derived-form",
|
|
16107
|
+
builderId: "stream-params",
|
|
16108
|
+
tab: "streaming"
|
|
16109
|
+
} },
|
|
16110
|
+
methods: {
|
|
16111
|
+
getOptions: method(object({ deviceId: number() }), StreamParamsOptionsSchema),
|
|
16112
|
+
setProfile: method(object({
|
|
16113
|
+
deviceId: number(),
|
|
16114
|
+
profile: StreamProfileSchema,
|
|
16115
|
+
patch: StreamProfilePatchSchema
|
|
16116
|
+
}), _void(), {
|
|
16117
|
+
kind: "mutation",
|
|
16118
|
+
auth: "admin"
|
|
16119
|
+
}),
|
|
16120
|
+
/**
|
|
16121
|
+
* Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
|
|
16122
|
+
* shape) for this camera's stream-encoder settings — one section per
|
|
16123
|
+
* profile (main / sub / ext) with the resolution / codec / framerate
|
|
16124
|
+
* / bitrate / bitrate-mode / encoder-profile / GOP controls the
|
|
16125
|
+
* firmware actually exposes.
|
|
16126
|
+
*
|
|
16127
|
+
* Driven by `getOptions` (camera-probed availability) + `getStatus`
|
|
16128
|
+
* (current per-profile config); each field's `default` is seeded
|
|
16129
|
+
* from the live config so the form renders the camera state in one
|
|
16130
|
+
* pass. Returns `null` when the camera exposes no configurable
|
|
16131
|
+
* stream property — the renderer then shows the unsupported message.
|
|
16132
|
+
*
|
|
16133
|
+
* Output is `z.unknown().nullable()` — the same convention every
|
|
16134
|
+
* other `ConfigUISchema`-returning cap method uses (`device-ops`,
|
|
16135
|
+
* `device-manager`); `ConfigUISchema` is a TS-only type with no
|
|
16136
|
+
* companion Zod schema, and a concrete object would collapse
|
|
16137
|
+
* unrelated AppRouter branches to `unknown` during codegen.
|
|
16138
|
+
*/
|
|
16139
|
+
getConfigSchema: method(object({ deviceId: number() }), unknown().nullable())
|
|
16140
|
+
},
|
|
16141
|
+
status: {
|
|
16142
|
+
schema: StreamParamsStatusSchema,
|
|
16143
|
+
kind: "poll"
|
|
16144
|
+
},
|
|
16145
|
+
runtimeState: StreamParamsStatusSchema
|
|
16146
|
+
};
|
|
16147
|
+
/**
|
|
16148
|
+
* Generic on/off switch cap for accessory children (siren, floodlight,
|
|
16149
|
+
* spotlight, PIR toggle, chime silencer, autotrack enable, …). The cap
|
|
16150
|
+
* owns ONLY the boolean state — driver-specific settings (brightness,
|
|
16151
|
+
* sensitivity, schedule, duration) live on the device's own
|
|
16152
|
+
* `getSettingsUISchema()` surface.
|
|
16153
|
+
*/
|
|
16154
|
+
var SwitchStatusSchema = object({
|
|
14166
16155
|
on: boolean(),
|
|
14167
16156
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
14168
16157
|
lastChangedAt: number()
|
|
14169
16158
|
});
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
16159
|
+
var switchCapability = {
|
|
16160
|
+
name: "switch",
|
|
16161
|
+
scope: "device",
|
|
16162
|
+
deviceNative: true,
|
|
16163
|
+
mode: "singleton",
|
|
16164
|
+
deviceTypes: [
|
|
16165
|
+
DeviceType.Switch,
|
|
16166
|
+
DeviceType.Siren,
|
|
16167
|
+
DeviceType.Light
|
|
16168
|
+
],
|
|
16169
|
+
methods: { setState: method(object({
|
|
16170
|
+
deviceId: number(),
|
|
16171
|
+
on: boolean()
|
|
16172
|
+
}), _void(), {
|
|
16173
|
+
kind: "mutation",
|
|
16174
|
+
auth: "admin"
|
|
16175
|
+
}) },
|
|
16176
|
+
events: { onStateChanged: { data: object({
|
|
16177
|
+
deviceId: number(),
|
|
16178
|
+
on: boolean(),
|
|
16179
|
+
lastChangedAt: number()
|
|
16180
|
+
}) } },
|
|
16181
|
+
status: {
|
|
16182
|
+
schema: SwitchStatusSchema,
|
|
16183
|
+
kind: "command-driven"
|
|
16184
|
+
},
|
|
16185
|
+
/**
|
|
16186
|
+
* Runtime-state slice — the last applied on/off state, mirrored by
|
|
16187
|
+
* the kernel. Read via `device.state.switch.value`; UI toggles do
|
|
16188
|
+
* not need to re-query the provider after a setState mutation.
|
|
16189
|
+
*/
|
|
16190
|
+
runtimeState: SwitchStatusSchema,
|
|
16191
|
+
settings: { bindings: [{
|
|
16192
|
+
kind: "scalar",
|
|
16193
|
+
statusPath: "on",
|
|
16194
|
+
method: "setState",
|
|
16195
|
+
valueArg: "on",
|
|
16196
|
+
field: {
|
|
16197
|
+
label: "On",
|
|
16198
|
+
kind: "boolean"
|
|
16199
|
+
}
|
|
16200
|
+
}] }
|
|
16201
|
+
};
|
|
16202
|
+
/**
|
|
16203
|
+
* Tamper / case-open detection sensor. Drives Home Assistant
|
|
16204
|
+
* `binary_sensor` entries with `device_class: tamper`. Push-driven.
|
|
16205
|
+
*/
|
|
16206
|
+
var TamperStatusSchema = object({
|
|
14182
16207
|
/** True when the device's tamper switch / case-open contact is
|
|
14183
16208
|
* currently triggered. */
|
|
14184
16209
|
tampered: boolean(),
|
|
14185
16210
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
14186
16211
|
lastChangedAt: number()
|
|
14187
16212
|
});
|
|
14188
|
-
|
|
14189
|
-
|
|
16213
|
+
var tamperCapability = {
|
|
16214
|
+
name: "tamper",
|
|
16215
|
+
scope: "device",
|
|
16216
|
+
deviceNative: true,
|
|
16217
|
+
mode: "singleton",
|
|
16218
|
+
deviceTypes: [DeviceType.Sensor],
|
|
16219
|
+
methods: {},
|
|
16220
|
+
status: {
|
|
16221
|
+
schema: TamperStatusSchema,
|
|
16222
|
+
kind: "push"
|
|
16223
|
+
},
|
|
16224
|
+
runtimeState: TamperStatusSchema
|
|
16225
|
+
};
|
|
16226
|
+
/**
|
|
16227
|
+
* Single-metric temperature reading. Drives Home Assistant `sensor`
|
|
16228
|
+
* entries with `device_class: temperature` and any future native
|
|
16229
|
+
* thermometer.
|
|
16230
|
+
*
|
|
16231
|
+
* Unit normalisation: providers convert to Celsius before storing.
|
|
16232
|
+
* The slice value is always Celsius so cross-cap aggregators
|
|
16233
|
+
* (climate-control's `currentTemp`, energy analytics) can compose
|
|
16234
|
+
* without per-source unit fixups. The canonical display unit (`°C`) is
|
|
16235
|
+
* a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
|
|
16236
|
+
* `sourceInfo`.
|
|
16237
|
+
*
|
|
16238
|
+
* Status `lastFetchedAt` lets staleness-aware consumers detect a
|
|
16239
|
+
* frozen feed (provider hung) distinct from a "temperature hasn't
|
|
16240
|
+
* changed" steady state.
|
|
16241
|
+
*/
|
|
16242
|
+
var TemperatureSensorStatusSchema = object({
|
|
14190
16243
|
/** Current temperature in Celsius. */
|
|
14191
16244
|
celsius: number(),
|
|
14192
16245
|
/** Ms epoch when the slice was last updated (push or poll). */
|
|
@@ -14202,8 +16255,29 @@ object({
|
|
|
14202
16255
|
* auto-formatting when absent. */
|
|
14203
16256
|
precision: number().int().min(0).max(10).optional()
|
|
14204
16257
|
});
|
|
14205
|
-
|
|
14206
|
-
|
|
16258
|
+
var temperatureSensorCapability = {
|
|
16259
|
+
name: "temperature-sensor",
|
|
16260
|
+
scope: "device",
|
|
16261
|
+
deviceNative: true,
|
|
16262
|
+
mode: "singleton",
|
|
16263
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
|
|
16264
|
+
methods: {},
|
|
16265
|
+
status: {
|
|
16266
|
+
schema: TemperatureSensorStatusSchema,
|
|
16267
|
+
kind: "push"
|
|
16268
|
+
},
|
|
16269
|
+
runtimeState: TemperatureSensorStatusSchema
|
|
16270
|
+
};
|
|
16271
|
+
/**
|
|
16272
|
+
* Firmware/software update entity. Installed on a `DeviceType.Update` device.
|
|
16273
|
+
* Surfaces current vs available version, whether an update is available, the
|
|
16274
|
+
* update state, and an install action. Read-mostly and POLL-driven: backends
|
|
16275
|
+
* such as the Homematic CCU do not broadcast firmware-descriptor changes, so
|
|
16276
|
+
* the client re-queries `getStatus` (which reads the live descriptor) rather
|
|
16277
|
+
* than relying on push delivery. `installUpdate` triggers the install
|
|
16278
|
+
* (best-effort — providers throw if the backend rejects it).
|
|
16279
|
+
*/
|
|
16280
|
+
var UpdateStatusSchema = object({
|
|
14207
16281
|
currentVersion: string().nullable(),
|
|
14208
16282
|
availableVersion: string().nullable(),
|
|
14209
16283
|
/**
|
|
@@ -14216,10 +16290,22 @@ object({
|
|
|
14216
16290
|
state: string().nullable(),
|
|
14217
16291
|
inProgress: boolean()
|
|
14218
16292
|
});
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
16293
|
+
var updateCapability = {
|
|
16294
|
+
name: "update",
|
|
16295
|
+
scope: "device",
|
|
16296
|
+
deviceNative: true,
|
|
16297
|
+
mode: "singleton",
|
|
16298
|
+
deviceTypes: [DeviceType.Update],
|
|
16299
|
+
methods: { installUpdate: method(_void(), _void(), {
|
|
16300
|
+
kind: "mutation",
|
|
16301
|
+
auth: "admin"
|
|
16302
|
+
}) },
|
|
16303
|
+
status: {
|
|
16304
|
+
schema: UpdateStatusSchema,
|
|
16305
|
+
kind: "poll"
|
|
16306
|
+
},
|
|
16307
|
+
runtimeState: UpdateStatusSchema
|
|
16308
|
+
};
|
|
14223
16309
|
/**
|
|
14224
16310
|
* Robot-vacuum cap. Models HA `vacuum.*` entities — anything with a
|
|
14225
16311
|
* cleaning lifecycle plus a return-to-base / locate surface and an
|
|
@@ -14271,7 +16357,7 @@ var TankStatusSchema = object({
|
|
|
14271
16357
|
"full"
|
|
14272
16358
|
]).nullable()
|
|
14273
16359
|
});
|
|
14274
|
-
object({
|
|
16360
|
+
var VacuumControlStatusSchema = object({
|
|
14275
16361
|
/** Lifecycle state of the vacuum. */
|
|
14276
16362
|
state: VacuumStateSchema,
|
|
14277
16363
|
/** 0..100 battery percentage. Null when the device has no battery
|
|
@@ -14299,29 +16385,52 @@ object({
|
|
|
14299
16385
|
/** Ms epoch when the slice was last updated. */
|
|
14300
16386
|
lastChangedAt: number()
|
|
14301
16387
|
});
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
|
|
14320
|
-
}),
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
16388
|
+
var vacuumControlCapability = {
|
|
16389
|
+
name: "vacuum-control",
|
|
16390
|
+
scope: "device",
|
|
16391
|
+
deviceNative: true,
|
|
16392
|
+
mode: "singleton",
|
|
16393
|
+
deviceTypes: [DeviceType.Vacuum],
|
|
16394
|
+
methods: {
|
|
16395
|
+
start: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16396
|
+
kind: "mutation",
|
|
16397
|
+
auth: "admin"
|
|
16398
|
+
}),
|
|
16399
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16400
|
+
kind: "mutation",
|
|
16401
|
+
auth: "admin"
|
|
16402
|
+
}),
|
|
16403
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16404
|
+
kind: "mutation",
|
|
16405
|
+
auth: "admin"
|
|
16406
|
+
}),
|
|
16407
|
+
returnToBase: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16408
|
+
kind: "mutation",
|
|
16409
|
+
auth: "admin"
|
|
16410
|
+
}),
|
|
16411
|
+
locate: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16412
|
+
kind: "mutation",
|
|
16413
|
+
auth: "admin"
|
|
16414
|
+
}),
|
|
16415
|
+
setFanSpeed: method(object({
|
|
16416
|
+
deviceId: number().int().nonnegative(),
|
|
16417
|
+
speed: string().min(1)
|
|
16418
|
+
}), _void(), {
|
|
16419
|
+
kind: "mutation",
|
|
16420
|
+
auth: "admin"
|
|
16421
|
+
})
|
|
16422
|
+
},
|
|
16423
|
+
status: {
|
|
16424
|
+
schema: VacuumControlStatusSchema,
|
|
16425
|
+
kind: "push"
|
|
16426
|
+
},
|
|
16427
|
+
/**
|
|
16428
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16429
|
+
* slice for live state + battery + fan-speed changes.
|
|
16430
|
+
*/
|
|
16431
|
+
runtimeState: VacuumControlStatusSchema
|
|
16432
|
+
};
|
|
16433
|
+
var ValveStatusSchema = object({
|
|
14325
16434
|
/** Lifecycle state of the valve. */
|
|
14326
16435
|
state: _enum([
|
|
14327
16436
|
"open",
|
|
@@ -14336,29 +16445,80 @@ object({
|
|
|
14336
16445
|
/** Ms epoch when the slice was last updated. */
|
|
14337
16446
|
lastChangedAt: number()
|
|
14338
16447
|
});
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
})
|
|
14355
|
-
|
|
16448
|
+
var valveCapability = {
|
|
16449
|
+
name: "valve",
|
|
16450
|
+
scope: "device",
|
|
16451
|
+
deviceNative: true,
|
|
16452
|
+
mode: "singleton",
|
|
16453
|
+
deviceTypes: [DeviceType.Valve],
|
|
16454
|
+
methods: {
|
|
16455
|
+
open: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16456
|
+
kind: "mutation",
|
|
16457
|
+
auth: "admin"
|
|
16458
|
+
}),
|
|
16459
|
+
close: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16460
|
+
kind: "mutation",
|
|
16461
|
+
auth: "admin"
|
|
16462
|
+
}),
|
|
16463
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
16464
|
+
kind: "mutation",
|
|
16465
|
+
auth: "admin"
|
|
16466
|
+
}),
|
|
16467
|
+
setPosition: method(object({
|
|
16468
|
+
deviceId: number().int().nonnegative(),
|
|
16469
|
+
position: number().min(0).max(100)
|
|
16470
|
+
}), _void(), {
|
|
16471
|
+
kind: "mutation",
|
|
16472
|
+
auth: "admin"
|
|
16473
|
+
})
|
|
16474
|
+
},
|
|
16475
|
+
status: {
|
|
16476
|
+
schema: ValveStatusSchema,
|
|
16477
|
+
kind: "push"
|
|
16478
|
+
},
|
|
16479
|
+
/**
|
|
16480
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16481
|
+
* slice for live position changes during a move.
|
|
16482
|
+
*/
|
|
16483
|
+
runtimeState: ValveStatusSchema
|
|
16484
|
+
};
|
|
16485
|
+
/**
|
|
16486
|
+
* Vibration / shake / impact sensor. Drives Home Assistant
|
|
16487
|
+
* `binary_sensor` entries with `device_class: vibration`. Push-driven.
|
|
16488
|
+
*/
|
|
16489
|
+
var VibrationStatusSchema = object({
|
|
14356
16490
|
detected: boolean(),
|
|
14357
16491
|
/** Ms epoch of the last transition. 0 if never observed. */
|
|
14358
16492
|
lastChangedAt: number()
|
|
14359
16493
|
});
|
|
14360
|
-
|
|
14361
|
-
|
|
16494
|
+
var vibrationCapability = {
|
|
16495
|
+
name: "vibration",
|
|
16496
|
+
scope: "device",
|
|
16497
|
+
deviceNative: true,
|
|
16498
|
+
mode: "singleton",
|
|
16499
|
+
deviceTypes: [DeviceType.Sensor],
|
|
16500
|
+
methods: {},
|
|
16501
|
+
status: {
|
|
16502
|
+
schema: VibrationStatusSchema,
|
|
16503
|
+
kind: "push"
|
|
16504
|
+
},
|
|
16505
|
+
runtimeState: VibrationStatusSchema
|
|
16506
|
+
};
|
|
16507
|
+
/**
|
|
16508
|
+
* Water heater / boiler cap. Models HA `water_heater.*` entities — a
|
|
16509
|
+
* climate-family actuator with a target temperature, an operation-mode
|
|
16510
|
+
* selector (`eco` / `electric` / `gas` / `heat_pump` / `high_demand` /
|
|
16511
|
+
* `performance` / `off`), and an optional away-mode toggle.
|
|
16512
|
+
*
|
|
16513
|
+
* The slice carries the current + target temperature, the active
|
|
16514
|
+
* operation mode (HA's `state`) and its available set (HA's
|
|
16515
|
+
* `operation_list`), the away flag (HA `away_mode` 'on'/'off' → bool,
|
|
16516
|
+
* null when unsupported), plus the `min_temp` / `max_temp` bounds.
|
|
16517
|
+
*
|
|
16518
|
+
* Providers populate only what the hardware reports — temperature and
|
|
16519
|
+
* away fields stay null when the device has no such surface.
|
|
16520
|
+
*/
|
|
16521
|
+
var WaterHeaterStatusSchema = object({
|
|
14362
16522
|
/** Current measured temperature. Null when not reported. */
|
|
14363
16523
|
currentTemp: number().nullable(),
|
|
14364
16524
|
/** Target temperature setpoint. Null when no setpoint surface. */
|
|
@@ -14379,26 +16539,65 @@ object({
|
|
|
14379
16539
|
/** Ms epoch when the slice was last updated. */
|
|
14380
16540
|
lastChangedAt: number()
|
|
14381
16541
|
});
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
16542
|
+
var waterHeaterCapability = {
|
|
16543
|
+
name: "water-heater",
|
|
16544
|
+
scope: "device",
|
|
16545
|
+
deviceNative: true,
|
|
16546
|
+
mode: "singleton",
|
|
16547
|
+
deviceTypes: [DeviceType.WaterHeater],
|
|
16548
|
+
methods: {
|
|
16549
|
+
setTargetTemp: method(object({
|
|
16550
|
+
deviceId: number().int().nonnegative(),
|
|
16551
|
+
temp: number().finite()
|
|
16552
|
+
}), _void(), {
|
|
16553
|
+
kind: "mutation",
|
|
16554
|
+
auth: "admin"
|
|
16555
|
+
}),
|
|
16556
|
+
setOperationMode: method(object({
|
|
16557
|
+
deviceId: number().int().nonnegative(),
|
|
16558
|
+
mode: string().min(1)
|
|
16559
|
+
}), _void(), {
|
|
16560
|
+
kind: "mutation",
|
|
16561
|
+
auth: "admin"
|
|
16562
|
+
}),
|
|
16563
|
+
setAway: method(object({
|
|
16564
|
+
deviceId: number().int().nonnegative(),
|
|
16565
|
+
on: boolean()
|
|
16566
|
+
}), _void(), {
|
|
16567
|
+
kind: "mutation",
|
|
16568
|
+
auth: "admin"
|
|
16569
|
+
})
|
|
16570
|
+
},
|
|
16571
|
+
status: {
|
|
16572
|
+
schema: WaterHeaterStatusSchema,
|
|
16573
|
+
kind: "push"
|
|
16574
|
+
},
|
|
16575
|
+
/**
|
|
16576
|
+
* Runtime-state slice — mirrored by the kernel. UI controls watch the
|
|
16577
|
+
* slice for live temperature / mode / away changes.
|
|
16578
|
+
*/
|
|
16579
|
+
runtimeState: WaterHeaterStatusSchema
|
|
16580
|
+
};
|
|
16581
|
+
/**
|
|
16582
|
+
* Weather provider cap. Models HA `weather.*` entities — a read-only
|
|
16583
|
+
* snapshot of the CURRENT conditions a weather integration reports.
|
|
16584
|
+
*
|
|
16585
|
+
* Read-only: there are no setters. The slice is populated from upstream
|
|
16586
|
+
* pushes (the HA weather entity's state + attributes) and rendered by
|
|
16587
|
+
* the UI as a sky scene + readouts.
|
|
16588
|
+
*
|
|
16589
|
+
* `condition` is the verbatim HA state string (`sunny` / `cloudy` /
|
|
16590
|
+
* `rainy` / `snowy` / `partlycloudy` / `pouring` / `lightning` /
|
|
16591
|
+
* `lightning-rainy` / `fog` / `windy` / `windy-variant` / `hail` /
|
|
16592
|
+
* `clear-night` / `exceptional` / …). The UI maps it to a glyph + tint;
|
|
16593
|
+
* unknown strings fall back to a neutral cloud.
|
|
16594
|
+
*
|
|
16595
|
+
* Every numeric reading is nullable — a given weather integration only
|
|
16596
|
+
* populates the metrics it actually provides.
|
|
16597
|
+
*
|
|
16598
|
+
* Forecast deferred — current conditions only for v1.
|
|
16599
|
+
*/
|
|
16600
|
+
var WeatherStatusSchema = object({
|
|
14402
16601
|
/** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
|
|
14403
16602
|
* when no condition has been reported yet. */
|
|
14404
16603
|
condition: string().nullable(),
|
|
@@ -14421,7 +16620,23 @@ object({
|
|
|
14421
16620
|
/** Ms epoch when the slice was last updated. */
|
|
14422
16621
|
lastFetchedAt: number()
|
|
14423
16622
|
});
|
|
14424
|
-
|
|
16623
|
+
var weatherCapability = {
|
|
16624
|
+
name: "weather",
|
|
16625
|
+
scope: "device",
|
|
16626
|
+
deviceNative: true,
|
|
16627
|
+
mode: "singleton",
|
|
16628
|
+
deviceTypes: [DeviceType.Weather],
|
|
16629
|
+
methods: {},
|
|
16630
|
+
status: {
|
|
16631
|
+
schema: WeatherStatusSchema,
|
|
16632
|
+
kind: "push"
|
|
16633
|
+
},
|
|
16634
|
+
/**
|
|
16635
|
+
* Runtime-state slice — mirrored by the kernel. The UI reads the
|
|
16636
|
+
* current conditions directly from the slice on each weather push.
|
|
16637
|
+
*/
|
|
16638
|
+
runtimeState: WeatherStatusSchema
|
|
16639
|
+
};
|
|
14425
16640
|
/**
|
|
14426
16641
|
* Per-zone occupancy aggregation produced by the analytics frame
|
|
14427
16642
|
* processor on every inference result. Covers the full combinatorial
|
|
@@ -14684,21 +16899,604 @@ var ZoneRuleStageEnum = _enum([
|
|
|
14684
16899
|
"detection",
|
|
14685
16900
|
"package"
|
|
14686
16901
|
]);
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
16902
|
+
/**
|
|
16903
|
+
* Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
|
|
16904
|
+
* `state` getter looks up the cap definition here to construct a
|
|
16905
|
+
* `sliceProxy()` lazily on first access. Generated alongside the type
|
|
16906
|
+
* so type and runtime registry can never drift apart.
|
|
16907
|
+
*/
|
|
16908
|
+
var DEVICE_LOCAL_STATE_CAPS = {
|
|
16909
|
+
airQualitySensor: airQualitySensorCapability,
|
|
16910
|
+
alarmPanel: alarmPanelCapability,
|
|
16911
|
+
ambientLightSensor: ambientLightSensorCapability,
|
|
16912
|
+
audioMetrics: audioMetricsCapability,
|
|
16913
|
+
automationControl: automationControlCapability,
|
|
16914
|
+
battery: batteryCapability,
|
|
16915
|
+
binary: binaryCapability,
|
|
16916
|
+
brightness: brightnessCapability,
|
|
16917
|
+
cameraStreams: cameraStreamsCapability,
|
|
16918
|
+
carbonMonoxide: carbonMonoxideCapability,
|
|
16919
|
+
climateControl: climateControlCapability,
|
|
16920
|
+
color: colorCapability,
|
|
16921
|
+
connectivity: connectivityCapability,
|
|
16922
|
+
consumables: consumablesCapability,
|
|
16923
|
+
contact: contactCapability,
|
|
16924
|
+
control: controlCapability,
|
|
16925
|
+
cover: coverCapability,
|
|
16926
|
+
dayNight: dayNightCapability,
|
|
16927
|
+
deviceDiscovery: deviceDiscoveryCapability,
|
|
16928
|
+
deviceStatus: deviceStatusCapability,
|
|
16929
|
+
doorbell: doorbellCapability,
|
|
16930
|
+
enumSensor: enumSensorCapability,
|
|
16931
|
+
eventEmitter: eventEmitterCapability,
|
|
16932
|
+
fanControl: fanControlCapability,
|
|
16933
|
+
featureProbe: featureProbeCapability,
|
|
16934
|
+
flood: floodCapability,
|
|
16935
|
+
gas: gasCapability,
|
|
16936
|
+
humidifier: humidifierCapability,
|
|
16937
|
+
humiditySensor: humiditySensorCapability,
|
|
16938
|
+
image: imageCapability,
|
|
16939
|
+
imageSettings: imageSettingsCapability,
|
|
16940
|
+
lawnMowerControl: lawnMowerControlCapability,
|
|
16941
|
+
lockControl: lockControlCapability,
|
|
16942
|
+
mediaPlayer: mediaPlayerCapability,
|
|
16943
|
+
motion: motionCapability,
|
|
16944
|
+
motionTrigger: motionTriggerCapability,
|
|
16945
|
+
motionZones: motionZonesCapability,
|
|
16946
|
+
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
16947
|
+
notifier: notifierCapability,
|
|
16948
|
+
numericSensor: numericSensorCapability,
|
|
16949
|
+
petFeeder: petFeederCapability,
|
|
16950
|
+
powerMeter: powerMeterCapability,
|
|
16951
|
+
presence: presenceCapability,
|
|
16952
|
+
pressureSensor: pressureSensorCapability,
|
|
16953
|
+
privacyMask: privacyMaskCapability,
|
|
16954
|
+
ptzAutotrack: ptzAutotrackCapability,
|
|
16955
|
+
sceneMonitor: sceneMonitorCapability,
|
|
16956
|
+
scriptRunner: scriptRunnerCapability,
|
|
16957
|
+
smoke: smokeCapability,
|
|
16958
|
+
streamParams: streamParamsCapability,
|
|
16959
|
+
switch: switchCapability,
|
|
16960
|
+
tamper: tamperCapability,
|
|
16961
|
+
temperatureSensor: temperatureSensorCapability,
|
|
16962
|
+
update: updateCapability,
|
|
16963
|
+
vacuumControl: vacuumControlCapability,
|
|
16964
|
+
valve: valveCapability,
|
|
16965
|
+
vibration: vibrationCapability,
|
|
16966
|
+
waterHeater: waterHeaterCapability,
|
|
16967
|
+
weather: weatherCapability,
|
|
16968
|
+
zoneAnalytics: zoneAnalyticsCapability,
|
|
16969
|
+
zoneRules: {
|
|
16970
|
+
name: "zone-rules",
|
|
16971
|
+
scope: "device",
|
|
16972
|
+
mode: "singleton",
|
|
16973
|
+
deviceTypes: [DeviceType.Camera],
|
|
16974
|
+
methods: {
|
|
16975
|
+
/** Read the full rule list for a given stage (empty when no rules
|
|
16976
|
+
* are defined yet). */
|
|
16977
|
+
listRules: method(object({
|
|
16978
|
+
deviceId: number(),
|
|
16979
|
+
stage: ZoneRuleStageEnum
|
|
16980
|
+
}), array(ZoneRuleSchema).readonly()),
|
|
16981
|
+
/** Bulk-replace the rule list for one stage. The provider validates
|
|
16982
|
+
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
16983
|
+
* thresholds in range) and rejects the whole patch if any entry
|
|
16984
|
+
* is invalid — partial writes are a configuration footgun. */
|
|
16985
|
+
setRules: method(object({
|
|
16986
|
+
deviceId: number(),
|
|
16987
|
+
stage: ZoneRuleStageEnum,
|
|
16988
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
16989
|
+
}), _void(), {
|
|
16990
|
+
kind: "mutation",
|
|
16991
|
+
auth: "admin"
|
|
16992
|
+
})
|
|
16993
|
+
},
|
|
16994
|
+
/**
|
|
16995
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
16996
|
+
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
16997
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
16998
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
16999
|
+
* complete current set. Consumers that only care about one stage
|
|
17000
|
+
* just read the matching property.
|
|
17001
|
+
*
|
|
17002
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
17003
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
17004
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
17005
|
+
* The orchestrator provider writes this stage as a first-class slice
|
|
17006
|
+
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
17007
|
+
* package}` shape, so consumers read the current package rules directly
|
|
17008
|
+
* off `device.state.zoneRules.value.package`.
|
|
17009
|
+
*/
|
|
17010
|
+
runtimeState: object({
|
|
17011
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
17012
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
17013
|
+
package: array(ZoneRuleSchema).readonly()
|
|
17014
|
+
})
|
|
17015
|
+
},
|
|
17016
|
+
zones: zonesCapability
|
|
17017
|
+
};
|
|
17018
|
+
var BaseDevice = class {
|
|
17019
|
+
id;
|
|
17020
|
+
stableId;
|
|
17021
|
+
type;
|
|
17022
|
+
name;
|
|
17023
|
+
parentDeviceId;
|
|
17024
|
+
role;
|
|
17025
|
+
/**
|
|
17026
|
+
* Cap-keyed runtime-state slice is the single source of truth for
|
|
17027
|
+
* `online`. Both getter and setter proxy to the slice — drivers can
|
|
17028
|
+
* write `this.online = true` ergonomically, and the cap event fires
|
|
17029
|
+
* automatically through the runtime-state writer. `markOnline()` is
|
|
17030
|
+
* kept as the explicit method form mandated by `IDevice`.
|
|
17031
|
+
*/
|
|
17032
|
+
get online() {
|
|
17033
|
+
return this.runtimeState.getCapState("device-status")?.online ?? false;
|
|
17034
|
+
}
|
|
17035
|
+
set online(value) {
|
|
17036
|
+
this.markOnline(value);
|
|
17037
|
+
}
|
|
17038
|
+
/**
|
|
17039
|
+
* Generic per-cap runtime-state namespace. One entry per cap with
|
|
17040
|
+
* `runtimeState:` declared, auto-generated by codegen — see
|
|
17041
|
+
* `device-local-state.ts`. Drivers access via:
|
|
17042
|
+
*
|
|
17043
|
+
* `this.state.battery.sleeping = true` // patches the battery slice
|
|
17044
|
+
* `const pct = this.state.battery.percentage` // reads the battery slice
|
|
17045
|
+
* `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
|
|
17046
|
+
*
|
|
17047
|
+
* Adding a new cap with `runtimeState:` automatically extends this
|
|
17048
|
+
* namespace — drivers don't have to declare proxies. Reads return
|
|
17049
|
+
* `undefined` when the slice hasn't been seeded; writes patch via
|
|
17050
|
+
* `runtimeState.patchCapState` and validate against the cap's schema
|
|
17051
|
+
* (so partial writes need the slice to be seeded with the required
|
|
17052
|
+
* fields first — drivers do this on cap registration).
|
|
17053
|
+
*
|
|
17054
|
+
* For caps not exposed in `DeviceLocalState`, drivers can build their
|
|
17055
|
+
* own typed proxy via `this.sliceProxy(cap)`.
|
|
17056
|
+
*/
|
|
17057
|
+
get state() {
|
|
17058
|
+
if (!this._stateProxyCache) {
|
|
17059
|
+
const cache = {};
|
|
17060
|
+
const handler = { get: (_target, key) => {
|
|
17061
|
+
const k = key;
|
|
17062
|
+
if (k in cache) return cache[k];
|
|
17063
|
+
const cap = DEVICE_LOCAL_STATE_CAPS[k];
|
|
17064
|
+
if (!cap) return void 0;
|
|
17065
|
+
const proxy = this.sliceProxy(cap);
|
|
17066
|
+
cache[k] = proxy;
|
|
17067
|
+
return proxy;
|
|
17068
|
+
} };
|
|
17069
|
+
this._stateProxyCache = new Proxy(cache, handler);
|
|
17070
|
+
}
|
|
17071
|
+
return this._stateProxyCache;
|
|
17072
|
+
}
|
|
17073
|
+
_stateProxyCache;
|
|
17074
|
+
config;
|
|
17075
|
+
/**
|
|
17076
|
+
* Per-device runtime state, cap-keyed. Always installed — slices
|
|
17077
|
+
* for individual caps materialise as those caps register their
|
|
17078
|
+
* native providers (`ctx.registerNativeCap`). The cap's own
|
|
17079
|
+
* `runtimeState` schema is the source of truth for the slice
|
|
17080
|
+
* shape; drivers don't redeclare it, they just write through.
|
|
17081
|
+
*
|
|
17082
|
+
* Read: `this.runtimeState.getCapState('battery')` →
|
|
17083
|
+
* `{percentage, charging, sleeping, lastUpdated}` for any
|
|
17084
|
+
* provider that registers `batteryCapability`.
|
|
17085
|
+
* Write: `this.runtimeState.setCapState('battery', { … })`.
|
|
17086
|
+
*
|
|
17087
|
+
* Cross-process consumers reach this state through the
|
|
17088
|
+
* `deviceState` cap router (or via cap-specific events the driver
|
|
17089
|
+
* emits — e.g. `battery.onStatusChanged`). The local handle is
|
|
17090
|
+
* accessed in-process by the driver to avoid roundtrips.
|
|
17091
|
+
*/
|
|
17092
|
+
runtimeState;
|
|
17093
|
+
ctx;
|
|
17094
|
+
/**
|
|
17095
|
+
* Operator-organisational location label (room / area / zone).
|
|
17096
|
+
* Read from `ctx.deviceMeta.location`; mutated via
|
|
17097
|
+
* `kernel.devices.setLocation(id, value)`. Free-text — providers
|
|
17098
|
+
* don't interpret it; the UI groups devices by this for filters
|
|
17099
|
+
* like "show me all cameras in Kitchen". `null` when unset.
|
|
17100
|
+
*/
|
|
17101
|
+
location;
|
|
17102
|
+
/**
|
|
17103
|
+
* Soft-disabled flag. When `true`, the device class is still
|
|
17104
|
+
* instantiated and visible in the UI (so the operator can flip
|
|
17105
|
+
* back on without re-adding) but lifecycle hooks (publishToBroker,
|
|
17106
|
+
* alarm-stream subscribe, …) MUST be gated by the driver to skip
|
|
17107
|
+
* work. The `BaseDevice` enforces this by exposing the flag here;
|
|
17108
|
+
* it does NOT mutate cap behaviour automatically — drivers consult
|
|
17109
|
+
* `this.disabled` at the top of their lifecycle methods. Read from
|
|
17110
|
+
* `ctx.deviceMeta.disabled`; mutated via
|
|
17111
|
+
* `kernel.devices.setDisabled(id, value)`.
|
|
17112
|
+
*/
|
|
17113
|
+
disabled;
|
|
17114
|
+
/**
|
|
17115
|
+
* Cached materialised `SourceInfo` — either the value persisted under
|
|
17116
|
+
* `metadata.sourceInfo` at construction time, or a synthetic
|
|
17117
|
+
* `{ id: stableId, system: addonId }` for providers that haven't
|
|
17118
|
+
* migrated yet. Lazily populated on first `sourceInfo` read so the
|
|
17119
|
+
* cost of Zod-parsing the meta blob is paid once per device boot.
|
|
17120
|
+
* Invalidated by `updateSourceInfo()` so providers see the new value
|
|
17121
|
+
* back through the getter without a re-fetch from the meta surface.
|
|
17122
|
+
*/
|
|
17123
|
+
_sourceInfoCache = null;
|
|
17124
|
+
constructor(ctx, schema, options) {
|
|
17125
|
+
this.ctx = ctx;
|
|
17126
|
+
this.id = ctx.id;
|
|
17127
|
+
this.stableId = ctx.stableId;
|
|
17128
|
+
this.type = options.type;
|
|
17129
|
+
if (!ctx.deviceMeta) throw new Error(`BaseDevice constructor: ctx.deviceMeta is required (id=${ctx.id} stableId=${ctx.stableId})`);
|
|
17130
|
+
this.name = ctx.deviceMeta.name;
|
|
17131
|
+
this.location = ctx.deviceMeta.location;
|
|
17132
|
+
this.disabled = ctx.deviceMeta.disabled;
|
|
17133
|
+
this.role = options.role;
|
|
17134
|
+
this.parentDeviceId = ctx.parentDeviceId;
|
|
17135
|
+
const seedData = ctx.persistedConfig ?? {};
|
|
17136
|
+
this.config = DeviceConfig.fromSchema(schema, (data) => ctx.persistConfig(data), seedData, ({ droppedKeys, issues }) => {
|
|
17137
|
+
ctx.logger.warn("Device config recovery: dropping invalid persisted fields", {
|
|
17138
|
+
tags: {
|
|
17139
|
+
deviceId: ctx.id,
|
|
17140
|
+
stableId: ctx.stableId
|
|
17141
|
+
},
|
|
17142
|
+
meta: {
|
|
17143
|
+
droppedKeys: [...droppedKeys],
|
|
17144
|
+
firstIssue: issues[0]?.message ?? null
|
|
17145
|
+
}
|
|
17146
|
+
});
|
|
17147
|
+
});
|
|
17148
|
+
let cachedProxy = null;
|
|
17149
|
+
const writer = async (capName, slice) => {
|
|
17150
|
+
if (!cachedProxy) cachedProxy = ctx.fetchDevice(ctx.id);
|
|
17151
|
+
await (await cachedProxy).deviceState.setCapSlice({
|
|
17152
|
+
capName,
|
|
17153
|
+
slice
|
|
17154
|
+
});
|
|
17155
|
+
};
|
|
17156
|
+
const initial = ctx.initialRuntimeState ?? {};
|
|
17157
|
+
this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer);
|
|
17158
|
+
ctx.bindRuntimeState?.(this.runtimeState);
|
|
17159
|
+
ctx.registerNativeCap?.(deviceStatusCapability, {});
|
|
17160
|
+
const seed = {
|
|
17161
|
+
online: true,
|
|
17162
|
+
lastChangedAt: Date.now()
|
|
17163
|
+
};
|
|
17164
|
+
this.runtimeState.setCapState("device-status", seed);
|
|
17165
|
+
ctx.registerNativeCap?.(featureProbeCapability, {});
|
|
17166
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
17167
|
+
flags: {},
|
|
17168
|
+
deviceType: null,
|
|
17169
|
+
model: null,
|
|
17170
|
+
channelCount: null,
|
|
17171
|
+
lastProbedAt: 0,
|
|
17172
|
+
lastFetchedAt: 0
|
|
17173
|
+
});
|
|
17174
|
+
}
|
|
17175
|
+
deviceActions = /* @__PURE__ */ new Map();
|
|
17176
|
+
/** Declare a device custom action + its typed handler. Idempotent per name. */
|
|
17177
|
+
registerDeviceAction(name, spec, handler) {
|
|
17178
|
+
this.deviceActions.set(name, {
|
|
17179
|
+
spec,
|
|
17180
|
+
handler
|
|
17181
|
+
});
|
|
17182
|
+
}
|
|
17183
|
+
/** Invoke a registered device action. Validates input against the spec. */
|
|
17184
|
+
async runDeviceAction(action, input) {
|
|
17185
|
+
const entry = this.deviceActions.get(action);
|
|
17186
|
+
if (!entry) throw new Error(`unknown device action "${action}" on device ${this.id}`);
|
|
17187
|
+
const parsed = entry.spec.input.parse(input);
|
|
17188
|
+
return entry.handler(parsed);
|
|
17189
|
+
}
|
|
17190
|
+
async removeDevice() {}
|
|
17191
|
+
/**
|
|
17192
|
+
* Set the device's online flag. Called by `BaseDeviceProvider` after
|
|
17193
|
+
* aggregating per-profile stream-broker health, or directly by drivers
|
|
17194
|
+
* that have provider-side liveness signals (e.g. ONVIF heartbeats,
|
|
17195
|
+
* Reolink Baichuan firmware push events). Mirrors the new value into
|
|
17196
|
+
* the `device-status` runtime-state slice so cross-process consumers
|
|
17197
|
+
* pick it up via the standard cap-state channel. Subclasses can
|
|
17198
|
+
* override to gate side effects on the transition.
|
|
17199
|
+
*/
|
|
17200
|
+
markOnline(online) {
|
|
17201
|
+
if (this.online === online) return;
|
|
17202
|
+
const next = {
|
|
17203
|
+
online,
|
|
17204
|
+
lastChangedAt: Date.now()
|
|
17205
|
+
};
|
|
17206
|
+
this.runtimeState.setCapState("device-status", next);
|
|
17207
|
+
}
|
|
17208
|
+
/**
|
|
17209
|
+
* Upstream-system identity + rendering envelope for this device. See
|
|
17210
|
+
* `SourceInfo` for the field contract. Always returns a valid object:
|
|
17211
|
+
* if the persisted `metadata.sourceInfo` blob is absent or fails Zod
|
|
17212
|
+
* validation, falls back to a synthetic `{ id: stableId, system: addonId }`
|
|
17213
|
+
* so providers that haven't migrated keep working without code changes.
|
|
17214
|
+
*
|
|
17215
|
+
* The value is cached after the first read. `updateSourceInfo()`
|
|
17216
|
+
* invalidates the cache so subsequent reads see the new patch. The
|
|
17217
|
+
* returned object is frozen to prevent accidental in-place mutation —
|
|
17218
|
+
* use `updateSourceInfo({ patch })` to change fields.
|
|
17219
|
+
*/
|
|
17220
|
+
get sourceInfo() {
|
|
17221
|
+
if (this._sourceInfoCache) return this._sourceInfoCache;
|
|
17222
|
+
const resolved = extractSourceInfoFromMetadata(this.ctx.deviceMeta.metadata) ?? synthesizeSourceInfo({
|
|
17223
|
+
stableId: this.stableId,
|
|
17224
|
+
addonId: this.ctx.deviceMeta.addonId
|
|
17225
|
+
});
|
|
17226
|
+
this._sourceInfoCache = Object.freeze({ ...resolved });
|
|
17227
|
+
return this._sourceInfoCache;
|
|
17228
|
+
}
|
|
17229
|
+
/**
|
|
17230
|
+
* Convenience accessor for the upstream dispatch key. Equivalent to
|
|
17231
|
+
* `this.sourceInfo.id` — providers use this to keep a
|
|
17232
|
+
* `Map<sourceId, IDevice>` for routing inbound push events.
|
|
17233
|
+
*/
|
|
17234
|
+
get sourceId() {
|
|
17235
|
+
return this.sourceInfo.id;
|
|
17236
|
+
}
|
|
17237
|
+
/**
|
|
17238
|
+
* Patch the device's `SourceInfo`. Shallow-merges `patch` over the
|
|
17239
|
+
* current value, persists the merged result under
|
|
17240
|
+
* `metadata.sourceInfo` via the `device-manager.setMetadata` cap, and
|
|
17241
|
+
* emits `EventCategory.DeviceSourceInfoChanged` for live consumers.
|
|
17242
|
+
*
|
|
17243
|
+
* Safe to call from anywhere in the device's lifetime — the call is
|
|
17244
|
+
* idempotent for `undefined` patch values (ignored) and best-effort
|
|
17245
|
+
* for persistence (a transient device-manager error doesn't unwind
|
|
17246
|
+
* the local cache update, so subsequent reads still see the patch).
|
|
17247
|
+
*
|
|
17248
|
+
* Drivers populate this on adoption + on every metadata change push
|
|
17249
|
+
* from the upstream source. Subscribers (UI, export adapters) react
|
|
17250
|
+
* via the `DeviceSourceInfoChanged` event without polling.
|
|
17251
|
+
*/
|
|
17252
|
+
async updateSourceInfo(patch) {
|
|
17253
|
+
const next = mergeSourceInfo(this.sourceInfo, patch);
|
|
17254
|
+
this._sourceInfoCache = Object.freeze({ ...next });
|
|
17255
|
+
const action = this.ctx.api?.deviceManager?.setMetadata;
|
|
17256
|
+
if (action) try {
|
|
17257
|
+
await action.mutate({
|
|
17258
|
+
deviceId: this.id,
|
|
17259
|
+
patch: { [SOURCE_INFO_METADATA_KEY]: next }
|
|
17260
|
+
});
|
|
17261
|
+
} catch {}
|
|
17262
|
+
this.ctx.eventBus.emit(createEvent("device.source-info-changed", {
|
|
17263
|
+
type: "device",
|
|
17264
|
+
id: this.stableId
|
|
17265
|
+
}, {
|
|
17266
|
+
deviceId: this.id,
|
|
17267
|
+
sourceInfo: next
|
|
17268
|
+
}));
|
|
17269
|
+
}
|
|
17270
|
+
/**
|
|
17271
|
+
* Re-publish the device's current `features` array to the persisted
|
|
17272
|
+
* meta blob. Drivers call this after a probe finishes when the live
|
|
17273
|
+
* `features` getter has gained new flags (e.g. `hasIntercom` flips
|
|
17274
|
+
* to true → `DeviceFeature.TwoWayAudio` joins the list).
|
|
17275
|
+
*
|
|
17276
|
+
* Without this, only the construction-time snapshot is written —
|
|
17277
|
+
* `deviceManager.registerDevice` is invoked once per boot, so probe-
|
|
17278
|
+
* driven additions don't reach the persisted index until the next
|
|
17279
|
+
* server restart, and `getDevice` / `listAll` keep returning the
|
|
17280
|
+
* stale list for forked-worker devices (whose live IDevice instance
|
|
17281
|
+
* is invisible to the hub registry).
|
|
17282
|
+
*
|
|
17283
|
+
* Idempotent: re-calling with the same features just no-ops on the
|
|
17284
|
+
* persisted meta. Best-effort: lookup or write failures are logged
|
|
17285
|
+
* at debug and swallowed — the live `device.features` getter is
|
|
17286
|
+
* still authoritative within this process, so callers never block
|
|
17287
|
+
* device boot on a meta refresh.
|
|
17288
|
+
*/
|
|
17289
|
+
async refreshFeatures() {
|
|
17290
|
+
const action = this.ctx.api?.deviceManager?.registerDevice;
|
|
17291
|
+
if (!action) return;
|
|
17292
|
+
try {
|
|
17293
|
+
await action.mutate({
|
|
17294
|
+
addonId: this.ctx.deviceMeta.addonId,
|
|
17295
|
+
stableId: this.stableId,
|
|
17296
|
+
id: this.id,
|
|
17297
|
+
type: this.type,
|
|
17298
|
+
name: this.name,
|
|
17299
|
+
parentDeviceId: this.parentDeviceId,
|
|
17300
|
+
features: [...this.features],
|
|
17301
|
+
config: {}
|
|
17302
|
+
});
|
|
17303
|
+
} catch (err) {}
|
|
17304
|
+
}
|
|
17305
|
+
/**
|
|
17306
|
+
* Typed read-through to a cap-keyed runtime-state slice. Drivers
|
|
17307
|
+
* call `this.getCapSlice(batteryCapability)` and the return type
|
|
17308
|
+
* is inferred from the cap's `runtimeState` Zod schema — no string
|
|
17309
|
+
* key, no manual generic. Returns `null` when the slice hasn't
|
|
17310
|
+
* been written yet (e.g. driver hasn't seeded battery yet).
|
|
17311
|
+
*/
|
|
17312
|
+
getCapSlice(cap) {
|
|
17313
|
+
return this.runtimeState.getCapState(cap.name) ?? null;
|
|
17314
|
+
}
|
|
17315
|
+
/**
|
|
17316
|
+
* Typed writer to a cap-keyed runtime-state slice. Routes through
|
|
17317
|
+
* the runtime-state writer (validate → persist → emit cap event).
|
|
17318
|
+
* Equivalent to `this.runtimeState.setCapState(cap.name, value)`
|
|
17319
|
+
* but with the cap's `runtimeState` schema enforcing the value
|
|
17320
|
+
* shape at compile time. Mirrors the symmetry of
|
|
17321
|
+
* `getCapSlice` / `setCapSlice` for cross-cap consistency.
|
|
17322
|
+
*/
|
|
17323
|
+
setCapSlice(cap, value) {
|
|
17324
|
+
this.runtimeState.setCapState(cap.name, value);
|
|
17325
|
+
}
|
|
17326
|
+
/**
|
|
17327
|
+
* Field-level read/write proxy over a cap's runtime-state slice.
|
|
17328
|
+
* Drivers that want ergonomic per-field access declare:
|
|
17329
|
+
*
|
|
17330
|
+
* ```ts
|
|
17331
|
+
* protected battery = this.sliceProxy(batteryCapability)
|
|
17332
|
+
* // …
|
|
17333
|
+
* this.battery.sleeping = true // patches the slice
|
|
17334
|
+
* const charging = this.battery.charging // reads the slice
|
|
17335
|
+
* ```
|
|
17336
|
+
*
|
|
17337
|
+
* Reads return `undefined` when the slice hasn't been seeded yet
|
|
17338
|
+
* (cap not registered, or seeded but the field is absent). Writes
|
|
17339
|
+
* route through `runtimeState.patchCapState` so the cap's `runtimeState`
|
|
17340
|
+
* schema validates the merged result and the cap event fires.
|
|
17341
|
+
*
|
|
17342
|
+
* Pattern is generic — same shape works for `battery`, `device-status`,
|
|
17343
|
+
* `motion`, `doorbell`, anything with a `runtimeState:` schema. Drivers
|
|
17344
|
+
* declare one proxy per cap they read/write directly.
|
|
17345
|
+
*/
|
|
17346
|
+
sliceProxy(cap) {
|
|
17347
|
+
return new Proxy({}, {
|
|
17348
|
+
get: (_, key) => {
|
|
17349
|
+
return this.runtimeState.getCapState(cap.name)?.[key];
|
|
17350
|
+
},
|
|
17351
|
+
set: (_, key, value) => {
|
|
17352
|
+
this.runtimeState.patchCapState(cap.name, { [key]: value });
|
|
17353
|
+
return true;
|
|
17354
|
+
},
|
|
17355
|
+
has: (_, key) => {
|
|
17356
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17357
|
+
return slice ? key in slice : false;
|
|
17358
|
+
},
|
|
17359
|
+
ownKeys: () => {
|
|
17360
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17361
|
+
return slice ? Object.keys(slice) : [];
|
|
17362
|
+
},
|
|
17363
|
+
getOwnPropertyDescriptor: (_, key) => {
|
|
17364
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
17365
|
+
if (!slice || !(key in slice)) return void 0;
|
|
17366
|
+
return {
|
|
17367
|
+
configurable: true,
|
|
17368
|
+
enumerable: true,
|
|
17369
|
+
value: slice[key]
|
|
17370
|
+
};
|
|
17371
|
+
}
|
|
17372
|
+
});
|
|
17373
|
+
}
|
|
17374
|
+
/**
|
|
17375
|
+
* Default empty settings UI. Drivers override this to expose an
|
|
17376
|
+
* editable form in the device-details page. Returning an empty sections
|
|
17377
|
+
* array signals "nothing to contribute" — the aggregator drops the
|
|
17378
|
+
* contribution entirely rather than rendering a blank panel.
|
|
17379
|
+
*/
|
|
17380
|
+
getSettingsUISchema() {
|
|
17381
|
+
return { sections: [] };
|
|
17382
|
+
}
|
|
17383
|
+
/**
|
|
17384
|
+
* Default write path: forward the flat patch directly to storage.
|
|
17385
|
+
* Drivers that project a UI shape different from storage (e.g. `RtspCamera`
|
|
17386
|
+
* exposing `mainStreamUrl`/`subStreamUrl` over `streams[]`) override this
|
|
17387
|
+
* to reshape before `config.setAll`.
|
|
17388
|
+
*/
|
|
17389
|
+
async applySettingsPatch(patch) {
|
|
17390
|
+
await this.config.setAll(patch);
|
|
17391
|
+
}
|
|
17392
|
+
/**
|
|
17393
|
+
* Phase 3 — populate device-scoped state needed by downstream phases
|
|
17394
|
+
* (accessory reconciliation, public `features` array, optional cap
|
|
17395
|
+
* registration). Called ONCE per construction, after register but
|
|
17396
|
+
* before `getAccessoryChildren()`.
|
|
17397
|
+
*
|
|
17398
|
+
* Drivers write the `feature-probe` runtime-state slice via
|
|
17399
|
+
* `this.runtimeState.setCapState('feature-probe', {...})` — flag bag
|
|
17400
|
+
* is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
|
|
17401
|
+
* `hasSupplementalLight/hasAlarmIo`, etc).
|
|
17402
|
+
*
|
|
17403
|
+
* Default: nothing to probe → mark the device PROBED (set `lastProbedAt`) so
|
|
17404
|
+
* the kernel treats it as ready immediately. A device that derives its shape
|
|
17405
|
+
* from a spec (a container, or an accessory sensor) rather than from a
|
|
17406
|
+
* hardware probe has no probe to "complete"; without stamping `lastProbedAt`
|
|
17407
|
+
* it would look perpetually un-probed — logging "Initial probe did not
|
|
17408
|
+
* complete" on every boot and spinning a pointless retry chain. Drivers that
|
|
17409
|
+
* DO probe override this and write their own `feature-probe` slice (including
|
|
17410
|
+
* `lastProbedAt`) once their probe actually succeeds.
|
|
17411
|
+
*/
|
|
17412
|
+
async onProbe() {
|
|
17413
|
+
const base = this.runtimeState.getCapState("feature-probe") ?? {
|
|
17414
|
+
flags: {},
|
|
17415
|
+
deviceType: null,
|
|
17416
|
+
model: null,
|
|
17417
|
+
channelCount: null,
|
|
17418
|
+
lastProbedAt: 0,
|
|
17419
|
+
lastFetchedAt: 0
|
|
17420
|
+
};
|
|
17421
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
17422
|
+
...base,
|
|
17423
|
+
lastProbedAt: Date.now()
|
|
17424
|
+
});
|
|
17425
|
+
}
|
|
17426
|
+
/**
|
|
17427
|
+
* Phase 5 — fired after the device + its accessories are registered.
|
|
17428
|
+
* Drivers publish streams to the broker, kick off background tasks,
|
|
17429
|
+
* or subscribe to lib events that need a fully-registered device id.
|
|
17430
|
+
*
|
|
17431
|
+
* Default: no-op.
|
|
17432
|
+
*
|
|
17433
|
+
* RENAMED FROM `onCreated` (which still exists for back-compat in this
|
|
17434
|
+
* pass). The new name reflects the post-probe, post-accessory contract.
|
|
17435
|
+
*/
|
|
17436
|
+
async onActivate() {}
|
|
17437
|
+
/**
|
|
17438
|
+
* Re-run the probe + reconcile accessories + refresh features meta.
|
|
17439
|
+
* Drivers call this when device-side state changes (battery cam wakes,
|
|
17440
|
+
* firmware update, manual operator trigger).
|
|
17441
|
+
*
|
|
17442
|
+
* The kernel injects `_kernelReprobe` on registration so this method
|
|
17443
|
+
* delegates to the same orchestrator that runs the boot-time phase
|
|
17444
|
+
* 3 + 4 sequence. Drivers should NOT override this — they override
|
|
17445
|
+
* `onProbe()` instead.
|
|
17446
|
+
*/
|
|
17447
|
+
async reprobe() {
|
|
17448
|
+
if (this._kernelReprobe) await this._kernelReprobe();
|
|
17449
|
+
else await this.onProbe();
|
|
17450
|
+
}
|
|
17451
|
+
/**
|
|
17452
|
+
* Kernel-injected callback that runs the full post-probe orchestration
|
|
17453
|
+
* (onProbe → registerDevice meta refresh → accessory reconciliation).
|
|
17454
|
+
* Set by `device-cap-proxy.register()`. Drivers should not touch this
|
|
17455
|
+
* directly — call `reprobe()` instead.
|
|
17456
|
+
*/
|
|
17457
|
+
_kernelReprobe;
|
|
17458
|
+
/**
|
|
17459
|
+
* Declare accessory child devices the kernel should auto-spawn
|
|
17460
|
+
* after `onProbe()` resolves. Each spec fully describes one child
|
|
17461
|
+
* — stableId suffix (deterministic per kind for restore-safety),
|
|
17462
|
+
* meta (type / name / location), config (initial blob the child
|
|
17463
|
+
* self-hydrates), and a factory that constructs the concrete
|
|
17464
|
+
* class with whatever closure-captured refs it needs (typically
|
|
17465
|
+
* `this` for the parent reference).
|
|
17466
|
+
*
|
|
17467
|
+
* The kernel handles the rest: allocateDeviceId, persistInitialConfig
|
|
17468
|
+
* (skipped on restore when the row already exists),
|
|
17469
|
+
* persistInitialMeta, createContext, factory invocation, register,
|
|
17470
|
+
* and recursive lifecycle (probe + accessories + activate).
|
|
17471
|
+
*
|
|
17472
|
+
* Implementations should derive children from
|
|
17473
|
+
* `this.runtimeState.getCapState('feature-probe')` (post-probe truth).
|
|
17474
|
+
* Drivers can use the `getProbeFlags()` helper to read the flag bag
|
|
17475
|
+
* with a typed cast.
|
|
17476
|
+
*
|
|
17477
|
+
* Default: no children.
|
|
17478
|
+
*/
|
|
17479
|
+
getAccessoryChildren() {
|
|
17480
|
+
return [];
|
|
17481
|
+
}
|
|
17482
|
+
/**
|
|
17483
|
+
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
17484
|
+
* for `getAccessoryChildren()` and `features` getters that derive
|
|
17485
|
+
* outputs from the probe results.
|
|
17486
|
+
*/
|
|
17487
|
+
getProbeFlags() {
|
|
17488
|
+
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
17489
|
+
}
|
|
17490
|
+
/**
|
|
17491
|
+
* Returns true once `onProbe` has completed at least once
|
|
17492
|
+
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
17493
|
+
* to avoid spawning stale accessories on a fresh device whose probe
|
|
17494
|
+
* hasn't landed yet.
|
|
17495
|
+
*/
|
|
17496
|
+
hasProbed() {
|
|
17497
|
+
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
17498
|
+
}
|
|
17499
|
+
};
|
|
14702
17500
|
var ProviderStatusSchema = object({
|
|
14703
17501
|
connected: boolean(),
|
|
14704
17502
|
deviceCount: number(),
|
|
@@ -15782,6 +18580,10 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
|
|
|
15782
18580
|
auth: "admin"
|
|
15783
18581
|
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
15784
18582
|
DeviceType.Camera;
|
|
18583
|
+
/** kebab-case cap name → camelCase router-map key. */
|
|
18584
|
+
function kebabToCamel(s) {
|
|
18585
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
18586
|
+
}
|
|
15785
18587
|
/**
|
|
15786
18588
|
* Identity — preserves literal types for downstream inference.
|
|
15787
18589
|
*
|
|
@@ -27841,6 +30643,12 @@ Object.defineProperty(exports, "BaseAddon", {
|
|
|
27841
30643
|
return BaseAddon;
|
|
27842
30644
|
}
|
|
27843
30645
|
});
|
|
30646
|
+
Object.defineProperty(exports, "BaseDevice", {
|
|
30647
|
+
enumerable: true,
|
|
30648
|
+
get: function() {
|
|
30649
|
+
return BaseDevice;
|
|
30650
|
+
}
|
|
30651
|
+
});
|
|
27844
30652
|
Object.defineProperty(exports, "DEFAULT_EVENT_COLOR", {
|
|
27845
30653
|
enumerable: true,
|
|
27846
30654
|
get: function() {
|
|
@@ -27973,6 +30781,12 @@ Object.defineProperty(exports, "addonWidgetsSourceCapability", {
|
|
|
27973
30781
|
return addonWidgetsSourceCapability;
|
|
27974
30782
|
}
|
|
27975
30783
|
});
|
|
30784
|
+
Object.defineProperty(exports, "alarmPanelCapability", {
|
|
30785
|
+
enumerable: true,
|
|
30786
|
+
get: function() {
|
|
30787
|
+
return alarmPanelCapability;
|
|
30788
|
+
}
|
|
30789
|
+
});
|
|
27976
30790
|
Object.defineProperty(exports, "array", {
|
|
27977
30791
|
enumerable: true,
|
|
27978
30792
|
get: function() {
|
|
@@ -28051,6 +30865,18 @@ Object.defineProperty(exports, "hydrateSchema", {
|
|
|
28051
30865
|
return hydrateSchema;
|
|
28052
30866
|
}
|
|
28053
30867
|
});
|
|
30868
|
+
Object.defineProperty(exports, "isDeviceScopedCap", {
|
|
30869
|
+
enumerable: true,
|
|
30870
|
+
get: function() {
|
|
30871
|
+
return isDeviceScopedCap;
|
|
30872
|
+
}
|
|
30873
|
+
});
|
|
30874
|
+
Object.defineProperty(exports, "kebabToCamel", {
|
|
30875
|
+
enumerable: true,
|
|
30876
|
+
get: function() {
|
|
30877
|
+
return kebabToCamel;
|
|
30878
|
+
}
|
|
30879
|
+
});
|
|
28054
30880
|
Object.defineProperty(exports, "literal", {
|
|
28055
30881
|
enumerable: true,
|
|
28056
30882
|
get: function() {
|
|
@@ -28099,6 +30925,12 @@ Object.defineProperty(exports, "readDeviceStateFrom", {
|
|
|
28099
30925
|
return readDeviceStateFrom;
|
|
28100
30926
|
}
|
|
28101
30927
|
});
|
|
30928
|
+
Object.defineProperty(exports, "record", {
|
|
30929
|
+
enumerable: true,
|
|
30930
|
+
get: function() {
|
|
30931
|
+
return record;
|
|
30932
|
+
}
|
|
30933
|
+
});
|
|
28102
30934
|
Object.defineProperty(exports, "string", {
|
|
28103
30935
|
enumerable: true,
|
|
28104
30936
|
get: function() {
|
|
@@ -28111,6 +30943,12 @@ Object.defineProperty(exports, "subKindsOf", {
|
|
|
28111
30943
|
return subKindsOf;
|
|
28112
30944
|
}
|
|
28113
30945
|
});
|
|
30946
|
+
Object.defineProperty(exports, "unknown", {
|
|
30947
|
+
enumerable: true,
|
|
30948
|
+
get: function() {
|
|
30949
|
+
return unknown;
|
|
30950
|
+
}
|
|
30951
|
+
});
|
|
28114
30952
|
Object.defineProperty(exports, "videoclipsCapability", {
|
|
28115
30953
|
enumerable: true,
|
|
28116
30954
|
get: function() {
|