@camstack/addon-post-analysis 0.1.19 → 0.1.20
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/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +1 -1
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{index-BFbwYH1P.js → index-B0RhVv1c.js} +3514 -750
- package/dist/index-B0RhVv1c.js.map +1 -0
- package/dist/{index-BrTlzsrE.mjs → index-ot5PeFg_.mjs} +3517 -753
- package/dist/index-ot5PeFg_.mjs.map +1 -0
- package/dist/pipeline-analytics/@mf-types.zip +0 -0
- package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-lantnv8e.mjs} +1 -1
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-BD3oMNGB.mjs +29 -0
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BgOHCakr.mjs +18 -0
- package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-BZTB2scQ.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-D1qPKjvR.mjs} +2 -1
- package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CJO5YKGV.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-B5X50Xa4.mjs} +1 -1
- package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-B0h0AGOH.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-B10b5k5J.mjs} +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -3
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-kZBmgzMg.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DWB3apaJ.mjs} +6 -6
- package/dist/pipeline-analytics/{client-BlxIUpgf.mjs → client-C6xdgLZU.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-qBB1Thhi.mjs → hostInit-3cyL9eyG.mjs} +12 -12
- package/dist/pipeline-analytics/{index-Dw6Q30NI.mjs → index-BCTHeI2m.mjs} +253 -267
- package/dist/pipeline-analytics/{index-DlhiA9R0.mjs → index-BuWLz0GG.mjs} +1 -1
- package/dist/pipeline-analytics/{index-DtdgkNgf.mjs → index-CIwq-tQL.mjs} +1 -1
- package/dist/pipeline-analytics/{index-BoL0rgZt.mjs → index-CWBMDbou.mjs} +1 -1
- package/dist/pipeline-analytics/index-CZhagnlH.mjs +67784 -0
- package/dist/pipeline-analytics/{index-CR1aiZDH.mjs → index-D883Q5B8.mjs} +1 -1
- package/dist/pipeline-analytics/{index-Dy2V7VOm.mjs → index-DtOI1aTU.mjs} +10112 -5987
- package/dist/pipeline-analytics/index.js +605 -42
- package/dist/pipeline-analytics/index.js.map +1 -1
- package/dist/pipeline-analytics/index.mjs +604 -42
- package/dist/pipeline-analytics/index.mjs.map +1 -1
- package/dist/pipeline-analytics/{jsx-runtime-Dlbl3gpr.mjs → jsx-runtime-DdLhuHmJ.mjs} +1 -1
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/pipeline-analytics/{schemas-ClCuS4qa.mjs → schemas-B7L0qZtq.mjs} +411 -406
- package/package.json +12 -27
- package/dist/ffmpeg-config-DRONlBsj.mjs +0 -56
- package/dist/ffmpeg-config-DRONlBsj.mjs.map +0 -1
- package/dist/ffmpeg-config-uANz3sV5.js +0 -73
- package/dist/ffmpeg-config-uANz3sV5.js.map +0 -1
- package/dist/index-BFbwYH1P.js.map +0 -1
- package/dist/index-BrTlzsrE.mjs.map +0 -1
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-NjF4kxzW.mjs +0 -19
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-7HAAnpQu.mjs +0 -18
- package/dist/pipeline-analytics/index-i47purqY.mjs +0 -37880
- package/dist/playlist-generator-EhPaB7Hn.js +0 -48
- package/dist/playlist-generator-EhPaB7Hn.js.map +0 -1
- package/dist/playlist-generator-VTkgn53O.mjs +0 -48
- package/dist/playlist-generator-VTkgn53O.mjs.map +0 -1
- package/dist/recording/index.js +0 -257
- package/dist/recording/index.js.map +0 -1
- package/dist/recording/index.mjs +0 -235
- package/dist/recording/index.mjs.map +0 -1
- package/dist/recording-coordinator-BoGr5moz.js +0 -1052
- package/dist/recording-coordinator-BoGr5moz.js.map +0 -1
- package/dist/recording-coordinator-CsYH9LqF.mjs +0 -1012
- package/dist/recording-coordinator-CsYH9LqF.mjs.map +0 -1
- package/dist/recording-db-gOgaoQh0.js +0 -348
- package/dist/recording-db-gOgaoQh0.js.map +0 -1
- package/dist/recording-db-lIkSMTLq.mjs +0 -348
- package/dist/recording-db-lIkSMTLq.mjs.map +0 -1
- package/dist/recording-service-facade-B9lG6OFn.mjs +0 -123
- package/dist/recording-service-facade-B9lG6OFn.mjs.map +0 -1
- package/dist/recording-service-facade-Do1PKlAL.js +0 -123
- package/dist/recording-service-facade-Do1PKlAL.js.map +0 -1
- package/dist/storage-estimator-CRpoQc9j.js +0 -72
- package/dist/storage-estimator-CRpoQc9j.js.map +0 -1
- package/dist/storage-estimator-DzD8gWJH.mjs +0 -72
- package/dist/storage-estimator-DzD8gWJH.mjs.map +0 -1
|
@@ -1273,10 +1273,10 @@ const $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
1273
1273
|
return;
|
|
1274
1274
|
}
|
|
1275
1275
|
}
|
|
1276
|
-
const
|
|
1276
|
+
const url2 = new URL(trimmed);
|
|
1277
1277
|
if (def.hostname) {
|
|
1278
1278
|
def.hostname.lastIndex = 0;
|
|
1279
|
-
if (!def.hostname.test(
|
|
1279
|
+
if (!def.hostname.test(url2.hostname)) {
|
|
1280
1280
|
payload.issues.push({
|
|
1281
1281
|
code: "invalid_format",
|
|
1282
1282
|
format: "url",
|
|
@@ -1290,7 +1290,7 @@ const $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
1290
1290
|
}
|
|
1291
1291
|
if (def.protocol) {
|
|
1292
1292
|
def.protocol.lastIndex = 0;
|
|
1293
|
-
if (!def.protocol.test(
|
|
1293
|
+
if (!def.protocol.test(url2.protocol.endsWith(":") ? url2.protocol.slice(0, -1) : url2.protocol)) {
|
|
1294
1294
|
payload.issues.push({
|
|
1295
1295
|
code: "invalid_format",
|
|
1296
1296
|
format: "url",
|
|
@@ -1303,7 +1303,7 @@ const $ZodURL = /* @__PURE__ */ $constructor("$ZodURL", (inst, def) => {
|
|
|
1303
1303
|
}
|
|
1304
1304
|
}
|
|
1305
1305
|
if (def.normalize) {
|
|
1306
|
-
payload.value =
|
|
1306
|
+
payload.value = url2.href;
|
|
1307
1307
|
} else {
|
|
1308
1308
|
payload.value = trimmed;
|
|
1309
1309
|
}
|
|
@@ -4427,6 +4427,9 @@ const ZodURL = /* @__PURE__ */ $constructor("ZodURL", (inst, def) => {
|
|
|
4427
4427
|
$ZodURL.init(inst, def);
|
|
4428
4428
|
ZodStringFormat.init(inst, def);
|
|
4429
4429
|
});
|
|
4430
|
+
function url(params) {
|
|
4431
|
+
return /* @__PURE__ */ _url(ZodURL, params);
|
|
4432
|
+
}
|
|
4430
4433
|
const ZodEmoji = /* @__PURE__ */ $constructor("ZodEmoji", (inst, def) => {
|
|
4431
4434
|
$ZodEmoji.init(inst, def);
|
|
4432
4435
|
ZodStringFormat.init(inst, def);
|
|
@@ -5110,6 +5113,7 @@ const WELL_KNOWN_TABS = [
|
|
|
5110
5113
|
{ id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
|
|
5111
5114
|
{ id: "streaming", label: "Streaming", icon: "video", order: 35 },
|
|
5112
5115
|
{ id: "ptz", label: "PTZ", icon: "move", order: 40 },
|
|
5116
|
+
{ id: "consumables", label: "Consumables", icon: "recycle", order: 44 },
|
|
5113
5117
|
{ id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
|
|
5114
5118
|
{ id: "zones", label: "Detection", icon: "shapes", order: 38 },
|
|
5115
5119
|
{ id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
|
|
@@ -5129,7 +5133,7 @@ const WELL_KNOWN_TABS = [
|
|
|
5129
5133
|
];
|
|
5130
5134
|
Object.fromEntries(WELL_KNOWN_TABS.map((t) => [t.id, t]));
|
|
5131
5135
|
function isValuelessField(field) {
|
|
5132
|
-
return field.type === "separator" || field.type === "info" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button";
|
|
5136
|
+
return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button";
|
|
5133
5137
|
}
|
|
5134
5138
|
function hydrateSchema(schema, values) {
|
|
5135
5139
|
return {
|
|
@@ -5191,6 +5195,8 @@ function typeOf(field) {
|
|
|
5191
5195
|
case "password":
|
|
5192
5196
|
case "color":
|
|
5193
5197
|
case "probe":
|
|
5198
|
+
case "timezone":
|
|
5199
|
+
case "datetime":
|
|
5194
5200
|
return "string";
|
|
5195
5201
|
case "number":
|
|
5196
5202
|
case "slider":
|
|
@@ -5201,32 +5207,262 @@ function typeOf(field) {
|
|
|
5201
5207
|
return "other";
|
|
5202
5208
|
}
|
|
5203
5209
|
}
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
"
|
|
5207
|
-
"
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5210
|
+
const CamProfileSchema = _enum(["high", "mid", "low"]);
|
|
5211
|
+
const CamStreamKindSchema = _enum([
|
|
5212
|
+
"pull-rtsp",
|
|
5213
|
+
"pull-rtmp",
|
|
5214
|
+
"pull-http",
|
|
5215
|
+
"pull-rfc4571",
|
|
5216
|
+
"push-annexb",
|
|
5217
|
+
/** Broker-spawned ffmpeg transcode plane — reads from a source cam-stream
|
|
5218
|
+
* and writes a re-encoded RTP plane. The broker routes demand signals to
|
|
5219
|
+
* the source broker; the transcode process is single-flighted per
|
|
5220
|
+
* identical EncodeProfile so multiple derived subscribers share one ffmpeg
|
|
5221
|
+
* process. */
|
|
5222
|
+
"derived"
|
|
5223
|
+
]);
|
|
5224
|
+
const CamStreamResolutionSchema = object({
|
|
5225
|
+
width: number().int().positive(),
|
|
5226
|
+
height: number().int().positive()
|
|
5227
|
+
});
|
|
5228
|
+
const CameraStreamSchema = object({
|
|
5229
|
+
/** Stable, provider-assigned id unique within the (deviceId) scope. */
|
|
5230
|
+
camStreamId: string().min(1),
|
|
5231
|
+
deviceId: number().int().nonnegative(),
|
|
5232
|
+
kind: CamStreamKindSchema,
|
|
5233
|
+
/** Required for pull-* kinds. Ignored for push-annexb. */
|
|
5234
|
+
url: string().optional(),
|
|
5235
|
+
codec: string().optional(),
|
|
5236
|
+
resolution: CamStreamResolutionSchema.optional(),
|
|
5237
|
+
fps: number().positive().optional(),
|
|
5238
|
+
/** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
|
|
5239
|
+
label: string().optional(),
|
|
5240
|
+
/**
|
|
5241
|
+
* Device-level features the publisher advertised (e.g. `battery-operated`).
|
|
5242
|
+
* The broker, snapshot orchestrator, and prebuffer manager all consult
|
|
5243
|
+
* this list to derive policy — relaxed stall watchdog for battery
|
|
5244
|
+
* cams, prebuffer off by default, longer snapshot rate-limit, etc.
|
|
5245
|
+
*
|
|
5246
|
+
* Single source of truth replacing per-stream flags like the
|
|
5247
|
+
* historical `allowStall`: if the publisher knows the camera is
|
|
5248
|
+
* battery-powered, every downstream service derives the right policy
|
|
5249
|
+
* from this list.
|
|
5250
|
+
*/
|
|
5251
|
+
deviceFeatures: array(string()).optional(),
|
|
5252
|
+
/**
|
|
5253
|
+
* Whether this stream participates in the broker's automatic profile
|
|
5254
|
+
* assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
|
|
5255
|
+
* use `false` when they want a stream to be SELECTABLE in the UI but not
|
|
5256
|
+
* picked by default — e.g. Reolink publishes its native Baichuan streams
|
|
5257
|
+
* as `autoEligible: true` (the recommended path) and its RTSP / RTMP
|
|
5258
|
+
* mirrors as `autoEligible: false` (still pickable per slot, just not
|
|
5259
|
+
* the auto choice). Manual `assignProfile` calls remain valid for
|
|
5260
|
+
* non-eligible streams.
|
|
5261
|
+
*/
|
|
5262
|
+
autoEligible: boolean().optional(),
|
|
5263
|
+
/**
|
|
5264
|
+
* Transport-specific opaque metadata. The broker passes it through to
|
|
5265
|
+
* the source reader without inspecting it. Currently used by
|
|
5266
|
+
* `pull-rfc4571` streams to carry the upstream SDP (so the reader can
|
|
5267
|
+
* route RTP packets to the right depacketizer without an in-band
|
|
5268
|
+
* DESCRIBE phase). Other kinds typically leave it undefined.
|
|
5269
|
+
*/
|
|
5270
|
+
metadata: record(string(), unknown()).optional()
|
|
5271
|
+
});
|
|
5272
|
+
const ProfileSlotStatusSchema = _enum([
|
|
5273
|
+
"unassigned",
|
|
5274
|
+
"idle",
|
|
5275
|
+
"connecting",
|
|
5276
|
+
"streaming",
|
|
5277
|
+
"error"
|
|
5278
|
+
]);
|
|
5279
|
+
const ProfileSlotSchema = object({
|
|
5280
|
+
deviceId: number().int().nonnegative(),
|
|
5281
|
+
profile: CamProfileSchema,
|
|
5282
|
+
/** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
|
|
5283
|
+
brokerId: string(),
|
|
5284
|
+
/** `null` when the profile is unassigned. */
|
|
5285
|
+
sourceCamStreamId: string().nullable(),
|
|
5286
|
+
status: ProfileSlotStatusSchema,
|
|
5287
|
+
resolution: CamStreamResolutionSchema.optional(),
|
|
5288
|
+
codec: string().optional(),
|
|
5289
|
+
preBufferSec: number().nonnegative().optional(),
|
|
5290
|
+
errorMessage: string().optional()
|
|
5291
|
+
});
|
|
5292
|
+
const StreamSourceEntrySchema$1 = object({
|
|
5293
|
+
id: string(),
|
|
5294
|
+
label: string(),
|
|
5295
|
+
protocol: _enum(["rtsp", "rtmp", "annexb", "http-mjpeg", "webrtc", "custom"]),
|
|
5296
|
+
url: string().optional(),
|
|
5297
|
+
resolution: object({ width: number(), height: number() }).readonly().optional(),
|
|
5298
|
+
fps: number().optional(),
|
|
5299
|
+
bitrate: number().optional(),
|
|
5300
|
+
codec: string().optional(),
|
|
5301
|
+
profileHint: CamProfileSchema.optional()
|
|
5302
|
+
});
|
|
5303
|
+
object({
|
|
5304
|
+
type: string(),
|
|
5305
|
+
url: string(),
|
|
5306
|
+
videoCodec: string().optional(),
|
|
5307
|
+
audioCodec: string().optional(),
|
|
5308
|
+
metadata: record(string(), unknown()).readonly().optional()
|
|
5309
|
+
});
|
|
5310
|
+
const EncodedPacketSchema = object({
|
|
5311
|
+
type: _enum(["video", "audio"]),
|
|
5312
|
+
data: _instanceof(Uint8Array),
|
|
5313
|
+
pts: number(),
|
|
5314
|
+
dts: number(),
|
|
5315
|
+
keyframe: boolean(),
|
|
5316
|
+
codec: string()
|
|
5317
|
+
});
|
|
5318
|
+
const DecodedFrameSchema = object({
|
|
5319
|
+
data: _instanceof(Uint8Array),
|
|
5320
|
+
width: number(),
|
|
5321
|
+
height: number(),
|
|
5322
|
+
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
5323
|
+
timestamp: number()
|
|
5324
|
+
});
|
|
5325
|
+
const FrameHandleSchema = object({
|
|
5326
|
+
shmId: string(),
|
|
5327
|
+
slot: number().int().nonnegative(),
|
|
5328
|
+
seq: number().int().nonnegative(),
|
|
5329
|
+
width: number().int().positive(),
|
|
5330
|
+
height: number().int().positive(),
|
|
5331
|
+
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
5332
|
+
pts: number(),
|
|
5333
|
+
byteLength: number().int().nonnegative(),
|
|
5334
|
+
nodeId: string(),
|
|
5335
|
+
slotCount: number().int().positive()
|
|
5336
|
+
});
|
|
5337
|
+
const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
|
|
5338
|
+
const SubscribeFramesInputSchema = object({
|
|
5339
|
+
brokerId: string(),
|
|
5340
|
+
format: FrameHandleFormatSchema,
|
|
5341
|
+
/**
|
|
5342
|
+
* Optional reader-side cadence hint in frames per second. The broker does
|
|
5343
|
+
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
5344
|
+
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
5345
|
+
* the consumer can pace its own `pullFrameHandles` polling.
|
|
5346
|
+
*/
|
|
5347
|
+
maxFps: number().positive().optional(),
|
|
5348
|
+
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
5349
|
+
tag: string().optional()
|
|
5350
|
+
});
|
|
5351
|
+
const SubscribeFramesResultSchema = object({
|
|
5352
|
+
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
5353
|
+
subscriptionId: string(),
|
|
5354
|
+
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
5355
|
+
maxFps: number().nonnegative()
|
|
5356
|
+
});
|
|
5357
|
+
const DecodedAudioChunkSchema = object({
|
|
5358
|
+
data: _instanceof(Uint8Array),
|
|
5359
|
+
sampleRate: number().int().positive(),
|
|
5360
|
+
channels: number().int().positive(),
|
|
5361
|
+
timestamp: number()
|
|
5362
|
+
});
|
|
5363
|
+
const SubscribeAudioChunksInputSchema = object({
|
|
5364
|
+
brokerId: string(),
|
|
5365
|
+
/** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
|
|
5366
|
+
tag: string().optional()
|
|
5367
|
+
});
|
|
5368
|
+
const SubscribeAudioChunksResultSchema = object({
|
|
5369
|
+
/** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
|
|
5370
|
+
subscriptionId: string()
|
|
5371
|
+
});
|
|
5372
|
+
const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
|
|
5373
|
+
const BrokerStatsSchema = object({
|
|
5374
|
+
status: BrokerStatusSchema$1,
|
|
5375
|
+
inputFps: number(),
|
|
5376
|
+
decodeFps: number(),
|
|
5377
|
+
encodedSubscribers: number(),
|
|
5378
|
+
decodedSubscribers: number(),
|
|
5379
|
+
uptimeMs: number(),
|
|
5380
|
+
bitrateKbps: number(),
|
|
5381
|
+
idrIntervalMs: number(),
|
|
5382
|
+
codec: string().optional(),
|
|
5383
|
+
totalBytes: number(),
|
|
5384
|
+
packetCount: number(),
|
|
5385
|
+
rtspClients: number(),
|
|
5386
|
+
pipeClients: number(),
|
|
5387
|
+
preBufferSec: number(),
|
|
5388
|
+
preBufferMs: number(),
|
|
5389
|
+
preBufferPackets: number(),
|
|
5390
|
+
/**
|
|
5391
|
+
* Moleculer node id of the decoder provider currently servicing this
|
|
5392
|
+
* stream's decoded subscribers. `null` until the deferred decoder is
|
|
5393
|
+
* created (no decoded clients yet, or codec not detected). Surfaces the
|
|
5394
|
+
* runtime decoder placement so the UI can show "Decoder: <agent>" without
|
|
5395
|
+
* a separate cap call per broker.
|
|
5396
|
+
*/
|
|
5397
|
+
decoderNodeId: string().nullable(),
|
|
5398
|
+
/**
|
|
5399
|
+
* Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
|
|
5400
|
+
* when the stream has no audio track or the broker is in cold start.
|
|
5401
|
+
* `supported = false` means the codec was detected but the local
|
|
5402
|
+
* decoder pipeline cannot produce PCM chunks (e.g. AAC without the
|
|
5403
|
+
* AAC pipeline wired). Surfaced in the UI device-overview so operators
|
|
5404
|
+
* can pre-pick the audio-analysis model that matches the codec.
|
|
5405
|
+
*/
|
|
5406
|
+
audio: object({
|
|
5407
|
+
codec: string(),
|
|
5408
|
+
sampleRate: number(),
|
|
5409
|
+
channels: number(),
|
|
5410
|
+
supported: boolean()
|
|
5411
|
+
}).nullable().optional()
|
|
5412
|
+
});
|
|
5413
|
+
const ProfileRtspEntrySchema = object({
|
|
5414
|
+
profile: CamProfileSchema,
|
|
5415
|
+
/** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
|
|
5416
|
+
brokerId: string(),
|
|
5417
|
+
url: string(),
|
|
5418
|
+
mutedUrl: string(),
|
|
5419
|
+
enabled: boolean(),
|
|
5420
|
+
codec: string().optional(),
|
|
5421
|
+
resolution: CamStreamResolutionSchema.optional()
|
|
5422
|
+
});
|
|
5423
|
+
const RecordingWeekdaySchema = number().int().min(0).max(6);
|
|
5424
|
+
const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
5425
|
+
const RecordingScheduleSchema = discriminatedUnion("kind", [
|
|
5426
|
+
object({ kind: literal("always") }),
|
|
5427
|
+
object({
|
|
5428
|
+
kind: literal("timeOfDay"),
|
|
5429
|
+
start: string().regex(HHMM),
|
|
5430
|
+
end: string().regex(HHMM),
|
|
5431
|
+
/** Restrict to these weekdays; omit = every day. */
|
|
5432
|
+
days: array(RecordingWeekdaySchema).optional()
|
|
5433
|
+
})
|
|
5226
5434
|
]);
|
|
5435
|
+
const RecordingModeSchema = _enum(["continuous", "onMotion", "onAudioThreshold"]);
|
|
5436
|
+
const RecordingRuleSchema = object({
|
|
5437
|
+
schedule: RecordingScheduleSchema,
|
|
5438
|
+
mode: RecordingModeSchema,
|
|
5439
|
+
/** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
|
|
5440
|
+
preBufferSec: number().min(0).default(0),
|
|
5441
|
+
/** Keep recording until this many seconds after the last trigger. */
|
|
5442
|
+
postBufferSec: number().min(0).default(0),
|
|
5443
|
+
/** Each new trigger restarts the post-buffer window. */
|
|
5444
|
+
resetTimeoutOnNewEvent: boolean().default(true),
|
|
5445
|
+
/** onAudioThreshold only — dBFS level that counts as a trigger. */
|
|
5446
|
+
thresholdDbfs: number().optional()
|
|
5447
|
+
});
|
|
5448
|
+
const RecordingRetentionSchema = object({
|
|
5449
|
+
maxAgeDays: number().min(0).optional(),
|
|
5450
|
+
maxSizeGb: number().min(0).optional()
|
|
5451
|
+
});
|
|
5452
|
+
const RecordingConfigSchema = object({
|
|
5453
|
+
enabled: boolean(),
|
|
5454
|
+
profiles: array(CamProfileSchema).optional(),
|
|
5455
|
+
segmentSeconds: number().int().positive().optional(),
|
|
5456
|
+
rules: array(RecordingRuleSchema).optional(),
|
|
5457
|
+
retention: RecordingRetentionSchema.optional()
|
|
5458
|
+
});
|
|
5459
|
+
const StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
|
|
5227
5460
|
const StorageLocationSchema = object({
|
|
5228
|
-
id: string().regex(/^[a-z][a-
|
|
5229
|
-
|
|
5461
|
+
id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
|
|
5462
|
+
// Open string — the location's *kind* is an addon-declared id matching
|
|
5463
|
+
// `StorageLocationDeclaration.id`. All built-in ids are valid plain
|
|
5464
|
+
// strings, and addon-declared ids are admitted without schema changes.
|
|
5465
|
+
type: string(),
|
|
5230
5466
|
displayName: string().min(1),
|
|
5231
5467
|
providerId: string().min(1),
|
|
5232
5468
|
config: record(string(), unknown()),
|
|
@@ -5237,8 +5473,36 @@ const StorageLocationSchema = object({
|
|
|
5237
5473
|
});
|
|
5238
5474
|
const StorageLocationRefSchema = union([
|
|
5239
5475
|
StorageLocationTypeSchema,
|
|
5240
|
-
string().regex(/^[a-z][a-
|
|
5476
|
+
string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)
|
|
5241
5477
|
]);
|
|
5478
|
+
const StorageLocationDeclarationSchema = object({
|
|
5479
|
+
/**
|
|
5480
|
+
* Global location identifier, e.g. `recordings` or `recordingsLow`.
|
|
5481
|
+
* Must start with a lowercase letter and may contain letters, digits, and
|
|
5482
|
+
* hyphens.
|
|
5483
|
+
*/
|
|
5484
|
+
id: string().regex(/^[a-z][a-zA-Z0-9-]*$/, {
|
|
5485
|
+
message: "id must start with a lowercase letter and contain only letters, digits, or hyphens"
|
|
5486
|
+
}),
|
|
5487
|
+
/** Human-readable name shown in the admin UI. */
|
|
5488
|
+
displayName: string().min(1, { message: "displayName must not be empty" }),
|
|
5489
|
+
/** Optional longer explanation of what data this location stores. */
|
|
5490
|
+
description: string().optional(),
|
|
5491
|
+
/**
|
|
5492
|
+
* `single` — exactly one instance of this location is allowed system-wide
|
|
5493
|
+
* (e.g. `logs`, `models`). The operator can edit it but not add more.
|
|
5494
|
+
* `multi` — the operator may register several instances (e.g. a second
|
|
5495
|
+
* `recordings` on a NAS for disk tiering); one is the default at any time.
|
|
5496
|
+
*/
|
|
5497
|
+
cardinality: _enum(["single", "multi"]),
|
|
5498
|
+
/**
|
|
5499
|
+
* When set, the default instance for this location inherits its resolved
|
|
5500
|
+
* root from the named location's default instance. Useful for derivative
|
|
5501
|
+
* slots (e.g. `recordingsLow` → `recordings`) so operators only need to
|
|
5502
|
+
* configure the primary location.
|
|
5503
|
+
*/
|
|
5504
|
+
defaultsTo: string().optional()
|
|
5505
|
+
});
|
|
5242
5506
|
const DecoderStatsSchema = object({
|
|
5243
5507
|
inputFps: number(),
|
|
5244
5508
|
outputFps: number(),
|
|
@@ -5280,6 +5544,51 @@ const DecoderSessionConfigSchema = object({
|
|
|
5280
5544
|
*/
|
|
5281
5545
|
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
5282
5546
|
});
|
|
5547
|
+
const VideoEncodeSchema = object({
|
|
5548
|
+
codec: _enum(["h264", "h265", "copy"]),
|
|
5549
|
+
profile: _enum(["baseline", "main", "high"]).optional(),
|
|
5550
|
+
width: number().int().positive().optional(),
|
|
5551
|
+
height: number().int().positive().optional(),
|
|
5552
|
+
fps: number().positive().optional(),
|
|
5553
|
+
bitrateKbps: number().int().positive().optional(),
|
|
5554
|
+
gopFrames: number().int().positive().optional(),
|
|
5555
|
+
bf: number().int().min(0).optional(),
|
|
5556
|
+
preset: _enum([
|
|
5557
|
+
"ultrafast",
|
|
5558
|
+
"superfast",
|
|
5559
|
+
"veryfast",
|
|
5560
|
+
"faster",
|
|
5561
|
+
"fast",
|
|
5562
|
+
"medium"
|
|
5563
|
+
]).optional(),
|
|
5564
|
+
tune: _enum(["zerolatency", "film", "animation"]).optional()
|
|
5565
|
+
});
|
|
5566
|
+
const AudioEncodeSchema = union([
|
|
5567
|
+
literal("passthrough"),
|
|
5568
|
+
object({
|
|
5569
|
+
codec: _enum(["opus", "aac", "pcmu", "pcma", "copy"]),
|
|
5570
|
+
bitrateKbps: number().int().positive().optional(),
|
|
5571
|
+
sampleRateHz: number().int().positive().optional(),
|
|
5572
|
+
channels: union([literal(1), literal(2)]).optional()
|
|
5573
|
+
})
|
|
5574
|
+
]);
|
|
5575
|
+
const EncodeProfileSchema = object({
|
|
5576
|
+
video: VideoEncodeSchema,
|
|
5577
|
+
audio: AudioEncodeSchema,
|
|
5578
|
+
/**
|
|
5579
|
+
* ffmpeg input-side args, inserted between the fixed global flags
|
|
5580
|
+
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
5581
|
+
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
5582
|
+
* used demuxer/format options.
|
|
5583
|
+
*/
|
|
5584
|
+
inputArgs: array(string()).optional(),
|
|
5585
|
+
/**
|
|
5586
|
+
* ffmpeg output-side args, inserted between the encode block and
|
|
5587
|
+
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
5588
|
+
* filters, codec-specific overrides. Free-text array.
|
|
5589
|
+
*/
|
|
5590
|
+
outputArgs: array(string()).optional()
|
|
5591
|
+
});
|
|
5283
5592
|
function errMsg(err) {
|
|
5284
5593
|
if (err instanceof Error) return err.message;
|
|
5285
5594
|
if (typeof err === "string") return err;
|
|
@@ -5586,18 +5895,74 @@ var DeviceType = /* @__PURE__ */ ((DeviceType2) => {
|
|
|
5586
5895
|
DeviceType2["Sensor"] = "sensor";
|
|
5587
5896
|
DeviceType2["Thermostat"] = "thermostat";
|
|
5588
5897
|
DeviceType2["Button"] = "button";
|
|
5898
|
+
DeviceType2["EventEmitter"] = "event-emitter";
|
|
5899
|
+
DeviceType2["Update"] = "update";
|
|
5589
5900
|
DeviceType2["Generic"] = "generic";
|
|
5901
|
+
DeviceType2["Notifier"] = "notifier";
|
|
5902
|
+
DeviceType2["Script"] = "script";
|
|
5903
|
+
DeviceType2["Automation"] = "automation";
|
|
5904
|
+
DeviceType2["Lock"] = "lock";
|
|
5905
|
+
DeviceType2["Cover"] = "cover";
|
|
5906
|
+
DeviceType2["Valve"] = "valve";
|
|
5907
|
+
DeviceType2["Humidifier"] = "humidifier";
|
|
5908
|
+
DeviceType2["WaterHeater"] = "water-heater";
|
|
5909
|
+
DeviceType2["Fan"] = "fan";
|
|
5910
|
+
DeviceType2["MediaPlayer"] = "media-player";
|
|
5911
|
+
DeviceType2["AlarmPanel"] = "alarm-panel";
|
|
5912
|
+
DeviceType2["Control"] = "control";
|
|
5913
|
+
DeviceType2["Presence"] = "presence";
|
|
5914
|
+
DeviceType2["Weather"] = "weather";
|
|
5915
|
+
DeviceType2["Vacuum"] = "vacuum";
|
|
5916
|
+
DeviceType2["LawnMower"] = "lawn-mower";
|
|
5917
|
+
DeviceType2["Container"] = "container";
|
|
5918
|
+
DeviceType2["Image"] = "image";
|
|
5590
5919
|
return DeviceType2;
|
|
5591
5920
|
})(DeviceType || {});
|
|
5592
5921
|
var DeviceFeature = /* @__PURE__ */ ((DeviceFeature2) => {
|
|
5593
5922
|
DeviceFeature2["BatteryOperated"] = "battery-operated";
|
|
5594
5923
|
DeviceFeature2["Rebootable"] = "rebootable";
|
|
5924
|
+
DeviceFeature2["Resyncable"] = "resyncable";
|
|
5595
5925
|
DeviceFeature2["NativeSnapshot"] = "native-snapshot";
|
|
5596
5926
|
DeviceFeature2["DoorbellButton"] = "doorbell-button";
|
|
5597
5927
|
DeviceFeature2["TwoWayAudio"] = "two-way-audio";
|
|
5598
5928
|
DeviceFeature2["PanTiltZoom"] = "pan-tilt-zoom";
|
|
5599
5929
|
DeviceFeature2["PtzAutotrack"] = "ptz-autotrack";
|
|
5600
5930
|
DeviceFeature2["MotionTrigger"] = "motion-trigger";
|
|
5931
|
+
DeviceFeature2["LightColorRgb"] = "light-color-rgb";
|
|
5932
|
+
DeviceFeature2["LightColorHsv"] = "light-color-hsv";
|
|
5933
|
+
DeviceFeature2["LightColorMired"] = "light-color-mired";
|
|
5934
|
+
DeviceFeature2["ClimateDualSetpoint"] = "climate-dual-setpoint";
|
|
5935
|
+
DeviceFeature2["ClimateHumidity"] = "climate-humidity";
|
|
5936
|
+
DeviceFeature2["ClimateFanMode"] = "climate-fan-mode";
|
|
5937
|
+
DeviceFeature2["ClimatePreset"] = "climate-preset";
|
|
5938
|
+
DeviceFeature2["CoverPositionable"] = "cover-positionable";
|
|
5939
|
+
DeviceFeature2["CoverTilt"] = "cover-tilt";
|
|
5940
|
+
DeviceFeature2["ValvePositionable"] = "valve-positionable";
|
|
5941
|
+
DeviceFeature2["FanSpeed"] = "fan-speed";
|
|
5942
|
+
DeviceFeature2["FanPreset"] = "fan-preset";
|
|
5943
|
+
DeviceFeature2["FanDirection"] = "fan-direction";
|
|
5944
|
+
DeviceFeature2["FanOscillating"] = "fan-oscillating";
|
|
5945
|
+
DeviceFeature2["LockPinRequired"] = "lock-pin-required";
|
|
5946
|
+
DeviceFeature2["LockOpen"] = "lock-open";
|
|
5947
|
+
DeviceFeature2["MediaPlayerSeek"] = "media-player-seek";
|
|
5948
|
+
DeviceFeature2["MediaPlayerVolume"] = "media-player-volume";
|
|
5949
|
+
DeviceFeature2["MediaPlayerMute"] = "media-player-mute";
|
|
5950
|
+
DeviceFeature2["MediaPlayerShuffle"] = "media-player-shuffle";
|
|
5951
|
+
DeviceFeature2["MediaPlayerRepeat"] = "media-player-repeat";
|
|
5952
|
+
DeviceFeature2["MediaPlayerSelectSource"] = "media-player-select-source";
|
|
5953
|
+
DeviceFeature2["MediaPlayerPlayMedia"] = "media-player-play-media";
|
|
5954
|
+
DeviceFeature2["MediaPlayerNext"] = "media-player-next";
|
|
5955
|
+
DeviceFeature2["MediaPlayerPrevious"] = "media-player-previous";
|
|
5956
|
+
DeviceFeature2["MediaPlayerStop"] = "media-player-stop";
|
|
5957
|
+
DeviceFeature2["AlarmPinRequired"] = "alarm-pin-required";
|
|
5958
|
+
DeviceFeature2["PresenceGps"] = "presence-gps";
|
|
5959
|
+
DeviceFeature2["NotifierImage"] = "notifier-image";
|
|
5960
|
+
DeviceFeature2["NotifierPriority"] = "notifier-priority";
|
|
5961
|
+
DeviceFeature2["NotifierData"] = "notifier-data";
|
|
5962
|
+
DeviceFeature2["NotifierActions"] = "notifier-actions";
|
|
5963
|
+
DeviceFeature2["NotifierRecipients"] = "notifier-recipients";
|
|
5964
|
+
DeviceFeature2["ScriptVariables"] = "script-variables";
|
|
5965
|
+
DeviceFeature2["AutomationSkipCondition"] = "automation-skip-condition";
|
|
5601
5966
|
return DeviceFeature2;
|
|
5602
5967
|
})(DeviceFeature || {});
|
|
5603
5968
|
var DeviceRole = /* @__PURE__ */ ((DeviceRole2) => {
|
|
@@ -5610,6 +5975,40 @@ var DeviceRole = /* @__PURE__ */ ((DeviceRole2) => {
|
|
|
5610
5975
|
DeviceRole2["Nightvision"] = "nightvision";
|
|
5611
5976
|
DeviceRole2["PrivacyMask"] = "privacy-mask";
|
|
5612
5977
|
DeviceRole2["Doorbell"] = "doorbell";
|
|
5978
|
+
DeviceRole2["BinaryHelper"] = "binary-helper";
|
|
5979
|
+
DeviceRole2["MotionSensor"] = "motion-sensor";
|
|
5980
|
+
DeviceRole2["ContactSensor"] = "contact-sensor";
|
|
5981
|
+
DeviceRole2["LeakSensor"] = "leak-sensor";
|
|
5982
|
+
DeviceRole2["SmokeSensor"] = "smoke-sensor";
|
|
5983
|
+
DeviceRole2["COSensor"] = "co-sensor";
|
|
5984
|
+
DeviceRole2["GasSensor"] = "gas-sensor";
|
|
5985
|
+
DeviceRole2["TamperSensor"] = "tamper-sensor";
|
|
5986
|
+
DeviceRole2["VibrationSensor"] = "vibration-sensor";
|
|
5987
|
+
DeviceRole2["ConnectivitySensor"] = "connectivity-sensor";
|
|
5988
|
+
DeviceRole2["SoundSensor"] = "sound-sensor";
|
|
5989
|
+
DeviceRole2["BinarySensor"] = "binary-sensor";
|
|
5990
|
+
DeviceRole2["TemperatureSensor"] = "temperature-sensor";
|
|
5991
|
+
DeviceRole2["HumiditySensor"] = "humidity-sensor";
|
|
5992
|
+
DeviceRole2["AmbientLightSensor"] = "ambient-light-sensor";
|
|
5993
|
+
DeviceRole2["PressureSensor"] = "pressure-sensor";
|
|
5994
|
+
DeviceRole2["PowerSensor"] = "power-sensor";
|
|
5995
|
+
DeviceRole2["EnergySensor"] = "energy-sensor";
|
|
5996
|
+
DeviceRole2["VoltageSensor"] = "voltage-sensor";
|
|
5997
|
+
DeviceRole2["CurrentSensor"] = "current-sensor";
|
|
5998
|
+
DeviceRole2["AirQualitySensor"] = "air-quality-sensor";
|
|
5999
|
+
DeviceRole2["BatterySensor"] = "battery-sensor";
|
|
6000
|
+
DeviceRole2["NumericSensor"] = "numeric-sensor";
|
|
6001
|
+
DeviceRole2["EnumSensor"] = "enum-sensor";
|
|
6002
|
+
DeviceRole2["DateTimeSensor"] = "datetime-sensor";
|
|
6003
|
+
DeviceRole2["GenericSensor"] = "generic-sensor";
|
|
6004
|
+
DeviceRole2["NumericControl"] = "numeric-control";
|
|
6005
|
+
DeviceRole2["SelectControl"] = "select-control";
|
|
6006
|
+
DeviceRole2["TextControl"] = "text-control";
|
|
6007
|
+
DeviceRole2["DateTimeControl"] = "datetime-control";
|
|
6008
|
+
DeviceRole2["MobilePushNotifier"] = "mobile-push-notifier";
|
|
6009
|
+
DeviceRole2["MessagingNotifier"] = "messaging-notifier";
|
|
6010
|
+
DeviceRole2["EmailNotifier"] = "email-notifier";
|
|
6011
|
+
DeviceRole2["GenericNotifier"] = "generic-notifier";
|
|
5613
6012
|
return DeviceRole2;
|
|
5614
6013
|
})(DeviceRole || {});
|
|
5615
6014
|
({
|
|
@@ -5691,6 +6090,33 @@ const FeatureProbeStatusSchema = object({
|
|
|
5691
6090
|
}) }
|
|
5692
6091
|
}
|
|
5693
6092
|
});
|
|
6093
|
+
object({
|
|
6094
|
+
/** Carbon dioxide concentration in ppm. */
|
|
6095
|
+
co2Ppm: number().min(0).optional(),
|
|
6096
|
+
/** Total volatile organic compounds in ppb. */
|
|
6097
|
+
vocPpb: number().min(0).optional(),
|
|
6098
|
+
/** Particulate matter ≤ 2.5 μm in µg/m³. */
|
|
6099
|
+
pm25: number().min(0).optional(),
|
|
6100
|
+
/** Particulate matter ≤ 10 μm in µg/m³. */
|
|
6101
|
+
pm10: number().min(0).optional(),
|
|
6102
|
+
/** Composite AQI value (typically 0..500). */
|
|
6103
|
+
aqi: number().optional(),
|
|
6104
|
+
/** Ms epoch when the slice was last updated. */
|
|
6105
|
+
lastFetchedAt: number(),
|
|
6106
|
+
/** Live display unit of the single metric this slice carries (e.g. HA
|
|
6107
|
+
* `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
|
|
6108
|
+
* upstream `sensor.*` entity surfaces ONE device_class, so one unit
|
|
6109
|
+
* per slice is unambiguous. */
|
|
6110
|
+
unit: string().optional(),
|
|
6111
|
+
/** Suggested decimal places for numeric display.
|
|
6112
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
6113
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
6114
|
+
* auto-formatting when absent. */
|
|
6115
|
+
precision: number().int().min(0).max(10).optional()
|
|
6116
|
+
});
|
|
6117
|
+
({
|
|
6118
|
+
deviceTypes: [DeviceType.Sensor]
|
|
6119
|
+
});
|
|
5694
6120
|
const ContributionSectionSchema = object({
|
|
5695
6121
|
id: string(),
|
|
5696
6122
|
title: string(),
|
|
@@ -5748,27 +6174,112 @@ function method(input, output, options) {
|
|
|
5748
6174
|
function event(data) {
|
|
5749
6175
|
return { data };
|
|
5750
6176
|
}
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
6177
|
+
const AlarmStateSchema = _enum([
|
|
6178
|
+
"disarmed",
|
|
6179
|
+
"armed_home",
|
|
6180
|
+
"armed_away",
|
|
6181
|
+
"armed_night",
|
|
6182
|
+
"armed_vacation",
|
|
6183
|
+
"armed_custom_bypass",
|
|
6184
|
+
"arming",
|
|
6185
|
+
"disarming",
|
|
6186
|
+
"pending",
|
|
6187
|
+
"triggered"
|
|
6188
|
+
]);
|
|
6189
|
+
const AlarmArmModeSchema = _enum([
|
|
6190
|
+
"home",
|
|
6191
|
+
"away",
|
|
6192
|
+
"night",
|
|
6193
|
+
"vacation",
|
|
6194
|
+
"custom_bypass"
|
|
6195
|
+
]);
|
|
6196
|
+
object({
|
|
6197
|
+
/** Current lifecycle state. */
|
|
6198
|
+
state: AlarmStateSchema,
|
|
6199
|
+
/** Subset of arm modes the panel accepts. UI renders one button per
|
|
6200
|
+
* mode in this list. */
|
|
6201
|
+
availableModes: array(AlarmArmModeSchema),
|
|
6202
|
+
/** Whether the panel requires a PIN on arm / disarm. Mirrors
|
|
6203
|
+
* `DeviceFeature.AlarmPinRequired` for slice consumers. */
|
|
6204
|
+
requiresCode: boolean(),
|
|
6205
|
+
/** Ms epoch when the slice was last updated. */
|
|
6206
|
+
lastChangedAt: number()
|
|
5759
6207
|
});
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
6208
|
+
({
|
|
6209
|
+
deviceTypes: [DeviceType.AlarmPanel],
|
|
6210
|
+
methods: {
|
|
6211
|
+
arm: method(
|
|
6212
|
+
object({
|
|
6213
|
+
deviceId: number().int().nonnegative(),
|
|
6214
|
+
mode: AlarmArmModeSchema,
|
|
6215
|
+
/** Optional PIN code. Required when `requiresCode === true`.
|
|
6216
|
+
* Passed through to the upstream service; never persisted. */
|
|
6217
|
+
code: string().min(1).optional()
|
|
6218
|
+
}),
|
|
6219
|
+
_void(),
|
|
6220
|
+
{ kind: "mutation", auth: "admin" }
|
|
6221
|
+
),
|
|
6222
|
+
disarm: method(
|
|
6223
|
+
object({
|
|
6224
|
+
deviceId: number().int().nonnegative(),
|
|
6225
|
+
code: string().min(1).optional()
|
|
6226
|
+
}),
|
|
6227
|
+
_void(),
|
|
6228
|
+
{ kind: "mutation", auth: "admin" }
|
|
6229
|
+
),
|
|
6230
|
+
/**
|
|
6231
|
+
* Force the panel into the `triggered` state — used by HA
|
|
6232
|
+
* automations to surface external sensor events through the panel
|
|
6233
|
+
* (e.g. a Reolink camera intrusion event firing the security
|
|
6234
|
+
* system). Provider rejects when the panel hardware doesn't
|
|
6235
|
+
* support a software-initiated trigger.
|
|
6236
|
+
*/
|
|
6237
|
+
trigger: method(
|
|
6238
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
6239
|
+
_void(),
|
|
6240
|
+
{ kind: "mutation", auth: "admin" }
|
|
6241
|
+
)
|
|
6242
|
+
}
|
|
6243
|
+
});
|
|
6244
|
+
object({
|
|
6245
|
+
/** Current illuminance in lux (lx). */
|
|
6246
|
+
lux: number().min(0),
|
|
6247
|
+
/** Ms epoch when the slice was last updated. */
|
|
6248
|
+
lastFetchedAt: number(),
|
|
6249
|
+
/** Live display unit from the upstream source (e.g. HA
|
|
6250
|
+
* `attributes.unit_of_measurement`). The UI prefers this over the
|
|
6251
|
+
* role's canonical unit. Absent → fall back to the canonical unit. */
|
|
6252
|
+
unit: string().optional(),
|
|
6253
|
+
/** Suggested decimal places for numeric display.
|
|
6254
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
6255
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
6256
|
+
* auto-formatting when absent. */
|
|
6257
|
+
precision: number().int().min(0).max(10).optional()
|
|
6258
|
+
});
|
|
6259
|
+
({
|
|
6260
|
+
deviceTypes: [DeviceType.Sensor]
|
|
6261
|
+
});
|
|
6262
|
+
const AudioClassSummarySchema = object({
|
|
6263
|
+
className: string(),
|
|
6264
|
+
/** Number of windows (chunks) where this class was the top hit. */
|
|
6265
|
+
hits: number().int().nonnegative(),
|
|
6266
|
+
/** Mean score across those hits, clamped to [0,1]. */
|
|
6267
|
+
avgScore: number().min(0).max(1),
|
|
6268
|
+
/** Peak score in the window. */
|
|
6269
|
+
peakScore: number().min(0).max(1)
|
|
6270
|
+
});
|
|
6271
|
+
const AudioMetricsSnapshotSchema = object({
|
|
6272
|
+
/** Wall-clock timestamp (ms) of the most recent audio window. */
|
|
6273
|
+
ts: number().int(),
|
|
6274
|
+
/** Sliding-window length (seconds) used for aggregation. */
|
|
6275
|
+
windowSec: number().int().positive(),
|
|
6276
|
+
/** Latest level reading from the most recent window. */
|
|
6277
|
+
level: object({
|
|
6278
|
+
rms: number(),
|
|
6279
|
+
dbfs: number()
|
|
6280
|
+
}),
|
|
6281
|
+
/** Peak dBFS observed across the rolling window. */
|
|
6282
|
+
peakDbfs: number(),
|
|
5772
6283
|
/** Mean dBFS across the rolling window. */
|
|
5773
6284
|
avgDbfs: number(),
|
|
5774
6285
|
/** Most recent above-threshold classification, or null on silence. */
|
|
@@ -5844,6 +6355,46 @@ const audioMetricsCapability = {
|
|
|
5844
6355
|
/** Reactive runtime-state mirror — live `device.state.audioMetrics.value`. */
|
|
5845
6356
|
runtimeState: AudioMetricsSnapshotSchema
|
|
5846
6357
|
};
|
|
6358
|
+
object({
|
|
6359
|
+
/** Whether the automation is currently enabled. Disabled automations
|
|
6360
|
+
* ignore their trigger block — manual `trigger` still works. */
|
|
6361
|
+
enabled: boolean(),
|
|
6362
|
+
/** Whether the automation is currently executing its action block. */
|
|
6363
|
+
isRunning: boolean(),
|
|
6364
|
+
/** Ms epoch of the last successful run. 0 when never run. */
|
|
6365
|
+
lastTriggeredAt: number(),
|
|
6366
|
+
/** Failure description from the last completed run. Null on success
|
|
6367
|
+
* or when never run. */
|
|
6368
|
+
lastError: string().nullable(),
|
|
6369
|
+
/** Ms epoch when the slice was last updated. */
|
|
6370
|
+
lastChangedAt: number()
|
|
6371
|
+
});
|
|
6372
|
+
({
|
|
6373
|
+
deviceTypes: [DeviceType.Automation],
|
|
6374
|
+
methods: {
|
|
6375
|
+
enable: method(
|
|
6376
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
6377
|
+
_void(),
|
|
6378
|
+
{ kind: "mutation", auth: "admin" }
|
|
6379
|
+
),
|
|
6380
|
+
disable: method(
|
|
6381
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
6382
|
+
_void(),
|
|
6383
|
+
{ kind: "mutation", auth: "admin" }
|
|
6384
|
+
),
|
|
6385
|
+
trigger: method(
|
|
6386
|
+
object({
|
|
6387
|
+
deviceId: number().int().nonnegative(),
|
|
6388
|
+
/** When true, fires the action block while bypassing the
|
|
6389
|
+
* automation's condition evaluation. Gated by
|
|
6390
|
+
* `DeviceFeature.AutomationSkipCondition`. */
|
|
6391
|
+
skipCondition: boolean().optional()
|
|
6392
|
+
}),
|
|
6393
|
+
_void(),
|
|
6394
|
+
{ kind: "mutation", auth: "admin" }
|
|
6395
|
+
)
|
|
6396
|
+
}
|
|
6397
|
+
});
|
|
5847
6398
|
const BatteryStatusSchema = object({
|
|
5848
6399
|
/** 0..100 inclusive. Firmware-reported. */
|
|
5849
6400
|
percentage: number().min(0).max(100),
|
|
@@ -5861,10 +6412,48 @@ const BatteryStatusSchema = object({
|
|
|
5861
6412
|
*/
|
|
5862
6413
|
sleeping: boolean(),
|
|
5863
6414
|
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
5864
|
-
lastUpdated: number()
|
|
6415
|
+
lastUpdated: number(),
|
|
6416
|
+
/**
|
|
6417
|
+
* True when the source is a BINARY low-battery indicator (HA
|
|
6418
|
+
* `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
|
|
6419
|
+
* charge level — `percentage` is then a coarse stand-in (100 = normal,
|
|
6420
|
+
* sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
|
|
6421
|
+
* misleading exact percentage. Absent/false → genuine 0–100 % reading.
|
|
6422
|
+
*/
|
|
6423
|
+
binary: boolean().optional()
|
|
5865
6424
|
});
|
|
5866
6425
|
({
|
|
5867
6426
|
deviceTypes: [DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch],
|
|
6427
|
+
methods: {
|
|
6428
|
+
/**
|
|
6429
|
+
* Explicitly wake the camera from low-power sleep ahead of a
|
|
6430
|
+
* streaming session start. Consumers that initiate a stream
|
|
6431
|
+
* against a sleeping battery cam (HomeKit Secure Video, Alexa
|
|
6432
|
+
* RTCSession, snapshot wrappers) call this with a short timeout
|
|
6433
|
+
* before establishing the media pipeline — the broker's own
|
|
6434
|
+
* passive wake-on-dial works but adds 5–7 seconds to first-frame,
|
|
6435
|
+
* during which the consumer renders a black screen. Pre-waking
|
|
6436
|
+
* compresses that gap.
|
|
6437
|
+
*
|
|
6438
|
+
* Returns `awoke: true` when the firmware acknowledged the wake
|
|
6439
|
+
* before `timeoutMs`. Returns `awoke: false` when it timed out OR
|
|
6440
|
+
* the cap surface is unavailable (no Baichuan / firmware
|
|
6441
|
+
* channel); the caller should still attempt the stream — the
|
|
6442
|
+
* passive broker wake remains as fallback.
|
|
6443
|
+
*/
|
|
6444
|
+
wakeForStream: method(
|
|
6445
|
+
object({
|
|
6446
|
+
deviceId: number(),
|
|
6447
|
+
/** Bound on the wait. Sensible range 3000–10000ms. */
|
|
6448
|
+
timeoutMs: number().int().min(500).max(3e4).default(8e3)
|
|
6449
|
+
}),
|
|
6450
|
+
object({
|
|
6451
|
+
awoke: boolean(),
|
|
6452
|
+
durationMs: number()
|
|
6453
|
+
}),
|
|
6454
|
+
{ kind: "mutation" }
|
|
6455
|
+
)
|
|
6456
|
+
},
|
|
5868
6457
|
events: {
|
|
5869
6458
|
/**
|
|
5870
6459
|
* Emitted whenever the cached status changes (firmware push OR
|
|
@@ -5878,6 +6467,14 @@ const BatteryStatusSchema = object({
|
|
|
5878
6467
|
}) }
|
|
5879
6468
|
}
|
|
5880
6469
|
});
|
|
6470
|
+
object({
|
|
6471
|
+
on: boolean(),
|
|
6472
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
6473
|
+
lastChangedAt: number()
|
|
6474
|
+
});
|
|
6475
|
+
({
|
|
6476
|
+
deviceTypes: [DeviceType.Sensor]
|
|
6477
|
+
});
|
|
5881
6478
|
object({
|
|
5882
6479
|
/** Current level as 0..100 inclusive. Firmware-reported. */
|
|
5883
6480
|
percentage: number().min(0).max(100),
|
|
@@ -5909,287 +6506,199 @@ object({
|
|
|
5909
6506
|
}) }
|
|
5910
6507
|
}
|
|
5911
6508
|
});
|
|
5912
|
-
const
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
"push-annexb"
|
|
5919
|
-
]);
|
|
5920
|
-
const CamStreamResolutionSchema = object({
|
|
5921
|
-
width: number().int().positive(),
|
|
5922
|
-
height: number().int().positive()
|
|
6509
|
+
const StreamFormatSchema = _enum(["webrtc", "hls", "mjpeg", "rtsp"]);
|
|
6510
|
+
const StreamInfoSchema = object({
|
|
6511
|
+
streamId: string(),
|
|
6512
|
+
format: StreamFormatSchema,
|
|
6513
|
+
url: string().nullable(),
|
|
6514
|
+
active: boolean()
|
|
5923
6515
|
});
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
6516
|
+
({
|
|
6517
|
+
methods: {
|
|
6518
|
+
registerStream: method(
|
|
6519
|
+
object({ streamId: string(), sourceUrl: string(), codec: string().optional() }),
|
|
6520
|
+
_void(),
|
|
6521
|
+
{ kind: "mutation" }
|
|
6522
|
+
),
|
|
6523
|
+
unregisterStream: method(object({ streamId: string() }), _void(), { kind: "mutation" }),
|
|
6524
|
+
getStreamUrl: method(
|
|
6525
|
+
object({ streamId: string(), format: StreamFormatSchema }),
|
|
6526
|
+
string().nullable()
|
|
6527
|
+
),
|
|
6528
|
+
listStreams: method(_void(), array(StreamInfoSchema))
|
|
6529
|
+
}
|
|
6530
|
+
});
|
|
6531
|
+
const RtspRestreamEntrySchema = object({
|
|
6532
|
+
brokerId: string(),
|
|
6533
|
+
url: string(),
|
|
6534
|
+
mutedUrl: string(),
|
|
6535
|
+
enabled: boolean(),
|
|
6536
|
+
/**
|
|
6537
|
+
* Source-stream codec / resolution for the camStream this entry serves
|
|
6538
|
+
* (the broker's "high"/"mid"/"low" profile slot for this device).
|
|
6539
|
+
* Used by exporter pickers (`pickPreferredRtspEntry`) to resolve
|
|
6540
|
+
* `streamPreference: 'auto'` to the slot whose source is closest to
|
|
6541
|
+
* the consumer's target — Alexa wants ~720p, HomeKit wants ~1080p,
|
|
6542
|
+
* and there's no point dialling the 4K slot for an Echo Show. Absent
|
|
6543
|
+
* when the source publisher never advertised the field; pickers fall
|
|
6544
|
+
* back to first-enabled in that case.
|
|
6545
|
+
*/
|
|
5931
6546
|
codec: string().optional(),
|
|
5932
|
-
resolution:
|
|
5933
|
-
|
|
5934
|
-
|
|
6547
|
+
resolution: object({
|
|
6548
|
+
width: number().int().positive(),
|
|
6549
|
+
height: number().int().positive()
|
|
6550
|
+
}).optional()
|
|
6551
|
+
});
|
|
6552
|
+
const BrokerRtspClientSchema = object({
|
|
6553
|
+
sessionId: string(),
|
|
6554
|
+
remoteAddr: string(),
|
|
6555
|
+
playing: boolean(),
|
|
6556
|
+
muted: boolean(),
|
|
6557
|
+
connectedAt: number(),
|
|
6558
|
+
lastRtpAt: number(),
|
|
6559
|
+
bytesSent: number()
|
|
6560
|
+
});
|
|
6561
|
+
const BrokerDecodedClientSchema = object({
|
|
6562
|
+
tag: string(),
|
|
6563
|
+
subscribedAt: number(),
|
|
6564
|
+
maxFps: number(),
|
|
6565
|
+
framesDelivered: number(),
|
|
6566
|
+
framesDropped: number()
|
|
6567
|
+
});
|
|
6568
|
+
const BrokerAudioClientSchema = object({
|
|
6569
|
+
tag: string(),
|
|
6570
|
+
subscribedAt: number(),
|
|
6571
|
+
chunksDelivered: number()
|
|
6572
|
+
});
|
|
6573
|
+
const BrokerConsumerKindSchema = _enum([
|
|
6574
|
+
"alexa",
|
|
6575
|
+
"homekit",
|
|
6576
|
+
"webrtc-browser",
|
|
6577
|
+
"webrtc-mobile",
|
|
6578
|
+
"webrtc-whep",
|
|
6579
|
+
"rtsp-listen",
|
|
6580
|
+
"derived-broker",
|
|
6581
|
+
"recording",
|
|
6582
|
+
"pipeline",
|
|
6583
|
+
"snapshot",
|
|
6584
|
+
"warmup",
|
|
6585
|
+
"unknown"
|
|
6586
|
+
]);
|
|
6587
|
+
const BrokerConsumerAttributionSchema = object({
|
|
6588
|
+
kind: BrokerConsumerKindSchema,
|
|
6589
|
+
/**
|
|
6590
|
+
* Free-form label intended to disambiguate consumers OF THE SAME kind
|
|
6591
|
+
* on the same broker — e.g. user name, device alias. Should NOT repeat
|
|
6592
|
+
* the kind / cam / cam-stream / sessionId (those are surfaced by
|
|
6593
|
+
* dedicated fields). When empty the widget falls back to `${kind} ·
|
|
6594
|
+
* <sessionId tail>`.
|
|
6595
|
+
*/
|
|
5935
6596
|
label: string().optional(),
|
|
5936
6597
|
/**
|
|
5937
|
-
*
|
|
5938
|
-
*
|
|
5939
|
-
*
|
|
5940
|
-
*
|
|
6598
|
+
* Which part of the encoded plane this subscription consumes:
|
|
6599
|
+
* - `'audio'` — audio packets only
|
|
6600
|
+
* - `'video'` — video packets only
|
|
6601
|
+
* - `'both'` — both video + audio (typical for AnnexB push paths
|
|
6602
|
+
* that don't split the source RTP)
|
|
5941
6603
|
*
|
|
5942
|
-
*
|
|
5943
|
-
*
|
|
5944
|
-
* battery-powered, every downstream service derives the right policy
|
|
5945
|
-
* from this list.
|
|
6604
|
+
* Missing field means "unknown" — older callers and the legacy
|
|
6605
|
+
* paths that haven't been migrated yet.
|
|
5946
6606
|
*/
|
|
5947
|
-
|
|
6607
|
+
media: _enum(["audio", "video", "both"]).optional(),
|
|
5948
6608
|
/**
|
|
5949
|
-
*
|
|
5950
|
-
*
|
|
5951
|
-
*
|
|
5952
|
-
*
|
|
5953
|
-
*
|
|
5954
|
-
* mirrors as `autoEligible: false` (still pickable per slot, just not
|
|
5955
|
-
* the auto choice). Manual `assignProfile` calls remain valid for
|
|
5956
|
-
* non-eligible streams.
|
|
6609
|
+
* Codec the consumer receives AFTER any per-session re-encoding. For
|
|
6610
|
+
* a WebRTC session this is the negotiated egress codec (e.g. `H264`,
|
|
6611
|
+
* `H265`, `Opus`, `Pcmu`); for an RTSP restreamer it mirrors the
|
|
6612
|
+
* source codec. Surfaced in the widget chip so operators see at a
|
|
6613
|
+
* glance whether a viewer is on H.264 (Echo) or H.265 (Safari).
|
|
5957
6614
|
*/
|
|
5958
|
-
|
|
6615
|
+
targetCodec: string().optional(),
|
|
5959
6616
|
/**
|
|
5960
|
-
*
|
|
5961
|
-
*
|
|
5962
|
-
* `
|
|
5963
|
-
*
|
|
5964
|
-
*
|
|
6617
|
+
* Whether the broker is re-encoding for this consumer:
|
|
6618
|
+
* - `'passthrough'` — bytes flow source→consumer without ffmpeg
|
|
6619
|
+
* - `'repacketize'` — RTP payload is re-packetized but NOT re-encoded
|
|
6620
|
+
* (preserves frames, just rebuilds headers; e.g.
|
|
6621
|
+
* the H.265 repacketizer path)
|
|
6622
|
+
* - `'transcode'` — a full encode/decode round (ffmpeg, libx264,
|
|
6623
|
+
* libav…); the most expensive path
|
|
5965
6624
|
*/
|
|
5966
|
-
|
|
6625
|
+
transport: _enum(["passthrough", "repacketize", "transcode"]).optional(),
|
|
6626
|
+
/** Remote peer IP if the consumer terminates a network socket. */
|
|
6627
|
+
remoteAddr: string().optional(),
|
|
6628
|
+
/**
|
|
6629
|
+
* Server-read User-Agent of the originating client; enriched by the hub
|
|
6630
|
+
* from the tRPC request context (browser sessions).
|
|
6631
|
+
*/
|
|
6632
|
+
userAgent: string().optional(),
|
|
6633
|
+
/** Authenticated user id (CamStack OAuth subject) when known. */
|
|
6634
|
+
userId: string().optional(),
|
|
6635
|
+
/** Higher-level session identifier (Alexa Echo sessionId, HAP session, …). */
|
|
6636
|
+
sessionId: string().optional(),
|
|
6637
|
+
/** Free-form key/value extras (e.g. clientHints from a WebRTC offer). */
|
|
6638
|
+
extra: record(string(), string()).optional()
|
|
6639
|
+
}).readonly();
|
|
6640
|
+
const BrokerEncodedClientSchema = object({
|
|
6641
|
+
/** Stable id assigned on attach; used by `killClient`. */
|
|
6642
|
+
id: string(),
|
|
6643
|
+
/** Which broker fanout the subscriber rides — annexB encoded packets vs raw RTP. */
|
|
6644
|
+
channel: _enum(["annexb", "rtp"]),
|
|
6645
|
+
attribution: BrokerConsumerAttributionSchema,
|
|
6646
|
+
subscribedAt: number(),
|
|
6647
|
+
/** Total packets delivered (annex-B EncodedPackets or RTP byte-buffers). */
|
|
6648
|
+
packetsDelivered: number()
|
|
5967
6649
|
});
|
|
5968
|
-
const
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
6650
|
+
const BrokerClientsSchema = object({
|
|
6651
|
+
rtsp: array(BrokerRtspClientSchema).readonly(),
|
|
6652
|
+
decoded: array(BrokerDecodedClientSchema).readonly(),
|
|
6653
|
+
audio: array(BrokerAudioClientSchema).readonly(),
|
|
6654
|
+
encoded: array(BrokerEncodedClientSchema).readonly(),
|
|
6655
|
+
pipeClients: number(),
|
|
6656
|
+
/** Total encoded + raw-RTP callback subscriber count (sum of `encoded[].length`). */
|
|
6657
|
+
encodedSubscribers: number()
|
|
6658
|
+
});
|
|
6659
|
+
_enum([
|
|
6660
|
+
"reconnecting",
|
|
6661
|
+
"sleeping",
|
|
6662
|
+
"offline",
|
|
6663
|
+
"disabled",
|
|
6664
|
+
"waking"
|
|
5974
6665
|
]);
|
|
5975
|
-
const
|
|
6666
|
+
const VideoCodecTargetSchema = _enum(["h264", "h265", "copy"]);
|
|
6667
|
+
const AudioCodecTargetSchema = _enum(["aac", "opus", "pcmu", "copy", "none"]);
|
|
6668
|
+
const GetStreamWithCodecInputSchema = object({
|
|
5976
6669
|
deviceId: number().int().nonnegative(),
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
resolution:
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
profileHint: _enum(["high", "mid", "low"]).optional()
|
|
6670
|
+
/** Target codec. `'copy'` = passthrough (no re-encode). */
|
|
6671
|
+
video: VideoCodecTargetSchema,
|
|
6672
|
+
/** Target audio codec. `'copy'` = passthrough. Defaults to `'aac'`. */
|
|
6673
|
+
audio: AudioCodecTargetSchema.optional(),
|
|
6674
|
+
/** Target a profile's assigned source camStream. Required to resolve a source. */
|
|
6675
|
+
profile: CamProfileSchema.optional(),
|
|
6676
|
+
/** Optional output resolution target. When set: drives the transcode
|
|
6677
|
+
* output scale (downscale toward WxH), and — when `profile` is absent —
|
|
6678
|
+
* selects the published source closest to this resolution. Ignored for
|
|
6679
|
+
* `video:'copy'` scaling (a copy can't be rescaled), but still used for
|
|
6680
|
+
* source selection. */
|
|
6681
|
+
targetResolution: object({ width: number().int().positive(), height: number().int().positive() }).optional(),
|
|
6682
|
+
/** Extra ffmpeg output flags appended verbatim by the consumer (in code).
|
|
6683
|
+
* Folded into the pipeline key so different args never share a child. */
|
|
6684
|
+
outputArgs: array(string()).optional(),
|
|
6685
|
+
/** Opt-in: carry transcoded audio over an RTP sidecar grafted into the
|
|
6686
|
+
* restreamer SDP (only for re-encoded audio targets — aac/opus/pcmu).
|
|
6687
|
+
* Default/absent = the live-verified video-only egress. */
|
|
6688
|
+
audioEgress: boolean().optional(),
|
|
6689
|
+
tag: string().optional()
|
|
5998
6690
|
});
|
|
5999
|
-
object({
|
|
6000
|
-
type: string(),
|
|
6691
|
+
const RtpSourceSchema = object({
|
|
6001
6692
|
url: string(),
|
|
6002
|
-
videoCodec:
|
|
6003
|
-
audioCodec: string()
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
keyframe: boolean(),
|
|
6012
|
-
codec: string()
|
|
6013
|
-
});
|
|
6014
|
-
const DecodedFrameSchema = object({
|
|
6015
|
-
data: _instanceof(Uint8Array),
|
|
6016
|
-
width: number(),
|
|
6017
|
-
height: number(),
|
|
6018
|
-
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6019
|
-
timestamp: number()
|
|
6020
|
-
});
|
|
6021
|
-
const FrameHandleSchema = object({
|
|
6022
|
-
shmId: string(),
|
|
6023
|
-
slot: number().int().nonnegative(),
|
|
6024
|
-
seq: number().int().nonnegative(),
|
|
6025
|
-
width: number().int().positive(),
|
|
6026
|
-
height: number().int().positive(),
|
|
6027
|
-
format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
|
|
6028
|
-
pts: number(),
|
|
6029
|
-
byteLength: number().int().nonnegative(),
|
|
6030
|
-
nodeId: string(),
|
|
6031
|
-
slotCount: number().int().positive()
|
|
6032
|
-
});
|
|
6033
|
-
const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
|
|
6034
|
-
const SubscribeFramesInputSchema = object({
|
|
6035
|
-
brokerId: string(),
|
|
6036
|
-
format: FrameHandleFormatSchema,
|
|
6037
|
-
/**
|
|
6038
|
-
* Optional reader-side cadence hint in frames per second. The broker does
|
|
6039
|
-
* NOT throttle — latest-wins ring reads drop frames implicitly for a slow
|
|
6040
|
-
* consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
|
|
6041
|
-
* the consumer can pace its own `pullFrameHandles` polling.
|
|
6042
|
-
*/
|
|
6043
|
-
maxFps: number().positive().optional(),
|
|
6044
|
-
/** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
|
|
6045
|
-
tag: string().optional()
|
|
6046
|
-
});
|
|
6047
|
-
const SubscribeFramesResultSchema = object({
|
|
6048
|
-
/** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
|
|
6049
|
-
subscriptionId: string(),
|
|
6050
|
-
/** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
|
|
6051
|
-
maxFps: number().nonnegative()
|
|
6052
|
-
});
|
|
6053
|
-
const DecodedAudioChunkSchema = object({
|
|
6054
|
-
data: _instanceof(Uint8Array),
|
|
6055
|
-
sampleRate: number().int().positive(),
|
|
6056
|
-
channels: number().int().positive(),
|
|
6057
|
-
timestamp: number()
|
|
6058
|
-
});
|
|
6059
|
-
const SubscribeAudioChunksInputSchema = object({
|
|
6060
|
-
brokerId: string(),
|
|
6061
|
-
/** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
|
|
6062
|
-
tag: string().optional()
|
|
6063
|
-
});
|
|
6064
|
-
const SubscribeAudioChunksResultSchema = object({
|
|
6065
|
-
/** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
|
|
6066
|
-
subscriptionId: string()
|
|
6067
|
-
});
|
|
6068
|
-
const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
|
|
6069
|
-
const BrokerStatsSchema = object({
|
|
6070
|
-
status: BrokerStatusSchema$1,
|
|
6071
|
-
inputFps: number(),
|
|
6072
|
-
decodeFps: number(),
|
|
6073
|
-
encodedSubscribers: number(),
|
|
6074
|
-
decodedSubscribers: number(),
|
|
6075
|
-
uptimeMs: number(),
|
|
6076
|
-
bitrateKbps: number(),
|
|
6077
|
-
idrIntervalMs: number(),
|
|
6078
|
-
codec: string().optional(),
|
|
6079
|
-
totalBytes: number(),
|
|
6080
|
-
packetCount: number(),
|
|
6081
|
-
rtspClients: number(),
|
|
6082
|
-
pipeClients: number(),
|
|
6083
|
-
preBufferSec: number(),
|
|
6084
|
-
preBufferMs: number(),
|
|
6085
|
-
preBufferPackets: number(),
|
|
6086
|
-
/**
|
|
6087
|
-
* Moleculer node id of the decoder provider currently servicing this
|
|
6088
|
-
* stream's decoded subscribers. `null` until the deferred decoder is
|
|
6089
|
-
* created (no decoded clients yet, or codec not detected). Surfaces the
|
|
6090
|
-
* runtime decoder placement so the UI can show "Decoder: <agent>" without
|
|
6091
|
-
* a separate cap call per broker.
|
|
6092
|
-
*/
|
|
6093
|
-
decoderNodeId: string().nullable(),
|
|
6094
|
-
/**
|
|
6095
|
-
* Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
|
|
6096
|
-
* when the stream has no audio track or the broker is in cold start.
|
|
6097
|
-
* `supported = false` means the codec was detected but the local
|
|
6098
|
-
* decoder pipeline cannot produce PCM chunks (e.g. AAC without the
|
|
6099
|
-
* AAC pipeline wired). Surfaced in the UI device-overview so operators
|
|
6100
|
-
* can pre-pick the audio-analysis model that matches the codec.
|
|
6101
|
-
*/
|
|
6102
|
-
audio: object({
|
|
6103
|
-
codec: string(),
|
|
6104
|
-
sampleRate: number(),
|
|
6105
|
-
channels: number(),
|
|
6106
|
-
supported: boolean()
|
|
6107
|
-
}).nullable().optional()
|
|
6108
|
-
});
|
|
6109
|
-
const StreamFormatSchema = _enum(["webrtc", "hls", "mjpeg", "rtsp"]);
|
|
6110
|
-
const StreamInfoSchema = object({
|
|
6111
|
-
streamId: string(),
|
|
6112
|
-
format: StreamFormatSchema,
|
|
6113
|
-
url: string().nullable(),
|
|
6114
|
-
active: boolean()
|
|
6115
|
-
});
|
|
6116
|
-
({
|
|
6117
|
-
methods: {
|
|
6118
|
-
registerStream: method(
|
|
6119
|
-
object({ streamId: string(), sourceUrl: string(), codec: string().optional() }),
|
|
6120
|
-
_void(),
|
|
6121
|
-
{ kind: "mutation" }
|
|
6122
|
-
),
|
|
6123
|
-
unregisterStream: method(object({ streamId: string() }), _void(), { kind: "mutation" }),
|
|
6124
|
-
getStreamUrl: method(
|
|
6125
|
-
object({ streamId: string(), format: StreamFormatSchema }),
|
|
6126
|
-
string().nullable()
|
|
6127
|
-
),
|
|
6128
|
-
listStreams: method(_void(), array(StreamInfoSchema))
|
|
6129
|
-
}
|
|
6130
|
-
});
|
|
6131
|
-
const RtspRestreamEntrySchema = object({
|
|
6132
|
-
brokerId: string(),
|
|
6133
|
-
url: string(),
|
|
6134
|
-
mutedUrl: string(),
|
|
6135
|
-
enabled: boolean()
|
|
6136
|
-
});
|
|
6137
|
-
const BrokerRtspClientSchema = object({
|
|
6138
|
-
sessionId: string(),
|
|
6139
|
-
remoteAddr: string(),
|
|
6140
|
-
playing: boolean(),
|
|
6141
|
-
muted: boolean(),
|
|
6142
|
-
connectedAt: number(),
|
|
6143
|
-
lastRtpAt: number(),
|
|
6144
|
-
bytesSent: number()
|
|
6145
|
-
});
|
|
6146
|
-
const BrokerDecodedClientSchema = object({
|
|
6147
|
-
tag: string(),
|
|
6148
|
-
subscribedAt: number(),
|
|
6149
|
-
maxFps: number(),
|
|
6150
|
-
framesDelivered: number(),
|
|
6151
|
-
framesDropped: number()
|
|
6152
|
-
});
|
|
6153
|
-
const BrokerAudioClientSchema = object({
|
|
6154
|
-
tag: string(),
|
|
6155
|
-
subscribedAt: number(),
|
|
6156
|
-
chunksDelivered: number()
|
|
6157
|
-
});
|
|
6158
|
-
const BrokerClientsSchema = object({
|
|
6159
|
-
rtsp: array(BrokerRtspClientSchema).readonly(),
|
|
6160
|
-
decoded: array(BrokerDecodedClientSchema).readonly(),
|
|
6161
|
-
audio: array(BrokerAudioClientSchema).readonly(),
|
|
6162
|
-
pipeClients: number(),
|
|
6163
|
-
encodedSubscribers: number()
|
|
6164
|
-
});
|
|
6165
|
-
_enum([
|
|
6166
|
-
"reconnecting",
|
|
6167
|
-
"sleeping",
|
|
6168
|
-
"offline",
|
|
6169
|
-
"disabled",
|
|
6170
|
-
"waking"
|
|
6171
|
-
]);
|
|
6172
|
-
const VideoCodecTargetSchema = _enum(["H264", "H265", "auto"]);
|
|
6173
|
-
const AudioCodecTargetSchema = _enum(["AAC", "Opus", "PCMU", "none"]);
|
|
6174
|
-
const MaxResolutionSchema = object({
|
|
6175
|
-
width: number().int().positive(),
|
|
6176
|
-
height: number().int().positive()
|
|
6177
|
-
});
|
|
6178
|
-
const GetStreamWithCodecInputSchema = object({
|
|
6179
|
-
deviceId: number().int().nonnegative(),
|
|
6180
|
-
videoCodec: VideoCodecTargetSchema,
|
|
6181
|
-
audioCodec: AudioCodecTargetSchema.optional(),
|
|
6182
|
-
maxResolution: MaxResolutionSchema.optional(),
|
|
6183
|
-
tag: string().optional()
|
|
6184
|
-
});
|
|
6185
|
-
const RtpSourceSchema = object({
|
|
6186
|
-
url: string(),
|
|
6187
|
-
videoCodec: _enum(["H264", "H265"]),
|
|
6188
|
-
audioCodec: string(),
|
|
6189
|
-
resolution: MaxResolutionSchema,
|
|
6190
|
-
transcoded: boolean(),
|
|
6191
|
-
encoder: string(),
|
|
6192
|
-
pipelineKey: string()
|
|
6693
|
+
videoCodec: _enum(["H264", "H265"]),
|
|
6694
|
+
audioCodec: string(),
|
|
6695
|
+
resolution: object({
|
|
6696
|
+
width: number().int().positive(),
|
|
6697
|
+
height: number().int().positive()
|
|
6698
|
+
}),
|
|
6699
|
+
transcoded: boolean(),
|
|
6700
|
+
encoder: string(),
|
|
6701
|
+
pipelineKey: string()
|
|
6193
6702
|
});
|
|
6194
6703
|
({
|
|
6195
6704
|
methods: {
|
|
@@ -6205,7 +6714,17 @@ const RtpSourceSchema = object({
|
|
|
6205
6714
|
label: string().optional(),
|
|
6206
6715
|
deviceFeatures: array(string()).optional(),
|
|
6207
6716
|
autoEligible: boolean().optional(),
|
|
6208
|
-
metadata: record(string(), unknown()).optional()
|
|
6717
|
+
metadata: record(string(), unknown()).optional(),
|
|
6718
|
+
/** Required when kind === 'derived' — the source camStreamId the
|
|
6719
|
+
* derived broker reads its encoded plane from.
|
|
6720
|
+
* The runtime guard (broker manager, Task 10) enforces presence for
|
|
6721
|
+
* kind === 'derived'; the schema stays permissive so ts-morph emits
|
|
6722
|
+
* full field types instead of collapsing to ZodEffects. */
|
|
6723
|
+
sourceCamStreamId: string().min(1).optional(),
|
|
6724
|
+
/** Required when kind === 'derived' — the ffmpeg params for the
|
|
6725
|
+
* spawn. Reuses the same brokerId for identical params (single-flight).
|
|
6726
|
+
* Runtime guard in broker manager; schema is permissive (see above). */
|
|
6727
|
+
encodeProfile: EncodeProfileSchema.optional()
|
|
6209
6728
|
}),
|
|
6210
6729
|
object({ success: literal(true) }),
|
|
6211
6730
|
{ kind: "mutation", auth: "admin" }
|
|
@@ -6268,12 +6787,21 @@ const RtpSourceSchema = object({
|
|
|
6268
6787
|
object({ success: boolean() }),
|
|
6269
6788
|
{ kind: "mutation", auth: "admin" }
|
|
6270
6789
|
),
|
|
6790
|
+
/**
|
|
6791
|
+
* LEGACY / observability. Returns a raw per-broker restream URL for a
|
|
6792
|
+
* single stream. Prefer {@link getStreamWithCodec} for programmatic
|
|
6793
|
+
* CONSUMPTION — it is the single demand-counted entry that rides the
|
|
6794
|
+
* broker's one source dial and returns a codec-correct passthrough.
|
|
6795
|
+
*/
|
|
6271
6796
|
getStreamUrl: method(
|
|
6272
6797
|
object({ streamId: string(), format: StreamFormatSchema }),
|
|
6273
6798
|
object({ url: string() })
|
|
6274
6799
|
),
|
|
6275
6800
|
/**
|
|
6276
|
-
* Shared codec-targeted stream API — see Task #184.
|
|
6801
|
+
* Shared codec-targeted stream API — see Task #184. THE single public
|
|
6802
|
+
* stream-acquisition surface for programmatic consumers: single-dial
|
|
6803
|
+
* (rides the broker's one source pull) and passthrough-correct
|
|
6804
|
+
* (`videoCodec:'auto'` → `-c copy`, never a needless re-encode).
|
|
6277
6805
|
* Resolution order: source-select → HW transcode → libx264/libx265.
|
|
6278
6806
|
*/
|
|
6279
6807
|
getStreamWithCodec: method(
|
|
@@ -6367,10 +6895,20 @@ const RtpSourceSchema = object({
|
|
|
6367
6895
|
object({ configuredSec: number(), bufferedMs: number(), packetCount: number() })
|
|
6368
6896
|
),
|
|
6369
6897
|
getRtspPort: method(_void(), number()),
|
|
6898
|
+
/**
|
|
6899
|
+
* Enumeration surface — backs the admin RTSP-export picker and
|
|
6900
|
+
* `pickPreferredRtspEntry`. Lists every per-broker restream entry. NOT a
|
|
6901
|
+
* consumption API: programmatic readers use {@link getStreamWithCodec}.
|
|
6902
|
+
*/
|
|
6370
6903
|
getAllRtspEntries: method(
|
|
6371
6904
|
object({ hostname: string().optional() }),
|
|
6372
6905
|
array(RtspRestreamEntrySchema).readonly()
|
|
6373
6906
|
),
|
|
6907
|
+
/**
|
|
6908
|
+
* LEGACY / observability. Resolves one broker's restream entry. Prefer
|
|
6909
|
+
* {@link getStreamWithCodec} for programmatic CONSUMPTION so the read is
|
|
6910
|
+
* demand-counted and rides the single source dial.
|
|
6911
|
+
*/
|
|
6374
6912
|
getRtspEntry: method(
|
|
6375
6913
|
object({ brokerId: string(), hostname: string().optional() }),
|
|
6376
6914
|
RtspRestreamEntrySchema.nullable()
|
|
@@ -6407,203 +6945,1099 @@ const RtpSourceSchema = object({
|
|
|
6407
6945
|
}))
|
|
6408
6946
|
}
|
|
6409
6947
|
});
|
|
6948
|
+
const STREAM_CODEC_VALUES = ["h264", "h265", "hevc", "av1", "mjpeg", "vp8", "vp9"];
|
|
6949
|
+
const StreamCodecSchema = _enum(STREAM_CODEC_VALUES);
|
|
6950
|
+
const PickStreamRequirementsSchema = object({
|
|
6951
|
+
/**
|
|
6952
|
+
* Codecs the consumer can decode without an intermediate transcode.
|
|
6953
|
+
* Match is case-insensitive against the camera stream's `codec`.
|
|
6954
|
+
* Default (omitted / empty array): no codec filter — useful when the
|
|
6955
|
+
* consumer just wants "the highest-quality stream of any codec".
|
|
6956
|
+
*/
|
|
6957
|
+
acceptCodecs: array(StreamCodecSchema).readonly().optional(),
|
|
6958
|
+
/** Minimum vertical resolution. Streams shorter than this are dropped. */
|
|
6959
|
+
minHeight: number().int().positive().optional(),
|
|
6960
|
+
/** Minimum horizontal resolution. */
|
|
6961
|
+
minWidth: number().int().positive().optional(),
|
|
6962
|
+
/**
|
|
6963
|
+
* When true (default), `derived:*` streams (transcoded broker pipes)
|
|
6964
|
+
* are excluded — the caller wants a SOURCE stream the broker can
|
|
6965
|
+
* forward without a re-encode. Set false to allow derived in the
|
|
6966
|
+
* picker (useful for a "best playable" fallback).
|
|
6967
|
+
*/
|
|
6968
|
+
excludeDerived: boolean().optional(),
|
|
6969
|
+
/**
|
|
6970
|
+
* Bypass gate. When set, the picker returns a hit ONLY IF the device
|
|
6971
|
+
* also exposes a non-derived stream in one of these (rejected) codecs.
|
|
6972
|
+
* Mirrors the Alexa "bypass only when the natural path WOULD have
|
|
6973
|
+
* transcoded" guard: if the device is already serving the consumer's
|
|
6974
|
+
* codec end-to-end, there's nothing to optimise.
|
|
6975
|
+
*/
|
|
6976
|
+
requireSiblingCodec: array(StreamCodecSchema).readonly().optional()
|
|
6977
|
+
}).readonly();
|
|
6978
|
+
const PickStreamPreferencesSchema = object({
|
|
6979
|
+
/**
|
|
6980
|
+
* Ordered list of provider prefixes (e.g. `['native:', 'rtsp:']`) the
|
|
6981
|
+
* picker prefers — earlier entries win ties. Defaults to
|
|
6982
|
+
* `['native:']`, mirroring Alexa's "prefer the native dial path".
|
|
6983
|
+
*/
|
|
6984
|
+
preferredProviders: array(string()).readonly().optional(),
|
|
6985
|
+
/**
|
|
6986
|
+
* Resolution preference within the surviving candidates. `'highest'`
|
|
6987
|
+
* picks the tallest stream; `'lowest'` picks the shortest (used by
|
|
6988
|
+
* memory-constrained consumers / Apple Home guest sessions).
|
|
6989
|
+
*/
|
|
6990
|
+
resolutionPreference: _enum(["highest", "lowest"]).optional()
|
|
6991
|
+
}).readonly();
|
|
6992
|
+
const PickedCamStreamSchema = object({
|
|
6993
|
+
camStreamId: string(),
|
|
6994
|
+
codec: string().optional(),
|
|
6995
|
+
resolution: CamStreamResolutionSchema.optional(),
|
|
6996
|
+
/** One-line explanation of why this stream won — for logs / debug UI. */
|
|
6997
|
+
reason: string()
|
|
6998
|
+
});
|
|
6999
|
+
({
|
|
7000
|
+
deviceTypes: [DeviceType.Camera],
|
|
7001
|
+
methods: {
|
|
7002
|
+
getCameraStreams: method(
|
|
7003
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7004
|
+
array(CameraStreamSchema).readonly()
|
|
7005
|
+
),
|
|
7006
|
+
getBrokerStreams: method(
|
|
7007
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7008
|
+
array(ProfileSlotSchema).readonly()
|
|
7009
|
+
),
|
|
7010
|
+
/**
|
|
7011
|
+
* Per-device RAW RTSP restream entries — one per published camStream
|
|
7012
|
+
* that has RTSP restream enabled (`native:main`, `rtsp:sub`, …).
|
|
7013
|
+
*
|
|
7014
|
+
* LIVE-VIEW ONLY. This is the surface the device-details stream
|
|
7015
|
+
* picker uses so an operator can hit each physical stream directly.
|
|
7016
|
+
* Programmatic / external consumers (HAP, Alexa, ha-mqtt, recording)
|
|
7017
|
+
* MUST use `getProfileRtspEntries` instead — picking from raw
|
|
7018
|
+
* variants makes two consumers of the same camera land on two
|
|
7019
|
+
* different physical pulls (e.g. Reolink `native:main` vs
|
|
7020
|
+
* `rtsp:main`) and trips the camera's concurrent-session limit.
|
|
7021
|
+
*/
|
|
7022
|
+
getRtspEntries: method(
|
|
7023
|
+
object({
|
|
7024
|
+
deviceId: number().int().nonnegative(),
|
|
7025
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
7026
|
+
hostname: string().optional()
|
|
7027
|
+
}),
|
|
7028
|
+
array(RtspRestreamEntrySchema).readonly()
|
|
7029
|
+
),
|
|
7030
|
+
/**
|
|
7031
|
+
* Per-device PROFILE RTSP restream entries — one per ASSIGNED
|
|
7032
|
+
* profile slot (high/mid/low). Each entry's `url` is a profile-keyed
|
|
7033
|
+
* broker restream that aliases the profile's assigned source broker,
|
|
7034
|
+
* so HAP / Alexa / recording / WebRTC all converge on the broker's
|
|
7035
|
+
* single on-demand pull for that profile. This is the supported
|
|
7036
|
+
* exporter-facing surface; the raw `getRtspEntries` is live-view
|
|
7037
|
+
* only. Returns `[]` for a device with no assigned profiles
|
|
7038
|
+
* (cold-start before first publish).
|
|
7039
|
+
*/
|
|
7040
|
+
getProfileRtspEntries: method(
|
|
7041
|
+
object({
|
|
7042
|
+
deviceId: number().int().nonnegative(),
|
|
7043
|
+
/** Override hostname embedded in returned URLs. Defaults to the broker's bound address. */
|
|
7044
|
+
hostname: string().optional()
|
|
7045
|
+
}),
|
|
7046
|
+
array(ProfileRtspEntrySchema).readonly()
|
|
7047
|
+
),
|
|
7048
|
+
/**
|
|
7049
|
+
* "Best source stream for these decode constraints". Returns the
|
|
7050
|
+
* camStreamId the caller should dial (or null when no stream
|
|
7051
|
+
* matches). See `PickStreamRequirementsSchema` for the filter shape
|
|
7052
|
+
* and `PickStreamPreferencesSchema` for the ranking inputs.
|
|
7053
|
+
*
|
|
7054
|
+
* Returning null instructs the caller to fall back to its existing
|
|
7055
|
+
* path (derived-broker transcode, profile-slot pick, etc.) — the
|
|
7056
|
+
* picker NEVER ranks `derived:*` candidates as a "match" because
|
|
7057
|
+
* its whole job is to avoid the transcode.
|
|
7058
|
+
*/
|
|
7059
|
+
pickStream: method(
|
|
7060
|
+
object({
|
|
7061
|
+
deviceId: number().int().nonnegative(),
|
|
7062
|
+
requirements: PickStreamRequirementsSchema,
|
|
7063
|
+
preferences: PickStreamPreferencesSchema.optional()
|
|
7064
|
+
}),
|
|
7065
|
+
PickedCamStreamSchema.nullable()
|
|
7066
|
+
)
|
|
7067
|
+
},
|
|
7068
|
+
events: {
|
|
7069
|
+
/** Fires on publishCameraStream / retractCameraStream. */
|
|
7070
|
+
onCamStreamsChanged: event(object({
|
|
7071
|
+
deviceId: number().int().nonnegative(),
|
|
7072
|
+
camStreams: array(CameraStreamSchema).readonly()
|
|
7073
|
+
})),
|
|
7074
|
+
/** Fires on assignProfile / unassignProfile / runtime status change. */
|
|
7075
|
+
onProfileSlotsChanged: event(object({
|
|
7076
|
+
deviceId: number().int().nonnegative(),
|
|
7077
|
+
profileSlots: array(ProfileSlotSchema).readonly()
|
|
7078
|
+
}))
|
|
7079
|
+
},
|
|
7080
|
+
/**
|
|
7081
|
+
* Per-device live stream-broker state. Persistent settings (RTSP
|
|
7082
|
+
* tokens, profile assignments, pre-buffer config, RTSP-enabled toggles,
|
|
7083
|
+
* streamingDebug) stay in the broker's addon store — they survive
|
|
7084
|
+
* restarts. The slice below carries ONLY what's truly runtime:
|
|
7085
|
+
*
|
|
7086
|
+
* - `online` — at least one profile slot is currently `'streaming'`.
|
|
7087
|
+
* Drivers without a firmware liveness signal (RTSP, ONVIF…) can
|
|
7088
|
+
* subscribe and mirror this into `state.deviceStatus.online`.
|
|
7089
|
+
* - `slotStatuses` — current `ProfileSlotStatus` per profile,
|
|
7090
|
+
* mirroring the runtime-mutable subset of `ProfileSlot`.
|
|
7091
|
+
* - `slotErrors` — last error message per profile (only set when the
|
|
7092
|
+
* corresponding slot is in `'error'`).
|
|
7093
|
+
* - `lastChangedAt` — freshness signal for consumers that want to
|
|
7094
|
+
* reason about how stale the slice is.
|
|
7095
|
+
*
|
|
7096
|
+
* Written by the stream-broker manager on every transition that
|
|
7097
|
+
* affects these aggregates. Read via `device.state.cameraStreams.<field>`
|
|
7098
|
+
* (BaseDevice proxy) or, cross-process, via
|
|
7099
|
+
* `device-state.getCapSlice({deviceId, capName: 'camera-streams'})`.
|
|
7100
|
+
* The cap's `onChanged` event fires automatically on each write so
|
|
7101
|
+
* subscribers get push semantics for free.
|
|
7102
|
+
*/
|
|
7103
|
+
runtimeState: object({
|
|
7104
|
+
online: boolean(),
|
|
7105
|
+
slotStatuses: object({
|
|
7106
|
+
high: ProfileSlotStatusSchema.optional(),
|
|
7107
|
+
mid: ProfileSlotStatusSchema.optional(),
|
|
7108
|
+
low: ProfileSlotStatusSchema.optional()
|
|
7109
|
+
}),
|
|
7110
|
+
slotErrors: object({
|
|
7111
|
+
high: string().optional(),
|
|
7112
|
+
mid: string().optional(),
|
|
7113
|
+
low: string().optional()
|
|
7114
|
+
}),
|
|
7115
|
+
lastChangedAt: number()
|
|
7116
|
+
})
|
|
7117
|
+
});
|
|
7118
|
+
object({
|
|
7119
|
+
detected: boolean(),
|
|
7120
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
7121
|
+
lastChangedAt: number()
|
|
7122
|
+
});
|
|
7123
|
+
({
|
|
7124
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7125
|
+
});
|
|
7126
|
+
const HvacModeSchema = _enum([
|
|
7127
|
+
"off",
|
|
7128
|
+
"heat",
|
|
7129
|
+
"cool",
|
|
7130
|
+
"auto",
|
|
7131
|
+
"heat_cool",
|
|
7132
|
+
"fan_only",
|
|
7133
|
+
"dry"
|
|
7134
|
+
]);
|
|
7135
|
+
object({
|
|
7136
|
+
/** Active HVAC mode. */
|
|
7137
|
+
mode: HvacModeSchema,
|
|
7138
|
+
/** Available HVAC modes the device accepts. Subset of HvacMode. */
|
|
7139
|
+
availableModes: array(HvacModeSchema),
|
|
7140
|
+
/** Active fan mode (`auto` / `low` / `medium` / `high` / `on` / `off`
|
|
7141
|
+
* / vendor-specific) — empty string when the device has no fan
|
|
7142
|
+
* surface. */
|
|
7143
|
+
fanMode: string(),
|
|
7144
|
+
/** Available fan modes the device accepts. */
|
|
7145
|
+
availableFanModes: array(string()),
|
|
7146
|
+
/** Active preset (`eco` / `away` / `sleep` / vendor-specific) —
|
|
7147
|
+
* empty string when no preset is active or the device has no
|
|
7148
|
+
* preset surface. */
|
|
7149
|
+
preset: string(),
|
|
7150
|
+
/** Available presets the device accepts. */
|
|
7151
|
+
availablePresets: array(string()),
|
|
7152
|
+
/** Single setpoint in Celsius. Used by single-target modes
|
|
7153
|
+
* (heat / cool / dry). Null when the device is in a range mode
|
|
7154
|
+
* (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
|
|
7155
|
+
target: number().nullable(),
|
|
7156
|
+
/** Upper bound of the dual setpoint in Celsius. Populated only
|
|
7157
|
+
* in `heat_cool` mode. */
|
|
7158
|
+
targetHigh: number().nullable(),
|
|
7159
|
+
/** Lower bound of the dual setpoint in Celsius. Populated only
|
|
7160
|
+
* in `heat_cool` mode. */
|
|
7161
|
+
targetLow: number().nullable(),
|
|
7162
|
+
/** Target relative humidity (0..100). Null when the device has no
|
|
7163
|
+
* humidity control. */
|
|
7164
|
+
targetHumidity: number().min(0).max(100).nullable(),
|
|
7165
|
+
/** Current measured temperature in Celsius. Read-only — pushed by
|
|
7166
|
+
* the device's internal sensor. */
|
|
7167
|
+
currentTemp: number().nullable(),
|
|
7168
|
+
/** Current measured relative humidity (0..100). Read-only. */
|
|
7169
|
+
currentHumidity: number().min(0).max(100).nullable(),
|
|
7170
|
+
/** Ms epoch when the slice was last updated (push or command). */
|
|
7171
|
+
lastFetchedAt: number()
|
|
7172
|
+
});
|
|
7173
|
+
({
|
|
7174
|
+
deviceTypes: [DeviceType.Thermostat],
|
|
7175
|
+
methods: {
|
|
7176
|
+
setMode: method(
|
|
7177
|
+
object({
|
|
7178
|
+
deviceId: number().int().nonnegative(),
|
|
7179
|
+
mode: HvacModeSchema
|
|
7180
|
+
}),
|
|
7181
|
+
_void(),
|
|
7182
|
+
{ kind: "mutation", auth: "admin" }
|
|
7183
|
+
),
|
|
7184
|
+
setFanMode: method(
|
|
7185
|
+
object({
|
|
7186
|
+
deviceId: number().int().nonnegative(),
|
|
7187
|
+
fanMode: string().min(1)
|
|
7188
|
+
}),
|
|
7189
|
+
_void(),
|
|
7190
|
+
{ kind: "mutation", auth: "admin" }
|
|
7191
|
+
),
|
|
7192
|
+
setPreset: method(
|
|
7193
|
+
object({
|
|
7194
|
+
deviceId: number().int().nonnegative(),
|
|
7195
|
+
preset: string().min(1)
|
|
7196
|
+
}),
|
|
7197
|
+
_void(),
|
|
7198
|
+
{ kind: "mutation", auth: "admin" }
|
|
7199
|
+
),
|
|
7200
|
+
setTarget: method(
|
|
7201
|
+
object({
|
|
7202
|
+
deviceId: number().int().nonnegative(),
|
|
7203
|
+
target: number()
|
|
7204
|
+
}),
|
|
7205
|
+
_void(),
|
|
7206
|
+
{ kind: "mutation", auth: "admin" }
|
|
7207
|
+
),
|
|
7208
|
+
setTargetRange: method(
|
|
7209
|
+
object({
|
|
7210
|
+
deviceId: number().int().nonnegative(),
|
|
7211
|
+
targetLow: number(),
|
|
7212
|
+
targetHigh: number()
|
|
7213
|
+
}),
|
|
7214
|
+
_void(),
|
|
7215
|
+
{ kind: "mutation", auth: "admin" }
|
|
7216
|
+
),
|
|
7217
|
+
setTargetHumidity: method(
|
|
7218
|
+
object({
|
|
7219
|
+
deviceId: number().int().nonnegative(),
|
|
7220
|
+
targetHumidity: number().min(0).max(100)
|
|
7221
|
+
}),
|
|
7222
|
+
_void(),
|
|
7223
|
+
{ kind: "mutation", auth: "admin" }
|
|
7224
|
+
)
|
|
7225
|
+
}
|
|
7226
|
+
});
|
|
7227
|
+
const RgbTripletSchema = object({
|
|
7228
|
+
r: number().int().min(0).max(255),
|
|
7229
|
+
g: number().int().min(0).max(255),
|
|
7230
|
+
b: number().int().min(0).max(255)
|
|
7231
|
+
});
|
|
7232
|
+
const HsvTripletSchema = object({
|
|
7233
|
+
/** Hue in degrees, 0..360. */
|
|
7234
|
+
h: number().min(0).max(360),
|
|
7235
|
+
/** Saturation as 0..100 inclusive. */
|
|
7236
|
+
s: number().min(0).max(100),
|
|
7237
|
+
/** Value/brightness as 0..100 inclusive. */
|
|
7238
|
+
v: number().min(0).max(100)
|
|
7239
|
+
});
|
|
7240
|
+
const ColorInputSchema = discriminatedUnion("mode", [
|
|
7241
|
+
object({ mode: literal("rgb"), rgb: RgbTripletSchema }),
|
|
7242
|
+
object({ mode: literal("hsv"), hsv: HsvTripletSchema }),
|
|
7243
|
+
object({ mode: literal("mired"), mireds: number().int().min(50).max(1e3) })
|
|
7244
|
+
]);
|
|
7245
|
+
object({
|
|
7246
|
+
/** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
|
|
7247
|
+
* the bulb's current state. */
|
|
7248
|
+
mode: _enum(["rgb", "hsv", "mired"]),
|
|
7249
|
+
/** Populated when `mode === 'rgb'`. */
|
|
7250
|
+
rgb: RgbTripletSchema.optional(),
|
|
7251
|
+
/** Populated when `mode === 'hsv'`. */
|
|
7252
|
+
hsv: HsvTripletSchema.optional(),
|
|
7253
|
+
/** Populated when `mode === 'mired'`. */
|
|
7254
|
+
mireds: number().int().optional(),
|
|
7255
|
+
/** Ms epoch of the last operator-driven change. */
|
|
7256
|
+
lastChangedAt: number()
|
|
7257
|
+
});
|
|
7258
|
+
({
|
|
7259
|
+
deviceTypes: [DeviceType.Light],
|
|
7260
|
+
methods: {
|
|
7261
|
+
setColor: method(
|
|
7262
|
+
object({
|
|
7263
|
+
deviceId: number().int().nonnegative(),
|
|
7264
|
+
color: ColorInputSchema
|
|
7265
|
+
}),
|
|
7266
|
+
_void(),
|
|
7267
|
+
{ kind: "mutation", auth: "admin" }
|
|
7268
|
+
)
|
|
7269
|
+
},
|
|
7270
|
+
events: {
|
|
7271
|
+
/**
|
|
7272
|
+
* Emitted whenever the color changes — operator action OR firmware
|
|
7273
|
+
* push. Subscribers (UI color pickers, automation engines) react
|
|
7274
|
+
* without polling.
|
|
7275
|
+
*/
|
|
7276
|
+
onColorChanged: { data: object({
|
|
7277
|
+
deviceId: number(),
|
|
7278
|
+
mode: _enum(["rgb", "hsv", "mired"]),
|
|
7279
|
+
rgb: RgbTripletSchema.optional(),
|
|
7280
|
+
hsv: HsvTripletSchema.optional(),
|
|
7281
|
+
mireds: number().int().optional(),
|
|
7282
|
+
lastChangedAt: number()
|
|
7283
|
+
}) }
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
7286
|
+
object({
|
|
7287
|
+
/** True when the upstream system considers the entity connected. */
|
|
7288
|
+
connected: boolean(),
|
|
7289
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
7290
|
+
lastChangedAt: number()
|
|
7291
|
+
});
|
|
7292
|
+
({
|
|
7293
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7294
|
+
});
|
|
7295
|
+
const ConsumableItemSchema = object({
|
|
7296
|
+
/** Stable id, e.g. 'main-brush'. */
|
|
7297
|
+
key: string().min(1),
|
|
7298
|
+
/** Display name. */
|
|
7299
|
+
label: string().min(1),
|
|
7300
|
+
/** Remaining life % when known (0..100). */
|
|
7301
|
+
level: number().min(0).max(100).nullable(),
|
|
7302
|
+
/** Discrete state when known (binary mode). */
|
|
7303
|
+
status: _enum(["ok", "replace"]).nullable(),
|
|
7304
|
+
/** Ms epoch of the last replace, when known. */
|
|
7305
|
+
lastResetAt: number().nullable(),
|
|
7306
|
+
/** Whether `reset()` is meaningful for this item. */
|
|
7307
|
+
resettable: boolean()
|
|
7308
|
+
});
|
|
7309
|
+
const ConsumablesStatusSchema = object({
|
|
7310
|
+
items: array(ConsumableItemSchema),
|
|
7311
|
+
lastChangedAt: number()
|
|
7312
|
+
});
|
|
7313
|
+
({
|
|
7314
|
+
// Device-agnostic — any device may expose consumables, so the cap
|
|
7315
|
+
// binds on EVERY DeviceType. The list enumerates every enum member
|
|
7316
|
+
// (kept in sync with `device-type.ts`) rather than a subset, so a
|
|
7317
|
+
// provider on any device type — vacuums/mowers included — can register
|
|
7318
|
+
// it and no future device type silently fails to bind.
|
|
7319
|
+
deviceTypes: [
|
|
7320
|
+
DeviceType.Camera,
|
|
7321
|
+
DeviceType.Hub,
|
|
7322
|
+
DeviceType.Light,
|
|
7323
|
+
DeviceType.Siren,
|
|
7324
|
+
DeviceType.Switch,
|
|
7325
|
+
DeviceType.Sensor,
|
|
7326
|
+
DeviceType.Thermostat,
|
|
7327
|
+
DeviceType.Button,
|
|
7328
|
+
DeviceType.EventEmitter,
|
|
7329
|
+
DeviceType.Update,
|
|
7330
|
+
DeviceType.Generic,
|
|
7331
|
+
DeviceType.Notifier,
|
|
7332
|
+
DeviceType.Script,
|
|
7333
|
+
DeviceType.Automation,
|
|
7334
|
+
DeviceType.Lock,
|
|
7335
|
+
DeviceType.Cover,
|
|
7336
|
+
DeviceType.Valve,
|
|
7337
|
+
DeviceType.Humidifier,
|
|
7338
|
+
DeviceType.WaterHeater,
|
|
7339
|
+
DeviceType.Fan,
|
|
7340
|
+
DeviceType.MediaPlayer,
|
|
7341
|
+
DeviceType.AlarmPanel,
|
|
7342
|
+
DeviceType.Control,
|
|
7343
|
+
DeviceType.Presence,
|
|
7344
|
+
DeviceType.Weather,
|
|
7345
|
+
DeviceType.Vacuum,
|
|
7346
|
+
DeviceType.LawnMower,
|
|
7347
|
+
DeviceType.Container,
|
|
7348
|
+
DeviceType.Image
|
|
7349
|
+
],
|
|
7350
|
+
methods: {
|
|
7351
|
+
/** Mark a consumable as replaced — resets its remaining life. Only
|
|
7352
|
+
* meaningful when the item's `resettable` is true. */
|
|
7353
|
+
reset: method(
|
|
7354
|
+
object({ deviceId: number().int().nonnegative(), key: string().min(1) }),
|
|
7355
|
+
_void(),
|
|
7356
|
+
{ kind: "mutation", auth: "admin" }
|
|
7357
|
+
)
|
|
7358
|
+
},
|
|
7359
|
+
// Per the cap checklist, runtimeState carries the bridge helper's
|
|
7360
|
+
// freshness field. The status surface stays the plain schema; only the
|
|
7361
|
+
// persistent slice gains `lastFetchedAt` so future poll-backed
|
|
7362
|
+
// providers can use the runtime-state bridge unchanged.
|
|
7363
|
+
runtimeState: ConsumablesStatusSchema.extend({ lastFetchedAt: number() })
|
|
7364
|
+
});
|
|
7365
|
+
object({
|
|
7366
|
+
/** True when the entry is open; false when closed. */
|
|
7367
|
+
entryOpen: boolean(),
|
|
7368
|
+
/** Ms epoch of the last open↔closed transition. 0 if never observed. */
|
|
7369
|
+
lastChangedAt: number()
|
|
7370
|
+
});
|
|
7371
|
+
({
|
|
7372
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7373
|
+
});
|
|
7374
|
+
const ControlKindSchema = _enum(["numeric", "select", "text", "datetime"]);
|
|
7375
|
+
object({
|
|
7376
|
+
kind: ControlKindSchema,
|
|
7377
|
+
/** Current value. Numeric kind → number; select/text/datetime → string.
|
|
7378
|
+
* Datetime values are ISO 8601 strings (full timestamp, date-only,
|
|
7379
|
+
* or time-only — HA accepts all three forms). */
|
|
7380
|
+
value: union([number(), string()]),
|
|
7381
|
+
/** Acceptable values when `kind === 'select'`. Empty for other kinds. */
|
|
7382
|
+
options: array(string()),
|
|
7383
|
+
/** Ms epoch when the slice was last updated. */
|
|
7384
|
+
lastChangedAt: number(),
|
|
7385
|
+
/** Display unit-of-measurement (e.g. '°C', '%', 'lx').
|
|
7386
|
+
* Populated live from HA `attributes.unit_of_measurement` on each push.
|
|
7387
|
+
* Meaningful for `kind === 'numeric'`; absent for other kinds. */
|
|
7388
|
+
unit: string().optional(),
|
|
7389
|
+
/** Minimum allowed value. Populated live from HA `attributes.min`.
|
|
7390
|
+
* Meaningful for `kind === 'numeric'`; absent for other kinds. */
|
|
7391
|
+
min: number().optional(),
|
|
7392
|
+
/** Maximum allowed value. Populated live from HA `attributes.max`.
|
|
7393
|
+
* Meaningful for `kind === 'numeric'`; absent for other kinds. */
|
|
7394
|
+
max: number().optional(),
|
|
7395
|
+
/** Step increment. Populated live from HA `attributes.step`.
|
|
7396
|
+
* Meaningful for `kind === 'numeric'`; absent for other kinds. */
|
|
7397
|
+
step: number().optional(),
|
|
7398
|
+
/** Suggested decimal places for numeric display. Populated live from HA
|
|
7399
|
+
* `attributes.suggested_display_precision`, or derived from `step` when the
|
|
7400
|
+
* HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
|
|
7401
|
+
* other kinds. Falls back to auto-formatting when absent. */
|
|
7402
|
+
precision: number().int().min(0).max(10).optional(),
|
|
7403
|
+
/** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
|
|
7404
|
+
* 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
|
|
7405
|
+
* (UI defaults to 'datetime'). Derived live from the device's HA domain
|
|
7406
|
+
* (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
|
|
7407
|
+
* `has_date`/`has_time` attributes on each push. */
|
|
7408
|
+
format: _enum(["date", "time", "datetime"]).optional()
|
|
7409
|
+
});
|
|
7410
|
+
const ControlSetValueInputSchema = discriminatedUnion("kind", [
|
|
7411
|
+
object({ kind: literal("numeric"), value: number() }),
|
|
7412
|
+
object({ kind: literal("select"), value: string().min(1) }),
|
|
7413
|
+
object({ kind: literal("text"), value: string() }),
|
|
7414
|
+
object({ kind: literal("datetime"), value: string().min(1) })
|
|
7415
|
+
]);
|
|
7416
|
+
({
|
|
7417
|
+
deviceTypes: [DeviceType.Control],
|
|
7418
|
+
methods: {
|
|
7419
|
+
setValue: method(
|
|
7420
|
+
object({
|
|
7421
|
+
deviceId: number().int().nonnegative(),
|
|
7422
|
+
control: ControlSetValueInputSchema
|
|
7423
|
+
}),
|
|
7424
|
+
_void(),
|
|
7425
|
+
{ kind: "mutation", auth: "admin" }
|
|
7426
|
+
)
|
|
7427
|
+
}
|
|
7428
|
+
});
|
|
7429
|
+
const CoverStateSchema = _enum([
|
|
7430
|
+
"open",
|
|
7431
|
+
"opening",
|
|
7432
|
+
"closing",
|
|
7433
|
+
"closed",
|
|
7434
|
+
"stopped"
|
|
7435
|
+
]);
|
|
7436
|
+
object({
|
|
7437
|
+
/** Lifecycle state of the cover. */
|
|
7438
|
+
state: CoverStateSchema,
|
|
7439
|
+
/** 0 = fully closed, 100 = fully open. Null when the device has
|
|
7440
|
+
* no intermediate position surface. */
|
|
7441
|
+
position: number().min(0).max(100).nullable(),
|
|
7442
|
+
/** 0 = closed slats, 100 = open slats. Null when no tilt support. */
|
|
7443
|
+
tiltPosition: number().min(0).max(100).nullable(),
|
|
7444
|
+
/** Ms epoch when the slice was last updated. */
|
|
7445
|
+
lastChangedAt: number()
|
|
7446
|
+
});
|
|
7447
|
+
({
|
|
7448
|
+
deviceTypes: [DeviceType.Cover],
|
|
7449
|
+
methods: {
|
|
7450
|
+
open: method(
|
|
7451
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7452
|
+
_void(),
|
|
7453
|
+
{ kind: "mutation", auth: "admin" }
|
|
7454
|
+
),
|
|
7455
|
+
close: method(
|
|
7456
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7457
|
+
_void(),
|
|
7458
|
+
{ kind: "mutation", auth: "admin" }
|
|
7459
|
+
),
|
|
7460
|
+
stop: method(
|
|
7461
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7462
|
+
_void(),
|
|
7463
|
+
{ kind: "mutation", auth: "admin" }
|
|
7464
|
+
),
|
|
7465
|
+
setPosition: method(
|
|
7466
|
+
object({
|
|
7467
|
+
deviceId: number().int().nonnegative(),
|
|
7468
|
+
position: number().min(0).max(100)
|
|
7469
|
+
}),
|
|
7470
|
+
_void(),
|
|
7471
|
+
{ kind: "mutation", auth: "admin" }
|
|
7472
|
+
),
|
|
7473
|
+
setTiltPosition: method(
|
|
7474
|
+
object({
|
|
7475
|
+
deviceId: number().int().nonnegative(),
|
|
7476
|
+
tiltPosition: number().min(0).max(100)
|
|
7477
|
+
}),
|
|
7478
|
+
_void(),
|
|
7479
|
+
{ kind: "mutation", auth: "admin" }
|
|
7480
|
+
)
|
|
7481
|
+
}
|
|
7482
|
+
});
|
|
7483
|
+
const SourceInfoSchema = object({
|
|
7484
|
+
// ── Identity (required) ─────────────────────────────────────────
|
|
7485
|
+
/** Live dispatch key — mutable when the source system allows rename. */
|
|
7486
|
+
id: string(),
|
|
7487
|
+
/** Source system tag — e.g. 'homeassistant' | 'reolink' | 'frigate'. */
|
|
7488
|
+
system: string(),
|
|
7489
|
+
// ── Stability (optional) ────────────────────────────────────────
|
|
7490
|
+
/** Immutable upstream identifier when available (HA `unique_id`, … ).
|
|
7491
|
+
* Used to detect rename when `id` changes. */
|
|
7492
|
+
uniqueId: string().optional(),
|
|
7493
|
+
// ── Free-form passthrough ───────────────────────────────────────
|
|
7494
|
+
/** Provider-specific extras that don't fit the structured slots.
|
|
7495
|
+
* Whitelist what goes here per provider — do NOT dump entire HA
|
|
7496
|
+
* attribute blobs (would explode the meta JSON column). */
|
|
7497
|
+
raw: record(string(), unknown()).optional()
|
|
7498
|
+
});
|
|
7499
|
+
const DiscoveredChildStatusSchema = _enum(["online", "sleeping", "offline", "unknown"]);
|
|
7500
|
+
const DiscoveredChildDeviceSchema = lazy(
|
|
7501
|
+
() => object({
|
|
7502
|
+
/** Stable, integration-defined identifier. Must be unique per parent and persistent across reboots. */
|
|
7503
|
+
childNativeId: string(),
|
|
7504
|
+
/** Friendly name as reported by the source (Reolink camera name, ONVIF profile, ...). */
|
|
7505
|
+
name: string(),
|
|
7506
|
+
/** DeviceType the child should be created with on adopt. */
|
|
7507
|
+
type: _enum(DeviceType),
|
|
7508
|
+
status: DiscoveredChildStatusSchema,
|
|
7509
|
+
/** Free-form integration-specific metadata surfaced in the panel. */
|
|
7510
|
+
metadata: object({
|
|
7511
|
+
model: string().optional(),
|
|
7512
|
+
serialNumber: string().optional(),
|
|
7513
|
+
uid: string().optional(),
|
|
7514
|
+
/** Reolink: 0-based channel index inside the parent NVR/Hub. */
|
|
7515
|
+
rtspChannel: number().int().nonnegative().optional(),
|
|
7516
|
+
isBattery: boolean().optional(),
|
|
7517
|
+
isDoorbell: boolean().optional(),
|
|
7518
|
+
isMultifocal: boolean().optional(),
|
|
7519
|
+
externalLocation: string().optional(),
|
|
7520
|
+
/** Vendor / manufacturer name as reported by the source (HA device
|
|
7521
|
+
* `manufacturer`, …). Surfaced in the adoption modal's fuzzy search. */
|
|
7522
|
+
manufacturer: string().optional(),
|
|
7523
|
+
/** Integration / platform domain the candidate belongs to (HA
|
|
7524
|
+
* `entity_registry.platform`, e.g. `dreame_vacuum`). Surfaced in the
|
|
7525
|
+
* adoption modal's fuzzy search. */
|
|
7526
|
+
integration: string().optional()
|
|
7527
|
+
}).default({}),
|
|
7528
|
+
/**
|
|
7529
|
+
* `true` when the framework already created a child device for this
|
|
7530
|
+
* `childNativeId` under the current parent. The panel uses it to
|
|
7531
|
+
* gate the Add/Remove button and surface the existing `deviceId`.
|
|
7532
|
+
*/
|
|
7533
|
+
alreadyAdopted: boolean(),
|
|
7534
|
+
/** When `alreadyAdopted=true`, the framework-assigned child device id. */
|
|
7535
|
+
adoptedDeviceId: number().int().nonnegative().nullable(),
|
|
7536
|
+
/** Capability names the candidate advertises — integrations populate, Hubs omit. */
|
|
7537
|
+
capabilities: array(string()).readonly().optional(),
|
|
7538
|
+
/** Feature flags the candidate advertises — integrations populate, Hubs omit. */
|
|
7539
|
+
features: array(string()).readonly().optional(),
|
|
7540
|
+
/** Upstream-system identity + rendering hints for the candidate. */
|
|
7541
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
7542
|
+
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
7543
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
7544
|
+
})
|
|
7545
|
+
);
|
|
7546
|
+
const DeviceDiscoveryStatusSchema = object({
|
|
7547
|
+
discovered: array(DiscoveredChildDeviceSchema),
|
|
7548
|
+
/** Wall-clock ms of the last successful enumeration. */
|
|
7549
|
+
lastDiscoveryAt: number().int().nonnegative().nullable(),
|
|
7550
|
+
/** Last error surfaced from the source (rendered as a banner). */
|
|
7551
|
+
lastError: string().nullable()
|
|
7552
|
+
});
|
|
7553
|
+
({
|
|
7554
|
+
// Hub is the canonical parent. Other integrations (gateway-style)
|
|
7555
|
+
// can register against the cap by also targeting their root type.
|
|
7556
|
+
deviceTypes: [DeviceType.Hub],
|
|
7557
|
+
// Mirror status into the per-device runtime-state slice so the panel
|
|
7558
|
+
// hydrates from the kernel cache without a round-trip on every open.
|
|
7559
|
+
runtimeState: DeviceDiscoveryStatusSchema.extend({
|
|
7560
|
+
lastFetchedAt: number().int().nonnegative()
|
|
7561
|
+
}),
|
|
7562
|
+
methods: {
|
|
7563
|
+
/**
|
|
7564
|
+
* Snapshot of the current `discovered` list. Returns the
|
|
7565
|
+
* runtime-state cache — call `refreshDiscovery` first if a
|
|
7566
|
+
* fresh round-trip to the source is required.
|
|
7567
|
+
*/
|
|
7568
|
+
listDiscovered: method(
|
|
7569
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7570
|
+
array(DiscoveredChildDeviceSchema).readonly()
|
|
7571
|
+
),
|
|
7572
|
+
/**
|
|
7573
|
+
* Force the integration to re-enumerate and update the
|
|
7574
|
+
* runtime-state slice. Returns the freshly-enumerated list (also
|
|
7575
|
+
* available via `listDiscovered` post-call).
|
|
7576
|
+
*/
|
|
7577
|
+
refreshDiscovery: method(
|
|
7578
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7579
|
+
array(DiscoveredChildDeviceSchema).readonly(),
|
|
7580
|
+
{ kind: "mutation", auth: "admin" }
|
|
7581
|
+
),
|
|
7582
|
+
/**
|
|
7583
|
+
* Promote a discovered entry to a real child device. The framework
|
|
7584
|
+
* creates the child via `kernel.devices.create()` with
|
|
7585
|
+
* `parentDeviceId = parent.id` and seeds the child's config from
|
|
7586
|
+
* `childInitialConfig` (driver-defined; usually carries channel +
|
|
7587
|
+
* uid + parent reference). Returns the kernel-assigned numeric id.
|
|
7588
|
+
*/
|
|
7589
|
+
adoptDevice: method(
|
|
7590
|
+
object({
|
|
7591
|
+
deviceId: number().int().nonnegative(),
|
|
7592
|
+
childNativeId: string(),
|
|
7593
|
+
/** Optional override for the child's display name. */
|
|
7594
|
+
name: string().optional()
|
|
7595
|
+
}),
|
|
7596
|
+
object({
|
|
7597
|
+
deviceId: number().int().nonnegative(),
|
|
7598
|
+
stableId: string()
|
|
7599
|
+
}),
|
|
7600
|
+
{ kind: "mutation", auth: "admin" }
|
|
7601
|
+
),
|
|
7602
|
+
/**
|
|
7603
|
+
* Inverse of `adoptDevice`: removes the child device from the
|
|
7604
|
+
* kernel registry. The discovered entry remains in the
|
|
7605
|
+
* enumeration (status updates resume) so the operator can re-adopt
|
|
7606
|
+
* it later without a fresh refresh.
|
|
7607
|
+
*/
|
|
7608
|
+
releaseDevice: method(
|
|
7609
|
+
object({
|
|
7610
|
+
deviceId: number().int().nonnegative(),
|
|
7611
|
+
childDeviceId: number().int().nonnegative()
|
|
7612
|
+
}),
|
|
7613
|
+
_void(),
|
|
7614
|
+
{ kind: "mutation", auth: "admin" }
|
|
7615
|
+
)
|
|
7616
|
+
}
|
|
7617
|
+
});
|
|
7618
|
+
object({
|
|
7619
|
+
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
7620
|
+
lastPressedAt: number().nullable(),
|
|
7621
|
+
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
7622
|
+
pressCountSinceStart: number()
|
|
7623
|
+
});
|
|
7624
|
+
object({
|
|
7625
|
+
deviceId: number(),
|
|
7626
|
+
timestamp: number()
|
|
7627
|
+
});
|
|
7628
|
+
({
|
|
7629
|
+
deviceTypes: [DeviceType.Button]
|
|
7630
|
+
});
|
|
7631
|
+
const EnumSensorDateTimeFormatSchema = _enum(["date", "time", "datetime"]);
|
|
7632
|
+
object({
|
|
7633
|
+
value: string(),
|
|
7634
|
+
/**
|
|
7635
|
+
* Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
|
|
7636
|
+
* as a locale date/time/datetime; absent for plain enum sensors. HA
|
|
7637
|
+
* `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
|
|
7638
|
+
* a time-only sensor → `'time'`.
|
|
7639
|
+
*/
|
|
7640
|
+
format: EnumSensorDateTimeFormatSchema.optional(),
|
|
7641
|
+
/** Ms epoch when the slice was last updated. */
|
|
7642
|
+
lastFetchedAt: number()
|
|
7643
|
+
});
|
|
7644
|
+
({
|
|
7645
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7646
|
+
});
|
|
7647
|
+
const EventFireSchema = object({
|
|
7648
|
+
deviceId: number(),
|
|
7649
|
+
eventType: string(),
|
|
7650
|
+
data: record(string(), unknown()).nullable(),
|
|
7651
|
+
timestamp: number(),
|
|
7652
|
+
seq: number()
|
|
7653
|
+
});
|
|
7654
|
+
object({
|
|
7655
|
+
eventTypes: array(string()),
|
|
7656
|
+
lastEvent: EventFireSchema.nullable(),
|
|
7657
|
+
eventCountSinceStart: number()
|
|
7658
|
+
});
|
|
7659
|
+
({
|
|
7660
|
+
deviceTypes: [DeviceType.EventEmitter]
|
|
7661
|
+
});
|
|
7662
|
+
const FanDirectionSchema = _enum(["forward", "reverse"]);
|
|
7663
|
+
object({
|
|
7664
|
+
/** Active speed as 0..100 inclusive. Null when the device has no
|
|
7665
|
+
* speed surface (single-speed fan). */
|
|
7666
|
+
percentage: number().min(0).max(100).nullable(),
|
|
7667
|
+
/** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
|
|
7668
|
+
* a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
|
|
7669
|
+
percentageStep: number().positive().optional(),
|
|
7670
|
+
/** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
|
|
7671
|
+
* — empty string when no preset is active or supported. */
|
|
7672
|
+
preset: string(),
|
|
7673
|
+
/** Available preset modes the device accepts. */
|
|
7674
|
+
availablePresets: array(string()),
|
|
7675
|
+
/** Ceiling-fan blade direction. Null when the device has no
|
|
7676
|
+
* direction surface. */
|
|
7677
|
+
direction: FanDirectionSchema.nullable(),
|
|
7678
|
+
/** Oscillation toggle. Null when the device has no oscillation
|
|
7679
|
+
* surface. */
|
|
7680
|
+
oscillating: boolean().nullable(),
|
|
7681
|
+
/** Ms epoch when the slice was last updated. */
|
|
7682
|
+
lastChangedAt: number()
|
|
7683
|
+
});
|
|
7684
|
+
({
|
|
7685
|
+
deviceTypes: [DeviceType.Fan],
|
|
7686
|
+
methods: {
|
|
7687
|
+
setPercentage: method(
|
|
7688
|
+
object({
|
|
7689
|
+
deviceId: number().int().nonnegative(),
|
|
7690
|
+
percentage: number().min(0).max(100)
|
|
7691
|
+
}),
|
|
7692
|
+
_void(),
|
|
7693
|
+
{ kind: "mutation", auth: "admin" }
|
|
7694
|
+
),
|
|
7695
|
+
setPreset: method(
|
|
7696
|
+
object({
|
|
7697
|
+
deviceId: number().int().nonnegative(),
|
|
7698
|
+
preset: string().min(1)
|
|
7699
|
+
}),
|
|
7700
|
+
_void(),
|
|
7701
|
+
{ kind: "mutation", auth: "admin" }
|
|
7702
|
+
),
|
|
7703
|
+
setDirection: method(
|
|
7704
|
+
object({
|
|
7705
|
+
deviceId: number().int().nonnegative(),
|
|
7706
|
+
direction: FanDirectionSchema
|
|
7707
|
+
}),
|
|
7708
|
+
_void(),
|
|
7709
|
+
{ kind: "mutation", auth: "admin" }
|
|
7710
|
+
),
|
|
7711
|
+
setOscillating: method(
|
|
7712
|
+
object({
|
|
7713
|
+
deviceId: number().int().nonnegative(),
|
|
7714
|
+
oscillating: boolean()
|
|
7715
|
+
}),
|
|
7716
|
+
_void(),
|
|
7717
|
+
{ kind: "mutation", auth: "admin" }
|
|
7718
|
+
)
|
|
7719
|
+
}
|
|
7720
|
+
});
|
|
7721
|
+
object({
|
|
7722
|
+
/** True when leak is currently detected. */
|
|
7723
|
+
flooded: boolean(),
|
|
7724
|
+
/** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
|
|
7725
|
+
lastChangedAt: number()
|
|
7726
|
+
});
|
|
7727
|
+
({
|
|
7728
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7729
|
+
});
|
|
7730
|
+
object({
|
|
7731
|
+
detected: boolean(),
|
|
7732
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
7733
|
+
lastChangedAt: number()
|
|
7734
|
+
});
|
|
7735
|
+
({
|
|
7736
|
+
deviceTypes: [DeviceType.Sensor]
|
|
7737
|
+
});
|
|
7738
|
+
object({
|
|
7739
|
+
/** Whether the humidifier is currently on. */
|
|
7740
|
+
on: boolean(),
|
|
7741
|
+
/** Current measured relative humidity (0..100). Null when not reported. */
|
|
7742
|
+
currentHumidity: number().min(0).max(100).nullable(),
|
|
7743
|
+
/** Target relative humidity (0..100). Null when no setpoint surface. */
|
|
7744
|
+
targetHumidity: number().min(0).max(100).nullable(),
|
|
7745
|
+
/** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
|
|
7746
|
+
* device has no mode surface. */
|
|
7747
|
+
mode: string().nullable(),
|
|
7748
|
+
/** Available modes the device accepts. */
|
|
7749
|
+
availableModes: array(string()),
|
|
7750
|
+
/** HA `action` attribute, verbatim (`humidifying` / `drying` /
|
|
7751
|
+
* `idle` / `off`). Null when the device doesn't report it. */
|
|
7752
|
+
action: string().nullable(),
|
|
7753
|
+
/** HA `min_humidity` attribute. Null → UI uses 0. */
|
|
7754
|
+
minHumidity: number().nullable(),
|
|
7755
|
+
/** HA `max_humidity` attribute. Null → UI uses 100. */
|
|
7756
|
+
maxHumidity: number().nullable(),
|
|
7757
|
+
/** Ms epoch when the slice was last updated. */
|
|
7758
|
+
lastChangedAt: number()
|
|
7759
|
+
});
|
|
7760
|
+
({
|
|
7761
|
+
deviceTypes: [DeviceType.Humidifier],
|
|
7762
|
+
methods: {
|
|
7763
|
+
setOn: method(
|
|
7764
|
+
object({
|
|
7765
|
+
deviceId: number().int().nonnegative(),
|
|
7766
|
+
on: boolean()
|
|
7767
|
+
}),
|
|
7768
|
+
_void(),
|
|
7769
|
+
{ kind: "mutation", auth: "admin" }
|
|
7770
|
+
),
|
|
7771
|
+
setTargetHumidity: method(
|
|
7772
|
+
object({
|
|
7773
|
+
deviceId: number().int().nonnegative(),
|
|
7774
|
+
humidity: number().min(0).max(100)
|
|
7775
|
+
}),
|
|
7776
|
+
_void(),
|
|
7777
|
+
{ kind: "mutation", auth: "admin" }
|
|
7778
|
+
),
|
|
7779
|
+
setMode: method(
|
|
7780
|
+
object({
|
|
7781
|
+
deviceId: number().int().nonnegative(),
|
|
7782
|
+
mode: string().min(1)
|
|
7783
|
+
}),
|
|
7784
|
+
_void(),
|
|
7785
|
+
{ kind: "mutation", auth: "admin" }
|
|
7786
|
+
)
|
|
7787
|
+
}
|
|
7788
|
+
});
|
|
7789
|
+
object({
|
|
7790
|
+
/** Current relative humidity, 0..100. */
|
|
7791
|
+
percent: number().min(0).max(100),
|
|
7792
|
+
/** Ms epoch when the slice was last updated. */
|
|
7793
|
+
lastFetchedAt: number(),
|
|
7794
|
+
/** Live display unit from the upstream source (e.g. HA
|
|
7795
|
+
* `attributes.unit_of_measurement`). The UI prefers this over the
|
|
7796
|
+
* role's canonical unit. Absent → fall back to the canonical unit. */
|
|
7797
|
+
unit: string().optional(),
|
|
7798
|
+
/** Suggested decimal places for numeric display.
|
|
7799
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
7800
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
7801
|
+
* auto-formatting when absent. */
|
|
7802
|
+
precision: number().int().min(0).max(10).optional()
|
|
7803
|
+
});
|
|
7804
|
+
({
|
|
7805
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat]
|
|
7806
|
+
});
|
|
7807
|
+
object({
|
|
7808
|
+
/** Absolute signed URL the browser loads directly. Null when the
|
|
7809
|
+
* entity exposes no `entity_picture` (yet). */
|
|
7810
|
+
url: string().nullable(),
|
|
7811
|
+
/** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
|
|
7812
|
+
lastUpdated: number().nullable()
|
|
7813
|
+
});
|
|
7814
|
+
({
|
|
7815
|
+
deviceTypes: [DeviceType.Image]
|
|
7816
|
+
});
|
|
7817
|
+
const LawnMowerActivitySchema = _enum([
|
|
7818
|
+
"idle",
|
|
7819
|
+
"mowing",
|
|
7820
|
+
"paused",
|
|
7821
|
+
"docked",
|
|
7822
|
+
"error"
|
|
7823
|
+
]);
|
|
7824
|
+
object({
|
|
7825
|
+
/** Lifecycle activity of the mower. */
|
|
7826
|
+
activity: LawnMowerActivitySchema,
|
|
7827
|
+
/** 0..100 battery percentage. Null when the device has no battery
|
|
7828
|
+
* reading. */
|
|
7829
|
+
batteryLevel: number().min(0).max(100).nullable(),
|
|
7830
|
+
/** Ms epoch when the slice was last updated. */
|
|
7831
|
+
lastChangedAt: number()
|
|
7832
|
+
});
|
|
6410
7833
|
({
|
|
6411
|
-
deviceTypes: [DeviceType.
|
|
7834
|
+
deviceTypes: [DeviceType.LawnMower],
|
|
6412
7835
|
methods: {
|
|
6413
|
-
|
|
7836
|
+
startMowing: method(
|
|
6414
7837
|
object({ deviceId: number().int().nonnegative() }),
|
|
6415
|
-
|
|
7838
|
+
_void(),
|
|
7839
|
+
{ kind: "mutation", auth: "admin" }
|
|
6416
7840
|
),
|
|
6417
|
-
|
|
7841
|
+
pause: method(
|
|
6418
7842
|
object({ deviceId: number().int().nonnegative() }),
|
|
6419
|
-
|
|
7843
|
+
_void(),
|
|
7844
|
+
{ kind: "mutation", auth: "admin" }
|
|
6420
7845
|
),
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
7846
|
+
dock: method(
|
|
7847
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7848
|
+
_void(),
|
|
7849
|
+
{ kind: "mutation", auth: "admin" }
|
|
7850
|
+
)
|
|
7851
|
+
}
|
|
7852
|
+
});
|
|
7853
|
+
const LockStateSchema = _enum([
|
|
7854
|
+
"locked",
|
|
7855
|
+
"unlocked",
|
|
7856
|
+
"locking",
|
|
7857
|
+
"unlocking",
|
|
7858
|
+
"jammed"
|
|
7859
|
+
]);
|
|
7860
|
+
object({
|
|
7861
|
+
/** Lifecycle state of the lock. `jammed` means the motor reported
|
|
7862
|
+
* failure to reach the target — operator intervention required. */
|
|
7863
|
+
state: LockStateSchema,
|
|
7864
|
+
/** Ms epoch when the slice was last updated. */
|
|
7865
|
+
lastChangedAt: number()
|
|
7866
|
+
});
|
|
7867
|
+
({
|
|
7868
|
+
deviceTypes: [DeviceType.Lock],
|
|
7869
|
+
methods: {
|
|
7870
|
+
lock: method(
|
|
6435
7871
|
object({
|
|
6436
7872
|
deviceId: number().int().nonnegative(),
|
|
6437
|
-
/**
|
|
6438
|
-
|
|
7873
|
+
/** Optional PIN code required by some keypad locks. NOT
|
|
7874
|
+
* persisted — passed directly to the upstream service. */
|
|
7875
|
+
code: string().min(1).optional()
|
|
6439
7876
|
}),
|
|
6440
|
-
|
|
7877
|
+
_void(),
|
|
7878
|
+
{ kind: "mutation", auth: "admin" }
|
|
7879
|
+
),
|
|
7880
|
+
unlock: method(
|
|
7881
|
+
object({
|
|
7882
|
+
deviceId: number().int().nonnegative(),
|
|
7883
|
+
code: string().min(1).optional()
|
|
7884
|
+
}),
|
|
7885
|
+
_void(),
|
|
7886
|
+
{ kind: "mutation", auth: "admin" }
|
|
7887
|
+
),
|
|
7888
|
+
open: method(
|
|
7889
|
+
object({
|
|
7890
|
+
deviceId: number().int().nonnegative()
|
|
7891
|
+
}),
|
|
7892
|
+
_void(),
|
|
7893
|
+
{ kind: "mutation", auth: "admin" }
|
|
6441
7894
|
)
|
|
6442
|
-
}
|
|
6443
|
-
events: {
|
|
6444
|
-
/** Fires on publishCameraStream / retractCameraStream. */
|
|
6445
|
-
onCamStreamsChanged: event(object({
|
|
6446
|
-
deviceId: number().int().nonnegative(),
|
|
6447
|
-
camStreams: array(CameraStreamSchema).readonly()
|
|
6448
|
-
})),
|
|
6449
|
-
/** Fires on assignProfile / unassignProfile / runtime status change. */
|
|
6450
|
-
onProfileSlotsChanged: event(object({
|
|
6451
|
-
deviceId: number().int().nonnegative(),
|
|
6452
|
-
profileSlots: array(ProfileSlotSchema).readonly()
|
|
6453
|
-
}))
|
|
6454
|
-
},
|
|
6455
|
-
/**
|
|
6456
|
-
* Per-device live stream-broker state. Persistent settings (RTSP
|
|
6457
|
-
* tokens, profile assignments, pre-buffer config, RTSP-enabled toggles,
|
|
6458
|
-
* streamingDebug) stay in the broker's addon store — they survive
|
|
6459
|
-
* restarts. The slice below carries ONLY what's truly runtime:
|
|
6460
|
-
*
|
|
6461
|
-
* - `online` — at least one profile slot is currently `'streaming'`.
|
|
6462
|
-
* Drivers without a firmware liveness signal (RTSP, ONVIF…) can
|
|
6463
|
-
* subscribe and mirror this into `state.deviceStatus.online`.
|
|
6464
|
-
* - `slotStatuses` — current `ProfileSlotStatus` per profile,
|
|
6465
|
-
* mirroring the runtime-mutable subset of `ProfileSlot`.
|
|
6466
|
-
* - `slotErrors` — last error message per profile (only set when the
|
|
6467
|
-
* corresponding slot is in `'error'`).
|
|
6468
|
-
* - `lastChangedAt` — freshness signal for consumers that want to
|
|
6469
|
-
* reason about how stale the slice is.
|
|
6470
|
-
*
|
|
6471
|
-
* Written by the stream-broker manager on every transition that
|
|
6472
|
-
* affects these aggregates. Read via `device.state.cameraStreams.<field>`
|
|
6473
|
-
* (BaseDevice proxy) or, cross-process, via
|
|
6474
|
-
* `device-state.getCapSlice({deviceId, capName: 'camera-streams'})`.
|
|
6475
|
-
* The cap's `onChanged` event fires automatically on each write so
|
|
6476
|
-
* subscribers get push semantics for free.
|
|
6477
|
-
*/
|
|
6478
|
-
runtimeState: object({
|
|
6479
|
-
online: boolean(),
|
|
6480
|
-
slotStatuses: object({
|
|
6481
|
-
high: ProfileSlotStatusSchema.optional(),
|
|
6482
|
-
mid: ProfileSlotStatusSchema.optional(),
|
|
6483
|
-
low: ProfileSlotStatusSchema.optional()
|
|
6484
|
-
}),
|
|
6485
|
-
slotErrors: object({
|
|
6486
|
-
high: string().optional(),
|
|
6487
|
-
mid: string().optional(),
|
|
6488
|
-
low: string().optional()
|
|
6489
|
-
}),
|
|
6490
|
-
lastChangedAt: number()
|
|
6491
|
-
})
|
|
7895
|
+
}
|
|
6492
7896
|
});
|
|
6493
|
-
const
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
/**
|
|
6514
|
-
|
|
6515
|
-
* `childNativeId` under the current parent. The panel uses it to
|
|
6516
|
-
* gate the Add/Remove button and surface the existing `deviceId`.
|
|
6517
|
-
*/
|
|
6518
|
-
alreadyAdopted: boolean(),
|
|
6519
|
-
/** When `alreadyAdopted=true`, the framework-assigned child device id. */
|
|
6520
|
-
adoptedDeviceId: number().int().nonnegative().nullable()
|
|
7897
|
+
const MediaPlayerStateSchema = _enum([
|
|
7898
|
+
"off",
|
|
7899
|
+
"on",
|
|
7900
|
+
"idle",
|
|
7901
|
+
"playing",
|
|
7902
|
+
"paused",
|
|
7903
|
+
"buffering",
|
|
7904
|
+
"standby"
|
|
7905
|
+
]);
|
|
7906
|
+
const MediaPlayerRepeatSchema = _enum(["off", "all", "one"]);
|
|
7907
|
+
const MediaInfoSchema = object({
|
|
7908
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
7909
|
+
* `podcast`, …). */
|
|
7910
|
+
type: string(),
|
|
7911
|
+
/** Human-readable title. */
|
|
7912
|
+
title: string(),
|
|
7913
|
+
/** Optional artist / channel / station label. */
|
|
7914
|
+
artist: string().optional(),
|
|
7915
|
+
/** Optional album / season / show name. */
|
|
7916
|
+
album: string().optional(),
|
|
7917
|
+
/** Optional cover-art / thumbnail URL. */
|
|
7918
|
+
imageUrl: string().optional()
|
|
6521
7919
|
});
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
7920
|
+
object({
|
|
7921
|
+
/** Playback lifecycle state. */
|
|
7922
|
+
state: MediaPlayerStateSchema,
|
|
7923
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
7924
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
7925
|
+
volumeLevel: number().min(0).max(100).nullable(),
|
|
7926
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
7927
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
7928
|
+
isMuted: boolean().nullable(),
|
|
7929
|
+
/** Active source / input. Empty when no source surface. */
|
|
7930
|
+
source: string(),
|
|
7931
|
+
/** Selectable sources. Empty when no source surface.
|
|
7932
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
7933
|
+
availableSources: array(string()),
|
|
7934
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
7935
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
7936
|
+
/** Current playback position in ms. Null when not seekable or
|
|
7937
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
7938
|
+
positionMs: number().int().nonnegative().nullable(),
|
|
7939
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
7940
|
+
* (live stream) or nothing is playing. */
|
|
7941
|
+
durationMs: number().int().nonnegative().nullable(),
|
|
7942
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
7943
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
7944
|
+
shuffle: boolean().nullable(),
|
|
7945
|
+
/** Repeat mode. Null when no repeat surface.
|
|
7946
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
7947
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
7948
|
+
/** Ms epoch when the slice was last updated. */
|
|
7949
|
+
lastChangedAt: number()
|
|
6528
7950
|
});
|
|
6529
7951
|
({
|
|
6530
|
-
|
|
6531
|
-
// can register against the cap by also targeting their root type.
|
|
6532
|
-
deviceTypes: [DeviceType.Hub],
|
|
6533
|
-
// Mirror status into the per-device runtime-state slice so the panel
|
|
6534
|
-
// hydrates from the kernel cache without a round-trip on every open.
|
|
6535
|
-
runtimeState: DeviceDiscoveryStatusSchema.extend({
|
|
6536
|
-
lastFetchedAt: number().int().nonnegative()
|
|
6537
|
-
}),
|
|
7952
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
6538
7953
|
methods: {
|
|
6539
|
-
|
|
6540
|
-
* Snapshot of the current `discovered` list. Returns the
|
|
6541
|
-
* runtime-state cache — call `refreshDiscovery` first if a
|
|
6542
|
-
* fresh round-trip to the source is required.
|
|
6543
|
-
*/
|
|
6544
|
-
listDiscovered: method(
|
|
7954
|
+
play: method(
|
|
6545
7955
|
object({ deviceId: number().int().nonnegative() }),
|
|
6546
|
-
|
|
7956
|
+
_void(),
|
|
7957
|
+
{ kind: "mutation", auth: "admin" }
|
|
6547
7958
|
),
|
|
6548
|
-
|
|
6549
|
-
* Force the integration to re-enumerate and update the
|
|
6550
|
-
* runtime-state slice. Returns the freshly-enumerated list (also
|
|
6551
|
-
* available via `listDiscovered` post-call).
|
|
6552
|
-
*/
|
|
6553
|
-
refreshDiscovery: method(
|
|
7959
|
+
pause: method(
|
|
6554
7960
|
object({ deviceId: number().int().nonnegative() }),
|
|
6555
|
-
|
|
7961
|
+
_void(),
|
|
6556
7962
|
{ kind: "mutation", auth: "admin" }
|
|
6557
7963
|
),
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
7964
|
+
stop: method(
|
|
7965
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7966
|
+
_void(),
|
|
7967
|
+
{ kind: "mutation", auth: "admin" }
|
|
7968
|
+
),
|
|
7969
|
+
next: method(
|
|
7970
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7971
|
+
_void(),
|
|
7972
|
+
{ kind: "mutation", auth: "admin" }
|
|
7973
|
+
),
|
|
7974
|
+
previous: method(
|
|
7975
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
7976
|
+
_void(),
|
|
7977
|
+
{ kind: "mutation", auth: "admin" }
|
|
7978
|
+
),
|
|
7979
|
+
seek: method(
|
|
6566
7980
|
object({
|
|
6567
7981
|
deviceId: number().int().nonnegative(),
|
|
6568
|
-
|
|
6569
|
-
/** Optional override for the child's display name. */
|
|
6570
|
-
name: string().optional()
|
|
7982
|
+
positionMs: number().int().nonnegative()
|
|
6571
7983
|
}),
|
|
7984
|
+
_void(),
|
|
7985
|
+
{ kind: "mutation", auth: "admin" }
|
|
7986
|
+
),
|
|
7987
|
+
setVolume: method(
|
|
6572
7988
|
object({
|
|
6573
7989
|
deviceId: number().int().nonnegative(),
|
|
6574
|
-
|
|
7990
|
+
volumeLevel: number().min(0).max(100)
|
|
6575
7991
|
}),
|
|
7992
|
+
_void(),
|
|
6576
7993
|
{ kind: "mutation", auth: "admin" }
|
|
6577
7994
|
),
|
|
6578
|
-
|
|
6579
|
-
* Inverse of `adoptDevice`: removes the child device from the
|
|
6580
|
-
* kernel registry. The discovered entry remains in the
|
|
6581
|
-
* enumeration (status updates resume) so the operator can re-adopt
|
|
6582
|
-
* it later without a fresh refresh.
|
|
6583
|
-
*/
|
|
6584
|
-
releaseDevice: method(
|
|
7995
|
+
setMute: method(
|
|
6585
7996
|
object({
|
|
6586
7997
|
deviceId: number().int().nonnegative(),
|
|
6587
|
-
|
|
7998
|
+
muted: boolean()
|
|
7999
|
+
}),
|
|
8000
|
+
_void(),
|
|
8001
|
+
{ kind: "mutation", auth: "admin" }
|
|
8002
|
+
),
|
|
8003
|
+
setShuffle: method(
|
|
8004
|
+
object({
|
|
8005
|
+
deviceId: number().int().nonnegative(),
|
|
8006
|
+
shuffle: boolean()
|
|
8007
|
+
}),
|
|
8008
|
+
_void(),
|
|
8009
|
+
{ kind: "mutation", auth: "admin" }
|
|
8010
|
+
),
|
|
8011
|
+
setRepeat: method(
|
|
8012
|
+
object({
|
|
8013
|
+
deviceId: number().int().nonnegative(),
|
|
8014
|
+
repeat: MediaPlayerRepeatSchema
|
|
8015
|
+
}),
|
|
8016
|
+
_void(),
|
|
8017
|
+
{ kind: "mutation", auth: "admin" }
|
|
8018
|
+
),
|
|
8019
|
+
selectSource: method(
|
|
8020
|
+
object({
|
|
8021
|
+
deviceId: number().int().nonnegative(),
|
|
8022
|
+
source: string().min(1)
|
|
8023
|
+
}),
|
|
8024
|
+
_void(),
|
|
8025
|
+
{ kind: "mutation", auth: "admin" }
|
|
8026
|
+
),
|
|
8027
|
+
playMedia: method(
|
|
8028
|
+
object({
|
|
8029
|
+
deviceId: number().int().nonnegative(),
|
|
8030
|
+
/** Media identifier / URL. */
|
|
8031
|
+
mediaId: string().min(1),
|
|
8032
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
8033
|
+
* passes it through to the upstream service. */
|
|
8034
|
+
mediaType: string().min(1)
|
|
6588
8035
|
}),
|
|
6589
8036
|
_void(),
|
|
6590
8037
|
{ kind: "mutation", auth: "admin" }
|
|
6591
8038
|
)
|
|
6592
8039
|
}
|
|
6593
8040
|
});
|
|
6594
|
-
object({
|
|
6595
|
-
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
6596
|
-
lastPressedAt: number().nullable(),
|
|
6597
|
-
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
6598
|
-
pressCountSinceStart: number()
|
|
6599
|
-
});
|
|
6600
|
-
object({
|
|
6601
|
-
deviceId: number(),
|
|
6602
|
-
timestamp: number()
|
|
6603
|
-
});
|
|
6604
|
-
({
|
|
6605
|
-
deviceTypes: [DeviceType.Button]
|
|
6606
|
-
});
|
|
6607
8041
|
const FrameFormatSchema = _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]);
|
|
6608
8042
|
const FrameInputSchema = object({
|
|
6609
8043
|
data: custom(),
|
|
@@ -7010,8 +8444,8 @@ const PipelineRunResultBridge = custom();
|
|
|
7010
8444
|
* calls. Single root step + uniform model assumed; trees with crop
|
|
7011
8445
|
* children fall back to sequential execution.
|
|
7012
8446
|
*
|
|
7013
|
-
* Used by `scripts/bench-
|
|
7014
|
-
*
|
|
8447
|
+
* Used by `scripts/bench-batch-style.mts` for batch benchmarking —
|
|
8448
|
+
* N frames in one call to amortise per-call IPC overhead.
|
|
7015
8449
|
*/
|
|
7016
8450
|
runPipelineBatch: method(
|
|
7017
8451
|
object({
|
|
@@ -7392,8 +8826,8 @@ object({
|
|
|
7392
8826
|
lastDetectedAt: number().nullable(),
|
|
7393
8827
|
/**
|
|
7394
8828
|
* Ms after which `detected` auto-reverts to false if no fresh push
|
|
7395
|
-
* arrives.
|
|
7396
|
-
*
|
|
8829
|
+
* arrives. Null means the provider leaves detected state until a
|
|
8830
|
+
* native "clear" event.
|
|
7397
8831
|
*/
|
|
7398
8832
|
autoClearAfterMs: number().nullable()
|
|
7399
8833
|
});
|
|
@@ -7579,6 +9013,194 @@ NativeObjectDetectionStatusSchema.extend({
|
|
|
7579
9013
|
}) }
|
|
7580
9014
|
}
|
|
7581
9015
|
});
|
|
9016
|
+
const NotifierPrioritySchema = _enum(["min", "low", "normal", "high"]);
|
|
9017
|
+
const NotifierActionSchema = object({
|
|
9018
|
+
/** Stable id used in the callback when the user taps the button. */
|
|
9019
|
+
id: string().min(1),
|
|
9020
|
+
/** User-visible button label. */
|
|
9021
|
+
title: string().min(1),
|
|
9022
|
+
/** Optional deep-link URI invoked on tap (rich notifiers only). */
|
|
9023
|
+
uri: url().optional(),
|
|
9024
|
+
/** Optional flag — when true, the action is destructive and the
|
|
9025
|
+
* client should render the button in a warning style. */
|
|
9026
|
+
destructive: boolean().optional()
|
|
9027
|
+
});
|
|
9028
|
+
const NotifierSupportsSchema = object({
|
|
9029
|
+
/** Inline / URL image attachment. Pair with `DeviceFeature.NotifierImage`. */
|
|
9030
|
+
image: boolean(),
|
|
9031
|
+
/** Priority hint. Pair with `DeviceFeature.NotifierPriority`. */
|
|
9032
|
+
priority: boolean(),
|
|
9033
|
+
/** Free-form platform-specific data block.
|
|
9034
|
+
* Pair with `DeviceFeature.NotifierData`. */
|
|
9035
|
+
data: boolean(),
|
|
9036
|
+
/** Interactive action buttons. Pair with `DeviceFeature.NotifierActions`. */
|
|
9037
|
+
actions: boolean(),
|
|
9038
|
+
/** Per-call recipient targeting (multi-user notifiers).
|
|
9039
|
+
* Pair with `DeviceFeature.NotifierRecipients`. */
|
|
9040
|
+
recipients: boolean()
|
|
9041
|
+
});
|
|
9042
|
+
object({
|
|
9043
|
+
/** Ms epoch of the most recent successful send. 0 if none yet. */
|
|
9044
|
+
lastSentAt: number(),
|
|
9045
|
+
/** Failure description from the most recent send attempt. Null on
|
|
9046
|
+
* success or when nothing has been sent yet. */
|
|
9047
|
+
lastError: string().nullable(),
|
|
9048
|
+
/** Number of deliveries currently buffered server-side (rate-limited
|
|
9049
|
+
* notifiers may queue). 0 when the provider sends synchronously. */
|
|
9050
|
+
queueDepth: number().int().nonnegative(),
|
|
9051
|
+
/** Per-feature capability matrix — authoritative source for the
|
|
9052
|
+
* compose-form field gating. */
|
|
9053
|
+
supports: NotifierSupportsSchema
|
|
9054
|
+
});
|
|
9055
|
+
const NotifierSendInputSchema = object({
|
|
9056
|
+
deviceId: number().int().nonnegative(),
|
|
9057
|
+
/** Optional title — many platforms render it bolder than the body. */
|
|
9058
|
+
title: string().optional(),
|
|
9059
|
+
/** Required message body. */
|
|
9060
|
+
body: string().min(1),
|
|
9061
|
+
/** Optional image URL or inline data URI (`data:image/...;base64,...`). */
|
|
9062
|
+
image: string().optional(),
|
|
9063
|
+
/** Priority hint — providers without priority support ignore the field. */
|
|
9064
|
+
priority: NotifierPrioritySchema.optional(),
|
|
9065
|
+
/** Channel / topic / tag the notifier should route through (Android
|
|
9066
|
+
* notification channel, Telegram chat id, ntfy topic, …). */
|
|
9067
|
+
channel: string().optional(),
|
|
9068
|
+
/** Optional list of recipient ids (multi-user notifiers). Empty /
|
|
9069
|
+
* omitted = the device's default recipient. */
|
|
9070
|
+
recipients: array(string()).optional(),
|
|
9071
|
+
/** Optional interactive action buttons. */
|
|
9072
|
+
actions: array(NotifierActionSchema).optional(),
|
|
9073
|
+
/** Free-form platform-specific payload — passed through to the
|
|
9074
|
+
* upstream service untouched. */
|
|
9075
|
+
data: record(string(), unknown()).optional()
|
|
9076
|
+
});
|
|
9077
|
+
const NotifierSendResultSchema = object({
|
|
9078
|
+
/** Provider-assigned id for the delivery. Used for `cancel`. */
|
|
9079
|
+
notificationId: string(),
|
|
9080
|
+
/** Ms epoch when the notifier accepted the send (not when delivered). */
|
|
9081
|
+
acceptedAt: number()
|
|
9082
|
+
});
|
|
9083
|
+
({
|
|
9084
|
+
deviceTypes: [DeviceType.Notifier],
|
|
9085
|
+
methods: {
|
|
9086
|
+
send: method(
|
|
9087
|
+
NotifierSendInputSchema,
|
|
9088
|
+
NotifierSendResultSchema,
|
|
9089
|
+
{ kind: "mutation", auth: "admin" }
|
|
9090
|
+
),
|
|
9091
|
+
cancel: method(
|
|
9092
|
+
object({
|
|
9093
|
+
deviceId: number().int().nonnegative(),
|
|
9094
|
+
notificationId: string().min(1)
|
|
9095
|
+
}),
|
|
9096
|
+
_void(),
|
|
9097
|
+
{ kind: "mutation", auth: "admin" }
|
|
9098
|
+
)
|
|
9099
|
+
},
|
|
9100
|
+
events: {
|
|
9101
|
+
/**
|
|
9102
|
+
* Emitted after every send attempt — success or failure. Subscribers
|
|
9103
|
+
* (admin UI history pane, automation engines, retry workers) react
|
|
9104
|
+
* without polling the provider's `lastSentAt`.
|
|
9105
|
+
*/
|
|
9106
|
+
onSent: { data: object({
|
|
9107
|
+
deviceId: number(),
|
|
9108
|
+
notificationId: string(),
|
|
9109
|
+
success: boolean(),
|
|
9110
|
+
error: string().nullable(),
|
|
9111
|
+
acceptedAt: number()
|
|
9112
|
+
}) }
|
|
9113
|
+
}
|
|
9114
|
+
});
|
|
9115
|
+
object({
|
|
9116
|
+
value: number(),
|
|
9117
|
+
/** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
|
|
9118
|
+
* Populated live from the upstream source on each state push.
|
|
9119
|
+
* Absent for unitless sensors. */
|
|
9120
|
+
unit: string().optional(),
|
|
9121
|
+
/** Suggested decimal places for numeric display.
|
|
9122
|
+
* Populated live from the upstream source when provided.
|
|
9123
|
+
* Falls back to auto-formatting when absent. */
|
|
9124
|
+
precision: number().int().min(0).max(10).optional(),
|
|
9125
|
+
/** Ms epoch when the slice was last updated. */
|
|
9126
|
+
lastFetchedAt: number()
|
|
9127
|
+
});
|
|
9128
|
+
({
|
|
9129
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9130
|
+
});
|
|
9131
|
+
object({
|
|
9132
|
+
/** Instantaneous power draw in watts. */
|
|
9133
|
+
watts: number().optional(),
|
|
9134
|
+
/** Cumulative energy in kilowatt-hours since the meter was reset. */
|
|
9135
|
+
kwhTotal: number().optional(),
|
|
9136
|
+
/** Voltage in volts. */
|
|
9137
|
+
volts: number().optional(),
|
|
9138
|
+
/** Current in amperes. */
|
|
9139
|
+
amps: number().optional(),
|
|
9140
|
+
/** Apparent power in volt-amperes (W × power-factor reciprocal). */
|
|
9141
|
+
va: number().optional(),
|
|
9142
|
+
/** Power factor (0..1) if reported. */
|
|
9143
|
+
powerFactor: number().min(0).max(1).optional(),
|
|
9144
|
+
/** Ms epoch when the slice was last updated. */
|
|
9145
|
+
lastFetchedAt: number(),
|
|
9146
|
+
/** Live display unit of the single metric this slice carries (e.g. HA
|
|
9147
|
+
* `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
|
|
9148
|
+
* Each upstream `sensor.*` entity surfaces ONE device_class, so one
|
|
9149
|
+
* unit per slice is unambiguous. The UI prefers this over the role's
|
|
9150
|
+
* canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
|
|
9151
|
+
unit: string().optional(),
|
|
9152
|
+
/** Suggested decimal places for numeric display.
|
|
9153
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
9154
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
9155
|
+
* auto-formatting when absent. */
|
|
9156
|
+
precision: number().int().min(0).max(10).optional()
|
|
9157
|
+
});
|
|
9158
|
+
({
|
|
9159
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9160
|
+
});
|
|
9161
|
+
const GpsLocationSchema = object({
|
|
9162
|
+
/** Latitude in decimal degrees, -90..90. */
|
|
9163
|
+
latitude: number().min(-90).max(90),
|
|
9164
|
+
/** Longitude in decimal degrees, -180..180. */
|
|
9165
|
+
longitude: number().min(-180).max(180),
|
|
9166
|
+
/** Reported accuracy in meters (lower = better). */
|
|
9167
|
+
accuracyMeters: number().nonnegative()
|
|
9168
|
+
});
|
|
9169
|
+
object({
|
|
9170
|
+
/** `home` / `not_home` / any user-defined zone name. */
|
|
9171
|
+
state: string(),
|
|
9172
|
+
/** Optional textual location label (zone name, city, address). Null
|
|
9173
|
+
* when only the binary state is known. */
|
|
9174
|
+
location: string().nullable(),
|
|
9175
|
+
/** GPS coordinates when available. Null otherwise. */
|
|
9176
|
+
gps: GpsLocationSchema.nullable(),
|
|
9177
|
+
/** Optional battery level of the tracking device (0..100). Null
|
|
9178
|
+
* when not reported. */
|
|
9179
|
+
batteryPercent: number().min(0).max(100).nullable(),
|
|
9180
|
+
/** Ms epoch when the slice was last updated. */
|
|
9181
|
+
lastChangedAt: number()
|
|
9182
|
+
});
|
|
9183
|
+
({
|
|
9184
|
+
deviceTypes: [DeviceType.Presence]
|
|
9185
|
+
});
|
|
9186
|
+
object({
|
|
9187
|
+
/** Current pressure in hPa. */
|
|
9188
|
+
hpa: number(),
|
|
9189
|
+
/** Ms epoch when the slice was last updated. */
|
|
9190
|
+
lastFetchedAt: number(),
|
|
9191
|
+
/** Live display unit from the upstream source (e.g. HA
|
|
9192
|
+
* `attributes.unit_of_measurement`). The UI prefers this over the
|
|
9193
|
+
* role's canonical unit. Absent → fall back to the canonical unit. */
|
|
9194
|
+
unit: string().optional(),
|
|
9195
|
+
/** Suggested decimal places for numeric display.
|
|
9196
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
9197
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
9198
|
+
* auto-formatting when absent. */
|
|
9199
|
+
precision: number().int().min(0).max(10).optional()
|
|
9200
|
+
});
|
|
9201
|
+
({
|
|
9202
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9203
|
+
});
|
|
7582
9204
|
const PrivacyMaskShapeSchema = discriminatedUnion("kind", [
|
|
7583
9205
|
MaskRectShapeSchema,
|
|
7584
9206
|
MaskPolygonShapeSchema
|
|
@@ -7707,6 +9329,51 @@ PtzAutotrackStatusSchema.extend({
|
|
|
7707
9329
|
}) }
|
|
7708
9330
|
}
|
|
7709
9331
|
});
|
|
9332
|
+
object({
|
|
9333
|
+
/** Whether the script is currently executing. */
|
|
9334
|
+
isRunning: boolean(),
|
|
9335
|
+
/** Ms epoch of the last invocation start. 0 when never run. */
|
|
9336
|
+
lastRunAt: number(),
|
|
9337
|
+
/** Outcome of the last completed run. Null when never run or still
|
|
9338
|
+
* running. */
|
|
9339
|
+
lastRunSuccess: boolean().nullable(),
|
|
9340
|
+
/** Failure description from the last completed run. Null on success
|
|
9341
|
+
* or when never run. */
|
|
9342
|
+
lastError: string().nullable(),
|
|
9343
|
+
/** Ms epoch when the slice was last updated. */
|
|
9344
|
+
lastChangedAt: number()
|
|
9345
|
+
});
|
|
9346
|
+
({
|
|
9347
|
+
deviceTypes: [DeviceType.Script],
|
|
9348
|
+
methods: {
|
|
9349
|
+
run: method(
|
|
9350
|
+
object({
|
|
9351
|
+
deviceId: number().int().nonnegative(),
|
|
9352
|
+
/** Optional variables map — passed through to the upstream
|
|
9353
|
+
* script. Provider rejects when the script doesn't declare
|
|
9354
|
+
* input fields (gated by `DeviceFeature.ScriptVariables`). */
|
|
9355
|
+
variables: record(string(), unknown()).optional()
|
|
9356
|
+
}),
|
|
9357
|
+
_void(),
|
|
9358
|
+
{ kind: "mutation", auth: "admin" }
|
|
9359
|
+
),
|
|
9360
|
+
/** Cancel a running script. Provider rejects when the script
|
|
9361
|
+
* isn't currently running. */
|
|
9362
|
+
stop: method(
|
|
9363
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9364
|
+
_void(),
|
|
9365
|
+
{ kind: "mutation", auth: "admin" }
|
|
9366
|
+
)
|
|
9367
|
+
}
|
|
9368
|
+
});
|
|
9369
|
+
object({
|
|
9370
|
+
detected: boolean(),
|
|
9371
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
9372
|
+
lastChangedAt: number()
|
|
9373
|
+
});
|
|
9374
|
+
({
|
|
9375
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9376
|
+
});
|
|
7710
9377
|
const StreamProfileSchema = _enum(["main", "sub", "ext"]);
|
|
7711
9378
|
const StreamProfileConfigSchema = object({
|
|
7712
9379
|
width: number(),
|
|
@@ -7823,6 +9490,257 @@ object({
|
|
|
7823
9490
|
}) }
|
|
7824
9491
|
}
|
|
7825
9492
|
});
|
|
9493
|
+
object({
|
|
9494
|
+
/** True when the device's tamper switch / case-open contact is
|
|
9495
|
+
* currently triggered. */
|
|
9496
|
+
tampered: boolean(),
|
|
9497
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
9498
|
+
lastChangedAt: number()
|
|
9499
|
+
});
|
|
9500
|
+
({
|
|
9501
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9502
|
+
});
|
|
9503
|
+
object({
|
|
9504
|
+
/** Current temperature in Celsius. */
|
|
9505
|
+
celsius: number(),
|
|
9506
|
+
/** Ms epoch when the slice was last updated (push or poll). */
|
|
9507
|
+
lastFetchedAt: number(),
|
|
9508
|
+
/** Live display unit from the upstream source (e.g. HA
|
|
9509
|
+
* `attributes.unit_of_measurement`). The UI prefers this over the
|
|
9510
|
+
* role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
|
|
9511
|
+
* Absent → the UI falls back to the role's canonical unit. */
|
|
9512
|
+
unit: string().optional(),
|
|
9513
|
+
/** Suggested decimal places for numeric display.
|
|
9514
|
+
* Populated live from the upstream source when provided (e.g. HA
|
|
9515
|
+
* `attributes.suggested_display_precision`). Falls back to
|
|
9516
|
+
* auto-formatting when absent. */
|
|
9517
|
+
precision: number().int().min(0).max(10).optional()
|
|
9518
|
+
});
|
|
9519
|
+
({
|
|
9520
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat]
|
|
9521
|
+
});
|
|
9522
|
+
object({
|
|
9523
|
+
currentVersion: string().nullable(),
|
|
9524
|
+
availableVersion: string().nullable(),
|
|
9525
|
+
/**
|
|
9526
|
+
* DEVICE-CAPABILITY flag: the entity supports firmware updates at all — NOT
|
|
9527
|
+
* a per-state "an update is available right now" flag. The canonical
|
|
9528
|
+
* "update available" signal is `availableVersion !== currentVersion` (which
|
|
9529
|
+
* is what the UI uses); HA's `state === 'on'` mirrors that same condition.
|
|
9530
|
+
*/
|
|
9531
|
+
updatable: boolean(),
|
|
9532
|
+
state: string().nullable(),
|
|
9533
|
+
// e.g. 'UP_TO_DATE' / 'DELIVER_FIRMWARE_IMAGE' (provider-verbatim)
|
|
9534
|
+
inProgress: boolean()
|
|
9535
|
+
});
|
|
9536
|
+
({
|
|
9537
|
+
deviceTypes: [DeviceType.Update],
|
|
9538
|
+
methods: {
|
|
9539
|
+
installUpdate: method(_void(), _void(), { kind: "mutation", auth: "admin" })
|
|
9540
|
+
}
|
|
9541
|
+
});
|
|
9542
|
+
const VacuumStateSchema = _enum([
|
|
9543
|
+
"idle",
|
|
9544
|
+
"cleaning",
|
|
9545
|
+
"paused",
|
|
9546
|
+
"returning",
|
|
9547
|
+
"docked",
|
|
9548
|
+
"error"
|
|
9549
|
+
]);
|
|
9550
|
+
const TankStatusSchema = object({
|
|
9551
|
+
/** Numeric fill 0..100 when the hardware reports a percentage; null otherwise. */
|
|
9552
|
+
level: number().min(0).max(100).nullable(),
|
|
9553
|
+
/** Discrete state when the hardware is binary-mode; null otherwise. */
|
|
9554
|
+
status: _enum(["ok", "low", "full"]).nullable()
|
|
9555
|
+
});
|
|
9556
|
+
object({
|
|
9557
|
+
/** Lifecycle state of the vacuum. */
|
|
9558
|
+
state: VacuumStateSchema,
|
|
9559
|
+
/** 0..100 battery percentage. Null when the device has no battery
|
|
9560
|
+
* reading. */
|
|
9561
|
+
batteryLevel: number().min(0).max(100).nullable(),
|
|
9562
|
+
/** Current fan-speed token (provider-verbatim). Null when unknown or
|
|
9563
|
+
* the vacuum has no speed control. */
|
|
9564
|
+
fanSpeed: string().nullable(),
|
|
9565
|
+
/** Speed tokens the hardware accepts — drives the UI selector. */
|
|
9566
|
+
availableFanSpeeds: array(string()),
|
|
9567
|
+
/** Clean-water (mop) tank. Null when the hardware has no clean-water tank. */
|
|
9568
|
+
cleanWater: TankStatusSchema.nullable(),
|
|
9569
|
+
/** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
|
|
9570
|
+
dirtyWater: TankStatusSchema.nullable(),
|
|
9571
|
+
/** Detergent tank. Null when the hardware has no detergent tank. */
|
|
9572
|
+
detergent: TankStatusSchema.nullable(),
|
|
9573
|
+
/** Dust bin. Null when the hardware has no dust bin. */
|
|
9574
|
+
dustBin: TankStatusSchema.nullable(),
|
|
9575
|
+
/** Ms epoch when the slice was last updated. */
|
|
9576
|
+
lastChangedAt: number()
|
|
9577
|
+
});
|
|
9578
|
+
({
|
|
9579
|
+
deviceTypes: [DeviceType.Vacuum],
|
|
9580
|
+
methods: {
|
|
9581
|
+
start: method(
|
|
9582
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9583
|
+
_void(),
|
|
9584
|
+
{ kind: "mutation", auth: "admin" }
|
|
9585
|
+
),
|
|
9586
|
+
pause: method(
|
|
9587
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9588
|
+
_void(),
|
|
9589
|
+
{ kind: "mutation", auth: "admin" }
|
|
9590
|
+
),
|
|
9591
|
+
stop: method(
|
|
9592
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9593
|
+
_void(),
|
|
9594
|
+
{ kind: "mutation", auth: "admin" }
|
|
9595
|
+
),
|
|
9596
|
+
returnToBase: method(
|
|
9597
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9598
|
+
_void(),
|
|
9599
|
+
{ kind: "mutation", auth: "admin" }
|
|
9600
|
+
),
|
|
9601
|
+
locate: method(
|
|
9602
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9603
|
+
_void(),
|
|
9604
|
+
{ kind: "mutation", auth: "admin" }
|
|
9605
|
+
),
|
|
9606
|
+
setFanSpeed: method(
|
|
9607
|
+
object({
|
|
9608
|
+
deviceId: number().int().nonnegative(),
|
|
9609
|
+
speed: string().min(1)
|
|
9610
|
+
}),
|
|
9611
|
+
_void(),
|
|
9612
|
+
{ kind: "mutation", auth: "admin" }
|
|
9613
|
+
)
|
|
9614
|
+
}
|
|
9615
|
+
});
|
|
9616
|
+
const ValveStateSchema = _enum([
|
|
9617
|
+
"open",
|
|
9618
|
+
"opening",
|
|
9619
|
+
"closing",
|
|
9620
|
+
"closed",
|
|
9621
|
+
"stopped"
|
|
9622
|
+
]);
|
|
9623
|
+
object({
|
|
9624
|
+
/** Lifecycle state of the valve. */
|
|
9625
|
+
state: ValveStateSchema,
|
|
9626
|
+
/** 0 = fully closed, 100 = fully open. Null when the device has no
|
|
9627
|
+
* intermediate position surface. */
|
|
9628
|
+
position: number().min(0).max(100).nullable(),
|
|
9629
|
+
/** Ms epoch when the slice was last updated. */
|
|
9630
|
+
lastChangedAt: number()
|
|
9631
|
+
});
|
|
9632
|
+
({
|
|
9633
|
+
deviceTypes: [DeviceType.Valve],
|
|
9634
|
+
methods: {
|
|
9635
|
+
open: method(
|
|
9636
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9637
|
+
_void(),
|
|
9638
|
+
{ kind: "mutation", auth: "admin" }
|
|
9639
|
+
),
|
|
9640
|
+
close: method(
|
|
9641
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9642
|
+
_void(),
|
|
9643
|
+
{ kind: "mutation", auth: "admin" }
|
|
9644
|
+
),
|
|
9645
|
+
stop: method(
|
|
9646
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
9647
|
+
_void(),
|
|
9648
|
+
{ kind: "mutation", auth: "admin" }
|
|
9649
|
+
),
|
|
9650
|
+
setPosition: method(
|
|
9651
|
+
object({
|
|
9652
|
+
deviceId: number().int().nonnegative(),
|
|
9653
|
+
position: number().min(0).max(100)
|
|
9654
|
+
}),
|
|
9655
|
+
_void(),
|
|
9656
|
+
{ kind: "mutation", auth: "admin" }
|
|
9657
|
+
)
|
|
9658
|
+
}
|
|
9659
|
+
});
|
|
9660
|
+
object({
|
|
9661
|
+
detected: boolean(),
|
|
9662
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
9663
|
+
lastChangedAt: number()
|
|
9664
|
+
});
|
|
9665
|
+
({
|
|
9666
|
+
deviceTypes: [DeviceType.Sensor]
|
|
9667
|
+
});
|
|
9668
|
+
object({
|
|
9669
|
+
/** Current measured temperature. Null when not reported. */
|
|
9670
|
+
currentTemp: number().nullable(),
|
|
9671
|
+
/** Target temperature setpoint. Null when no setpoint surface. */
|
|
9672
|
+
targetTemp: number().nullable(),
|
|
9673
|
+
/** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
|
|
9674
|
+
* `heat_pump` / `high_demand` / `performance` / `off`). Null when the
|
|
9675
|
+
* device reports an unknown state. */
|
|
9676
|
+
operationMode: string().nullable(),
|
|
9677
|
+
/** Available operation modes = HA `operation_list`. */
|
|
9678
|
+
availableModes: array(string()),
|
|
9679
|
+
/** Away mode (HA `away_mode` 'on'/'off' → bool). Null when the device
|
|
9680
|
+
* has no away surface. */
|
|
9681
|
+
away: boolean().nullable(),
|
|
9682
|
+
/** HA `min_temp` attribute. Null when not reported. */
|
|
9683
|
+
minTemp: number().nullable(),
|
|
9684
|
+
/** HA `max_temp` attribute. Null when not reported. */
|
|
9685
|
+
maxTemp: number().nullable(),
|
|
9686
|
+
/** Ms epoch when the slice was last updated. */
|
|
9687
|
+
lastChangedAt: number()
|
|
9688
|
+
});
|
|
9689
|
+
({
|
|
9690
|
+
deviceTypes: [DeviceType.WaterHeater],
|
|
9691
|
+
methods: {
|
|
9692
|
+
setTargetTemp: method(
|
|
9693
|
+
object({
|
|
9694
|
+
deviceId: number().int().nonnegative(),
|
|
9695
|
+
temp: number().finite()
|
|
9696
|
+
}),
|
|
9697
|
+
_void(),
|
|
9698
|
+
{ kind: "mutation", auth: "admin" }
|
|
9699
|
+
),
|
|
9700
|
+
setOperationMode: method(
|
|
9701
|
+
object({
|
|
9702
|
+
deviceId: number().int().nonnegative(),
|
|
9703
|
+
mode: string().min(1)
|
|
9704
|
+
}),
|
|
9705
|
+
_void(),
|
|
9706
|
+
{ kind: "mutation", auth: "admin" }
|
|
9707
|
+
),
|
|
9708
|
+
setAway: method(
|
|
9709
|
+
object({
|
|
9710
|
+
deviceId: number().int().nonnegative(),
|
|
9711
|
+
on: boolean()
|
|
9712
|
+
}),
|
|
9713
|
+
_void(),
|
|
9714
|
+
{ kind: "mutation", auth: "admin" }
|
|
9715
|
+
)
|
|
9716
|
+
}
|
|
9717
|
+
});
|
|
9718
|
+
object({
|
|
9719
|
+
/** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
|
|
9720
|
+
* when no condition has been reported yet. */
|
|
9721
|
+
condition: string().nullable(),
|
|
9722
|
+
/** Current temperature in the reported unit. Null when not provided. */
|
|
9723
|
+
temperature: number().nullable(),
|
|
9724
|
+
/** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
|
|
9725
|
+
temperatureUnit: string().nullable(),
|
|
9726
|
+
/** Relative humidity (0..100). Null when not provided. */
|
|
9727
|
+
humidity: number().min(0).max(100).nullable(),
|
|
9728
|
+
/** Barometric pressure in the reported unit. Null when not provided. */
|
|
9729
|
+
pressure: number().nullable(),
|
|
9730
|
+
/** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
|
|
9731
|
+
pressureUnit: string().nullable(),
|
|
9732
|
+
/** Wind speed in the reported unit. Null when not provided. */
|
|
9733
|
+
windSpeed: number().nullable(),
|
|
9734
|
+
/** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
|
|
9735
|
+
windSpeedUnit: string().nullable(),
|
|
9736
|
+
/** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
|
|
9737
|
+
windBearing: number().nullable(),
|
|
9738
|
+
/** Ms epoch when the slice was last updated. */
|
|
9739
|
+
lastFetchedAt: number()
|
|
9740
|
+
});
|
|
9741
|
+
({
|
|
9742
|
+
deviceTypes: [DeviceType.Weather]
|
|
9743
|
+
});
|
|
7826
9744
|
const PerScopeBreakdownSchema = object({
|
|
7827
9745
|
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
7828
9746
|
totalObjects: number().int().nonnegative(),
|
|
@@ -8018,7 +9936,14 @@ const DiscoveryCandidateSchema = object({
|
|
|
8018
9936
|
stableId: string(),
|
|
8019
9937
|
type: _enum(DeviceType),
|
|
8020
9938
|
suggestedName: string(),
|
|
8021
|
-
prefilledConfig: record(string(), unknown())
|
|
9939
|
+
prefilledConfig: record(string(), unknown()),
|
|
9940
|
+
/**
|
|
9941
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
9942
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
9943
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
9944
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
9945
|
+
*/
|
|
9946
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
8022
9947
|
});
|
|
8023
9948
|
const DeviceSummarySchema = object({
|
|
8024
9949
|
id: number(),
|
|
@@ -8029,7 +9954,12 @@ const DeviceSummarySchema = object({
|
|
|
8029
9954
|
parentDeviceId: number().nullable(),
|
|
8030
9955
|
online: boolean(),
|
|
8031
9956
|
features: array(string()),
|
|
8032
|
-
config: record(string(), unknown())
|
|
9957
|
+
config: record(string(), unknown()),
|
|
9958
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
9959
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
9960
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
9961
|
+
* synthetic backfill is in effect. */
|
|
9962
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
8033
9963
|
});
|
|
8034
9964
|
const FieldProbeResultSchema = object({
|
|
8035
9965
|
status: _enum(["ok", "error"]),
|
|
@@ -8208,6 +10138,22 @@ const AlertSchema = object({
|
|
|
8208
10138
|
dismiss: method(object({ alertId: string() }), _void(), { kind: "mutation" })
|
|
8209
10139
|
}
|
|
8210
10140
|
});
|
|
10141
|
+
const ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [
|
|
10142
|
+
object({
|
|
10143
|
+
providerId: string().min(1),
|
|
10144
|
+
displayName: string().min(1),
|
|
10145
|
+
configSchema: unknown(),
|
|
10146
|
+
shouldSaveDiskSpace: literal(true),
|
|
10147
|
+
minFreePercent: number().min(0).max(100)
|
|
10148
|
+
}),
|
|
10149
|
+
object({
|
|
10150
|
+
providerId: string().min(1),
|
|
10151
|
+
displayName: string().min(1),
|
|
10152
|
+
configSchema: unknown(),
|
|
10153
|
+
shouldSaveDiskSpace: literal(false),
|
|
10154
|
+
minFreePercent: literal(null)
|
|
10155
|
+
})
|
|
10156
|
+
]);
|
|
8211
10157
|
({
|
|
8212
10158
|
methods: {
|
|
8213
10159
|
// ── Small-file primitives ────────────────────────────────────────
|
|
@@ -8282,6 +10228,16 @@ const AlertSchema = object({
|
|
|
8282
10228
|
object({ type: StorageLocationTypeSchema }),
|
|
8283
10229
|
StorageLocationSchema.nullable()
|
|
8284
10230
|
),
|
|
10231
|
+
// The admin-UI Data screen renders one group per declared location
|
|
10232
|
+
// (header = `displayName`, "+ Add" shown only for `cardinality:
|
|
10233
|
+
// 'multi'`). Source: the kernel-aggregated `storageLocations`
|
|
10234
|
+
// declarations injected into the orchestrator via `setRegistry`.
|
|
10235
|
+
// No closed type enum in the UI — the screen is fully declaration-
|
|
10236
|
+
// driven.
|
|
10237
|
+
listLocationDeclarations: method(
|
|
10238
|
+
_void(),
|
|
10239
|
+
array(StorageLocationDeclarationSchema).readonly()
|
|
10240
|
+
),
|
|
8285
10241
|
upsertLocation: method(
|
|
8286
10242
|
StorageLocationSchema.omit({ createdAt: true, updatedAt: true }),
|
|
8287
10243
|
StorageLocationSchema,
|
|
@@ -8306,12 +10262,7 @@ const AlertSchema = object({
|
|
|
8306
10262
|
// never changes after boot).
|
|
8307
10263
|
listProviders: method(
|
|
8308
10264
|
_void(),
|
|
8309
|
-
array(
|
|
8310
|
-
providerId: string(),
|
|
8311
|
-
displayName: string(),
|
|
8312
|
-
supportedLocationTypes: array(StorageLocationTypeSchema).readonly(),
|
|
8313
|
-
configSchema: unknown()
|
|
8314
|
-
})).readonly()
|
|
10265
|
+
array(ProviderListEntrySchema).readonly()
|
|
8315
10266
|
),
|
|
8316
10267
|
// Validate a candidate config against a provider BEFORE persisting.
|
|
8317
10268
|
// Used by the wizard so operators can preflight a connection
|
|
@@ -8327,12 +10278,24 @@ const AlertSchema = object({
|
|
|
8327
10278
|
)
|
|
8328
10279
|
}
|
|
8329
10280
|
});
|
|
8330
|
-
const ProviderInfoSchema =
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
10281
|
+
const ProviderInfoSchema = discriminatedUnion("shouldSaveDiskSpace", [
|
|
10282
|
+
object({
|
|
10283
|
+
providerId: string().min(1),
|
|
10284
|
+
displayName: string().min(1),
|
|
10285
|
+
configSchema: unknown(),
|
|
10286
|
+
// Provider manages a finite volume → declares a default free-space threshold.
|
|
10287
|
+
shouldSaveDiskSpace: literal(true),
|
|
10288
|
+
minFreePercent: number().min(0).max(100)
|
|
10289
|
+
}),
|
|
10290
|
+
object({
|
|
10291
|
+
providerId: string().min(1),
|
|
10292
|
+
displayName: string().min(1),
|
|
10293
|
+
configSchema: unknown(),
|
|
10294
|
+
// No local free-space concept (remote/object store) → no threshold.
|
|
10295
|
+
shouldSaveDiskSpace: literal(false),
|
|
10296
|
+
minFreePercent: literal(null)
|
|
10297
|
+
})
|
|
10298
|
+
]);
|
|
8336
10299
|
const TestLocationResultSchema = object({
|
|
8337
10300
|
ok: boolean(),
|
|
8338
10301
|
error: string().optional()
|
|
@@ -8418,6 +10381,35 @@ const EndDownloadInputSchema = object({ downloadId: string() });
|
|
|
8418
10381
|
endDownload: method(EndDownloadInputSchema, _void(), { kind: "mutation" })
|
|
8419
10382
|
}
|
|
8420
10383
|
});
|
|
10384
|
+
const EvictableUsageSchema = object({
|
|
10385
|
+
/** Bytes this provider currently holds on the location (0 if none). */
|
|
10386
|
+
bytes: number().int().nonnegative()
|
|
10387
|
+
});
|
|
10388
|
+
const EvictResultSchema = object({
|
|
10389
|
+
/** Bytes actually reclaimed (only successfully-deleted data counts). */
|
|
10390
|
+
reclaimedBytes: number().int().nonnegative(),
|
|
10391
|
+
/** True when the provider has nothing left it is willing to drop on this location. */
|
|
10392
|
+
exhausted: boolean()
|
|
10393
|
+
});
|
|
10394
|
+
({
|
|
10395
|
+
methods: {
|
|
10396
|
+
/** Bytes this provider holds on the given location — drives proportional fan-out. */
|
|
10397
|
+
getEvictableUsage: method(
|
|
10398
|
+
object({ locationId: string() }),
|
|
10399
|
+
EvictableUsageSchema
|
|
10400
|
+
),
|
|
10401
|
+
/**
|
|
10402
|
+
* Free approximately `targetBytes` of this provider's OWN least-valuable
|
|
10403
|
+
* data on the location (oldest footage, expired clips, …). Returns what it
|
|
10404
|
+
* actually reclaimed + whether it is now exhausted.
|
|
10405
|
+
*/
|
|
10406
|
+
evict: method(
|
|
10407
|
+
object({ locationId: string(), targetBytes: number().int().positive() }),
|
|
10408
|
+
EvictResultSchema,
|
|
10409
|
+
{ kind: "mutation" }
|
|
10410
|
+
)
|
|
10411
|
+
}
|
|
10412
|
+
});
|
|
8421
10413
|
const BackupSubDestinationInfoSchema = object({
|
|
8422
10414
|
/**
|
|
8423
10415
|
* Sub-id within this addon. Convention `default` for single-
|
|
@@ -8628,7 +10620,7 @@ const QueryFilterSchema = object({
|
|
|
8628
10620
|
limit: number().optional(),
|
|
8629
10621
|
offset: number().optional()
|
|
8630
10622
|
});
|
|
8631
|
-
const SettingsRecordSchema = object({
|
|
10623
|
+
const SettingsRecordSchema$1 = object({
|
|
8632
10624
|
id: string(),
|
|
8633
10625
|
data: record(string(), unknown())
|
|
8634
10626
|
});
|
|
@@ -8660,11 +10652,11 @@ const CollectionIndexSchema = object({
|
|
|
8660
10652
|
/** Get all entries matching an optional filter. */
|
|
8661
10653
|
query: method(
|
|
8662
10654
|
object({ namespace: string().optional(), collection: string(), filter: QueryFilterSchema.optional() }),
|
|
8663
|
-
array(SettingsRecordSchema).readonly()
|
|
10655
|
+
array(SettingsRecordSchema$1).readonly()
|
|
8664
10656
|
),
|
|
8665
10657
|
/** Insert a new record. */
|
|
8666
10658
|
insert: method(
|
|
8667
|
-
object({ namespace: string().optional(), collection: string(), record: SettingsRecordSchema }),
|
|
10659
|
+
object({ namespace: string().optional(), collection: string(), record: SettingsRecordSchema$1 }),
|
|
8668
10660
|
_void(),
|
|
8669
10661
|
{ kind: "mutation" }
|
|
8670
10662
|
),
|
|
@@ -8685,6 +10677,18 @@ const CollectionIndexSchema = object({
|
|
|
8685
10677
|
object({ namespace: string().optional(), collection: string(), filter: QueryFilterSchema.optional() }),
|
|
8686
10678
|
number()
|
|
8687
10679
|
),
|
|
10680
|
+
/** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
|
|
10681
|
+
histogram: method(
|
|
10682
|
+
object({
|
|
10683
|
+
namespace: string().optional(),
|
|
10684
|
+
collection: string(),
|
|
10685
|
+
field: string(),
|
|
10686
|
+
bucketSize: number().int().positive(),
|
|
10687
|
+
origin: number().int(),
|
|
10688
|
+
filter: QueryFilterSchema.optional()
|
|
10689
|
+
}),
|
|
10690
|
+
array(object({ bucket: number().int(), count: number().int() })).readonly()
|
|
10691
|
+
),
|
|
8688
10692
|
/** Check if a collection is empty. */
|
|
8689
10693
|
isEmpty: method(
|
|
8690
10694
|
object({ namespace: string().optional(), collection: string() }),
|
|
@@ -9005,6 +11009,297 @@ const SmtpStatusSchema = object({
|
|
|
9005
11009
|
)
|
|
9006
11010
|
}
|
|
9007
11011
|
});
|
|
11012
|
+
const AdoptionFilterSchema = object({
|
|
11013
|
+
id: string(),
|
|
11014
|
+
label: string(),
|
|
11015
|
+
isDefault: boolean().optional()
|
|
11016
|
+
});
|
|
11017
|
+
const CandidateQueryFilterSchema = object({
|
|
11018
|
+
/** Substring filter on name + manufacturer + model. */
|
|
11019
|
+
search: string().optional(),
|
|
11020
|
+
/** Area-name exact match. */
|
|
11021
|
+
area: string().optional(),
|
|
11022
|
+
/** Manufacturer exact match. */
|
|
11023
|
+
manufacturer: string().optional(),
|
|
11024
|
+
/** When true, only return candidates the operator already adopted. */
|
|
11025
|
+
adoptedOnly: boolean().optional(),
|
|
11026
|
+
/** When true, only return candidates the operator hasn't adopted yet. */
|
|
11027
|
+
unadoptedOnly: boolean().optional()
|
|
11028
|
+
});
|
|
11029
|
+
const ListCandidatesInputSchema = object({
|
|
11030
|
+
integrationId: string(),
|
|
11031
|
+
page: number().int().positive().default(1),
|
|
11032
|
+
// Pagination is client-side in the adoption UI: the modal fetches the whole
|
|
11033
|
+
// candidate set in one page and the shared list paginates locally. The cap
|
|
11034
|
+
// stays high enough to return every candidate at once. The large case is the
|
|
11035
|
+
// HA `entities` granularity — one HA device maps to many entities, so an
|
|
11036
|
+
// install can expose several thousand candidates; the ceiling is sized for it.
|
|
11037
|
+
pageSize: number().int().positive().max(2e4).default(50),
|
|
11038
|
+
/**
|
|
11039
|
+
* Optional provider-declared discovery GRANULARITY id (opaque; see
|
|
11040
|
+
* `AdoptionFilterSchema`). Omitted = the reserved `'devices'` granularity =
|
|
11041
|
+
* exactly the pre-existing behavior (fully back-compatible).
|
|
11042
|
+
*/
|
|
11043
|
+
filter: string().optional(),
|
|
11044
|
+
/** Optional candidate-list text/query narrowing within the granularity. */
|
|
11045
|
+
filterText: CandidateQueryFilterSchema.optional()
|
|
11046
|
+
});
|
|
11047
|
+
const ListCandidatesOutputSchema = object({
|
|
11048
|
+
candidates: array(DiscoveredChildDeviceSchema).readonly(),
|
|
11049
|
+
totalCount: number().int().nonnegative(),
|
|
11050
|
+
page: number().int().positive(),
|
|
11051
|
+
pageSize: number().int().positive()
|
|
11052
|
+
});
|
|
11053
|
+
const GetCandidateInputSchema = object({
|
|
11054
|
+
integrationId: string(),
|
|
11055
|
+
childNativeId: string()
|
|
11056
|
+
});
|
|
11057
|
+
const AdoptionStatusSchema = object({
|
|
11058
|
+
/** Last refresh timestamp (ms epoch) — null when never refreshed. */
|
|
11059
|
+
lastDiscoveryAt: number().int().nonnegative().nullable(),
|
|
11060
|
+
/** Count of candidates in the discovery cache. */
|
|
11061
|
+
candidateCount: number().int().nonnegative(),
|
|
11062
|
+
/** Count of candidates the operator has already adopted. */
|
|
11063
|
+
adoptedCount: number().int().nonnegative(),
|
|
11064
|
+
/** Last error message from a refresh attempt. */
|
|
11065
|
+
lastError: string().nullable()
|
|
11066
|
+
});
|
|
11067
|
+
const PerCandidateSchema = object({
|
|
11068
|
+
/** Override the default display name for this candidate's parent. */
|
|
11069
|
+
name: string().min(1).optional(),
|
|
11070
|
+
/** Pre-hide a subset of entity-children — created (state still flows)
|
|
11071
|
+
* but listed in the parent's `accessories.hiddenChildIds`. */
|
|
11072
|
+
hiddenChildIds: array(string()).optional()
|
|
11073
|
+
});
|
|
11074
|
+
const AdoptInputSchema = object({
|
|
11075
|
+
integrationId: string(),
|
|
11076
|
+
/**
|
|
11077
|
+
* Candidate native ids to adopt. Their MEANING is filter-relative: under the
|
|
11078
|
+
* default `'devices'` granularity these are device-native ids; under a
|
|
11079
|
+
* provider-declared granularity (e.g. HA `'entities'`) they are that
|
|
11080
|
+
* granularity's native ids (e.g. entity ids). The field name is kept stable
|
|
11081
|
+
* to avoid a breaking rename.
|
|
11082
|
+
*/
|
|
11083
|
+
childNativeIds: array(string()).min(1),
|
|
11084
|
+
/**
|
|
11085
|
+
* Optional provider-declared discovery GRANULARITY id (opaque; see
|
|
11086
|
+
* `AdoptionFilterSchema`). Omitted = the reserved `'devices'` granularity =
|
|
11087
|
+
* exactly the pre-existing behavior (fully back-compatible).
|
|
11088
|
+
*/
|
|
11089
|
+
filter: string().optional(),
|
|
11090
|
+
/** When true, import each adopted device's source-system location (e.g. HA
|
|
11091
|
+
* area) into CamStack — fuzzy-match an existing location or create it, then
|
|
11092
|
+
* assign. Omitted/false = no location work (back-compat). */
|
|
11093
|
+
importLocations: boolean().optional(),
|
|
11094
|
+
perCandidate: record(string(), PerCandidateSchema).optional()
|
|
11095
|
+
});
|
|
11096
|
+
const AdoptResultSchema = object({
|
|
11097
|
+
adopted: array(
|
|
11098
|
+
object({
|
|
11099
|
+
childNativeId: string(),
|
|
11100
|
+
parentDeviceId: number().int().nonnegative(),
|
|
11101
|
+
accessoryDeviceIds: array(number().int().nonnegative()).readonly()
|
|
11102
|
+
})
|
|
11103
|
+
).readonly()
|
|
11104
|
+
});
|
|
11105
|
+
const ReleaseInputSchema = object({
|
|
11106
|
+
integrationId: string(),
|
|
11107
|
+
/** Parent CamStack device id (NOT an accessory child id). Removing
|
|
11108
|
+
* the parent cascades into every accessory. */
|
|
11109
|
+
camDeviceId: number().int().nonnegative()
|
|
11110
|
+
});
|
|
11111
|
+
const ResyncInputSchema = object({
|
|
11112
|
+
/** Parent CamStack device id of an adopted device. The provider resolves its
|
|
11113
|
+
* source (integration/broker + native id) and re-aligns the device's
|
|
11114
|
+
* structural spec (type/role/capabilities/units) with the live mapping,
|
|
11115
|
+
* rebuilding any child whose class changed while preserving operator edits. */
|
|
11116
|
+
camDeviceId: number().int().nonnegative()
|
|
11117
|
+
});
|
|
11118
|
+
const ResyncResultSchema = object({
|
|
11119
|
+
/** True when the persisted spec actually changed (children may have been rebuilt). */
|
|
11120
|
+
changed: boolean(),
|
|
11121
|
+
/** Number of child devices rebuilt into a new class by this re-sync. */
|
|
11122
|
+
rebuiltChildren: number().int().nonnegative()
|
|
11123
|
+
});
|
|
11124
|
+
({
|
|
11125
|
+
methods: {
|
|
11126
|
+
listCandidateFilters: method(
|
|
11127
|
+
object({ integrationId: string() }),
|
|
11128
|
+
object({ filters: array(AdoptionFilterSchema) }),
|
|
11129
|
+
{ auth: "admin" }
|
|
11130
|
+
),
|
|
11131
|
+
listCandidates: method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }),
|
|
11132
|
+
getCandidate: method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }),
|
|
11133
|
+
refresh: method(object({ integrationId: string() }), AdoptionStatusSchema, { kind: "mutation", auth: "admin" }),
|
|
11134
|
+
adopt: method(AdoptInputSchema, AdoptResultSchema, { kind: "mutation", auth: "admin" }),
|
|
11135
|
+
release: method(ReleaseInputSchema, _void(), { kind: "mutation", auth: "admin" }),
|
|
11136
|
+
resync: method(ResyncInputSchema, ResyncResultSchema, { kind: "mutation", auth: "admin" })
|
|
11137
|
+
}
|
|
11138
|
+
});
|
|
11139
|
+
const BrokerStatusEnum = _enum([
|
|
11140
|
+
"connected",
|
|
11141
|
+
"disconnected",
|
|
11142
|
+
"connecting",
|
|
11143
|
+
"auth-failed",
|
|
11144
|
+
"unreachable",
|
|
11145
|
+
"error"
|
|
11146
|
+
]);
|
|
11147
|
+
const BrokerInfoSchema$1 = object({
|
|
11148
|
+
/** Stable broker id. Persisted; survives addon restarts. */
|
|
11149
|
+
id: string(),
|
|
11150
|
+
/** Addon id of the provider that OWNS this broker.
|
|
11151
|
+
*
|
|
11152
|
+
* The `broker` cap is a system-scoped collection: several addons
|
|
11153
|
+
* register a `broker` provider, each owning a DISJOINT set of
|
|
11154
|
+
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
11155
|
+
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
11156
|
+
* device belongs to one integration. The admin UI threads this id
|
|
11157
|
+
* back as the `{ addonId }` system-collection selector on every
|
|
11158
|
+
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
11159
|
+
* `testConnection`), so the call routes to the OWNING provider
|
|
11160
|
+
* instead of defaulting to the first-registered one. */
|
|
11161
|
+
addonId: string(),
|
|
11162
|
+
/** Human-readable name (operator-chosen at add-time). */
|
|
11163
|
+
name: string(),
|
|
11164
|
+
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
11165
|
+
kind: string(),
|
|
11166
|
+
status: BrokerStatusEnum,
|
|
11167
|
+
/** Free-form provider-specific info (HA version, MQTT broker
|
|
11168
|
+
* flavour, latency, mTLS active, …). */
|
|
11169
|
+
info: record(string(), unknown()),
|
|
11170
|
+
/** Ms epoch of the last connection probe / status update. */
|
|
11171
|
+
lastCheckedAt: number().nullable(),
|
|
11172
|
+
/** Last error message, when `status` indicates failure. */
|
|
11173
|
+
error: string().nullable()
|
|
11174
|
+
});
|
|
11175
|
+
const RegistryStatusSchema = object({
|
|
11176
|
+
brokerCount: number().int().nonnegative(),
|
|
11177
|
+
connectedCount: number().int().nonnegative()
|
|
11178
|
+
});
|
|
11179
|
+
const BrokerProviderInfoSchema = object({
|
|
11180
|
+
/** Addon id of the `broker` provider this entry describes. */
|
|
11181
|
+
addonId: string(),
|
|
11182
|
+
/** Broker kinds this provider can create, with a display label. */
|
|
11183
|
+
kinds: array(object({ kind: string(), label: string() }))
|
|
11184
|
+
});
|
|
11185
|
+
const ListInputSchema = object({
|
|
11186
|
+
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
11187
|
+
* only HA brokers. Omit to list every broker. */
|
|
11188
|
+
kind: string().optional()
|
|
11189
|
+
});
|
|
11190
|
+
const GetInputSchema = object({ id: string() });
|
|
11191
|
+
const AddInputSchema = object({
|
|
11192
|
+
kind: string().min(1),
|
|
11193
|
+
name: string().min(1),
|
|
11194
|
+
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
11195
|
+
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
11196
|
+
* branch on receipt — invalid shape rejects the add. */
|
|
11197
|
+
settings: record(string(), unknown())
|
|
11198
|
+
});
|
|
11199
|
+
const AddResultSchema = object({ id: string() });
|
|
11200
|
+
const RemoveInputSchema = object({ id: string() });
|
|
11201
|
+
const TestConnectionResultSchema$1 = discriminatedUnion("ok", [
|
|
11202
|
+
object({ ok: literal(true), latencyMs: number().nonnegative() }),
|
|
11203
|
+
object({ ok: literal(false), error: string() })
|
|
11204
|
+
]);
|
|
11205
|
+
const SettingsRecordSchema = record(string(), unknown());
|
|
11206
|
+
const SettingsSchemaInputSchema = object({ kind: string() });
|
|
11207
|
+
const TestSettingsInputSchema = object({
|
|
11208
|
+
kind: string(),
|
|
11209
|
+
settings: SettingsRecordSchema
|
|
11210
|
+
});
|
|
11211
|
+
const TestSettingsResultSchema = discriminatedUnion("ok", [
|
|
11212
|
+
// `.strict()` on the success branch so a stray `error` field is rejected,
|
|
11213
|
+
// not silently stripped — a success result must never carry an error.
|
|
11214
|
+
object({ ok: literal(true), latencyMs: number().nonnegative().optional() }).strict(),
|
|
11215
|
+
object({ ok: literal(false), error: string() })
|
|
11216
|
+
]);
|
|
11217
|
+
const SettingsSchemaResultSchema = unknown().nullable();
|
|
11218
|
+
const PublishInputSchema = object({
|
|
11219
|
+
brokerId: string(),
|
|
11220
|
+
/** Kind-specific routing target.
|
|
11221
|
+
* MQTT: `{ topic: string, qos?: 0|1|2, retain?: boolean }`.
|
|
11222
|
+
* HA: `{ domain: string, service: string, entityId?: string, area?: string }`. */
|
|
11223
|
+
target: record(string(), unknown()),
|
|
11224
|
+
/** Kind-specific payload.
|
|
11225
|
+
* MQTT: raw string / number / object (provider serialises).
|
|
11226
|
+
* HA: service-call `data` object (transition, brightness, …). */
|
|
11227
|
+
payload: unknown().optional()
|
|
11228
|
+
});
|
|
11229
|
+
const SubscribeInputSchema = object({
|
|
11230
|
+
brokerId: string(),
|
|
11231
|
+
/** Kind-specific filter.
|
|
11232
|
+
* MQTT: `{ topic: string, qos?: 0|1|2 }` — topic can include wildcards.
|
|
11233
|
+
* HA: `{ entityIds: string[] }` or `{ domain: string }`. */
|
|
11234
|
+
filter: record(string(), unknown())
|
|
11235
|
+
});
|
|
11236
|
+
const SubscribeResultSchema = object({
|
|
11237
|
+
/** Stable subscription id. Used to unsubscribe and to filter the
|
|
11238
|
+
* matching `broker.message` events on the bus. */
|
|
11239
|
+
subscriptionId: string()
|
|
11240
|
+
});
|
|
11241
|
+
const UnsubscribeInputSchema = object({
|
|
11242
|
+
brokerId: string(),
|
|
11243
|
+
subscriptionId: string()
|
|
11244
|
+
});
|
|
11245
|
+
const GetStateInputSchema = object({
|
|
11246
|
+
brokerId: string(),
|
|
11247
|
+
/** Kind-specific lookup key.
|
|
11248
|
+
* MQTT: topic string (returns the last retained message).
|
|
11249
|
+
* HA: entity_id (returns the cached entity state). */
|
|
11250
|
+
key: string()
|
|
11251
|
+
});
|
|
11252
|
+
({
|
|
11253
|
+
methods: {
|
|
11254
|
+
// ── Registry CRUD ────────────────────────────────────────────────
|
|
11255
|
+
list: method(ListInputSchema, array(BrokerInfoSchema$1)),
|
|
11256
|
+
get: method(GetInputSchema, BrokerInfoSchema$1.nullable()),
|
|
11257
|
+
/** Enumerate which addon provides which broker kind(s) for the
|
|
11258
|
+
* unified create picker. The auto-mount fans this array across
|
|
11259
|
+
* every registered `broker` provider (array-output method), so the
|
|
11260
|
+
* picker sees every kind from every provider in one call. */
|
|
11261
|
+
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
11262
|
+
add: method(AddInputSchema, AddResultSchema, { kind: "mutation", auth: "admin" }),
|
|
11263
|
+
remove: method(RemoveInputSchema, _void(), { kind: "mutation", auth: "admin" }),
|
|
11264
|
+
testConnection: method(GetInputSchema, TestConnectionResultSchema$1, { kind: "mutation", auth: "admin" }),
|
|
11265
|
+
// ── Settings ─────────────────────────────────────────────────────
|
|
11266
|
+
/** Read the persisted settings record for a broker (kind-specific
|
|
11267
|
+
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
11268
|
+
* when the broker id is unknown to the provider (the collection
|
|
11269
|
+
* fallback may route a foreign id to the first provider). */
|
|
11270
|
+
getSettings: method(GetInputSchema, SettingsRecordSchema.nullable(), { auth: "admin" }),
|
|
11271
|
+
/** Overwrite the persisted settings record. The kind-specific
|
|
11272
|
+
* provider validates the shape and applies the change (reconnects
|
|
11273
|
+
* if credentials changed). */
|
|
11274
|
+
setSettings: method(
|
|
11275
|
+
object({ id: string(), settings: SettingsRecordSchema }),
|
|
11276
|
+
_void(),
|
|
11277
|
+
{ kind: "mutation", auth: "admin" }
|
|
11278
|
+
),
|
|
11279
|
+
// ── Connection-config (for consumers that open their own client) ─
|
|
11280
|
+
/** Returns the kind-specific connection config the consumer needs
|
|
11281
|
+
* to open its own client (MQTT pattern: `{url, username, password,
|
|
11282
|
+
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
11283
|
+
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
11284
|
+
* Returns `null` when the broker id is unknown to the provider. */
|
|
11285
|
+
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema.nullable(), { auth: "admin" }),
|
|
11286
|
+
// ── Per-kind creation schema + pre-creation test ─────────────────
|
|
11287
|
+
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
11288
|
+
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, { kind: "mutation", auth: "admin" }),
|
|
11289
|
+
// ── Pub/sub primitives ───────────────────────────────────────────
|
|
11290
|
+
publish: method(PublishInputSchema, unknown(), { kind: "mutation", auth: "admin" }),
|
|
11291
|
+
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, { kind: "mutation", auth: "admin" }),
|
|
11292
|
+
unsubscribe: method(UnsubscribeInputSchema, _void(), { kind: "mutation", auth: "admin" }),
|
|
11293
|
+
/** Read the broker's cached state for a key. Returns `null` when
|
|
11294
|
+
* unknown to the broker (never published / unknown entity). */
|
|
11295
|
+
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
11296
|
+
/** Status method — explicit registration with a `z.void()` input so
|
|
11297
|
+
* the codegen-generated tRPC router types its input as
|
|
11298
|
+
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
11299
|
+
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
11300
|
+
getStatus: method(_void(), RegistryStatusSchema)
|
|
11301
|
+
}
|
|
11302
|
+
});
|
|
9008
11303
|
const BrokerKindSchema = _enum(["external", "embedded"]);
|
|
9009
11304
|
const BrokerStatusSchema = _enum([
|
|
9010
11305
|
"connected",
|
|
@@ -9518,7 +11813,15 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9518
11813
|
* direct (host/srflx) path. Clients MUST NOT send this — the
|
|
9519
11814
|
* server overwrites it from the request context.
|
|
9520
11815
|
*/
|
|
9521
|
-
relayOnly: boolean().optional()
|
|
11816
|
+
relayOnly: boolean().optional(),
|
|
11817
|
+
/**
|
|
11818
|
+
* Subscriber attribution surfaced in the stream-broker widget's
|
|
11819
|
+
* client list. Callers (Alexa addon, browser hooks, WHEP server)
|
|
11820
|
+
* populate `kind` and any of `label`/`userId`/`sessionId`/
|
|
11821
|
+
* `remoteAddr` they know. The hub layer enriches `remoteAddr`
|
|
11822
|
+
* from the tRPC request context.
|
|
11823
|
+
*/
|
|
11824
|
+
consumerAttribution: BrokerConsumerAttributionSchema.optional()
|
|
9522
11825
|
}),
|
|
9523
11826
|
object({ sessionId: string(), sdpOffer: string() }),
|
|
9524
11827
|
{ kind: "mutation" }
|
|
@@ -9556,7 +11859,62 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9556
11859
|
* Untrusted browser clients MUST NOT send it — the hub overwrites
|
|
9557
11860
|
* it from the request context.
|
|
9558
11861
|
*/
|
|
9559
|
-
relayOnly: boolean().optional()
|
|
11862
|
+
relayOnly: boolean().optional(),
|
|
11863
|
+
/**
|
|
11864
|
+
* Force a NON-TRICKLE answer: the server awaits ICE gathering to
|
|
11865
|
+
* COMPLETE and returns a full-candidate answer SDP (with `a=candidate`
|
|
11866
|
+
* lines) instead of the default bare/trickle answer.
|
|
11867
|
+
*
|
|
11868
|
+
* Set `true` by callers that DON'T support trickle ICE — notably
|
|
11869
|
+
* Alexa's RTCSessionController, which never polls `getIceCandidates`
|
|
11870
|
+
* nor trickles back via `addIceCandidate`, so without server
|
|
11871
|
+
* candidates in the answer SDP its ICE agent stalls (no nominated
|
|
11872
|
+
* pair → DTLS never connects → Echo shows black → SessionDisconnected).
|
|
11873
|
+
*
|
|
11874
|
+
* Browser viewers leave it absent/false: they poll `getIceCandidates`
|
|
11875
|
+
* and trickle, so the bare answer lets media start in ~0s instead of
|
|
11876
|
+
* blocking on full gathering.
|
|
11877
|
+
*/
|
|
11878
|
+
nonTrickle: boolean().optional(),
|
|
11879
|
+
/**
|
|
11880
|
+
* Suppress TURN servers from the per-session iceConfig: the answer
|
|
11881
|
+
* will carry ONLY host + srflx candidates, never a relay one.
|
|
11882
|
+
*
|
|
11883
|
+
* Set `true` by peers that are themselves directly reachable on the
|
|
11884
|
+
* public internet (Alexa's RTCSessionController media gateway is
|
|
11885
|
+
* AWS-hosted with public host candidates). Including a TURN relay
|
|
11886
|
+
* for such a peer is counterproductive: (a) gathering blocks on the
|
|
11887
|
+
* TURN allocation round-trip and slows the answer; (b) werift
|
|
11888
|
+
* spends ICE-checking budget on a relay-pair that the peer cannot
|
|
11889
|
+
* use, sometimes stalling the host/srflx pair validation enough
|
|
11890
|
+
* that Echo's agent times out before nominating one — visible as
|
|
11891
|
+
* 'ICE checking → DTLS never connected'. Browser viewers leave it
|
|
11892
|
+
* absent/false so the relay candidate remains as a fallback for
|
|
11893
|
+
* symmetric-NAT clients.
|
|
11894
|
+
*
|
|
11895
|
+
* Independent from `relayOnly`. `relayOnly: true` forces ICE to a
|
|
11896
|
+
* relay-only pair set; `disableTurn: true` strips the relay
|
|
11897
|
+
* candidate entirely. They are mutually exclusive in practice but
|
|
11898
|
+
* not policed at the schema level — callers MUST NOT set both.
|
|
11899
|
+
*/
|
|
11900
|
+
disableTurn: boolean().optional(),
|
|
11901
|
+
/**
|
|
11902
|
+
* Suppress IPv6 host candidates from gathering and from
|
|
11903
|
+
* `additionalHostAddresses`. The answer SDP will contain only IPv4
|
|
11904
|
+
* host/srflx (and TURN, unless `disableTurn` is also set).
|
|
11905
|
+
*
|
|
11906
|
+
* Set `true` by peers that document IPv4-only ICE — Amazon's
|
|
11907
|
+
* `Alexa.RTCSessionController` spec states: "For ICE candidates,
|
|
11908
|
+
* you can use either UDP or TCP but you must use IPv4." Echo's
|
|
11909
|
+
* media gateway ignores IPv6 candidates and they consume budget
|
|
11910
|
+
* against the gateway's documented 6-second SDP processing limit.
|
|
11911
|
+
*
|
|
11912
|
+
* Browser viewers leave it absent/false so IPv6-only paths
|
|
11913
|
+
* (Tailscale, native dual-stack) remain available.
|
|
11914
|
+
*/
|
|
11915
|
+
disableIpv6: boolean().optional(),
|
|
11916
|
+
/** Subscriber attribution. See `createSession` for the contract. */
|
|
11917
|
+
consumerAttribution: BrokerConsumerAttributionSchema.optional()
|
|
9560
11918
|
}),
|
|
9561
11919
|
object({ sessionId: string(), sdpAnswer: string() }),
|
|
9562
11920
|
{ kind: "mutation" }
|
|
@@ -9575,7 +11933,7 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9575
11933
|
* Lets the client send its SDP offer/answer IMMEDIATELY (before ICE
|
|
9576
11934
|
* gathering finishes) and deliver candidates as they arrive, so the
|
|
9577
11935
|
* connection establishes in ~0s instead of waiting for full gathering.
|
|
9578
|
-
* The dual of `getIceCandidates
|
|
11936
|
+
* The dual of `getIceCandidates` in the trickle-ICE signaling exchange.
|
|
9579
11937
|
*/
|
|
9580
11938
|
addIceCandidate: method(
|
|
9581
11939
|
object({
|
|
@@ -9625,6 +11983,18 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9625
11983
|
profile: CamProfileSchema
|
|
9626
11984
|
}),
|
|
9627
11985
|
boolean()
|
|
11986
|
+
),
|
|
11987
|
+
/** Poll the live signaling state of a session. `pendingRenegotiation` is
|
|
11988
|
+
* non-null after an adaptive tier switch — the client re-offers on the
|
|
11989
|
+
* same sessionId; `epoch` guards against acting on a stale poll twice. */
|
|
11990
|
+
getSessionState: method(
|
|
11991
|
+
object({
|
|
11992
|
+
deviceId: number().int().nonnegative(),
|
|
11993
|
+
sessionId: string()
|
|
11994
|
+
}),
|
|
11995
|
+
object({
|
|
11996
|
+
pendingRenegotiation: object({ target: WebrtcStreamTargetSchema, epoch: number() }).nullable()
|
|
11997
|
+
})
|
|
9628
11998
|
)
|
|
9629
11999
|
}
|
|
9630
12000
|
});
|
|
@@ -10237,6 +12607,26 @@ const embeddingEncoderCapability = {
|
|
|
10237
12607
|
getInfo: method(_void(), EmbeddingInfoSchema)
|
|
10238
12608
|
}
|
|
10239
12609
|
};
|
|
12610
|
+
const ChildLayoutEntrySchema = object({
|
|
12611
|
+
childKey: string(),
|
|
12612
|
+
section: string(),
|
|
12613
|
+
order: number().optional(),
|
|
12614
|
+
// Section-level default-collapsed hint: when true the accordion section this
|
|
12615
|
+
// entry belongs to renders closed by default. The section is treated as
|
|
12616
|
+
// collapsed if ANY of its entries sets collapsed:true; consistent values
|
|
12617
|
+
// across a section's entries are recommended but not enforced. Absent ⇒ open.
|
|
12618
|
+
collapsed: boolean().optional()
|
|
12619
|
+
});
|
|
12620
|
+
const DeviceLinkSchema = object({
|
|
12621
|
+
id: string(),
|
|
12622
|
+
source: object({ sourceKey: string(), cap: string(), fieldPath: string() }),
|
|
12623
|
+
target: object({ cap: string(), fieldPath: string(), itemKey: string().optional() }),
|
|
12624
|
+
transform: discriminatedUnion("kind", [
|
|
12625
|
+
object({ kind: literal("identity") }),
|
|
12626
|
+
object({ kind: literal("enum-map"), mapping: record(string(), union([string(), number(), boolean()])), fallback: union([string(), number(), boolean()]).optional() }),
|
|
12627
|
+
object({ kind: literal("linear"), scale: number(), offset: number(), clamp: tuple([number(), number()]).readonly().optional() })
|
|
12628
|
+
]).optional()
|
|
12629
|
+
});
|
|
10240
12630
|
const DeviceInfoSchema = object({
|
|
10241
12631
|
/** Progressive, system-wide unique number. Allocated synchronously by
|
|
10242
12632
|
* `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
|
|
@@ -10256,6 +12646,12 @@ const DeviceInfoSchema = object({
|
|
|
10256
12646
|
/** Optional semantic role — `DeviceRole` string. null for top-level devices. */
|
|
10257
12647
|
role: string().nullable().optional(),
|
|
10258
12648
|
online: boolean(),
|
|
12649
|
+
/** True when the device's initial feature-probe has completed (or it has
|
|
12650
|
+
* no probe) — exported shape is stable; exporters gate advertise on this.
|
|
12651
|
+
* Optional for deploy-order resilience: a record produced by an older
|
|
12652
|
+
* device-manager build omits it, and consumers treat absent as "not ready"
|
|
12653
|
+
* (export gate carries forward, never advertises a partial shape). */
|
|
12654
|
+
probed: boolean().optional(),
|
|
10259
12655
|
features: array(string()),
|
|
10260
12656
|
/** true when the device has a getStreamSources() method (ICameraDevice) */
|
|
10261
12657
|
isCamera: boolean(),
|
|
@@ -10263,7 +12659,25 @@ const DeviceInfoSchema = object({
|
|
|
10263
12659
|
config: record(string(), unknown()),
|
|
10264
12660
|
/** Hardware + identity blob (manufacturer / model / firmware / sn /
|
|
10265
12661
|
* uid / mac / …). Populated by drivers; editable via setMetadata. */
|
|
10266
|
-
metadata: record(string(), unknown()).nullable().optional()
|
|
12662
|
+
metadata: record(string(), unknown()).nullable().optional(),
|
|
12663
|
+
/** Optional upstream-system identity + rendering envelope. See
|
|
12664
|
+
* `SourceInfo` in `packages/types/src/device/source-info.ts`. */
|
|
12665
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
12666
|
+
/** Owning integration id, stamped at create — used by the core to
|
|
12667
|
+
* cascade-delete an integration's devices. Optional: only present
|
|
12668
|
+
* when the device was created through an integration (e.g. HA). */
|
|
12669
|
+
integrationId: string().optional(),
|
|
12670
|
+
linkDeviceId: number().nullable().optional(),
|
|
12671
|
+
/** Durable primary-child override for a CONTAINER device, keyed on the
|
|
12672
|
+
* chosen child's re-sync/rename-stable `entityId`. Survives a re-sync
|
|
12673
|
+
* that reallocates the child's numeric id. `null` clears the override. */
|
|
12674
|
+
primaryChildEntityId: string().nullable().optional(),
|
|
12675
|
+
/** Container-level layout hint: assigns specific child/accessory devices to
|
|
12676
|
+
* named accordion sections (with optional intra-section order). See
|
|
12677
|
+
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
12678
|
+
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
12679
|
+
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
12680
|
+
deviceLinks: array(DeviceLinkSchema).readonly().optional()
|
|
10267
12681
|
});
|
|
10268
12682
|
const ConfigEntrySchema$1 = object({
|
|
10269
12683
|
key: string(),
|
|
@@ -10294,7 +12708,32 @@ const DeviceMetaSchema = object({
|
|
|
10294
12708
|
location: string().nullable(),
|
|
10295
12709
|
disabled: boolean(),
|
|
10296
12710
|
parentDeviceId: number().nullable(),
|
|
10297
|
-
metadata: record(string(), unknown()).nullable().optional()
|
|
12711
|
+
metadata: record(string(), unknown()).nullable().optional(),
|
|
12712
|
+
/** Optional upstream-system identity + rendering envelope. See
|
|
12713
|
+
* `SourceInfo`. Present when the device has been persisted with a
|
|
12714
|
+
* non-synthetic source identifier (HA entities, vendor MAC, …);
|
|
12715
|
+
* omitted when the synthetic `{ id: stableId, system: addonId }`
|
|
12716
|
+
* fallback is in effect. Persisted under
|
|
12717
|
+
* `DeviceMeta.metadata.sourceInfo` — the field on this schema is
|
|
12718
|
+
* the typed projection callers consume directly. */
|
|
12719
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
12720
|
+
/** Owning integration id, stamped at create — used by the core to
|
|
12721
|
+
* cascade-delete an integration's devices. Optional: only present
|
|
12722
|
+
* when the device was created through an integration (e.g. HA). */
|
|
12723
|
+
integrationId: string().optional(),
|
|
12724
|
+
linkDeviceId: number().nullable().optional(),
|
|
12725
|
+
/** Durable primary-child override for a CONTAINER device, keyed on the
|
|
12726
|
+
* chosen child's re-sync/rename-stable `entityId`. See `DeviceMeta`. */
|
|
12727
|
+
primaryChildEntityId: string().nullable().optional(),
|
|
12728
|
+
/** Container-level layout hint: assigns child/accessory devices to named
|
|
12729
|
+
* accordion sections (with optional intra-section order). See
|
|
12730
|
+
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
12731
|
+
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
12732
|
+
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
12733
|
+
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
12734
|
+
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
12735
|
+
* Optional: only present for accessory children that carry a known role. */
|
|
12736
|
+
role: string().nullable().optional()
|
|
10298
12737
|
});
|
|
10299
12738
|
const ConfigUISchemaOutput = unknown().nullable();
|
|
10300
12739
|
const StreamProbeResultSchema = object({
|
|
@@ -10388,6 +12827,109 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10388
12827
|
_void(),
|
|
10389
12828
|
{ kind: "mutation", auth: "admin" }
|
|
10390
12829
|
),
|
|
12830
|
+
/** Update the device type. Writes the meta row, emits a
|
|
12831
|
+
* `DeviceMetaChanged` event. Used by the kernel to apply
|
|
12832
|
+
* `initialMeta.type` before device construction so the device
|
|
12833
|
+
* constructs with its real type instead of the allocateDeviceId
|
|
12834
|
+
* placeholder `'generic'`. */
|
|
12835
|
+
setType: method(
|
|
12836
|
+
object({ deviceId: number(), type: _enum(DeviceType) }),
|
|
12837
|
+
_void(),
|
|
12838
|
+
{ kind: "mutation", auth: "admin" }
|
|
12839
|
+
),
|
|
12840
|
+
/** Stamp (or update) the owning integration id on the device's meta
|
|
12841
|
+
* row. Called by the kernel's `create()` pre-seed path when
|
|
12842
|
+
* `initialMeta.integrationId` is set (analogous to `setName` /
|
|
12843
|
+
* `setType`). Idempotent. */
|
|
12844
|
+
setIntegrationId: method(
|
|
12845
|
+
object({ deviceId: number(), integrationId: string() }),
|
|
12846
|
+
_void(),
|
|
12847
|
+
{ kind: "mutation", auth: "admin" }
|
|
12848
|
+
),
|
|
12849
|
+
/** Stamp (or update) the soft-link device id on the device's meta
|
|
12850
|
+
* row. Called by the kernel `create()` pre-seed when
|
|
12851
|
+
* `initialMeta.linkDeviceId !== undefined`. Idempotent. */
|
|
12852
|
+
setLinkDeviceId: method(
|
|
12853
|
+
object({ deviceId: number(), linkDeviceId: number().nullable() }),
|
|
12854
|
+
_void(),
|
|
12855
|
+
{ kind: "mutation", auth: "admin" }
|
|
12856
|
+
),
|
|
12857
|
+
/** Set (or clear) the durable primary-child override on a CONTAINER
|
|
12858
|
+
* device's meta row, keyed on the chosen child's re-sync/rename-stable
|
|
12859
|
+
* `entityId`. Unlike `setLinkDeviceId` (numeric child id, goes stale on
|
|
12860
|
+
* re-sync), this survives a re-sync that reallocates the child's numeric
|
|
12861
|
+
* id. `null` clears the override → priority default. Idempotent. */
|
|
12862
|
+
setPrimaryChildEntityId: method(
|
|
12863
|
+
object({ deviceId: number(), primaryChildEntityId: string().nullable() }),
|
|
12864
|
+
_void(),
|
|
12865
|
+
{ kind: "mutation", auth: "admin" }
|
|
12866
|
+
),
|
|
12867
|
+
/** Set (or replace) the container-level `childLayout` on a device's meta
|
|
12868
|
+
* row — assigns specific child/accessory devices to named accordion
|
|
12869
|
+
* sections (with optional intra-section order). Mirrors
|
|
12870
|
+
* `setPrimaryChildEntityId`; the layout is parent-only. Persisted,
|
|
12871
|
+
* projected, and preserved across re-register/restore. Idempotent. */
|
|
12872
|
+
setChildLayout: method(
|
|
12873
|
+
object({ deviceId: number(), childLayout: array(ChildLayoutEntrySchema).readonly() }),
|
|
12874
|
+
_void(),
|
|
12875
|
+
{ kind: "mutation", auth: "admin" }
|
|
12876
|
+
),
|
|
12877
|
+
/** Set (or replace) the cross-device field wirings on a device's meta row.
|
|
12878
|
+
* Mirrors `setChildLayout`; persisted, projected, preserved across
|
|
12879
|
+
* re-register/restore. Idempotent. */
|
|
12880
|
+
setDeviceLinks: method(
|
|
12881
|
+
object({ deviceId: number(), deviceLinks: array(DeviceLinkSchema).readonly() }),
|
|
12882
|
+
_void(),
|
|
12883
|
+
{ kind: "mutation", auth: "admin" }
|
|
12884
|
+
),
|
|
12885
|
+
/** List the wireable status-schema fields per cap bound to a device.
|
|
12886
|
+
* Powers the Wiring tab's field pickers. Caps without a status schema are omitted. */
|
|
12887
|
+
getWireableFields: method(
|
|
12888
|
+
object({ deviceId: number() }),
|
|
12889
|
+
object({
|
|
12890
|
+
caps: array(object({
|
|
12891
|
+
cap: string(),
|
|
12892
|
+
fields: array(object({
|
|
12893
|
+
path: string(),
|
|
12894
|
+
kind: _enum(["string", "number", "boolean", "enum"]),
|
|
12895
|
+
enumValues: array(string()).optional()
|
|
12896
|
+
})).readonly()
|
|
12897
|
+
})).readonly()
|
|
12898
|
+
}),
|
|
12899
|
+
{ kind: "query" }
|
|
12900
|
+
),
|
|
12901
|
+
/** Stamp (or update) the semantic role on the device's meta row.
|
|
12902
|
+
* Called by the kernel's `create()` / `spawnAccessoryChild` pre-seed
|
|
12903
|
+
* path when `initialMeta.role` is set (analogous to `setIntegrationId`).
|
|
12904
|
+
* Idempotent. `null` explicitly clears a previous role. */
|
|
12905
|
+
setRole: method(
|
|
12906
|
+
object({ deviceId: number(), role: string().nullable() }),
|
|
12907
|
+
_void(),
|
|
12908
|
+
{ kind: "mutation", auth: "admin" }
|
|
12909
|
+
),
|
|
12910
|
+
/** Batched meta pre-seed — applies every provided field to the device's
|
|
12911
|
+
* meta row in ONE settings round-trip (one lock acquisition, one
|
|
12912
|
+
* `deviceMeta` write) and emits one `DeviceMetaChanged` event per field
|
|
12913
|
+
* that was supplied. Each field carries the SAME semantics as its
|
|
12914
|
+
* individual setter (`setName` / `setLocation` / `setType` /
|
|
12915
|
+
* `setIntegrationId` / `setLinkDeviceId` / `setRole`) — omitted fields are
|
|
12916
|
+
* left untouched; `null` clears `location` / `linkDeviceId` / `role`.
|
|
12917
|
+
* Idempotent. Used by the kernel's `spawnAccessoryChild` flow to collapse
|
|
12918
|
+
* the per-child meta pre-seed (4-6 individual setter calls) into one,
|
|
12919
|
+
* which is the dominant cost when reconciling a many-entity container. */
|
|
12920
|
+
applyInitialMeta: method(
|
|
12921
|
+
object({
|
|
12922
|
+
deviceId: number(),
|
|
12923
|
+
name: string().optional(),
|
|
12924
|
+
location: string().nullable().optional(),
|
|
12925
|
+
type: _enum(DeviceType).optional(),
|
|
12926
|
+
integrationId: string().optional(),
|
|
12927
|
+
linkDeviceId: number().nullable().optional(),
|
|
12928
|
+
role: string().nullable().optional()
|
|
12929
|
+
}),
|
|
12930
|
+
_void(),
|
|
12931
|
+
{ kind: "mutation", auth: "admin" }
|
|
12932
|
+
),
|
|
10391
12933
|
/** Patch the device's hardware-identity metadata blob. Merges
|
|
10392
12934
|
* `patch` over the current blob (shallow). Value `null` for a
|
|
10393
12935
|
* given key removes that key. Drivers populate factual fields
|
|
@@ -10501,6 +13043,15 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10501
13043
|
object({ success: literal(true) }),
|
|
10502
13044
|
{ kind: "mutation", auth: "admin" }
|
|
10503
13045
|
),
|
|
13046
|
+
/** Cascade-delete every top-level device whose `integrationId` matches.
|
|
13047
|
+
* Children cascade automatically via the per-parent remove path.
|
|
13048
|
+
* Idempotent: a device whose `integrationId` was never set never matches.
|
|
13049
|
+
* Returns the count of top-level parents actually removed. */
|
|
13050
|
+
removeByIntegration: method(
|
|
13051
|
+
object({ integrationId: string() }),
|
|
13052
|
+
object({ removed: number() }),
|
|
13053
|
+
{ kind: "mutation", auth: "admin" }
|
|
13054
|
+
),
|
|
10504
13055
|
// ── Stream profile map ────────────────────────────────────────────────────
|
|
10505
13056
|
/** Get quality→streamId mapping for a camera. Derived from profileHint if not explicitly set. */
|
|
10506
13057
|
getStreamProfileMap: method(
|
|
@@ -10644,6 +13195,15 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10644
13195
|
live: SettingsSchemaWithValuesSchema.nullable()
|
|
10645
13196
|
})
|
|
10646
13197
|
),
|
|
13198
|
+
runDeviceAction: method(
|
|
13199
|
+
object({
|
|
13200
|
+
deviceId: number().int().nonnegative(),
|
|
13201
|
+
action: string().min(1),
|
|
13202
|
+
input: unknown()
|
|
13203
|
+
}),
|
|
13204
|
+
unknown(),
|
|
13205
|
+
{ kind: "mutation" }
|
|
13206
|
+
),
|
|
10647
13207
|
updateDeviceField: method(
|
|
10648
13208
|
object({
|
|
10649
13209
|
deviceId: number(),
|
|
@@ -10698,7 +13258,11 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10698
13258
|
adoptDevice: method(
|
|
10699
13259
|
object({
|
|
10700
13260
|
addonId: string(),
|
|
10701
|
-
candidate: DiscoveryCandidateSchema
|
|
13261
|
+
candidate: DiscoveryCandidateSchema,
|
|
13262
|
+
/** Owning integration id, stamped onto the new device's meta by the
|
|
13263
|
+
* device-manager forwarder so `removeByIntegration` can cascade it.
|
|
13264
|
+
* Optional for back-compat (omitted = no stamp = pre-existing behavior). */
|
|
13265
|
+
integrationId: string().optional()
|
|
10702
13266
|
}),
|
|
10703
13267
|
DeviceSummarySchema,
|
|
10704
13268
|
{ kind: "mutation", auth: "admin" }
|
|
@@ -10719,7 +13283,11 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10719
13283
|
object({
|
|
10720
13284
|
addonId: string(),
|
|
10721
13285
|
type: _enum(DeviceType),
|
|
10722
|
-
config: record(string(), unknown())
|
|
13286
|
+
config: record(string(), unknown()),
|
|
13287
|
+
/** Owning integration id, stamped onto the new device's meta by the
|
|
13288
|
+
* device-manager forwarder so `removeByIntegration` can cascade it.
|
|
13289
|
+
* Optional for back-compat (omitted = no stamp = pre-existing behavior). */
|
|
13290
|
+
integrationId: string().optional()
|
|
10723
13291
|
}),
|
|
10724
13292
|
DeviceSummarySchema,
|
|
10725
13293
|
{ kind: "mutation", auth: "admin" }
|
|
@@ -10743,6 +13311,41 @@ const DevicePersistConfigPayloadSchema = object({
|
|
|
10743
13311
|
FieldProbeResultSchema,
|
|
10744
13312
|
{ kind: "mutation", auth: "admin" }
|
|
10745
13313
|
),
|
|
13314
|
+
// ── Device-adoption operations (routed via CapabilityRegistry) ────────────
|
|
13315
|
+
//
|
|
13316
|
+
// These methods proxy to the `device-adoption` capability for an
|
|
13317
|
+
// integration-style addon (Home Assistant, …), routing by addonId —
|
|
13318
|
+
// the same hub-side proxy pattern as `discoverDevices`/`adoptDevice`
|
|
13319
|
+
// above. The admin UI's generic adopt modal calls them through this
|
|
13320
|
+
// singleton so it never needs a direct handle on the addon's
|
|
13321
|
+
// `device-adoption` provider.
|
|
13322
|
+
// getCandidate is not proxied — the adopt modal expands rows from
|
|
13323
|
+
// listCandidates' nested `children`, so a single-candidate fetch is
|
|
13324
|
+
// never needed at this layer.
|
|
13325
|
+
/** List adoption candidates for an integration via its device-adoption provider. */
|
|
13326
|
+
adoptionListCandidates: method(
|
|
13327
|
+
ListCandidatesInputSchema.extend({ addonId: string() }),
|
|
13328
|
+
ListCandidatesOutputSchema,
|
|
13329
|
+
{ auth: "admin" }
|
|
13330
|
+
),
|
|
13331
|
+
/** Trigger a discovery refresh on the integration's device-adoption provider. */
|
|
13332
|
+
adoptionRefresh: method(
|
|
13333
|
+
object({ addonId: string(), integrationId: string() }),
|
|
13334
|
+
AdoptionStatusSchema,
|
|
13335
|
+
{ kind: "mutation", auth: "admin" }
|
|
13336
|
+
),
|
|
13337
|
+
/** Adopt one or more discovered candidates via the device-adoption provider. */
|
|
13338
|
+
adoptionAdopt: method(
|
|
13339
|
+
AdoptInputSchema.extend({ addonId: string() }),
|
|
13340
|
+
AdoptResultSchema,
|
|
13341
|
+
{ kind: "mutation", auth: "admin" }
|
|
13342
|
+
),
|
|
13343
|
+
/** Release an adopted parent device via the device-adoption provider. */
|
|
13344
|
+
adoptionRelease: method(
|
|
13345
|
+
ReleaseInputSchema.extend({ addonId: string() }),
|
|
13346
|
+
_void(),
|
|
13347
|
+
{ kind: "mutation", auth: "admin" }
|
|
13348
|
+
),
|
|
10746
13349
|
/**
|
|
10747
13350
|
* Test a field value on an existing device (e.g. probe an RTSP URL).
|
|
10748
13351
|
* Routes through the device-provider for the owning addon.
|
|
@@ -11030,7 +13633,11 @@ const NotificationRuleConditionsSchema = object({
|
|
|
11030
13633
|
endHour: number()
|
|
11031
13634
|
}).optional(),
|
|
11032
13635
|
cooldownSeconds: number().optional(),
|
|
11033
|
-
minDwellSeconds: number().optional()
|
|
13636
|
+
minDwellSeconds: number().optional(),
|
|
13637
|
+
/** Match against `event.data.eventType` token (e.g. `'press_long'`). When non-empty, only events
|
|
13638
|
+
* carrying a matching `data.eventType` string pass this condition. Rules without this field are
|
|
13639
|
+
* unaffected (back-compat). Distinct from `rule.eventTypes` which holds EventCategory strings. */
|
|
13640
|
+
eventTypeTokens: array(string()).readonly().optional()
|
|
11034
13641
|
});
|
|
11035
13642
|
const NotificationRuleTemplateSchema = object({
|
|
11036
13643
|
title: string(),
|
|
@@ -11099,192 +13706,6 @@ const NotificationHistoryFilterSchema = object({
|
|
|
11099
13706
|
)
|
|
11100
13707
|
}
|
|
11101
13708
|
});
|
|
11102
|
-
const RecordingModeSchema = _enum(["continuous", "motion", "scheduled", "composite"]);
|
|
11103
|
-
const StreamPolicySchema = object({
|
|
11104
|
-
streamId: string(),
|
|
11105
|
-
mode: _enum(["always", "inherit"])
|
|
11106
|
-
});
|
|
11107
|
-
const ScheduleRuleSchema = object({
|
|
11108
|
-
days: array(number()).readonly(),
|
|
11109
|
-
startTime: string(),
|
|
11110
|
-
endTime: string(),
|
|
11111
|
-
mode: _enum(["continuous", "motion"])
|
|
11112
|
-
});
|
|
11113
|
-
const RecordingPolicySchema = object({
|
|
11114
|
-
deviceId: number(),
|
|
11115
|
-
mode: RecordingModeSchema,
|
|
11116
|
-
streams: array(StreamPolicySchema).readonly(),
|
|
11117
|
-
enabled: boolean(),
|
|
11118
|
-
preBufferSec: number(),
|
|
11119
|
-
postBufferSec: number(),
|
|
11120
|
-
scheduleRules: array(ScheduleRuleSchema).readonly().optional()
|
|
11121
|
-
});
|
|
11122
|
-
const DataCategorySchema = _enum([
|
|
11123
|
-
"recording:main",
|
|
11124
|
-
"recording:mid",
|
|
11125
|
-
"recording:sub",
|
|
11126
|
-
"thumbnail:scrub",
|
|
11127
|
-
"thumbnail:event"
|
|
11128
|
-
]);
|
|
11129
|
-
const RecordingStorageConfigSchema = object({
|
|
11130
|
-
deviceId: number(),
|
|
11131
|
-
dataCategory: DataCategorySchema,
|
|
11132
|
-
storageName: string(),
|
|
11133
|
-
subDirectory: string(),
|
|
11134
|
-
retentionDays: number().nullable(),
|
|
11135
|
-
retentionGb: number().nullable()
|
|
11136
|
-
});
|
|
11137
|
-
const RecordingSegmentSchema = object({
|
|
11138
|
-
id: string(),
|
|
11139
|
-
deviceId: number(),
|
|
11140
|
-
streamId: string(),
|
|
11141
|
-
startTime: number(),
|
|
11142
|
-
endTime: number(),
|
|
11143
|
-
duration: number(),
|
|
11144
|
-
path: string(),
|
|
11145
|
-
storageName: string(),
|
|
11146
|
-
subDirectory: string(),
|
|
11147
|
-
sizeBytes: number(),
|
|
11148
|
-
codec: _enum(["h264", "h265"]),
|
|
11149
|
-
hasAudio: boolean()
|
|
11150
|
-
});
|
|
11151
|
-
const RecordingThumbnailSchema = object({
|
|
11152
|
-
deviceId: number(),
|
|
11153
|
-
timestamp: number(),
|
|
11154
|
-
path: string(),
|
|
11155
|
-
storageName: string(),
|
|
11156
|
-
subDirectory: string(),
|
|
11157
|
-
sizeBytes: number(),
|
|
11158
|
-
category: _enum(["scrub", "event"])
|
|
11159
|
-
});
|
|
11160
|
-
const AvailabilityRangeSchema = object({
|
|
11161
|
-
startTime: number(),
|
|
11162
|
-
endTime: number(),
|
|
11163
|
-
streams: array(string()).readonly()
|
|
11164
|
-
});
|
|
11165
|
-
const StorageUsageSchema = object({
|
|
11166
|
-
totalBytes: number(),
|
|
11167
|
-
segmentCount: number()
|
|
11168
|
-
});
|
|
11169
|
-
const StreamEstimateSchema = object({
|
|
11170
|
-
bitrateKbps: number(),
|
|
11171
|
-
retentionDays: number().nullable(),
|
|
11172
|
-
retentionGb: number().nullable(),
|
|
11173
|
-
estimatedGb: number(),
|
|
11174
|
-
estimatedDaysAtCapacity: number().nullable()
|
|
11175
|
-
});
|
|
11176
|
-
const StorageEstimateSchema = object({
|
|
11177
|
-
perStream: record(string(), StreamEstimateSchema),
|
|
11178
|
-
thumbnails: object({ estimatedGb: number() }),
|
|
11179
|
-
totalEstimatedGb: number(),
|
|
11180
|
-
motionEstimate: object({
|
|
11181
|
-
avgEventsPerDay: number(),
|
|
11182
|
-
avgDurationSec: number(),
|
|
11183
|
-
dutyCyclePercent: number()
|
|
11184
|
-
}).optional()
|
|
11185
|
-
});
|
|
11186
|
-
const MotionStatsSchema = object({
|
|
11187
|
-
totalEvents: number(),
|
|
11188
|
-
avgDurationSec: number(),
|
|
11189
|
-
avgEventsPerDay: number(),
|
|
11190
|
-
dutyCyclePercent: number()
|
|
11191
|
-
});
|
|
11192
|
-
const DeviceIdInput = object({ deviceId: number() });
|
|
11193
|
-
const EnableInput = object({
|
|
11194
|
-
deviceId: number(),
|
|
11195
|
-
policy: RecordingPolicySchema.omit({ deviceId: true }),
|
|
11196
|
-
storageOverrides: array(RecordingStorageConfigSchema.omit({ deviceId: true })).readonly().optional(),
|
|
11197
|
-
ffmpegOverrides: record(string(), unknown()).optional()
|
|
11198
|
-
});
|
|
11199
|
-
const TimeRangeInput = object({
|
|
11200
|
-
deviceId: number(),
|
|
11201
|
-
startTime: number(),
|
|
11202
|
-
endTime: number()
|
|
11203
|
-
});
|
|
11204
|
-
const StreamTimeRangeInput = object({
|
|
11205
|
-
deviceId: number(),
|
|
11206
|
-
streamId: string(),
|
|
11207
|
-
startTime: number(),
|
|
11208
|
-
endTime: number()
|
|
11209
|
-
});
|
|
11210
|
-
const PlaylistInput = object({
|
|
11211
|
-
deviceId: number(),
|
|
11212
|
-
streamId: string(),
|
|
11213
|
-
startTime: number(),
|
|
11214
|
-
endTime: number(),
|
|
11215
|
-
live: boolean().optional()
|
|
11216
|
-
});
|
|
11217
|
-
const ThumbnailInput = object({
|
|
11218
|
-
deviceId: number(),
|
|
11219
|
-
timestamp: number(),
|
|
11220
|
-
category: string().optional()
|
|
11221
|
-
});
|
|
11222
|
-
const DeviceStreamInput = object({
|
|
11223
|
-
deviceId: number(),
|
|
11224
|
-
streamId: string()
|
|
11225
|
-
});
|
|
11226
|
-
const StorageEstimateInput = object({
|
|
11227
|
-
deviceId: number(),
|
|
11228
|
-
motionInput: object({
|
|
11229
|
-
avgEventsPerDay: number(),
|
|
11230
|
-
avgDurationSec: number()
|
|
11231
|
-
}).optional()
|
|
11232
|
-
});
|
|
11233
|
-
const RetentionConfigInput = object({
|
|
11234
|
-
deviceId: number(),
|
|
11235
|
-
dataCategory: DataCategorySchema
|
|
11236
|
-
});
|
|
11237
|
-
const SetPolicyInput = object({
|
|
11238
|
-
deviceId: number(),
|
|
11239
|
-
policy: RecordingPolicySchema.omit({ deviceId: true })
|
|
11240
|
-
});
|
|
11241
|
-
const UpdateConfigInput = object({
|
|
11242
|
-
deviceId: number(),
|
|
11243
|
-
policy: RecordingPolicySchema.omit({ deviceId: true }),
|
|
11244
|
-
ffmpegOverrides: record(string(), unknown()).optional()
|
|
11245
|
-
});
|
|
11246
|
-
const recordingEngineCapability = {
|
|
11247
|
-
name: "recording-engine",
|
|
11248
|
-
scope: "system",
|
|
11249
|
-
mode: "singleton",
|
|
11250
|
-
methods: {
|
|
11251
|
-
// ── Status ────────────────────────────────────────────────────────
|
|
11252
|
-
getStatus: method(_void(), object({
|
|
11253
|
-
activeRecordings: number(),
|
|
11254
|
-
totalSegments: number(),
|
|
11255
|
-
totalSizeMB: number()
|
|
11256
|
-
})),
|
|
11257
|
-
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
11258
|
-
enable: method(EnableInput, _void(), { kind: "mutation", auth: "admin" }),
|
|
11259
|
-
disable: method(DeviceIdInput, _void(), { kind: "mutation", auth: "admin" }),
|
|
11260
|
-
// ── Config ────────────────────────────────────────────────────────
|
|
11261
|
-
getConfig: method(DeviceIdInput, RecordingPolicySchema.nullable()),
|
|
11262
|
-
updateConfig: method(UpdateConfigInput, _void(), { kind: "mutation", auth: "admin" }),
|
|
11263
|
-
// ── Playback ──────────────────────────────────────────────────────
|
|
11264
|
-
getPlaylist: method(PlaylistInput, string()),
|
|
11265
|
-
getThumbnail: method(ThumbnailInput, RecordingThumbnailSchema.nullable()),
|
|
11266
|
-
getSegments: method(StreamTimeRangeInput, array(RecordingSegmentSchema).readonly()),
|
|
11267
|
-
getAvailability: method(TimeRangeInput, array(AvailabilityRangeSchema).readonly()),
|
|
11268
|
-
// ── Storage ───────────────────────────────────────────────────────
|
|
11269
|
-
estimateStorage: method(StorageEstimateInput, StorageEstimateSchema),
|
|
11270
|
-
estimateGlobalStorage: method(_void(), StorageEstimateSchema),
|
|
11271
|
-
getStorageUsage: method(DeviceStreamInput, StorageUsageSchema),
|
|
11272
|
-
// ── Policy ────────────────────────────────────────────────────────
|
|
11273
|
-
setPolicy: method(SetPolicyInput, _void(), { kind: "mutation", auth: "admin" }),
|
|
11274
|
-
getPolicy: method(DeviceIdInput, RecordingPolicySchema.nullable()),
|
|
11275
|
-
getPolicyStatus: method(DeviceIdInput, object({
|
|
11276
|
-
deviceId: number(),
|
|
11277
|
-
enabled: boolean(),
|
|
11278
|
-
mode: RecordingModeSchema,
|
|
11279
|
-
activeStreams: number()
|
|
11280
|
-
}).nullable()),
|
|
11281
|
-
// ── Retention ─────────────────────────────────────────────────────
|
|
11282
|
-
getRetentionConfig: method(RetentionConfigInput, RecordingStorageConfigSchema.nullable()),
|
|
11283
|
-
updateRetentionConfig: method(RecordingStorageConfigSchema, _void(), { kind: "mutation", auth: "admin" }),
|
|
11284
|
-
// ── Motion ────────────────────────────────────────────────────────
|
|
11285
|
-
getMotionStats: method(TimeRangeInput, MotionStatsSchema)
|
|
11286
|
-
}
|
|
11287
|
-
};
|
|
11288
13709
|
({
|
|
11289
13710
|
deviceTypes: [DeviceType.Camera]
|
|
11290
13711
|
});
|
|
@@ -11333,26 +13754,38 @@ const MotionEventSchema = object({
|
|
|
11333
13754
|
...BaseEventFields,
|
|
11334
13755
|
kind: literal("motion"),
|
|
11335
13756
|
regionCount: number(),
|
|
13757
|
+
/** Heavy JSON array — omitted in slim projection. */
|
|
11336
13758
|
regions: array(object({
|
|
11337
13759
|
bbox: BoundingBoxSchema,
|
|
11338
13760
|
pixelCount: number(),
|
|
11339
13761
|
intensity: number()
|
|
11340
|
-
})).readonly(),
|
|
11341
|
-
|
|
11342
|
-
|
|
13762
|
+
})).readonly().optional(),
|
|
13763
|
+
/** Omitted in slim projection. */
|
|
13764
|
+
frameWidth: number().optional(),
|
|
13765
|
+
/** Omitted in slim projection. */
|
|
13766
|
+
frameHeight: number().optional(),
|
|
13767
|
+
/** Populated by B5 (recording playback URL for this event). */
|
|
13768
|
+
mediaUrl: string().optional()
|
|
11343
13769
|
});
|
|
11344
13770
|
const ObjectEventSchema = object({
|
|
11345
13771
|
...BaseEventFields,
|
|
11346
13772
|
kind: literal("object"),
|
|
11347
|
-
|
|
13773
|
+
/** Omitted in slim projection. */
|
|
13774
|
+
trackId: string().optional(),
|
|
11348
13775
|
className: string(),
|
|
11349
13776
|
label: string().optional(),
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
13777
|
+
/** Omitted in slim projection. */
|
|
13778
|
+
confidence: number().optional(),
|
|
13779
|
+
/** Heavy JSON — omitted in slim projection. */
|
|
13780
|
+
bbox: BoundingBoxSchema.optional(),
|
|
13781
|
+
/** Heavy JSON — omitted in slim projection. */
|
|
13782
|
+
zones: array(string()).readonly().optional(),
|
|
13783
|
+
/** Omitted in slim projection. */
|
|
13784
|
+
state: TrackStateSchema.optional(),
|
|
11354
13785
|
/** MediaStore key for the crop attached to this event (if any). */
|
|
11355
|
-
mediaKey: string().optional()
|
|
13786
|
+
mediaKey: string().optional(),
|
|
13787
|
+
/** Populated by B5 (recording playback URL for this event). */
|
|
13788
|
+
mediaUrl: string().optional()
|
|
11356
13789
|
});
|
|
11357
13790
|
const AudioEventSchema = object({
|
|
11358
13791
|
...BaseEventFields,
|
|
@@ -11363,7 +13796,9 @@ const AudioEventSchema = object({
|
|
|
11363
13796
|
className: string(),
|
|
11364
13797
|
originalClass: string().optional(),
|
|
11365
13798
|
score: number()
|
|
11366
|
-
}).optional()
|
|
13799
|
+
}).optional(),
|
|
13800
|
+
/** Populated by B5 (recording playback URL for this event). */
|
|
13801
|
+
mediaUrl: string().optional()
|
|
11367
13802
|
});
|
|
11368
13803
|
const MediaFileSchema = object({
|
|
11369
13804
|
key: string(),
|
|
@@ -11378,7 +13813,12 @@ const DeviceEventQueryInput = object({
|
|
|
11378
13813
|
deviceId: number(),
|
|
11379
13814
|
since: number().optional(),
|
|
11380
13815
|
until: number().optional(),
|
|
11381
|
-
limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
|
|
13816
|
+
limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
|
|
13817
|
+
/** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
|
|
13818
|
+
* optional `mediaUrl` (populated by B5). `full` (default) keeps today's
|
|
13819
|
+
* exact behaviour. Callers may omit this field — the store defaults to
|
|
13820
|
+
* `full` when not provided. */
|
|
13821
|
+
projection: _enum(["full", "slim"]).optional()
|
|
11382
13822
|
});
|
|
11383
13823
|
const ObjectEventQueryInput = DeviceEventQueryInput.extend({
|
|
11384
13824
|
classFilter: string().optional()
|
|
@@ -11439,6 +13879,37 @@ const pipelineAnalyticsCapability = {
|
|
|
11439
13879
|
DeviceEventQueryInput,
|
|
11440
13880
|
array(AudioEventSchema).readonly()
|
|
11441
13881
|
),
|
|
13882
|
+
// ── Density (timeline histogram) ──────────────────────────
|
|
13883
|
+
/** Server-side bucketed event counts for the 24-hour timeline.
|
|
13884
|
+
* Returns one entry per non-empty bucket; empty buckets are omitted. */
|
|
13885
|
+
getEventDensity: method(
|
|
13886
|
+
object({
|
|
13887
|
+
deviceId: number(),
|
|
13888
|
+
since: number(),
|
|
13889
|
+
until: number(),
|
|
13890
|
+
bucketMs: number().int().positive()
|
|
13891
|
+
}),
|
|
13892
|
+
array(object({
|
|
13893
|
+
bucketStart: number(),
|
|
13894
|
+
motion: number().int(),
|
|
13895
|
+
object: number().int(),
|
|
13896
|
+
audio: number().int()
|
|
13897
|
+
})).readonly()
|
|
13898
|
+
),
|
|
13899
|
+
// ── Maintenance ───────────────────────────────────────────
|
|
13900
|
+
/**
|
|
13901
|
+
* Delete all events (motion + object + audio) for the given device
|
|
13902
|
+
* that are older than `cutoffMs` (exclusive), and delete their
|
|
13903
|
+
* thumbnails in lockstep. Returns per-kind deleted counts.
|
|
13904
|
+
*
|
|
13905
|
+
* Called by Phase B3 recorder orchestration to keep event history
|
|
13906
|
+
* aligned with available footage.
|
|
13907
|
+
*/
|
|
13908
|
+
pruneEventsBefore: method(
|
|
13909
|
+
object({ deviceId: number(), cutoffMs: number() }),
|
|
13910
|
+
object({ motion: number().int(), object: number().int(), audio: number().int() }),
|
|
13911
|
+
{ kind: "mutation", auth: "admin" }
|
|
13912
|
+
),
|
|
11442
13913
|
// ── Media ─────────────────────────────────────────────────
|
|
11443
13914
|
getEventMedia: method(
|
|
11444
13915
|
object({ eventId: string() }),
|
|
@@ -11792,32 +14263,102 @@ const EventItemSchema = object({
|
|
|
11792
14263
|
)
|
|
11793
14264
|
}
|
|
11794
14265
|
});
|
|
11795
|
-
const
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
14266
|
+
const RecordingStatusSchema = object({
|
|
14267
|
+
deviceId: number(),
|
|
14268
|
+
enabled: boolean(),
|
|
14269
|
+
activeMode: _enum(["off", "continuous"]),
|
|
14270
|
+
nodeId: string(),
|
|
14271
|
+
storageBytes: number()
|
|
14272
|
+
});
|
|
14273
|
+
const RecordingRangeSchema = object({
|
|
14274
|
+
profile: string(),
|
|
14275
|
+
startMs: number(),
|
|
14276
|
+
endMs: number()
|
|
14277
|
+
});
|
|
14278
|
+
const RecordingAvailabilitySchema = object({
|
|
14279
|
+
deviceId: number(),
|
|
14280
|
+
ranges: array(RecordingRangeSchema)
|
|
14281
|
+
});
|
|
14282
|
+
const RecordingManifestSchema = object({
|
|
14283
|
+
deviceId: number(),
|
|
14284
|
+
/** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
|
|
14285
|
+
localMasterPath: string().nullable(),
|
|
14286
|
+
/** HTTP(S) URL to the master playlist on the recording node's playback server
|
|
14287
|
+
* (the PRIMARY candidate); null when no recording / server. Carries the
|
|
14288
|
+
* scoped playback token in its path. */
|
|
14289
|
+
playbackUrl: string().nullable(),
|
|
14290
|
+
/**
|
|
14291
|
+
* Candidate master-playlist URLs the client tries in order (LAN first, then
|
|
14292
|
+
* remote — Tailscale/Cloudflare if the operator configured extra hosts), each
|
|
14293
|
+
* carrying the same scoped token. `playbackUrl` is the first entry. Empty when
|
|
14294
|
+
* there is no recording / server.
|
|
14295
|
+
*/
|
|
14296
|
+
playbackEndpoints: array(string())
|
|
14297
|
+
});
|
|
14298
|
+
const RecordingDeviceUsageSchema = object({
|
|
14299
|
+
deviceId: number(),
|
|
14300
|
+
usedBytes: number()
|
|
14301
|
+
});
|
|
14302
|
+
const RecordingLocationUsageSchema = object({
|
|
14303
|
+
/** StorageLocation id; null for the legacy/degraded single-root fallback. */
|
|
14304
|
+
locationId: string().nullable(),
|
|
14305
|
+
/** Bytes of recordings stored on this location. */
|
|
14306
|
+
usedBytes: number(),
|
|
14307
|
+
/** Free bytes on the location's volume; null when capacity is unknown (remote). */
|
|
14308
|
+
availableBytes: number().nullable(),
|
|
14309
|
+
/** Total bytes of the location's volume; null when unknown. */
|
|
14310
|
+
totalBytes: number().nullable()
|
|
14311
|
+
});
|
|
14312
|
+
const RecordingStorageUsageSchema = object({
|
|
14313
|
+
nodeId: string(),
|
|
14314
|
+
totalUsedBytes: number(),
|
|
14315
|
+
devices: array(RecordingDeviceUsageSchema),
|
|
14316
|
+
locations: array(RecordingLocationUsageSchema)
|
|
11802
14317
|
});
|
|
11803
14318
|
({
|
|
11804
|
-
deviceTypes: [DeviceType.Camera],
|
|
11805
14319
|
methods: {
|
|
11806
|
-
|
|
11807
|
-
object({
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
to: number().optional()
|
|
11811
|
-
}),
|
|
11812
|
-
array(SegmentSchema)
|
|
14320
|
+
getAvailability: method(
|
|
14321
|
+
object({ deviceId: number(), fromMs: number(), toMs: number() }),
|
|
14322
|
+
RecordingAvailabilitySchema,
|
|
14323
|
+
{ kind: "query", auth: "admin" }
|
|
11813
14324
|
),
|
|
11814
|
-
|
|
11815
|
-
object({ deviceId: number(),
|
|
11816
|
-
|
|
14325
|
+
getPlaybackManifest: method(
|
|
14326
|
+
object({ deviceId: number(), fromMs: number(), toMs: number() }),
|
|
14327
|
+
RecordingManifestSchema,
|
|
14328
|
+
{ kind: "query", auth: "admin" }
|
|
11817
14329
|
),
|
|
11818
|
-
|
|
11819
|
-
object({
|
|
11820
|
-
|
|
14330
|
+
getStorageUsage: method(
|
|
14331
|
+
object({}),
|
|
14332
|
+
RecordingStorageUsageSchema,
|
|
14333
|
+
{ kind: "query", auth: "admin" }
|
|
14334
|
+
),
|
|
14335
|
+
getDeviceConfig: method(
|
|
14336
|
+
object({ deviceId: number() }),
|
|
14337
|
+
RecordingConfigSchema,
|
|
14338
|
+
{ kind: "query", auth: "admin" }
|
|
14339
|
+
),
|
|
14340
|
+
setDeviceConfig: method(
|
|
14341
|
+
object({ deviceId: number(), config: RecordingConfigSchema }),
|
|
14342
|
+
RecordingConfigSchema,
|
|
14343
|
+
{ kind: "mutation", auth: "admin" }
|
|
14344
|
+
),
|
|
14345
|
+
/** Re-scan this device's footage from disk (stat sizes) and reseed the
|
|
14346
|
+
* index, then return the fresh status. */
|
|
14347
|
+
rescanStorage: method(
|
|
14348
|
+
object({ deviceId: number() }),
|
|
14349
|
+
RecordingStatusSchema,
|
|
14350
|
+
{ kind: "mutation", auth: "admin" }
|
|
14351
|
+
),
|
|
14352
|
+
/** Apply this device's retention policy to footage now; returns the oldest
|
|
14353
|
+
* surviving footage start (the retention floor) or null if no footage. */
|
|
14354
|
+
pruneFootage: method(
|
|
14355
|
+
object({ deviceId: number() }),
|
|
14356
|
+
object({
|
|
14357
|
+
floorMs: number().nullable(),
|
|
14358
|
+
deletedBuckets: number().int(),
|
|
14359
|
+
reclaimedBytes: number().int()
|
|
14360
|
+
}),
|
|
14361
|
+
{ kind: "mutation", auth: "admin" }
|
|
11821
14362
|
)
|
|
11822
14363
|
}
|
|
11823
14364
|
});
|
|
@@ -11830,13 +14371,19 @@ const StreamSourceEntrySchema = object({
|
|
|
11830
14371
|
fps: number().optional(),
|
|
11831
14372
|
bitrate: number().optional(),
|
|
11832
14373
|
codec: string().optional(),
|
|
11833
|
-
profileHint:
|
|
14374
|
+
profileHint: CamProfileSchema.optional(),
|
|
11834
14375
|
sdp: string().optional()
|
|
11835
14376
|
});
|
|
11836
14377
|
const ConfigEntrySchema = object({
|
|
11837
14378
|
key: string(),
|
|
11838
14379
|
value: unknown()
|
|
11839
14380
|
});
|
|
14381
|
+
const RawStateResultSchema = object({
|
|
14382
|
+
/** Originating provider id, e.g. 'homeassistant' | 'reolink' | 'hikvision'. */
|
|
14383
|
+
source: string(),
|
|
14384
|
+
/** Opaque, DISPLAY-SAFE upstream blob (no secrets/PII). */
|
|
14385
|
+
data: record(string(), unknown())
|
|
14386
|
+
});
|
|
11840
14387
|
({
|
|
11841
14388
|
methods: {
|
|
11842
14389
|
/**
|
|
@@ -11868,6 +14415,21 @@ const ConfigEntrySchema = object({
|
|
|
11868
14415
|
_void(),
|
|
11869
14416
|
{ kind: "mutation" }
|
|
11870
14417
|
),
|
|
14418
|
+
/**
|
|
14419
|
+
* Invoke a device custom action on a forked/remote device (the
|
|
14420
|
+
* cross-process transport for `IDevice.runDeviceAction`). Mirrors
|
|
14421
|
+
* `setConfig` — the device-manager calls this when the device is not
|
|
14422
|
+
* hub-local.
|
|
14423
|
+
*/
|
|
14424
|
+
runAction: method(
|
|
14425
|
+
object({
|
|
14426
|
+
deviceId: number(),
|
|
14427
|
+
action: string().min(1),
|
|
14428
|
+
input: unknown()
|
|
14429
|
+
}),
|
|
14430
|
+
unknown(),
|
|
14431
|
+
{ kind: "mutation" }
|
|
14432
|
+
),
|
|
11871
14433
|
/**
|
|
11872
14434
|
* Invoke `IDevice.removeDevice()` so the driver can release resources
|
|
11873
14435
|
* (close sockets, stop background tasks, …). The device-manager still
|
|
@@ -11895,6 +14457,18 @@ const ConfigEntrySchema = object({
|
|
|
11895
14457
|
getSettingsSchema: method(
|
|
11896
14458
|
object({ deviceId: number() }),
|
|
11897
14459
|
unknown().nullable()
|
|
14460
|
+
),
|
|
14461
|
+
/**
|
|
14462
|
+
* Opt-in: return the device's RAW upstream state (the provider's
|
|
14463
|
+
* cached values) as a display-safe `{ source, data }` blob. Returns
|
|
14464
|
+
* `null` when the device exposes no raw state — the State panel hides
|
|
14465
|
+
* its Raw toggle in that case. One-shot (read the provider's existing
|
|
14466
|
+
* cache; no upstream round-trip).
|
|
14467
|
+
*/
|
|
14468
|
+
getRawState: method(
|
|
14469
|
+
object({ deviceId: number() }),
|
|
14470
|
+
RawStateResultSchema.nullable(),
|
|
14471
|
+
{ auth: "protected" }
|
|
11898
14472
|
)
|
|
11899
14473
|
}
|
|
11900
14474
|
});
|
|
@@ -11952,6 +14526,16 @@ object({
|
|
|
11952
14526
|
)
|
|
11953
14527
|
}
|
|
11954
14528
|
});
|
|
14529
|
+
({
|
|
14530
|
+
deviceTypes: [DeviceType.Button],
|
|
14531
|
+
methods: {
|
|
14532
|
+
press: method(
|
|
14533
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
14534
|
+
_void(),
|
|
14535
|
+
{ kind: "mutation", auth: "admin" }
|
|
14536
|
+
)
|
|
14537
|
+
}
|
|
14538
|
+
});
|
|
11955
14539
|
const OsdOverlayKindEnum = _enum(["text", "timestamp", "watermark"]);
|
|
11956
14540
|
const OsdPositionEnum = _enum([
|
|
11957
14541
|
"top-left",
|
|
@@ -12014,18 +14598,72 @@ const OsdOverlayPatchSchema = object({
|
|
|
12014
14598
|
}
|
|
12015
14599
|
});
|
|
12016
14600
|
object({
|
|
12017
|
-
|
|
14601
|
+
/** All accessory children of the parent. */
|
|
14602
|
+
childDeviceIds: array(number()).readonly(),
|
|
14603
|
+
/** Subset of `childDeviceIds` the operator hid from the UI. The order
|
|
14604
|
+
* is irrelevant; the array is treated as a set by consumers. */
|
|
14605
|
+
hiddenChildIds: array(number()).readonly()
|
|
12018
14606
|
});
|
|
12019
14607
|
({
|
|
12020
|
-
|
|
14608
|
+
// Cameras + Hubs are the historical consumers; the HA integration
|
|
14609
|
+
// landed in Phase E adds Switch / Light / Sensor / Thermostat / Cover
|
|
14610
|
+
// / Lock / Fan / MediaPlayer / AlarmPanel / Generic as parent types
|
|
14611
|
+
// for adopted HA devices whose entity-children sit under accessories.
|
|
14612
|
+
deviceTypes: [
|
|
14613
|
+
DeviceType.Camera,
|
|
14614
|
+
DeviceType.Hub,
|
|
14615
|
+
DeviceType.Switch,
|
|
14616
|
+
DeviceType.Light,
|
|
14617
|
+
DeviceType.Sensor,
|
|
14618
|
+
DeviceType.Thermostat,
|
|
14619
|
+
DeviceType.Cover,
|
|
14620
|
+
DeviceType.Lock,
|
|
14621
|
+
DeviceType.Fan,
|
|
14622
|
+
DeviceType.MediaPlayer,
|
|
14623
|
+
DeviceType.AlarmPanel,
|
|
14624
|
+
DeviceType.Generic
|
|
14625
|
+
],
|
|
14626
|
+
methods: {
|
|
14627
|
+
/**
|
|
14628
|
+
* Toggle the UI-visibility of a single accessory child. Hidden
|
|
14629
|
+
* children stay registered and continue to receive state updates;
|
|
14630
|
+
* they're omitted only from the parent's UI accessories panel.
|
|
14631
|
+
*
|
|
14632
|
+
* Idempotent — hiding an already-hidden child or unhiding a
|
|
14633
|
+
* non-hidden child is a no-op. Providers that don't support
|
|
14634
|
+
* per-child visibility may implement this as a no-op (the UI hide
|
|
14635
|
+
* toggle gracefully degrades).
|
|
14636
|
+
*/
|
|
14637
|
+
setChildHidden: method(
|
|
14638
|
+
object({
|
|
14639
|
+
deviceId: number().int().nonnegative(),
|
|
14640
|
+
childDeviceId: number().int().nonnegative(),
|
|
14641
|
+
hidden: boolean()
|
|
14642
|
+
}),
|
|
14643
|
+
_void(),
|
|
14644
|
+
{ kind: "mutation", auth: "admin" }
|
|
14645
|
+
)
|
|
14646
|
+
},
|
|
12021
14647
|
events: {
|
|
12022
14648
|
/**
|
|
12023
14649
|
* Emitted when a child device is created, removed, or its parent
|
|
12024
|
-
* assignment changes. Payload carries the fresh list
|
|
14650
|
+
* assignment changes. Payload carries the fresh list and the
|
|
14651
|
+
* hidden subset.
|
|
12025
14652
|
*/
|
|
12026
14653
|
onAccessoriesChanged: { data: object({
|
|
12027
14654
|
deviceId: number(),
|
|
12028
|
-
childDeviceIds: array(number()).readonly()
|
|
14655
|
+
childDeviceIds: array(number()).readonly(),
|
|
14656
|
+
hiddenChildIds: array(number()).readonly()
|
|
14657
|
+
}) },
|
|
14658
|
+
/**
|
|
14659
|
+
* Emitted when the operator toggles a child's UI-visibility.
|
|
14660
|
+
* Subscribers (admin UI accessories panel) re-render off this
|
|
14661
|
+
* signal without re-fetching the full status block.
|
|
14662
|
+
*/
|
|
14663
|
+
onChildVisibilityChanged: { data: object({
|
|
14664
|
+
deviceId: number(),
|
|
14665
|
+
childDeviceId: number(),
|
|
14666
|
+
hidden: boolean()
|
|
12029
14667
|
}) }
|
|
12030
14668
|
}
|
|
12031
14669
|
});
|
|
@@ -12046,6 +14684,12 @@ const IntercomStatusSchema = object({
|
|
|
12046
14684
|
/** Firmware ability cached at first session creation. Null until probed. */
|
|
12047
14685
|
ability: IntercomAbilitySchema.nullable()
|
|
12048
14686
|
});
|
|
14687
|
+
const TalkAudioCodecSchema = _enum([
|
|
14688
|
+
"opus",
|
|
14689
|
+
"s16le",
|
|
14690
|
+
"g711ulaw",
|
|
14691
|
+
"g711alaw"
|
|
14692
|
+
]);
|
|
12049
14693
|
({
|
|
12050
14694
|
deviceTypes: [DeviceType.Camera],
|
|
12051
14695
|
methods: {
|
|
@@ -12090,20 +14734,35 @@ const IntercomStatusSchema = object({
|
|
|
12090
14734
|
{ kind: "mutation", auth: "admin" }
|
|
12091
14735
|
),
|
|
12092
14736
|
/**
|
|
12093
|
-
* Push
|
|
12094
|
-
*
|
|
12095
|
-
*
|
|
12096
|
-
*
|
|
12097
|
-
*
|
|
12098
|
-
*
|
|
12099
|
-
*
|
|
14737
|
+
* Push one chunk of talk-back audio onto the active talk session.
|
|
14738
|
+
* The cap is codec-agnostic: the caller declares (or omits) the
|
|
14739
|
+
* wire format via `codec`; the provider decides between passthrough
|
|
14740
|
+
* (when the wire codec matches the camera's native talk channel),
|
|
14741
|
+
* transcoding via the `audio-codec` cap, or rejecting the call.
|
|
14742
|
+
*
|
|
14743
|
+
* Callers do NOT need to know the camera's wire format or sample
|
|
14744
|
+
* rate — that information lives entirely inside the provider.
|
|
14745
|
+
*
|
|
14746
|
+
* Sequence numbers MUST be monotonic per talk session; older frames
|
|
14747
|
+
* arriving after newer ones are dropped to avoid smearing the
|
|
14748
|
+
* downstream encoder state (G.711 is stateless but IMA ADPCM's
|
|
14749
|
+
* predictor would corrupt with re-ordering).
|
|
12100
14750
|
*/
|
|
12101
|
-
|
|
14751
|
+
pushTalkAudio: method(
|
|
12102
14752
|
object({
|
|
12103
14753
|
deviceId: number(),
|
|
12104
|
-
/**
|
|
12105
|
-
*
|
|
12106
|
-
|
|
14754
|
+
/** Audio bytes for ONE frame, base64-encoded so the payload
|
|
14755
|
+
* survives tRPC JSON serialization. */
|
|
14756
|
+
audioBase64: string(),
|
|
14757
|
+
/** Wire codec of the payload. Omit to let the provider default
|
|
14758
|
+
* to its native expected format (s16le @ provider-native rate,
|
|
14759
|
+
* mono). See {@link TalkAudioCodecSchema} for the supported set. */
|
|
14760
|
+
codec: TalkAudioCodecSchema.optional(),
|
|
14761
|
+
/** Sample rate (Hz). REQUIRED for `s16le`; advisory for
|
|
14762
|
+
* `opus` (encoder clock); ignored for `g711*` (implied 8000). */
|
|
14763
|
+
sampleRate: number().int().positive().optional(),
|
|
14764
|
+
/** Channel count. Default 1. */
|
|
14765
|
+
channels: number().int().positive().optional(),
|
|
12107
14766
|
/** Sequence number for ordering / dropping out-of-order frames. */
|
|
12108
14767
|
sequenceNumber: number().int()
|
|
12109
14768
|
}),
|
|
@@ -12197,6 +14856,10 @@ const HardwareEncodersSchema = object({
|
|
|
12197
14856
|
defaultH265: HardwareEncoderIdSchema,
|
|
12198
14857
|
probedAt: number()
|
|
12199
14858
|
});
|
|
14859
|
+
const HardwareDecodeAccelsSchema = object({
|
|
14860
|
+
methods: array(string()).readonly(),
|
|
14861
|
+
probedAt: number()
|
|
14862
|
+
});
|
|
12200
14863
|
const HardwarePlatformSchema = _enum(["darwin", "linux", "win32"]);
|
|
12201
14864
|
const HardwareArchSchema = _enum(["arm64", "x64"]);
|
|
12202
14865
|
const GpuInfoSchema = object({
|
|
@@ -12265,6 +14928,16 @@ const ResolvedInferenceConfigSchema = object({
|
|
|
12265
14928
|
_void(),
|
|
12266
14929
|
HardwareEncodersSchema,
|
|
12267
14930
|
{ kind: "mutation", auth: "admin" }
|
|
14931
|
+
),
|
|
14932
|
+
/**
|
|
14933
|
+
* Decode-side hw-accel probe — the `-hwaccel` methods the configured ffmpeg
|
|
14934
|
+
* binary supports (via `ffmpeg -hwaccels`). Cached after first call.
|
|
14935
|
+
*/
|
|
14936
|
+
getHardwareDecodeAccels: method(_void(), HardwareDecodeAccelsSchema),
|
|
14937
|
+
refreshHardwareDecodeAccels: method(
|
|
14938
|
+
_void(),
|
|
14939
|
+
HardwareDecodeAccelsSchema,
|
|
14940
|
+
{ kind: "mutation", auth: "admin" }
|
|
12268
14941
|
)
|
|
12269
14942
|
}
|
|
12270
14943
|
});
|
|
@@ -12954,6 +15627,7 @@ const ClientNetworkStatsSchema = object({
|
|
|
12954
15627
|
rttMs: number(),
|
|
12955
15628
|
jitterMs: number(),
|
|
12956
15629
|
estimatedBandwidthKbps: number(),
|
|
15630
|
+
packetLossPercent: number().min(0).max(100),
|
|
12957
15631
|
lastUpdated: number()
|
|
12958
15632
|
});
|
|
12959
15633
|
const DeviceNetworkStatsSchema = object({
|
|
@@ -12976,7 +15650,8 @@ const DeviceNetworkStatsSchema = object({
|
|
|
12976
15650
|
deviceId: number(),
|
|
12977
15651
|
rttMs: number().min(0).max(6e4),
|
|
12978
15652
|
jitterMs: number().min(0).max(1e4),
|
|
12979
|
-
estimatedBandwidthKbps: number().min(0).max(1e6)
|
|
15653
|
+
estimatedBandwidthKbps: number().min(0).max(1e6),
|
|
15654
|
+
packetLossPercent: number().min(0).max(100)
|
|
12980
15655
|
}),
|
|
12981
15656
|
_void(),
|
|
12982
15657
|
{ kind: "mutation" }
|
|
@@ -13201,6 +15876,24 @@ const AvailableIntegrationTypeSchema = object({
|
|
|
13201
15876
|
color: string(),
|
|
13202
15877
|
instanceMode: string(),
|
|
13203
15878
|
discoveryMode: string(),
|
|
15879
|
+
/**
|
|
15880
|
+
* Which integration-marker cap the addon declared, so the wizard can
|
|
15881
|
+
* branch on CAP — never on addon name. `device-adoption` integrations
|
|
15882
|
+
* (Home Assistant, …) route through the broker step (Approach A);
|
|
15883
|
+
* `device-provider` integrations (Reolink/Frigate/ONVIF) keep the
|
|
15884
|
+
* legacy config → discovery flow.
|
|
15885
|
+
*/
|
|
15886
|
+
kind: _enum(["device-adoption", "device-provider"]),
|
|
15887
|
+
/**
|
|
15888
|
+
* For `device-adoption` addons: the broker kind to create/link
|
|
15889
|
+
* (e.g. `home-assistant`), declared in the addon manifest. `null` for
|
|
15890
|
+
* `device-provider` addons, which carry no broker.
|
|
15891
|
+
*/
|
|
15892
|
+
brokerKind: string().nullable(),
|
|
15893
|
+
/** True when the integration's source system exposes locations the adoption
|
|
15894
|
+
* flow can import (e.g. HA areas). Drives the adopt modal's "import
|
|
15895
|
+
* locations" checkbox. Provider-declared in the addon manifest. */
|
|
15896
|
+
supportsLocationImport: boolean(),
|
|
13204
15897
|
existingInstances: array(object({ id: string(), name: string() })),
|
|
13205
15898
|
canAdd: boolean()
|
|
13206
15899
|
});
|
|
@@ -13807,10 +16500,15 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13807
16500
|
EventCategory2["DeviceSettingsUpdated"] = "device.settings-updated";
|
|
13808
16501
|
EventCategory2["DeviceBindingsChanged"] = "device.bindings-changed";
|
|
13809
16502
|
EventCategory2["DeviceMetaChanged"] = "device.meta-changed";
|
|
16503
|
+
EventCategory2["DeviceSourceInfoChanged"] = "device.source-info-changed";
|
|
13810
16504
|
EventCategory2["DeviceStreamsRegistered"] = "device.streams-registered";
|
|
16505
|
+
EventCategory2["DeviceProvisioned"] = "device.provisioned";
|
|
16506
|
+
EventCategory2["DeviceReady"] = "device.ready";
|
|
13811
16507
|
EventCategory2["IntegrationEnabled"] = "integration.enabled";
|
|
13812
16508
|
EventCategory2["IntegrationDisabled"] = "integration.disabled";
|
|
13813
16509
|
EventCategory2["IntegrationDeleted"] = "integration.deleted";
|
|
16510
|
+
EventCategory2["BrokerStatusChanged"] = "broker.status-changed";
|
|
16511
|
+
EventCategory2["BrokerMessage"] = "broker.message";
|
|
13814
16512
|
EventCategory2["ProviderStarted"] = "provider.started";
|
|
13815
16513
|
EventCategory2["ProviderStopped"] = "provider.stopped";
|
|
13816
16514
|
EventCategory2["ProcessCrashed"] = "process.crashed";
|
|
@@ -13844,7 +16542,9 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13844
16542
|
EventCategory2["StreamParamsChanged"] = "stream-params.changed";
|
|
13845
16543
|
EventCategory2["DeviceStateChanged"] = "device.state-changed";
|
|
13846
16544
|
EventCategory2["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
16545
|
+
EventCategory2["BatteryOnWakeStarted"] = "battery.onWakeStarted";
|
|
13847
16546
|
EventCategory2["DoorbellOnPressed"] = "doorbell.onPressed";
|
|
16547
|
+
EventCategory2["EventEmitted"] = "event-emitter.event";
|
|
13848
16548
|
EventCategory2["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
|
|
13849
16549
|
EventCategory2["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
|
|
13850
16550
|
EventCategory2["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
|
|
@@ -13927,6 +16627,27 @@ function emitReadiness(bus, params) {
|
|
|
13927
16627
|
}
|
|
13928
16628
|
));
|
|
13929
16629
|
}
|
|
16630
|
+
function createDurableState(deps) {
|
|
16631
|
+
const get = async () => {
|
|
16632
|
+
const store = await deps.read();
|
|
16633
|
+
const raw = store[deps.key];
|
|
16634
|
+
if (raw === void 0) return deps.fallback;
|
|
16635
|
+
const parsed = deps.schema.safeParse(raw);
|
|
16636
|
+
if (!parsed.success) {
|
|
16637
|
+
deps.onParseError?.(deps.key, parsed.error);
|
|
16638
|
+
return deps.fallback;
|
|
16639
|
+
}
|
|
16640
|
+
return parsed.data;
|
|
16641
|
+
};
|
|
16642
|
+
const set = async (next) => {
|
|
16643
|
+
const validated = deps.schema.parse(next);
|
|
16644
|
+
await deps.write({ [deps.key]: validated });
|
|
16645
|
+
};
|
|
16646
|
+
const update = async (fn2) => {
|
|
16647
|
+
await set(fn2(await get()));
|
|
16648
|
+
};
|
|
16649
|
+
return { get, set, update };
|
|
16650
|
+
}
|
|
13930
16651
|
class BaseAddon {
|
|
13931
16652
|
_ctx = null;
|
|
13932
16653
|
_config;
|
|
@@ -14170,12 +16891,18 @@ class BaseAddon {
|
|
|
14170
16891
|
// ── Lifecycle event emission ────────────────────────────────────────────
|
|
14171
16892
|
emitLifecycle(category, data) {
|
|
14172
16893
|
try {
|
|
14173
|
-
this._ctx
|
|
14174
|
-
|
|
16894
|
+
const ctx = this._ctx;
|
|
16895
|
+
if (!ctx) return;
|
|
16896
|
+
ctx.eventBus.emit({
|
|
16897
|
+
id: `${ctx.id}-${Date.now()}`,
|
|
14175
16898
|
timestamp: /* @__PURE__ */ new Date(),
|
|
14176
|
-
source: { type: "addon", id:
|
|
16899
|
+
source: { type: "addon", id: ctx.id, nodeId: ctx.kernel.localNodeId ?? "hub" },
|
|
14177
16900
|
category,
|
|
14178
|
-
|
|
16901
|
+
// Always carry `addonId` in the payload (the lifecycle event
|
|
16902
|
+
// contract declares it) so consumers — e.g. the alerts builder —
|
|
16903
|
+
// can name the addon without reaching into `source`. An explicit
|
|
16904
|
+
// `data.addonId` still wins if a caller provides one.
|
|
16905
|
+
data: { addonId: ctx.id, ...data ?? {} }
|
|
14179
16906
|
});
|
|
14180
16907
|
} catch {
|
|
14181
16908
|
}
|
|
@@ -14248,6 +16975,44 @@ class BaseAddon {
|
|
|
14248
16975
|
}
|
|
14249
16976
|
this._config = resolved;
|
|
14250
16977
|
}
|
|
16978
|
+
/**
|
|
16979
|
+
* Typed durable handle over ONE key of this addon's store. The whole
|
|
16980
|
+
* Zod-validated value round-trips on every read/write — no hand-listed
|
|
16981
|
+
* fields, so a field can never be silently dropped on persist. Reads use
|
|
16982
|
+
* the same retry budget as config resolution; a corrupt/legacy blob logs
|
|
16983
|
+
* a warning and falls back rather than crashing boot.
|
|
16984
|
+
*/
|
|
16985
|
+
state(key, schema, fallback) {
|
|
16986
|
+
return createDurableState({
|
|
16987
|
+
key,
|
|
16988
|
+
schema,
|
|
16989
|
+
fallback,
|
|
16990
|
+
read: () => this.readAddonStoreWithRetry(),
|
|
16991
|
+
write: async (patch) => {
|
|
16992
|
+
await this._ctx?.settings?.writeAddonStore(patch);
|
|
16993
|
+
},
|
|
16994
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(
|
|
16995
|
+
`durable-state: stored "${k}" failed validation — using fallback`,
|
|
16996
|
+
{ meta: { addonId: this._ctx?.id, key: k, error: String(e) } }
|
|
16997
|
+
)
|
|
16998
|
+
});
|
|
16999
|
+
}
|
|
17000
|
+
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
17001
|
+
deviceState(deviceId, key, schema, fallback) {
|
|
17002
|
+
return createDurableState({
|
|
17003
|
+
key,
|
|
17004
|
+
schema,
|
|
17005
|
+
fallback,
|
|
17006
|
+
read: async () => await this._ctx?.settings?.readDeviceStore(deviceId) ?? {},
|
|
17007
|
+
write: async (patch) => {
|
|
17008
|
+
await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
|
|
17009
|
+
},
|
|
17010
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(
|
|
17011
|
+
`durable-state: stored device ${deviceId} "${k}" failed validation — using fallback`,
|
|
17012
|
+
{ meta: { addonId: this._ctx?.id, deviceId, key: k, error: String(e) } }
|
|
17013
|
+
)
|
|
17014
|
+
});
|
|
17015
|
+
}
|
|
14251
17016
|
/**
|
|
14252
17017
|
* Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
|
|
14253
17018
|
* transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
|
|
@@ -14336,8 +17101,7 @@ exports.hydrateSchema = hydrateSchema;
|
|
|
14336
17101
|
exports.number = number;
|
|
14337
17102
|
exports.object = object;
|
|
14338
17103
|
exports.pipelineAnalyticsCapability = pipelineAnalyticsCapability;
|
|
14339
|
-
exports.recordingEngineCapability = recordingEngineCapability;
|
|
14340
17104
|
exports.string = string;
|
|
14341
17105
|
exports.tuple = tuple;
|
|
14342
17106
|
exports.zoneAnalyticsCapability = zoneAnalyticsCapability;
|
|
14343
|
-
//# sourceMappingURL=index-
|
|
17107
|
+
//# sourceMappingURL=index-B0RhVv1c.js.map
|